Merge branch 'master' into alpha.8.1

This commit is contained in:
Jim LAURIE 2018-01-19 09:15:28 +01:00 committed by GitHub
commit 2e55706fdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 532 additions and 256 deletions

View File

@ -41,7 +41,7 @@ This is the production-ready version of Strapi (v1). You should also consider th
npm install strapi -g
```
Read the [Getting started](http://strapi.io/documentation/getting-started/quick-start.html) page to create your first project using Strapi.
Read the [Getting started](https://strapi.io/getting-started) page to create your first project using Strapi.
## Features
@ -56,7 +56,7 @@ Read the [Getting started](http://strapi.io/documentation/getting-started/quick-
## Philosophy ?
> At [Strapi](http://strapi.io), everything we do we believe in changing the status quo of web development. Our products are simple to use, user friendly and production-ready.
> At [Strapi](https://strapi.io), everything we do we believe in changing the status quo of web development. Our products are simple to use, user friendly and production-ready.
Web and mobile applications needed a powerful, simple to use and production-ready API-driven solution. That's why we created Strapi, an open-source Content Management Framework (CMF) for exposing your content (data, media) accross multi-devices.
@ -78,8 +78,12 @@ For general help using Strapi, please refer to [the official Strapi documentatio
### Professional support
[Strapi Solutions](http://strapi.io), the company behind Strapi, provides a full range of solutions to get better results, faster. We're always looking for the next challenge: coaching, consulting, training, customization, etc. [Drop us an email](mailto:support@strapi.io) to see how we can help you.
[Strapi Solutions](https://strapi.io), the company behind Strapi, provides a full range of solutions to get better results, faster. We're always looking for the next challenge: coaching, consulting, training, customization, etc. [Drop us an email](mailto:support@strapi.io) to see how we can help you.
### Migration
Follow our [migration guides](https://strapi.io/documentation/migration/migration-guide.html) on the website to keep your Strapi projects updated.
## License
[MIT License](LICENSE.md) Copyright (c) 2015-2018 [Strapi Solutions](http://strapi.io/).
[MIT License](LICENSE.md) Copyright (c) 2015-2018 [Strapi Solutions](https://strapi.io/).

View File

@ -12,10 +12,10 @@ The most advanced open-source Content Management Framework to build powerful API
{% endcenter %}
## v3@alpha.7 is available!
## v3@alpha.8 is available!
We've been working on a major update for Strapi during the past months, rewriting the core framework and the dashboard.
This documentation is only related to Strapi v3@alpha.7 ([v1 documentation is still available](http://strapi.io/documentation/1.x.x)).
This documentation is only related to Strapi v3@alpha.8 ([v1 documentation is still available](http://strapi.io/documentation/1.x.x)).
**[Get Started](getting-started/installation.md)**<br />
Learn how to install Strapi and start developing your API.
@ -36,4 +36,3 @@ Understand how to develop your own plugin.
Learn about Strapi's API, the `strapi` object that is available in your backend.
**[Migration guide](migration/migration-guide.md)**<br />
Migrate from v1 to v3@alpha.7.

View File

@ -53,3 +53,4 @@
### Migration
* [Migrating from v1 to v3](migration/migration-guide.md)
* [Migrating from 3.0.0-alpha.7.4 to 3.0.0-alpha.8](migration/migration-guide-alpha-7-4-to-alpha-8.md)

View File

@ -0,0 +1,57 @@
# Migrating from 3.0.0-alpha.7.3 to 3.0.0-alpha.8
**Here are the major changes:**
- Fix deployment process
- Setup database connection on project creation
- Helper for table creation for SQL database
> Feel free to [join us on Slack](http://slack.strapi.io) and ask questions about the migration process.
## Getting started
Install Strapi `alpha.8` globally on your computer. To do so run `npm install strapi@3.0.0-alpha.8 -g`.
When it's done, generate a new empty project `strapi new myNewProject` (don't pay attention to the database configuration).
## Configurations
You will have to update just 1 file: `package.json`
- Edit the scripts section: (only the `setup` line has changed)
```json
{
"scripts": {
"setup": "cd admin && npm run setup",
"start": "node server.js",
"strapi": "node_modules/strapi/bin/strapi.js",
"lint": "node_modules/.bin/eslint api/**/*.js config/**/*.js plugins/**/*.js",
"postinstall": "node node_modules/strapi/lib/utils/post-install.js"
}
}
```
- Edit the Strapi's dependencies version: (move Strapi's dependencies to `3.0.0-alpha.8` version)
```json
{
"dependencies": {
"lodash": "4.x.x",
"strapi": "3.0.0-alpha.8",
"strapi-mongoose": "3.0.0-alpha.8"
}
}
```
## Update the Admin
Delete your old admin folder and replace by the new one.
## Update the Plugins
Copy these 3 files `/plugins/users-permissions/config/jwt.json`, `/plugins/users-permissions/config/roles.json` and `/plugins/users-permissions/models/User.settings.json` **from your old project** and paste them in the corresponding ones in your new project. It is important to save these files.
Then, delete your old `plugins` folder and replace it by the new one.
That's all, you have now upgraded to Strapi `alpha.8`.

View File

@ -330,7 +330,7 @@ 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.
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. This component is automatically displayed when the server needs to restart. You need to disable it in order to override the current design (once disabled it won't show on the other plugins so it's really important to enable it back when the component is unmounting).
### Usage
@ -341,7 +341,7 @@ The OverlayBlocker is a React component that is very useful to block user intera
### 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).
In this example we'll have a button that when clicked 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
@ -373,6 +373,12 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
// Actions required for disabling and enabling the OverlayBlocker
import {
disableGlobalOverlayBlocker,
enableGlobalOverlayBlocker,
} from 'actions/overlayBlocker';
// Design
import Button from 'components/Button';
import OverlayBlocker from 'components/OverlayBlocker';
@ -395,6 +401,16 @@ import makeSelectFooPage from './selectors';
export class FooPage extends React.Component {
componentDidMount() {
// Disable the AdminPage OverlayBlocker in order to give it a custom design (children)
this.props.disableGlobalOverlayBlocker();
}
componentWillUnmount() {
// Enable the AdminPage OverlayBlocker so it is displayed when the server is restarting in the other plugins
this.props.enableGlobalOverlayBlocker();
}
render() {
return (
<div>
@ -410,6 +426,8 @@ export class FooPage extends React.Component {
}
FooPage.propTypes = {
disableGlobalOverlayBlocker: PropTypes.func.isRequired,
enableGlobalOverlayBlocker: PropTypes.func.isRequired,
onButtonClick: PropTypes.func.isRequired,
showOverlayBlocker: PropTypes.bool.isRequired,
};
@ -419,6 +437,8 @@ const mapStateToProps = makeSelectFooPage();
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
disableGlobalOverlayBlocker,
enableGlobalOverlayBlocker,
onButtonClick,
},
dispatch,

View File

@ -2,6 +2,63 @@
Strapi provides helpers so you don't have to develop again and again the same generic functions.
## Auth
`auth.js` lets you get, set and delete data in either the browser's `localStorage` or `sessionStorage`.
### Methods
| Name | Description |
| ---- | ----------- |
| clear(key) | Remove the data in either `localStorage` or `sessionStorage` |
| clearAppStorage() | Remove all data from both storage |
| clearToken() | Remove the user's `jwt Token` in the appropriate browser's storage |
| clearUserInfo() | Remove the user's info from storage |
| get(key) | Get the item in the browser's storage |
| getToken() | Get the user's `jwtToken` |
| getUserInfo() | Get the user's infos |
| set(value, key, isLocalStorage) | Set an item in the `sessionStorage`. If `true` is passed as the 3rd parameter it sets the value in the `localStorage` |
| setToken(value, isLocalStorage) | Set the user's `jwtToken` in the `sessionStorage`. If `true` is passed as the 2nd parameter it sets the value in the `localStorage` |
| setUserInfo(value, isLocalStorage) | Set the user's info in the `sessionStorage`. If `true` is passed as the 2nd parameter it sets the value in the `localStorage` |
```js
import auth from 'utils/auth';
// ...
//
auth.setToken('12345', true); // This will set 1234 in the browser's localStorage associated with the key: jwtToken
```
## Colors
This function allows to darken a color.
### Usage
```js
import { darken } from 'utils/colors';
const linkColor = darken('#f5f5f5', 1.5); // Will darken #F5F5F5 by 1.5% which gives #f2f2f2.
```
## Get URL Query Parameters
The helpers allows to retrieve the query parameters in the URL.
### Example
```js
import getQueryParameters from 'utils/getQueryParameters';
const URL = '/create?source=users-permissions';
const source = getQueryParameters(URL, 'source');
console.log(source); // users-permissions
```
## Request helper
A request helper is available to handle all requests inside a plugin.
@ -368,7 +425,6 @@ function* defaultSaga() {
export default defaultSaga;
```
***
***
### Example with server autoReload watcher

