Merge branch 'master' into master

This commit is contained in:
Jim LAURIE 2018-07-16 18:59:08 +02:00 committed by GitHub
commit dc9de9abd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
112 changed files with 2309 additions and 305 deletions

View File

@ -62,6 +62,8 @@ The panel will be available through [http://localhost:1337/dashboard](http://loc
### Development mode
Note that to modify the administration panel, your project needs to be created with using the `dev` flag, an example of such would be: `strapi new strapi --dev`.
**#1 — Install its own dependencies**
Run `npm install` from the `./admin` folder.
@ -99,14 +101,16 @@ Note: make sure the size of your image is the same as the existing one (434px x
## Build
To build the administration, run the following command from the `./admin` folder:
To build the administration, run the following command from the root directory of your project.
```
npm run build
npm run setup
```
This will replace the folder's content located at `./admin/admin/build`. Visit http://localhost:1337/admin/ to make sure your updates have been taken in account.
After you have built the admininistration you can now create a new project to develop your API with the changes implemented. **Note:** You should now create a project without `--dev`
***
## Deployment

View File

@ -11,7 +11,7 @@ Create a new project
```bash
strapi new <name>
options: [--dev|--dbclient=<dbclient> --dbhost=<dbhost> --dbport=<dbport> --dbname=<dbname> --dbusername=<dbusername> --dbpassword=<dbpassword>]
options: [--dev|--dbclient=<dbclient> --dbhost=<dbhost> --dbport=<dbport> --dbname=<dbname> --dbusername=<dbusername> --dbpassword=<dbpassword> --dbssl=<dbssl> --dbauth=<dbauth>]
```
- **strapi new &#60;name&#62;**<br/>
@ -20,8 +20,8 @@ options: [--dev|--dbclient=<dbclient> --dbhost=<dbhost> --dbport=<dbport> --dbna
- **strapi new &#60;name&#62; --dev**<br/>
Generates a new project called **&#60;name&#62;** and creates symlinks for the `./admin` folder and each plugin inside the `./plugin` folder. It means that the Strapi's development workflow has been set up on the machine earlier.
- **strapi new &#60;name&#62; --dbclient=&#60;dbclient&#62; --dbhost=&#60;dbhost&#62; --dbport=&#60;dbport&#62; --dbname=&#60;dbname&#62; --dbusername=&#60;dbusername&#62; --dbpassword=&#60;dbpassword&#62;**<br/>
Generates a new project called **&#60;name&#62;** and skip the interactive database configuration and initilize with these options. **&#60;dbclient&#62;** can be `mongo`, `postgres`, `mysql`, `sqlite3` or `redis`. **&#60;dbusername&#62;** and **&#60;dbpassword&#62;** are optional.
- **strapi new &#60;name&#62; --dbclient=&#60;dbclient&#62; --dbhost=&#60;dbhost&#62; --dbport=&#60;dbport&#62; --dbname=&#60;dbname&#62; --dbusername=&#60;dbusername&#62; --dbpassword=&#60;dbpassword&#62; --dbssl=&#60;dbssl&#62; --dbauth=&#60;dbauth&#62;**<br/>
Generates a new project called **&#60;name&#62;** and skip the interactive database configuration and initilize with these options. **&#60;dbclient&#62;** can be `mongo`, `postgres`, `mysql`, `sqlite3` or `redis`. **&#60;dbssl&#62;** and **&#60;dbauth&#62;** are optional.
See the [CONTRIBUTING guide](https://github.com/strapi/strapi/blob/master/CONTRIBUTING.md) for more details.

View File

@ -69,13 +69,17 @@ function LeftMenuLinkContainer({ layout, plugins }) {
// Check if the plugins list is empty or not and display plugins by name
const pluginsLinks = !isEmpty(pluginsObject) ? (
map(sortBy(pluginsObject, 'name'), plugin => {
if (plugin.id !== 'email' && plugin.id !== 'content-manager' && plugin.id !== 'settings-manager') {
if (plugin.id !== 'email' && plugin.id !== 'settings-manager') {
const basePath = `/plugins/${get(plugin, 'id')}`;
// NOTE: this should be dynamic
const destination = plugin.id === 'content-manager' ? `${basePath}/ctm-configurations` : basePath;
return (
<LeftMenuLink
key={get(plugin, 'id')}
icon={get(plugin, 'icon') || 'plug'}
label={get(plugin, 'name')}
destination={`/plugins/${get(plugin, 'id')}`}
destination={destination}
/>
);
}

View File

@ -16,7 +16,7 @@ import Row from 'components/Row';
import styles from './styles.scss';
class ListPlugins extends React.Component {
class ListPlugins extends React.PureComponent {
render() {
const listSize = size(this.props.plugins);
let titleType = listSize === 1 ? 'singular' : 'plural';

View File

@ -8,7 +8,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import { FormattedMessage } from 'react-intl';
import { isEmpty } from 'lodash';
import { includes, isEmpty } from 'lodash';
// Design
import IcoContainer from 'components/IcoContainer';
@ -17,6 +17,8 @@ import PopUpWarning from 'components/PopUpWarning';
import styles from './styles.scss';
const PLUGINS_WITH_CONFIG = ['content-manager', 'email', 'upload'];
class Row extends React.Component {
state = { showModal: false };
@ -33,8 +35,10 @@ class Row extends React.Component {
render() {
// const uploadPath = `/plugins/upload/configurations/${this.context.currentEnvironment}`;
const settingsPath = `/plugins/${this.props.name}/configurations/${this.context.currentEnvironment}`;
const icons = this.props.name === 'upload' || this.props.name === 'email' ? [
// Make sure to match the ctm config URI instead of content-type view URI
const settingsPath = this.props.name === 'content-manager' ? '/plugins/content-manager/ctm-configurations' : `/plugins/${this.props.name}/configurations/${this.context.currentEnvironment}`;
// const icons = this.props.name === 'upload' || this.props.name === 'email' ? [
const icons = includes(PLUGINS_WITH_CONFIG, this.props.name) ? [
{
icoType: 'cog',
onClick: (e) => {

View File

@ -14,16 +14,14 @@ import LeftMenuFooter from 'components/LeftMenuFooter';
import styles from './styles.scss';
export class LeftMenu extends React.Component { // eslint-disable-line react/prefer-stateless-function
render() {
return (
<div className={styles.leftMenu}>
<LeftMenuHeader />
<LeftMenuLinkContainer {...this.props} />
<LeftMenuFooter plugins={this.props.plugins} version={this.props.version} />
</div>
);
}
function LeftMenu(props) {
return (
<div className={styles.leftMenu}>
<LeftMenuHeader />
<LeftMenuLinkContainer {...props} />
<LeftMenuFooter plugins={props.plugins} version={props.version} />
</div>
);
}
LeftMenu.defaultProps = {

View File

@ -192,7 +192,7 @@ module.exports = (scope, cb) => {
])
.then(answers => {
if (hasDatabaseConfig) {
answers = _.omit(scope.database.settings, ['client']);
answers = _.merge((_.omit(scope.database.settings, ['client'])), scope.database.options);
}
scope.database.settings.host = answers.host;

View File

@ -1,13 +1,25 @@
.gradientOff {
background-image: linear-gradient( to bottom right, #F65A1D, #F68E0E );
color: white !important;
box-shadow: inset -1px 1px 3px rgba(0,0,0,0.1);
z-index: 0!important;
&:active, :hover {
box-shadow: inset -1px 1px 3px rgba(0,0,0,0.1);
background-image: linear-gradient( to bottom right, #F65A1D, #F68E0E );
color: white !important;
z-index: 0!important;
}
}
.gradientOn {
background-image: linear-gradient( to bottom right, #005EEA, #0097F6);
color: white !important;
box-shadow: inset 1px 1px 3px rgba(0,0,0,0.1);
&:active, :hover {
background-image: linear-gradient( to bottom right, #005EEA, #0097F6);
color: white !important;
z-index: 0!important;
}
}
.inputToggleContainer {

View File

@ -21,8 +21,6 @@ import InputPasswordWithErrors from 'components/InputPasswordWithErrors';
import InputTextAreaWithErrors from 'components/InputTextAreaWithErrors';
import InputTextWithErrors from 'components/InputTextWithErrors';
import InputToggleWithErrors from 'components/InputToggleWithErrors';
import WysiwygWithErrors from 'components/WysiwygWithErrors';
import InputJSONWithErrors from 'components/InputJSONWithErrors';
const DefaultInputError = ({ type }) => <div>Your input type: <b>{type}</b> does not exist</div>;
@ -32,7 +30,6 @@ const inputs = {
date: InputDateWithErrors,
email: InputEmailWithErrors,
file: InputFileWithErrors,
json: InputJSONWithErrors,
number: InputNumberWithErrors,
password: InputPasswordWithErrors,
search: InputSearchWithErrors,
@ -41,7 +38,6 @@ const inputs = {
text: InputTextWithErrors,
textarea: InputTextAreaWithErrors,
toggle: InputToggleWithErrors,
wysiwyg: WysiwygWithErrors,
};
function InputsIndex(props) {

View File

@ -48,6 +48,7 @@
}
.modalPosition {
max-width: 37.5rem !important;
> div {
width: 37.5rem;
padding: 0 !important;

View File

@ -48,11 +48,9 @@
"bootstrap": "^4.0.0-alpha.6",
"chalk": "^2.1.0",
"classnames": "^2.2.5",
"codemirror": "^5.38.0",
"copy-webpack-plugin": "^4.3.1",
"cross-env": "^5.0.5",
"css-loader": "^0.28.5",
"draft-js": "^0.10.5",
"eslint": "4.4.1",
"eslint-config-airbnb": "15.1.0",
"eslint-config-airbnb-base": "11.3.1",
@ -108,7 +106,6 @@
"rimraf": "^2.6.1",
"sass-loader": "^6.0.6",
"shelljs": "^0.7.8",
"showdown": "^1.8.6",
"style-loader": "^0.18.2",
"styled-components": "^3.2.6",
"url-loader": "^0.5.9",

View File

@ -0,0 +1 @@
<svg width="19" height="10" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><text font-family="Lato-Semibold, Lato" font-size="11" font-weight="500" fill="#41464E" transform="translate(0 -2)"><tspan x="1" y="11">abc</tspan></text><path d="M.5 6.5h18" stroke="#2C3039" stroke-linecap="square"/></g></svg>

After

Width:  |  Height:  |  Size: 325 B

View File

@ -0,0 +1 @@
<svg width="9" height="10" xmlns="http://www.w3.org/2000/svg"><text transform="translate(-12 -10)" fill="#333740" fill-rule="evenodd" font-size="13" font-family="Baskerville-SemiBold, Baskerville" font-weight="500"><tspan x="12" y="20">B</tspan></text></svg>

After

Width:  |  Height:  |  Size: 258 B

View File

@ -0,0 +1 @@
<svg width="13" height="7" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#333740" d="M5 0h8v1H5zM5 2h8v1H5zM5 4h8v1H5zM5 6h8v1H5z"/><rect stroke="#333740" x=".5" y=".5" width="2" height="2" rx="1"/><rect stroke="#333740" x=".5" y="4.5" width="2" height="2" rx="1"/></g></svg>

After

Width:  |  Height:  |  Size: 311 B

View File

@ -0,0 +1 @@
<svg width="12" height="8" xmlns="http://www.w3.org/2000/svg"><g fill="#333740" fill-rule="evenodd"><path d="M3.653 7.385a.632.632 0 0 1-.452-.191L.214 4.154a.66.66 0 0 1 0-.922L3.201.19a.632.632 0 0 1 .905 0 .66.66 0 0 1 0 .921l-2.534 2.58 2.534 2.58a.66.66 0 0 1 0 .922.632.632 0 0 1-.453.19zM8.347 7.385a.632.632 0 0 0 .452-.191l2.987-3.04a.66.66 0 0 0 0-.922L8.799.19a.632.632 0 0 0-.905 0 .66.66 0 0 0 0 .921l2.534 2.58-2.534 2.58a.66.66 0 0 0 0 .922c.125.127.289.19.453.19z"/></g></svg>

After

Width:  |  Height:  |  Size: 492 B

View File

@ -0,0 +1 @@
<svg width="6" height="9" xmlns="http://www.w3.org/2000/svg"><text transform="translate(-13 -11)" fill="#333740" fill-rule="evenodd" font-weight="500" font-size="13" font-family="Baskerville-SemiBoldItalic, Baskerville" font-style="italic"><tspan x="13" y="20">I</tspan></text></svg>

After

Width:  |  Height:  |  Size: 283 B

View File

@ -0,0 +1 @@
<svg width="12" height="6" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M6.063 1.5H6h.063z" fill="#000"/><path d="M9.516 0H8s.813.531.988 1.5h.528c.55 0 .984.434.984.984v1c0 .55-.434 1.016-.984 1.016h-3.5A1.03 1.03 0 0 1 5 3.484V2.5H3.5v.984A2.518 2.518 0 0 0 6.016 6h3.5C10.896 6 12 4.866 12 3.484v-1A2.473 2.473 0 0 0 9.516 0z" fill="#333740"/><path d="M8.3 1.5A2.473 2.473 0 0 0 6.016 0h-3.5C1.134 0 0 1.103 0 2.484v1A2.526 2.526 0 0 0 2.516 6H4s-.806-.531-1.003-1.5h-.481A1.03 1.03 0 0 1 1.5 3.484v-1c0-.55.466-.984 1.016-.984h3.5c.55 0 .984.434.984.984V3.5h1.5V2.484c0-.35-.072-.684-.2-.984z" fill="#333740"/></g></svg>

After

Width:  |  Height:  |  Size: 658 B

View File

@ -0,0 +1 @@
<svg width="12" height="11" xmlns="http://www.w3.org/2000/svg"><g fill="#333740" fill-rule="evenodd"><path d="M9 4.286a1.286 1.286 0 1 0 0-2.572 1.286 1.286 0 0 0 0 2.572z"/><path d="M11.25 0H.75C.332 0 0 .34 0 .758v8.77c0 .418.332.758.75.758h10.5c.418 0 .75-.34.75-.758V.758A.752.752 0 0 0 11.25 0zM8.488 5.296a.46.46 0 0 0-.342-.167c-.137 0-.234.065-.343.153l-.501.423c-.105.075-.188.126-.308.126a.443.443 0 0 1-.295-.11 3.5 3.5 0 0 1-.115-.11L5.143 4.054a.59.59 0 0 0-.897.008L.857 8.148V1.171a.353.353 0 0 1 .351-.314h9.581a.34.34 0 0 1 .346.322l.008 6.975-2.655-2.858z"/></g></svg>

After

Width:  |  Height:  |  Size: 586 B

View File

@ -0,0 +1 @@
<svg width="12" height="8" xmlns="http://www.w3.org/2000/svg"><g fill="#333740" fill-rule="evenodd"><path d="M2.4 3H.594v-.214h.137c.123 0 .212-.01.266-.032.053-.022.086-.052.1-.092a.67.67 0 0 0 .018-.188V.74a.46.46 0 0 0-.03-.194C1.064.504 1.021.476.955.46A1.437 1.437 0 0 0 .643.435H.539V.23c.332-.035.565-.067.7-.096.135-.03.258-.075.37-.134h.275v2.507c0 .104.023.177.07.218.047.04.14.061.278.061H2.4V3zM2.736 6.695l-.132.528h-.246a.261.261 0 0 0 .015-.074c0-.058-.049-.087-.146-.087H.293v-.198c.258-.173.511-.367.76-.581.25-.215.457-.437.623-.667.166-.23.249-.447.249-.653a.49.49 0 0 0-.321-.478.794.794 0 0 0-.582-.006.482.482 0 0 0-.196.138.284.284 0 0 0-.07.182c0 .074.04.17.12.289.006.008.009.015.009.02 0 .012-.041.03-.123.053l-.19.057a.693.693 0 0 1-.115.03c-.031 0-.067-.038-.108-.114a.516.516 0 0 1 .071-.586.899.899 0 0 1 .405-.238c.18-.058.4-.087.657-.087.317 0 .566.044.749.132.183.087.306.187.37.3a.64.64 0 0 1 .094.312c0 .197-.089.389-.266.575a5.296 5.296 0 0 1-.916.74 62.947 62.947 0 0 1-.62.413h1.843zM4 0h8v1H4zM4 2h8v1H4zM4 4h8v1H4zM4 6h8v1H4z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg width="9" height="9" xmlns="http://www.w3.org/2000/svg"><g fill="#333740" fill-rule="evenodd"><path d="M3 0C2.047 0 1.301.263.782.782.263 1.302 0 2.047 0 3v6h3.75V3H1.5c0-.54.115-.93.343-1.157C2.07 1.615 2.46 1.5 3 1.5M8.25 0c-.953 0-1.699.263-2.218.782-.519.52-.782 1.265-.782 2.218v6H9V3H6.75c0-.54.115-.93.343-1.157.227-.228.617-.343 1.157-.343"/></g></svg>

After

Width:  |  Height:  |  Size: 365 B

View File

@ -0,0 +1 @@
<svg width="10" height="10" xmlns="http://www.w3.org/2000/svg"><text transform="translate(-10 -11)" fill="#101622" fill-rule="evenodd" font-size="13" font-family="Baskerville-SemiBold, Baskerville" font-weight="500"><tspan x="10" y="20">U</tspan></text></svg>

After

Width:  |  Height:  |  Size: 259 B

View File

@ -1,18 +1 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="8px" height="8px" viewBox="0 0 8 8" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
<title>Shape</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Pages" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Content-Manager---List-view" transform="translate(-568.000000, -172.000000)" fill="#007EFF">
<g id="Container" transform="translate(234.000000, 0.000000)">
<g id="Filter" transform="translate(29.000000, 160.000000)">
<g id="Filter-#1" transform="translate(91.000000, 0.000000)">
<path d="M221.779774,18.7197709 L219.060003,16 L221.779774,13.2802291 C222.073409,12.9865938 222.073409,12.5138618 221.779774,12.2202265 C221.486138,11.9265912 221.013406,11.9265912 220.719771,12.2202265 L218,14.9399974 L215.280229,12.2202265 C214.986594,11.9265912 214.513862,11.9265912 214.220226,12.2202265 C213.926591,12.5138618 213.926591,12.9865938 214.220226,13.2802291 L216.939997,16 L214.220226,18.7197709 C213.926591,19.0134062 213.926591,19.4861382 214.220226,19.7797735 C214.513862,20.0734088 214.986594,20.0734088 215.280229,19.7797735 L218,17.0600026 L220.719771,19.7797735 C221.013406,20.0734088 221.486138,20.0734088 221.779774,19.7797735 C222.071326,19.4861382 222.071326,19.0113237 221.779774,18.7197709 L221.779774,18.7197709 Z" id="Shape"></path>
</g>
</g>
</g>
</g>
</g>
</svg>
<svg width="8" height="8" xmlns="http://www.w3.org/2000/svg"><path d="M7.78 6.72L5.06 4l2.72-2.72a.748.748 0 0 0 0-1.06.748.748 0 0 0-1.06 0L4 2.94 1.28.22a.748.748 0 0 0-1.06 0 .748.748 0 0 0 0 1.06L2.94 4 .22 6.72a.748.748 0 0 0 0 1.06.748.748 0 0 0 1.06 0L4 5.06l2.72 2.72a.748.748 0 0 0 1.06 0 .752.752 0 0 0 0-1.06z" fill="#007EFF" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 364 B

View File

@ -1,14 +1 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="8px" height="8px" viewBox="0 0 8 8" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
<title>Shape</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Search" transform="translate(-300.000000, -29.000000)" fill="#B3B5B9">
<g id="Shape">
<path d="M307.779774,35.7197709 L305.060003,33 L307.779774,30.2802291 C308.073409,29.9865938 308.073409,29.5138618 307.779774,29.2202265 C307.486138,28.9265912 307.013406,28.9265912 306.719771,29.2202265 L304,31.9399974 L301.280229,29.2202265 C300.986594,28.9265912 300.513862,28.9265912 300.220226,29.2202265 C299.926591,29.5138618 299.926591,29.9865938 300.220226,30.2802291 L302.939997,33 L300.220226,35.7197709 C299.926591,36.0134062 299.926591,36.4861382 300.220226,36.7797735 C300.513862,37.0734088 300.986594,37.0734088 301.280229,36.7797735 L304,34.0600026 L306.719771,36.7797735 C307.013406,37.0734088 307.486138,37.0734088 307.779774,36.7797735 C308.071326,36.4861382 308.071326,36.0113237 307.779774,35.7197709 L307.779774,35.7197709 Z"></path>
</g>
</g>
</g>
</svg>
<svg width="8" height="8" xmlns="http://www.w3.org/2000/svg"><path d="M7.78 6.72L5.06 4l2.72-2.72a.748.748 0 0 0 0-1.06.748.748 0 0 0-1.06 0L4 2.94 1.28.22a.748.748 0 0 0-1.06 0 .748.748 0 0 0 0 1.06L2.94 4 .22 6.72a.748.748 0 0 0 0 1.06.748.748 0 0 0 1.06 0L4 5.06l2.72 2.72a.748.748 0 0 0 1.06 0 .752.752 0 0 0 0-1.06z" fill="#B3B5B9" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 364 B

View File

@ -0,0 +1 @@
<svg width="10" height="10" xmlns="http://www.w3.org/2000/svg"><path d="M2.396 9.155l.6-.6-1.55-1.552-.601.601v.706h.845v.845h.706zM5.848 3.03c0-.097-.048-.146-.145-.146a.153.153 0 0 0-.112.047L2.013 6.508a.153.153 0 0 0-.046.112c0 .097.048.146.145.146a.153.153 0 0 0 .112-.047l3.578-3.577a.153.153 0 0 0 .046-.112zm-.356-1.268l2.746 2.746L2.746 10H0V7.254l5.492-5.492zM10 2.396a.809.809 0 0 1-.244.594L8.66 4.086 5.914 1.34 7.01.25A.784.784 0 0 1 7.604 0a.82.82 0 0 1 .6.25l1.552 1.545a.845.845 0 0 1 .244.601z" fill="#007EFF" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 555 B

View File

@ -1,13 +1,24 @@
import { generateMenu } from 'containers/App/sagas';
import { map, omit } from 'lodash';
import request from 'utils/request';
// This method is executed before the load of the plugin
const bootstrap = (plugin) => new Promise((resolve, reject) => {
generateMenu()
.then(menu => {
request('/content-manager/models', { method: 'GET' })
.then(models => {
const menu = [{
name: 'Content Types',
links: map(omit(models.models.models, 'plugins'), (model, key) => ({
label: model.labelPlural || model.label || key,
destination: key,
})),
}];
plugin.leftMenuSections = menu;
resolve(plugin);
})
.catch(e => reject(e));
.catch(e => {
strapi.notification.error('content-manager.error.model.fetch');
reject(e);
});
});
export default bootstrap;

View File

@ -0,0 +1,39 @@
/**
*
* Block
*/
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import styles from './styles.scss';
const Block = ({ children, description, title }) => (
<div className="col-md-12">
<div className={styles.ctmBlock}>
<div className={styles.ctmBlockTitle}>
<FormattedMessage id={title} />
<FormattedMessage id={description}>
{msg => <p>{msg}</p>}
</FormattedMessage>
</div>
{children}
</div>
</div>
);
Block.defaultProps = {
children: null,
description: 'app.utils.defaultMessage',
title: 'app.utils.defaultMessage',
};
Block.propTypes = {
children: PropTypes.any,
description: PropTypes.string,
title: PropTypes.string,
};
export default Block;

View File

@ -0,0 +1,22 @@
.ctmBlock{
margin-bottom: 35px;
background: #ffffff;
padding: 22px 28px 18px;
border-radius: 2px;
box-shadow: 0 2px 4px #E3E9F3;
-webkit-font-smoothing: antialiased;
}
.ctmBlockTitle {
padding-top: 0px;
line-height: 18px;
> span {
font-weight: 600;
color: #333740;
font-size: 18px;
}
> p {
color: #787E8F;
font-size: 13px;
}
}

View File

@ -0,0 +1,185 @@
/**
*
* DraggableAttr
*/
/* eslint-disable react/no-find-dom-node */
import React from 'react';
import { findDOMNode } from 'react-dom';
import {
DragSource,
DropTarget,
} from 'react-dnd';
import { flow, upperFirst } from 'lodash';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import cn from 'classnames';
import styles from './styles.scss';
const draggableAttrSource = {
beginDrag: (props) => {
props.updateSiblingHoverState();
return {
id: props.id,
index: props.index,
};
},
endDrag: (props) => {
props.updateSiblingHoverState();
return {};
},
};
const draggableAttrTarget = {
hover: (props, monitor, component) => {
const dragIndex = monitor.getItem().index;
const hoverIndex = props.index;
// Don't replace items with themselves
if (dragIndex === hoverIndex) {
return;
}
// Determine rectangle on screen
const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
// Get vertical middle
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
// Determine mouse position
const clientOffset = monitor.getClientOffset();
// Get pixels to the top
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
// Only perform the move when the mouse has crossed half of the items height
// When dragging downwards, only move when the cursor is below 50%
// When dragging upwards, only move when the cursor is above 50%
// Dragging downwards
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
// Dragging upwards
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}
// Time to actually perform the action
props.moveAttr(dragIndex, hoverIndex, props.keys);
// Note: we're mutating the monitor item here!
// Generally it's better to avoid mutations,
// but it's good here for the sake of performance
// to avoid expensive index searches.
monitor.getItem().index = hoverIndex;
},
};
class DraggableAttr extends React.Component {
state = { isOver: false, dragStart: false };
componentDidUpdate(prevProps) {
const { isDraggingSibling } = this.props;
if (isDraggingSibling !== prevProps.isDraggingSibling && isDraggingSibling) {
this.handleMouseLeave();
}
}
handleClickEdit = (e) => {
e.preventDefault();
e.stopPropagation();
this.props.onClickEditListItem(this.props.index);
}
handleMouseEnter = () => {
if (!this.props.isDraggingSibling) {
this.setState({ isOver: true });
}
};
handleMouseLeave = () => this.setState({ isOver: false });
handleRemove = (e) => {
e.preventDefault();
e.stopPropagation();
this.props.onRemove(this.props.index, this.props.keys);
}
render() {
const { label, name, isDragging, isEditing, connectDragSource, connectDropTarget } = this.props;
const { isOver, dragStart } = this.state;
const opacity = isDragging ? 0.2 : 1;
const overClass = isOver ? styles.draggableAttrOvered : '';
const className = dragStart ? styles.dragged : styles.draggableAttr;
return (
connectDragSource(
connectDropTarget(
<div
className={cn(className, isEditing && styles.editingAttr, overClass)}
onDragStart={() => this.setState({ dragStart: true })}
onDragEnd={() => this.setState({ dragStart: false })}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onClick={this.handleClickEdit}
style={{ opacity }}
>
<i className="fa fa-th" aria-hidden="true" />
<span>{name}</span>
{ isOver && !isDragging && (
<div className={styles.info}>
<FormattedMessage id="content-manager.components.DraggableAttr.edit" />
</div>
)}
{ !isOver && upperFirst(name) !== label && (
<div className={styles.info}>
{label}
</div>
)}
{isEditing && !isOver? (
<span className={styles.editIcon} onClick={this.handleClickEdit} />
) : (
<span className={cn( dragStart ? styles.removeIconDragged : styles.removeIcon)} onClick={this.handleRemove} />
)}
</div>
),
)
);
}
}
DraggableAttr.defaultProps = {
isEditing: false,
onRemove: () => {},
};
DraggableAttr.propTypes = {
connectDragSource: PropTypes.func.isRequired,
connectDropTarget: PropTypes.func.isRequired,
index: PropTypes.number.isRequired,
isDragging: PropTypes.bool.isRequired,
isDraggingSibling: PropTypes.bool.isRequired,
isEditing: PropTypes.bool,
keys: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
onClickEditListItem: PropTypes.func.isRequired,
onRemove: PropTypes.func,
};
const withDropTarget = DropTarget('draggableAttr', draggableAttrTarget, connect => ({
connectDropTarget: connect.dropTarget(),
}));
const withDragSource = DragSource('draggableAttr', draggableAttrSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging(),
}));
export default flow([withDropTarget, withDragSource])(DraggableAttr);

View File

@ -0,0 +1,116 @@
.draggableAttr {
position: relative;
height: 30px;
width: 100%;
margin-bottom: 6px;
padding-left: 10px;
justify-content: space-between;
background: #FAFAFB;
line-height: 30px;
color: #333740;
border: 1px solid #E3E9F3;
border-radius: 2px;
> i {
margin-right: 10px;
font-size: 11px;
color: #B3B5B9;
}
}
.draggableAttrOvered {
border: 1px solid #AED4FB!important;
}
.editingAttr {
background: #E6F0FB!important;
border: 1px solid #AED4FB!important;
}
.info {
position: absolute;
top: 0;
right: 40px;
color: #B4B6BA;
font-style: italic;
}
.removeIcon {
width: 30px;
background: #F3F4F4;
height: 28px;
cursor: pointer;
text-align: center;
float: right;
&:after {
display: inline-block;
content: '';
width: 8px;
height: 8px;
margin: auto;
margin-top: -3px;
background-image: url('../../assets/images/icon-cross.svg');
}
}
.removeIconDragged {
width: 30px;
background: #F3F4F4;
height: 28px;
cursor: pointer;
text-align: center;
float: right;
&:after {
display: inline-block;
content: '';
width: 8px;
height: 8px;
margin: auto;
margin-top: -3px;
background-image: url('../../assets/images/icon-cross-blue.svg');
}
}
.editIcon {
width: 30px;
background: #E6F0FB;
height: 28px;
cursor: pointer;
text-align: center;
float: right;
&:after {
display: inline-block;
content: '';
width: 10px;
height: 10px;
margin: auto;
margin-top: -3px;
background-image: url('../../assets/images/icon-edit-blue.svg');
}
}
.dragged {
position: relative;
height: 30px !important;
width: 100%;
margin-bottom: 6px;
box-shadow: 0!important;
padding-left: 10px;
justify-content: space-between;
background: #E6F0FB !important;
line-height: 30px;
color: #333740;
border: 1px solid darken(#AED4FB, 20%)!important;
border-radius: 2px;
> i {
margin-right: 10px;
font-size: 10px;
color: #B3B5B9;
}
> span:last-child {
background:#AED4FB;
}
}

View File

@ -22,6 +22,9 @@ import {
// or strapi/packages/strapi-helper-plugin/lib/src
import Input from 'components/InputsIndex';
import InputJSONWithErrors from 'components/InputJSONWithErrors';
import WysiwygWithErrors from 'components/WysiwygWithErrors';
import styles from './styles.scss';
const getInputType = (type = '') => {
@ -146,6 +149,7 @@ class Edit extends React.PureComponent {
<Input
autoFocus={key === 0}
customBootstrapClass={get(layout, 'className')}
customInputs={{ json: InputJSONWithErrors, wysiwyg: WysiwygWithErrors }}
didCheckErrors={this.props.didCheckErrors}
errors={this.getInputErrors(attr)}
key={attr}

View File

@ -0,0 +1,36 @@
/**
*
* SettingsRow
*/
import React from 'react';
import { upperFirst } from 'lodash';
import PropTypes from 'prop-types';
import IcoContainer from 'components/IcoContainer';
import styles from './styles.scss';
function SettingsRow({ destination, name, onClick }) {
return (
<div className={styles.settingsRow} onClick={() => onClick(destination)}>
<div>
<div className={styles.frame}>
<div className={styles.icon}>
<i className="fa fa-cube"></i>
</div>
{upperFirst(name)}
</div>
<IcoContainer icons={[{ icoType: 'cog', onClick: () => onClick(destination) }]} />
</div>
</div>
);
}
SettingsRow.propTypes = {
destination: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
};
export default SettingsRow;

View File

@ -0,0 +1,32 @@
.settingsRow {
height: 54px;
&:hover {
background-color: #F7F8F8;
}
> div {
display: flex;
line-height: 53px;
margin: 0 28px 0 36px;
justify-content: space-between;
border-bottom: 1px solid rgba(14,22,34,0.04);
font-size: 13px;
color: #333740;
}
cursor: pointer;
}
.icon {
position: absolute;
left: 0;
height: 53px;
display: flex;
flex-direction: column;
justify-content: center;
}
.frame {
position: relative;
padding-left: 55px;
font-weight: 500;
}

View File

@ -22,13 +22,14 @@ class Table extends React.Component {
(
<TableEmpty
filters={this.props.filters}
colspan={this.props.headers.length + 1}
colspan={this.props.enableBulkActions ? this.props.headers.length + 1 : this.props.headers.length}
contentType={this.props.routeParams.slug}
search={this.props.search}
/>
) :
this.props.records.map((record, key) => (
<TableRow
enableBulkActions={this.props.enableBulkActions}
onChange={this.props.onClickSelect}
key={key}
destination={`${this.props.route.path.replace(':slug', this.props.routeParams.slug)}/${record[this.props.primaryKey]}`}
@ -42,10 +43,11 @@ class Table extends React.Component {
/>
));
const entriesToDeleteNumber = this.props.entriesToDelete.length;
return (
<table className={`table ${styles.table}`}>
<TableHeader
enableBulkActions={this.props.enableBulkActions}
onClickSelectAll={this.props.onClickSelectAll}
value={this.props.deleteAllValue}
headers={this.props.headers}
@ -74,6 +76,7 @@ Table.contextTypes = {
};
Table.defaultProps = {
enableBulkActions: true,
entriesToDelete: [],
handleDelete: () => {},
search: '',
@ -82,6 +85,7 @@ Table.defaultProps = {
Table.propTypes = {
deleteAllValue: PropTypes.bool.isRequired,
enableBulkActions: PropTypes.bool,
entriesToDelete: PropTypes.array,
filters: PropTypes.array.isRequired,
handleDelete: PropTypes.func,

View File

@ -6,6 +6,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import CustomInputCheckbox from 'components/CustomInputCheckbox';
@ -22,17 +23,23 @@ class TableHeader extends React.Component {
}
}
renderBulk = () => (
<th key="bulk_action">
<CustomInputCheckbox
entriesToDelete={this.props.entriesToDelete}
isAll
name="all"
onChange={this.props.onClickSelectAll}
value={this.props.value}
/>
</th>
);
renderBulk = () => {
if (this.props.enableBulkActions) {
return (
<th key="bulk_action">
<CustomInputCheckbox
entriesToDelete={this.props.entriesToDelete}
isAll
name="all"
onChange={this.props.onClickSelectAll}
value={this.props.value}
/>
</th>
);
}
return null;
}
render() {
// Generate headers list
@ -40,7 +47,7 @@ class TableHeader extends React.Component {
// Define sort icon
let icon;
if (this.props.sort === header.name) {
if (this.props.sort === header.name || this.props.sort === 'id' && header.name === '_id') {
icon = <i className={`fa fa-sort-asc ${styles.iconAsc}`} />;
} else if (this.props.sort === `-${header.name}`) {
icon = <i className={`fa fa-sort-asc ${styles.iconDesc}`} />;
@ -49,7 +56,11 @@ class TableHeader extends React.Component {
return (
<th // eslint-disable-line jsx-a11y/no-static-element-interactions
key={i}
onClick={() => this.handleChangeSort(header.name)}
onClick={() => {
if (header.sortable) {
this.handleChangeSort(header.name);
}
}}
>
<span>
{header.label}
@ -64,7 +75,7 @@ class TableHeader extends React.Component {
headers.push(<th key="th_action"></th>);
return (
<thead className={styles.tableHeader}>
<thead className={cn(styles.tableHeader, this.props.enableBulkActions && styles.withBulk)}>
<tr >
{[this.renderBulk()].concat(headers)}
</tr>
@ -74,10 +85,12 @@ class TableHeader extends React.Component {
}
TableHeader.defaultProps = {
enableBulkActions: true,
value: false,
};
TableHeader.propTypes = {
enableBulkActions: PropTypes.bool,
entriesToDelete: PropTypes.array.isRequired,
headers: PropTypes.array.isRequired,
onChangeSort: PropTypes.func.isRequired,

View File

@ -14,7 +14,9 @@
cursor: pointer;
}
}
}
.withBulk {
> tr {
th:first-child {
width: 50px;

View File

@ -8,6 +8,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { isEmpty, isObject, toString } from 'lodash';
import cn from 'classnames';
import CustomInputCheckbox from 'components/CustomInputCheckbox';
import IcoContainer from 'components/IcoContainer';
@ -100,19 +101,25 @@ class TableRow extends React.Component {
.concat([this.renderAction()]);
}
renderDelete = () => (
<td onClick={(e) => e.stopPropagation()} key="i">
<CustomInputCheckbox
name={this.props.record.id}
onChange={this.props.onChange}
value={this.props.value}
/>
</td>
);
renderDelete = () => {
if (this.props.enableBulkActions) {
return (
<td onClick={(e) => e.stopPropagation()} key="i">
<CustomInputCheckbox
name={this.props.record.id}
onChange={this.props.onChange}
value={this.props.value}
/>
</td>
);
}
return null;
}
render() {
return (
<tr className={styles.tableRow} onClick={() => this.handleClick(this.props.destination)}>
<tr className={cn(styles.tableRow, this.props.enableBulkActions && styles.tableRowWithBulk)} onClick={() => this.handleClick(this.props.destination)}>
{this.renderCells()}
</tr>
);
@ -124,11 +131,13 @@ TableRow.contextTypes = {
};
TableRow.defaultProps = {
enableBulkActions: true,
value: false,
};
TableRow.propTypes = {
destination: PropTypes.string.isRequired,
enableBulkActions: PropTypes.bool,
headers: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
onDelete: PropTypes.func,

View File

@ -16,9 +16,6 @@
border-collapse: collapse;
border-top: 1px solid #F1F1F2 !important;
}
> td:first-child {
width: 50px;
}
}
.truncate {

View File

@ -4,13 +4,21 @@
*
*/
import { includes } from 'lodash';
import {
EMPTY_STORE,
GET_MODEL_ENTRIES,
GET_MODEL_ENTRIES_SUCCEEDED,
LOAD_MODELS,
LOADED_MODELS,
UPDATE_SCHEMA,
MOVE_ATTR,
ON_CHANGE,
ON_CHANGE_SETTINGS,
ON_CLICK_ADD_ATTR,
ON_REMOVE,
ON_RESET,
ON_SUBMIT,
SUBMIT_SUCCEEDED,
} from './constants';
export function emptyStore() {
@ -47,9 +55,65 @@ export function loadedModels(models) {
};
}
export function updateSchema(schema) {
export function moveAttr(dragIndex, hoverIndex, keys) {
return {
type: UPDATE_SCHEMA,
schema,
type: MOVE_ATTR,
dragIndex,
hoverIndex,
keys,
};
}
export function onChange({ target }) {
const value = includes(target.name, 'pageEntries') ? parseInt(target.value, 10) : target.value;
return {
type: ON_CHANGE,
keys: target.name.split('.'),
value,
};
}
export function onChangeSettings({ target }) {
const value = includes(target.name, 'pageEntries') ? parseInt(target.value, 10) : target.value;
return {
type: ON_CHANGE_SETTINGS,
keys: target.name.split('.'),
value,
};
}
export function onClickAddAttr(data, keys) {
return {
type: ON_CLICK_ADD_ATTR,
data,
keys,
};
}
export function onRemove(index, keys) {
return {
type: ON_REMOVE,
index,
keys,
};
}
export function onReset() {
return {
type: ON_RESET,
};
}
export function onSubmit() {
return {
type: ON_SUBMIT,
};
}
export function submitSucceeded() {
return {
type: SUBMIT_SUCCEEDED,
};
}

View File

@ -9,4 +9,11 @@ export const GET_MODEL_ENTRIES = 'contentManager/App/GET_MODEL_ENTRIES';
export const GET_MODEL_ENTRIES_SUCCEEDED = 'contentManager/App/GET_MODEL_ENTRIES_SUCCEEDED';
export const LOAD_MODELS = 'contentManager/App/LOAD_MODELS';
export const LOADED_MODELS = 'contentManager/App/LOADED_MODELS';
export const UPDATE_SCHEMA = 'contentManager/App/UPDATE_SCHEMA';
export const MOVE_ATTR = 'contentManager/App/MOVE_ATTR';
export const ON_CHANGE = 'contentManager/App/ON_CHANGE';
export const ON_CHANGE_SETTINGS = 'contentManager/App/ON_CHANGE_SETTINGS';
export const ON_CLICK_ADD_ATTR = 'contentManager/App/ON_CLICK_ADD_ATTR';
export const ON_REMOVE = 'contentManager/App/ON_REMOVE';
export const ON_RESET = 'contentManager/App/ON_RESET';
export const ON_SUBMIT = 'contentManager/App/ON_SUBMIT';
export const SUBMIT_SUCCEEDED = 'contentManager/App/SUBMIT_SUCCEEDED';

View File

@ -16,16 +16,17 @@ import { Switch, Route } from 'react-router-dom';
import injectSaga from 'utils/injectSaga';
import getQueryParameters from 'utils/getQueryParameters';
import Home from 'containers/Home';
import EditPage from 'containers/EditPage';
import ListPage from 'containers/ListPage';
import SettingsPage from 'containers/SettingsPage';
import SettingPage from 'containers/SettingPage';
import LoadingIndicatorPage from 'components/LoadingIndicatorPage';
import EmptyAttributesView from 'components/EmptyAttributesView';
import {
loadModels,
} from './actions';
import { makeSelectLoading, makeSelectModels, makeSelectModelEntries } from './selectors';
import { makeSelectLoading, makeSelectModelEntries, makeSelectSchema } from './selectors';
import saga from './sagas';
@ -41,19 +42,19 @@ class App extends React.Component {
const currentModelName = this.props.location.pathname.split('/')[3];
const source = getQueryParameters(this.props.location.search, 'source');
const attrPath = source === 'content-manager' ? ['models', currentModelName, 'fields'] : ['models', 'plugins', source, currentModelName, 'fields'];
if (currentModelName && source && isEmpty(get(this.props.models.plugins, [source, 'models', currentModelName, 'attributes']))) {
if (currentModelName && isEmpty(get(this.props.models.models, [currentModelName, 'attributes']))) {
return <EmptyAttributesView currentModelName={currentModelName} history={this.props.history} modelEntries={this.props.modelEntries} />;
}
if (currentModelName && source && isEmpty(get(this.props.schema, attrPath))) {
return <EmptyAttributesView currentModelName={currentModelName} history={this.props.history} modelEntries={this.props.modelEntries} />;
}
return (
<div className="content-manager">
<Switch>
<Route path="/plugins/content-manager/ctm-configurations/:slug/:source?/:endPoint?" component={SettingPage} />
<Route path="/plugins/content-manager/ctm-configurations" component={SettingsPage} />
<Route path="/plugins/content-manager/:slug/:id" component={EditPage} />
<Route path="/plugins/content-manager/:slug" component={ListPage} />
<Route path="/plugins/content-manager" component={Home} />
</Switch>
</div>
);
@ -70,7 +71,7 @@ App.propTypes = {
loadModels: PropTypes.func.isRequired,
location: PropTypes.object.isRequired,
modelEntries: PropTypes.number.isRequired,
models: PropTypes.oneOfType([
schema: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.object,
]).isRequired,
@ -88,7 +89,7 @@ export function mapDispatchToProps(dispatch) {
const mapStateToProps = createStructuredSelector({
loading: makeSelectLoading(),
modelEntries: makeSelectModelEntries(),
models: makeSelectModels(),
schema: makeSelectSchema(),
});
const withConnect = connect(mapStateToProps, mapDispatchToProps);

View File

@ -5,15 +5,27 @@
*/
import { fromJS, List } from 'immutable';
import { EMPTY_STORE, GET_MODEL_ENTRIES_SUCCEEDED, LOAD_MODELS, LOADED_MODELS, UPDATE_SCHEMA } from './constants';
import {
EMPTY_STORE,
GET_MODEL_ENTRIES_SUCCEEDED,
LOAD_MODELS,
LOADED_MODELS,
MOVE_ATTR,
ON_CHANGE,
ON_CHANGE_SETTINGS,
ON_CLICK_ADD_ATTR,
ON_REMOVE,
ON_RESET,
SUBMIT_SUCCEEDED,
} from './constants';
const initialState = fromJS({
modelEntries: 0,
formValidations: List([]),
loading: true,
models: false,
schema: false,
formValidations: List(),
modelEntries: 0,
modifiedSchema: fromJS({}),
schema: fromJS({}),
submitSuccess: false,
});
function appReducer(state = initialState, action) {
@ -26,11 +38,74 @@ function appReducer(state = initialState, action) {
return state;
case LOADED_MODELS:
return state
.set('models', action.models);
case UPDATE_SCHEMA:
.update('schema', () => fromJS(action.models.models))
.update('modifiedSchema', () => fromJS(action.models.models))
.set('loading', false);
case MOVE_ATTR:
return state
.set('loading', false)
.set('schema', action.schema);
.updateIn(['modifiedSchema', 'models'].concat(action.keys.split('.')).concat(['listDisplay']), list => (
list
.delete(action.dragIndex)
.insert(action.hoverIndex, list.get(action.dragIndex))
));
case ON_CHANGE:
return state
.updateIn(['modifiedSchema'].concat(action.keys), () => action.value)
.updateIn(['modifiedSchema', 'models'], models => {
return models
.keySeq()
.reduce((acc, current) => {
if (current !== 'plugins') {
return acc.setIn([current, action.keys[1]], action.value);
}
return acc
.get(current)
.keySeq()
.reduce((acc1, curr) => {
return acc1
.getIn([current, curr])
.keySeq()
.reduce((acc2, curr1) => {
return acc2.setIn([ current, curr, curr1, action.keys[1]], action.value);
}, acc1);
}, acc);
}, models);
});
case ON_CHANGE_SETTINGS:
return state
.updateIn(['modifiedSchema', 'models'].concat(action.keys), () => action.value);
case ON_CLICK_ADD_ATTR:
return state
.updateIn(['modifiedSchema', 'models'].concat(action.keys.split('.')).concat(['listDisplay']), list => list.push(fromJS(action.data)));
case ON_REMOVE:
return state.updateIn(['modifiedSchema', 'models'].concat(action.keys.split('.')).concat(['listDisplay']), list => {
// If the list is empty add the default Id attribute
if (list.size -1 === 0) {
const attrToAdd = state.getIn(['schema', 'models'].concat(action.keys.split('.')).concat(['listDisplay']))
.filter(attr => {
return attr.get('name') === '_id' || attr.get('name') === 'id';
});
attrToAdd.setIn(['0', 'sortable'], () => true);
return list
.delete(action.index)
.push(attrToAdd.get('0'));
}
return list.delete(action.index);
});
case ON_RESET:
return state
.update('modifiedSchema', () => state.get('schema'));
case SUBMIT_SUCCEEDED:
return state
.update('submitSuccess', v => v = !v)
.update('schema', () => state.get('modifiedSchema'));
default:
return state;
}

View File

@ -1,18 +1,15 @@
import { LOCATION_CHANGE } from 'react-router-redux';
import { map, omit } from 'lodash';
import { fork, put, select, call, takeLatest, take, cancel } from 'redux-saga/effects';
import { fork, put, call, takeLatest, take, cancel, select } from 'redux-saga/effects';
import request from 'utils/request';
import { generateSchema } from 'utils/schema';
import { getModelEntriesSucceeded, loadedModels, updateSchema } from './actions';
import { GET_MODEL_ENTRIES, LOAD_MODELS, LOADED_MODELS } from './constants';
import { makeSelectModels } from './selectors';
import { getModelEntriesSucceeded, loadedModels, submitSucceeded } from './actions';
import { GET_MODEL_ENTRIES, LOAD_MODELS, ON_SUBMIT } from './constants';
import { makeSelectModifiedSchema } from './selectors';
export function* modelEntriesGet(action) {
try {
const requestUrl = `/content-manager/explorer/${action.modelName}/count${action.source !== undefined ? `?source=${action.source}`: ''}`;
const response = yield call(request, requestUrl, { method: 'GET' });
yield put(getModelEntriesSucceeded(response.count));
@ -21,26 +18,6 @@ export function* modelEntriesGet(action) {
}
}
export const generateMenu = function () {
return request(`/content-manager/models`, {
method: 'GET',
})
.then(response => generateSchema(response))
.then(displayedModels => {
return [{
name: 'Content Types',
links: map(omit(displayedModels, 'plugins'), (model, key) => ({
label: model.labelPlural || model.label || key,
destination: key,
})),
}];
})
.catch((error) => {
strapi.notification.error('content-manager.error.model.fetch');
throw Error(error);
});
};
export function* getModels() {
try {
const response = yield call(request, `/content-manager/models`, {
@ -53,25 +30,23 @@ export function* getModels() {
}
}
export function* modelsLoaded() {
const models = yield select(makeSelectModels());
let schema;
export function* submit() {
try {
schema = generateSchema(models);
} catch (err) {
strapi.notification.error('content-manager.error.schema.generation');
throw new Error(err);
}
const schema = yield select(makeSelectModifiedSchema());
yield call(request, '/content-manager/models', { method: 'PUT', body: { schema } });
yield put(updateSchema(schema));
yield put(submitSucceeded());
} catch(err) {
// Silent
// NOTE: should we add another notification??
}
}
// Individual exports for testing
export function* defaultSaga() {
const loadModelsWatcher = yield fork(takeLatest, LOAD_MODELS, getModels);
const loadedModelsWatcher = yield fork(takeLatest, LOADED_MODELS, modelsLoaded);
const loadEntriesWatcher = yield fork(takeLatest, GET_MODEL_ENTRIES, modelEntriesGet);
yield fork(takeLatest, ON_SUBMIT, submit);
yield take(LOCATION_CHANGE);

View File

@ -29,10 +29,6 @@ const selectLocationState = () => {
* Default selector used by List
*/
const makeSelectModels = () =>
createSelector(selectGlobalDomain(), globalState =>
globalState.get('models')
);
const makeSelectModelEntries = () =>
createSelector(selectGlobalDomain(), globalState =>
@ -43,13 +39,20 @@ const makeSelectLoading = () =>
createSelector(selectGlobalDomain(), substate => substate.get('loading'));
const makeSelectSchema = () =>
createSelector(selectGlobalDomain(), substate => substate.get('schema'));
createSelector(selectGlobalDomain(), substate => substate.get('schema').toJS());
const makeSelectModifiedSchema = () =>
createSelector(selectGlobalDomain(), substate => substate.get('modifiedSchema').toJS());
const makeSelectSubmitSuccess = () =>
createSelector(selectGlobalDomain(), substate => substate.get('submitSuccess'));
export {
selectGlobalDomain,
selectLocationState,
makeSelectLoading,
makeSelectModelEntries,
makeSelectModels,
makeSelectModifiedSchema,
makeSelectSchema,
makeSelectSubmitSuccess,
};

View File

@ -19,13 +19,14 @@ import cn from 'classnames';
import BackHeader from 'components/BackHeader';
import LoadingIndicator from 'components/LoadingIndicator';
import PluginHeader from 'components/PluginHeader';
import PopUpWarning from 'components/PopUpWarning';
// Plugin's components
import Edit from 'components/Edit';
import EditRelations from 'components/EditRelations';
// App selectors
import { makeSelectModels, makeSelectSchema } from 'containers/App/selectors';
import { makeSelectSchema } from 'containers/App/selectors';
import injectReducer from 'utils/injectReducer';
import injectSaga from 'utils/injectSaga';
@ -53,6 +54,8 @@ import makeSelectEditPage from './selectors';
import styles from './styles.scss';
export class EditPage extends React.Component {
state = { showWarning: false };
componentDidMount() {
this.props.initModelProps(this.getModelName(), this.isCreating(), this.getSource(), this.getModelAttributes());
@ -123,7 +126,7 @@ export class EditPage extends React.Component {
* Retrieve the model
* @type {Object}
*/
getModel = () => get(this.props.models, ['models', this.getModelName()]) || get(this.props.models, ['plugins', this.getSource(), 'models', this.getModelName()]);
getModel = () => get(this.props.schema, ['models', this.getModelName()]) || get(this.props.schema, ['models', 'plugins', this.getSource(), this.getModelName()]);
/**
* Retrieve specific attribute
@ -148,8 +151,8 @@ export class EditPage extends React.Component {
* @return {Object}
*/
getSchema = () => this.getSource() !== 'content-manager' ?
get(this.props.schema, ['plugins', this.getSource(), this.getModelName()])
: get(this.props.schema, [this.getModelName()]);
get(this.props.schema, ['models', 'plugins', this.getSource(), this.getModelName()])
: get(this.props.schema, ['models', this.getModelName()]);
getPluginHeaderTitle = () => {
if (this.isCreating()) {
@ -243,7 +246,7 @@ export class EditPage extends React.Component {
{
label: 'content-manager.containers.Edit.reset',
kind: 'secondary',
onClick: this.props.onCancel,
onClick: this.toggle,
type: 'button',
disabled: this.showLoaders(),
},
@ -265,8 +268,11 @@ export class EditPage extends React.Component {
return isLoading && !this.isCreating() || isLoading && get(layout, this.getModelName()) === undefined;
}
toggle = () => this.setState(prevState => ({ showWarning: !prevState.showWarning }));
render() {
const { editPage } = this.props;
const { editPage, onCancel } = this.props;
const { showWarning } = this.state;
return (
<div>
@ -277,6 +283,21 @@ export class EditPage extends React.Component {
actions={this.pluginHeaderActions()}
title={{ id: this.getPluginHeaderTitle() }}
/>
<PopUpWarning
isOpen={showWarning}
toggleModal={this.toggle}
content={{
title: 'content-manager.popUpWarning.title',
message: 'content-manager.popUpWarning.warning.cancelAllSettings',
cancel: 'content-manager.popUpWarning.button.cancel',
confirm: 'content-manager.popUpWarning.button.confirm',
}}
popUpWarningType="danger"
onConfirm={() => {
onCancel();
this.toggle();
}}
/>
<div className="row">
<div className={this.isRelationComponentNull() ? 'col-lg-12' : 'col-lg-9'}>
<div className={styles.main_wrapper}>
@ -327,7 +348,7 @@ EditPage.contextTypes = {
};
EditPage.defaultProps = {
models: {},
schema: {},
};
EditPage.propTypes = {
@ -339,10 +360,9 @@ EditPage.propTypes = {
initModelProps: PropTypes.func.isRequired,
location: PropTypes.object.isRequired,
match: PropTypes.object.isRequired,
models: PropTypes.object,
onCancel: PropTypes.func.isRequired,
resetProps: PropTypes.func.isRequired,
schema: PropTypes.object.isRequired,
schema: PropTypes.object,
setFileRelations: PropTypes.func.isRequired,
setFormErrors: PropTypes.func.isRequired,
submit: PropTypes.func.isRequired,
@ -367,7 +387,6 @@ function mapDispatchToProps(dispatch) {
const mapStateToProps = createStructuredSelector({
editPage: makeSelectEditPage(),
models: makeSelectModels(),
schema: makeSelectSchema(),
});

View File

@ -9,11 +9,11 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
import { createStructuredSelector } from 'reselect';
import { capitalize, get, isUndefined, map, toInteger } from 'lodash';
import { capitalize, get, isUndefined, toInteger } from 'lodash';
import cn from 'classnames';
// App selectors
import { makeSelectModels, makeSelectSchema } from 'containers/App/selectors';
import { makeSelectSchema } from 'containers/App/selectors';
// You can find these components in either
// ./node_modules/strapi-helper-plugin/lib/src
@ -106,9 +106,21 @@ export class ListPage extends React.Component {
* Helper to retrieve the current model data
* @return {Object} the current model
*/
getCurrentModel = () =>
get(this.props.models, ['models', this.getCurrentModelName()]) ||
get(this.props.models, ['plugins', this.getSource(), 'models', this.getCurrentModelName()]);
getCurrentModel = () => (
get(this.props.schema, ['models', this.getCurrentModelName()]) ||
get(this.props.schema, ['models', 'plugins', this.getSource(), this.getCurrentModelName()])
);
getCurrentModelDefaultLimit = () => (
get(this.getCurrentModel(), 'pageEntries', 10)
);
getCurrentModelDefaultSort = () => {
const sortAttr = get(this.getCurrentModel(), 'defaultSort', 'id');
const order = get(this.getCurrentModel(), 'sort', 'ASC');
return order === 'ASC' ? sortAttr : `-${sortAttr}`;
};
/**
* Helper to retrieve the current model name
@ -122,7 +134,7 @@ export class ListPage extends React.Component {
*/
getData = (props, setUpdatingParams = false) => {
const source = getQueryParameters(props.location.search, 'source');
const _limit = toInteger(getQueryParameters(props.location.search, '_limit')) || 10;
const _limit = toInteger(getQueryParameters(props.location.search, '_limit')) || this.getCurrentModelDefaultLimit();
const _page = toInteger(getQueryParameters(props.location.search, '_page')) || 1;
const _sort = this.findPageSort(props);
const _q = getQueryParameters(props.location.search, '_q') || '';
@ -144,8 +156,8 @@ export class ListPage extends React.Component {
* @return {Object} Fields
*/
getCurrentSchema = () =>
get(this.props.schema, [this.getCurrentModelName(), 'fields']) ||
get(this.props.schema, ['plugins', this.getSource(), this.getCurrentModelName(), 'fields']);
get(this.props.schema, ['models', this.getCurrentModelName(), 'fields']) ||
get(this.props.schema, ['models', 'plugins', this.getSource(), this.getCurrentModelName(), 'fields']);
getPopUpDeleteAllMsg = () => (
this.props.listPage.entriesToDelete.length > 1 ?
@ -153,6 +165,14 @@ export class ListPage extends React.Component {
: 'content-manager.popUpWarning.bodyMessage.contentType.delete'
);
getModelPrimaryKey = () => (
get(this.getCurrentModel(), ['primaryKey'], '_id')
);
getTableHeaders = () => (
get(this.getCurrentModel(), ['listDisplay'], [])
);
/**
* Generate the redirect URI when editing an entry
* @type {String}
@ -169,29 +189,6 @@ export class ListPage extends React.Component {
return `?${generateSearchFromParams(params)}&source=${this.getSource()}${generateSearchFromFilters(filters)}`;
}
/**
* Function to generate the Table's headers
* @return {Array}
*/
generateTableHeaders = () => {
const currentSchema =
get(this.props.schema, [this.getCurrentModelName()]) ||
get(this.props.schema, ['plugins', this.getSource(), this.getCurrentModelName()]);
const tableHeaders = map(currentSchema.list, value => ({
name: value,
label: currentSchema.fields[value].label,
type: currentSchema.fields[value].type,
}));
tableHeaders.splice(0, 0, {
name: this.getCurrentModel().primaryKey || 'id',
label: 'Id',
type: 'string',
});
return tableHeaders;
};
areAllEntriesSelected = () => {
const { listPage: { entriesToDelete, records } } = this.props;
@ -204,26 +201,9 @@ export class ListPage extends React.Component {
* @return {String} the model's primaryKey
*/
findPageSort = props => {
const {
match: {
params: { slug },
},
} = props;
const source = this.getSource();
const modelPrimaryKey = get(props.models, ['models', slug.toLowerCase(), 'primaryKey']);
// Check if the model is in a plugin
const pluginModelPrimaryKey = get(props.models.plugins, [
source,
'models',
slug.toLowerCase(),
'primaryKey',
]);
return (
getQueryParameters(props.location.search, '_sort') ||
modelPrimaryKey ||
pluginModelPrimaryKey ||
'id'
this.getCurrentModelDefaultSort()
);
};
@ -311,6 +291,12 @@ export class ListPage extends React.Component {
return updatingParams || isLoading && get(records, this.getCurrentModelName()) === undefined;
}
showSearch = () => get(this.getCurrentModel(), ['search']);
showFilters = () => get(this.getCurrentModel(), ['filters']);
showBulkActions = () => get(this.getCurrentModel(), ['bulkActions']);
render() {
const {
addFilter,
@ -337,6 +323,7 @@ export class ListPage extends React.Component {
removeAllFilters,
removeFilter,
} = this.props;
const pluginHeaderActions = [
{
label: 'content-manager.containers.List.addAnEntry',
@ -355,12 +342,14 @@ export class ListPage extends React.Component {
return (
<div>
<div className={cn('container-fluid', styles.containerFluid)}>
<Search
changeParams={this.props.changeParams}
initValue={getQueryParameters(this.props.location.search, '_q') || ''}
model={this.getCurrentModelName()}
value={params._q}
/>
{this.showSearch() && (
<Search
changeParams={this.props.changeParams}
initValue={getQueryParameters(this.props.location.search, '_q') || ''}
model={this.getCurrentModelName()}
value={params._q}
/>
)}
<PluginHeader
actions={pluginHeaderActions}
description={{
@ -378,54 +367,59 @@ export class ListPage extends React.Component {
withDescriptionAnim={this.showLoaders()}
/>
<div className={cn(styles.wrapper)}>
<FiltersPickWrapper
addFilter={addFilter}
appliedFilters={appliedFilters}
close={onToggleFilters}
filterToFocus={filterToFocus}
modelName={this.getCurrentModelName()}
onChange={onChange}
onSubmit={this.handleSubmit}
removeAllFilters={removeAllFilters}
removeFilter={removeFilter}
schema={this.getCurrentSchema()}
show={showFilter}
/>
<div className={cn('row', styles.row)}>
<div className="col-md-12">
<Div
decreaseMarginBottom={filters.length > 0}
>
<div className="row">
<AddFilterCTA onClick={onToggleFilters} showHideText={showFilter} />
{filters.map((filter, key) => (
<Filter
key={key}
filter={filter}
index={key}
onClick={onClickRemove}
onClickOpen={openFiltersWithSelections}
schema={this.getCurrentSchema()}
/>
))}
{this.showFilters() && (
<React.Fragment>
<FiltersPickWrapper
addFilter={addFilter}
appliedFilters={appliedFilters}
close={onToggleFilters}
filterToFocus={filterToFocus}
modelName={this.getCurrentModelName()}
onChange={onChange}
onSubmit={this.handleSubmit}
removeAllFilters={removeAllFilters}
removeFilter={removeFilter}
schema={this.getCurrentSchema()}
show={showFilter}
/>
<div className={cn('row', styles.row)}>
<div className="col-md-12">
<Div
decreaseMarginBottom={filters.length > 0}
>
<div className="row">
<AddFilterCTA onClick={onToggleFilters} showHideText={showFilter} />
{filters.map((filter, key) => (
<Filter
key={key}
filter={filter}
index={key}
onClick={onClickRemove}
onClickOpen={openFiltersWithSelections}
schema={this.getCurrentSchema()}
/>
))}
</div>
</Div>
</div>
</Div>
</div>
</div>
</div>
</React.Fragment>
)}
<div className={cn('row', styles.row)}>
<div className="col-md-12">
<Table
deleteAllValue={this.areAllEntriesSelected()}
entriesToDelete={entriesToDelete}
enableBulkActions={this.showBulkActions()}
filters={filters}
handleDelete={this.toggleModalWarning}
headers={this.generateTableHeaders()}
headers={this.getTableHeaders()}
history={this.props.history}
onChangeSort={this.handleChangeSort}
onClickSelectAll={onClickSelectAll}
onClickSelect={onClickSelect}
onToggleDeleteAll={onToggleDeleteAll}
primaryKey={this.getCurrentModel().primaryKey || 'id'}
primaryKey={this.getModelPrimaryKey()}
records={get(records, this.getCurrentModelName(), [])}
redirectUrl={this.generateRedirectURI()}
route={this.props.match}
@ -485,7 +479,6 @@ ListPage.propTypes = {
listPage: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
match: PropTypes.object.isRequired,
models: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
onClickRemove: PropTypes.func.isRequired,
onClickSelect: PropTypes.func.isRequired,
@ -526,7 +519,6 @@ function mapDispatchToProps(dispatch) {
const mapStateToProps = createStructuredSelector({
listPage: makeSelectListPage(),
models: makeSelectModels(),
schema: makeSelectSchema(),
});

View File

@ -0,0 +1,8 @@
import { ON_CLICK_EDIT_LIST_ITEM } from './constants';
export function onClickEditListItem(listItemToEdit) {
return {
type: ON_CLICK_EDIT_LIST_ITEM,
listItemToEdit,
};
}

View File

@ -0,0 +1 @@
export const ON_CLICK_EDIT_LIST_ITEM = 'contentManager/SettingPage/ON_CLICK_EDIT_LIST_ITEM';

View File

@ -0,0 +1,86 @@
{
"inputs": [
{
"label": { "id": "content-manager.form.Input.search" },
"customBootstrapClass": "col-md-4",
"didCheckErrors": false,
"errors": [],
"name": "search",
"type": "toggle",
"validations": {}
},
{
"label": { "id": "content-manager.form.Input.filters" },
"customBootstrapClass": "col-md-4",
"didCheckErrors": false,
"errors": [],
"name": "filters",
"type": "toggle",
"validations": {}
},
{
"label": { "id": "content-manager.form.Input.bulkActions" },
"customBootstrapClass": "col-md-2",
"didCheckErrors": false,
"errors": [],
"name": "bulkActions",
"type": "toggle",
"validations": {}
},
{
"label": { "id": "content-manager.form.Input.pageEntries" },
"customBootstrapClass": "col-md-4",
"didCheckErrors": false,
"errors": [],
"inputDescription": { "id": "content-manager.form.Input.pageEntries.inputDescription" },
"name": "pageEntries",
"selectOptions": ["10", "20", "50", "100"],
"type": "select",
"validations": {}
},
{
"label": { "id": "content-manager.form.Input.defaultSort" },
"customBootstrapClass": "col-md-4 ml-md-auto",
"didCheckErrors": false,
"errors": [],
"style": { "marginRight": "-20px" },
"name": "defaultSort",
"selectOptions": ["_id"],
"type": "select",
"validations": {}
},
{
"label": "",
"customBootstrapClass": "col-md-1",
"didCheckErrors": false,
"errors": [],
"name": "sort",
"selectOptions": ["ASC", "DESC"],
"type": "select",
"validations": {}
}
],
"editList": [
{
"label": { "id": "content-manager.form.Input.label" },
"customBootstrapClass": "col-md-7",
"didCheckErrors": false,
"errors": [],
"inputDescription": { "id": "content-manager.form.Input.label.inputDescription" },
"name": "label",
"type": "string",
"validations": {}
},
{
"label": { "id": "content-manager.form.Input.sort.field" },
"customBootstrapClass": "col-md-6",
"didCheckErrors": false,
"errors": [],
"name": "sortable",
"type": "toggle",
"validations": {}
}
]
}

View File

@ -0,0 +1,451 @@
/**
*
* SettingPage
*/
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
import { createStructuredSelector } from 'reselect';
import { findIndex, get, upperFirst } from 'lodash';
import cn from 'classnames';
import HTML5Backend from 'react-dnd-html5-backend';
import { DragDropContext } from 'react-dnd';
import { FormattedMessage } from 'react-intl';
import { ButtonDropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
import PropTypes from 'prop-types';
import {
moveAttr,
onChangeSettings,
onClickAddAttr,
onRemove,
onReset,
onSubmit,
} from 'containers/App/actions';
import { makeSelectModifiedSchema , makeSelectSubmitSuccess } from 'containers/App/selectors';
import BackHeader from 'components/BackHeader';
import Input from 'components/InputsIndex';
import PluginHeader from 'components/PluginHeader';
import PopUpWarning from 'components/PopUpWarning';
import Block from 'components/Block';
import DraggableAttr from 'components/DraggableAttr';
import injectReducer from 'utils/injectReducer';
import injectSaga from 'utils/injectSaga';
import { onClickEditListItem } from './actions';
import forms from './forms.json';
import reducer from './reducer';
import saga from './saga';
import makeSelectSettingPage from './selectors';
import styles from './styles.scss';
class SettingPage extends React.PureComponent {
state = {
isDraggingSibling: false,
isOpen: false,
showWarning: false,
showWarningCancel: false,
};
componentDidMount() {
this.handleClickEditAttr(0);
}
componentDidUpdate(prevProps) {
if (prevProps.submitSuccess !== this.props.submitSuccess) {
this.toggle();
}
}
componentWillUnmount() {
// Reset the modified data
this.props.onReset();
}
getDefaultSort = () => this.getValue(`${this.getPath()}.defaultSort`, 'string');
getDropDownItems = () => {
const name = get(this.props.schema, `models.${this.getPath()}.primaryKey`, 'id' );
// The id attribute is not present on schema so we need to add it manually
const defaultAttr = { [name]: { name, label: 'Id', type: 'string', searchable: true, sortable: true } };
const attributes = Object.assign(get(this.props.schema, `models.${this.getPath()}.attributes`, {}), defaultAttr);
return Object.keys(attributes)
.filter(attr => {
return findIndex(this.getListDisplay(), ['name', attr]) === -1 && !attributes[attr].hasOwnProperty('collection') && !attributes[attr].hasOwnProperty('model');
})
.map(attr => {
const searchable = attributes[attr].type !== 'json' && attributes[attr].type !== 'array';
const obj = Object.assign(attributes[attr], { name: attr, label: upperFirst(attr), searchable, sortable: searchable });
return obj;
});
}
getListDisplay = () => (
get(this.props.schema, `models.${this.getPath()}.listDisplay`, [])
);
getModelName = () => {
const { match: { params: { slug, endPoint } } } = this.props;
return endPoint || slug;
}
getPath = () => {
const { match: { params: { slug, source, endPoint } } } = this.props;
return [slug, source, endPoint]
.filter(param => param !== undefined)
.join('.');
}
getSelectOptions = (input) => {
const selectOptions = this.getListDisplay().reduce((acc, curr) => {
if (curr.sortable === true) {
return acc.concat([curr.name]);
}
return acc;
}, []);
if (selectOptions.length === 0) {
selectOptions.push(this.getPrimaryKey());
}
return input.name === 'defaultSort' ? selectOptions : input.selectOptions;
}
getPluginHeaderActions = () => (
[
{
label: 'content-manager.popUpWarning.button.cancel',
kind: 'secondary',
onClick: this.handleReset,
type: 'button',
},
{
kind: 'primary',
label: 'content-manager.containers.Edit.submit',
onClick: this.handleSubmit,
type: 'submit',
},
]
);
getPrimaryKey = () => get(this.props.schema, ['models', this.getModelName()].concat(['primaryKey']), 'id');
getValue = (keys, type) => {
const value = get(this.props.schema, ['models'].concat(keys.split('.')));
return type === 'toggle' ? value : value.toString();
}
handleChange = (e) => {
const defaultSort = this.getDefaultSort();
const name = e.target.name.split('.');
name.pop();
const attrName = get(this.props.schema.models, name.concat(['name']));
const isDisablingDefaultSort = attrName === defaultSort && e.target.value === false;
if (isDisablingDefaultSort) {
const enableAttrsSort = this.getSelectOptions({ name: 'defaultSort' }).filter(attr => attr !== attrName);
if (enableAttrsSort.length === 0) {
strapi.notification.info('content-manager.notification.info.SettingPage.disableSort');
} else {
const newDefaultSort = enableAttrsSort.length === 0 ? this.getPrimaryKey() : enableAttrsSort[0];
const target = { name: `${this.getPath()}.defaultSort`, value: newDefaultSort };
this.props.onChangeSettings({ target });
this.props.onChangeSettings(e);
}
} else {
this.props.onChangeSettings(e);
}
}
handleClickEditAttr = (index) => {
const attrToEdit = get(this.props.schema, ['models'].concat(this.getPath().split('.')).concat(['listDisplay', index]), {});
this.props.onClickEditListItem(attrToEdit);
}
handleRemove = (index, keys) => {
const attrToRemove = get(this.getListDisplay(), index, {});
const defaultSort = this.getDefaultSort();
const isRemovingDefaultSort = defaultSort === attrToRemove.name;
if (isRemovingDefaultSort) {
const enableAttrsSort = this.getSelectOptions({ name: 'defaultSort' }).filter(attr => attr !== attrToRemove.name);
const newDefaultSort = enableAttrsSort.length > 1 ? enableAttrsSort[0] : this.getPrimaryKey();
const target = { name: `${this.getPath()}.defaultSort`, value: newDefaultSort };
this.props.onChangeSettings({ target });
}
this.props.onRemove(index, keys);
}
handleReset = (e) => {
e.preventDefault();
this.setState({ showWarningCancel: true });
}
handleSubmit = (e) => {
e.preventDefault();
this.setState({ showWarning: true });
}
findIndexListItemToEdit = () => {
const index = findIndex(this.getListDisplay(), ['name', get(this.props.settingPage, ['listItemToEdit', 'name'])]);
return index === -1 ? 0 : index;
}
// We need to remove the Over state on the DraggableAttr component
updateSiblingHoverState = () => {
this.setState(prevState => ({ isDraggingSibling: !prevState.isDraggingSibling }));
};
toggle = () => this.setState(prevState => ({ showWarning: !prevState.showWarning }));
toggleWarningCancel = () => this.setState(prevState => ({ showWarningCancel: !prevState.showWarningCancel }));
toggleDropdown = () => {
if (this.getDropDownItems().length > 0) {
this.setState(prevState => ({ isOpen: !prevState.isOpen }));
}
}
render() {
const { isDraggingSibling, isOpen, showWarning, showWarningCancel } = this.state;
const {
moveAttr,
onChangeSettings,
onClickAddAttr,
onReset,
onSubmit,
} = this.props;
const namePath = this.getPath();
return (
<React.Fragment>
<BackHeader onClick={() => this.props.history.goBack()} />
<div className={cn('container-fluid', styles.containerFluid)}>
<PluginHeader
actions={this.getPluginHeaderActions()}
title={`Content Manager - ${upperFirst(this.getModelName())}`}
description={{ id: 'content-manager.containers.SettingPage.pluginHeaderDescription' }}
/>
<PopUpWarning
isOpen={showWarning}
toggleModal={this.toggle}
content={{
title: 'content-manager.popUpWarning.title',
message: 'content-manager.popUpWarning.warning.updateAllSettings',
cancel: 'content-manager.popUpWarning.button.cancel',
confirm: 'content-manager.popUpWarning.button.confirm',
}}
popUpWarningType="danger"
onConfirm={() => {
onSubmit();
}}
/>
<PopUpWarning
isOpen={showWarningCancel}
toggleModal={this.toggleWarningCancel}
content={{
title: 'content-manager.popUpWarning.title',
message: 'content-manager.popUpWarning.warning.cancelAllSettings',
cancel: 'content-manager.popUpWarning.button.cancel',
confirm: 'content-manager.popUpWarning.button.confirm',
}}
popUpWarningType="danger"
onConfirm={() => {
onReset();
this.toggleWarningCancel();
}}
/>
<div className={cn('row', styles.container)}>
<Block
description="content-manager.containers.SettingPage.listSettings.description"
title="content-manager.containers.SettingPage.listSettings.title"
>
<form onSubmit={this.handleSubmit} className={styles.ctmForm}>
<div className="row">
<div className="col-md-12">
<div className="row">
{forms.inputs.map(input => {
const inputName = `${namePath}.${input.name}`;
return (
<Input
{...input}
key={input.name}
name={inputName}
onChange={onChangeSettings}
selectOptions={this.getSelectOptions(input)}
value={this.getValue(inputName, input.type)}
/>
);
})}
</div>
</div>
<div className="col-md-12">
<div className={styles.separator} />
</div>
</div>
<div className={styles.listDisplayWrapper}>
<div className="row">
<div className={cn('col-md-12', styles.draggedDescription)}>
<FormattedMessage id="content-manager.containers.SettingPage.attributes" />
<p>
<FormattedMessage id="content-manager.containers.SettingPage.attributes.description" />
</p>
</div>
<div className="col-md-5">
{this.getListDisplay().map((attr, index) => (
<div key={attr.name} className={styles.draggedWrapper}>
<div>{index}.</div>
<DraggableAttr
index={index}
isDraggingSibling={isDraggingSibling}
isEditing={index === this.findIndexListItemToEdit()}
key={attr.name}
keys={this.getPath()}
label={attr.label}
name={attr.name}
moveAttr={moveAttr}
onClickEditListItem={this.handleClickEditAttr}
onRemove={this.handleRemove}
updateSiblingHoverState={this.updateSiblingHoverState}
/>
</div>
))}
<div className={styles.dropdownWrapper}>
<ButtonDropdown isOpen={isOpen} toggle={this.toggleDropdown}>
<DropdownToggle>
<FormattedMessage id="content-manager.containers.SettingPage.addField">
{msg => <p>{msg}</p>}
</FormattedMessage>
</DropdownToggle>
<DropdownMenu>
{this.getDropDownItems().map((item, i) => {
if (i !== this.getDropDownItems().length - 1 && this.getDropDownItems().length > 0) {
return (
<React.Fragment key={item.name}>
<DropdownItem onClick={() => onClickAddAttr(item, this.getPath())}>
{item.label}
</DropdownItem>
<DropdownItem divider />
</React.Fragment>
);
}
return (
<DropdownItem
key={item.name}
onClick={() => onClickAddAttr(item, this.getPath())}
>
{item.label}
</DropdownItem>
);
})}
</DropdownMenu>
</ButtonDropdown>
</div>
</div>
<div className="col-md-7">
<div className={styles.editWrapper}>
<div className="row">
{forms.editList.map((input, i) => {
const indexListItemToEdit = this.findIndexListItemToEdit();
const inputName = `${namePath}.listDisplay.${indexListItemToEdit}.${input.name}`;
const inputType = this.getListDisplay()[indexListItemToEdit].type;
if (indexListItemToEdit === -1) {
return <div key={i} />;
}
if ((inputType === 'json' || inputType === 'array') && (input.name === 'sortable' || input.name === 'searchable')) {
return null;
}
return (
<Input
key={input.name}
onChange={this.handleChange}
value={this.getValue(inputName, input.type)}
{...input}
name={inputName}
/>
);
})}
</div>
</div>
</div>
</div>
</div>
</form>
</Block>
</div>
</div>
</React.Fragment>
);
}
}
SettingPage.defaultProps = {};
SettingPage.propTypes = {
history: PropTypes.object.isRequired,
match: PropTypes.object.isRequired,
moveAttr: PropTypes.func.isRequired,
onChangeSettings: PropTypes.func.isRequired,
onClickAddAttr: PropTypes.func.isRequired,
onClickEditListItem: PropTypes.func.isRequired,
onRemove: PropTypes.func.isRequired,
onReset: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
schema: PropTypes.object.isRequired,
settingPage: PropTypes.object.isRequired,
submitSuccess: PropTypes.bool.isRequired,
};
const mapDispatchToProps = (dispatch) => (
bindActionCreators(
{
moveAttr,
onChangeSettings,
onClickAddAttr,
onClickEditListItem,
onRemove,
onReset,
onSubmit,
},
dispatch,
)
);
const mapStateToProps = createStructuredSelector({
schema: makeSelectModifiedSchema(),
settingPage: makeSelectSettingPage(),
submitSuccess: makeSelectSubmitSuccess(),
});
const withConnect = connect(mapStateToProps, mapDispatchToProps);
const withReducer = injectReducer({ key: 'settingPage', reducer });
const withSaga = injectSaga({ key: 'settingPage', saga });
export default compose(
withReducer,
withSaga,
withConnect,
)(DragDropContext(HTML5Backend)(SettingPage));

View File

@ -0,0 +1,24 @@
/**
*
* SettingPage reducer
*/
import { fromJS } from 'immutable';
import { ON_CLICK_EDIT_LIST_ITEM } from './constants';
const initialState = fromJS({
listItemToEdit: fromJS({}),
indexListItemToEdit: 0,
});
function settingPageReducer(state = initialState, action) {
switch (action.type) {
case ON_CLICK_EDIT_LIST_ITEM:
return state
.update('listItemToEdit', () => fromJS(action.listItemToEdit));
default:
return state;
}
}
export default settingPageReducer;

View File

@ -0,0 +1,3 @@
function* defaultSaga() {}
export default defaultSaga;

View File

@ -0,0 +1,24 @@
/**
*
* SettingPage selectors
*/
import { createSelector } from 'reselect';
/**
* Direct selector to the settingPage state domain
*/
const selectSettingPageDomain = () => state => state.get('settingPage');
/**
* Default selector used by EditPage
*/
const makeSelectSettingPage = () => createSelector(
selectSettingPageDomain(),
(substate) => substate.toJS()
);
export default makeSelectSettingPage;

View File

@ -0,0 +1,128 @@
.containerFluid {
padding: 18px 30px;
> div:first-child {
max-height: 33px;
}
}
.container {
padding-top: 18px;
}
.main_wrapper{
background: #ffffff;
padding: 22px 28px 0px;
border-radius: 2px;
box-shadow: 0 2px 4px #E3E9F3;
}
.ctmForm {
padding-top: 2.6rem;
}
.separator {
margin-top: 19px;
border-top: 1px solid #F6F6F6;
}
.draggedWrapper {
display: flex;
> div:first-child {
height: 30px;
width: 20px;
margin-right: 10px;
text-align: right;
line-height: 30px;
}
}
.listDisplayWrapper {
padding-top: 21px;
}
.draggedDescription {
color: #333740;
font-size: 13px;
font-weight: 500;
> p {
margin-bottom: 28px;
color: #787E8F;
}
}
.editWrapper {
min-height: 246px;
padding: 24px 30px;
background-color: #FAFAFB;
border-radius: 2px;
}
.dropdownWrapper {
margin-left: 30px;
> div {
height: 30px;
width: 100%;
justify-content: space-between;
background: #ffffff;
color: #333740;
border: 1px solid #E3E9F3;
border-radius: 2px;
> button {
position: relative;
cursor: pointer;
padding-left: 10px !important;
line-height: 30px;
width: 100%;
color: #333740;
text-align: left;
background-color: #ffffff;
border: none;
font-size: 13px;
font-weight: 500;
&:focus, &:active, &:hover, &:visited {
background-color: transparent!important;
box-shadow: none;
color: #333740;
}
> p {
height: 100%;
margin-left: 20px;
margin-bottom: 0;
margin-top: -1px;
color: #007EFF !important;
font-size: 13px !important;
}
&:before {
position: absolute;
top: 0;
bottom: 0;
content: '\f067';
font-family: FontAwesome;
font-size: 10px;
color: #007EFF;
}
}
> div {
max-height: 180px;
min-width: calc(100% + 2px);
margin-left: -1px;
margin-top: -1px;
border-top-left-radius: 0 !important;
border-top-right-radius: 0;
overflow: scroll;
button {
height: 30px;
line-height: 30px;
div {
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}

View File

@ -0,0 +1,42 @@
{
"inputs": [
{
"label": { "id": "content-manager.form.Input.search" },
"customBootstrapClass": "col-md-5",
"didCheckErrors": false,
"errors": [],
"name": "generalSettings.search",
"type": "toggle",
"validations": {}
},
{
"label": { "id": "content-manager.form.Input.filters" },
"customBootstrapClass": "col-md-5",
"didCheckErrors": false,
"errors": [],
"name": "generalSettings.filters",
"type": "toggle",
"validations": {}
},
{
"label": { "id": "content-manager.form.Input.bulkActions" },
"customBootstrapClass": "col-md-2",
"didCheckErrors": false,
"errors": [],
"name": "generalSettings.bulkActions",
"type": "toggle",
"validations": {}
},
{
"label": { "id": "content-manager.form.Input.pageEntries" },
"customBootstrapClass": "col-md-3",
"didCheckErrors": false,
"errors": [],
"inputDescription": { "id": "content-manager.form.Input.pageEntries.inputDescription" },
"name": "generalSettings.pageEntries",
"selectOptions": ["10", "20", "50", "100"],
"type": "select",
"validations": {}
}
]
}

View File

@ -0,0 +1,221 @@
/**
*
* SettingsPage
*/
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
import { createStructuredSelector } from 'reselect';
import cn from 'classnames';
import { get, sortBy } from 'lodash';
import PropTypes from 'prop-types';
import { onChange, onSubmit, onReset } from 'containers/App/actions';
import { makeSelectModifiedSchema, makeSelectSubmitSuccess } from 'containers/App/selectors';
import Input from 'components/InputsIndex';
import PluginHeader from 'components/PluginHeader';
import PopUpWarning from 'components/PopUpWarning';
import Block from 'components/Block';
import SettingsRow from 'components/SettingsRow';
import injectReducer from 'utils/injectReducer';
import injectSaga from 'utils/injectSaga';
import reducer from './reducer';
import saga from './saga';
import styles from './styles.scss';
import forms from './forms.json';
class SettingsPage extends React.PureComponent {
state = { showWarning: false, showWarningCancel: false };
componentDidUpdate(prevProps) {
if (prevProps.submitSuccess !== this.props.submitSuccess) {
this.toggle();
}
}
componentWillUnmount() {
this.props.onReset();
}
getModels = (data = this.props.schema.models, destination = '/') => {
const models = Object.keys(data).reduce((acc, curr) => {
if (curr !== 'plugins') {
if (!data[curr].fields && _.isObject(data[curr])) {
return this.getModels(data[curr], `${destination}${curr}/`);
}
return acc.concat([{ name: curr, destination: `${destination}${curr}` }]);
}
return this.getModels(data[curr], `${destination}${curr}/`);
}, []);
return sortBy(
models.filter(obj => obj.name !== 'permission' && obj.name !== 'role'),
['name'],
);
}
getPluginHeaderActions = () => (
[
{
label: 'content-manager.popUpWarning.button.cancel',
kind: 'secondary',
onClick: this.handleReset,
type: 'button',
},
{
kind: 'primary',
label: 'content-manager.containers.Edit.submit',
onClick: this.handleSubmit,
type: 'submit',
},
]
);
getValue = (input) => {
const { schema: { generalSettings } } = this.props;
const value = get(generalSettings, input.name.split('.')[1], input.type === 'toggle' ? false : 10);
return input.type === 'toggle' ? value : value.toString();
}
handleClick = (destination) => {
const { location: { pathname } } = this.props;
this.props.history.push(`${pathname}${destination}`);
}
handleReset = (e) => {
e.preventDefault();
this.setState({ showWarningCancel: true });
}
handleSubmit = (e) => {
e.preventDefault();
this.setState({ showWarning: true });
}
toggle = () => this.setState(prevState => ({ showWarning: !prevState.showWarning }));
toggleWarningCancel = () => this.setState(prevState => ({ showWarningCancel: !prevState.showWarningCancel }));
render() {
const { showWarning, showWarningCancel } = this.state;
const { onChange, onReset, onSubmit } = this.props;
return (
<div className={cn('container-fluid', styles.containerFluid)}>
<PluginHeader
actions={this.getPluginHeaderActions()}
title="Content Manager"
description={{ id: 'content-manager.containers.SettingsPage.pluginHeaderDescription' }}
/>
<PopUpWarning
isOpen={showWarning}
toggleModal={this.toggle}
content={{
title: 'content-manager.popUpWarning.title',
message: 'content-manager.popUpWarning.warning.updateAllSettings',
cancel: 'content-manager.popUpWarning.button.cancel',
confirm: 'content-manager.popUpWarning.button.confirm',
}}
popUpWarningType="danger"
onConfirm={() => {
onSubmit();
}}
/>
<PopUpWarning
isOpen={showWarningCancel}
toggleModal={this.toggleWarningCancel}
content={{
title: 'content-manager.popUpWarning.title',
message: 'content-manager.popUpWarning.warning.cancelAllSettings',
cancel: 'content-manager.popUpWarning.button.cancel',
confirm: 'content-manager.popUpWarning.button.confirm',
}}
popUpWarningType="danger"
onConfirm={() => {
onReset();
this.toggleWarningCancel();
}}
/>
<div className={cn('row', styles.container)}>
<Block
description="content-manager.containers.SettingsPage.Block.generalSettings.description"
title="content-manager.containers.SettingsPage.Block.generalSettings.title"
>
<form onSubmit={this.handleSubmit} className={styles.ctmForm}>
<div className="row">
<div className="col-md-10">
<div className="row">
{forms.inputs.map(input => (
<Input
key={input.name}
onChange={onChange}
value={this.getValue(input)}
{...input}
/>
))}
</div>
</div>
</div>
</form>
</Block>
<Block
title="content-manager.containers.SettingsPage.Block.contentType.title"
description="content-manager.containers.SettingsPage.Block.contentType.description"
>
<div className={styles.contentTypesWrapper}>
{this.getModels().map(model => <SettingsRow key={model.name} {...model} onClick={this.handleClick} />)}
</div>
</Block>
</div>
</div>
);
}
}
SettingsPage.defaultProps = {};
SettingsPage.propTypes = {
history: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
onReset: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
schema: PropTypes.object.isRequired,
submitSuccess: PropTypes.bool.isRequired,
};
const mapDispatchToProps = (dispatch) => (
bindActionCreators(
{
onChange,
onReset,
onSubmit,
},
dispatch,
)
);
const mapStateToProps = createStructuredSelector({
schema: makeSelectModifiedSchema(),
submitSuccess: makeSelectSubmitSuccess(),
});
const withConnect = connect(mapStateToProps, mapDispatchToProps);
const withReducer = injectReducer({ key: 'settingsPage', reducer });
const withSaga = injectSaga({ key: 'settingsPage', saga });
export default compose(
withReducer,
withSaga,
withConnect,
)(SettingsPage);

View File

@ -0,0 +1,17 @@
/**
*
* SettingsPage reducer
*/
import { fromJS } from 'immutable';
const initialState = fromJS({});
function settingsPageReducer(state = initialState, action) {
switch (action.type) {
default:
return state;
}
}
export default settingsPageReducer;

View File

@ -0,0 +1,3 @@
function* defaultSaga() {}
export default defaultSaga;

View File

@ -0,0 +1,24 @@
/**
*
* SettingsPage selectors
*/
import { createSelector } from 'reselect';
/**
* Direct selector to the settingsPage state domain
*/
const selectSettingsPageDomain = () => state => state.get('settingsPage');
/**
* Default selector used by EditPage
*/
const makeSelectSettingsPage = () => createSelector(
selectSettingsPageDomain(),
(substate) => substate.toJS()
);
export default makeSelectSettingsPage;

View File

@ -0,0 +1,38 @@
.containerFluid {
padding: 18px 30px;
> div:first-child {
max-height: 33px;
}
}
.container {
padding-top: 18px;
> div:last-child {
> div {
padding-bottom: 0 !important;
}
}
}
.main_wrapper{
background: #ffffff;
padding: 22px 28px 0px;
border-radius: 2px;
box-shadow: 0 2px 4px #E3E9F3;
}
.ctmForm {
padding-top: 2.6rem;
}
.contentTypesWrapper {
padding-top: 9px;
margin-left: -28px;
margin-right: -28px;
> div:last-child {
> div {
border-bottom: none;
}
}
}

View File

@ -0,0 +1 @@
export default [];

View File

@ -14,16 +14,32 @@
"containers.List.pluginHeaderDescription.singular": "{label} entry found",
"components.LimitSelect.itemsPerPage": "Items per page",
"containers.List.errorFetchRecords": "Error",
"containers.SettingPage.addField": "Add new field",
"containers.SettingPage.attributes": "Attributes fields",
"containers.SettingPage.attributes.description": "Define the order of the attributes",
"containers.SettingPage.listSettings.title": "List — Settings",
"containers.SettingPage.listSettings.description": "Configure the options for this content type",
"containers.SettingPage.pluginHeaderDescription": "Configure the specific settings for this Content Type",
"containers.SettingsPage.pluginHeaderDescription": "Configure the default settings for all your Content types",
"containers.SettingsPage.Block.generalSettings.description": "Configure the default options for your Content Types",
"containers.SettingsPage.Block.generalSettings.title": "General",
"containers.SettingsPage.Block.contentType.title": "Content Types",
"containers.SettingsPage.Block.contentType.description": "Configure the specific settings",
"components.AddFilterCTA.add": "Filters",
"components.AddFilterCTA.hide": "Filters",
"components.FilterOptions.button.apply": "Apply",
"components.DraggableAttr.edit": "Click to edit",
"components.FiltersPickWrapper.PluginHeader.actions.apply": "Apply",
"components.FiltersPickWrapper.PluginHeader.actions.clearAll": "Clear all",
"components.FiltersPickWrapper.PluginHeader.description": "Set the conditions to apply to filter the entries",
"components.FiltersPickWrapper.PluginHeader.title.filter": "Filters",
"components.FiltersPickWrapper.hide": "Hide",
"components.FilterOptions.button.apply": "Apply",
"components.FilterOptions.FILTER_TYPES.=": "is",
"components.FilterOptions.FILTER_TYPES._ne": "is not",
"components.FilterOptions.FILTER_TYPES._lt": "is lower than",
@ -71,7 +87,19 @@
"error.validation.minSupMax": "Can't be superior",
"error.validation.json": "This is not a JSON",
"form.Input.label.inputDescription": "This value overrides the label displayed in the table's head",
"form.Input.label": "Label",
"form.Input.search": "Enable search",
"form.Input.search.field": "Enable search on this field",
"form.Input.filters": "Enable filters",
"form.Input.sort.field": "Enable sort on this field",
"form.Input.bulkActions": "Enable bulk actions",
"form.Input.pageEntries": "Entries per page",
"form.Input.pageEntries.inputDescription": "Note: You can override this value in the Content Type settings page.",
"form.Input.defaultSort": "Default sort attribute",
"notification.error.relationship.fetch": "An error occurred during relationship fetch.",
"notification.info.SettingPage.disableSort": "You need to have one attribute with the sorting allowed",
"success.record.delete": "Deleted",
"success.record.save": "Saved",
@ -82,5 +110,7 @@
"popUpWarning.button.confirm": "Confirm",
"popUpWarning.title": "Please confirm",
"popUpWarning.bodyMessage.contentType.delete": "Are you sure you want to delete this entry?",
"popUpWarning.bodyMessage.contentType.delete.all": "Are you sure you want to delete theses entries?"
"popUpWarning.bodyMessage.contentType.delete.all": "Are you sure you want to delete theses entries?",
"popUpWarning.warning.cancelAllSettings": "Are you sure you want to cancel your modifications?",
"popUpWarning.warning.updateAllSettings": "This will modify all your settings"
}

View File

@ -4,7 +4,6 @@
"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.",
"containers.Home.pluginHeaderDescription": "Un outil complet pour gérer facilement vos données.",
"containers.Edit.submit": "Valider",
"containers.Edit.editing": "Édition en cours...",
"containers.Edit.delete": "Supprimer",
@ -16,25 +15,39 @@
"components.LimitSelect.itemsPerPage": "Éléments par page",
"containers.List.errorFetchRecords": "Erreur",
"containers.SettingPage.addField": "Ajouter un nouveau champs",
"containers.SettingPage.attributes": "Attributs",
"containers.SettingPage.attributes.description": "Organiser les attributs du modèle",
"containers.SettingPage.pluginHeaderDescription": "Configurez les paramètres de ce modèle",
"containers.SettingPage.listSettings.title": "Liste — Paramètres",
"containers.SettingPage.listSettings.description": "Configurez les options de ce modèle",
"containers.SettingsPage.pluginHeaderDescription": "Configurez les paramètres par défaut de vos modèles",
"containers.SettingsPage.Block.generalSettings.description": "Configurez les options par défault de vos modèles",
"containers.SettingsPage.Block.generalSettings.title": "Général",
"components.AddFilterCTA.add": "Filtres",
"components.AddFilterCTA.hide": "Filtres",
"components.FilterOptions.button.apply": "Appliquer",
"components.DraggableAttr.edit": "Clicquez pour modifier",
"components.FiltersPickWrapper.PluginHeader.actions.apply": "Appliquer",
"components.FiltersPickWrapper.PluginHeader.actions.clearAll": "Tout supprimer",
"components.FiltersPickWrapper.PluginHeader.description": "Définissez les conditions des filtres à appliquer",
"components.FiltersPickWrapper.PluginHeader.title.filter": "Filtres",
"components.FiltersPickWrapper.hide": "Fermer",
"components.Search.placeholder": "Rechercher une entrée...",
"components.TableDelete.entries.plural": "{number} entrées sélectionnées",
"components.TableDelete.entries.singular": "{number} entrée sélectionnée",
"components.TableDelete.delete": "Tout supprimer",
"components.TableEmpty.withFilters": "Aucun {contentType} n'a été trouvé avec ces filtres...",
"components.TableEmpty.withoutFilter": "Aucun {contentType} n'a été trouvé...",
"components.TableEmpty.withSearch": "Aucun {contentType} n'a été trouvé avec cette recherche ({search})...",
"components.FilterOptions.button.apply": "Appliquer",
"components.FilterOptions.FILTER_TYPES.=": "est",
"components.FilterOptions.FILTER_TYPES._ne": "n'est pas",
"components.FilterOptions.FILTER_TYPES._lt": "inférieur à",
@ -71,7 +84,19 @@
"error.validation.minSupMax": "Ne peut pas être plus grand",
"error.validation.json": "Le format JSON n'est pas respecté",
"form.Input.label": "Label",
"form.Input.label.inputDescription": "Cette valeur modifie celle du champs de la table",
"form.Input.search": "Autoriser la search",
"form.Input.search.field": "Autoriser la search sur ce champs",
"form.Input.filters": "Autoriser les filtres",
"form.Input.sort.field": "Autoriser le tri sur ce champs",
"form.Input.bulkActions": "Autoriser les actions groupées",
"form.Input.pageEntries": "Nombre d'entrées par page",
"form.Input.pageEntries.inputDescription": "Note: Vous pouvez modifier ces valeurs par modèle",
"form.Input.defaultSort": "Attribut de tri par défault",
"notification.error.relationship.fetch": "Une erreur est survenue en récupérant les relations.",
"notification.info.SettingPage.disableSort": "Vous devez avoir au moins un attribut de tri par défaut",
"success.record.delete": "Supprimé",
"success.record.save": "Sauvegardé",
@ -80,5 +105,7 @@
"popUpWarning.button.confirm": "Confirmer",
"popUpWarning.title": "Confirmation requise",
"popUpWarning.bodyMessage.contentType.delete": "Êtes-vous sûr de vouloir supprimer cette entrée ?",
"popUpWarning.bodyMessage.contentType.delete.all": "Êtes-vous sûr de vouloir supprimer ces entrées ?"
"popUpWarning.bodyMessage.contentType.delete.all": "Êtes-vous sûr de vouloir supprimer ces entrées ?",
"popUpWarning.warning.cancelAllSettings": "Êtes-vous sûr de vouloir vos modifications?",
"popUpWarning.warning.updateAllSettings": "Cela modifiera tous vos précédents paramètres."
}

View File

@ -0,0 +1,243 @@
const _ = require('lodash');
const pluralize = require('pluralize');
module.exports = async cb => {
const pickData = (model) => _.pick(model, [
'info',
'connection',
'collectionName',
'attributes',
'identity',
'globalId',
'globalName',
'orm',
'loadedModel',
'primaryKey',
'associations'
]);
const models = _.mapValues(strapi.models, pickData);
delete models['core_store'];
const pluginsModel = Object.keys(strapi.plugins).reduce((acc, current) => {
acc[current] = {
models: _.mapValues(strapi.plugins[current].models, pickData),
};
return acc;
}, {});
// Init schema
const schema = {
generalSettings: {
search: true,
filters: true,
bulkActions: true,
pageEntries: 10,
},
models: {
plugins: {},
},
};
const buildSchema = (model, name, plugin = false) => {
// Model data
const schemaModel = Object.assign({
label: _.upperFirst(name),
labelPlural: _.upperFirst(pluralize(name)),
orm: model.orm || 'mongoose',
search: true,
filters: true,
bulkActions: true,
pageEntries: 10,
defaultSort: model.primaryKey,
sort: 'ASC',
}, model);
// Fields (non relation)
schemaModel.fields = _.mapValues(_.pickBy(model.attributes, attribute =>
!attribute.model && !attribute.collection
), (value, attribute) => ({
label: _.upperFirst(attribute),
description: '',
type: value.type || 'string',
}));
// Select fields displayed in list view
// schemaModel.list = _.slice(_.keys(schemaModel.fields), 0, 4);
schemaModel.listDisplay = Object.keys(schemaModel.fields)
// Construct Array of attr ex { type: 'string', label: 'Foo', name: 'Foo', description: '' }
// NOTE: Do we allow sort on boolean?
.map(attr => {
const attrType = schemaModel.fields[attr].type;
const sortable = attrType !== 'json' && attrType !== 'array';
return Object.assign(schemaModel.fields[attr], { name: attr, sortable, searchable: sortable });
})
// Retrieve only the fourth first items
.slice(0, 4);
schemaModel.listDisplay.splice(0, 0, {
name: model.primaryKey || 'id',
label: 'Id',
type: 'string',
sortable: true,
searchable: true,
});
if (model.associations) {
// Model relations
schemaModel.relations = model.associations.reduce((acc, current) => {
const displayedAttribute = current.plugin ?
_.get(pluginsModel, [current.plugin, 'models', current.model || current.collection, 'info', 'mainField']) ||
_.findKey(_.get(pluginsModel, [current.plugin, 'models', current.model || current.collection, 'attributes']), { type : 'string'}) ||
'id' :
_.get(models, [current.model || current.collection, 'info', 'mainField']) ||
_.findKey(_.get(models, [current.model || current.collection, 'attributes']), { type : 'string'}) ||
'id';
acc[current.alias] = {
...current,
description: '',
displayedAttribute,
};
return acc;
}, {});
}
if (plugin) {
return _.set(schema.models.plugins, `${plugin}.${name}`, schemaModel);
}
// Set the formatted model to the schema
schema.models[name] = schemaModel;
};
_.forEach(pluginsModel, (plugin, pluginName) => {
_.forEach(plugin.models, (model, name) => {
buildSchema(model, name, pluginName);
});
});
// Generate schema for models.
_.forEach(models, (model, name) => {
buildSchema(model, name);
});
const pluginStore = strapi.store({
environment: '',
type: 'plugin',
name: 'content-manager'
});
const getApis = (data) => Object.keys(data).reduce((acc, curr) => {
if (data[curr].fields) {
return acc.concat([curr]);
}
if (curr === 'plugins') {
Object.keys(data[curr]).map(plugin => {
Object.keys(data[curr][plugin]).map(api => {
acc = acc.concat([`${curr}.${plugin}.${api}`]);
});
});
}
return acc;
}, []);
const getApisKeys = (data, sameArray) => sameArray.map(apiPath => {
const fields = Object.keys(_.get(data.models, apiPath.concat(['fields'])));
return fields.map(field => `${apiPath.join('.')}.fields.${field}`);
});
try {
const prevSchema = await pluginStore.get({ key: 'schema' });
if (!prevSchema) {
pluginStore.set({ key: 'schema', value: schema });
return cb();
}
const splitted = str => str.split('.');
const prevSchemaApis = getApis(prevSchema.models);
const schemaApis = getApis(schema.models);
const apisToAdd = schemaApis.filter(api => prevSchemaApis.indexOf(api) === -1).map(splitted);
const apisToRemove = prevSchemaApis.filter(api => schemaApis.indexOf(api) === -1).map(splitted);
const sameApis = schemaApis.filter(api => prevSchemaApis.indexOf(api) !== -1).map(splitted);
const schemaSameApisKeys = _.flattenDeep(getApisKeys(schema, sameApis));
const prevSchemaSameApisKeys = _.flattenDeep(getApisKeys(prevSchema, sameApis));
const sameApisAttrToAdd = schemaSameApisKeys.filter(attr => prevSchemaSameApisKeys.indexOf(attr) === -1).map(splitted);
const sameApisAttrToRemove = prevSchemaSameApisKeys.filter(attr => schemaSameApisKeys.indexOf(attr) === -1).map(splitted);
// Remove api
apisToRemove.map(apiPath => {
_.unset(prevSchema.models, apiPath);
});
// Remove API attribute
sameApisAttrToRemove.map(attrPath => {
// Check default sort and change it if needed
_.unset(prevSchema.models, attrPath);
const apiPath = attrPath.length > 3 ? _.take(attrPath, 3) : _.take(attrPath, 1);
const listDisplayPath = apiPath.concat('listDisplay');
const prevListDisplay = _.get(prevSchema.models, listDisplayPath);
const defaultSortPath = apiPath.concat('defaultSort');
const currentAttr = attrPath.slice(-1);
const defaultSort = _.get(prevSchema.models, defaultSortPath);
if (_.includes(currentAttr, defaultSort)) {
_.set(prevSchema.models, defaultSortPath, _.get(schema.models, defaultSortPath));
}
// Update the displayed fields
const updatedListDisplay = prevListDisplay.filter(obj => obj.name !== currentAttr.join());
if (updatedListDisplay.length === 0) {
// Update it with the one from the generaeted schema
_.set(prevSchema.models, listDisplayPath, _.get(schema.models, listDisplayPath, []));
} else {
_.set(prevSchema.models, listDisplayPath, updatedListDisplay);
}
});
// Add API
apisToAdd.map(apiPath => {
const api = _.get(schema.models, apiPath);
const { search, filters, bulkActions, pageEntries } = _.get(prevSchema, 'generalSettings');
_.set(api, 'filters', filters);
_.set(api, 'search', search);
_.set(api, 'bulkActions', bulkActions);
_.set(api, 'pageEntries', pageEntries);
_.set(prevSchema.models, apiPath, api);
});
// Add attribute to existing API
sameApisAttrToAdd.map(attrPath => {
const attr = _.get(schema.models, attrPath);
_.set(prevSchema.models, attrPath, attr);
});
// Update other keys
sameApis.map(apiPath => {
const keysToUpdate = ['relations', 'loadedModel', 'associations', 'attributes'].map(key => apiPath.concat(key));
keysToUpdate.map(keyPath => {
const newValue = _.get(schema.models, keyPath);
_.set(prevSchema.models, keyPath, newValue);
});
});
await pluginStore.set({ key: 'schema', value: prevSchema });
} catch(err) {
console.log('error', err);
}
cb();
};

View File

@ -0,0 +1 @@
{}

View File

@ -32,6 +32,14 @@
"policies": ["routing"]
}
},
{
"method": "PUT",
"path": "/models",
"handler": "ContentManager.updateSettings",
"config": {
"policies": ["routing"]
}
},
{
"method": "GET",
"path": "/explorer/:model/:id",

View File

@ -14,32 +14,15 @@ module.exports = {
},
models: async ctx => {
const pickData = (model) => _.pick(model, [
'info',
'connection',
'collectionName',
'attributes',
'identity',
'globalId',
'globalName',
'orm',
'loadedModel',
'primaryKey',
'associations'
]);
const models = _.mapValues(strapi.models, pickData);
delete models['core_store'];
const pluginsStore = strapi.store({
environment: '',
type: 'plugin',
name: 'content-manager',
});
const models = await pluginsStore.get({ key: 'schema' });
ctx.body = {
models,
plugins: Object.keys(strapi.plugins).reduce((acc, current) => {
acc[current] = {
models: _.mapValues(strapi.plugins[current].models, pickData)
};
return acc;
}, {})
};
},
@ -105,6 +88,18 @@ module.exports = {
}
},
updateSettings: async ctx => {
const { schema } = ctx.request.body;
const pluginStore = strapi.store({
environment: '',
type: 'plugin',
name: 'content-manager'
});
await pluginStore.set({ key: 'schema', value: schema });
return ctx.body = { ok: true };
},
delete: async ctx => {
ctx.body = await strapi.plugins['content-manager'].services['contentmanager'].delete(ctx.params, ctx.request.query);
},

View File

@ -22,7 +22,12 @@
"prepublishOnly": "IS_MONOREPO=true npm run build"
},
"devDependencies": {
"codemirror": "^5.39.0",
"draft-js": "^0.10.5",
"react-dnd": "^5.0.0",
"react-dnd-html5-backend": "^5.0.1",
"react-select": "^1.2.1",
"showdown": "^1.8.6",
"strapi-helper-plugin": "3.0.0-alpha.12.7.1"
},
"author": {

Some files were not shown because too many files have changed in this diff Show More