diff --git a/docs/3.x.x/en/plugin-development/ui-components.md b/docs/3.x.x/en/plugin-development/ui-components.md index 0396a12233..85a904a897 100644 --- a/docs/3.x.x/en/plugin-development/ui-components.md +++ b/docs/3.x.x/en/plugin-development/ui-components.md @@ -328,6 +328,171 @@ export default FooPage; *** +## OverlayBlocker + +The OverlayBlocker is a React component that is very useful to block user interactions when the strapi server is restarting in order to avoid front-end errors. + +### Usage + +| Property | Type | Required | Description | +| -------- | ---- | -------- | ----------- | +| children | node | no | Anything that is wrapped inside the OverlayBlocker | +| isOpen | bool | no | If set to `true` it will display the component | + +### Example + +In this example we'll have a button that when clicked it will display the OverlayBlocker for 5 seconds thus 'freezes' the admin so the user can't navigate (it simulates a very long server restart). + +**Path -** `./plugins/my-plugin/admin/src/containers/FooPage/constants.js`. +```js +export const ON_BUTTON_CLICK = 'MyPlugin/FooPage/ON_BUTTON_CLICK'; +export const RESET_SHOW_OVERLAY_PROP = 'MyPlugin/FooPage/RESET_SHOW_OVERLAY_PROP'; +``` + +**Path -** `./plugins/my-plugin/admin/src/containers/FooPage/actions.js`. +```js +import { ON_BUTTON_CLICK, RESET_SHOW_OVERLAY_PROP } from './constants'; + +export function onButtonClick() { + return { + type: ON_BUTTON_CLICK, + }; +} + +export function resetShowOverlayProp() { + return { + type: RESET_SHOW_OVERLAY_PROP, + }; +} +``` + +**Path -** `./plugins/my-plugin/admin/src/containers/FooPage/index.js`. +```js +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { bindActionCreators, compose } from 'redux'; + +// Design +import Button from 'components/Button'; +import OverlayBlocker from 'components/OverlayBlocker'; + +// Utils +import injectSaga from 'utils/injectSaga'; +import injectReducer from 'utils/injectReducer'; + +// Actions +import { onButtonClick } from './actions'; + +// Reducer +import reducer from './reducer'; + +// Saga +import saga from './saga'; + +// Selectors (see the documentation to see how to use selectors) +import makeSelectFooPage from './selectors'; + + +export class FooPage extends React.Component { + render() { + return ( +
+ +
+

The app is now blocked for 5 seconds

