mirror of
				https://github.com/strapi/strapi.git
				synced 2025-10-31 01:47:13 +00:00 
			
		
		
		
	Content manager edit layout and fields links list
This commit is contained in:
		
							parent
							
								
									c1ffbbfb57
								
							
						
					
					
						commit
						cbb53ac8d4
					
				| @ -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 | ||||
|   ); | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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
 | ||||
|  | ||||
| @ -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 | 
| @ -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 | 
| @ -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; | ||||
| @ -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'); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -2,23 +2,26 @@ 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) => { | ||||
|   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 => { | ||||
|       strapi.notification.error('content-manager.error.model.fetch'); | ||||
|       reject(e); | ||||
|     }); | ||||
| }); | ||||
| const bootstrap = plugin => | ||||
|   new Promise((resolve, reject) => { | ||||
|     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 => { | ||||
|         strapi.notification.error('content-manager.error.model.fetch'); | ||||
|         reject(e); | ||||
|       }); | ||||
|   }); | ||||
| 
 | ||||
| export default bootstrap; | ||||
|  | ||||
| @ -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)); | ||||
|  | ||||
| @ -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; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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", | ||||
|  | ||||
| @ -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é" | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -5,30 +5,35 @@ const { | ||||
|   getApisKeys, | ||||
|   getApisUploadRelations, | ||||
|   getEditDisplayAvailableFieldsPath, | ||||
|   getEditDisplayFieldsPath | ||||
|   getEditDisplayFieldsPath, | ||||
| } = require('./utils/getters'); | ||||
| const splitted = str => str.split('.'); | ||||
| const pickData = (model) => _.pick(model, [ | ||||
|   'info', | ||||
|   'connection', | ||||
|   'collectionName', | ||||
|   'attributes', | ||||
|   'identity', | ||||
|   'globalId', | ||||
|   'globalName', | ||||
|   'orm', | ||||
|   'options', | ||||
|   'loadedModel', | ||||
|   'primaryKey', | ||||
|   'associations' | ||||
| ]); | ||||
| const pickData = model => | ||||
|   _.pick(model, [ | ||||
|     'info', | ||||
|     'connection', | ||||
|     'collectionName', | ||||
|     'attributes', | ||||
|     'identity', | ||||
|     'globalId', | ||||
|     'globalName', | ||||
|     'orm', | ||||
|     'options', | ||||
|     'loadedModel', | ||||
|     'primaryKey', | ||||
|     'associations', | ||||
|   ]); | ||||
| 
 | ||||
| module.exports = async cb => { | ||||
|   // Retrieve all layout files from the plugins config folder
 | ||||
|   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,48 +74,59 @@ module.exports = async cb => { | ||||
|     models: { | ||||
|       plugins: {}, | ||||
|     }, | ||||
|     layout: {} | ||||
|     layout: {}, | ||||
|   }; | ||||
| 
 | ||||
|   // Populate the schema object
 | ||||
|   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', | ||||
|       options: model.options, | ||||
|       editDisplay: { | ||||
|         availableFields: {}, | ||||
|         displayedField: model.primaryKey, | ||||
|         fields: [], | ||||
|         relations: [], | ||||
|     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', | ||||
|         options: model.options, | ||||
|         editDisplay: { | ||||
|           availableFields: {}, | ||||
|           displayedField: model.primaryKey, | ||||
|           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); | ||||
|         if (fieldClassName === 'd-none') { | ||||
|           fieldsToRemove.push(attribute); | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|           label: _.upperFirst(attribute), | ||||
|           description: '', | ||||
|           type: value.type || 'string', | ||||
|           disabled: false, | ||||
|         }; | ||||
|       } | ||||
| 
 | ||||
|       return { | ||||
|         label: _.upperFirst(attribute), | ||||
|         description: '', | ||||
|         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,13 +189,37 @@ 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'}) || | ||||
|           'id'; | ||||
|         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] = { | ||||
|           ...current, | ||||
| @ -175,37 +230,55 @@ 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 model = _.get(schemaModel, ['relations', current]); | ||||
| 
 | ||||
|           acc[current] = { | ||||
|             description: '', | ||||
|             editable: true, | ||||
|             label: _.upperFirst(current), | ||||
|             multiple: _.has(model, 'collection'), | ||||
|             name: current, | ||||
|             placeholder: '', | ||||
|             type: 'file', | ||||
|             disabled: false, | ||||
|           }; | ||||
|           return !isUploadRelation && !isMorphSide; | ||||
|         } | ||||
|       ); | ||||
| 
 | ||||
|         return acc; | ||||
|       }, {}); | ||||
|       const uploadRelations = Object.keys(schemaModel.relations).reduce( | ||||
|         (acc, current) => { | ||||
|           if ( | ||||
|             _.get(schemaModel, ['relations', current, 'plugin']) === 'upload' | ||||
|           ) { | ||||
|             const model = _.get(schemaModel, ['relations', current]); | ||||
| 
 | ||||
|       schemaModel.editDisplay.availableFields = _.merge(schemaModel.editDisplay.availableFields, uploadRelations); | ||||
|             acc[current] = { | ||||
|               description: '', | ||||
|               editable: true, | ||||
|               label: _.upperFirst(current), | ||||
|               multiple: _.has(model, 'collection'), | ||||
|               name: current, | ||||
|               placeholder: '', | ||||
|               type: 'file', | ||||
|               disabled: false, | ||||
|             }; | ||||
|           } | ||||
| 
 | ||||
|           return acc; | ||||
|         }, | ||||
|         {} | ||||
|       ); | ||||
| 
 | ||||
|       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); | ||||
| @ -378,22 +491,29 @@ module.exports = async cb => { | ||||
|       const attr = _.get(schema.models, attrPath); | ||||
|       _.set(prevSchema.models, attrPath, attr); | ||||
| 
 | ||||
|       const fieldsPath = [..._.take(attrPath, attrPath.length -2), 'fields']; | ||||
|       const fieldsPath = [..._.take(attrPath, attrPath.length - 2), 'fields']; | ||||
|       const currentFields = _.get(prevSchema.models, fieldsPath, []); | ||||
|       currentFields.push(attr.name); | ||||
|       _.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); | ||||
|   } catch (err) { | ||||
|     console.log('error', err); // eslint-disable-line no-console
 | ||||
|   } | ||||
| 
 | ||||
|   cb(); | ||||
|  | ||||
| @ -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; | ||||
| @ -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; | ||||
| @ -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', | ||||
|     }, | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Ky
						Ky