diff --git a/packages/core/content-type-builder/admin/src/components/DataManagerProvider/index.js b/packages/core/content-type-builder/admin/src/components/DataManagerProvider/index.js index b95d43071e..3503bf4d16 100644 --- a/packages/core/content-type-builder/admin/src/components/DataManagerProvider/index.js +++ b/packages/core/content-type-builder/admin/src/components/DataManagerProvider/index.js @@ -28,6 +28,7 @@ import retrieveComponentsFromSchema from './utils/retrieveComponentsFromSchema'; import retrieveNestedComponents from './utils/retrieveNestedComponents'; import { retrieveComponentsThatHaveComponents } from './utils/retrieveComponentsThatHaveComponents'; import { getComponentsToPost, formatMainDataType, sortContentType } from './utils/cleanData'; +import validateSchema from './utils/validateSchema'; import { ADD_ATTRIBUTE, @@ -440,6 +441,21 @@ const DataManagerProvider = ({ initialData.contentType ); + const isValidSchema = validateSchema(contentType); + + if (!isValidSchema) { + toggleNotification({ + type: 'warning', + message: { + id: getTrad('notification.error.dynamiczone-min.validation'), + defaultMessage: + 'At least one component is required in a dynamic zone to be able to save a content type', + }, + }); + + return; + } + body.contentType = contentType; trackUsage('willSaveContentType'); diff --git a/packages/core/content-type-builder/admin/src/components/DataManagerProvider/utils/validateSchema.js b/packages/core/content-type-builder/admin/src/components/DataManagerProvider/utils/validateSchema.js new file mode 100644 index 0000000000..03dc962619 --- /dev/null +++ b/packages/core/content-type-builder/admin/src/components/DataManagerProvider/utils/validateSchema.js @@ -0,0 +1,11 @@ +const validateSchema = schema => { + const dynamicZoneAttributes = Object.values(schema.attributes).filter( + ({ type }) => type === 'dynamiczone' + ); + + return dynamicZoneAttributes.every( + ({ components }) => Array.isArray(components) && components.length > 0 + ); +}; + +export default validateSchema; diff --git a/packages/core/content-type-builder/admin/src/translations/en.json b/packages/core/content-type-builder/admin/src/translations/en.json index 32b280af61..ead6d9ba0d 100644 --- a/packages/core/content-type-builder/admin/src/translations/en.json +++ b/packages/core/content-type-builder/admin/src/translations/en.json @@ -166,6 +166,7 @@ "modelPage.attribute.relation-polymorphic": "Relation (polymorphic)", "modelPage.attribute.relationWith": "Relation with", "none": "None", + "notification.error.dynamiczone-min.validation": "At least one component is required in a dynamic zone to be able to save a content type", "notification.info.autoreaload-disable": "The autoReload feature is required to use this plugin. Start your server with `strapi develop`", "notification.info.creating.notSaved": "Please save your work before creating a new collection type or component", "plugin.description.long": "Modelize the data structure of your API. Create new fields and relations in just a minute. The files are automatically created and updated in your project.", diff --git a/packages/core/content-type-builder/server/controllers/validation/__tests__/types.test.js b/packages/core/content-type-builder/server/controllers/validation/__tests__/types.test.js index e679483484..4dea1965a1 100644 --- a/packages/core/content-type-builder/server/controllers/validation/__tests__/types.test.js +++ b/packages/core/content-type-builder/server/controllers/validation/__tests__/types.test.js @@ -43,6 +43,42 @@ describe('Type validators', () => { }); }); + describe('Dynamiczone type validator', () => { + test('Components cannot be empty', () => { + const attributes = { + dz: { + type: 'dynamiczone', + components: [], + }, + }; + + const validator = getTypeValidator(attributes.dz, { + types: ['dynamiczone'], + modelType: 'collectionType', + attributes, + }); + + expect(validator.isValidSync(attributes.dz)).toBeFalsy(); + }); + + test('Components must have at least one item', () => { + const attributes = { + dz: { + type: 'dynamiczone', + components: ['compoA', 'compoB'], + }, + }; + + const validator = getTypeValidator(attributes.dz, { + types: ['dynamiczone'], + modelType: 'collectionType', + attributes, + }); + + expect(validator.isValidSync(attributes.dz)).toBeTruthy(); + }); + }); + describe('UID type validator', () => { test('Target field can be null', () => { const attributes = { diff --git a/packages/core/content-type-builder/server/controllers/validation/types.js b/packages/core/content-type-builder/server/controllers/validation/types.js index 038614fe91..735f4ea6e4 100644 --- a/packages/core/content-type-builder/server/controllers/validation/types.js +++ b/packages/core/content-type-builder/server/controllers/validation/types.js @@ -258,7 +258,8 @@ const getTypeShape = (attribute, { modelType, attributes } = {}) => { components: yup .array() .of(yup.string().required()) - .test('isArray', '${path} must be an array', value => Array.isArray(value)), + .test('isArray', '${path} must be an array', value => Array.isArray(value)) + .min(1), min: yup.number(), max: yup.number(), }; diff --git a/packages/plugins/i18n/admin/src/components/CMEditViewInjectedComponents/CMEditViewCopyLocale/index.js b/packages/plugins/i18n/admin/src/components/CMEditViewInjectedComponents/CMEditViewCopyLocale/index.js index 29bc7853ae..fdeb5aa4a8 100644 --- a/packages/plugins/i18n/admin/src/components/CMEditViewInjectedComponents/CMEditViewCopyLocale/index.js +++ b/packages/plugins/i18n/admin/src/components/CMEditViewInjectedComponents/CMEditViewCopyLocale/index.js @@ -45,7 +45,7 @@ const Content = ({ appLocales, currentLocale, localizations, readPermissions }) const toggleNotification = useNotification(); const { formatMessage } = useIntl(); const dispatch = useDispatch(); - const { allLayoutData, slug } = useCMEditViewDataManager(); + const { allLayoutData, initialData, slug } = useCMEditViewDataManager(); const [isLoading, setIsLoading] = useState(false); const [isOpen, setIsOpen] = useState(false); const [value, setValue] = useState(options[0]?.value || ''); @@ -59,12 +59,15 @@ const Content = ({ appLocales, currentLocale, localizations, readPermissions }) const requestURL = `/content-manager/collection-types/${slug}/${value}`; + setIsLoading(true); try { - setIsLoading(true); - const { data: response } = await axiosInstance.get(requestURL); const cleanedData = cleanData(response, allLayoutData, localizations); + ['createdBy', 'updatedBy', 'publishedAt', 'id', 'createdAt'].forEach(key => { + if (!initialData[key]) return; + cleanedData[key] = initialData[key]; + }); dispatch({ type: 'ContentManager/CrudReducer/GET_DATA_SUCCEEDED', data: cleanedData }); diff --git a/packages/plugins/i18n/admin/src/components/CMEditViewInjectedComponents/CMEditViewCopyLocale/utils/cleanData.js b/packages/plugins/i18n/admin/src/components/CMEditViewInjectedComponents/CMEditViewCopyLocale/utils/cleanData.js index 9c1b1602fb..4198e93517 100644 --- a/packages/plugins/i18n/admin/src/components/CMEditViewInjectedComponents/CMEditViewCopyLocale/utils/cleanData.js +++ b/packages/plugins/i18n/admin/src/components/CMEditViewInjectedComponents/CMEditViewCopyLocale/utils/cleanData.js @@ -13,15 +13,7 @@ const cleanData = (data, { contentType, components }, initialLocalizations) => { dataWithoutPasswordsAndRelations.localizations = initialLocalizations; - const fieldsToRemove = [ - 'createdBy', - 'updatedBy', - 'publishedAt', - 'id', - '_id', - 'updatedAt', - 'createdAt', - ]; + const fieldsToRemove = ['createdBy', 'updatedBy', 'publishedAt', 'id', 'updatedAt', 'createdAt']; const cleanedClonedData = contentManagementUtilRemoveFieldsFromData( dataWithoutPasswordsAndRelations, diff --git a/packages/plugins/i18n/server/services/content-types.js b/packages/plugins/i18n/server/services/content-types.js index 05e14f041c..790cb400bb 100644 --- a/packages/plugins/i18n/server/services/content-types.js +++ b/packages/plugins/i18n/server/services/content-types.js @@ -113,9 +113,8 @@ const getNonLocalizedAttributes = model => { }; const removeId = value => { - if (typeof value === 'object' && (has('id', value) || has('_id', value))) { + if (typeof value === 'object' && has('id', value)) { delete value.id; - delete value._id; } }; diff --git a/packages/plugins/i18n/tests/content-manager/list-relation.test.e2e.js b/packages/plugins/i18n/tests/content-manager/list-relation.test.e2e.js index 88d5946b7e..601c58d231 100644 --- a/packages/plugins/i18n/tests/content-manager/list-relation.test.e2e.js +++ b/packages/plugins/i18n/tests/content-manager/list-relation.test.e2e.js @@ -114,7 +114,7 @@ describe('i18n - Relation-list route', () => { }); expect(res.body).toHaveLength(1); - expect(res.body[0]).toStrictEqual(pick(['_id', 'id', 'name'], data.products[1])); + expect(res.body[0]).toStrictEqual(pick(['id', 'name'], data.products[1])); }); test('Can filter on any locale', async () => { @@ -125,6 +125,6 @@ describe('i18n - Relation-list route', () => { }); expect(res.body).toHaveLength(1); - expect(res.body[0]).toStrictEqual(pick(['_id', 'id', 'name'], data.products[0])); + expect(res.body[0]).toStrictEqual(pick(['id', 'name'], data.products[0])); }); });