diff --git a/packages/strapi-admin/admin/src/containers/Admin/index.js b/packages/strapi-admin/admin/src/containers/Admin/index.js index dcf4aeb3c5..2a95ac83d3 100644 --- a/packages/strapi-admin/admin/src/containers/Admin/index.js +++ b/packages/strapi-admin/admin/src/containers/Admin/index.js @@ -10,6 +10,7 @@ import { connect } from 'react-redux'; import { createStructuredSelector } from 'reselect'; import { bindActionCreators, compose } from 'redux'; import { Switch, Route } from 'react-router-dom'; +import { injectIntl } from 'react-intl'; import { isEmpty } from 'lodash'; // Components from strapi-helper-plugin import { @@ -145,6 +146,7 @@ export class Admin extends React.Component { currentEnvironment={this.props.global.currentEnvironment} disableGlobalOverlayBlocker={this.props.disableGlobalOverlayBlocker} enableGlobalOverlayBlocker={this.props.enableGlobalOverlayBlocker} + formatMessage={this.props.intl.formatMessage} plugins={this.props.global.plugins} updatePlugin={this.props.updatePlugin} > @@ -212,6 +214,9 @@ Admin.propTypes = { showGlobalAppBlocker: PropTypes.bool, strapiVersion: PropTypes.string, }).isRequired, + intl: PropTypes.shape({ + formatMessage: PropTypes.func, + }), location: PropTypes.object.isRequired, setAppError: PropTypes.func.isRequired, updatePlugin: PropTypes.func.isRequired, @@ -243,6 +248,7 @@ const withReducer = injectReducer({ key: 'admin', reducer }); const withSaga = injectSaga({ key: 'admin', saga }); export default compose( + injectIntl, withReducer, withSaga, withConnect diff --git a/packages/strapi-admin/admin/src/translations/en.json b/packages/strapi-admin/admin/src/translations/en.json index e8f4859b05..75fa6a12c5 100644 --- a/packages/strapi-admin/admin/src/translations/en.json +++ b/packages/strapi-admin/admin/src/translations/en.json @@ -128,6 +128,7 @@ "components.Input.error.validation.minSupMax": "Can't be superior", "components.Input.error.validation.regex": "The value not match the regex.", "components.Input.error.validation.required": "This value is required.", + "components.Input.error.validation.unique": "This value is already used.", "components.InputSelect.option.placeholder": "Choose here", "components.ListRow.empty": "There is no data to be shown.", "components.OverlayBlocker.description": "You're using a feature that needs the server to restart. Please wait until the server is up.", diff --git a/packages/strapi-helper-plugin/lib/src/utils/translatedErrors.js b/packages/strapi-helper-plugin/lib/src/utils/translatedErrors.js index ca091fc633..ccaa42194a 100644 --- a/packages/strapi-helper-plugin/lib/src/utils/translatedErrors.js +++ b/packages/strapi-helper-plugin/lib/src/utils/translatedErrors.js @@ -7,6 +7,7 @@ const errorsTrads = { minLength: 'components.Input.error.validation.minLength', regex: 'components.Input.error.validation.regex', required: 'components.Input.error.validation.required', + unique: 'components.Input.error.validation.unique', }; export default errorsTrads; diff --git a/packages/strapi-plugin-content-type-builder/admin/src/containers/FormModal/index.js b/packages/strapi-plugin-content-type-builder/admin/src/containers/FormModal/index.js index 8d0d691266..78eb74d9db 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/containers/FormModal/index.js +++ b/packages/strapi-plugin-content-type-builder/admin/src/containers/FormModal/index.js @@ -1,18 +1,20 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useReducer, useState } from 'react'; // import PropTypes from 'prop-types'; -import { isEmpty } from 'lodash'; import { ButtonModal, HeaderModal, HeaderModalTitle, Modal, + ModalBody, ModalFooter, ModalForm, getYupInnerErrors, + useGlobalContext, } from 'strapi-helper-plugin'; +import { Inputs } from '@buffetjs/custom'; import { useHistory, useLocation } from 'react-router-dom'; import { FormattedMessage } from 'react-intl'; -import { get, upperFirst } from 'lodash'; +import { get, isEmpty, upperFirst } from 'lodash'; import pluginId from '../../pluginId'; import useQuery from '../../hooks/useQuery'; import useDataManager from '../../hooks/useDataManager'; @@ -20,23 +22,28 @@ import ModalHeader from '../../components/ModalHeader'; import HeaderModalNavContainer from '../../components/HeaderModalNavContainer'; import HeaderNavLink from '../../components/HeaderNavLink'; import forms from './utils/forms'; +import init from './init'; +import reducer, { initialState } from './reducer'; const getTrad = id => `${pluginId}.${id}`; const NAVLINKS = [{ id: 'base' }, { id: 'advanced' }]; const FormModal = () => { - const initialState = { + const initialStateData = { actionType: null, modalType: null, settingType: null, // uid: null, }; - const [state, setState] = useState(initialState); + const [state, setState] = useState(initialStateData); + const [reducerState, dispatch] = useReducer(reducer, initialState, init); const { push } = useHistory(); const { search } = useLocation(); + const { formatMessage } = useGlobalContext(); const isOpen = !isEmpty(search); const query = useQuery(); - const { initialData } = useDataManager(); + const { contentTypes, initialData } = useDataManager(); + const { formErrors, modifiedData } = reducerState.toJS(); useEffect(() => { if (isOpen) { @@ -54,9 +61,6 @@ const FormModal = () => { ? `modalForm.${state.modalType}.header-create` : 'modalForm.header-edit'; const name = get(initialData, ['schema', 'name'], ''); - const onClosed = () => { - setState(initialState); - }; const getNextSearch = nextTab => { const newSearch = Object.keys(state).reduce((acc, current) => { if (current !== 'settingType') { @@ -70,22 +74,43 @@ const FormModal = () => { return newSearch; }; + + const handleChange = ({ target: { name, value } }) => { + dispatch({ + type: 'ON_CHANGE', + keys: name.split('.'), + value, + }); + }; const handleSubmit = async e => { e.preventDefault(); try { - const schema = forms.contentType.schema(['admin', 'series', 'file']); + const schema = forms.contentType.schema(Object.keys(contentTypes)); - await schema.validate({ name: 'admin' }, { abortEarly: false }); + await schema.validate(modifiedData, { abortEarly: false }); } catch (err) { const errors = getYupInnerErrors(err); // TODO console.log({ errors }); + dispatch({ + type: 'SET_ERRORS', + errors, + }); } }; const handleToggle = () => { push({ search: '' }); }; + const onClosed = () => { + setState(initialStateData); + dispatch({ + type: 'RESET_PROPS', + }); + }; + const form = get(forms, [state.modalType, 'form', state.settingType], () => ({ + items: [], + })); return ( @@ -123,7 +148,53 @@ const FormModal = () => {
- {/* {renderForm()} */} + + +
+ {form(modifiedData).items.map((row, index) => { + return ( +
+ {row.map(input => { + const errorId = get( + formErrors, + [...input.name.split('.'), 'id'], + null + ); + + return ( +
+ +
+ ); + })} +
+ ); + })} +
+
+
{ + switch (action.type) { + case 'ON_CHANGE': + return state.updateIn( + ['modifiedData', ...action.keys], + () => action.value + ); + case 'RESET_PROPS': + return initialState; + case 'SET_ERRORS': + return state.update('formErrors', () => fromJS(action.errors)); + default: + return state; + } +}; + +export default reducer; +export { initialState }; diff --git a/packages/strapi-plugin-content-type-builder/admin/src/containers/FormModal/utils/createUid.js b/packages/strapi-plugin-content-type-builder/admin/src/containers/FormModal/utils/createUid.js new file mode 100644 index 0000000000..d842624a6e --- /dev/null +++ b/packages/strapi-plugin-content-type-builder/admin/src/containers/FormModal/utils/createUid.js @@ -0,0 +1,12 @@ +import slugify from '@sindresorhus/slugify'; + +const nameToSlug = name => slugify(name, { separator: '-' }); + +const createUid = name => { + const modelName = nameToSlug(name); + const uid = `application::${modelName}.${modelName}`; + + return uid; +}; + +export { createUid, nameToSlug }; diff --git a/packages/strapi-plugin-content-type-builder/admin/src/containers/FormModal/utils/forms.js b/packages/strapi-plugin-content-type-builder/admin/src/containers/FormModal/utils/forms.js index 778d0a4cd1..a0b98ffa58 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/containers/FormModal/utils/forms.js +++ b/packages/strapi-plugin-content-type-builder/admin/src/containers/FormModal/utils/forms.js @@ -1,5 +1,7 @@ import * as yup from 'yup'; import { translatedErrors as errorsTrads } from 'strapi-helper-plugin'; +import pluginId from '../../../pluginId'; +import { createUid, nameToSlug } from './createUid'; yup.addMethod(yup.mixed, 'defined', function() { return this.test( @@ -10,10 +12,12 @@ yup.addMethod(yup.mixed, 'defined', function() { }); yup.addMethod(yup.string, 'unique', function(message, allReadyTakenValues) { - console.log({ allReadyTakenValues }); return this.test('unique', message, function(string) { - console.log({ string }); - return !allReadyTakenValues.includes(string); + if (!string) { + return false; + } + + return !allReadyTakenValues.includes(createUid(string)); }); }); @@ -23,21 +27,75 @@ const forms = { return yup.object().shape({ name: yup .string() - .required() - .unique('duplicate key', allReadyTakenValues), + .unique(errorsTrads.unique, allReadyTakenValues) + .required(errorsTrads.required), + collectionName: yup.string(), }); }, form: { - base: { - name: 'name', - type: 'string', - validations: { - required: true, - }, + base(data = {}) { + return { + items: [ + [ + { + autoFocus: true, + name: 'name', + type: 'text', + label: { + id: `${pluginId}.contentType.displayName.label`, + }, + validations: { + required: true, + }, + }, + { + description: { + id: `${pluginId}.contentType.UID.description`, + }, + label: 'UID', + name: 'uid', + type: 'text', + readOnly: true, + disabled: true, + value: data.name ? nameToSlug(data.name) : '', + }, + ], + // Maybe for later + // [ + // { + // name: 'repeatable', + // type: 'customBooleanContentType', + // value: true, + // title: 'Something', + // description: 'Cool', + // icon: 'multipleFiles', + // }, + // ], + ], + }; + }, + advanced() { + return { + items: [ + [ + { + autoFocus: true, + label: { + id: `${pluginId}.contentType.collectionName.label`, + }, + description: { + id: `${pluginId}.contentType.collectionName.description`, + }, + name: 'collectionName', + type: 'text', + validations: {}, + }, + ], + ], + }; }, }, }, - // }, }; export default forms; diff --git a/packages/strapi-plugin-content-type-builder/admin/src/translations/en.json b/packages/strapi-plugin-content-type-builder/admin/src/translations/en.json index 090cda7154..12fa852971 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/translations/en.json +++ b/packages/strapi-plugin-content-type-builder/admin/src/translations/en.json @@ -220,8 +220,14 @@ "table.relations.title.plural": "including {number} relationships", "table.relations.title.singular": "including {number} relationship", "prompt.content.unsaved": "Are you sure you want to leave this content type? All your modifications will be lost.", + "modalForm.contentType.header-create": "Create a content type", "modalForm.header-edit": "Edit {name}", "modalForm.component.header-create": "Create a component", - "configurations": "configurations" + "configurations": "configurations", + "contentType.displayName.label": "Display name", + "contentType.collectionName.description": "Useful when the name of your Content Type and your table name differ", + "contentType.collectionName.label": "Collection name", + + "contentType.UID.description": "The UID is used to generate the API routes and databases tables/collections" }