Merge branch 'master' into fix/deploy

This commit is contained in:
Aurélien GEORGET 2018-01-10 18:27:52 +01:00 committed by GitHub
commit bcb7a68dde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 1283 additions and 132 deletions

View File

@ -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 (
<div>
<OverlayBlocker isOpen={this.props.showOverlayBlocker}>
<div style={{ width: '100px', height: '100px', backgroundColor: '#fff' }}>
<h4>The app is now blocked for 5 seconds</h4>
</div>
</Overlay>
<Button onClick={this.props.onButtonClick} primary>Click me</Button>
</div>
);
}
}
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/).

View File

@ -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;

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 44.1 (41455) - http://www.bohemiancoding.com/sketch -->
<title>Check</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Pages" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Home-view" transform="translate(-1086.000000, -34.000000)">
<g id="Notification-2---Success" transform="translate(1066.000000, 14.000000)">
<g id="Check" transform="translate(20.000000, 20.000000)">
<g id="check" transform="translate(5.500000, 7.000000)" fill-rule="nonzero" fill="#27B70F">
<path d="M9.34239514,1.3983456 C9.34239514,1.55907497 9.28613986,1.69569495 9.1736293,1.80820551 L4.80982666,6.17200815 L3.99010683,6.99172798 C3.87759626,7.10423854 3.74097629,7.16049383 3.58024691,7.16049383 C3.41951753,7.16049383 3.28289756,7.10423854 3.170387,6.99172798 L2.35066717,6.17200815 L0.168765848,3.99010683 C0.0562552826,3.87759626 0,3.74097629 0,3.58024691 C0,3.41951753 0.0562552826,3.28289756 0.168765848,3.170387 L0.98848568,2.35066717 C1.10099625,2.2381566 1.23761622,2.18190132 1.3983456,2.18190132 C1.55907497,2.18190132 1.69569495,2.2381566 1.80820551,2.35066717 L3.58024691,4.12873592 L7.53418963,0.168765848 C7.6467002,0.0562552826 7.78332017,0 7.94404955,0 C8.10477893,0 8.2413989,0.0562552826 8.35390947,0.168765848 L9.1736293,0.98848568 C9.28613986,1.10099625 9.34239514,1.23761622 9.34239514,1.3983456 Z" id="Shape"></path>
</g>
<rect id="Rectangle-2" stroke="#27B70F" x="0.5" y="0.5" width="19" height="19" rx="9.5"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -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 (
<div className={styles.wrapper}>
<div className={styles.content}>
<img src={Icon} alt="info" />
<div>
<FormattedMessage id="app.components.DownloadInfo.download" />
<br />
<FormattedMessage id="app.components.DownloadInfo.text" />
</div>
</div>
</div>
);
}
export default DownloadInfo;

View File

@ -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;
}
}
}

View File

@ -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' ? <FormattedMessage id={this.props.plugin.description.short} /> : this.props.plugin.description.short,
long: this.props.plugin.id === 'support-us' ? <FormattedMessage id={this.props.plugin.description.long || this.props.plugin.description.short} /> : this.props.plugin.description.long || this.props.plugin.description.short,
};
return (
<Modal isOpen={this.props.isOpen} toggle={this.toggle} className={styles.modalPosition}>
<ModalHeader toggle={this.toggle} className={styles.modalHeader} />
<ModalBody className={styles.modalBody}>
<div className={styles.wrapper}>
<div className={styles.headerWrapper}>
<div className={styles.logo}><img src={`${this.props.plugin.logo}`} alt="icon" /></div>
<div className={styles.headerInfo}>
<div className={styles.name}>{this.props.plugin.name}</div>
<div className={styles.ratings}>
{/*}
<StarsContainer ratings={this.props.plugin.ratings} />
<div>
<span style={{ fontWeight: '600', color: '#333740', fontSize: '12px'}}>{this.props.plugin.ratings}</span>
<span style={{ fontWeight: '500', color: '#666666', fontSize: '11px' }}>/5</span>
</div>
*/}
<Official style={{ marginTop: '0' }} />
</div>
<div className={styles.headerDescription}>
{descriptions.short}
</div>
<div className={styles.headerButtonContainer}>
<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>
<div>
{/*}
<span style={{ fontWeight: '600' }}>+{this.props.plugin.downloads_nb}k&nbsp;</span><FormattedMessage id="app.components.InstallPluginPopup.downloads" />
*/}
</div>
<div className={styles.buttonWrapper} onClick={this.handleClick}>
<div>
<FormattedMessage id="app.components.InstallPluginPopup.downloads" />
</div>
{/* Uncomment whebn prices are running}
<div>{this.props.plugin.price}&nbsp;</div>
*/}
</div>
</div>
</div>
</div>
</div>
</div>
<div className={styles.navContainer}>
{map(this.navLinks, link => {
const isActive = this.props.history.location.hash.split('::')[1] === link.name;
return (
<div
key={link.name}
className={cn(isActive ? styles.navLink : '', link.name !== 'description' ? styles.notAllowed : '')}
onClick={() => {
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' }}
>
<FormattedMessage id={link.content} />
</div>
);
})}
</div>
<div className={styles.pluginDescription}>
{descriptions.long}
</div>
</ModalBody>
</Modal>
);
}
}
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;