View File

@ -23,7 +23,14 @@ import LanguageProvider from 'containers/LanguageProvider';
import App from 'containers/App';
import { showNotification } from 'containers/NotificationProvider/actions';
import { pluginLoaded, updatePlugin, unsetHasUserPlugin } from 'containers/App/actions';
import {
freezeApp,
pluginLoaded,
unfreezeApp,
unsetHasUserPlugin,
updatePlugin,
} from 'containers/App/actions';
import auth from 'utils/auth';
import configureStore from './store';
import { translationMessages, languages } from './i18n';
@ -175,6 +182,13 @@ const displayNotification = (message, status) => {
store.dispatch(showNotification(message, status));
};
const lockApp = () => {
store.dispatch(freezeApp());
};
const unlockApp = () => {
store.dispatch(unfreezeApp());
};
/**
* Public Strapi object exposed to the `window` object
@ -208,6 +222,8 @@ window.strapi = Object.assign(window.strapi || {}, {
router: history,
languages,
currentLanguage: window.localStorage.getItem('strapi-admin-language') || window.navigator.language || window.navigator.userLanguage || 'en',
lockApp,
unlockApp,
});
const dispatch = store.dispatch;

View File

@ -17,7 +17,13 @@ import { Switch, Route } from 'react-router-dom';
import { get, includes, isFunction, map, omit } from 'lodash';
import { pluginLoaded, updatePlugin } from 'containers/App/actions';
import { selectHasUserPlugin, selectPlugins } from 'containers/App/selectors';
import {
makeSelectBlockApp,
makeSelectShowGlobalAppBlocker,
selectHasUserPlugin,
selectPlugins,
} from 'containers/App/selectors';
import { hideNotification } from 'containers/NotificationProvider/actions';
// Design
@ -30,6 +36,7 @@ import LeftMenu from 'containers/LeftMenu';
import ListPluginsPage from 'containers/ListPluginsPage';
import Logout from 'components/Logout';
import NotFoundPage from 'containers/NotFoundPage';
import OverlayBlocker from 'components/OverlayBlocker';
import PluginPage from 'containers/PluginPage';
import auth from 'utils/auth';
@ -130,6 +137,7 @@ export class AdminPage extends React.Component { // eslint-disable-line react/pr
</Switch>
</Content>
</div>
<OverlayBlocker isOpen={this.props.blockApp && this.props.showGlobalAppBlocker} />
</div>
);
}
@ -149,17 +157,21 @@ AdminPage.defaultProps = {
};
AdminPage.propTypes = {
blockApp: PropTypes.bool.isRequired,
hasUserPlugin: PropTypes.bool,
history: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
pluginLoaded: PropTypes.func.isRequired,
plugins: PropTypes.object.isRequired,
showGlobalAppBlocker: PropTypes.bool.isRequired,
updatePlugin: PropTypes.func.isRequired,
};
const mapStateToProps = createStructuredSelector({
blockApp: makeSelectBlockApp(),
hasUserPlugin: selectHasUserPlugin(),
plugins: selectPlugins(),
showGlobalAppBlocker: makeSelectShowGlobalAppBlocker(),
});
function mapDispatchToProps(dispatch) {

View File

@ -5,13 +5,21 @@
*/
import {
FREEZE_APP,
LOAD_PLUGIN,
UPDATE_PLUGIN,
PLUGIN_LOADED,
PLUGIN_DELETED,
PLUGIN_LOADED,
UNFREEZE_APP,
UNSET_HAS_USERS_PLUGIN,
UPDATE_PLUGIN,
} from './constants';
export function freezeApp() {
return {
type: FREEZE_APP,
};
}
export function loadPlugin(newPlugin) {
return {
type: LOAD_PLUGIN,
@ -19,22 +27,6 @@ export function loadPlugin(newPlugin) {
};
}
export function updatePlugin(pluginId, updatedKey, updatedValue) {
return {
type: UPDATE_PLUGIN,
pluginId,
updatedKey,
updatedValue,
};
}
export function pluginLoaded(newPlugin) {
return {
type: PLUGIN_LOADED,
plugin: newPlugin,
};
}
export function pluginDeleted(plugin) {
return {
type: PLUGIN_DELETED,
@ -42,8 +34,30 @@ export function pluginDeleted(plugin) {
};
}
export function pluginLoaded(newPlugin) {
return {
type: PLUGIN_LOADED,
plugin: newPlugin,
};
}
export function unfreezeApp() {
return {
type: UNFREEZE_APP,
};
}
export function unsetHasUserPlugin() {
return {
type: UNSET_HAS_USERS_PLUGIN,
};
}
export function updatePlugin(pluginId, updatedKey, updatedValue) {
return {
type: UPDATE_PLUGIN,
pluginId,
updatedKey,
updatedValue,
};
}

View File

@ -4,8 +4,10 @@
*
*/
export const UNSET_HAS_USERS_PLUGIN = 'app/App/UNSET_HAS_USERS_PLUGIN';
export const FREEZE_APP = 'app/App/FREEZE_APP';
export const LOAD_PLUGIN = 'app/App/LOAD_PLUGIN';
export const UPDATE_PLUGIN = 'app/App/UPDATE_PLUGIN';
export const PLUGIN_LOADED = 'app/App/PLUGIN_LOADED';
export const PLUGIN_DELETED = 'app/App/PLUGIN_DELETED';
export const UNFREEZE_APP = 'app/App/UNFREEZE_APP';
export const UNSET_HAS_USERS_PLUGIN = 'app/App/UNSET_HAS_USERS_PLUGIN';
export const UPDATE_PLUGIN = 'app/App/UPDATE_PLUGIN';

View File

@ -1,24 +1,43 @@
import { fromJS } from 'immutable';
// Shared constants
import {
UPDATE_PLUGIN,
DISABLE_GLOBAL_OVERLAY_BLOCKER,
ENABLE_GLOBAL_OVERLAY_BLOCKER,
} from 'constants/overlayBlocker';
import { fromJS } from 'immutable';
import {
FREEZE_APP,
PLUGIN_DELETED,
PLUGIN_LOADED,
UNFREEZE_APP,
UNSET_HAS_USERS_PLUGIN,
UPDATE_PLUGIN,
} from './constants';
const initialState = fromJS({
plugins: {},
blockApp: false,
hasUserPlugin: true,
plugins: {},
showGlobalAppBlocker: true,
});
function appReducer(state = initialState, action) {
switch (action.type) {
case DISABLE_GLOBAL_OVERLAY_BLOCKER:
return state.set('showGlobalAppBlocker', false);
case ENABLE_GLOBAL_OVERLAY_BLOCKER:
return state.set('showGlobalAppBlocker', true);
case FREEZE_APP:
return state.set('blockApp', true);
case PLUGIN_LOADED:
return state.setIn(['plugins', action.plugin.id], fromJS(action.plugin));
case UPDATE_PLUGIN:
return state.setIn(['plugins', action.pluginId, action.updatedKey], fromJS(action.updatedValue));
case PLUGIN_DELETED:
return state.deleteIn(['plugins', action.plugin]);
case UNFREEZE_APP:
return state.set('blockApp', false);
case UNSET_HAS_USERS_PLUGIN:
return state.set('hasUserPlugin', false);
default:

View File

@ -19,8 +19,20 @@ const selectHasUserPlugin = () => createSelector(
(appState) => appState.get('hasUserPlugin'),
);
const makeSelectShowGlobalAppBlocker = () => createSelector(
selectApp(),
(appState) => appState.get('showGlobalAppBlocker'),
);
const makeSelectBlockApp = () => createSelector(
selectApp(),
(appState) => appState.get('blockApp'),
);
export {
selectApp,
selectHasUserPlugin,
selectPlugins,
makeSelectBlockApp,
makeSelectShowGlobalAppBlocker,
};

View File

@ -13,6 +13,11 @@ import { bindActionCreators, compose } from 'redux';
import cn from 'classnames';
import { get, isUndefined, map } from 'lodash';
import {
disableGlobalOverlayBlocker,
enableGlobalOverlayBlocker,
} from 'actions/overlayBlocker';
// Design
// import Input from 'components/Input';
import DownloadInfo from 'components/DownloadInfo';
@ -43,12 +48,20 @@ export class InstallPluginPage extends React.Component { // eslint-disable-line
);
componentDidMount() {
// Disable the AdminPage OverlayBlocker in order to give it a custom design (children)
this.props.disableGlobalOverlayBlocker();
// Don't fetch the available plugins if it has already been done
if (!this.props.didFetchPlugins) {
this.props.getPlugins();
}
}
componentWillUnmount() {
// Enable the AdminPage OverlayBlocker so it is displayed when the server is restarting
this.props.enableGlobalOverlayBlocker();
}
render() {
return (
<div>
@ -118,7 +131,9 @@ InstallPluginPage.propTypes = {
availablePlugins: PropTypes.array.isRequired,
blockApp: PropTypes.bool.isRequired,
didFetchPlugins: PropTypes.bool.isRequired,
disableGlobalOverlayBlocker: PropTypes.func.isRequired,
downloadPlugin: PropTypes.func.isRequired,
enableGlobalOverlayBlocker: PropTypes.func.isRequired,
getPlugins: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
// onChange: PropTypes.func.isRequired,
@ -130,7 +145,9 @@ const mapStateToProps = makeSelectInstallPluginPage();
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
disableGlobalOverlayBlocker,
downloadPlugin,
enableGlobalOverlayBlocker,
getPlugins,
onChange,
},

View File

@ -37,7 +37,7 @@ export class PluginPage extends React.Component { // eslint-disable-line react/p
}
return (
<ErrorBoundary key={plugin.id}>
<ErrorBoundary key={plugin.id}>
<Elem {...this.props} {...blockerComponentProps} />
</ErrorBoundary>
);

View File

@ -60,6 +60,9 @@
"components.ErrorBoundary.title": "Something wen't wrong...",
"components.OverlayBlocker.title": "Waiting for restart...",
"components.OverlayBlocker.description": "You're using a feature that needs the server to restart. Please wait until the server is up.",
"components.ProductionBlocker.header": "This plugin is only available in development.",
"components.ProductionBlocker.description": "For safety we have to disable this plugin in other environments.",

View File

@ -58,6 +58,9 @@
"components.AutoReloadBlocker.header": "L'autoReload doit être activé pour ce plugin.",
"components.AutoReloadBlocker.description": "Ouvrez le fichier suivant pour activer cette fonctionnalité.",
"components.OverlayBlocker.title": "Le serveur est en train de redémarrer",
"components.OverlayBlocker.description": "Vous utilisez une fonctionnalité qui nécessite le redémarrage du server. Merci d'attendre que celui-ci ait redémarré.",
"components.ErrorBoundary.title": "Une erreur est survenue...",
"components.ProductionBlocker.header": "Ce plugin est disponible uniquement en développement.",

View File

@ -0,0 +1,23 @@
/*
*
* Shared OverlayBlocker actions
*
*/
import {
DISABLE_GLOBAL_OVERLAY_BLOCKER,
ENABLE_GLOBAL_OVERLAY_BLOCKER,
} from '../constants/overlayBlocker';
export function disableGlobalOverlayBlocker() {
return {
type: DISABLE_GLOBAL_OVERLAY_BLOCKER,
};
}
export function enableGlobalOverlayBlocker() {
return {
type: ENABLE_GLOBAL_OVERLAY_BLOCKER,
};
}

View File

@ -4,7 +4,7 @@
height: 6rem;
width: 6.5rem;
line-height: 6rem;
z-index: 999999;
z-index: 999;
text-align: center;
background-color: #FFFFFF;
color: #81848A;

View File

@ -7,6 +7,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import cn from 'classnames';
import styles from './styles.scss';
@ -22,11 +24,32 @@ class OverlayBlocker extends React.Component {
}
render() {
const content = this.props.children ? (
this.props.children
) : (
<div className={styles.container}>
<div className={styles.icoContainer}>
<i className="fa fa-refresh" />
</div>
<div>
<h4>
<FormattedMessage id="components.OverlayBlocker.title" />
</h4>
<p>
<FormattedMessage id="components.OverlayBlocker.description" />
</p>
<div className={styles.buttonContainer}>
<a className={cn(styles.primary, 'btn')} href="https://strapi.io/documentation/configurations/configurations.html#server" target="_blank">Read the documentation</a>
</div>
</div>
</div>
);
if (this.props.isOpen) {
return ReactDOM.createPortal(
<div className={styles.overlay}>
<div>
{this.props.children}
{content}
</div>
</div>,
this.overlayContainer

View File

@ -1,3 +1,35 @@
.container {
display: flex;
justify-content: center;
> div {
padding-top: 2.5rem;
> h4 {
font-size: 24px;
font-weight: 700;
line-height: 24px;
margin-bottom: 0;
}
> p {
margin-top: -1px;
font-size: 14px;
color: #919BAE;
}
}
}
.icoContainer {
padding-top: 0 !important;
font-size: 4.2rem;
color: #323740;
margin-right: 20px;
line-height: 9.3rem;
> i {
-webkit-animation:spin 4s linear infinite;
-moz-animation:spin 4s linear infinite;
animation:spin 4s linear infinite;
}
}
.overlay {
position: fixed;
top: 0;
@ -5,12 +37,86 @@
bottom: 0;
left: 0;
z-index: 1040;
background-color: rgba(0, 0, 0, 0.5);
&:before {
position: fixed;
content: '';
top: 6rem;
right: 0;
bottom: 0;
left: 0;
background: linear-gradient(rgba(0,0,0, 15) 0%, rgba(0,0,0,0) 100%);
opacity: 0.5;
}
&:after {
content:'';
position: fixed;
top: 6rem;
right: 0;
bottom: 0;
left: 24rem;
background: linear-gradient(#FBFBFB 20%, rgba(0, 0, 100, 0) 100%);
box-shadow: inset 0px 2px 4px rgba(0,0,0,0.1);
box-shadow: inset 0 1px 2px 0 rgba(40, 42, 49, 0.16);
}
> div {
position: fixed;
top: 17.5rem;
top: 11.5rem;
left: 50%;
margin-left: -24rem;
margin-left: -17.5rem;
z-index: 1100;
}
}
.buttonContainer {
padding-top: 3.9rem;
> a {
height: 30px;
font-size: 13px;
}
}
.primary {
min-width: 15rem;
padding-top: 4px;
padding-left: 1.6rem;
padding-right: 1.6rem;
border-radius: 0.3rem;
border: none;
background: linear-gradient(315deg, #0097F6 0%, #005EEA 100%);
color: white;
font-family: Lato;
font-weight: 600;
-webkit-font-smoothing: antialiased;
cursor: pointer;
> i {
margin-right: 1.3rem;
font-weight: 600;
padding-top: 1px;
}
&:before {
content: '\f02d';
font-family: 'FontAwesome';
font-weight: 600;
font-size: 1.3rem;
margin-right: 13px;
}
&:active {
box-shadow: inset 1px 1px 3px rgba(0,0,0,.15);
}
&:focus {
outline: 0;
}
&:hover {
color: white;
}
}
@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }

View File

@ -0,0 +1,8 @@
/*
*
* Shared OverlayBlocker constants
*
*/
export const DISABLE_GLOBAL_OVERLAY_BLOCKER = 'strapiHelperPlugin/OverlayBlocker/DISABLE_GLOBAL_OVERLAY_BLOCKER';
export const ENABLE_GLOBAL_OVERLAY_BLOCKER = 'strapiHelperPlugin/OverlayBlocker/ENABLE_GLOBAL_OVERLAY_BLOCKER';

View File

@ -1,5 +1,6 @@
import 'whatwg-fetch';
import auth from 'utils/auth';
/**
* Parses the JSON returned by a network request
*
@ -86,6 +87,8 @@ function serverRestartWatcher(response) {
}
})
.then(() => {
// Hide the global OverlayBlocker
strapi.unlockApp();
resolve(response);
})
.catch(err => {
@ -141,6 +144,8 @@ export default function request(url, options = {}, shouldWatchServerRestart = fa
.then(parseJSON)
.then((response) => {
if (shouldWatchServerRestart) {
// Display the global OverlayBlocker
strapi.lockApp();
return serverRestartWatcher(response);
}

View File

@ -45,7 +45,11 @@ class TableList extends React.Component { // eslint-disable-line react/prefer-st
</div>
</li>
{map(this.props.rowItems, (rowItem, key) => (
<TableListRow key={key} rowItem={rowItem} onDelete={this.props.onHandleDelete} />
<TableListRow
key={key}
onDelete={this.props.onHandleDelete}
rowItem={rowItem}
/>
))}
</ul>
</div>

View File

@ -20,6 +20,7 @@ import NotFoundPage from 'containers/NotFoundPage';
import formSaga from 'containers/Form/sagas';
import formReducer from 'containers/Form/reducer';
// Other containers actions
import { makeSelectShouldRefetchContentType } from 'containers/Form/selectors';
// Utils

View File

@ -7,15 +7,13 @@ export function* deleteContentType(action) {
try {
if (action.sendRequest) {
const requestUrl = `/content-type-builder/models/${action.itemToDelete}`;
const response = yield call(request, requestUrl, { method: 'DELETE' }, true);
yield call(request, requestUrl, { method: 'DELETE' });
if (action.updateLeftMenu) {
if (response.ok && action.updateLeftMenu) {
action.updatePlugin('content-manager', 'leftMenuSections', action.leftMenuContentTypes);
strapi.notification.success('content-type-builder.notification.success.contentTypeDeleted');
}
strapi.notification.success('content-type-builder.notification.success.contentTypeDeleted');
}
} catch(error) {
strapi.notification.error('content-type-builder.notification.error.message');
}

View File

@ -41,6 +41,6 @@ const selectLocationState = () => {
export {
selectLocationState,
makeSelectLoading,
makeSelectModels,
makeSelectMenu,
makeSelectModels,
};

View File

@ -254,6 +254,7 @@ export class Form extends React.Component { // eslint-disable-line react/prefer-
this.props.contentTypeCreate(contentType);
}
this.setState({ showModal: false });
return this.props.contentTypeEdit(this.context);
}

View File

@ -69,8 +69,8 @@ export class HomePage extends React.Component { // eslint-disable-line react/pre
title={title}
buttonLabel={'content-type-builder.button.contentType.add'}
onButtonClick={this.handleButtonClick}
rowItems={this.props.models}
onHandleDelete={this.handleDelete}
rowItems={this.props.models}
/>
);
}

View File

@ -76,8 +76,6 @@ export class ModelPage extends React.Component { // eslint-disable-line react/pr
if (this.props.updatedContentType !== nextProps.updatedContentType) {
if (this.state.contentTypeTemporary && storeData.getContentType()) {
this.props.modelFetchSucceeded({ model: storeData.getContentType() });
} else {
this.fetchModel(nextProps);
}
}
@ -86,24 +84,24 @@ export class ModelPage extends React.Component { // eslint-disable-line react/pr
}
}
componentWillUpdate(nextProps) {
if (!isEmpty(nextProps.menu)) {
const allowedPaths = nextProps.menu.reduce((acc, current) => {
const models = current.items.reduce((acc, current) => {
acc.push(current.name);
return acc;
}, []);
return acc.concat(models);
}, []);
const shouldRedirect = allowedPaths.filter(el => el === this.props.match.params.modelName.split('&')[0]).length === 0;
if (shouldRedirect) {
this.props.history.push('/404');
}
}
}
// componentWillUpdate(nextProps) {
// if (!isEmpty(nextProps.menu)) {
// const allowedPaths = nextProps.menu.reduce((acc, current) => {
// const models = current.items.reduce((acc, current) => {
// acc.push(current.name);
//
// return acc;
// }, []);
// return acc.concat(models);
// }, []);
//
// const shouldRedirect = allowedPaths.filter(el => el === this.props.match.params.modelName.split('&')[0]).length === 0;
//
// if (shouldRedirect) {
// this.props.history.push('/404');
// }
// }
// }
componentDidUpdate(prevProps) {
if (prevProps.match.params.modelName !== this.props.match.params.modelName) {
@ -347,7 +345,7 @@ ModelPage.propTypes = {
cancelChanges: PropTypes.func.isRequired,
checkIfTableExists: PropTypes.func.isRequired,
deleteAttribute: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
// history: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
match: PropTypes.object.isRequired,
menu: PropTypes.array.isRequired,

View File

@ -140,7 +140,6 @@ export function* submitChanges(action) {
}
yield put(submitActionSucceeded());
yield put(resetShowButtonsProps());
// Remove loader
yield put(unsetButtonLoader());

View File

@ -3,7 +3,7 @@ import request from 'utils/request';
const shouldRenderCompo = (plugin) => new Promise((resolve, reject) => {
request(`${strapi.backendURL}/content-type-builder/autoReload`)
.then(response => {
plugin.preventComponentRendering = !response.autoReload;
plugin.preventComponentRendering = !response.autoReload.enabled;
plugin.blockerComponentProps = {
blockerComponentTitle: 'components.AutoReloadBlocker.header',
blockerComponentDescription: 'components.AutoReloadBlocker.description',

View File

@ -5,26 +5,12 @@
*/
import {
MENU_FETCH,
ENVIRONMENTS_FETCH,
MENU_FETCH_SUCCEEDED,
ENVIRONMENTS_FETCH_SUCCEEDED,
MENU_FETCH_SUCCEEDED,
MENU_FETCH,
} from './constants';
export function menuFetch() {
return {
type: MENU_FETCH,
};
}
export function fetchMenuSucceeded(menu) {
return {
type: MENU_FETCH_SUCCEEDED,
menu,
};
}
export function environmentsFetch() {
return {
type: ENVIRONMENTS_FETCH,
@ -37,3 +23,16 @@ export function environmentsFetchSucceeded(environments) {
environments,
};
}
export function fetchMenuSucceeded(menu) {
return {
type: MENU_FETCH_SUCCEEDED,
menu,
};
}
export function menuFetch() {
return {
type: MENU_FETCH,
};
}

View File

@ -4,7 +4,7 @@
*
*/
export const MENU_FETCH = 'SettingsManager/App/MENU_FETCH';
export const ENVIRONMENTS_FETCH = 'SettingsManager/App/ENVIRONMENTS_FETCH';
export const MENU_FETCH_SUCCEEDED = 'SettingsManager/App/MENU_FETCH_SUCCEEDED';
export const ENVIRONMENTS_FETCH_SUCCEEDED = 'SettingsManager/App/ENVIRONMENTS_FETCH_SUCCEEDED';
export const MENU_FETCH = 'SettingsManager/App/MENU_FETCH';
export const MENU_FETCH_SUCCEEDED = 'SettingsManager/App/MENU_FETCH_SUCCEEDED';

View File

@ -6,24 +6,23 @@
import { fromJS, List } from 'immutable';
import {
MENU_FETCH_SUCCEEDED,
ENVIRONMENTS_FETCH_SUCCEEDED,
MENU_FETCH_SUCCEEDED,
} from './constants';
/* eslint-disable new-cap */
const initialState = fromJS({
sections: List(), // eslint-disable-line new-cap
environments: List(),
sections: List([]),
environments: List([]),
loading: true,
});
function appReducer(state = initialState, action) {
switch (action.type) {
case MENU_FETCH_SUCCEEDED:
return state.set('sections', List(action.menu.sections)).set('loading', false);
case ENVIRONMENTS_FETCH_SUCCEEDED:
return state
.set('environments', List(action.environments.environments));
case MENU_FETCH_SUCCEEDED:
return state.set('sections', List(action.menu.sections)).set('loading', false);
default:
return state;
}

View File

@ -37,5 +37,10 @@ const makeSelectLoading = () => createSelector(
(globalSate) => globalSate.get('loading'),
);
export { selectLocationState, makeSelectSections, makeSelectEnvironments, makeSelectLoading };
export {
makeSelectEnvironments,
makeSelectLoading,
makeSelectSections,
selectLocationState,
};
export default selectGlobalDomain;

View File

@ -1,5 +1,4 @@
import { LOCATION_CHANGE } from 'react-router-redux';
import { forEach, set, map, replace } from 'lodash';
import { call, take, put, fork, cancel, select, takeLatest } from 'redux-saga/effects';
import request from 'utils/request';
@ -86,7 +85,6 @@ export function* deleteLanguage(action) {
method: 'DELETE',
};
const requestUrl = `/settings-manager/configurations/languages/${action.languageToDelete}`;
const resp = yield call(request, requestUrl, opts, true);
if (resp.ok) {
@ -152,7 +150,6 @@ export function* fetchLanguages() {
export function* postLanguage() {
try {
const newLanguage = yield select(makeSelectModifiedData());
const body = {
name: newLanguage['language.defaultLocale'],
};
@ -161,12 +158,11 @@ export function* postLanguage() {
method: 'POST',
};
const requestUrl = '/settings-manager/configurations/languages';
const resp = yield call(request, requestUrl, opts, true);
if (resp.ok) {
strapi.notification.success('settings-manager.strapi.notification.success.languageAdd');
yield put(languageActionSucceeded());
strapi.notification.success('settings-manager.strapi.notification.success.languageAdd');
}
} catch(error) {
yield put(languageActionError());
@ -187,7 +183,6 @@ export function* postDatabase(action) {
body,
};
const requestUrl = `/settings-manager/configurations/databases/${action.endPoint}`;
const resp = yield call(request, requestUrl, opts, true);
if (resp.ok) {
@ -203,7 +198,6 @@ export function* postDatabase(action) {
});
yield put(databaseActionError(formErrors));
strapi.notification.error('settings-manager.strapi.notification.error');
}
}
@ -221,13 +215,13 @@ export function* settingsEdit(action) {
const resp = yield call(request, requestUrl, opts, true);
if (resp.ok) {
strapi.notification.success('settings-manager.strapi.notification.success.settingsEdit');
yield put(editSettingsSucceeded());
yield put(unsetLoader());
strapi.notification.success('settings-manager.strapi.notification.success.settingsEdit');
}
} catch(error) {
strapi.notification.error('settings-manager.strapi.notification.error');
yield put(unsetLoader());
strapi.notification.error('settings-manager.strapi.notification.error');
}
}
@ -237,7 +231,6 @@ export function* fetchSpecificDatabase(action) {
method: 'GET',
};
const requestUrl = `/settings-manager/configurations/databases/${action.databaseName}/${action.endPoint}`;
const data = yield call(request, requestUrl, opts);
yield put(specificDatabaseFetchSucceeded(data));

View File

@ -1,126 +0,0 @@
import 'whatwg-fetch';
import { startsWith } from 'lodash';
import auth from 'utils/auth';
/**
* Parses the JSON returned by a network request
*
* @param {object} response A response from a network request
*
* @return {object} The parsed JSON from the request
*/
function parseJSON(response) {
return response.json();
}
/**
* Checks if a network request came back fine, and throws an error if not
*
* @param {object} response A response from a network request
*
* @return {object|undefined} Returns either the response, or throws an error
*/
function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
}
return parseJSON(response).then(responseFormatted => {
const error = new Error(response.statusText);
error.response = response;
error.response.payload = responseFormatted;
throw error;
});
}
/**
* Format query params
*
* @param params
* @returns {string}
*/
function formatQueryParams(params) {
return Object.keys(params)
.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
.join('&');
}
/**
* Server restart watcher
* @param response
* @returns {object} the response data
*/
function serverRestartWatcher(response) {
return new Promise((resolve) => {
fetch(`${strapi.backendURL}/_health`, {
method: 'HEAD',
mode: 'no-cors',
headers: {
'Content-Type': 'application/json',
'Keep-Alive': false,
},
})
.then(() => {
resolve(response);
})
.catch(() => {
setTimeout(() => {
return serverRestartWatcher(response)
.then(resolve);
}, 100);
});
});
}
/**
* Requests a URL, returning a promise
*
* @param {string} url The URL we want to request
* @param {object} [options] The options we want to pass to "fetch"
*
* @return {object} The response data
*/
export default function request(url, options, shouldWatchServerRestart = false) {
const optionsObj = options || {};
// Set headers
optionsObj.headers = {
'Content-Type': 'application/json',
'X-Forwarded-Host': 'strapi',
};
const token = auth.getToken();
if (token) {
optionsObj.headers = Object.assign({
'Authorization': `Bearer ${token}`,
}, optionsObj.headers);
}
// Add parameters to url
let urlFormatted = startsWith(url, '/')
? `${strapi.backendURL}${url}`
: url;
if (optionsObj && optionsObj.params) {
const params = formatQueryParams(optionsObj.params);
urlFormatted = `${url}?${params}`;
}
// Stringify body object
if (optionsObj && optionsObj.body) {
optionsObj.body = JSON.stringify(optionsObj.body);
}
return fetch(urlFormatted, optionsObj)
.then(checkStatus)
.then(parseJSON)
.then((response) => {
if (shouldWatchServerRestart) {
return serverRestartWatcher(response);
}
return response;
});
}

View File

@ -48,7 +48,7 @@ const generateListTitle = (data, settingType) => {
}
};
function List({ data, deleteActionSucceeded, deleteData, noButton, onButtonClick, settingType }) {
function List({ data, deleteData, noButton, onButtonClick, settingType }) {
return (
<div className={styles.list}>
<div className={styles.flex}>
@ -69,7 +69,6 @@ function List({ data, deleteActionSucceeded, deleteData, noButton, onButtonClick
<ul className={noButton ? styles.listPadded : ''}>
{map(data, item => (
<ListRow
deleteActionSucceeded={deleteActionSucceeded}
deleteData={deleteData}
item={item}
key={item.name}
@ -89,7 +88,6 @@ List.defaultProps = {
List.propTypes = {
data: PropTypes.array.isRequired,
deleteActionSucceeded: PropTypes.bool.isRequired,
deleteData: PropTypes.func.isRequired,
noButton: PropTypes.bool,
onButtonClick: PropTypes.func,

View File

@ -19,12 +19,6 @@ import styles from './styles.scss';
class ListRow extends React.Component { // eslint-disable-line react/prefer-stateless-function
state = { showModalDelete: false };
componentWillReceiveProps(nextProps) {
if (nextProps.deleteActionSucceeded !== this.props.deleteActionSucceeded) {
this.setState({ showModalDelete: false });
}
}
// Roles that can't be deleted && modified
// Don't delete this line
protectedRoleIDs = ['0'];
@ -148,7 +142,10 @@ class ListRow extends React.Component { // eslint-disable-line react/prefer-stat
}
}
handleDelete = () => this.props.deleteData(this.props.item, this.props.settingType);
handleDelete = () => {
this.props.deleteData(this.props.item, this.props.settingType);
this.setState({ showModalDelete: false });
}
render() {
return (
@ -176,7 +173,6 @@ ListRow.defaultProps = {
};
ListRow.propTypes = {
deleteActionSucceeded: PropTypes.bool.isRequired,
deleteData: PropTypes.func.isRequired,
item: PropTypes.object,
settingType: PropTypes.string,

View File

@ -117,7 +117,7 @@ Plugin.propTypes = {
plugin: PropTypes.shape({
description: PropTypes.string,
information: PropTypes.shape({
logo: PropTypes.string.isRequired,
logo: PropTypes.string,
}),
}),
pluginSelected: PropTypes.string.isRequired,

View File

@ -5,5 +5,3 @@
*/
// const selectGlobalDomain = () => state => state.get('global');
export {};

View File

@ -8,6 +8,7 @@ import {
take,
takeLatest,
} from 'redux-saga/effects';
import request from 'utils/request';
import {
@ -98,7 +99,7 @@ export function* submit() {
};
const requestURL = actionType === 'POST' ? '/users-permissions/roles' : `/users-permissions/roles/${roleId}`;
const response = yield call(request, requestURL, opts);
const response = yield call(request, requestURL, opts, true);
if (response.ok) {
yield put(submitSucceeded());

View File

@ -116,7 +116,6 @@ export class HomePage extends React.Component {
<EditForm onChange={this.props.onChange} values={this.props.modifiedData} /> : (
<List
data={this.props.data}
deleteActionSucceeded={this.props.deleteActionSucceeded}
deleteData={this.props.deleteData}
noButton={noButtonList}
onButtonClick={this.handleButtonClick}
@ -161,7 +160,6 @@ HomePage.defaultProps = {};
HomePage.propTypes = {
data: PropTypes.array.isRequired,
deleteActionSucceeded: PropTypes.bool.isRequired,
deleteData: PropTypes.func.isRequired,
fetchData: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,

View File

@ -17,7 +17,6 @@ import {
const initialState = fromJS({
data: List([]),
dataToDelete: Map({}),
deleteActionSucceeded: false,
deleteEndPoint: '',
initialData: Map({}),
modifiedData: Map({}),
@ -34,8 +33,7 @@ function homePageReducer(state = initialState, action) {
return state
.update('data', list => list.splice(action.indexDataToDelete, 1))
.set('deleteEndPoint', '')
.set('dataToDelete', Map({}))
.set('deleteActionSucceeded', !state.get('deleteActionSucceeded'));
.set('dataToDelete', Map({}));
case FETCH_DATA_SUCCEEDED:
return state.set('data', List(action.data));
case ON_CHANGE:

View File

@ -1,6 +1,7 @@
import { LOCATION_CHANGE } from 'react-router-redux';
import { findIndex } from 'lodash';
import { takeLatest, put, fork, take, cancel, select, call } from 'redux-saga/effects';
import request from 'utils/request';
import {
@ -8,6 +9,7 @@ import {
fetchDataSucceeded,
setForm,
} from './actions';
import {
DELETE_DATA,
FETCH_DATA,
@ -32,8 +34,7 @@ export function* dataDelete() {
if (indexDataToDelete !== -1) {
const id = dataToDelete.id;
const requestURL = `/users-permissions/${endPointAPI}/${id}`;
// TODO watchServerRestart
const response = yield call(request, requestURL, { method: 'DELETE' });
const response = yield call(request, requestURL, { method: 'DELETE' }, true);
if (response.ok) {
yield put(deleteDataSucceeded(indexDataToDelete));

View File

@ -20,9 +20,14 @@ module.exports = {
return ctx.badRequest(null, [{ messages: [{ id: 'Cannot be empty' }] }]);
}
strapi.reload.isWatching = false;
try {
await strapi.plugins['users-permissions'].services.userspermissions.createRole(ctx.request.body);
ctx.send({ ok: true });
strapi.reload();
} catch(err) {
ctx.badRequest(null, [{ messages: [{ id: 'An error occured' }] }]);
}
@ -50,9 +55,14 @@ module.exports = {
return ctx.badRequest(null, [{ messages: [{ id: 'Unauthorized' }] }]);
}
strapi.reload.isWatching = false;
try {
await strapi.plugins['users-permissions'].services.userspermissions.deleteRole(role);
return ctx.send({ ok: true });
ctx.send({ ok: true });
strapi.reload();
} catch(err) {
return ctx.badRequest(null, [{ messages: [{ id: 'Bad request' }] }]);
}
@ -139,9 +149,14 @@ module.exports = {
return ctx.badRequest(null, [{ messages: [{ id: 'Bad request' }] }]);
}
strapi.reload.isWatching = false;
try {
await strapi.plugins['users-permissions'].services.userspermissions.updateRole(roleId, ctx.request.body);
ctx.send({ ok: true });
strapi.reload();
} catch(error) {
ctx.badRequest(null, [{ messages: [{ id: 'An error occurred' }] }]);
}