mirror of
https://github.com/strapi/strapi.git
synced 2025-11-01 10:23:34 +00:00
Merge pull request #2690 from strapi/design/marketplace
Redesign marketplace and add new blocker component
This commit is contained in:
commit
c72d5698d8
@ -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();
|
||||
})
|
||||
|
||||
@ -110,7 +110,7 @@ function LeftMenuLinkContainer({ layout, plugins }) {
|
||||
<LeftMenuLink
|
||||
icon="shopping-basket"
|
||||
label={messages.installNewPlugin.id}
|
||||
destination="/install-plugin"
|
||||
destination="/marketplace"
|
||||
/>
|
||||
{hasSettingsManager && (
|
||||
<LeftMenuLink
|
||||
|
||||
@ -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}
|
||||
<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}
|
||||
{/* <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 {
|
||||
|
||||
</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 |
@ -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;
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
import Loadable from 'react-loadable';
|
||||
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
|
||||
export default Loadable({
|
||||
loader: () => import('./index'),
|
||||
loading: LoadingIndicator,
|
||||
});
|
||||
@ -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;
|
||||
@ -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
@ -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);
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
.adminPage { /* stylelint-disable */
|
||||
display: flex;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.adminPageRightWrapper {
|
||||
|
||||
@ -15,9 +15,10 @@ import {
|
||||
UPDATE_PLUGIN,
|
||||
} from './constants';
|
||||
|
||||
export function freezeApp() {
|
||||
export function freezeApp(data) {
|
||||
return {
|
||||
type: FREEZE_APP,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
@ -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';
|
||||
@ -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);
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
@ -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';
|
||||
155
packages/strapi-admin/admin/src/containers/Marketplace/index.js
Normal file
155
packages/strapi-admin/admin/src/containers/Marketplace/index.js
Normal 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);
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
@ -22,5 +22,5 @@
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
padding-top: .3rem;
|
||||
padding-top: 3.8rem;
|
||||
}
|
||||
@ -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());
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -51,4 +51,4 @@
|
||||
"npm": ">= 6.0.0"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user