View File

@ -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;
}

View File

@ -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 (
<button className={styles.wrapper} style={props.style}>
<i className="fa fa-star" />
<FormattedMessage id="app.components.Official" />
</button>
);
}
Official.defaultProps = {
style: {},
};
Official.propTypes = {
style: PropTypes.object,
};
export default Official;

View File

@ -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;
}
}

View File

@ -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' ? (
<div className={styles.frame}>
<span className={styles.helper} />
<img src={`${this.props.plugin.logo}`} alt="icon" />
</div>
) : (
<div className={styles.iconContainer}><i className={`fa fa-${this.props.plugin.icon}`} /></div>
);
const descriptions = {
short: this.props.plugin.id === 'support-us' ? <FormattedMessage id={this.props.plugin.description.short} /> : this.props.plugin.description.short,
long: this.props.plugin.id === 'support-us' ? <FormattedMessage id={this.props.plugin.description.long || this.props.plugin.description.short} /> : this.props.plugin.description.long || this.props.plugin.description.short,
};
return (
<div className={cn('col-md-4', styles.pluginCard)}>
<div className={cn(this.state.boostrapCol, styles.pluginCard)} onClick={this.handleClick}>
<div className={styles.wrapper}>
<div className={styles.cardTitle}>
<div><i className={`fa fa-${this.props.plugin.icon}`} /></div>
{pluginIcon}
<div>{this.props.plugin.name}</div>
</div>
<div className={styles.cardDescription}>
<FormattedMessage id={this.props.plugin.description} />
{descriptions.short}
&nbsp;<FormattedMessage id="app.components.PluginCard.more-details" />
</div>
<div className={styles.cardScreenshot}>
<img src={FakeScreenShot} alt='plugin screenshot' />
<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}` : <FormattedMessage id="app.components.PluginCard.price.free" />}</div>
<div>{this.props.plugin.price !== 0 ? `${this.props.plugin.price}` : ''}</div>
</div>
<div className={styles.cardFooter}>
<div className={styles.cardFooter} onClick={e => e.stopPropagation()}>
<div className={styles.ratings}>
<div className={styles.starsContainer}>
<div>
{map(coloredStars, star => <i key={star} className=" fa fa-star" />)}
</div>
<div>
{map(emptyStars, s => <i key={s} className="fa fa-star" />)}
</div>
</div>
{/*<StarsContainer ratings={this.props.plugin.ratings} />
<div>
<span style={{ fontWeight: '600', color: '#333740' }}>{this.props.plugin.ratings}</span>
<span style={{ fontWeight: '500', color: '#666666' }}>/5</span>
</div>
*/}
<Official />
</div>
<div>
<Button
className={cn(buttonClass, styles.button)}
label={buttonLabel}
onClick={this.handleDownloadPlugin}
/>
<a
href="mailto:hi@strapi.io?subject=I'd like to support Strapi"
style={{ display: 'none' }}
ref={(a) => { this.aTag = a; }}
>
&nbsp;
</a>
</div>
</div>
</div>
<InstallPluginPopup
history={this.props.history}
isAlreadyInstalled={this.props.isAlreadyInstalled}
isOpen={!isEmpty(this.props.history.location.hash) && replace(this.props.history.location.hash.split('::')[0], '#', '') === this.props.plugin.id}
plugin={this.props.plugin}
/>
</div>
);
}
@ -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,

View File

@ -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;

View File

@ -49,20 +49,12 @@ class Row extends React.Component {
}
render() {
const pluginIcon = this.props.plugin.name !== 'Email' ? (
<div className={styles.frame} style={{ marginRight: '30px' }}>
<span className={styles.helper} />{this.renderImg()}
</div>
) : (
<div className={styles.icoContainer} style={{ marginRight: '30px' }}>
<i className={`fa fa-${this.props.plugin.icon}`} />
</div>
);
return (
<ListRow>
<div className={cn("col-md-11", styles.nameWrapper)}>
{pluginIcon}
<div className={styles.icoContainer} style={{ marginRight: '30px' }}>
<img src={`${this.props.plugin.logo}`} alt="icon" />
</div>
<div className={styles.pluginContent}>
<span>{this.props.plugin.name} &nbsp;</span>
<FormattedMessage id={this.props.plugin.description} />

View File

@ -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 (
<div className={styles.starsContainer}>
<div>
{map(coloredStars, star => <i key={star} className="fa fa-star" />)}
</div>
<div>
{map(emptyStars, s => <i key={s} className="fa fa-star" />)}
</div>
</div>
);
}
StarsContainer.defaultProps = {
ratings: 5,
};
StarsContainer.propTypes = {
ratings: PropTypes.number,
};
export default StarsContainer;

View File

@ -0,0 +1,13 @@
.starsContainer {
display: flex;
margin-right: 10px;
> div {
color: #EEA348;
> i {
margin-right: 2px;
}
}
> div:last-child {
color: #B3B5B9;
}
}

View File

@ -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
<Route path="/plugins/:pluginId" component={PluginPage} />
<Route path="/plugins" component={ComingSoonPage} />
<Route path="/list-plugins" component={ListPluginsPage} exact />
<Route path="/install-plugin" component={ComingSoonPage} exact />
<Route path="/install-plugin" component={InstallPluginPage} exact />
<Route path="/configuration" component={ComingSoonPage} exact />
<Route path="" component={NotFoundPage} />
<Route path="404" component={NotFoundPage} />

View File

@ -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,

View File

@ -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';

View File

@ -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"
}
]
}

View File

@ -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 (
<div>
<OverlayBlocker isOpen={this.props.blockApp}>
<DownloadInfo />
</OverlayBlocker>
<FormattedMessage id="app.components.InstallPluginPage.helmet">
{message => (
<Helmet>
@ -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);
}
}}
/>
))}
</div>
@ -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,
},

View File

@ -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)

View File

@ -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);

View File

@ -19,7 +19,13 @@ const makeSelectInstallPluginPage = () => createSelector(
(substate) => substate.toJS()
);
const makeSelectPluginToDownload = () => createSelector(
selectInstallPluginPageDomain(),
(substate) => substate.get('pluginToDownload'),
);
export default makeSelectInstallPluginPage;
export {
selectInstallPluginPageDomain,
makeSelectPluginToDownload,
};

View File

@ -23,5 +23,5 @@
}
.wrapper {
padding-top: 2.8rem;
padding-top: .3rem;
}

View File

@ -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:

View File

@ -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);

View File

@ -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 */

View File

@ -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.",

View File

@ -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é.",

View File

@ -8,6 +8,14 @@
"policies": []
}
},
{
"method": "POST",
"path": "/plugins/install",
"handler": "Admin.installPlugin",
"config": {
"policies": []
}
},
{
"method": "DELETE",
"path": "/plugins/uninstall/:plugin",

View File

@ -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) => {

View File

@ -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) {

View File

@ -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(
<div className={styles.overlay}>
<div>
{this.props.children}
</div>
</div>,
this.overlayContainer
);
}
return '';
}
}
OverlayBlocker.defaultProps = {
children: '',
isOpen: false,
};
OverlayBlocker.propTypes = {
children: PropTypes.node,
isOpen: PropTypes.bool,
};
export default OverlayBlocker;

View File

@ -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;
}
}

View File

@ -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 */

View File

@ -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;
});
}
});
}

View File

@ -0,0 +1 @@
<svg width="35" height="24" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M1.148 20.03c.567-7.141 2.974-7.675 7.222-1.604 6.372 9.107 7.533-.667 9.19-2.017" stroke="#FFD2B6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><g fill-rule="nonzero"><path d="M21.313 16.48l1.081-1.082-2.792-2.792-1.081 1.081v1.271h1.52v1.521h1.272zm6.214-11.027c0-.174-.087-.26-.262-.26a.275.275 0 0 0-.202.082l-6.44 6.44a.275.275 0 0 0-.082.202c0 .174.087.261.261.261.08 0 .147-.028.202-.083l6.44-6.44a.275.275 0 0 0 .083-.202zm-.642-2.28l4.943 4.942L21.943 18H17v-4.943l9.885-9.885zM35 4.312c0 .42-.147.776-.44 1.07l-1.972 1.971-4.942-4.942 1.972-1.96A1.412 1.412 0 0 1 30.688 0c.419 0 .78.15 1.08.451l2.792 2.78c.293.31.44.67.44 1.082z" fill="#181D29"/><path d="M35 4.313c0 .42-.147.776-.44 1.07l-1.972 1.971-4.942-4.942 1.972-1.96A1.412 1.412 0 0 1 30.688 0c.419 0 .78.15 1.08.451l2.792 2.78c.293.31.44.67.44 1.082z" fill="#FF84B3"/></g></g></svg>

After

Width:  |  Height:  |  Size: 980 B

View File

@ -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.",

View File

@ -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.",

View File

@ -0,0 +1 @@
<svg width="22" height="22" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M8.604.607c.317.623.75 1.155 1.298 1.597.549.443 1.16.754 1.835.934l.012.873c.032 1.745-.494 3.166-1.579 4.264-1.084 1.098-2.5 1.647-4.247 1.647-1 0-1.885-.19-2.657-.571a4.852 4.852 0 0 1-1.858-1.567 7.325 7.325 0 0 1-1.055-2.25A9.927 9.927 0 0 1 0 2.832l.5.369c.276.205.528.387.755.547.228.16.468.309.72.448.251.14.438.21.56.21.333 0 .557-.152.67-.455a6.33 6.33 0 0 1 .701-1.383c.264-.381.547-.692.847-.934.3-.242.658-.436 1.073-.584A6.547 6.547 0 0 1 7.08.736c.422-.062.93-.105 1.523-.13z" id="a"/></defs><g fill="none" fill-rule="evenodd"><g transform="translate(0 12)"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#EABE8D" fill-rule="nonzero" xlink:href="#a"/><path d="M10.18.264c4.086 6.335 2.315 9.584-5.314 9.747-7.629.163-6.185-1.797 4.331-5.88l.982-3.867z" fill="#DB1818" mask="url(#b)"/><path d="M10.564.16C11.422 6.26 8.9 9.507 2.998 9.904c-5.902.397-4.484-1.392 4.255-5.367L10.563.16z" fill="#F1D520" mask="url(#b)"/><path d="M10.196-1.556C9.168 5.281 6.272 8.728 1.507 8.786c-4.764.059-3.209-1.83 4.668-5.663l4.021-4.679z" fill="#32B167" mask="url(#b)"/><path d="M9.506-4.516C9.417 2.713 6.99 6.357 2.226 6.414-2.539 6.474-.784 4.262 7.49-.22l2.016-4.295z" fill="#1279EA" mask="url(#b)"/><path d="M9.449-4.392C8.72 1.871 5.974 5.032 1.209 5.09c-4.764.058-3.398-1.712 4.099-5.311l4.14-4.17z" fill="#4C2DCE" mask="url(#b)"/></g><path d="M19.88 0c.564 0 1.058.186 1.482.56.424.372.635.84.635 1.401 0 .505-.181 1.111-.544 1.817-2.679 5.045-4.555 8.06-5.628 9.047-.783.73-1.662 1.095-2.638 1.095-1.017 0-1.89-.37-2.62-1.113-.73-.742-1.096-1.622-1.096-2.64 0-1.027.371-1.877 1.113-2.551L18.306.65c.476-.433 1-.65 1.573-.65z" fill-rule="nonzero" fill="#553D38"/></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -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",

View File

@ -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",

View File

@ -1,3 +1,4 @@
{
"plugin.description": "Send emails"
"plugin.description.short": "Send emails",
"plugin.description.long": "Send emails"
}

View File

@ -1,3 +1,4 @@
{
"plugin.description": "Envoyez des emails"
"plugin.description.short": "Envoyez des emails",
"plugin.description.long": "Envoyez des emails"
}

View File

@ -0,0 +1 @@
<svg width="54" height="34" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><g fill-rule="nonzero"><path d="M11.392 30.3a1.797 1.797 0 0 0-.22-1.409 1.797 1.797 0 0 0-1.141-.857 1.797 1.797 0 0 0-1.41.22c-.449.27-.734.65-.857 1.142-.122.492-.049.962.22 1.41.27.449.65.734 1.142.857.491.122.961.049 1.41-.22.448-.27.734-.65.856-1.142zm21.224-7.353L8.464 37.459c-.874.525-1.812.663-2.813.413-.983-.245-1.756-.809-2.318-1.692L1.09 32.37c-.548-.86-.695-1.8-.44-2.82.249-1.002.822-1.772 1.72-2.311l24.117-14.492a14.885 14.885 0 0 0 2.02 5.728 14.885 14.885 0 0 0 4.108 4.472z" fill="#181D29"/><path d="M53.663 15.097c-.184.737-.65 1.684-1.401 2.842-1.52 2.31-3.587 3.978-6.2 5.003-2.615 1.024-5.254 1.204-7.918.54-3.497-.872-6.177-2.86-8.043-5.965-1.865-3.105-2.362-6.405-1.49-9.901.871-3.496 2.86-6.177 5.964-8.043 3.105-1.865 6.405-2.362 9.901-1.49 1.096.273 2.205.715 3.328 1.326 1.122.611 2.028 1.304 2.718 2.078.251.283.336.586.256.907-.08.321-.297.548-.651.68l-9.5 2.72-1.584 6.35 4.715 4.397c.109-.033.97-.305 2.582-.816 1.613-.511 3.084-.957 4.414-1.339 1.33-.38 2.08-.55 2.25-.508.283.071.481.22.595.45.113.229.135.485.064.769z" fill="#A1A1A1"/></g><path stroke="#C6C6C6" fill="#D8D8D8" d="M46.521 5.425l3.657 3.41-1.125 4.872-4.781 1.461-3.657-3.41 1.125-4.871z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -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",

View File

@ -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",

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 447.373 124.856"><path d="M59.392 84.949L45.274 93.1m61.279-23.138L87.322 81l.029.049-14.076 7.995-13.925-3.939m-14.004 32.174l.014-24.031-.014 24.031 9.18 5.297 52.05-30.117M23.329 55.09L2.404 43.016m28.999 21.498l-6.961 4.018m11.863 4.472L22.948 80.65m18.287.894l-16.108 9.037m15.234-57.632l-2.886 14.039M86.84 34.679l7.96 4.553-.008 9.219-7.948 4.589-7.939-4.631-.012-9.142zm19.713 35.283l9.824-5.671.065-37.62-32.514-18.81-43.505 24.83M54.38 2.28L2.28 32.425l.124 10.591m35.043 3.923L23.329 55.09" fill="none" stroke="#215cea" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.56"/><path d="M217.23 70.167a16.674 16.674 0 0 1-6.96 13.944 23.419 23.419 0 0 1-7.656 3.587 36.216 36.216 0 0 1-9.628 1.215 51.307 51.307 0 0 1-9.57-.987 44.292 44.292 0 0 1-8.99-2.61l2.32-11.252a47.382 47.382 0 0 0 7.656 2.378 37.023 37.023 0 0 0 8.004.87 16.16 16.16 0 0 0 6.554-1.16 4.19 4.19 0 0 0 2.61-4.176 4.056 4.056 0 0 0-2.553-3.886q-2.552-1.218-6.727-2.959-3.367-1.391-6.496-2.84a23.814 23.814 0 0 1-5.51-3.48 16.169 16.169 0 0 1-3.828-4.757 14.309 14.309 0 0 1-1.45-6.786 16.434 16.434 0 0 1 1.682-7.656 14.719 14.719 0 0 1 4.64-5.336 21.237 21.237 0 0 1 6.96-3.132 34.133 34.133 0 0 1 8.641-1.044 48.31 48.31 0 0 1 9.338.88 31.134 31.134 0 0 1 8.643 3.104l-2.437 11.328a47.347 47.347 0 0 0-7.017-2.494 28.209 28.209 0 0 0-7.366-.986 15.901 15.901 0 0 0-5.394.752 3.048 3.048 0 0 0-2.03 3.183 3.707 3.707 0 0 0 2.435 3.471q2.436 1.158 5.801 2.66 2.436 1.044 5.626 2.373a27.855 27.855 0 0 1 5.974 3.415 20.364 20.364 0 0 1 4.756 5.033 12.945 12.945 0 0 1 1.972 7.348zM219.087 31.26h9.164V16.644h15.891V31.26h16.937v12.412h-16.937v24.245a11.726 11.726 0 0 0 1.45 6.263q1.45 2.322 5.743 2.32a28.445 28.445 0 0 0 8.584-1.276l1.16 11.252a32.326 32.326 0 0 1-6.381 1.74 42.634 42.634 0 0 1-7.655.697 21.308 21.308 0 0 1-8.643-1.567 15.322 15.322 0 0 1-5.8-4.35 17.812 17.812 0 0 1-3.305-6.495 29.903 29.903 0 0 1-1.044-8.121V43.672h-9.164zM303.3 44.832h-2.668a23.137 23.137 0 0 0-9.859 1.856 15.077 15.077 0 0 0-6.265 5.336v35.729h-15.891V31.26h15.311v9.976a15.79 15.79 0 0 1 5.162-7.772q3.538-2.784 9.454-2.784a17.838 17.838 0 0 1 4.756.58zM339.725 81.72a28.045 28.045 0 0 1-7.54 5.104 23.604 23.604 0 0 1-10.208 2.089 17.8 17.8 0 0 1-6.727-1.219 14.668 14.668 0 0 1-5.105-3.363 15.292 15.292 0 0 1-3.248-5.047 17.02 17.02 0 0 1-1.16-6.38q0-7.771 5.22-11.832 5.22-4.06 14.268-4.64 3.48-.231 7.193-.348 3.71-.114 6.727-.232V53.88q0-5.45-2.668-8.294-2.669-2.841-8.351-2.842a32.492 32.492 0 0 0-8.353 1.044 31.835 31.835 0 0 0-6.96 2.668l-3.132-11.136a37.516 37.516 0 0 1 9.744-3.886A47.28 47.28 0 0 1 330.91 30.1q11.251 0 17.69 6.148 6.438 6.15 6.439 18.212v33.293h-15.313zm-11.948-3.596a16.699 16.699 0 0 0 6.265-1.16 20.378 20.378 0 0 0 5.103-2.9v-8.933q-2.436.235-5.22.465-2.784.235-5.104.58a14.924 14.924 0 0 0-5.567 1.914 4.888 4.888 0 0 0-2.088 4.466 4.949 4.949 0 0 0 1.797 4.176 7.65 7.65 0 0 0 4.814 1.392zM366.753 31.26h15.196v7.192a20.668 20.668 0 0 1 6.96-6.09 21.187 21.187 0 0 1 10.208-2.262 20.456 20.456 0 0 1 9.28 2.088 20.833 20.833 0 0 1 7.134 5.858 27.726 27.726 0 0 1 4.582 9.048 39.976 39.976 0 0 1 1.624 11.774 33.714 33.714 0 0 1-2.32 12.933 27.712 27.712 0 0 1-6.264 9.455A25.634 25.634 0 0 1 403.93 87a32.946 32.946 0 0 1-11.31 1.914 34.277 34.277 0 0 1-5.568-.407 37.931 37.931 0 0 1-4.408-.986v24.128h-15.891zm27.028 11.252a13.216 13.216 0 0 0-6.67 1.624 15.954 15.954 0 0 0-4.467 3.596v27.956a16.191 16.191 0 0 0 3.48.87 28.552 28.552 0 0 0 4.176.29q8.119 0 11.832-4.988 3.712-4.986 3.712-12.876a21.71 21.71 0 0 0-2.958-11.774q-2.957-4.698-9.105-4.698zM428.928 14.788a9.048 9.048 0 0 1 2.553-6.496 8.735 8.735 0 0 1 6.611-2.668 9.019 9.019 0 0 1 9.28 9.164 8.996 8.996 0 0 1-2.61 6.438 8.829 8.829 0 0 1-6.67 2.726 8.639 8.639 0 0 1-6.61-2.726 9.101 9.101 0 0 1-2.554-6.438zm1.277 72.965V31.26h15.891v56.493z" fill="#111722"/><path fill="#215cea" d="M54.381 4.916l9.912 5.712 4.62-2.642L55.506.298l-1.125 4.618zM108.854 92.417l-4.57-1.264V79.682l4.56-2.606.01 15.341z"/></svg>
<svg width="28" height="26" xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" fill="none"><path d="M8.453 12.772c-1.54.047-2.799.656-3.778 1.824h-1.91c-.779 0-1.435-.192-1.967-.577C.266 13.634 0 13.071 0 12.33c0-3.354.59-5.032 1.768-5.032.057 0 .263.1.62.3.356.2.82.401 1.39.605.57.205 1.135.307 1.696.307.636 0 1.268-.11 1.896-.328a7.106 7.106 0 0 0-.072.94c0 1.322.385 2.538 1.155 3.65zm15.266 9.08c0 1.14-.347 2.04-1.04 2.701-.694.66-1.616.99-2.766.99H7.455c-1.15 0-2.072-.33-2.765-.99-.694-.66-1.04-1.56-1.04-2.701 0-.504.016-.995.049-1.475.033-.48.1-.998.2-1.554s.225-1.072.377-1.547c.152-.475.357-.938.613-1.39.257-.45.551-.836.884-1.154a3.718 3.718 0 0 1 1.219-.763 4.28 4.28 0 0 1 1.59-.285c.094 0 .298.102.612.307.314.204.66.432 1.04.684.38.252.89.48 1.526.684a6.264 6.264 0 0 0 1.924.307c.646 0 1.288-.103 1.925-.307.636-.204 1.145-.432 1.525-.684.38-.252.727-.48 1.04-.684.314-.205.518-.307.613-.307.58 0 1.11.095 1.59.285.48.19.886.445 1.218.763.333.318.628.703.884 1.155.257.45.461.914.613 1.39.152.474.278.99.378 1.546.1.556.166 1.074.2 1.554.033.48.05.971.05 1.475zm3.65-9.522c0 .741-.267 1.304-.799 1.69-.532.384-1.188.576-1.967.576h-1.91c-.979-1.168-2.238-1.777-3.777-1.824.77-1.112 1.154-2.328 1.154-3.65 0-.275-.024-.588-.071-.94.627.219 1.259.328 1.896.328.56 0 1.126-.102 1.696-.307a9.374 9.374 0 0 0 1.39-.605c.356-.2.563-.3.62-.3 1.178 0 1.767 1.678 1.767 5.032z" fill="#553D38"/><path d="M9.123 3.65a3.516 3.516 0 0 1-1.07 2.58 3.516 3.516 0 0 1-2.58 1.068 3.516 3.516 0 0 1-2.58-1.069 3.516 3.516 0 0 1-1.068-2.58c0-1.007.356-1.867 1.069-2.58A3.516 3.516 0 0 1 5.474 0C6.48 0 7.34.356 8.054 1.07a3.516 3.516 0 0 1 1.069 2.58zm10.035 5.473c0 1.51-.535 2.8-1.604 3.87-1.069 1.069-2.359 1.603-3.87 1.603-1.51 0-2.8-.534-3.87-1.603-1.069-1.07-1.603-2.36-1.603-3.87 0-1.511.534-2.801 1.603-3.87 1.07-1.07 2.36-1.604 3.87-1.604 1.511 0 2.801.535 3.87 1.604 1.07 1.069 1.604 2.359 1.604 3.87zm6.386-5.474a3.516 3.516 0 0 1-1.07 2.58 3.516 3.516 0 0 1-2.58 1.07 3.516 3.516 0 0 1-2.58-1.07 3.516 3.516 0 0 1-1.068-2.58c0-1.007.356-1.867 1.069-2.58A3.516 3.516 0 0 1 21.895 0c1.007 0 1.867.356 2.58 1.07a3.516 3.516 0 0 1 1.069 2.58z" fill="#EBBF8E"/><path d="M12.496 19.991h2.393v-.897c0-.33-.117-.612-.35-.846a1.153 1.153 0 0 0-.847-.35c-.33 0-.612.116-.846.35-.233.234-.35.516-.35.846v.897zm3.889.45v2.691c0 .125-.044.231-.131.318a.433.433 0 0 1-.318.131h-4.487a.433.433 0 0 1-.318-.13.433.433 0 0 1-.131-.319V20.44c0-.124.044-.23.13-.318a.433.433 0 0 1 .319-.13h.15v-.898c0-.573.205-1.066.616-1.477A2.015 2.015 0 0 1 13.692 17c.574 0 1.066.206 1.477.617.412.411.617.904.617 1.477v.897h.15c.125 0 .23.044.318.131.087.088.13.194.13.318z" fill="#FFF"/></g></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 447.373 124.856"><path d="M59.392 84.949L45.274 93.1m61.279-23.138L87.322 81l.029.049-14.076 7.995-13.925-3.939m-14.004 32.174l.014-24.031-.014 24.031 9.18 5.297 52.05-30.117M23.329 55.09L2.404 43.016m28.999 21.498l-6.961 4.018m11.863 4.472L22.948 80.65m18.287.894l-16.108 9.037m15.234-57.632l-2.886 14.039M86.84 34.679l7.96 4.553-.008 9.219-7.948 4.589-7.939-4.631-.012-9.142zm19.713 35.283l9.824-5.671.065-37.62-32.514-18.81-43.505 24.83M54.38 2.28L2.28 32.425l.124 10.591m35.043 3.923L23.329 55.09" fill="none" stroke="#215cea" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.56"/><path d="M217.23 70.167a16.674 16.674 0 0 1-6.96 13.944 23.419 23.419 0 0 1-7.656 3.587 36.216 36.216 0 0 1-9.628 1.215 51.307 51.307 0 0 1-9.57-.987 44.292 44.292 0 0 1-8.99-2.61l2.32-11.252a47.382 47.382 0 0 0 7.656 2.378 37.023 37.023 0 0 0 8.004.87 16.16 16.16 0 0 0 6.554-1.16 4.19 4.19 0 0 0 2.61-4.176 4.056 4.056 0 0 0-2.553-3.886q-2.552-1.218-6.727-2.959-3.367-1.391-6.496-2.84a23.814 23.814 0 0 1-5.51-3.48 16.169 16.169 0 0 1-3.828-4.757 14.309 14.309 0 0 1-1.45-6.786 16.434 16.434 0 0 1 1.682-7.656 14.719 14.719 0 0 1 4.64-5.336 21.237 21.237 0 0 1 6.96-3.132 34.133 34.133 0 0 1 8.641-1.044 48.31 48.31 0 0 1 9.338.88 31.134 31.134 0 0 1 8.643 3.104l-2.437 11.328a47.347 47.347 0 0 0-7.017-2.494 28.209 28.209 0 0 0-7.366-.986 15.901 15.901 0 0 0-5.394.752 3.048 3.048 0 0 0-2.03 3.183 3.707 3.707 0 0 0 2.435 3.471q2.436 1.158 5.801 2.66 2.436 1.044 5.626 2.373a27.855 27.855 0 0 1 5.974 3.415 20.364 20.364 0 0 1 4.756 5.033 12.945 12.945 0 0 1 1.972 7.348zM219.087 31.26h9.164V16.644h15.891V31.26h16.937v12.412h-16.937v24.245a11.726 11.726 0 0 0 1.45 6.263q1.45 2.322 5.743 2.32a28.445 28.445 0 0 0 8.584-1.276l1.16 11.252a32.326 32.326 0 0 1-6.381 1.74 42.634 42.634 0 0 1-7.655.697 21.308 21.308 0 0 1-8.643-1.567 15.322 15.322 0 0 1-5.8-4.35 17.812 17.812 0 0 1-3.305-6.495 29.903 29.903 0 0 1-1.044-8.121V43.672h-9.164zM303.3 44.832h-2.668a23.137 23.137 0 0 0-9.859 1.856 15.077 15.077 0 0 0-6.265 5.336v35.729h-15.891V31.26h15.311v9.976a15.79 15.79 0 0 1 5.162-7.772q3.538-2.784 9.454-2.784a17.838 17.838 0 0 1 4.756.58zM339.725 81.72a28.045 28.045 0 0 1-7.54 5.104 23.604 23.604 0 0 1-10.208 2.089 17.8 17.8 0 0 1-6.727-1.219 14.668 14.668 0 0 1-5.105-3.363 15.292 15.292 0 0 1-3.248-5.047 17.02 17.02 0 0 1-1.16-6.38q0-7.771 5.22-11.832 5.22-4.06 14.268-4.64 3.48-.231 7.193-.348 3.71-.114 6.727-.232V53.88q0-5.45-2.668-8.294-2.669-2.841-8.351-2.842a32.492 32.492 0 0 0-8.353 1.044 31.835 31.835 0 0 0-6.96 2.668l-3.132-11.136a37.516 37.516 0 0 1 9.744-3.886A47.28 47.28 0 0 1 330.91 30.1q11.251 0 17.69 6.148 6.438 6.15 6.439 18.212v33.293h-15.313zm-11.948-3.596a16.699 16.699 0 0 0 6.265-1.16 20.378 20.378 0 0 0 5.103-2.9v-8.933q-2.436.235-5.22.465-2.784.235-5.104.58a14.924 14.924 0 0 0-5.567 1.914 4.888 4.888 0 0 0-2.088 4.466 4.949 4.949 0 0 0 1.797 4.176 7.65 7.65 0 0 0 4.814 1.392zM366.753 31.26h15.196v7.192a20.668 20.668 0 0 1 6.96-6.09 21.187 21.187 0 0 1 10.208-2.262 20.456 20.456 0 0 1 9.28 2.088 20.833 20.833 0 0 1 7.134 5.858 27.726 27.726 0 0 1 4.582 9.048 39.976 39.976 0 0 1 1.624 11.774 33.714 33.714 0 0 1-2.32 12.933 27.712 27.712 0 0 1-6.264 9.455A25.634 25.634 0 0 1 403.93 87a32.946 32.946 0 0 1-11.31 1.914 34.277 34.277 0 0 1-5.568-.407 37.931 37.931 0 0 1-4.408-.986v24.128h-15.891zm27.028 11.252a13.216 13.216 0 0 0-6.67 1.624 15.954 15.954 0 0 0-4.467 3.596v27.956a16.191 16.191 0 0 0 3.48.87 28.552 28.552 0 0 0 4.176.29q8.119 0 11.832-4.988 3.712-4.986 3.712-12.876a21.71 21.71 0 0 0-2.958-11.774q-2.957-4.698-9.105-4.698zM428.928 14.788a9.048 9.048 0 0 1 2.553-6.496 8.735 8.735 0 0 1 6.611-2.668 9.019 9.019 0 0 1 9.28 9.164 8.996 8.996 0 0 1-2.61 6.438 8.829 8.829 0 0 1-6.67 2.726 8.639 8.639 0 0 1-6.61-2.726 9.101 9.101 0 0 1-2.554-6.438zm1.277 72.965V31.26h15.891v56.493z" fill="#111722"/><path fill="#215cea" d="M54.381 4.916l9.912 5.712 4.62-2.642L55.506.298l-1.125 4.618zM108.854 92.417l-4.57-1.264V79.682l4.56-2.606.01 15.341z"/></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -55,7 +55,7 @@ class Plugin extends React.Component { // eslint-disable-line react/prefer-state
<div>
{ icon ? (
<div className={styles.iconContainer}>
<i className={`fa fa-${icon}`} />
<img src={this.props.plugin.information.logo} alt="icon" />
</div>
) : ''}
<div className={styles.name}>{this.props.name}</div>
@ -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,
};

View File

@ -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) {

View File

@ -5,11 +5,6 @@ import { createSelector } from 'reselect';
*/
const selectEditPageDomain = () => (state) => state.get('editPage');
/**
* Other specific selectors
*/
/**
* Default selector used by EditPage
*/

View File

@ -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.",

View File

@ -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.",

View File

@ -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` }] }]);

View File

@ -26,6 +26,7 @@
"dependencies": {
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^8.1.0",
"request": "^2.83.0",
"uuid": "^3.1.0"
},
"devDependencies": {

View File

@ -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];
},

View File

@ -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;

View File

@ -89,4 +89,4 @@
},
"preferGlobal": true,
"license": "MIT"
}
}