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.
if (window.location.port !== '4000') {
fetch(`${strapi.remoteURL}/config/plugins.json`)
fetch(`${strapi.remoteURL}/config/plugins.json`, { cache: 'no-cache' })
.then(response => {
return response.json();
})

View File

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

View File

@ -10,32 +10,25 @@ import cn from 'classnames';
import { isEmpty, replace } from 'lodash';
import { FormattedMessage } from 'react-intl';
// Temporary picture
import Button from 'components/Button';
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 Screenshot from './screenshot.png';
const PLUGINS_WITH_CONFIG = ['content-manager', 'email', 'upload'];
/* eslint-disable react/no-unused-state */
class PluginCard extends React.Component {
state = { isOpen: false, boostrapCol: 'col-lg-4' };
state = {
boostrapCol: 'col-lg-4',
};
componentDidMount() {
this.shouldOpenModal(this.props);
// Listen window resize.
window.addEventListener('resize', this.setBoostrapCol);
this.setBoostrapCol();
}
componentWillReceiveProps(nextProps) {
if (nextProps.history.location.hash !== this.props.history.location.hash) {
this.shouldOpenModal(nextProps);
}
}
componentWillUnmount() {
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) => {
if (!this.props.isAlreadyInstalled && this.props.plugin.id !== 'support-us') {
this.props.downloadPlugin(e);
@ -75,23 +77,15 @@ class PluginCard extends React.Component {
}
}
shouldOpenModal = (props) => {
this.setState({ isOpen: !isEmpty(props.history.location.hash) });
}
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';
if (this.props.showSupportUsButton) {
buttonLabel = 'app.components.PluginCard.Button.label.support';
}
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" />
// Display settings link for a selection of plugins.
const settingsComponent = (PLUGINS_WITH_CONFIG.includes(this.props.plugin.id) &&
<div className={styles.settings} onClick={this.handleClickSettings}>
<i className='fa fa-cog' />
<FormattedMessage id='app.components.PluginCard.settings' />
</div>
);
@ -101,37 +95,21 @@ class PluginCard extends React.Component {
};
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.cardTitle}>
{pluginIcon}
<div>{this.props.plugin.name}</div>
<div className={styles.frame}>
<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 className={styles.cardDescription}>
{descriptions.short}
&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>
{descriptions.long}
{/* &nbsp;<FormattedMessage id="app.components.PluginCard.more-details" /> */}
</div>
<div className={styles.cardFooter} onClick={e => e.stopPropagation()}>
<div className={styles.ratings}>
{/*<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>
<div className={styles.cardFooterButton}>
<Button
className={cn(buttonClass, styles.button)}
label={buttonLabel}
@ -146,6 +124,18 @@ class PluginCard extends React.Component {
&nbsp;
</a>
</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>
<InstallPluginPopup
@ -168,15 +158,14 @@ PluginCard.defaultProps = {
price: 0,
ratings: 5,
},
showSupportUsButton: false,
};
PluginCard.propTypes = {
currentEnvironment: PropTypes.string.isRequired,
downloadPlugin: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
isAlreadyInstalled: PropTypes.bool,
plugin: PropTypes.object,
showSupportUsButton: PropTypes.bool,
};
export default PluginCard;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -1,14 +1,37 @@
.button {
height: 26px;
min-width: 89px !important;
padding-top: 2px;
padding-left: 15px;
padding-right: 15px;
margin: 0;
border-radius: 2px !important;
.wrapper {
position: relative;
min-height: 216px;
margin-bottom: 3.6rem;
padding: 1.2rem 1.5rem;
padding-bottom: 0;
background-color: #fff;
box-shadow: 0 2px 4px #E3E9F3;
-webkit-font-smoothing: antialiased;
}
.cardTitle {
display: flex;
font-size: 13px;
font-weight: 500 !important;
cursor: pointer;
font-weight: 600;
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 {
@ -25,62 +48,60 @@
}
.cardFooter {
cursor: initial;
position: absolute;
bottom: 0; left: 0;
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%;
margin-bottom: 10px;
padding-top: 2rem;
> div:first-child {
> i {
margin-right: 10px;
}
color: #5A9E06;
height: 45px;
padding: 0.9rem 1.5rem 1rem;
background-color: #FAFAFB;
justify-content: space-between;
flex-direction: row-reverse;
cursor: initial;
}
.compatible {
margin-top: 3px;
color: #5A9E06;
font-size: 1.3rem;
font-style: italic;
> i {
margin-right: 7px;
font-size: 12px;
font-style: italic;
line-height: 2.1rem;
}
> div:last-child {
font-size: 16px;
font-weight: 700;
line-height: 1.8rem;
}
}
.cardScreenshot {
height: 78px;
border-radius: 2px;
background-size: cover;
.settings{
margin-top: 3px;
color: #323740;
font-size: 1.3rem;
font-weight: 500;
cursor: pointer;
> i {
margin-right: 7px;
font-size: 12px;
}
}
.cardTitle {
display: flex;
.button {
height: 26px;
min-width: 89px !important;
padding: 0 15px;
margin: 0;
border-radius: 2px !important;
line-height: 24px;
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
> div:first-child {
margin-right: 14px;
}
font-weight: 500 !important;
cursor: pointer;
> div:last-child {
height: 36px;
line-height: 36px;
span {
display: inline-block;
width: 100%;
height: 100%;
line-height: 24px;
}
}
@ -89,7 +110,6 @@
height: 36px;
margin: auto 0;
text-align: center;
background: #FAFAFB;
border: 1px solid #F3F3F7;
border-radius: 3px;
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 {
display: inline-block;
height: 100%;
@ -118,58 +126,21 @@
}
.pluginCard {
}
.primary {
font-weight: 500;
background: linear-gradient(315deg, #0097F6 0%, #005EEA 100%);
-webkit-font-smoothing: antialiased;
color: white;
font-weight: 500;
-webkit-font-smoothing: antialiased;
&: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 {
border: 1px solid #DFE0E1;
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 {
makeSelectAppPlugins,
makeSelectBlockApp,
makeSelectOverlayBlockerProps,
makeSelectIsAppLoading,
makeSelectShowGlobalAppBlocker,
selectHasUserPlugin,
@ -35,7 +36,7 @@ import LocaleToggle from 'containers/LocaleToggle';
import CTAWrapper from 'components/CtaWrapper';
import Header from 'components/Header/index';
import HomePage from 'containers/HomePage/Loadable';
import InstallPluginPage from 'containers/InstallPluginPage/Loadable';
import Marketplace from 'containers/Marketplace/Loadable';
import LeftMenu from 'containers/LeftMenu';
import ListPluginsPage from 'containers/ListPluginsPage/Loadable';
import LoadingIndicatorPage from 'components/LoadingIndicatorPage';
@ -188,6 +189,8 @@ export class AdminPage extends React.Component {
return plugins;
};
renderMarketPlace = props => <Marketplace {...props} {...this.props} />;
render() {
const { adminPage } = this.props;
const header = this.showLeftMenu() ? <Header /> : '';
@ -219,14 +222,17 @@ export class AdminPage extends React.Component {
<Route path="/plugins/:pluginId" component={PluginPage} />
<Route path="/plugins" component={ComingSoonPage} />
<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="" component={NotFoundPage} />
<Route path="404" component={NotFoundPage} />
</Switch>
</Content>
</div>
<OverlayBlocker isOpen={this.props.blockApp && this.props.showGlobalAppBlocker} />
<OverlayBlocker
isOpen={this.props.blockApp && this.props.showGlobalAppBlocker}
{...this.props.overlayBlockerData}
/>
</div>
);
}
@ -248,6 +254,7 @@ AdminPage.defaultProps = {
appPlugins: [],
hasUserPlugin: true,
isAppLoading: true,
overlayBlockerData: {},
};
AdminPage.propTypes = {
@ -261,6 +268,7 @@ AdminPage.propTypes = {
history: PropTypes.object.isRequired,
isAppLoading: PropTypes.bool,
location: PropTypes.object.isRequired,
overlayBlockerData: PropTypes.object,
pluginLoaded: PropTypes.func.isRequired,
plugins: PropTypes.object.isRequired,
showGlobalAppBlocker: PropTypes.bool.isRequired,
@ -271,6 +279,7 @@ const mapStateToProps = createStructuredSelector({
adminPage: selectAdminPage(),
appPlugins: makeSelectAppPlugins(),
blockApp: makeSelectBlockApp(),
overlayBlockerData: makeSelectOverlayBlockerProps(),
hasUserPlugin: selectHasUserPlugin(),
isAppLoading: makeSelectIsAppLoading(),
plugins: selectPlugins(),
@ -295,4 +304,3 @@ const withReducer = injectReducer({ key: 'adminPage', reducer });
const withSaga = injectSaga({ key: 'adminPage', saga });
export default compose(withReducer, withSaga, withConnect)(AdminPage);

View File

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

View File

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

View File

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

View File

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

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

View File

@ -52,6 +52,8 @@
"app.components.InputFileDetails.originalName": "Original name:",
"app.components.InputFileDetails.remove": "Remove this file",
"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.placeholder": "Search for a plugin... (ex: authentication)",
"app.components.InstallPluginPage.description": "Extend your app effortlessly.",
@ -89,6 +91,7 @@
"app.components.PluginCard.compatibleCommunity": "Compatible with the community",
"app.components.PluginCard.more-details": "More details",
"app.components.PluginCard.price.free": "Free",
"app.components.PluginCard.settings": "Settings",
"app.components.listPlugins.button": "Add New Plugin",
"app.components.listPlugins.title.none": "No plugins 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.remove": "Supprimer ce fichier",
"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.placeholder": "Recherchez un plugin... (ex : authentification)",
"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.more-details": "Plus de détails",
"app.components.PluginCard.price.free": "Gratuit",
"app.components.PluginCard.settings": "Réglages",
"app.components.listPlugins.button": "Ajouter un Nouveau Plugin",
"app.components.listPlugins.title.none": "Aucun plugin n'est installé",
"app.components.listPlugins.title.plural": "{number} sont disponibles",

View File

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

View File

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

View File

@ -15,7 +15,9 @@ import styles from './styles.scss';
class OverlayBlocker extends React.Component {
constructor(props) {
super(props);
this.overlayContainer = document.createElement('div');
document.body.appendChild(this.overlayContainer);
}
@ -24,19 +26,21 @@ class OverlayBlocker extends React.Component {
}
render() {
const { title, description, icon } = this.props;
const content = this.props.children ? (
this.props.children
) : (
<div className={styles.container}>
<div className={styles.icoContainer}>
<i className="fa fa-refresh" />
<i className={icon} />
</div>
<div>
<h4>
<FormattedMessage id="components.OverlayBlocker.title" />
<FormattedMessage id={title} />
</h4>
<p>
<FormattedMessage id="components.OverlayBlocker.description" />
<FormattedMessage id={description} />
</p>
<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>
@ -62,12 +66,18 @@ class OverlayBlocker extends React.Component {
OverlayBlocker.defaultProps = {
children: '',
description: 'components.OverlayBlocker.description',
icon: 'fa fa-refresh',
isOpen: false,
title: 'components.OverlayBlocker.title',
};
OverlayBlocker.propTypes = {
children: PropTypes.node,
description: PropTypes.string,
icon: PropTypes.string,
isOpen: PropTypes.bool,
title: PropTypes.string,
};
export default OverlayBlocker;

View File

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