+
+ + +
+ ); + } +} + +FooPage.propTypes = { + onButtonClick: PropTypes.func.isRequired, + showOverlayBlocker: PropTypes.bool.isRequired, +}; + +const mapStateToProps = makeSelectFooPage(); + +function mapDispatchToProps(dispatch) { + return bindActionCreators( + { + onButtonClick, + }, + dispatch, + ); +} + +const withConnect = connect(mapStateToProps, mapDispatchToProps); +const withReducer = injectReducer({ key: 'fooPage', reducer }); +const withSaga = injectSaga({ key: 'fooPage', saga }); + +export default compose( + withReducer, + withSaga, + withConnect, +)(FooPage); + +``` +**Path -** `./plugins/my-plugin/admin/src/containers/FooPage/reducer.js`. +```js +import { fromJS } from 'immutable'; +import { ON_BUTTON_CLICK, RESET_SHOW_OVERLAY_PROP } from './constants'; + +const initialState = fromJS({ + showOverlayBlocker: false, +}); + +function fooPageReducer(state = initialState, action) { + switch (action.type) { + case ON_BUTTON_CLICK: + return state.set('showOverlayBlocker', true); + case RESET_SHOW_OVERLAY_PROP: + return state.set('showOverlayBlocker', false); + default: + return state; + } +} + +export default fooPageReducer; +``` + +**Path -** `./plugins/my-plugin/admin/src/containers/FooPage/saga.js`. +```js +import { + fork, + put, + takeLatest, +} from 'redux-saga/effects'; + +import { resetShowOverlayProp } from './actions'; + +import { ON_BUTTON_CLICK } from './constants'; + +export function* buttonClicked() { + try { + // Start the timer + yield new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 5000); + }); + + yield put(resetShowOverlayProp()); + } catch(err) { + yield put(resetShowOverlayProp()); + } +} + +export default function* defaultSaga() { + yield fork(takeLatest, ON_BUTTON_CLICK, buttonClicked); +} +``` + +*** + ## PopUp Warning PopUp warning library based on [reactstrap](https://reactstrap.github.io/components/modals/). diff --git a/packages/strapi-admin/admin/src/app.js b/packages/strapi-admin/admin/src/app.js index 9da2d4e8c6..df591e83ad 100755 --- a/packages/strapi-admin/admin/src/app.js +++ b/packages/strapi-admin/admin/src/app.js @@ -229,6 +229,7 @@ window.strapi = Object.assign(window.strapi || {}, { }), router: history, languages, + currentLanguage: window.localStorage.getItem('strapi-admin-language') || window.navigator.language || window.navigator.userLanguage || 'en', }); const dispatch = store.dispatch; diff --git a/packages/strapi-admin/admin/src/assets/icons/icon_success.svg b/packages/strapi-admin/admin/src/assets/icons/icon_success.svg new file mode 100755 index 0000000000..6c2c58707a --- /dev/null +++ b/packages/strapi-admin/admin/src/assets/icons/icon_success.svg @@ -0,0 +1,19 @@ + + + + Check + Created with Sketch. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/strapi-admin/admin/src/components/DownloadInfo/index.js b/packages/strapi-admin/admin/src/components/DownloadInfo/index.js new file mode 100644 index 0000000000..07fc3422e3 --- /dev/null +++ b/packages/strapi-admin/admin/src/components/DownloadInfo/index.js @@ -0,0 +1,27 @@ +/* +* +* DownloadInfo +* +*/ + +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import Icon from '../../assets/icons/icon_success.svg'; +import styles from './styles.scss'; + +function DownloadInfo() { + return ( +
+
+ info +
+ +
+ +
+
+
+ ); +} + +export default DownloadInfo; diff --git a/packages/strapi-admin/admin/src/components/DownloadInfo/styles.scss b/packages/strapi-admin/admin/src/components/DownloadInfo/styles.scss new file mode 100644 index 0000000000..0f235ed3f9 --- /dev/null +++ b/packages/strapi-admin/admin/src/components/DownloadInfo/styles.scss @@ -0,0 +1,34 @@ +.wrapper { + width: 372px; + height: 159px; + border-radius: 2px; + background-color: #fff; +} + +.content { + padding-top: 3rem; + // color: #F64D0A; + text-align: center; + font-family: Lato; + font-size: 1.3rem; + > img { + width: 2.5rem; + margin-bottom: 1.5rem; + } + + > div { + padding-top: 9px; + line-height: 18px; + > span:first-child { + color: #333740; + font-size: 16px; + font-weight: 600; + } + + > span { + color: #787E8F; + font-size: 13px; + } + } + +} diff --git a/packages/strapi-admin/admin/src/components/InstallPluginPopup/index.js b/packages/strapi-admin/admin/src/components/InstallPluginPopup/index.js new file mode 100644 index 0000000000..b2ec928670 --- /dev/null +++ b/packages/strapi-admin/admin/src/components/InstallPluginPopup/index.js @@ -0,0 +1,156 @@ +/* +* +* InstallPluginPopup +* +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { Modal, ModalHeader, ModalBody } from 'reactstrap'; +import { map } from 'lodash'; +import cn from 'classnames'; + +import Official from 'components/Official'; +// import StarsContainer from 'components/StarsContainer'; + +import styles from './styles.scss'; + +class InstallPluginPopup extends React.Component { + handleClick = () => { + this.props.history.push({ pathname: this.props.history.location.pathname }); + this.context.downloadPlugin(this.props.plugin.id); + } + + toggle = () => { + this.props.history.push({ + pathname: this.props.history.location.pathname, + }); + } + + navLinks = [ + { + content: 'app.components.InstallPluginPopup.navLink.description', + name: 'description', + }, + { + content: 'app.components.InstallPluginPopup.navLink.screenshots', + name: 'screenshots', + }, + { + content: 'app.components.InstallPluginPopup.navLink.avis', + name: 'avis', + }, + { + content: 'app.components.InstallPluginPopup.navLink.faq', + name: 'faq', + }, + { + content: 'app.components.InstallPluginPopup.navLink.changelog', + name: 'changelog', + }, + ]; + + render() { + const descriptions = { + short: this.props.plugin.id === 'support-us' ? : this.props.plugin.description.short, + long: this.props.plugin.id === 'support-us' ? : this.props.plugin.description.long || this.props.plugin.description.short, + }; + + return ( + + + +
+ +
+
icon
+
+
{this.props.plugin.name}
+
+ {/*} + +
+ {this.props.plugin.ratings} + /5 +
+ */} + +
+
+ {descriptions.short} +
+
+
+ + +
+
+
+ {/*} + +{this.props.plugin.downloads_nb}k  + */} +
+
+
+ +
+ {/* Uncomment whebn prices are running} +
{this.props.plugin.price} €
+ */} +
+
+
+
+
+
+
+ {map(this.navLinks, link => { + const isActive = this.props.history.location.hash.split('::')[1] === link.name; + + return ( +
{ + if (link.name === 'description') { + this.props.history.push({ pathname: this.props.history.location.pathname, hash: `${this.props.plugin.id}::${link.name}` }); + } + }} + style={isActive ? { paddingTop: '4px'} : { paddingTop: '6px' }} + > + +
+ ); + })} +
+
+ {descriptions.long} +
+
+
+ ); + } +} + +InstallPluginPopup.contextTypes = { + downloadPlugin: PropTypes.func.isRequired, +}; + +InstallPluginPopup.defaultProps = { + description: { + short: 'app.Components.InstallPluginPopup.noDescription', + }, +}; + +InstallPluginPopup.propTypes = { + description: PropTypes.shape({ + long: PropTypes.string, + short: PropTypes.string, + }), + history: PropTypes.object.isRequired, + isOpen: PropTypes.bool.isRequired, + plugin: PropTypes.object.isRequired, +}; + +export default InstallPluginPopup; diff --git a/packages/strapi-admin/admin/src/components/InstallPluginPopup/styles.scss b/packages/strapi-admin/admin/src/components/InstallPluginPopup/styles.scss new file mode 100644 index 0000000000..196c2806f4 --- /dev/null +++ b/packages/strapi-admin/admin/src/components/InstallPluginPopup/styles.scss @@ -0,0 +1,201 @@ +.buttonWrapper { + display: flex; + justify-content: space-between; + min-width: 136px; + height: 30px; + margin-left: 20px; + border-radius: 0.3rem; + background: linear-gradient(315deg, #0097F6 0%, #005EEA 100%); + + color: white; + font-size: 13px; + font-style: normal; + font-weight: 600; + + cursor: pointer; + &:focus { + outline: 0; + } + > div { + text-align: center; + text-transform: capitalize; + &:first-child { + flex-grow: 2; + } + } + // > div:last-child { + // width: 50px; + // border-left: 1px solid #0774D9; + // } +} + +.headerButtonContainer { + display: flex; + justify-content: space-between; + padding-top: 13px; + line-height: 29px; + + > div { + font-size: 13px; + font-style: italic; + } + + > div:first-child { + > i { + margin-right: 10px; + } + color: #5A9E06; + } + + > div:nth-child(2) { + display: flex; + > span:nth-child(2) { + font-style: normal; + } + } +} + +.headerDescription { + height: 48px; + padding-top: 12px; + font-size: 13px; +} + +.headerInfo { + flex-grow: 1; + padding-top: 7px; + padding-left: 30px; +} + +.headerWrapper { + display: flex; +} + +.logo { + flex-shrink: 0; + width: 144px; + height: 144px; + border: 1px solid #E8EEFC; + border-radius: 3px; + background-size: contain; + line-height: 144px; + text-align: center; + > img { + min-width: 72px; + // max-height: 72px; + + } +} + +.modalHeader { + margin: 0 1.4rem !important; + padding: 1.4rem 0 0 0 !important; + border-bottom: 0 !important; + > button { + margin-right: -1rem !important; + color: #C3C5C8; + opacity: 1; + font-size: 1.8rem; + font-weight: 100; + z-index: 999; + &:hover, &:focus { + color: #C3C5C8; + opacity: 1; + outline: 0!important; + } + > span { + display: none; + } + &:before { + -webkit-font-smoothing: antialiased; + content: '\F00d'; + font-family: 'FontAwesome'; + font-weight: 400; + font-size: 1.2rem; + margin-right: 10px; + } + } +} + +.modalBody { + padding: 5px 0px !important; +} + +.modalPosition { + > div { + border:none; + border-radius: 2px; + width: 74.5rem; + padding-left: 0; + padding-right: 0; + } +} + +.name { + padding-bottom: 1px; + font-size: 16px; + font-weight: 700; + text-transform: uppercase; +} + +.navContainer { + display: flex; + justify-content: space-between; + height: 36px; + + margin-top: 38px; + padding: 0 30px; + + background-color: #FAFAFB; + font-size: 13px; + font-weight: 600; + text-transform: capitalize; + + > div { + flex-grow: 2; + text-align: center; + line-height: 23px; + cursor: pointer; + } +} + +.navLink { + border-top: 2px solid #1C5DE7; +} + +.notAllowed { + opacity: .3; + cursor: not-allowed !important; +} +.pluginDescription { + padding: 24px 30px; + font-size: 13px; + line-height: 18px; +} + +.ratings { + display: flex; + line-height: 18px; + + > div:last-child { + padding-top: 1px; + font-style: italic; + } +} + +.starsContainer { + display: flex; + > div { + color: #EED348; + > i { + margin-right: 2px; + } + } + > div:last-child { + color: #B3B5B9; + } +} + +.wrapper { + padding: 0 30px; +} diff --git a/packages/strapi-admin/admin/src/components/Official/index.js b/packages/strapi-admin/admin/src/components/Official/index.js new file mode 100644 index 0000000000..22aa25d2fd --- /dev/null +++ b/packages/strapi-admin/admin/src/components/Official/index.js @@ -0,0 +1,30 @@ +/* +* +* Official +* +*/ + +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import PropTypes from 'prop-types'; +import styles from './styles.scss'; + +function Official(props) { + + return ( + + ); +} + +Official.defaultProps = { + style: {}, +}; + +Official.propTypes = { + style: PropTypes.object, +}; + +export default Official; diff --git a/packages/strapi-admin/admin/src/components/Official/styles.scss b/packages/strapi-admin/admin/src/components/Official/styles.scss new file mode 100644 index 0000000000..2c834ddda2 --- /dev/null +++ b/packages/strapi-admin/admin/src/components/Official/styles.scss @@ -0,0 +1,29 @@ +.wrapper { + display: flex; + // justify-content: space-between; + height: 20px !important; + width: 88px; + padding: 0 10px; + border-radius: 2px; + background-color: #EE8948; + line-height: 20px; + text-align: center; + text-transform: uppercase; + > span { + height: 20px; + padding: 0!important; + color: #fff; + letter-spacing: 0.5px; + font-weight: 600; + font-size: 11px; + } + + > i { + margin-top: 1px; + margin-right: 6px; + vertical-align: -webkit-baseline-middle; + + color: #FFDC00; + font-size: 10px; + } +} diff --git a/packages/strapi-admin/admin/src/components/PluginCard/index.js b/packages/strapi-admin/admin/src/components/PluginCard/index.js index caba911a49..c9ac5c1ba7 100644 --- a/packages/strapi-admin/admin/src/components/PluginCard/index.js +++ b/packages/strapi-admin/admin/src/components/PluginCard/index.js @@ -7,19 +7,25 @@ import React from 'react'; import PropTypes from 'prop-types'; import cn from 'classnames'; -import { isEmpty, map, times } from 'lodash'; +import { isEmpty, replace } from 'lodash'; import { FormattedMessage } from 'react-intl'; // Temporary picture import Button from 'components/Button'; -import FakeScreenShot from './screenshot.png'; +import InstallPluginPopup from 'components/InstallPluginPopup'; +import Official from 'components/Official'; +// import StarsContainer from 'components/StarsContainer'; + import styles from './styles.scss'; +import Screenshot from './screenshot.png'; class PluginCard extends React.Component { - state = { isOpen: false }; + state = { isOpen: false, boostrapCol: 'col-lg-4' }; componentDidMount() { this.shouldOpenModal(this.props); + window.addEventListener('resize', this.setBoostrapCol); + this.setBoostrapCol(); } componentWillReceiveProps(nextProps) { @@ -28,14 +34,44 @@ class PluginCard extends React.Component { } } + componentWillUnmount() { + window.removeEventListener('resize', this.setBoostrapCol); + } + + setBoostrapCol = () => { + let boostrapCol = 'col-lg-4'; + + if (window.innerWidth > 1680) { + boostrapCol = 'col-lg-3'; + } + + if (window.innerWidth > 2300) { + boostrapCol = 'col-lg-2'; + } + + this.setState({ boostrapCol }); + } + + handleClick = () => { + if (this.props.plugin.id !== 'support-us') { + this.props.history.push({ + pathname: this.props.history.location.pathname, + hash: `${this.props.plugin.id}::description`, + }); + } else { + this.aTag.click(); + } + } + + handleDownloadPlugin = () => { + this.props.downloadPlugin(); + } + shouldOpenModal = (props) => { this.setState({ isOpen: !isEmpty(props.history.location.hash) }); } render() { - const stars = Math.round(this.props.plugin.ratings); - const coloredStars = times(stars, String); - const emptyStars = times(5 - stars, String); const buttonClass = !this.props.isAlreadyInstalled || this.props.showSupportUsButton ? styles.primary : styles.secondary; let buttonLabel = this.props.isAlreadyInstalled ? 'app.components.PluginCard.Button.label.install' : 'app.components.PluginCard.Button.label.download'; @@ -44,49 +80,73 @@ class PluginCard extends React.Component { buttonLabel = 'app.components.PluginCard.Button.label.support'; } + const pluginIcon = this.props.plugin.id !== 'email' ? ( +
+ + icon +
+ ) : ( +
+ ); + + const descriptions = { + short: this.props.plugin.id === 'support-us' ? : this.props.plugin.description.short, + long: this.props.plugin.id === 'support-us' ? : this.props.plugin.description.long || this.props.plugin.description.short, + }; + return ( -
+
-
+ {pluginIcon}
{this.props.plugin.name}
- + {descriptions.short} +  
-
- plugin screenshot +
+
-
{this.props.plugin.price !== 0 ? `${this.props.plugin.price}€` : }
+
{this.props.plugin.price !== 0 ? `${this.props.plugin.price}€` : ''}
-
+
e.stopPropagation()}>
-
-
- {map(coloredStars, star => )} -
-
- {map(emptyStars, s => )} -
-
+ {/*
{this.props.plugin.ratings} /5
+ */} +
+
); } @@ -97,7 +157,6 @@ PluginCard.defaultProps = { plugin: { description: '', id: '', - icon: '', name: '', price: 0, ratings: 5, @@ -106,6 +165,7 @@ PluginCard.defaultProps = { }; PluginCard.propTypes = { + downloadPlugin: PropTypes.func.isRequired, history: PropTypes.object.isRequired, isAlreadyInstalled: PropTypes.bool, plugin: PropTypes.object, diff --git a/packages/strapi-admin/admin/src/components/PluginCard/styles.scss b/packages/strapi-admin/admin/src/components/PluginCard/styles.scss index ba5b47644f..0d218fbbc4 100644 --- a/packages/strapi-admin/admin/src/components/PluginCard/styles.scss +++ b/packages/strapi-admin/admin/src/components/PluginCard/styles.scss @@ -1,20 +1,30 @@ .button { height: 26px; min-width: 89px !important; + padding-top: 2px; padding-left: 15px; padding-right: 15px; + border-radius: 2px !important; font-size: 13px; + font-weight: 500 !important; + cursor: pointer; } .cardDescription { height: 54px; - margin-bottom: 19px; - padding-top: 10px; - font-size: 12px; + margin-top: 27px; + margin-bottom: 9px; + font-size: 13px; font-weight: 400; + + > span:last-child { + color: #1C5DE7; + } + -webkit-font-smoothing: antialiased; } .cardFooter { + cursor: initial; display: flex; justify-content: space-between; height: 54px; @@ -22,14 +32,18 @@ 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: 2.7rem; - padding-top: 1.8rem; + margin-bottom: 10px; + padding-top: 2rem; > div:first-child { > i { margin-right: 10px; @@ -48,9 +62,9 @@ } .cardScreenshot { - > img { - width: 100%; - } + height: 78px; + border-radius: 2px; + background-size: cover; } .cardTitle { @@ -58,22 +72,48 @@ font-size: 13px; font-weight: 700; text-transform: uppercase; - line-height: 36px; + letter-spacing: 0.5px; > div:first-child { - height: 36px; - width: 70px; margin-right: 14px; - border: 1px solid #E8EEFC; - border-radius: 3px; - text-align: center; - font-size: 20px; } > div:last-child { height: 36px; + line-height: 36px; } } +.frame { + width: 70px; + height: 36px; + margin: auto 0; + text-align: center; + border: 1px solid #E8EEFC; + border-radius: 3px; + white-space: nowrap; + > img { + max-height: 36px; + vertical-align: middle; + } +} + +.iconContainer { + height: 36px; + width: 70px; + margin-right: 14px; + border: 1px solid #E8EEFC; + border-radius: 3px; + text-align: center; + font-size: 20px; +} + + +.helper { + display: inline-block; + height: 100%; + vertical-align: middle; +} + .pluginCard { } @@ -89,12 +129,14 @@ } .ratings { - display: flex; - - > div:last-child { - padding-top: 1px; - font-style: italic; - } + // display: flex; + // padding-top: 1px; + // TODO uncomment when ratings + // > div:last-child { + // padding-top: 4px; + // font-size: 12px; + // font-style: italic; + // } } .secondary { @@ -119,9 +161,10 @@ } .wrapper { - height: 320px; + min-height: 320px; margin-bottom: 4rem; padding: 1.2rem 1.5rem; + padding-bottom: 0; background-color: #fff; box-shadow: 0 2px 4px #E3E9F3; -webkit-font-smoothing: antialiased; diff --git a/packages/strapi-admin/admin/src/components/Row/index.js b/packages/strapi-admin/admin/src/components/Row/index.js index de984fc14e..294e0a50c2 100644 --- a/packages/strapi-admin/admin/src/components/Row/index.js +++ b/packages/strapi-admin/admin/src/components/Row/index.js @@ -49,20 +49,12 @@ class Row extends React.Component { } render() { - const pluginIcon = this.props.plugin.name !== 'Email' ? ( -
- {this.renderImg()} -
- ) : ( -
- -
- ); - return (
- {pluginIcon} +
+ icon +
{this.props.plugin.name} —  diff --git a/packages/strapi-admin/admin/src/components/StarsContainer/index.js b/packages/strapi-admin/admin/src/components/StarsContainer/index.js new file mode 100644 index 0000000000..796ef80126 --- /dev/null +++ b/packages/strapi-admin/admin/src/components/StarsContainer/index.js @@ -0,0 +1,38 @@ +/* +* +* StarsContainer +* +* +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { map, times } from 'lodash'; +import styles from './styles.scss'; + +function StarsContainer({ ratings }) { + const stars = Math.round(ratings); + const coloredStars = times(stars, String); + const emptyStars = times(5 - stars, String); + + return ( +
+
+ {map(coloredStars, star => )} +
+
+ {map(emptyStars, s => )} +
+
+ ); +} + +StarsContainer.defaultProps = { + ratings: 5, +}; + +StarsContainer.propTypes = { + ratings: PropTypes.number, +}; + +export default StarsContainer; diff --git a/packages/strapi-admin/admin/src/components/StarsContainer/styles.scss b/packages/strapi-admin/admin/src/components/StarsContainer/styles.scss new file mode 100644 index 0000000000..5a83aeb8ef --- /dev/null +++ b/packages/strapi-admin/admin/src/components/StarsContainer/styles.scss @@ -0,0 +1,13 @@ +.starsContainer { + display: flex; + margin-right: 10px; + > div { + color: #EEA348; + > i { + margin-right: 2px; + } + } + > div:last-child { + color: #B3B5B9; + } +} diff --git a/packages/strapi-admin/admin/src/containers/AdminPage/index.js b/packages/strapi-admin/admin/src/containers/AdminPage/index.js index 95d2ff737f..d49065e07d 100644 --- a/packages/strapi-admin/admin/src/containers/AdminPage/index.js +++ b/packages/strapi-admin/admin/src/containers/AdminPage/index.js @@ -25,7 +25,7 @@ import ComingSoonPage from 'containers/ComingSoonPage'; import Content from 'containers/Content'; import Header from 'components/Header/index'; import HomePage from 'containers/HomePage'; -// import InstallPluginPage from 'containers/InstallPluginPage'; +import InstallPluginPage from 'containers/InstallPluginPage'; import LeftMenu from 'containers/LeftMenu'; import ListPluginsPage from 'containers/ListPluginsPage'; import Logout from 'components/Logout'; @@ -123,7 +123,7 @@ export class AdminPage extends React.Component { // eslint-disable-line react/pr - + diff --git a/packages/strapi-admin/admin/src/containers/InstallPluginPage/actions.js b/packages/strapi-admin/admin/src/containers/InstallPluginPage/actions.js index 3f685b0b3c..4de39e11f8 100644 --- a/packages/strapi-admin/admin/src/containers/InstallPluginPage/actions.js +++ b/packages/strapi-admin/admin/src/containers/InstallPluginPage/actions.js @@ -5,11 +5,33 @@ */ import { + DOWNLOAD_PLUGIN, + DOWNLOAD_PLUGIN_ERROR, + DOWNLOAD_PLUGIN_SUCCEEDED, GET_PLUGINS, GET_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 getPlugins() { return { type: GET_PLUGINS, diff --git a/packages/strapi-admin/admin/src/containers/InstallPluginPage/constants.js b/packages/strapi-admin/admin/src/containers/InstallPluginPage/constants.js index 00464a25a7..2141383d83 100644 --- a/packages/strapi-admin/admin/src/containers/InstallPluginPage/constants.js +++ b/packages/strapi-admin/admin/src/containers/InstallPluginPage/constants.js @@ -4,6 +4,9 @@ * */ +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_PLUGINS = 'StrapiAdmin/InstallPluginPage/GET_PLUGINS'; export const GET_PLUGINS_SUCCEEDED = 'StrapiAdmin/InstallPluginPage/GET_PLUGINS_SUCCEEDED'; export const ON_CHANGE = 'StrapiAdmin/InstallPluginPage/ON_CHANGE'; diff --git a/packages/strapi-admin/admin/src/containers/InstallPluginPage/fakeData.json b/packages/strapi-admin/admin/src/containers/InstallPluginPage/fakeData.json index 0bb007e54f..25792d3bbb 100644 --- a/packages/strapi-admin/admin/src/containers/InstallPluginPage/fakeData.json +++ b/packages/strapi-admin/admin/src/containers/InstallPluginPage/fakeData.json @@ -7,7 +7,10 @@ "isCompatible": true, "name": "Content Manager", "price": 0, - "ratings": 1 + "ratings": 1, + "longDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + "downloads_nb": 3, + "logo": "http://blog.sqreen.io/wp-content/uploads/2016/07/Sqreen-Logo-Vertical-1.png" }, { "description": "content-type-builder.plugin.description", @@ -16,7 +19,10 @@ "isCompatible": true, "name": "content type builder", "price": 0, - "ratings": 4.4 + "ratings": 4.4, + "longDescription": "lorem", + "downloads_nb": 3, + "logo": "http://blog.sqreen.io/wp-content/uploads/2016/07/Sqreen-Logo-Vertical-1.png" }, { "description": "settings-manager.plugin.description", @@ -25,7 +31,10 @@ "isCompatible": true, "name": "settings manager", "price": 0, - "ratings": 2.6 + "ratings": 2.6, + "longDescription": "lorem", + "downloads_nb": 3, + "logo": "http://blog.sqreen.io/wp-content/uploads/2016/07/Sqreen-Logo-Vertical-1.png" }, { "description": "users-permissions.plugin.description", @@ -34,7 +43,10 @@ "isCompatible": true, "name": "Auth & Permissions", "price": 0, - "ratings": 1 + "ratings": 1, + "longDescription": "lorem", + "downloads_nb": 3, + "logo": "http://blog.sqreen.io/wp-content/uploads/2016/07/Sqreen-Logo-Vertical-1.png" }, { "description": "users-permissions.plugin.description", @@ -43,7 +55,10 @@ "isCompatible": true, "name": "test", "price": 0, - "ratings": 1 + "ratings": 1, + "longDescription": "lorem", + "downloads_nb": 3, + "logo": "http://blog.sqreen.io/wp-content/uploads/2016/07/Sqreen-Logo-Vertical-1.png" } ] } diff --git a/packages/strapi-admin/admin/src/containers/InstallPluginPage/index.js b/packages/strapi-admin/admin/src/containers/InstallPluginPage/index.js index de12c73e13..cfb7895c3c 100644 --- a/packages/strapi-admin/admin/src/containers/InstallPluginPage/index.js +++ b/packages/strapi-admin/admin/src/containers/InstallPluginPage/index.js @@ -15,6 +15,8 @@ import { get, isUndefined, map } from 'lodash'; // 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'; @@ -22,6 +24,7 @@ import injectSaga from 'utils/injectSaga'; import injectReducer from 'utils/injectReducer'; import { + downloadPlugin, getPlugins, onChange, } from './actions'; @@ -33,6 +36,12 @@ 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() { // Don't fetch the available plugins if it has already been done if (!this.props.didFetchPlugins) { @@ -43,6 +52,9 @@ export class InstallPluginPage extends React.Component { // eslint-disable-line render() { return (
+ + + {message => ( @@ -76,7 +88,15 @@ export class InstallPluginPage extends React.Component { // eslint-disable-line key={plugin.id} plugin={plugin} showSupportUsButton={plugin.id === 'support-us'} - isAlreadyInstalled={isUndefined(get(this.context.plugins.toJS(), plugin.id))} + isAlreadyInstalled={!isUndefined(get(this.context.plugins.toJS(), plugin.id))} + downloadPlugin={(e) => { + e.preventDefault(); + e.stopPropagation(); + + if (plugin.id !== 'support-us') { + this.props.downloadPlugin(plugin.id); + } + }} /> ))}
@@ -86,13 +106,19 @@ export class InstallPluginPage extends React.Component { // eslint-disable-line } } +InstallPluginPage.childContextTypes = { + downloadPlugin: PropTypes.func.isRequired, +}; + InstallPluginPage.contextTypes = { plugins: PropTypes.object.isRequired, }; InstallPluginPage.propTypes = { availablePlugins: PropTypes.array.isRequired, + blockApp: PropTypes.bool.isRequired, didFetchPlugins: PropTypes.bool.isRequired, + downloadPlugin: PropTypes.func.isRequired, getPlugins: PropTypes.func.isRequired, history: PropTypes.object.isRequired, // onChange: PropTypes.func.isRequired, @@ -104,6 +130,7 @@ const mapStateToProps = makeSelectInstallPluginPage(); function mapDispatchToProps(dispatch) { return bindActionCreators( { + downloadPlugin, getPlugins, onChange, }, diff --git a/packages/strapi-admin/admin/src/containers/InstallPluginPage/reducer.js b/packages/strapi-admin/admin/src/containers/InstallPluginPage/reducer.js index 379f6a3a89..cc70f6f36c 100644 --- a/packages/strapi-admin/admin/src/containers/InstallPluginPage/reducer.js +++ b/packages/strapi-admin/admin/src/containers/InstallPluginPage/reducer.js @@ -6,18 +6,35 @@ import { fromJS, List } from 'immutable'; import { + DOWNLOAD_PLUGIN, + DOWNLOAD_PLUGIN_ERROR, + DOWNLOAD_PLUGIN_SUCCEEDED, GET_PLUGINS_SUCCEEDED, ON_CHANGE, } from './constants'; const initialState = fromJS({ availablePlugins: List([]), + blockApp: false, didFetchPlugins: 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_PLUGINS_SUCCEEDED: return state .set('didFetchPlugins', true) diff --git a/packages/strapi-admin/admin/src/containers/InstallPluginPage/saga.js b/packages/strapi-admin/admin/src/containers/InstallPluginPage/saga.js index d388aa9edc..9be98e391a 100644 --- a/packages/strapi-admin/admin/src/containers/InstallPluginPage/saga.js +++ b/packages/strapi-admin/admin/src/containers/InstallPluginPage/saga.js @@ -1,27 +1,77 @@ import { LOCATION_CHANGE } from 'react-router-redux'; import { - // call, + call, cancel, fork, put, - // select, + select, take, takeLatest, } from 'redux-saga/effects'; -// import request from 'utils/request'; -import fakeData from './fakeData.json'; +import request from 'utils/request'; -import { getPluginsSucceeded } from './actions'; -import { GET_PLUGINS } from './constants'; +import { selectLocale } from '../LanguageProvider/selectors'; +import { + downloadPluginError, + downloadPluginSucceeded, + getPluginsSucceeded, +} from './actions'; +import { DOWNLOAD_PLUGIN, GET_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 put(downloadPluginSucceeded()); + + setTimeout(() => { + window.location.reload(); + }, 500); + } + } catch(err) { + yield put(downloadPluginError()); + } +} export function* pluginsGet() { try { - const availablePlugins = fakeData.availablePlugins; + // Get current locale. + const locale = yield select(selectLocale()); + + const opts = { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + params: { + lang: locale, + }, + }; + + // Retrieve plugins list. + const availablePlugins = yield call(request, 'https://marketplace.strapi.io/plugins', opts); + + // Add support us card to the plugins list. const supportUs = { - description: 'app.components.InstallPluginPage.plugin.support-us.description', + description: { + short: 'app.components.InstallPluginPage.plugin.support-us.description', + long: 'app.components.InstallPluginPage.plugin.support-us.description', + }, id: 'support-us', icon: '', + logo: '', name: 'buy a t-shirt', price: 30, ratings: 5, @@ -39,6 +89,7 @@ export function* pluginsGet() { // Individual exports for testing export default function* defaultSaga() { const loadPluginsWatcher = yield fork(takeLatest, GET_PLUGINS, pluginsGet); + yield fork(takeLatest, DOWNLOAD_PLUGIN, pluginDownload); yield take(LOCATION_CHANGE); diff --git a/packages/strapi-admin/admin/src/containers/InstallPluginPage/selectors.js b/packages/strapi-admin/admin/src/containers/InstallPluginPage/selectors.js index ee9f8fbdae..8b788b4f00 100644 --- a/packages/strapi-admin/admin/src/containers/InstallPluginPage/selectors.js +++ b/packages/strapi-admin/admin/src/containers/InstallPluginPage/selectors.js @@ -19,7 +19,13 @@ const makeSelectInstallPluginPage = () => createSelector( (substate) => substate.toJS() ); +const makeSelectPluginToDownload = () => createSelector( + selectInstallPluginPageDomain(), + (substate) => substate.get('pluginToDownload'), +); + export default makeSelectInstallPluginPage; export { selectInstallPluginPageDomain, + makeSelectPluginToDownload, }; diff --git a/packages/strapi-admin/admin/src/containers/InstallPluginPage/styles.scss b/packages/strapi-admin/admin/src/containers/InstallPluginPage/styles.scss index b744a07537..5d84dcb5c8 100644 --- a/packages/strapi-admin/admin/src/containers/InstallPluginPage/styles.scss +++ b/packages/strapi-admin/admin/src/containers/InstallPluginPage/styles.scss @@ -23,5 +23,5 @@ } .wrapper { - padding-top: 2.8rem; + padding-top: .3rem; } diff --git a/packages/strapi-admin/admin/src/containers/LanguageProvider/reducer.js b/packages/strapi-admin/admin/src/containers/LanguageProvider/reducer.js index 40e1821b4f..10adcf024f 100644 --- a/packages/strapi-admin/admin/src/containers/LanguageProvider/reducer.js +++ b/packages/strapi-admin/admin/src/containers/LanguageProvider/reducer.js @@ -35,6 +35,8 @@ function languageProviderReducer(state = initialState, action) { case CHANGE_LOCALE: // Set user language in local storage. window.localStorage.setItem(localStorageKey, action.locale); + strapi.currentLanguage = action.locale; + return state .set('locale', action.locale); default: diff --git a/packages/strapi-admin/admin/src/containers/ListPluginsPage/saga.js b/packages/strapi-admin/admin/src/containers/ListPluginsPage/saga.js index 133a4a9cd8..a775970b33 100644 --- a/packages/strapi-admin/admin/src/containers/ListPluginsPage/saga.js +++ b/packages/strapi-admin/admin/src/containers/ListPluginsPage/saga.js @@ -1,9 +1,10 @@ +import { get } from 'lodash'; import { fork, call, put, select, takeLatest } from 'redux-saga/effects'; - import { pluginDeleted } from 'containers/App/actions'; import auth from 'utils/auth'; import request from 'utils/request'; +import { selectLocale } from '../LanguageProvider/selectors'; import { deletePluginSucceeded, getPluginsSucceeded } from './actions'; import { GET_PLUGINS, ON_DELETE_PLUGIN_CONFIRM } from './constants'; import { makeSelectPluginToDelete } from './selectors'; @@ -32,13 +33,34 @@ export function* deletePlugin() { export function* pluginsGet() { try { + // Fetch plugins. const response = yield call(request, '/admin/plugins', { method: 'GET' }); + const locale = yield select(selectLocale()); + + const opts = { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + params: { + lang: locale, + }, + }; + + // Fetch plugins informations. + const availablePlugins = yield call(request, 'https://marketplace.strapi.io/plugins', opts); + + // Add logo URL to object. + Object.keys(response.plugins).map(name => { + response.plugins[name].logo = get(availablePlugins.find(plugin => plugin.id === name), 'logo', ''); + }); yield put(getPluginsSucceeded(response)); } catch(err) { strapi.notification.error('notification.error'); } } + // Individual exports for testing export default function* defaultSaga() { yield fork(takeLatest, ON_DELETE_PLUGIN_CONFIRM, deletePlugin); diff --git a/packages/strapi-admin/admin/src/store.js b/packages/strapi-admin/admin/src/store.js index 398699cf7c..9e98513d44 100755 --- a/packages/strapi-admin/admin/src/store.js +++ b/packages/strapi-admin/admin/src/store.js @@ -33,6 +33,7 @@ export default function configureStore(initialState = {}, history) { // TODO Try to remove when `react-router-redux` is out of beta, LOCATION_CHANGE should not be fired more than once after hot reloading // Prevent recomputing reducers for `replaceReducer` shouldHotReload: false, + name: `Strapi - Dashboard`, }) : compose; /* eslint-enable */ diff --git a/packages/strapi-admin/admin/src/translations/en.json b/packages/strapi-admin/admin/src/translations/en.json index 73cb3087f4..4182b2a794 100755 --- a/packages/strapi-admin/admin/src/translations/en.json +++ b/packages/strapi-admin/admin/src/translations/en.json @@ -2,6 +2,9 @@ "app.components.ComingSoonPage.comingSoon": "Coming soon", "app.components.ComingSoonPage.featuresNotAvailable": "This feature is still under active development.", + "app.components.DownloadInfo.download": "Download in progress...", + "app.components.DownloadInfo.text": "It can take a minute. Thanks for your patience.", + "app.components.HomePage.welcome": "Welcome on board!", "app.components.HomePage.description.part1": "We are happy to have you as one of our users!", "app.components.HomePage.description.part2": "You are now a member of our community which will help you building your dreamed app.", @@ -14,6 +17,13 @@ "app.components.InstallPluginPage.plugin.support-us.description": "Support us by buying the Strapi t-shirt. It will allow us to keep working on the project and try giving the best possible experience!", "app.components.InstallPluginPage.InputSearch.label": " ", "app.components.InstallPluginPage.InputSearch.placeholder": "Search a plugin... (ex: authentication)", + "app.components.InstallPluginPopup.downloads": "download", + "app.components.InstallPluginPopup.navLink.description": "Description", + "app.components.InstallPluginPopup.navLink.screenshots": "Screenshots", + "app.components.InstallPluginPopup.navLink.avis": "avis", + "app.components.InstallPluginPopup.navLink.faq": "faq", + "app.components.InstallPluginPopup.navLink.changelog": "changelog", + "app.components.InstallPluginPopup.noDescription": "No description available", "app.components.LeftMenuFooter.poweredBy": "Proudly powered by", "app.components.LeftMenuLinkContainer.configuration": "Configuration", @@ -35,12 +45,15 @@ "app.components.NotFoundPage.description": "Not Found", "app.components.NotFoundPage.back": "Back to homepage", + "app.components.Official": "Official", + "app.components.PluginCard.compatible": "Compatible with your app", "app.components.PluginCard.compatibleCommunity": "Compatible with the community", "app.components.PluginCard.Button.label.download": "Download", "app.components.PluginCard.Button.label.install": "Already installed", "app.components.PluginCard.Button.label.support": "Support us", "app.components.PluginCard.price.free": "Free", + "app.components.PluginCard.more-details": "More details", "components.AutoReloadBlocker.header": "Reload feature is required for this plugin.", "components.AutoReloadBlocker.description": "Open the following file and enable the feature.", diff --git a/packages/strapi-admin/admin/src/translations/fr.json b/packages/strapi-admin/admin/src/translations/fr.json index c9c9e0db38..6d7fa2c743 100755 --- a/packages/strapi-admin/admin/src/translations/fr.json +++ b/packages/strapi-admin/admin/src/translations/fr.json @@ -2,6 +2,8 @@ "app.components.ComingSoonPage.comingSoon": "Bientôt disponible", "app.components.ComingSoonPage.featuresNotAvailable": "Cette fonctionnalité est toujours en cours de développement.", + "app.components.DownloadInfo.download": "Téléchargement en cours...", + "app.components.DownloadInfo.text": "Cela peut prendre une minute. Merci de patienter.", "app.components.HomePage.welcome": "Bienvenue à bord!", "app.components.HomePage.description.part1": "Nous somme heureux de vous compter parmi nos utilisateurs", @@ -15,6 +17,13 @@ "app.components.InstallPluginPage.plugin.support-us.description": "Soutenez-nous en achetant un Strapi tee shirt. Cela nous aidera a continuer de travailler sur le projet pour vous donner la meilleure expérience possible!", "app.components.InstallPluginPage.InputSearch.label": " ", "app.components.InstallPluginPage.InputSearch.placeholder": "Recherchez un plugin... (ex: authentification)", + "app.components.InstallPluginPopup.downloads": "téléchargements", + "app.components.InstallPluginPopup.navLink.description": "Description", + "app.components.InstallPluginPopup.navLink.screenshots": "Screenshots", + "app.components.InstallPluginPopup.navLink.avis": "avis", + "app.components.InstallPluginPopup.navLink.faq": "faq", + "app.components.InstallPluginPopup.navLink.changelog": "changelog", + "app.components.InstallPluginPopup.noDescription": "Aucune description disponible", "app.components.LeftMenuFooter.poweredBy": "Propulsé par", "app.components.LeftMenuLinkContainer.configuration": "Configuration", @@ -36,12 +45,15 @@ "app.components.NotFoundPage.description": "Page introuvable", "app.components.NotFoundPage.back": "Retourner à la page d'accueil", + "app.components.Official": "Officiel", + "app.components.PluginCard.compatible": "Compatble avec votre app", "app.components.PluginCard.compatibleCommunity": "Compatble avec la communauté", "app.components.PluginCard.Button.label.download": "Télécharger", "app.components.PluginCard.Button.label.install": "Déjà installé", "app.components.PluginCard.Button.label.support": "Nous soutenir", "app.components.PluginCard.price.free": "Gratuit", + "app.components.PluginCard.more-details": "Plus de détails", "components.AutoReloadBlocker.header": "L'autoReload doit être activé pour ce plugin.", "components.AutoReloadBlocker.description": "Ouvrez le fichier suivant pour activer cette fonctionnalité.", diff --git a/packages/strapi-admin/config/routes.json b/packages/strapi-admin/config/routes.json index 5bf32527d9..02dc35d5f8 100755 --- a/packages/strapi-admin/config/routes.json +++ b/packages/strapi-admin/config/routes.json @@ -8,6 +8,14 @@ "policies": [] } }, + { + "method": "POST", + "path": "/plugins/install", + "handler": "Admin.installPlugin", + "config": { + "policies": [] + } + }, { "method": "DELETE", "path": "/plugins/uninstall/:plugin", diff --git a/packages/strapi-admin/controllers/Admin.js b/packages/strapi-admin/controllers/Admin.js index 8438c24a0a..18374d2166 100755 --- a/packages/strapi-admin/controllers/Admin.js +++ b/packages/strapi-admin/controllers/Admin.js @@ -8,6 +8,26 @@ const exec = require('child_process').execSync; */ module.exports = { + installPlugin: async ctx => { + try { + const { plugin, port } = ctx.request.body; + const strapiBin = path.join(process.cwd(), 'node_modules', 'strapi', 'bin', 'strapi'); + + strapi.reload.isWatching = false; + + strapi.log.info(`Installing ${plugin}...`); + + exec(`node ${strapiBin} install ${plugin} ${port === '4000' ? '--dev' : ''}`); + + ctx.send({ ok: true }); + + strapi.reload(); + } catch(err) { + strapi.reload.isWatching = true; + ctx.badRequest(null, [{ messages: [{ id: 'An error occured' }] }]); + } + }, + plugins: async ctx => { try { const plugins = Object.keys(strapi.plugins).reduce((acc, key) => { diff --git a/packages/strapi-helper-plugin/lib/src/app.js b/packages/strapi-helper-plugin/lib/src/app.js index 8ef11811a5..6f13e019d7 100755 --- a/packages/strapi-helper-plugin/lib/src/app.js +++ b/packages/strapi-helper-plugin/lib/src/app.js @@ -53,7 +53,7 @@ const apiUrl = `${strapi.backendURL}/${pluginId}`; const router = strapi.router; // Create redux store with Strapi admin history -const store = configureStore({}, strapi.router); +const store = configureStore({}, strapi.router, pluginName); // Define the plugin root component function Comp(props) { diff --git a/packages/strapi-helper-plugin/lib/src/components/OverlayBlocker/index.js b/packages/strapi-helper-plugin/lib/src/components/OverlayBlocker/index.js new file mode 100644 index 0000000000..6fa4f0d5d0 --- /dev/null +++ b/packages/strapi-helper-plugin/lib/src/components/OverlayBlocker/index.js @@ -0,0 +1,50 @@ +/* +* +* OverlayBlocker +* +*/ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; + +import styles from './styles.scss'; + +class OverlayBlocker extends React.Component { + constructor(props) { + super(props); + this.overlayContainer = document.createElement('div'); + document.body.appendChild(this.overlayContainer); + } + + componentWillUnmount() { + document.body.removeChild(this.overlayContainer); + } + + render() { + if (this.props.isOpen) { + return ReactDOM.createPortal( +
+
+ {this.props.children} +
+
, + this.overlayContainer + ); + } + + return ''; + } +} + +OverlayBlocker.defaultProps = { + children: '', + isOpen: false, +}; + +OverlayBlocker.propTypes = { + children: PropTypes.node, + isOpen: PropTypes.bool, +}; + +export default OverlayBlocker; diff --git a/packages/strapi-helper-plugin/lib/src/components/OverlayBlocker/styles.scss b/packages/strapi-helper-plugin/lib/src/components/OverlayBlocker/styles.scss new file mode 100644 index 0000000000..7db766be8d --- /dev/null +++ b/packages/strapi-helper-plugin/lib/src/components/OverlayBlocker/styles.scss @@ -0,0 +1,16 @@ +.overlay { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: rgba(0, 0, 0, 0.5); + + > div { + position: fixed; + top: 17.5rem; + left: 50%; + margin-left: -24rem; + } +} diff --git a/packages/strapi-helper-plugin/lib/src/store.js b/packages/strapi-helper-plugin/lib/src/store.js index 2ac1f5991a..9a51ddb25f 100755 --- a/packages/strapi-helper-plugin/lib/src/store.js +++ b/packages/strapi-helper-plugin/lib/src/store.js @@ -10,7 +10,7 @@ import createReducer from './reducers'; const sagaMiddleware = createSagaMiddleware(); -export default function configureStore(initialState = {}, history) { +export default function configureStore(initialState = {}, history, name) { // Create the store with two middlewares // 1. sagaMiddleware: Makes redux-sagas work // 2. routerMiddleware: Syncs the location/URL path to the state @@ -33,6 +33,7 @@ export default function configureStore(initialState = {}, history) { // TODO Try to remove when `react-router-redux` is out of beta, LOCATION_CHANGE should not be fired more than once after hot reloading // Prevent recomputing reducers for `replaceReducer` shouldHotReload: false, + name: `Plugin - ${name}`, }) : compose; /* eslint-enable */ diff --git a/packages/strapi-helper-plugin/lib/src/utils/request.js b/packages/strapi-helper-plugin/lib/src/utils/request.js index 6751d6dc99..88db7bbbdc 100755 --- a/packages/strapi-helper-plugin/lib/src/utils/request.js +++ b/packages/strapi-helper-plugin/lib/src/utils/request.js @@ -78,38 +78,38 @@ function serverRestartWatcher(response) { * * @return {object} The response data */ - export default function request(url, options = {}, shouldWatchServerRestart = false) { - // Set headers - options.headers = Object.assign({ - 'Content-Type': 'application/json', - }, options.headers, { - 'X-Forwarded-Host': 'strapi', - }); +export default function request(url, options = {}, shouldWatchServerRestart = false) { + // Set headers + if (!options.headers) { + options.headers = Object.assign({ + 'Content-Type': 'application/json', + }, options.headers, { + 'X-Forwarded-Host': 'strapi', + }); + } - const token = auth.getToken(); + const token = auth.getToken(); - if (token) { - options.headers = Object.assign({ - 'Authorization': `Bearer ${token}`, - }, options.headers); - } + if (token) { + options.headers = Object.assign({ + 'Authorization': `Bearer ${token}`, + }, options.headers); + } - // Add parameters to url - url = _.startsWith(url, '/') - ? `${strapi.backendURL}${url}` - : url; + // Add parameters to url + url = _.startsWith(url, '/') ? `${strapi.backendURL}${url}` : url; - if (options && options.params) { - const params = formatQueryParams(options.params); - url = `${url}?${params}`; - } + if (options && options.params) { + const params = formatQueryParams(options.params); + url = `${url}?${params}`; + } - // Stringify body object - if (options && options.body) { - options.body = JSON.stringify(options.body); - } + // Stringify body object + if (options && options.body) { + options.body = JSON.stringify(options.body); + } - return fetch(url, options) + return fetch(url, options) .then(checkStatus) .then(parseJSON) .then((response) => { @@ -118,5 +118,5 @@ function serverRestartWatcher(response) { } return response; - }); - } + }); +} diff --git a/packages/strapi-plugin-content-manager/admin/src/assets/images/logo.svg b/packages/strapi-plugin-content-manager/admin/src/assets/images/logo.svg new file mode 100755 index 0000000000..ac6161da78 --- /dev/null +++ b/packages/strapi-plugin-content-manager/admin/src/assets/images/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/strapi-plugin-content-manager/admin/src/translations/en.json b/packages/strapi-plugin-content-manager/admin/src/translations/en.json index 00ff613d90..7403067c4c 100755 --- a/packages/strapi-plugin-content-manager/admin/src/translations/en.json +++ b/packages/strapi-plugin-content-manager/admin/src/translations/en.json @@ -1,5 +1,6 @@ { - "plugin.description": "Quick way to see, edit and delete the data in your database.", + "plugin.description.short": "Quick way to see, edit and delete the data in your database.", + "plugin.description.long": "Quick way to see, edit and delete the data in your database.", "containers.Home.pluginHeaderTitle": "Content Manager", "containers.Home.introduction": "To edit your entries go to the specific link in the left menu. This plugin doesn't have a proper way to edit settings and it's still under active development.", "containers.Home.pluginHeaderDescription": "Manage your entries through a powerful and beautiful interface.", diff --git a/packages/strapi-plugin-content-manager/admin/src/translations/fr.json b/packages/strapi-plugin-content-manager/admin/src/translations/fr.json index 849e99db91..ac1b9fe000 100755 --- a/packages/strapi-plugin-content-manager/admin/src/translations/fr.json +++ b/packages/strapi-plugin-content-manager/admin/src/translations/fr.json @@ -1,5 +1,6 @@ { - "plugin.description": "Visualisez, modifiez et supprimez les données de votre base de données.", + "plugin.description.short": "Visualisez, modifiez et supprimez les données de votre base de données.", + "plugin.description.long": "Visualisez, modifiez et supprimez les données de votre base de données.", "containers.Home.pluginHeaderTitle": "Type de contenu", "containers.Home.pluginHeaderDescription": "Créer et modifier votre type de contenu", "containers.Home.introduction": "Pour éditer du contenu, choisissez un type de données dans le menu de gauche.", diff --git a/packages/strapi-plugin-content-type-builder/admin/src/assets/images/logo.svg b/packages/strapi-plugin-content-type-builder/admin/src/assets/images/logo.svg new file mode 100755 index 0000000000..89602f579d --- /dev/null +++ b/packages/strapi-plugin-content-type-builder/admin/src/assets/images/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/strapi-plugin-content-type-builder/admin/src/translations/en.json b/packages/strapi-plugin-content-type-builder/admin/src/translations/en.json index 0e96fd6867..fda0d627f3 100755 --- a/packages/strapi-plugin-content-type-builder/admin/src/translations/en.json +++ b/packages/strapi-plugin-content-type-builder/admin/src/translations/en.json @@ -1,5 +1,6 @@ { - "plugin.description": "Modelize the data structure of you API.", + "plugin.description.short": "Modelize the data structure of you API.", + "plugin.description.long": "Modelize the data structure of you API. Create new fields and relations in a minute. The files are automatically created and updated in your project.", "attribute.string": "String", "attribute.text": "Text", "attribute.boolean": "Boolean", diff --git a/packages/strapi-plugin-content-type-builder/admin/src/translations/fr.json b/packages/strapi-plugin-content-type-builder/admin/src/translations/fr.json index 2805f32a61..cdf58be52c 100755 --- a/packages/strapi-plugin-content-type-builder/admin/src/translations/fr.json +++ b/packages/strapi-plugin-content-type-builder/admin/src/translations/fr.json @@ -1,5 +1,6 @@ { - "plugin.description": "Modelisez la structure de données de votre API.", + "plugin.description.short": "Modelisez la structure de données de votre API.", + "plugin.description.long": "Modelisez la structure de données de votre API. Créer des nouveaux champs et relations en un instant. Les fichiers se créent et se mettent à jour automatiquement.", "attribute.string": "Chaîne de caractères", "attribute.text": "Text", "attribute.boolean": "Booléen", diff --git a/packages/strapi-plugin-email/admin/src/translations/en.json b/packages/strapi-plugin-email/admin/src/translations/en.json index cf3bef0637..1bab23bf80 100755 --- a/packages/strapi-plugin-email/admin/src/translations/en.json +++ b/packages/strapi-plugin-email/admin/src/translations/en.json @@ -1,3 +1,4 @@ { - "plugin.description": "Send emails" + "plugin.description.short": "Send emails", + "plugin.description.long": "Send emails" } diff --git a/packages/strapi-plugin-email/admin/src/translations/fr.json b/packages/strapi-plugin-email/admin/src/translations/fr.json index 0b8ce2a2ec..f886d98035 100755 --- a/packages/strapi-plugin-email/admin/src/translations/fr.json +++ b/packages/strapi-plugin-email/admin/src/translations/fr.json @@ -1,3 +1,4 @@ { - "plugin.description": "Envoyez des emails" + "plugin.description.short": "Envoyez des emails", + "plugin.description.long": "Envoyez des emails" } diff --git a/packages/strapi-plugin-settings-manager/admin/src/assets/images/logo.svg b/packages/strapi-plugin-settings-manager/admin/src/assets/images/logo.svg new file mode 100755 index 0000000000..030f2cdfa0 --- /dev/null +++ b/packages/strapi-plugin-settings-manager/admin/src/assets/images/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/strapi-plugin-settings-manager/admin/src/translations/en.json b/packages/strapi-plugin-settings-manager/admin/src/translations/en.json index 6dbbfec6b0..90789e11f3 100755 --- a/packages/strapi-plugin-settings-manager/admin/src/translations/en.json +++ b/packages/strapi-plugin-settings-manager/admin/src/translations/en.json @@ -1,5 +1,6 @@ { - "plugin.description": "Configure your project in seconds", + "plugin.description.short": "Configure your project in seconds", + "plugin.description.long": "Configure your project in seconds", "menu.section.global-settings": "Global settings", "menu.item.application": "Application", "menu.item.languages": "Languages", diff --git a/packages/strapi-plugin-settings-manager/admin/src/translations/fr.json b/packages/strapi-plugin-settings-manager/admin/src/translations/fr.json index 9fcd147ced..5280fc8cf7 100755 --- a/packages/strapi-plugin-settings-manager/admin/src/translations/fr.json +++ b/packages/strapi-plugin-settings-manager/admin/src/translations/fr.json @@ -1,5 +1,6 @@ { - "plugin.description": "Configurez votre projet en quelques secondes.", + "plugin.description.short": "Configurez votre projet en quelques secondes.", + "plugin.description.long": "Configurez votre projet en quelques secondes.", "menu.section.global-settings": "Configuration générale", "menu.item.application": "Application", "menu.item.languages": "Langages", diff --git a/packages/strapi-plugin-users-permissions/admin/src/assets/images/logo.svg b/packages/strapi-plugin-users-permissions/admin/src/assets/images/logo.svg old mode 100644 new mode 100755 index 56d8006c95..c04f87fe51 --- a/packages/strapi-plugin-users-permissions/admin/src/assets/images/logo.svg +++ b/packages/strapi-plugin-users-permissions/admin/src/assets/images/logo.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/strapi-plugin-users-permissions/admin/src/assets/images/logo_strapi.svg b/packages/strapi-plugin-users-permissions/admin/src/assets/images/logo_strapi.svg new file mode 100644 index 0000000000..56d8006c95 --- /dev/null +++ b/packages/strapi-plugin-users-permissions/admin/src/assets/images/logo_strapi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/strapi-plugin-users-permissions/admin/src/components/Plugin/index.js b/packages/strapi-plugin-users-permissions/admin/src/components/Plugin/index.js index 98132cba35..c133e00c23 100644 --- a/packages/strapi-plugin-users-permissions/admin/src/components/Plugin/index.js +++ b/packages/strapi-plugin-users-permissions/admin/src/components/Plugin/index.js @@ -55,7 +55,7 @@ class Plugin extends React.Component { // eslint-disable-line react/prefer-state
{ icon ? (
- + icon
) : ''}
{this.props.name}
@@ -107,6 +107,7 @@ Plugin.defaultProps = { plugin: { description: 'users-permissions.Plugin.permissions.description.empty', controllers: {}, + information: {}, }, }; @@ -115,6 +116,9 @@ Plugin.propTypes = { name: PropTypes.string, plugin: PropTypes.shape({ description: PropTypes.string, + information: PropTypes.shape({ + logo: PropTypes.string.isRequired, + }).isRequired, }), pluginSelected: PropTypes.string.isRequired, }; diff --git a/packages/strapi-plugin-users-permissions/admin/src/containers/EditPage/saga.js b/packages/strapi-plugin-users-permissions/admin/src/containers/EditPage/saga.js index 88a2a595e5..b3f8289b26 100644 --- a/packages/strapi-plugin-users-permissions/admin/src/containers/EditPage/saga.js +++ b/packages/strapi-plugin-users-permissions/admin/src/containers/EditPage/saga.js @@ -45,7 +45,13 @@ export function* fetchUser(action) { export function* permissionsGet() { try { - const response = yield call(request, '/users-permissions/permissions', { method: 'GET' }); + const response = yield call(request, '/users-permissions/permissions', { + method: 'GET', + params: { + lang: strapi.currentLanguage, + }, + }); + yield put(getPermissionsSucceeded(response)); } catch(err) { strapi.notification.error('users-permissions.EditPage.notification.permissions.error'); @@ -68,7 +74,12 @@ export function* policiesGet() { export function* roleGet(action) { try { - const role = yield call(request, `/users-permissions/roles/${action.id}`, { method: 'GET' }); + const role = yield call(request, `/users-permissions/roles/${action.id}`, { + method: 'GET', + params: { + lang: strapi.currentLanguage, + }, + }); yield put(getRoleSucceeded(role)); } catch(err) { diff --git a/packages/strapi-plugin-users-permissions/admin/src/containers/EditPage/selectors.js b/packages/strapi-plugin-users-permissions/admin/src/containers/EditPage/selectors.js index 8a2f03efb7..ae0bb87c3a 100644 --- a/packages/strapi-plugin-users-permissions/admin/src/containers/EditPage/selectors.js +++ b/packages/strapi-plugin-users-permissions/admin/src/containers/EditPage/selectors.js @@ -5,11 +5,6 @@ import { createSelector } from 'reselect'; */ const selectEditPageDomain = () => (state) => state.get('editPage'); -/** - * Other specific selectors - */ - - /** * Default selector used by EditPage */ diff --git a/packages/strapi-plugin-users-permissions/admin/src/translations/en.json b/packages/strapi-plugin-users-permissions/admin/src/translations/en.json index ee125dedb1..cc91456dba 100755 --- a/packages/strapi-plugin-users-permissions/admin/src/translations/en.json +++ b/packages/strapi-plugin-users-permissions/admin/src/translations/en.json @@ -108,7 +108,8 @@ "notification.info.emailSent": "The email has been sent", "notification.success.delete": "The item has been deleted", - "plugin.description": "Protect your API with a full-authentication process", + "plugin.description.short": "Protect your API with a full-authentication process based on JWT", + "plugin.description.long": "Protect your API with a full-authentication process based on JWT. This plugin comes also with an ACL strategy that allows you to manage the permissions between the groups of users.", "Plugin.permissions.application.description": "Define the allowed actions in your project.", "Plugin.permissions.plugins.description": "Define the allowed actions in the {name} plugin.", diff --git a/packages/strapi-plugin-users-permissions/admin/src/translations/fr.json b/packages/strapi-plugin-users-permissions/admin/src/translations/fr.json index 09b343f211..480e0a122d 100755 --- a/packages/strapi-plugin-users-permissions/admin/src/translations/fr.json +++ b/packages/strapi-plugin-users-permissions/admin/src/translations/fr.json @@ -108,7 +108,8 @@ "notification.info.emailSent": "L'email a été envoyé", "notification.success.delete": "L'élément a bien été supprimé", - "plugin.description": "Protegez votre API avec un système d'authentification complet", + "plugin.description.short": "Protégez votre API avec un système d'authentification complet basé sur JWT", + "plugin.description.long": "Protégez votre API avec un système d'authentification complet basé sur JWT (JSON Web Token). Ce plugin ajoute aussi une stratégie ACL (Access Control Layer) qui vous permet de gérer les permissions entre les groupes d'utilisateurs.", "Plugin.permissions.application.description": "Définissez les actions autorisées dans votre projet.", "Plugin.permissions.plugins.description": "Définissez les actions autorisées dans le {name} plugin.", diff --git a/packages/strapi-plugin-users-permissions/controllers/UsersPermissions.js b/packages/strapi-plugin-users-permissions/controllers/UsersPermissions.js index 964424c368..da6c037a06 100644 --- a/packages/strapi-plugin-users-permissions/controllers/UsersPermissions.js +++ b/packages/strapi-plugin-users-permissions/controllers/UsersPermissions.js @@ -60,7 +60,9 @@ module.exports = { getPermissions: async (ctx) => { try { - const permissions = await strapi.plugins['users-permissions'].services.userspermissions.getActions(); + const { lang } = ctx.query; + const plugins = await strapi.plugins['users-permissions'].services.userspermissions.getPlugins(lang); + const permissions = await strapi.plugins['users-permissions'].services.userspermissions.getActions(plugins); ctx.send({ permissions }); } catch(err) { ctx.badRequest(null, [{ message: [{ id: 'Not Found' }] }]); @@ -75,7 +77,9 @@ module.exports = { getRole: async (ctx) => { const { id } = ctx.params; - const role = await strapi.plugins['users-permissions'].services.userspermissions.getRole(id); + const { lang } = ctx.query; + const plugins = await strapi.plugins['users-permissions'].services.userspermissions.getPlugins(lang); + const role = await strapi.plugins['users-permissions'].services.userspermissions.getRole(id, plugins); if (_.isEmpty(role)) { return ctx.badRequest(null, [{ messages: [{ id: `Role don't exist` }] }]); diff --git a/packages/strapi-plugin-users-permissions/package.json b/packages/strapi-plugin-users-permissions/package.json index 5f557e9fbe..4fc8ead557 100644 --- a/packages/strapi-plugin-users-permissions/package.json +++ b/packages/strapi-plugin-users-permissions/package.json @@ -26,6 +26,7 @@ "dependencies": { "bcryptjs": "^2.4.3", "jsonwebtoken": "^8.1.0", + "request": "^2.83.0", "uuid": "^3.1.0" }, "devDependencies": { diff --git a/packages/strapi-plugin-users-permissions/services/UsersPermissions.js b/packages/strapi-plugin-users-permissions/services/UsersPermissions.js index 8969dcc6e6..01c2e28d1e 100644 --- a/packages/strapi-plugin-users-permissions/services/UsersPermissions.js +++ b/packages/strapi-plugin-users-permissions/services/UsersPermissions.js @@ -4,6 +4,7 @@ const fs = require('fs') const path = require('path'); const stringify = JSON.stringify; const _ = require('lodash'); +const request = require('request'); /** * UsersPermissions.js service @@ -40,7 +41,25 @@ module.exports = { }); }, - getActions: () => { + getPlugins: (plugin, lang = 'en') => { + return new Promise((resolve, reject) => { + request({ + uri: `https://marketplace.strapi.io/plugins?lang=${lang}`, + json: true, + headers: { + 'cache-control': 'max-age=3600' + } + }, (err, response, body) => { + if (err) { + return reject(err); + } + + resolve(body); + }); + }); + }, + + getActions: (plugins = []) => { const generateActions = (data) => ( Object.keys(data).reduce((acc, key) => { acc[key] = { enabled: false, policy: '' }; @@ -60,7 +79,7 @@ module.exports = { return obj; - }, { controllers: {} }); + }, { controllers: {}, information: plugins.find(plugin => plugin.id === key) || {} }); return acc; }, {}); @@ -74,10 +93,17 @@ module.exports = { return _.merge(permissions, pluginsPermissions);; }, - getRole: async (roleId) => { - const appRoles = strapi.plugins['users-permissions'].config.roles + getRole: async (roleId, plugins) => { + const appRoles = strapi.plugins['users-permissions'].config.roles; + appRoles[roleId].users = await strapi.query('user', 'users-permissions').find(strapi.utils.models.convertParams('user', { role: roleId })); + Object.keys(appRoles[roleId].permissions) + .filter(name => name !== 'application') + .map(name => { + appRoles[roleId].permissions[name].information = plugins.find(plugin => plugin.id === name) || {}; + }); + return appRoles[roleId]; }, diff --git a/packages/strapi/lib/Strapi.js b/packages/strapi/lib/Strapi.js index 9a3da23ace..40ebc268f0 100755 --- a/packages/strapi/lib/Strapi.js +++ b/packages/strapi/lib/Strapi.js @@ -150,7 +150,7 @@ class Strapi extends EventEmitter { // Destroy server and available connections. this.server.destroy(); - if (cluster.isWorker && process.env.NODE_ENV === 'development' && get(this.config, 'currentEnvironment.server.autoReload.enabled') === true) { + if (cluster.isWorker && this.config.environment === 'development' && get(this.config, 'currentEnvironment.server.autoReload.enabled', true) === true) { process.send('stop'); } @@ -190,8 +190,10 @@ class Strapi extends EventEmitter { } reload() { - const reload = function() { - if (cluster.isWorker && process.env.NODE_ENV === 'development' && get(this.config, 'currentEnvironment.server.autoReload.enabled') === true) process.send('reload'); + const reload = function () { + if (cluster.isWorker && this.config.environment === 'development' && get(this.config, 'currentEnvironment.server.autoReload.enabled', true) === true) { + process.send('reload'); + } }; reload.isReloading = false; diff --git a/packages/strapi/package.json b/packages/strapi/package.json index f256c882a9..d4fd8c0c4b 100755 --- a/packages/strapi/package.json +++ b/packages/strapi/package.json @@ -89,4 +89,4 @@ }, "preferGlobal": true, "license": "MIT" -} +} \ No newline at end of file