Merge pull request #2690 from strapi/design/marketplace

Redesign marketplace and add new blocker component
This commit is contained in:
Jim LAURIE 2019-01-30 14:35:55 +01:00 committed by GitHub
commit c72d5698d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 545 additions and 762 deletions

View File

@ -23,7 +23,7 @@ const dispatch = store.dispatch;
// Don't inject plugins in development mode. // Don't inject plugins in development mode.
if (window.location.port !== '4000') { if (window.location.port !== '4000') {
fetch(`${strapi.remoteURL}/config/plugins.json`) fetch(`${strapi.remoteURL}/config/plugins.json`, { cache: 'no-cache' })
.then(response => { .then(response => {
return response.json(); return response.json();
}) })

View File

@ -110,7 +110,7 @@ function LeftMenuLinkContainer({ layout, plugins }) {
<LeftMenuLink <LeftMenuLink
icon="shopping-basket" icon="shopping-basket"
label={messages.installNewPlugin.id} label={messages.installNewPlugin.id}
destination="/install-plugin" destination="/marketplace"
/> />
{hasSettingsManager && ( {hasSettingsManager && (
<LeftMenuLink <LeftMenuLink

View File

@ -10,32 +10,25 @@ import cn from 'classnames';
import { isEmpty, replace } from 'lodash'; import { isEmpty, replace } from 'lodash';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
// Temporary picture
import Button from 'components/Button'; import Button from 'components/Button';
import InstallPluginPopup from 'components/InstallPluginPopup'; import InstallPluginPopup from 'components/InstallPluginPopup';
import Official from 'components/Official';
// import StarsContainer from 'components/StarsContainer';
import logoTShirt from 'assets/images/logo-t-shirt.svg';
import styles from './styles.scss'; import styles from './styles.scss';
import Screenshot from './screenshot.png';
const PLUGINS_WITH_CONFIG = ['content-manager', 'email', 'upload'];
/* eslint-disable react/no-unused-state */ /* eslint-disable react/no-unused-state */
class PluginCard extends React.Component { class PluginCard extends React.Component {
state = { isOpen: false, boostrapCol: 'col-lg-4' }; state = {
boostrapCol: 'col-lg-4',
};
componentDidMount() { componentDidMount() {
this.shouldOpenModal(this.props); // Listen window resize.
window.addEventListener('resize', this.setBoostrapCol); window.addEventListener('resize', this.setBoostrapCol);
this.setBoostrapCol(); this.setBoostrapCol();
} }
componentWillReceiveProps(nextProps) {
if (nextProps.history.location.hash !== this.props.history.location.hash) {
this.shouldOpenModal(nextProps);
}
}
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener('resize', this.setBoostrapCol); window.removeEventListener('resize', this.setBoostrapCol);
} }
@ -65,6 +58,15 @@ class PluginCard extends React.Component {
} }
} }
handleClickSettings = (e) => {
const settingsPath = this.props.plugin.id === 'content-manager' ? '/plugins/content-manager/ctm-configurations' : `/plugins/${this.props.plugin.id}/configurations/${this.props.currentEnvironment}`;
e.preventDefault();
e.stopPropagation();
this.props.history.push(settingsPath);
}
handleDownloadPlugin = (e) => { handleDownloadPlugin = (e) => {
if (!this.props.isAlreadyInstalled && this.props.plugin.id !== 'support-us') { if (!this.props.isAlreadyInstalled && this.props.plugin.id !== 'support-us') {
this.props.downloadPlugin(e); this.props.downloadPlugin(e);
@ -75,23 +77,15 @@ class PluginCard extends React.Component {
} }
} }
shouldOpenModal = (props) => {
this.setState({ isOpen: !isEmpty(props.history.location.hash) });
}
render() { render() {
const buttonClass = !this.props.isAlreadyInstalled || this.props.showSupportUsButton ? styles.primary : styles.secondary; const buttonClass = !this.props.isAlreadyInstalled ? styles.primary : styles.secondary;
const buttonLabel = this.props.isAlreadyInstalled ? 'app.components.PluginCard.Button.label.install' : 'app.components.PluginCard.Button.label.download';
let buttonLabel = this.props.isAlreadyInstalled ? 'app.components.PluginCard.Button.label.install' : 'app.components.PluginCard.Button.label.download'; // Display settings link for a selection of plugins.
const settingsComponent = (PLUGINS_WITH_CONFIG.includes(this.props.plugin.id) &&
if (this.props.showSupportUsButton) { <div className={styles.settings} onClick={this.handleClickSettings}>
buttonLabel = 'app.components.PluginCard.Button.label.support'; <i className='fa fa-cog' />
} <FormattedMessage id='app.components.PluginCard.settings' />
const pluginIcon = (
<div className={styles.frame}>
<span className={styles.helper} />
<img src={`${this.props.plugin.id === 'support-us' ? logoTShirt : this.props.plugin.logo}`} alt="icon" />
</div> </div>
); );
@ -101,37 +95,21 @@ class PluginCard extends React.Component {
}; };
return ( return (
<div className={cn(this.state.boostrapCol, styles.pluginCard)} onClick={this.handleClick}> <div className={cn(this.state.boostrapCol, styles.pluginCard)}>
<div className={styles.wrapper}> <div className={styles.wrapper}>
<div className={styles.cardTitle}> <div className={styles.cardTitle}>
{pluginIcon} <div className={styles.frame}>
<div>{this.props.plugin.name}</div> <span className={styles.helper} />
<img src={this.props.plugin.logo} alt="icon" />
</div>
<div>{this.props.plugin.name} <i className='fa fa-external-link' onClick={() => window.open(`https://github.com/strapi/strapi/tree/master/packages/strapi-plugin-${this.props.plugin.id}`, '_blank')} /></div>
</div> </div>
<div className={styles.cardDescription}> <div className={styles.cardDescription}>
{descriptions.short} {descriptions.long}
&nbsp;<FormattedMessage id="app.components.PluginCard.more-details" /> {/* &nbsp;<FormattedMessage id="app.components.PluginCard.more-details" /> */}
</div>
<div className={styles.cardScreenshot} style={{ backgroundImage: `url(${Screenshot})` }}>
</div>
<div className={styles.cardPrice}>
<div>
<i className={`fa fa-${this.props.plugin.isCompatible ? 'check' : 'times'}`} />
<FormattedMessage id={`app.components.PluginCard.compatible${this.props.plugin.id === 'support-us' ? 'Community' : ''}`} />
</div>
<div>{this.props.plugin.price !== 0 ? `${this.props.plugin.price}` : ''}</div>
</div> </div>
<div className={styles.cardFooter} onClick={e => e.stopPropagation()}> <div className={styles.cardFooter} onClick={e => e.stopPropagation()}>
<div className={styles.ratings}> <div className={styles.cardFooterButton}>
{/*<StarsContainer ratings={this.props.plugin.ratings} />
<div>
<span style={{ fontWeight: '600', color: '#333740' }}>{this.props.plugin.ratings}</span>
<span style={{ fontWeight: '500', color: '#666666' }}>/5</span>
</div>
*/}
<Official />
</div>
<div>
<Button <Button
className={cn(buttonClass, styles.button)} className={cn(buttonClass, styles.button)}
label={buttonLabel} label={buttonLabel}
@ -146,6 +124,18 @@ class PluginCard extends React.Component {
&nbsp; &nbsp;
</a> </a>
</div> </div>
{this.props.isAlreadyInstalled ?
(
settingsComponent
)
:
(
<div className={styles.compatible}>
<i className={`fa fa-${this.props.plugin.isCompatible ? 'check' : 'times'}`} />
<FormattedMessage id={`app.components.PluginCard.compatible${this.props.plugin.id === 'support-us' ? 'Community' : ''}`} />
</div>
)
}
</div> </div>
</div> </div>
<InstallPluginPopup <InstallPluginPopup
@ -168,15 +158,14 @@ PluginCard.defaultProps = {
price: 0, price: 0,
ratings: 5, ratings: 5,
}, },
showSupportUsButton: false,
}; };
PluginCard.propTypes = { PluginCard.propTypes = {
currentEnvironment: PropTypes.string.isRequired,
downloadPlugin: PropTypes.func.isRequired, downloadPlugin: PropTypes.func.isRequired,
history: PropTypes.object.isRequired, history: PropTypes.object.isRequired,
isAlreadyInstalled: PropTypes.bool, isAlreadyInstalled: PropTypes.bool,
plugin: PropTypes.object, plugin: PropTypes.object,
showSupportUsButton: PropTypes.bool,
}; };
export default PluginCard; export default PluginCard;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -1,14 +1,37 @@
.button { .wrapper {
height: 26px; position: relative;
min-width: 89px !important; min-height: 216px;
padding-top: 2px; margin-bottom: 3.6rem;
padding-left: 15px; padding: 1.2rem 1.5rem;
padding-right: 15px; padding-bottom: 0;
margin: 0; background-color: #fff;
border-radius: 2px !important; box-shadow: 0 2px 4px #E3E9F3;
-webkit-font-smoothing: antialiased;
}
.cardTitle {
display: flex;
font-size: 13px; font-size: 13px;
font-weight: 500 !important; font-weight: 600;
cursor: pointer; text-transform: uppercase;
letter-spacing: 0.5px;
> div:first-child {
margin-right: 14px;
}
> div:last-child {
height: 36px;
line-height: 36px;
}
i {
margin-left: 7px;
color: #B3B5B9;
font-size: 1rem;
vertical-align: baseline;
cursor: pointer;
}
} }
.cardDescription { .cardDescription {
@ -25,62 +48,60 @@
} }
.cardFooter { .cardFooter {
cursor: initial; position: absolute;
bottom: 0; left: 0;
display: flex; display: flex;
justify-content: space-between;
height: 54px;
margin-left: -1.5rem;
margin-right: -1.5rem;
padding: 1.5rem 1.5rem 0 1.5rem;
background-color: #FAFAFB;
&>div:first-of-type {
margin-top: 3px;
}
}
.cardPrice {
display: flex;
justify-content: space-between;
width: 100%; width: 100%;
margin-bottom: 10px; height: 45px;
padding-top: 2rem; padding: 0.9rem 1.5rem 1rem;
> div:first-child { background-color: #FAFAFB;
> i { justify-content: space-between;
margin-right: 10px; flex-direction: row-reverse;
} cursor: initial;
color: #5A9E06; }
.compatible {
margin-top: 3px;
color: #5A9E06;
font-size: 1.3rem;
font-style: italic;
> i {
margin-right: 7px;
font-size: 12px; font-size: 12px;
font-style: italic;
line-height: 2.1rem;
}
> div:last-child {
font-size: 16px;
font-weight: 700;
line-height: 1.8rem;
} }
} }
.cardScreenshot { .settings{
height: 78px; margin-top: 3px;
border-radius: 2px; color: #323740;
background-size: cover; font-size: 1.3rem;
font-weight: 500;
cursor: pointer;
> i {
margin-right: 7px;
font-size: 12px;
}
} }
.cardTitle { .button {
display: flex; height: 26px;
min-width: 89px !important;
padding: 0 15px;
margin: 0;
border-radius: 2px !important;
line-height: 24px;
font-size: 13px; font-size: 13px;
font-weight: 700; font-weight: 500 !important;
text-transform: uppercase; cursor: pointer;
letter-spacing: 0.5px;
> div:first-child {
margin-right: 14px;
}
> div:last-child { span {
height: 36px; display: inline-block;
line-height: 36px; width: 100%;
height: 100%;
line-height: 24px;
} }
} }
@ -89,7 +110,6 @@
height: 36px; height: 36px;
margin: auto 0; margin: auto 0;
text-align: center; text-align: center;
background: #FAFAFB;
border: 1px solid #F3F3F7; border: 1px solid #F3F3F7;
border-radius: 3px; border-radius: 3px;
white-space: nowrap; white-space: nowrap;
@ -99,18 +119,6 @@
} }
} }
.iconContainer {
height: 36px;
width: 70px;
margin-right: 14px;
background: #FAFAFB;
border: 1px solid #F3F3F7;
border-radius: 3px;
text-align: center;
font-size: 20px;
}
.helper { .helper {
display: inline-block; display: inline-block;
height: 100%; height: 100%;
@ -118,58 +126,21 @@
} }
.pluginCard { .pluginCard {
} }
.primary { .primary {
font-weight: 500;
background: linear-gradient(315deg, #0097F6 0%, #005EEA 100%); background: linear-gradient(315deg, #0097F6 0%, #005EEA 100%);
-webkit-font-smoothing: antialiased;
color: white; color: white;
font-weight: 500;
-webkit-font-smoothing: antialiased;
&:active { &:active {
box-shadow: inset 1px 1px 3px rgba(0,0,0,.15); box-shadow: inset 1px 1px 3px rgba(0,0,0,.15);
} }
} }
.ratings {
// display: flex;
// padding-top: 1px;
// TODO uncomment when ratings
// > div:last-child {
// padding-top: 4px;
// font-size: 12px;
// font-style: italic;
// }
}
.secondary { .secondary {
border: 1px solid #DFE0E1; border: 1px solid #DFE0E1;
font-weight: 600; font-weight: 600;
} }
.starsContainer {
display: flex;
margin-right: 10px;
> div {
> i {
margin-right: 2px;
}
}
> div:first-child {
color: #EEA348;
}
> div:last-child {
color: #B3B5B9;
}
}
.wrapper {
min-height: 320px;
margin-bottom: 3.9rem;
padding: 1.2rem 1.5rem;
padding-bottom: 0;
background-color: #fff;
box-shadow: 0 2px 4px #E3E9F3;
-webkit-font-smoothing: antialiased;
cursor: pointer;
}

View File

@ -1,8 +0,0 @@
import Loadable from 'react-loadable';
import LoadingIndicator from 'components/LoadingIndicator';
export default Loadable({
loader: () => import('./index'),
loading: LoadingIndicator,
});

View File

@ -1,32 +0,0 @@
/**
*
* SupportUsBanner
*
*/
import React from 'react';
import { FormattedMessage } from 'react-intl';
import SupportUsTitle from 'components/SupportUsTitle';
import SupportUsCta from 'components/SupportUsCta';
import styles from './styles.scss';
function SupportUsBanner() {
return (
<div className={styles.supportUsBanner}>
<div>
<div>
<SupportUsTitle />
<FormattedMessage id="app.components.HomePage.support.content">
{message => <p>{message}</p>}
</FormattedMessage>
</div>
<div>
<SupportUsCta />
</div>
</div>
</div>
);
}
export default SupportUsBanner;

View File

@ -1,59 +0,0 @@
.supportUsBanner {
position: relative;
height: 135px;
margin-top: 2px;
margin-bottom: 43px;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.20);
line-height: 18px;
> div {
padding: 38px 50px 0 50px;
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
display: flex;
justify-content: space-between;
border-radius: 2px;
z-index: 999;
color: #FFFFFF;
> div {
> p {
max-width: 50rem;
margin-top: 17px;
margin-bottom: 125px;
padding-right: 35px;
font-size: 13px;
font-weight: 400;
}
}
> div:last-child {
margin-right: 260px;
padding-top: 20px;
}
}
&:before {
content: '';
border-radius: 2px;
background-image: linear-gradient(45deg, #1A67DA 0%, #0097F6 100%) !important;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
&:after {
opacity: 0.8;
content: '';
border-radius: 2px;
background-image: url('../../assets/images/banner_t-shirt.png') !important;
background-size: contain;
background-repeat: no-repeat;
background-position: right;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
}

File diff suppressed because one or more lines are too long

View File

@ -23,6 +23,7 @@ import { pluginLoaded, updatePlugin } from 'containers/App/actions';
import { import {
makeSelectAppPlugins, makeSelectAppPlugins,
makeSelectBlockApp, makeSelectBlockApp,
makeSelectOverlayBlockerProps,
makeSelectIsAppLoading, makeSelectIsAppLoading,
makeSelectShowGlobalAppBlocker, makeSelectShowGlobalAppBlocker,
selectHasUserPlugin, selectHasUserPlugin,
@ -35,7 +36,7 @@ import LocaleToggle from 'containers/LocaleToggle';
import CTAWrapper from 'components/CtaWrapper'; import CTAWrapper from 'components/CtaWrapper';
import Header from 'components/Header/index'; import Header from 'components/Header/index';
import HomePage from 'containers/HomePage/Loadable'; import HomePage from 'containers/HomePage/Loadable';
import InstallPluginPage from 'containers/InstallPluginPage/Loadable'; import Marketplace from 'containers/Marketplace/Loadable';
import LeftMenu from 'containers/LeftMenu'; import LeftMenu from 'containers/LeftMenu';
import ListPluginsPage from 'containers/ListPluginsPage/Loadable'; import ListPluginsPage from 'containers/ListPluginsPage/Loadable';
import LoadingIndicatorPage from 'components/LoadingIndicatorPage'; import LoadingIndicatorPage from 'components/LoadingIndicatorPage';
@ -188,6 +189,8 @@ export class AdminPage extends React.Component {
return plugins; return plugins;
}; };
renderMarketPlace = props => <Marketplace {...props} {...this.props} />;
render() { render() {
const { adminPage } = this.props; const { adminPage } = this.props;
const header = this.showLeftMenu() ? <Header /> : ''; const header = this.showLeftMenu() ? <Header /> : '';
@ -219,14 +222,17 @@ export class AdminPage extends React.Component {
<Route path="/plugins/:pluginId" component={PluginPage} /> <Route path="/plugins/:pluginId" component={PluginPage} />
<Route path="/plugins" component={ComingSoonPage} /> <Route path="/plugins" component={ComingSoonPage} />
<Route path="/list-plugins" component={ListPluginsPage} exact /> <Route path="/list-plugins" component={ListPluginsPage} exact />
<Route path="/install-plugin" component={InstallPluginPage} exact /> <Route path="/marketplace" render={this.renderMarketPlace} exact />
<Route path="/configuration" component={ComingSoonPage} exact /> <Route path="/configuration" component={ComingSoonPage} exact />
<Route path="" component={NotFoundPage} /> <Route path="" component={NotFoundPage} />
<Route path="404" component={NotFoundPage} /> <Route path="404" component={NotFoundPage} />
</Switch> </Switch>
</Content> </Content>
</div> </div>
<OverlayBlocker isOpen={this.props.blockApp && this.props.showGlobalAppBlocker} /> <OverlayBlocker
isOpen={this.props.blockApp && this.props.showGlobalAppBlocker}
{...this.props.overlayBlockerData}
/>
</div> </div>
); );
} }
@ -248,6 +254,7 @@ AdminPage.defaultProps = {
appPlugins: [], appPlugins: [],
hasUserPlugin: true, hasUserPlugin: true,
isAppLoading: true, isAppLoading: true,
overlayBlockerData: {},
}; };
AdminPage.propTypes = { AdminPage.propTypes = {
@ -261,6 +268,7 @@ AdminPage.propTypes = {
history: PropTypes.object.isRequired, history: PropTypes.object.isRequired,
isAppLoading: PropTypes.bool, isAppLoading: PropTypes.bool,
location: PropTypes.object.isRequired, location: PropTypes.object.isRequired,
overlayBlockerData: PropTypes.object,
pluginLoaded: PropTypes.func.isRequired, pluginLoaded: PropTypes.func.isRequired,
plugins: PropTypes.object.isRequired, plugins: PropTypes.object.isRequired,
showGlobalAppBlocker: PropTypes.bool.isRequired, showGlobalAppBlocker: PropTypes.bool.isRequired,
@ -271,6 +279,7 @@ const mapStateToProps = createStructuredSelector({
adminPage: selectAdminPage(), adminPage: selectAdminPage(),
appPlugins: makeSelectAppPlugins(), appPlugins: makeSelectAppPlugins(),
blockApp: makeSelectBlockApp(), blockApp: makeSelectBlockApp(),
overlayBlockerData: makeSelectOverlayBlockerProps(),
hasUserPlugin: selectHasUserPlugin(), hasUserPlugin: selectHasUserPlugin(),
isAppLoading: makeSelectIsAppLoading(), isAppLoading: makeSelectIsAppLoading(),
plugins: selectPlugins(), plugins: selectPlugins(),
@ -295,4 +304,3 @@ const withReducer = injectReducer({ key: 'adminPage', reducer });
const withSaga = injectSaga({ key: 'adminPage', saga }); const withSaga = injectSaga({ key: 'adminPage', saga });
export default compose(withReducer, withSaga, withConnect)(AdminPage); export default compose(withReducer, withSaga, withConnect)(AdminPage);

View File

@ -3,6 +3,7 @@
.adminPage { /* stylelint-disable */ .adminPage { /* stylelint-disable */
display: flex; display: flex;
overflow-x: hidden;
} }
.adminPageRightWrapper { .adminPageRightWrapper {

View File

@ -15,9 +15,10 @@ import {
UPDATE_PLUGIN, UPDATE_PLUGIN,
} from './constants'; } from './constants';
export function freezeApp() { export function freezeApp(data) {
return { return {
type: FREEZE_APP, type: FREEZE_APP,
data,
}; };
} }

View File

@ -19,6 +19,7 @@ import {
const initialState = fromJS({ const initialState = fromJS({
appPlugins: List([]), appPlugins: List([]),
blockApp: false, blockApp: false,
overlayBlockerData: null,
hasUserPlugin: true, hasUserPlugin: true,
isAppLoading: true, isAppLoading: true,
plugins: {}, plugins: {},
@ -32,7 +33,15 @@ function appReducer(state = initialState, action) {
case ENABLE_GLOBAL_OVERLAY_BLOCKER: case ENABLE_GLOBAL_OVERLAY_BLOCKER:
return state.set('showGlobalAppBlocker', true); return state.set('showGlobalAppBlocker', true);
case FREEZE_APP: case FREEZE_APP:
return state.set('blockApp', true); return state
.set('blockApp', true)
.update('overlayBlockerData', () => {
if (action.data) {
return action.data;
}
return null;
});
case GET_APP_PLUGINS_SUCCEEDED: case GET_APP_PLUGINS_SUCCEEDED:
return state return state
.update('appPlugins', () => List(action.appPlugins)) .update('appPlugins', () => List(action.appPlugins))
@ -44,7 +53,9 @@ function appReducer(state = initialState, action) {
case PLUGIN_DELETED: case PLUGIN_DELETED:
return state.deleteIn(['plugins', action.plugin]); return state.deleteIn(['plugins', action.plugin]);
case UNFREEZE_APP: case UNFREEZE_APP:
return state.set('blockApp', false); return state
.set('blockApp', false)
.set('overlayBlockerData', null);
case UNSET_HAS_USERS_PLUGIN: case UNSET_HAS_USERS_PLUGIN:
return state.set('hasUserPlugin', false); return state.set('hasUserPlugin', false);
default: default:

View File

@ -34,6 +34,11 @@ const makeSelectBlockApp = () => createSelector(
(appState) => appState.get('blockApp'), (appState) => appState.get('blockApp'),
); );
const makeSelectOverlayBlockerProps = () => createSelector(
selectApp(),
(appState) => appState.get('overlayBlockerData'),
);
const makeSelectIsAppLoading = () => createSelector( const makeSelectIsAppLoading = () => createSelector(
selectApp(), selectApp(),
appState => appState.get('isAppLoading'), appState => appState.get('isAppLoading'),
@ -50,6 +55,7 @@ export {
selectPlugins, selectPlugins,
makeSelectAppPlugins, makeSelectAppPlugins,
makeSelectBlockApp, makeSelectBlockApp,
makeSelectOverlayBlockerProps,
makeSelectIsAppLoading, makeSelectIsAppLoading,
makeSelectShowGlobalAppBlocker, makeSelectShowGlobalAppBlocker,
}; };

View File

@ -1,69 +0,0 @@
/*
*
* InstallPluginPage actions
*
*/
import {
DOWNLOAD_PLUGIN,
DOWNLOAD_PLUGIN_ERROR,
DOWNLOAD_PLUGIN_SUCCEEDED,
GET_AVAILABLE_PLUGINS,
GET_AVAILABLE_PLUGINS_SUCCEEDED,
GET_INSTALLED_PLUGINS,
GET_INSTALLED_PLUGINS_SUCCEEDED,
ON_CHANGE,
} from './constants';
export function downloadPlugin(pluginToDownload) {
return {
type: DOWNLOAD_PLUGIN,
pluginToDownload,
};
}
export function downloadPluginError() {
return {
type: DOWNLOAD_PLUGIN_ERROR,
};
}
export function downloadPluginSucceeded() {
return {
type: DOWNLOAD_PLUGIN_SUCCEEDED,
};
}
export function getAvailablePlugins() {
return {
type: GET_AVAILABLE_PLUGINS,
};
}
export function getAvailablePluginsSucceeded(availablePlugins) {
return {
type: GET_AVAILABLE_PLUGINS_SUCCEEDED,
availablePlugins,
};
}
export function getInstalledPlugins() {
return {
type: GET_INSTALLED_PLUGINS,
};
}
export function getInstalledPluginsSucceeded(installedPlugins) {
return {
type: GET_INSTALLED_PLUGINS_SUCCEEDED,
installedPlugins,
};
}
export function onChange({ target }) {
return {
type: ON_CHANGE,
keys: target.name.split('.'),
value: target.value,
};
}

View File

@ -1,14 +0,0 @@
/*
*
* InstallPluginPage constants
*
*/
export const DOWNLOAD_PLUGIN = 'StrapiAdmin/InstallPluginPage/DOWNLOAD_PLUGIN';
export const DOWNLOAD_PLUGIN_ERROR = 'StrapiAdmin/InstallPluginPage/DOWNLOAD_PLUGIN_ERROR';
export const DOWNLOAD_PLUGIN_SUCCEEDED = 'StrapiAdmin/InstallPluginPage/DOWNLOAD_PLUGIN_SUCCEEDED';
export const GET_AVAILABLE_PLUGINS = 'StrapiAdmin/InstallPluginPage/GET_AVAILABLE_PLUGINS';
export const GET_AVAILABLE_PLUGINS_SUCCEEDED = 'StrapiAdmin/InstallPluginPage/GET_AVAILABLE_PLUGINS_SUCCEEDED';
export const GET_INSTALLED_PLUGINS = 'StrapiAdmin/InstallPluginPage/GET_INSTALLED_PLUGINS';
export const GET_INSTALLED_PLUGINS_SUCCEEDED = 'StrapiAdmin/InstallPluginPage/GET_INSTALLED_PLUGINS_SUCCEEDED';
export const ON_CHANGE = 'StrapiAdmin/InstallPluginPage/ON_CHANGE';

View File

@ -1,189 +0,0 @@
/**
*
* InstallPluginPage
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Helmet } from 'react-helmet';
import { FormattedMessage } from 'react-intl';
import { bindActionCreators, compose } from 'redux';
import cn from 'classnames';
import { map } from 'lodash';
import {
disableGlobalOverlayBlocker,
enableGlobalOverlayBlocker,
} from 'actions/overlayBlocker';
// Design
// import Input from 'components/Input';
import DownloadInfo from 'components/DownloadInfo';
import OverlayBlocker from 'components/OverlayBlocker';
import PluginCard from 'components/PluginCard';
import PluginHeader from 'components/PluginHeader';
import SupportUsBanner from 'components/SupportUsBanner';
import LoadingIndicatorPage from 'components/LoadingIndicatorPage';
import injectSaga from 'utils/injectSaga';
import injectReducer from 'utils/injectReducer';
import {
downloadPlugin,
getAvailablePlugins,
getInstalledPlugins,
onChange,
} from './actions';
import makeSelectInstallPluginPage from './selectors';
import reducer from './reducer';
import saga from './saga';
import styles from './styles.scss';
export class InstallPluginPage extends React.Component { // eslint-disable-line react/prefer-stateless-function
getChildContext = () => (
{
downloadPlugin: this.props.downloadPlugin,
}
);
componentDidMount() {
// Disable the AdminPage OverlayBlocker in order to give it a custom design (children)
this.props.disableGlobalOverlayBlocker();
// Don't fetch the available plugins if it has already been done
if (!this.props.didFetchPlugins) {
this.props.getAvailablePlugins();
}
// Get installed plugins
this.props.getInstalledPlugins();
}
componentWillUnmount() {
// Enable the AdminPage OverlayBlocker so it is displayed when the server is restarting
this.props.enableGlobalOverlayBlocker();
}
render() {
if (!this.props.didFetchPlugins || !this.props.didFetchInstalledPlugins) {
return <LoadingIndicatorPage />;
}
return (
<div>
<OverlayBlocker isOpen={this.props.blockApp}>
<DownloadInfo />
</OverlayBlocker>
<FormattedMessage id="app.components.InstallPluginPage.helmet">
{message => (
<Helmet>
<title>{message}</title>
<meta name="description" content="Description of InstallPluginPage" />
</Helmet>
)}
</FormattedMessage>
<div className={cn('container-fluid', styles.containerFluid)}>
<PluginHeader
title={{ id: 'app.components.InstallPluginPage.title' }}
description={{ id: 'app.components.InstallPluginPage.description' }}
actions={[]}
/>
<div className="row">
<div className="col-md-12 col-lg-12">
<SupportUsBanner />
</div>
</div>
{/*}<div className={cn('row', styles.inputContainer)}>
<Input
customBootstrapClass="col-md-12"
label="app.components.InstallPluginPage.InputSearch.label"
name="search"
onChange={this.props.onChange}
placeholder="app.components.InstallPluginPage.InputSearch.placeholder"
type="search"
validations={{}}
value={this.props.search}
/>
</div>*/}
<div className={cn('row', styles.wrapper)}>
{map(this.props.availablePlugins, (plugin) => (
<PluginCard
history={this.props.history}
key={plugin.id}
plugin={plugin}
showSupportUsButton={plugin.id === 'support-us'}
isAlreadyInstalled={this.props.installedPlugins.includes(plugin.id)}
downloadPlugin={(e) => {
e.preventDefault();
e.stopPropagation();
if (plugin.id !== 'support-us') {
this.props.downloadPlugin(plugin.id);
}
}}
/>
))}
</div>
</div>
</div>
);
}
}
InstallPluginPage.childContextTypes = {
downloadPlugin: PropTypes.func.isRequired,
};
InstallPluginPage.propTypes = {
availablePlugins: PropTypes.array.isRequired,
blockApp: PropTypes.bool.isRequired,
didFetchInstalledPlugins: PropTypes.bool.isRequired,
didFetchPlugins: PropTypes.bool.isRequired,
disableGlobalOverlayBlocker: PropTypes.func.isRequired,
downloadPlugin: PropTypes.func.isRequired,
enableGlobalOverlayBlocker: PropTypes.func.isRequired,
getAvailablePlugins: PropTypes.func.isRequired,
getInstalledPlugins: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
installedPlugins: PropTypes.array.isRequired,
// onChange: PropTypes.func.isRequired,
// search: PropTypes.string.isRequired,
};
const mapStateToProps = makeSelectInstallPluginPage();
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
disableGlobalOverlayBlocker,
downloadPlugin,
enableGlobalOverlayBlocker,
getAvailablePlugins,
getInstalledPlugins,
onChange,
},
dispatch,
);
}
const withConnect = connect(mapStateToProps, mapDispatchToProps);
/* Remove this line if the container doesn't have a route and
* check the documentation to see how to create the container's store
*/
const withReducer = injectReducer({ key: 'installPluginPage', reducer });
/* Remove the line below the container doesn't have a route and
* check the documentation to see how to create the container's store
*/
const withSaga = injectSaga({ key: 'installPluginPage', saga });
export default compose(
withReducer,
withSaga,
withConnect,
)(InstallPluginPage);

View File

@ -1,56 +0,0 @@
/*
*
* InstallPluginPage reducer
*
*/
import { fromJS, List } from 'immutable';
import {
DOWNLOAD_PLUGIN,
DOWNLOAD_PLUGIN_ERROR,
DOWNLOAD_PLUGIN_SUCCEEDED,
GET_AVAILABLE_PLUGINS_SUCCEEDED,
GET_INSTALLED_PLUGINS_SUCCEEDED,
ON_CHANGE,
} from './constants';
const initialState = fromJS({
availablePlugins: List([]),
installedPlugins: List([]),
blockApp: false,
didFetchPlugins: false,
didFetchInstalledPlugins: false,
pluginToDownload: '',
search: '',
});
function installPluginPageReducer(state = initialState, action) {
switch (action.type) {
case DOWNLOAD_PLUGIN:
return state
.set('blockApp', true)
.set('pluginToDownload', action.pluginToDownload);
case DOWNLOAD_PLUGIN_ERROR:
return state
.set('blockApp', false)
.set('pluginToDownload', '');
case DOWNLOAD_PLUGIN_SUCCEEDED:
return state
.set('blockApp', false)
.set('pluginToDownload', '');
case GET_AVAILABLE_PLUGINS_SUCCEEDED:
return state
.set('didFetchPlugins', true)
.set('availablePlugins', List(action.availablePlugins));
case GET_INSTALLED_PLUGINS_SUCCEEDED:
return state
.set('didFetchInstalledPlugins', true)
.set('installedPlugins', List(action.installedPlugins));
case ON_CHANGE:
return state.updateIn(action.keys, () => action.value);
default:
return state;
}
}
export default installPluginPageReducer;

View File

@ -1,117 +0,0 @@
import { LOCATION_CHANGE } from 'react-router-redux';
import {
call,
cancel,
fork,
put,
select,
take,
takeLatest,
} from 'redux-saga/effects';
import request from 'utils/request';
import { selectLocale } from '../LanguageProvider/selectors';
import {
downloadPluginError,
downloadPluginSucceeded,
getAvailablePluginsSucceeded,
getInstalledPluginsSucceeded,
} from './actions';
import { DOWNLOAD_PLUGIN, GET_AVAILABLE_PLUGINS, GET_INSTALLED_PLUGINS } from './constants';
import { makeSelectPluginToDownload } from './selectors';
export function* pluginDownload() {
try {
const pluginToDownload = yield select(makeSelectPluginToDownload());
const opts = {
method: 'POST',
body: {
plugin: pluginToDownload,
port: window.location.port,
},
};
const response = yield call(request, '/admin/plugins/install', opts, true);
if (response.ok) {
yield new Promise(resolve => {
setTimeout(() => {
resolve();
}, 8000);
});
yield put(downloadPluginSucceeded());
window.location.reload();
}
} catch(err) {
yield put(downloadPluginError());
}
}
export function* getAvailablePlugins() {
try {
// Get current locale.
const locale = yield select(selectLocale());
const opts = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
params: {
lang: locale,
},
};
let availablePlugins;
try {
// Retrieve plugins list.
availablePlugins = yield call(request, 'https://marketplace.strapi.io/plugins', opts);
} catch (e) {
availablePlugins = [];
}
yield put(getAvailablePluginsSucceeded(availablePlugins));
} catch(err) {
strapi.notification.error('notification.error');
}
}
export function* getInstalledPlugins() {
try {
const opts = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
};
let installedPlugins;
try {
// Retrieve plugins list.
installedPlugins = yield call(request, '/admin/plugins', opts);
} catch (e) {
installedPlugins = [];
}
yield put(getInstalledPluginsSucceeded(Object.keys(installedPlugins.plugins)));
} catch(err) {
strapi.notification.error('notification.error');
}
}
// Individual exports for testing
export default function* defaultSaga() {
const loadAvailablePluginsWatcher = yield fork(takeLatest, GET_AVAILABLE_PLUGINS, getAvailablePlugins);
const loadInstalledPluginsWatcher = yield fork(takeLatest, GET_INSTALLED_PLUGINS, getInstalledPlugins);
yield fork(takeLatest, DOWNLOAD_PLUGIN, pluginDownload);
yield take(LOCATION_CHANGE);
yield cancel(loadAvailablePluginsWatcher);
yield cancel(loadInstalledPluginsWatcher);
}

View File

@ -1,31 +0,0 @@
import { createSelector } from 'reselect';
/**
* Direct selector to the installPluginPage state domain
*/
const selectInstallPluginPageDomain = () => (state) => state.get('installPluginPage');
/**
* Other specific selectors
*/
/**
* Default selector used by InstallPluginPage
*/
const makeSelectInstallPluginPage = () => createSelector(
selectInstallPluginPageDomain(),
(substate) => substate.toJS()
);
const makeSelectPluginToDownload = () => createSelector(
selectInstallPluginPageDomain(),
(substate) => substate.get('pluginToDownload'),
);
export default makeSelectInstallPluginPage;
export {
selectInstallPluginPageDomain,
makeSelectPluginToDownload,
};

View File

@ -0,0 +1,40 @@
import {
DOWNLOAD_PLUGIN,
DOWNLOAD_PLUGIN_SUCCEEDED,
GET_AVAILABLE_AND_INSTALLED_PLUGINS,
GET_AVAILABLE_AND_INSTALLED_PLUGINS_SUCCEEDED,
RESET_PROPS,
} from './constants';
export function downloadPlugin(pluginToDownload) {
return {
type: DOWNLOAD_PLUGIN,
pluginToDownload,
};
}
export function downloadPluginSucceeded() {
return {
type: DOWNLOAD_PLUGIN_SUCCEEDED,
};
}
export function getAvailableAndInstalledPlugins() {
return {
type: GET_AVAILABLE_AND_INSTALLED_PLUGINS,
};
}
export function getAvailableAndInstalledPluginsSucceeded(availablePlugins, installedPlugins) {
return {
type: GET_AVAILABLE_AND_INSTALLED_PLUGINS_SUCCEEDED,
availablePlugins,
installedPlugins,
};
}
export function resetProps() {
return {
type: RESET_PROPS,
};
}

View File

@ -0,0 +1,5 @@
export const DOWNLOAD_PLUGIN = 'StrapiAdmin/Marketplace/DOWNLOAD_PLUGIN';
export const DOWNLOAD_PLUGIN_SUCCEEDED = 'StrapiAdmin/Marketplace/DOWNLOAD_PLUGIN_SUCCEEDED';
export const GET_AVAILABLE_AND_INSTALLED_PLUGINS = 'StrapiAdmin/Marketplace/GET_AVAILABLE_AND_INSTALLED_PLUGINS';
export const GET_AVAILABLE_AND_INSTALLED_PLUGINS_SUCCEEDED = 'StrapiAdmin/Marketplace/GET_AVAILABLE_AND_INSTALLED_PLUGINS_SUCCEEDED';
export const RESET_PROPS = 'StrapiAdmin/Marketplace/RESET_PROPS';

View File

@ -0,0 +1,155 @@
/**
*
* Marketplace
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Helmet } from 'react-helmet';
import { FormattedMessage } from 'react-intl';
import { bindActionCreators, compose } from 'redux';
import cn from 'classnames';
// Design
import PluginCard from 'components/PluginCard';
import PluginHeader from 'components/PluginHeader';
import LoadingIndicatorPage from 'components/LoadingIndicatorPage';
import injectSaga from 'utils/injectSaga';
import injectReducer from 'utils/injectReducer';
import {
downloadPlugin,
getAvailableAndInstalledPlugins,
resetProps,
} from './actions';
import makeSelectMarketplace from './selectors';
import reducer from './reducer';
import saga from './saga';
import styles from './styles.scss';
class Marketplace extends React.Component {
getChildContext = () => (
{
downloadPlugin: this.props.downloadPlugin,
}
);
componentDidMount() {
// Fetch the available and installed plugins
this.props.getAvailableAndInstalledPlugins();
}
componentWillUnmount() {
this.props.resetProps();
}
renderHelmet = message => (
<Helmet>
<title>{message}</title>
<meta name="description" content="Description of InstallPluginPage" />
</Helmet>
);
renderPluginCard = plugin => {
const { adminPage: { currentEnvironment }, availablePlugins, downloadPlugin, history, installedPlugins } = this.props;
const currentPlugin = availablePlugins[plugin];
return (
<PluginCard
currentEnvironment={currentEnvironment}
history={history}
key={currentPlugin.id}
plugin={currentPlugin}
showSupportUsButton={currentPlugin.id === 'support-us'}
isAlreadyInstalled={installedPlugins.includes(currentPlugin.id)}
downloadPlugin={(e) => {
e.preventDefault();
e.stopPropagation();
if (plugin.id !== 'support-us') {
downloadPlugin(currentPlugin.id);
}
}}
/>
);
}
render() {
const { availablePlugins, isLoading } = this.props;
if (isLoading) {
return <LoadingIndicatorPage />;
}
return (
<div>
<FormattedMessage id="app.components.InstallPluginPage.helmet">
{this.renderHelmet}
</FormattedMessage>
<div className={cn('container-fluid', styles.containerFluid)}>
<PluginHeader
title={{ id: 'app.components.InstallPluginPage.title' }}
description={{ id: 'app.components.InstallPluginPage.description' }}
actions={[]}
/>
<div className={cn('row', styles.wrapper)}>
{Object.keys(availablePlugins).map(this.renderPluginCard)}
</div>
</div>
</div>
);
}
}
Marketplace.childContextTypes = {
downloadPlugin: PropTypes.func.isRequired,
};
Marketplace.defaultProps = {};
Marketplace.propTypes = {
adminPage: PropTypes.object.isRequired,
availablePlugins: PropTypes.array.isRequired,
downloadPlugin: PropTypes.func.isRequired,
getAvailableAndInstalledPlugins: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
installedPlugins: PropTypes.array.isRequired,
isLoading: PropTypes.bool.isRequired,
resetProps: PropTypes.func.isRequired,
};
const mapStateToProps = makeSelectMarketplace();
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
downloadPlugin,
getAvailableAndInstalledPlugins,
resetProps,
},
dispatch,
);
}
const withConnect = connect(mapStateToProps, mapDispatchToProps);
/* Remove this line if the container doesn't have a route and
* check the documentation to see how to create the container's store
*/
const withReducer = injectReducer({ key: 'marketplace', reducer });
/* Remove the line below the container doesn't have a route and
* check the documentation to see how to create the container's store
*/
const withSaga = injectSaga({ key: 'marketplace', saga });
export default compose(
withReducer,
withSaga,
withConnect,
)(Marketplace);

View File

@ -0,0 +1,37 @@
import { fromJS, List } from 'immutable';
import {
DOWNLOAD_PLUGIN,
DOWNLOAD_PLUGIN_SUCCEEDED,
GET_AVAILABLE_AND_INSTALLED_PLUGINS_SUCCEEDED,
RESET_PROPS,
} from './constants';
const initialState = fromJS({
availablePlugins: List([]),
installedPlugins: List([]),
isLoading: true,
pluginToDownload: null,
});
function marketplaceReducer(state = initialState, action) {
switch (action.type) {
case DOWNLOAD_PLUGIN:
return state.update('pluginToDownload', () => action.pluginToDownload);
case DOWNLOAD_PLUGIN_SUCCEEDED:
return state
.update('installedPlugins', list => list.push(state.get('pluginToDownload')))
.update('pluginToDownload', () => null);
case GET_AVAILABLE_AND_INSTALLED_PLUGINS_SUCCEEDED:
return state
.update('availablePlugins', () => List(action.availablePlugins))
.update('installedPlugins', () => List(action.installedPlugins))
.update('isLoading', () => false);
case RESET_PROPS:
return initialState;
default:
return state;
}
}
export default marketplaceReducer;

View File

@ -0,0 +1,89 @@
import { LOCATION_CHANGE } from 'react-router-redux';
import {
all,
call,
cancel,
fork,
put,
select,
take,
takeLatest,
} from 'redux-saga/effects';
import request from 'utils/request';
import { selectLocale } from '../LanguageProvider/selectors';
import {
getAvailableAndInstalledPluginsSucceeded,
downloadPluginSucceeded,
} from './actions';
import { DOWNLOAD_PLUGIN, GET_AVAILABLE_AND_INSTALLED_PLUGINS } from './constants';
import { makeSelectPluginToDownload } from './selectors';
export function* pluginDownload() {
try {
// Force the Overlayblocker to be displayed
const overlayblockerParams = {
enabled: true,
title: 'app.components.InstallPluginPage.Download.title',
description: 'app.components.InstallPluginPage.Download.description',
};
strapi.lockApp(overlayblockerParams);
const pluginToDownload = yield select(makeSelectPluginToDownload());
const opts = {
method: 'POST',
body: {
plugin: pluginToDownload,
port: window.location.port,
},
};
const response = yield call(request, '/admin/plugins/install', opts, overlayblockerParams);
if (response.ok) {
yield put(downloadPluginSucceeded());
window.location.reload();
}
} catch(err) {
// Hide the global OverlayBlocker
strapi.unlockApp();
strapi.notification.error('notification.error');
}
}
export function* getData() {
// Get current locale.
const locale = yield select(selectLocale());
try {
const opts = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
params: {
lang: locale,
},
};
const [availablePlugins, { plugins }] = yield all([
call(request, 'https://marketplace.strapi.io/plugins', opts),
call(request, '/admin/plugins', { method: 'GET' }),
]);
yield put(getAvailableAndInstalledPluginsSucceeded(availablePlugins, Object.keys(plugins)));
} catch(err) {
strapi.notification.error('notification.error');
}
}
// Individual exports for testing
export default function* defaultSaga() {
const loadDataWatcher = yield fork(takeLatest, GET_AVAILABLE_AND_INSTALLED_PLUGINS, getData);
yield fork(takeLatest, DOWNLOAD_PLUGIN, pluginDownload);
yield take(LOCATION_CHANGE);
yield cancel(loadDataWatcher);
}

View File

@ -0,0 +1,31 @@
import { createSelector } from 'reselect';
/**
* Direct selector to the marketplace state domain
*/
const selectMarketplaceDomain = () => (state) => state.get('marketplace');
/**
* Other specific selectors
*/
/**
* Default selector used by Marketplace
*/
const makeSelectMarketplace = () => createSelector(
selectMarketplaceDomain(),
(substate) => substate.toJS()
);
const makeSelectPluginToDownload = () => createSelector(
selectMarketplaceDomain(),
(substate) => substate.get('pluginToDownload'),
);
export default makeSelectMarketplace;
export {
selectMarketplaceDomain,
makeSelectPluginToDownload,
};

View File

@ -22,5 +22,5 @@
} }
.wrapper { .wrapper {
padding-top: .3rem; padding-top: 3.8rem;
} }

View File

@ -57,8 +57,8 @@ const registerPlugin = (plugin) => {
const displayNotification = (message, status) => { const displayNotification = (message, status) => {
store.dispatch(showNotification(message, status)); store.dispatch(showNotification(message, status));
}; };
const lockApp = () => { const lockApp = (data) => {
store.dispatch(freezeApp()); store.dispatch(freezeApp(data));
}; };
const unlockApp = () => { const unlockApp = () => {
store.dispatch(unfreezeApp()); store.dispatch(unfreezeApp());

View File

@ -52,6 +52,8 @@
"app.components.InputFileDetails.originalName": "Original name:", "app.components.InputFileDetails.originalName": "Original name:",
"app.components.InputFileDetails.remove": "Remove this file", "app.components.InputFileDetails.remove": "Remove this file",
"app.components.InputFileDetails.size": "Size:", "app.components.InputFileDetails.size": "Size:",
"app.components.InstallPluginPage.Download.title": "Downloading...",
"app.components.InstallPluginPage.Download.description": "It might take a few seconds to download and install the plugin.",
"app.components.InstallPluginPage.InputSearch.label": " ", "app.components.InstallPluginPage.InputSearch.label": " ",
"app.components.InstallPluginPage.InputSearch.placeholder": "Search for a plugin... (ex: authentication)", "app.components.InstallPluginPage.InputSearch.placeholder": "Search for a plugin... (ex: authentication)",
"app.components.InstallPluginPage.description": "Extend your app effortlessly.", "app.components.InstallPluginPage.description": "Extend your app effortlessly.",
@ -89,6 +91,7 @@
"app.components.PluginCard.compatibleCommunity": "Compatible with the community", "app.components.PluginCard.compatibleCommunity": "Compatible with the community",
"app.components.PluginCard.more-details": "More details", "app.components.PluginCard.more-details": "More details",
"app.components.PluginCard.price.free": "Free", "app.components.PluginCard.price.free": "Free",
"app.components.PluginCard.settings": "Settings",
"app.components.listPlugins.button": "Add New Plugin", "app.components.listPlugins.button": "Add New Plugin",
"app.components.listPlugins.title.none": "No plugins installed", "app.components.listPlugins.title.none": "No plugins installed",
"app.components.listPlugins.title.plural": "{number} plugins are installed", "app.components.listPlugins.title.plural": "{number} plugins are installed",

View File

@ -53,6 +53,8 @@
"app.components.InputFileDetails.originalName": "Nom d'origine :", "app.components.InputFileDetails.originalName": "Nom d'origine :",
"app.components.InputFileDetails.remove": "Supprimer ce fichier", "app.components.InputFileDetails.remove": "Supprimer ce fichier",
"app.components.InputFileDetails.size": "Taille:", "app.components.InputFileDetails.size": "Taille:",
"app.components.InstallPluginPage.Download.title": "Téléchargement en cours...",
"app.components.InstallPluginPage.Download.description": "L'installation d'un plugin peut prendre quelques secondes.",
"app.components.InstallPluginPage.InputSearch.label": " ", "app.components.InstallPluginPage.InputSearch.label": " ",
"app.components.InstallPluginPage.InputSearch.placeholder": "Recherchez un plugin... (ex : authentification)", "app.components.InstallPluginPage.InputSearch.placeholder": "Recherchez un plugin... (ex : authentification)",
"app.components.InstallPluginPage.description": "Améliorez votre app sans efforts", "app.components.InstallPluginPage.description": "Améliorez votre app sans efforts",
@ -90,6 +92,7 @@
"app.components.PluginCard.compatibleCommunity": "Compatible avec la communauté", "app.components.PluginCard.compatibleCommunity": "Compatible avec la communauté",
"app.components.PluginCard.more-details": "Plus de détails", "app.components.PluginCard.more-details": "Plus de détails",
"app.components.PluginCard.price.free": "Gratuit", "app.components.PluginCard.price.free": "Gratuit",
"app.components.PluginCard.settings": "Réglages",
"app.components.listPlugins.button": "Ajouter un Nouveau Plugin", "app.components.listPlugins.button": "Ajouter un Nouveau Plugin",
"app.components.listPlugins.title.none": "Aucun plugin n'est installé", "app.components.listPlugins.title.none": "Aucun plugin n'est installé",
"app.components.listPlugins.title.plural": "{number} sont disponibles", "app.components.listPlugins.title.plural": "{number} sont disponibles",

View File

@ -51,4 +51,4 @@
"npm": ">= 6.0.0" "npm": ">= 6.0.0"
}, },
"license": "MIT" "license": "MIT"
} }

View File

@ -10,9 +10,7 @@ import PropTypes from 'prop-types';
import styles from './styles.scss'; import styles from './styles.scss';
const LoadingIndicatorPage = (props) => { const LoadingIndicatorPage = (props) => {
if (props.error) { if (props.error) {
console.log(props.error);
return <div>An error occurred</div>; return <div>An error occurred</div>;
} }

View File

@ -15,7 +15,9 @@ import styles from './styles.scss';
class OverlayBlocker extends React.Component { class OverlayBlocker extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.overlayContainer = document.createElement('div'); this.overlayContainer = document.createElement('div');
document.body.appendChild(this.overlayContainer); document.body.appendChild(this.overlayContainer);
} }
@ -24,19 +26,21 @@ class OverlayBlocker extends React.Component {
} }
render() { render() {
const { title, description, icon } = this.props;
const content = this.props.children ? ( const content = this.props.children ? (
this.props.children this.props.children
) : ( ) : (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.icoContainer}> <div className={styles.icoContainer}>
<i className="fa fa-refresh" /> <i className={icon} />
</div> </div>
<div> <div>
<h4> <h4>
<FormattedMessage id="components.OverlayBlocker.title" /> <FormattedMessage id={title} />
</h4> </h4>
<p> <p>
<FormattedMessage id="components.OverlayBlocker.description" /> <FormattedMessage id={description} />
</p> </p>
<div className={styles.buttonContainer}> <div className={styles.buttonContainer}>
<a className={cn(styles.primary, 'btn')} href="https://strapi.io/documentation/configurations/configurations.html#server" target="_blank">Read the documentation</a> <a className={cn(styles.primary, 'btn')} href="https://strapi.io/documentation/configurations/configurations.html#server" target="_blank">Read the documentation</a>
@ -62,12 +66,18 @@ class OverlayBlocker extends React.Component {
OverlayBlocker.defaultProps = { OverlayBlocker.defaultProps = {
children: '', children: '',
description: 'components.OverlayBlocker.description',
icon: 'fa fa-refresh',
isOpen: false, isOpen: false,
title: 'components.OverlayBlocker.title',
}; };
OverlayBlocker.propTypes = { OverlayBlocker.propTypes = {
children: PropTypes.node, children: PropTypes.node,
description: PropTypes.string,
icon: PropTypes.string,
isOpen: PropTypes.bool, isOpen: PropTypes.bool,
title: PropTypes.string,
}; };
export default OverlayBlocker; export default OverlayBlocker;

View File

@ -145,7 +145,7 @@ export default function request(url, options = {}, shouldWatchServerRestart = fa
.then((response) => { .then((response) => {
if (shouldWatchServerRestart) { if (shouldWatchServerRestart) {
// Display the global OverlayBlocker // Display the global OverlayBlocker
strapi.lockApp(); strapi.lockApp(shouldWatchServerRestart);
return serverRestartWatcher(response); return serverRestartWatcher(response);
} }