Content manager edit layout and fields links list

This commit is contained in:
Ky 2019-02-12 10:50:41 +01:00
parent c1ffbbfb57
commit cbb53ac8d4
16 changed files with 594 additions and 325 deletions

View File

@ -20,7 +20,7 @@ import { bindActionCreators, compose } from 'redux';
// Actions required for disabling and enabling the OverlayBlocker
import {
disableGlobalOverlayBlocker,
enableGlobalOverlayBlocker
enableGlobalOverlayBlocker,
} from 'actions/overlayBlocker';
import { pluginLoaded, updatePlugin } from 'containers/App/actions';
import {
@ -30,7 +30,7 @@ import {
makeSelectIsAppLoading,
makeSelectShowGlobalAppBlocker,
selectHasUserPlugin,
selectPlugins
selectPlugins,
} from 'containers/App/selectors';
// Design
import ComingSoonPage from 'containers/ComingSoonPage';
@ -60,7 +60,7 @@ import styles from './styles.scss';
const PLUGINS_TO_BLOCK_PRODUCTION = [
'content-type-builder',
'settings-manager'
'settings-manager',
];
export class AdminPage extends React.Component {
@ -71,7 +71,7 @@ export class AdminPage extends React.Component {
disableGlobalOverlayBlocker: this.props.disableGlobalOverlayBlocker,
enableGlobalOverlayBlocker: this.props.enableGlobalOverlayBlocker,
plugins: this.props.plugins,
updatePlugin: this.props.updatePlugin
updatePlugin: this.props.updatePlugin,
});
componentDidMount() {
@ -84,7 +84,7 @@ export class AdminPage extends React.Component {
const {
adminPage: { uuid },
location: { pathname },
plugins
plugins,
} = this.props;
if (prevProps.location.pathname !== pathname) {
@ -206,7 +206,7 @@ export class AdminPage extends React.Component {
showLoading = () => {
const {
isAppLoading,
adminPage: { isLoading }
adminPage: { isLoading },
} = this.props;
return (
@ -219,7 +219,7 @@ export class AdminPage extends React.Component {
retrievePlugins = () => {
const {
adminPage: { currentEnvironment },
plugins
plugins,
} = this.props;
if (currentEnvironment === 'production') {
@ -290,11 +290,11 @@ AdminPage.childContextTypes = {
disableGlobalOverlayBlocker: PropTypes.func,
enableGlobalOverlayBlocker: PropTypes.func,
plugins: PropTypes.object,
updatePlugin: PropTypes.func
updatePlugin: PropTypes.func,
};
AdminPage.contextTypes = {
router: PropTypes.object.isRequired
router: PropTypes.object.isRequired,
};
AdminPage.defaultProps = {
@ -302,7 +302,7 @@ AdminPage.defaultProps = {
appPlugins: [],
hasUserPlugin: true,
isAppLoading: true,
overlayBlockerData: {}
overlayBlockerData: {},
};
AdminPage.propTypes = {
@ -320,7 +320,7 @@ AdminPage.propTypes = {
pluginLoaded: PropTypes.func.isRequired,
plugins: PropTypes.object.isRequired,
showGlobalAppBlocker: PropTypes.bool.isRequired,
updatePlugin: PropTypes.func.isRequired
updatePlugin: PropTypes.func.isRequired,
};
const mapStateToProps = createStructuredSelector({
@ -331,7 +331,7 @@ const mapStateToProps = createStructuredSelector({
hasUserPlugin: selectHasUserPlugin(),
isAppLoading: makeSelectIsAppLoading(),
plugins: selectPlugins(),
showGlobalAppBlocker: makeSelectShowGlobalAppBlocker()
showGlobalAppBlocker: makeSelectShowGlobalAppBlocker(),
});
function mapDispatchToProps(dispatch) {
@ -341,7 +341,7 @@ function mapDispatchToProps(dispatch) {
enableGlobalOverlayBlocker,
getAdminData,
pluginLoaded,
updatePlugin
updatePlugin,
},
dispatch
);

View File

@ -23,12 +23,12 @@ import {
makeSelectCurrentEnv,
makeSelectPluginDeleteAction,
makeSelectPlugins,
makeSelectIsLoading
makeSelectIsLoading,
} from './selectors';
import {
getPlugins,
onDeletePluginClick,
onDeletePluginConfirm
onDeletePluginConfirm,
} from './actions';
import reducer from './reducer';
import saga from './saga';
@ -37,7 +37,7 @@ import styles from './styles.scss';
export class ListPluginsPage extends React.Component {
// eslint-disable-line react/prefer-stateless-function
getChildContext = () => ({
currentEnvironment: this.props.currentEnvironment
currentEnvironment: this.props.currentEnvironment,
});
componentDidMount() {
@ -64,10 +64,10 @@ export class ListPluginsPage extends React.Component {
<div className={cn('container-fluid', styles.listPluginsPage)}>
<PluginHeader
title={{
id: 'app.components.ListPluginsPage.title'
id: 'app.components.ListPluginsPage.title',
}}
description={{
id: 'app.components.ListPluginsPage.description'
id: 'app.components.ListPluginsPage.description',
}}
actions={[]}
/>
@ -85,7 +85,7 @@ export class ListPluginsPage extends React.Component {
}
ListPluginsPage.childContextTypes = {
currentEnvironment: PropTypes.string.isRequired
currentEnvironment: PropTypes.string.isRequired,
};
ListPluginsPage.contextTypes = {};
@ -98,14 +98,14 @@ ListPluginsPage.propTypes = {
onDeletePluginClick: PropTypes.func.isRequired,
onDeletePluginConfirm: PropTypes.func.isRequired,
pluginActionSucceeded: PropTypes.bool.isRequired,
plugins: PropTypes.object.isRequired
plugins: PropTypes.object.isRequired,
};
const mapStateToProps = createStructuredSelector({
currentEnvironment: makeSelectCurrentEnv(),
isLoading: makeSelectIsLoading(),
pluginActionSucceeded: makeSelectPluginDeleteAction(),
plugins: makeSelectPlugins()
plugins: makeSelectPlugins(),
});
function mapDispatchToProps(dispatch) {
@ -113,15 +113,15 @@ function mapDispatchToProps(dispatch) {
{
getPlugins,
onDeletePluginClick,
onDeletePluginConfirm
onDeletePluginConfirm,
},
dispatch
dispatch,
);
}
const withConnect = connect(
mapStateToProps,
mapDispatchToProps
mapDispatchToProps,
);
/* Remove this line if the container doesn't have a route and
@ -137,5 +137,5 @@ const withSaga = injectSaga({ key: 'listPluginsPage', saga });
export default compose(
withReducer,
withSaga,
withConnect
withConnect,
)(ListPluginsPage);

View File

@ -18,7 +18,7 @@ import { translationMessages } from './i18n';
const LoadableApp = Loadable({
loader: () => import('containers/App'),
loading: LoadingIndicatorPage
loading: LoadingIndicatorPage,
});
const tryRequireRoot = source => {
@ -70,7 +70,7 @@ function Comp(props) {
if (window.Cypress) {
window.__store__ = Object.assign(window.__store__ || {}, {
[pluginId]: store
[pluginId]: store,
});
}
@ -105,7 +105,7 @@ strapi.registerPlugin({
name: pluginPkg.strapi.name,
pluginRequirements,
preventComponentRendering: false,
translationMessages
translationMessages,
});
// Export store

View File

@ -0,0 +1 @@
<svg width="13" height="11" xmlns="http://www.w3.org/2000/svg"><g fill="#4B515A" fill-rule="evenodd"><rect x="4" y="8" width="9" height="3" rx="1.5"/><rect y="4" width="9" height="3" rx="1.5"/><rect x="3" width="9" height="3" rx="1.5"/></g></svg>

After

Width:  |  Height:  |  Size: 246 B

View File

@ -0,0 +1 @@
<svg width="13" height="11" xmlns="http://www.w3.org/2000/svg"><g fill="#007EFF" fill-rule="evenodd"><rect x="4" y="8" width="9" height="3" rx="1.5"/><rect y="4" width="9" height="3" rx="1.5"/><rect x="3" width="9" height="3" rx="1.5"/></g></svg>

After

Width:  |  Height:  |  Size: 246 B

View File

@ -0,0 +1,62 @@
/**
*
* NavLink
*
*/
import React from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { isObject } from 'lodash';
import cn from 'classnames';
import styles from './styles.scss';
function NavLink(props) {
let content = props.children;
if (typeof props.message === 'string') {
content = props.message;
}
if (isObject(props.message) && props.message.id) {
content = <FormattedMessage id={props.message.id} />;
}
let icon = <i className={`fa ${props.icon}`} />;
if (props.icon === 'layout') {
icon = <i className={cn(styles.fa, styles.layout)} />;
}
return (
<Link to={props.url} className={cn(styles.navLink)}>
{icon}
{content}
</Link>
);
}
NavLink.defaultProps = {
children: '',
icon: '',
message: '',
url: '',
};
NavLink.propTypes = {
children: PropTypes.node,
icon: PropTypes.string,
message: PropTypes.oneOfType([
PropTypes.func,
PropTypes.string,
PropTypes.shape({
id: PropTypes.string,
params: PropTypes.object,
}),
]),
url: PropTypes.string,
};
export default NavLink;

View File

@ -0,0 +1,28 @@
a.navLink {
text-decoration: none;
width: 100%;
display: block;
span, i {
color: #333740;
}
span {
font-size: 13px;
}
i {
margin-right: 10px;
width: 13px;
height: 11px;
}
.fa.layout {
background-image: url('../../assets/icons/icon_layout.svg');
display: inline-block;
}
&:hover {
span, i {
color: #007EFF;
}
.fa.layout {
background-image: url('../../assets/icons/icon_layout_hover.svg');
}
}
}

View File

@ -2,16 +2,19 @@ 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) => {
const bootstrap = plugin =>
new Promise((resolve, reject) => {
request('/content-manager/models', { method: 'GET' })
.then(models => {
const menu = [{
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);
})

View File

@ -7,7 +7,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
import { Link } from 'react-router-dom';
import { createStructuredSelector } from 'reselect';
import PropTypes from 'prop-types';
import {
@ -19,7 +18,7 @@ import {
toNumber,
toString,
truncate,
replace
replace,
} from 'lodash';
import HTML5Backend from 'react-dnd-html5-backend';
import { DragDropContext } from 'react-dnd';
@ -32,7 +31,7 @@ import EmptyAttributesBlock from 'components/EmptyAttributesBlock';
import LoadingIndicator from 'components/LoadingIndicator';
import PluginHeader from 'components/PluginHeader';
import PopUpWarning from 'components/PopUpWarning';
import ListRow from 'components/ListRow';
import NavLink from 'components/NavLink';
// Plugin's components
import CustomDragLayer from 'components/CustomDragLayer';
import Edit from 'components/Edit';
@ -59,13 +58,15 @@ import {
resetProps,
setFileRelations,
setFormErrors,
submit
submit,
} from './actions';
import reducer from './reducer';
import saga from './saga';
import makeSelectEditPage from './selectors';
import styles from './styles.scss';
const pluginId = 'content-manager';
export class EditPage extends React.Component {
state = { showWarning: false, showWarningDelete: false };
@ -87,17 +88,17 @@ export class EditPage extends React.Component {
includes(this.props.location.search, '?redirectUrl')
) {
const redirectUrl = this.props.location.search.split(
'?redirectUrl='
'?redirectUrl=',
)[1];
this.props.history.push({
pathname: redirectUrl.split('?')[0],
search: redirectUrl.split('?')[1]
search: redirectUrl.split('?')[1],
});
} else {
this.props.history.push({
pathname: replace(this.props.location.pathname, '/create', ''),
search: `?source=${this.getSource()}`
search: `?source=${this.getSource()}`,
});
}
}
@ -122,7 +123,7 @@ export class EditPage extends React.Component {
getLayout = () =>
bindLayout.call(
this,
get(this.props.schema, ['layout', this.getModelName()], {})
get(this.props.schema, ['layout', this.getModelName()], {}),
);
/**
@ -135,9 +136,9 @@ export class EditPage extends React.Component {
this.props.editPage.formValidations,
[
findIndex(this.props.editPage.formValidations, ['name', name]),
'validations'
'validations',
],
{}
{},
);
getDisplayedFields = () =>
@ -153,7 +154,7 @@ export class EditPage extends React.Component {
'models',
'plugins',
this.getSource(),
this.getModelName()
this.getModelName(),
]);
/**
@ -179,12 +180,12 @@ export class EditPage extends React.Component {
* @return {Object}
*/
getSchema = () =>
this.getSource() !== 'content-manager'
this.getSource() !== pluginId
? get(this.props.schema, [
'models',
'plugins',
this.getSource(),
this.getModelName()
this.getModelName(),
])
: get(this.props.schema, ['models', this.getModelName()]);
@ -195,18 +196,18 @@ export class EditPage extends React.Component {
const primaryKey = this.getModel().primaryKey;
const {
match: {
params: { id }
}
params: { id },
},
} = this.props;
const title = get(
this.getSchema(),
'editDisplay.displayedField',
primaryKey
primaryKey,
);
const valueToDisplay = get(
this.props.editPage,
['initialRecord', title],
id
id,
);
return isEmpty(valueToDisplay)
@ -220,6 +221,25 @@ export class EditPage extends React.Component {
*/
getSource = () => getQueryParameters(this.props.location.search, 'source');
/**
* Get url base to create edit layout link
* @type {String} url base
*/
getContentManagerBaseUrl = () => {
let url = `/plugins/${pluginId}/ctm-configurations/edit-settings/`;
if (this.getSource() === 'users-permissions') {
url = `${url}plugins/${this.getSource()}/`;
}
return url;
};
/**
* Access url base from injected component to create edit model link
* @type {String} url base
*/
getContentTypeBuilderBaseUrl = () => '/plugins/content-type-builder/models/';
/**
* Initialize component
*/
@ -229,7 +249,7 @@ export class EditPage extends React.Component {
this.isCreating(),
this.getSource(),
this.getModelAttributes(),
this.getDisplayedFields()
this.getDisplayedFields(),
);
if (!this.isCreating()) {
@ -240,7 +260,7 @@ export class EditPage extends React.Component {
// Get all relations made with the upload plugin
const fileRelations = Object.keys(
get(this.getSchema(), 'relations', {})
get(this.getSchema(), 'relations', {}),
).reduce((acc, current) => {
const association = get(this.getSchema(), ['relations', current], {});
@ -250,7 +270,7 @@ export class EditPage extends React.Component {
) {
const relation = {
name: current,
multiple: association.nature === 'manyToManyMorph'
multiple: association.nature === 'manyToManyMorph',
};
acc.push(relation);
@ -265,7 +285,7 @@ export class EditPage extends React.Component {
handleAddRelationItem = ({ key, value }) => {
this.props.addRelationItem({
key,
value
value,
});
};
@ -276,19 +296,19 @@ export class EditPage extends React.Component {
return this.props.changeData({
target: {
name: `record.${target.name}`,
value: defaultValue
}
value: defaultValue,
},
});
}
const errorIndex = findIndex(this.props.editPage.formErrors, [
'name',
target.name
target.name,
]);
const errors = inputValidations(
target.value,
this.getAttributeValidations(target.name),
target.type
target.type,
);
const formErrors = cloneDeep(this.props.editPage.formErrors);
@ -308,7 +328,7 @@ export class EditPage extends React.Component {
// Check if date
if (
['float', 'integer', 'biginteger', 'decimal'].indexOf(
get(this.getSchema(), ['fields', e.target.name, 'type'])
get(this.getSchema(), ['fields', e.target.name, 'type']),
) !== -1
) {
value = toNumber(e.target.value);
@ -316,7 +336,7 @@ export class EditPage extends React.Component {
const target = {
name: `record.${e.target.name}`,
value
value,
};
this.props.changeData({ target });
@ -336,14 +356,14 @@ export class EditPage extends React.Component {
handleGoBack = () => this.props.history.goBack();
handleRedirect = ({ model, id, source = 'content-manager' }) => {
handleRedirect = ({ model, id, source = pluginId }) => {
/* eslint-disable */
switch (model) {
case 'permission':
case 'role':
case 'file':
// Exclude special models which are handled by plugins.
if (source !== 'content-manager') {
if (source !== pluginId) {
break;
}
default:
@ -355,8 +375,8 @@ export class EditPage extends React.Component {
pathname,
search: `?source=${source}&redirectURI=${generateRedirectURI({
model,
search: `?source=${source}`
})}`
search: `?source=${source}`,
})}`,
});
}
/* eslint-enable */
@ -366,7 +386,7 @@ export class EditPage extends React.Component {
e.preventDefault();
const formErrors = checkFormValidity(
this.generateFormFromRecord(),
this.props.editPage.formValidations
this.props.editPage.formValidations,
);
if (isEmpty(formErrors)) {
@ -376,24 +396,20 @@ export class EditPage extends React.Component {
this.props.setFormErrors(formErrors);
};
hasDisplayedRelations = () => {
return this.getDisplayedRelations().length > 0;
};
hasDisplayedDevSection = () => {
return process.env.NODE_ENV === 'development';
};
hasDisplayedRightSection = () => {
return this.hasDisplayedRelations || this.hasDisplayedDevSection;
};
hasDisplayedFields = () => {
return get(this.getModel(), ['editDisplay', 'fields'], []).length > 0;
};
isCreating = () => this.props.match.params.id === 'create';
/**
* Check environment
* @type {boolean} current env is dev
*/
isDevEnvironment = () => {
return process.env.NODE_ENV === 'development';
};
isRelationComponentNull = () =>
Object.keys(get(this.getSchema(), 'relations', {})).filter(
relation =>
@ -401,7 +417,7 @@ export class EditPage extends React.Component {
(!get(this.getSchema(), ['relations', relation, 'nature'], '')
.toLowerCase()
.includes('morph') ||
!get(this.getSchema(), ['relations', relation, relation]))
!get(this.getSchema(), ['relations', relation, relation])),
).length === 0;
// NOTE: technical debt that needs to be redone
@ -412,25 +428,43 @@ export class EditPage extends React.Component {
return acc;
}, {});
/**
* Render the edit layout link
* @type {NavLink}
*/
layoutLink = () => {
// Retrieve URL
const url = `${this.getContentManagerBaseUrl()}${this.getModelName()}`;
// Link props to display
const message = {
message: {
id: `${pluginId}.containers.Edit.Link.Layout`,
},
icon: 'layout',
};
return <NavLink {...message} url={url} />;
};
pluginHeaderActions = () => [
{
label: 'content-manager.containers.Edit.reset',
label: `${pluginId}.containers.Edit.reset`,
kind: 'secondary',
onClick: this.toggle,
type: 'button',
disabled: this.showLoaders()
disabled: this.showLoaders(),
},
{
kind: 'primary',
label: 'content-manager.containers.Edit.submit',
label: `${pluginId}.containers.Edit.submit`,
onClick: this.handleSubmit,
type: 'submit',
loader: this.props.editPage.showLoader,
style: this.props.editPage.showLoader
? { marginRight: '18px', flexGrow: 2 }
: { flexGrow: 2 },
disabled: this.showLoaders()
}
disabled: this.showLoaders(),
},
];
pluginHeaderSubActions = () => {
@ -443,18 +477,67 @@ export class EditPage extends React.Component {
kind: 'delete',
onClick: this.toggleDelete,
type: 'button',
disabled: this.showLoaders()
}
disabled: this.showLoaders(),
},
];
return subActions;
/* eslint-enable indent */
};
/**
* Retrieve external links from injected components
* @type {Array} List of external links to display
*/
retrieveLinksContainerComponent = () => {
// Should be retrieved from the global props (@soupette)
const { plugins } = this.context;
const appPlugins = plugins.toJS();
const componentToInject = Object.keys(appPlugins).reduce((acc, current) => {
// Retrieve injected compos from plugin
// if compo can be injected in left.links area push the compo in the array
const currentPlugin = appPlugins[current];
const injectedComponents = get(currentPlugin, 'injectedComponents', []);
const compos = injectedComponents
.filter(compo => {
return (
compo.plugin === `${pluginId}.editPage` &&
compo.area === 'right.links'
);
})
.map(compo => {
const Component = compo.component;
return (
<li key={compo.key}>
<Component {...this} {...compo.props} />
</li>
);
});
return [...acc, ...compos];
}, []);
return componentToInject;
};
shouldDisplayedRelations = () => {
return this.getDisplayedRelations().length > 0;
};
/**
* Right section to display if needed
* @type {boolean}
*/
shouldDisplayedRightSection = () => {
return this.shouldDisplayedRelations() || this.isDevEnvironment();
};
showLoaders = () => {
const {
editPage: { isLoading },
schema: { layout }
schema: { layout },
} = this.props;
return (
@ -468,72 +551,38 @@ export class EditPage extends React.Component {
toggleDelete = () =>
this.setState(prevState => ({
showWarningDelete: !prevState.showWarningDelete
showWarningDelete: !prevState.showWarningDelete,
}));
layoutLink = () => {
const url = this.getContentManagerBaseUrl() + this.getModelName();
/**
* Render internal and external links
* @type {Array} List of all links to display
*/
renderNavLinks = () => {
// Add ctm link as list item to external links array to return the entire list
let ctmLink = <li key={`${pluginId}.link`}>{this.layoutLink()}</li>;
return <Link to={url}>Configure the layout</Link>;
};
getContentManagerBaseUrl = () => {
if (this.getModelName() !== 'user') {
return '/plugins/content-manager/ctm-configurations/edit-settings/';
} else {
return '/plugins/content-manager/ctm-configurations/edit-settings/plugins/users-permissions/';
}
};
getContentTypeBuilderBaseUrl = () => {
return '/plugins/content-type-builder/models/';
};
retrieveLinksContainerComponent = () => {
// Should be retrieved from the global props (@soupette)
const { plugins } = this.context;
const appPlugins = plugins.toJS();
const componentToInject = Object.keys(appPlugins).reduce((acc, current) => {
// Retrieve injected compo from plugin
// if compo can be injected in left.links area push the compo in the array
const currentPlugin = appPlugins[current];
const injectedComponents = get(currentPlugin, 'injectedComponents', []);
const compos = injectedComponents
.filter(compo => {
return (
compo.plugin === 'content-manager.editPage' &&
compo.area === 'left.links'
);
})
.map(compo => {
const Component = compo.component;
return <Component {...this} key={compo.key} />;
});
return [...acc, ...compos];
}, []);
return componentToInject;
return [ctmLink, ...this.retrieveLinksContainerComponent()];
};
renderEdit = () => {
const {
editPage,
location: { search }
location: { search },
} = this.props;
const source = getQueryParameters(search, 'source');
const basePath = '/plugins/content-manager/ctm-configurations';
const basePath = `/plugins/${pluginId}/ctm-configurations`;
const pathname =
source !== 'content-manager'
source !== pluginId
? `${basePath}/plugins/${source}/${this.getModelName()}`
: `${basePath}/${this.getModelName()}`;
if (this.showLoaders()) {
return (
<div
className={!this.hasDisplayedRelations() ? 'col-lg-12' : 'col-lg-9'}
className={
!this.shouldDisplayedRelations() ? 'col-lg-12' : 'col-lg-9'
}
>
<div className={styles.main_wrapper}>
<LoadingIndicator />
@ -545,11 +594,13 @@ export class EditPage extends React.Component {
if (!this.hasDisplayedFields()) {
return (
<div
className={!this.hasDisplayedRelations() ? 'col-lg-12' : 'col-lg-9'}
className={
!this.shouldDisplayedRelations() ? 'col-lg-12' : 'col-lg-9'
}
>
<EmptyAttributesBlock
description="content-manager.components.EmptyAttributesBlock.description"
label="content-manager.components.EmptyAttributesBlock.button"
description={`${pluginId}.components.EmptyAttributesBlock.description`}
label={`${pluginId}.components.EmptyAttributesBlock.button`}
onClick={() => this.props.history.push(pathname)}
/>
</div>
@ -558,7 +609,9 @@ export class EditPage extends React.Component {
return (
<div
className={!this.hasDisplayedRightSection() ? 'col-lg-12' : 'col-lg-9'}
className={
!this.shouldDisplayedRightSection() ? 'col-lg-12' : 'col-lg-9'
}
>
<div className={styles.main_wrapper}>
<Edit
@ -599,11 +652,10 @@ export class EditPage extends React.Component {
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'
title: `${pluginId}.popUpWarning.title`,
message: `${pluginId}.popUpWarning.warning.cancelAllSettings`,
cancel: `${pluginId}.popUpWarning.button.cancel`,
confirm: `${pluginId}.popUpWarning.button.confirm`,
}}
popUpWarningType="danger"
onConfirm={this.handleConfirm}
@ -612,20 +664,19 @@ export class EditPage extends React.Component {
isOpen={showWarningDelete}
toggleModal={this.toggleDelete}
content={{
title: 'content-manager.popUpWarning.title',
message:
'content-manager.popUpWarning.bodyMessage.contentType.delete',
cancel: 'content-manager.popUpWarning.button.cancel',
confirm: 'content-manager.popUpWarning.button.confirm'
title: `${pluginId}.popUpWarning.title`,
message: `${pluginId}.popUpWarning.bodyMessage.contentType.delete`,
cancel: `${pluginId}.popUpWarning.button.cancel`,
confirm: `${pluginId}.popUpWarning.button.confirm`,
}}
popUpWarningType="danger"
onConfirm={this.handleConfirm}
/>
<div className="row">
{this.renderEdit()}
{this.hasDisplayedRightSection() && (
{this.shouldDisplayedRightSection() && (
<div className={cn('col-lg-3')}>
{this.hasDisplayedRelations() && (
{this.shouldDisplayedRelations() && (
<div className={styles.sub_wrapper}>
<EditRelations
changeData={this.props.changeData}
@ -644,12 +695,9 @@ export class EditPage extends React.Component {
</div>
)}
{this.hasDisplayedDevSection() && (
{this.isDevEnvironment() && (
<div className={styles.links_wrapper}>
<ul>
<li>{this.layoutLink()}</li>
<li>{this.retrieveLinksContainerComponent()}</li>
</ul>
<ul>{this.renderNavLinks()}</ul>
</div>
)}
</div>
@ -663,11 +711,11 @@ export class EditPage extends React.Component {
}
EditPage.contextTypes = {
plugins: PropTypes.object
plugins: PropTypes.object,
};
EditPage.defaultProps = {
schema: {}
schema: {},
};
EditPage.propTypes = {
@ -688,7 +736,7 @@ EditPage.propTypes = {
schema: PropTypes.object,
setFileRelations: PropTypes.func.isRequired,
setFormErrors: PropTypes.func.isRequired,
submit: PropTypes.func.isRequired
submit: PropTypes.func.isRequired,
};
function mapDispatchToProps(dispatch) {
@ -706,20 +754,20 @@ function mapDispatchToProps(dispatch) {
resetProps,
setFileRelations,
setFormErrors,
submit
submit,
},
dispatch
dispatch,
);
}
const mapStateToProps = createStructuredSelector({
editPage: makeSelectEditPage(),
schema: makeSelectSchema()
schema: makeSelectSchema(),
});
const withConnect = connect(
mapStateToProps,
mapDispatchToProps
mapDispatchToProps,
);
const withReducer = injectReducer({ key: 'editPage', reducer });
@ -728,5 +776,5 @@ const withSaga = injectSaga({ key: 'editPage', saga });
export default compose(
withReducer,
withSaga,
withConnect
withConnect,
)(DragDropContext(HTML5Backend)(EditPage));

View File

@ -19,7 +19,7 @@
.sub_wrapper {
padding: 0 20px 1px;
margin-bottom: 40px;
margin-bottom: 28px;
}
.links_wrapper {
@ -33,13 +33,5 @@
&:first-of-type {
border-color: transparent;
}
a {
text-decoration: none;
width: 100%;
display: block;
p {
font-size: 13px;
}
}
}
}

View File

@ -35,6 +35,8 @@
"containers.Edit.returnList": "Return to list",
"containers.Edit.seeDetails": "Details",
"containers.Edit.submit": "Save",
"containers.Edit.Link.Layout": "Configure the layout",
"containers.Edit.Link.Fields": "Edit the fields",
"containers.Home.introduction": "To edit your entries go to the specific link in the left menu. This plugin doesn't have a proper way to edit settings and it's still under active development.",
"containers.Home.pluginHeaderDescription": "Manage your entries through a powerful and beautiful interface.",
"containers.Home.pluginHeaderTitle": "Content Manager",

View File

@ -35,6 +35,8 @@
"containers.Edit.returnList": "Retourner à la liste",
"containers.Edit.seeDetails": "Détails",
"containers.Edit.submit": "Valider",
"containers.Edit.Link.Layout": "Paramétrer la vue",
"containers.Edit.Link.Fields": "Éditer le modèle",
"containers.Home.introduction": "Pour éditer du contenu, choisissez un type de données dans le menu de gauche.",
"containers.Home.pluginHeaderDescription": "Créer et modifier votre type de contenu",
"containers.Home.pluginHeaderTitle": "Type de contenu",
@ -120,4 +122,3 @@
"success.record.delete": "Supprimé",
"success.record.save": "Sauvegardé"
}

View File

@ -5,10 +5,11 @@ const {
getApisKeys,
getApisUploadRelations,
getEditDisplayAvailableFieldsPath,
getEditDisplayFieldsPath
getEditDisplayFieldsPath,
} = require('./utils/getters');
const splitted = str => str.split('.');
const pickData = (model) => _.pick(model, [
const pickData = model =>
_.pick(model, [
'info',
'connection',
'collectionName',
@ -20,7 +21,7 @@ const pickData = (model) => _.pick(model, [
'options',
'loadedModel',
'primaryKey',
'associations'
'associations',
]);
module.exports = async cb => {
@ -28,7 +29,11 @@ module.exports = async cb => {
const pluginsLayout = Object.keys(strapi.plugins).reduce((acc, current) => {
const models = _.get(strapi.plugins, [current, 'config', 'layout'], {});
Object.keys(models).forEach(model => {
const layout = _.get(strapi.plugins, [current, 'config', 'layout', model], {});
const layout = _.get(
strapi.plugins,
[current, 'config', 'layout', model],
{}
);
acc[model] = layout;
});
@ -69,13 +74,14 @@ module.exports = async cb => {
models: {
plugins: {},
},
layout: {}
layout: {},
};
// Populate the schema object
const buildSchema = (model, name, plugin = false) => {
// Model data
const schemaModel = Object.assign({
const schemaModel = Object.assign(
{
label: _.upperFirst(name),
labelPlural: _.upperFirst(pluralize(name)),
orm: model.orm || 'mongoose',
@ -92,13 +98,22 @@ module.exports = async cb => {
fields: [],
relations: [],
},
}, model);
},
model
);
const fieldsToRemove = [];
// Fields (non relation)
const fields = _.mapValues(_.pickBy(model.attributes, attribute =>
!attribute.model && !attribute.collection
), (value, attribute) => {
const fieldClassName = _.get(tempLayout, [name, 'attributes', attribute, 'className'], '');
const fields = _.mapValues(
_.pickBy(
model.attributes,
attribute => !attribute.model && !attribute.collection
),
(value, attribute) => {
const fieldClassName = _.get(
tempLayout,
[name, 'attributes', attribute, 'className'],
''
);
if (fieldClassName === 'd-none') {
fieldsToRemove.push(attribute);
@ -110,7 +125,8 @@ module.exports = async cb => {
type: value.type || 'string',
disabled: false,
};
});
}
);
// Don't display fields that are hidden by default like the resetPasswordToken for the model user
fieldsToRemove.forEach(field => {
@ -128,7 +144,11 @@ module.exports = async cb => {
const attrType = schemaModel.fields[attr].type;
const sortable = attrType !== 'json' && attrType !== 'array';
return Object.assign(schemaModel.fields[attr], { name: attr, sortable, searchable: sortable });
return Object.assign(schemaModel.fields[attr], {
name: attr,
sortable,
searchable: sortable,
});
})
// Retrieve only the fourth first items
.slice(0, 4);
@ -143,13 +163,24 @@ module.exports = async cb => {
// This object will be used to customise the label and description and so on of an input.
// TODO: maybe add the customBootstrapClass in it;
schemaModel.editDisplay.availableFields = Object.keys(schemaModel.fields).reduce((acc, current) => {
schemaModel.editDisplay.availableFields = Object.keys(
schemaModel.fields
).reduce((acc, current) => {
acc[current] = Object.assign(
_.pick(_.get(schemaModel, ['fields', current], {}), ['label', 'type', 'description', 'name']),
_.pick(_.get(schemaModel, ['fields', current], {}), [
'label',
'type',
'description',
'name',
]),
{
editable: ['updatedAt', 'createdAt', 'updated_at', 'created_at'].indexOf(current) === -1,
editable:
['updatedAt', 'createdAt', 'updated_at', 'created_at'].indexOf(
current
) === -1,
placeholder: '',
});
}
);
return acc;
}, {});
@ -158,12 +189,36 @@ module.exports = async cb => {
// Model relations
schemaModel.relations = model.associations.reduce((acc, current) => {
const label = _.upperFirst(current.alias);
const displayedAttribute = current.plugin ? // Value to modified to custom what's displayed in the react-select
_.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'}) ||
const displayedAttribute = current.plugin // Value to modified to custom what's displayed in the react-select
? _.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] = {
@ -175,15 +230,26 @@ module.exports = async cb => {
return acc;
}, {});
const relationsArray = Object.keys(schemaModel.relations).filter(relation => {
const isUploadRelation = _.get(schemaModel, ['relations', relation, 'plugin'], '') === 'upload';
const isMorphSide = _.get(schemaModel, ['relations', relation, 'nature'], '').toLowerCase().includes('morp') && _.get(schemaModel, ['relations', relation, relation]) !== undefined;
const relationsArray = Object.keys(schemaModel.relations).filter(
relation => {
const isUploadRelation =
_.get(schemaModel, ['relations', relation, 'plugin'], '') ===
'upload';
const isMorphSide =
_.get(schemaModel, ['relations', relation, 'nature'], '')
.toLowerCase()
.includes('morp') &&
_.get(schemaModel, ['relations', relation, relation]) !== undefined;
return !isUploadRelation && !isMorphSide;
});
}
);
const uploadRelations = Object.keys(schemaModel.relations).reduce((acc, current) => {
if (_.get(schemaModel, ['relations', current, 'plugin']) === 'upload') {
const uploadRelations = Object.keys(schemaModel.relations).reduce(
(acc, current) => {
if (
_.get(schemaModel, ['relations', current, 'plugin']) === 'upload'
) {
const model = _.get(schemaModel, ['relations', current]);
acc[current] = {
@ -199,13 +265,20 @@ module.exports = async cb => {
}
return acc;
}, {});
},
{}
);
schemaModel.editDisplay.availableFields = _.merge(schemaModel.editDisplay.availableFields, uploadRelations);
schemaModel.editDisplay.availableFields = _.merge(
schemaModel.editDisplay.availableFields,
uploadRelations
);
schemaModel.editDisplay.relations = relationsArray;
}
schemaModel.editDisplay.fields = Object.keys(schemaModel.editDisplay.availableFields);
schemaModel.editDisplay.fields = Object.keys(
schemaModel.editDisplay.availableFields
);
if (plugin) {
return _.set(schema.models.plugins, `${plugin}.${name}`, schemaModel);
@ -230,7 +303,7 @@ module.exports = async cb => {
const pluginStore = strapi.store({
environment: '',
type: 'plugin',
name: 'content-manager'
name: 'content-manager',
});
try {
@ -263,24 +336,42 @@ module.exports = async cb => {
const schemaApis = getApis(schema.models);
// Array of apis to add
const apisToAdd = schemaApis.filter(api => prevSchemaApis.indexOf(api) === -1).map(splitted);
const apisToAdd = schemaApis
.filter(api => prevSchemaApis.indexOf(api) === -1)
.map(splitted);
// Array of apis to remove
const apisToRemove = prevSchemaApis.filter(api => schemaApis.indexOf(api) === -1).map(splitted);
const apisToRemove = prevSchemaApis
.filter(api => schemaApis.indexOf(api) === -1)
.map(splitted);
// Retrieve the same apis by name
const sameApis = schemaApis.filter(api => prevSchemaApis.indexOf(api) !== -1).map(splitted);
const sameApis = schemaApis
.filter(api => prevSchemaApis.indexOf(api) !== -1)
.map(splitted);
// Retrieve all the field's path of the current unchanged api name
const schemaSameApisKeys = _.flattenDeep(getApisKeys(schema, sameApis));
// Retrieve all the field's path of the previous unchanged api name
const prevSchemaSameApisKeys = _.flattenDeep(getApisKeys(prevSchema, sameApis));
const prevSchemaSameApisKeys = _.flattenDeep(
getApisKeys(prevSchema, sameApis)
);
// Determine for the same api if we need to add some fields
const sameApisAttrToAdd = schemaSameApisKeys.filter(attr => prevSchemaSameApisKeys.indexOf(attr) === -1).map(splitted);
const sameApisAttrToAdd = schemaSameApisKeys
.filter(attr => prevSchemaSameApisKeys.indexOf(attr) === -1)
.map(splitted);
// Special case for the relations
const prevSchemaSameApisUploadRelations = _.flattenDeep(getApisUploadRelations(prevSchema, sameApis));
const schemaSameApisUploadRelations = _.flattenDeep(getApisUploadRelations(schema, sameApis));
const sameApisUploadRelationsToAdd = schemaSameApisUploadRelations.filter(attr => prevSchemaSameApisUploadRelations.indexOf(attr) === -1).map(splitted);
const prevSchemaSameApisUploadRelations = _.flattenDeep(
getApisUploadRelations(prevSchema, sameApis)
);
const schemaSameApisUploadRelations = _.flattenDeep(
getApisUploadRelations(schema, sameApis)
);
const sameApisUploadRelationsToAdd = schemaSameApisUploadRelations
.filter(attr => prevSchemaSameApisUploadRelations.indexOf(attr) === -1)
.map(splitted);
// Determine the fields to remove for the unchanged api name
const sameApisAttrToRemove = prevSchemaSameApisKeys.filter(attr => schemaSameApisKeys.indexOf(attr) === -1).map(splitted);
const sameApisAttrToRemove = prevSchemaSameApisKeys
.filter(attr => schemaSameApisKeys.indexOf(attr) === -1)
.map(splitted);
// Remove api
apisToRemove.map(apiPath => {
@ -295,7 +386,8 @@ module.exports = async cb => {
// Check default sort and change it if needed
_.unset(prevSchema.models, attrPath);
// Retrieve the api path in the schema Object
const apiPath = attrPath.length > 3 ? _.take(attrPath, 3) : _.take(attrPath, 1);
const apiPath =
attrPath.length > 3 ? _.take(attrPath, 3) : _.take(attrPath, 1);
// Retrieve the listDisplay path in the schema Object
const listDisplayPath = apiPath.concat('listDisplay');
const prevListDisplay = _.get(prevSchema.models, listDisplayPath);
@ -306,24 +398,36 @@ module.exports = async cb => {
// If the user has deleted the default sort attribute in the content type builder
// Replace it by new generated one from the current schema
if (_.includes(currentAttr, defaultSort)) {
_.set(prevSchema.models, defaultSortPath, _.get(schema.models, defaultSortPath));
_.set(
prevSchema.models,
defaultSortPath,
_.get(schema.models, defaultSortPath)
);
}
// Update the displayed fields
const updatedListDisplay = prevListDisplay.filter(obj => obj.name !== currentAttr.join());
const updatedListDisplay = prevListDisplay.filter(
obj => obj.name !== currentAttr.join()
);
// Retrieve the model's displayed fields for the `EditPage`
const fieldsPath = getEditDisplayFieldsPath(attrPath);
// Retrieve the previous settings
const prevEditDisplayFields = _.get(prevSchema.models, fieldsPath);
// Update the fields
const updatedEditDisplayFields = prevEditDisplayFields.filter(field => field !== currentAttr.join());
const updatedEditDisplayFields = prevEditDisplayFields.filter(
field => field !== currentAttr.join()
);
// Set the new layout
_.set(prevSchema.models, fieldsPath, updatedEditDisplayFields);
if (updatedListDisplay.length === 0) {
// Update it with the one from the generated schema
_.set(prevSchema.models, listDisplayPath, _.get(schema.models, listDisplayPath, []));
_.set(
prevSchema.models,
listDisplayPath,
_.get(schema.models, listDisplayPath, [])
);
} else {
_.set(prevSchema.models, listDisplayPath, updatedListDisplay);
}
@ -333,7 +437,10 @@ module.exports = async cb => {
// Here we just need to add the data from the current schema Object
apisToAdd.map(apiPath => {
const api = _.get(schema.models, apiPath);
const { search, filters, bulkActions, pageEntries, options } = _.get(prevSchema, 'generalSettings');
const { search, filters, bulkActions, pageEntries, options } = _.get(
prevSchema,
'generalSettings'
);
_.set(api, 'options', options);
_.set(api, 'filters', filters);
@ -364,7 +471,13 @@ module.exports = async cb => {
sameApis.forEach(apiPath => {
// This doesn't keep the prevSettings for the relations, the user will have to reset it.
// We might have to improve this if we want the order of the relations to be kept
['relations', 'loadedModel', 'associations', 'attributes', ['editDisplay', 'relations']]
[
'relations',
'loadedModel',
'associations',
'attributes',
['editDisplay', 'relations'],
]
.map(key => apiPath.concat(key))
.forEach(keyPath => {
const newValue = _.get(schema.models, keyPath);
@ -384,16 +497,23 @@ module.exports = async cb => {
_.set(prevSchema.models, fieldsPath, currentFields);
});
schemaApis.map((model) => {
schemaApis.map(model => {
const isPlugin = model.includes('plugins.');
_.set(prevSchema.models[model], 'info', _.get(!isPlugin ? strapi.models[model] : strapi[model], 'info'));
_.set(prevSchema.models[model], 'options', _.get(!isPlugin ? strapi.models[model] : strapi[model], 'options'));
_.set(
prevSchema.models[model],
'info',
_.get(!isPlugin ? strapi.models[model] : strapi[model], 'info')
);
_.set(
prevSchema.models[model],
'options',
_.get(!isPlugin ? strapi.models[model] : strapi[model], 'options')
);
});
await pluginStore.set({ key: 'schema', value: prevSchema });
} catch (err) {
console.log('error', err);
console.log('error', err); // eslint-disable-line no-console
}
cb();

View File

@ -0,0 +1,30 @@
/**
*
* EditViewLink
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import NavLink from 'components/NavLink';
// Create link from content-type-builder to content-manager
function EditViewLink(props) {
// Retrieve URL from props
let url = `${props.getContentTypeBuilderBaseUrl()}${props.getModelName()}`;
// Add users-permissions to URL for permission, role and user content types
if (props.getSource() === 'users-permissions') {
url = `${url}&source=${props.getSource()}`;
}
return <NavLink {...props} url={url} />;
}
EditViewLink.propTypes = {
getContentTypeBuilderBaseUrl: PropTypes.func.isRequired,
getModelName: PropTypes.func.isRequired,
getSource: PropTypes.func.isRequired,
};
export default EditViewLink;

View File

@ -1,25 +0,0 @@
/**
*
* ContentManagerEditViewLink
*
*/
import React from 'react';
import { Link } from 'react-router-dom';
function ContentManagerEditViewLink(props) {
let url = props.getContentTypeBuilderBaseUrl() + props.getModelName();
if (
props.getModelName() === 'user' ||
props.getModelName() === 'role' ||
props.getModelName() === 'permission'
) {
url = url + '&source=users-permissions';
}
return <Link to={url}>Edit the fields</Link>;
}
ContentManagerEditViewLink.propTypes = {};
export default ContentManagerEditViewLink;

View File

@ -1,10 +1,16 @@
import Link from 'components/ContentManagerEditViewLink';
import Link from './InjectedComponents/ContentManager/EditViewLink';
export default [
{
plugin: 'content-manager.editPage',
area: 'left.links',
area: 'right.links',
component: Link,
key: 'content-type-builder.link'
}
key: 'content-type-builder.link',
props: {
message: {
id: 'content-manager.containers.Edit.Link.Fields',
},
icon: 'fa-cog',
},
},
];