diff --git a/packages/core/content-type-builder/admin/src/components/DataManagerProvider/reducer.js b/packages/core/content-type-builder/admin/src/components/DataManagerProvider/reducer.js index c979444c6f..a16c2fd627 100644 --- a/packages/core/content-type-builder/admin/src/components/DataManagerProvider/reducer.js +++ b/packages/core/content-type-builder/admin/src/components/DataManagerProvider/reducer.js @@ -311,6 +311,10 @@ const reducer = (state = initialState, action) => toSet.private = rest.private; } + if (rest.pluginOptions) { + toSet.pluginOptions = rest.pluginOptions; + } + const currentAttributeIndex = updatedAttributes.findIndex( ({ name }) => name === initialAttribute.name ); @@ -323,6 +327,7 @@ const reducer = (state = initialState, action) => let oppositeAttributeNameToRemove = null; let oppositeAttributeNameToUpdate = null; let oppositeAttributeToCreate = null; + let initialOppositeAttribute = null; const currentUid = get(state, ['modifiedData', ...pathToDataToEdit, 'uid']); const didChangeTargetRelation = initialAttribute.target !== rest.target; @@ -378,6 +383,29 @@ const reducer = (state = initialState, action) => updatedAttributes.splice(indexToRemove, 1); } + // In order to preserve plugin options need to get the initial opposite attribute settings + if (!shouldRemoveOppositeAttributeBecauseOfTargetChange) { + const initialTargetContentType = get(state, [ + 'initialContentTypes', + initialAttribute.target, + ]); + + if (initialTargetContentType) { + const oppositeAttributeIndex = findAttributeIndex( + initialTargetContentType, + initialAttribute.targetAttribute + ); + + initialOppositeAttribute = get(state, [ + 'initialContentTypes', + initialAttribute.target, + 'schema', + 'attributes', + oppositeAttributeIndex, + ]); + } + } + // Create the opposite attribute if ( shouldCreateOppositeAttributeBecauseOfRelationTypeChange || @@ -395,6 +423,10 @@ const reducer = (state = initialState, action) => oppositeAttributeToCreate.private = rest.private; } + if (initialOppositeAttribute && initialOppositeAttribute.pluginOptions) { + oppositeAttributeToCreate.pluginOptions = initialOppositeAttribute.pluginOptions; + } + const indexOfInitialAttribute = updatedAttributes.findIndex( ({ name }) => name === initialAttribute.name ); @@ -424,6 +456,10 @@ const reducer = (state = initialState, action) => oppositeAttributeToCreate.private = rest.private; } + if (initialOppositeAttribute && initialOppositeAttribute.pluginOptions) { + oppositeAttributeToCreate.pluginOptions = initialOppositeAttribute.pluginOptions; + } + if (oppositeAttributeNameToUpdate) { const indexToUpdate = updatedAttributes.findIndex( ({ name }) => name === oppositeAttributeNameToUpdate diff --git a/packages/core/content-type-builder/admin/src/components/DataManagerProvider/tests/reducer_edit_attribute_action.test.js b/packages/core/content-type-builder/admin/src/components/DataManagerProvider/tests/reducer_edit_attribute_action.test.js index 0418153292..b0564f1013 100644 --- a/packages/core/content-type-builder/admin/src/components/DataManagerProvider/tests/reducer_edit_attribute_action.test.js +++ b/packages/core/content-type-builder/admin/src/components/DataManagerProvider/tests/reducer_edit_attribute_action.test.js @@ -1366,5 +1366,851 @@ describe('CTB | components | DataManagerProvider | reducer | EDIT_ATTRIBUTE', () expect(reducer(state, action)).toEqual(expected); }); }); + + describe('Editing a relation and preserve plugin options', () => { + it('Should save pluginOptions if the relation is a one side relation (oneWay, manyWay)', () => { + const contentTypeUID = 'api::category.category'; + const updatedTargetUID = 'api::address.address'; + const contentType = { + uid: contentTypeUID, + schema: { + name: 'address', + description: '', + connection: 'default', + collectionName: '', + attributes: [ + { name: 'postal_code', type: 'string' }, + { + name: 'one_way', + relation: 'oneToOne', + targetAttribute: null, + target: contentTypeUID, + type: 'relation', + }, + { + name: 'category', + relation: 'oneToOne', + target: 'api::category.category', + targetAttribute: null, + type: 'relation', + }, + { + name: 'cover', + type: 'media', + multiple: false, + required: false, + }, + ], + }, + }; + const state = { + ...initialState, + components: {}, + initialComponents: {}, + contentTypes: { [contentTypeUID]: contentType }, + initialContentTypes: { [contentTypeUID]: contentType }, + modifiedData: { + components: {}, + contentType, + }, + }; + + const action = { + type: EDIT_ATTRIBUTE, + attributeToSet: { + relation: 'oneToOne', + targetAttribute: null, + target: updatedTargetUID, + type: 'relation', + name: 'one_way', + pluginOptions: { + myplugin: { + example: 'first', + }, + }, + }, + forTarget: 'contentType', + targetUid: contentTypeUID, + initialAttribute: { + relation: 'oneToOne', + targetAttribute: null, + target: contentTypeUID, + type: 'relation', + name: 'one_way', + }, + shouldAddComponentToData: false, + }; + const expected = { + ...initialState, + components: {}, + initialComponents: {}, + contentTypes: { [contentTypeUID]: contentType }, + initialContentTypes: { [contentTypeUID]: contentType }, + modifiedData: { + components: {}, + contentType: { + ...contentType, + schema: { + ...contentType.schema, + attributes: [ + { name: 'postal_code', type: 'string' }, + { + name: 'one_way', + relation: 'oneToOne', + targetAttribute: null, + target: updatedTargetUID, + type: 'relation', + pluginOptions: { + myplugin: { + example: 'first', + }, + }, + }, + { + name: 'category', + relation: 'oneToOne', + target: 'api::category.category', + targetAttribute: null, + type: 'relation', + }, + { + name: 'cover', + type: 'media', + multiple: false, + required: false, + }, + ], + }, + }, + }, + }; + + expect(reducer(state, action)).toEqual(expected); + }); + + it('Should preserve plugin options on the opposite attribute if the target is a the same content type and the nature is not a one side relation (oneToOne, ...)', () => { + const contentTypeUID = 'api::address.address'; + const contentType = { + uid: contentTypeUID, + schema: { + name: 'address', + description: '', + connection: 'default', + collectionName: '', + attributes: [ + { name: 'postal_code', type: 'string' }, + { + name: 'one_to_many', + relation: 'oneToMany', + targetAttribute: 'many_to_one', + target: contentTypeUID, + type: 'relation', + }, + { + name: 'many_to_one', + relation: 'manyToOne', + targetAttribute: 'one_to_many', + target: contentTypeUID, + type: 'relation', + pluginOptions: { + myplugin: { + example: 'first', + }, + }, + }, + { + name: 'category', + relation: 'oneToOne', + target: 'api::category.category', + targetAttribute: null, + type: 'relation', + }, + { + name: 'cover', + type: 'media', + multiple: false, + required: false, + }, + ], + }, + }; + const state = { + ...initialState, + components: {}, + initialComponents: {}, + contentTypes: { [contentTypeUID]: contentType }, + initialContentTypes: { [contentTypeUID]: contentType }, + modifiedData: { + components: {}, + contentType, + }, + }; + + const action = { + type: EDIT_ATTRIBUTE, + attributeToSet: { + relation: 'oneToMany', + targetAttribute: 'many_to_one', + target: contentTypeUID, + type: 'relation', + name: 'one_to_many', + pluginOptions: { + myplugin: { + example: 'first', + }, + }, + }, + forTarget: 'contentType', + targetUid: contentTypeUID, + initialAttribute: { + relation: 'oneToMany', + targetAttribute: 'many_to_one', + target: contentTypeUID, + type: 'relation', + name: 'one_to_many', + }, + shouldAddComponentToData: false, + }; + + const expected = { + ...initialState, + components: {}, + initialComponents: {}, + contentTypes: { [contentTypeUID]: contentType }, + initialContentTypes: { [contentTypeUID]: contentType }, + modifiedData: { + components: {}, + contentType: { + ...contentType, + schema: { + ...contentType.schema, + attributes: [ + { name: 'postal_code', type: 'string' }, + { + name: 'one_to_many', + relation: 'oneToMany', + targetAttribute: 'many_to_one', + target: contentTypeUID, + type: 'relation', + pluginOptions: { + myplugin: { + example: 'first', + }, + }, + }, + { + name: 'many_to_one', + relation: 'manyToOne', + targetAttribute: 'one_to_many', + target: contentTypeUID, + type: 'relation', + pluginOptions: { + myplugin: { + example: 'first', + }, + }, + }, + { + name: 'category', + relation: 'oneToOne', + target: 'api::category.category', + targetAttribute: null, + type: 'relation', + }, + { + name: 'cover', + type: 'media', + multiple: false, + required: false, + }, + ], + }, + }, + }, + }; + + expect(reducer(state, action)).toEqual(expected); + }); + + it('Should save pluginOptions if the relation is nested inside a component', () => { + const contentTypeUID = 'api::address.address'; + const componentUID = 'default.dish'; + const contentType = { + uid: contentTypeUID, + schema: { + name: 'address', + description: '', + connection: 'default', + collectionName: '', + attributes: [ + { + name: 'dishes', + component: componentUID, + type: 'component', + repeatable: true, + }, + { name: 'dynamiczone', type: 'dynamiczone', components: [componentUID] }, + ], + }, + }; + const component = { + uid: componentUID, + category: 'default', + schema: { + icon: 'book', + name: 'dish', + description: '', + connection: 'default', + collectionName: 'components_dishes', + attributes: [ + { + name: 'name', + type: 'string', + required: true, + default: 'My super dish', + }, + { + name: 'description', + type: 'text', + }, + { + name: 'price', + type: 'float', + }, + { + name: 'category', + relation: 'oneToOne', + target: 'api::category.category', + targetAttribute: null, + type: 'relation', + }, + ], + }, + }; + + const state = { + ...initialState, + components: { [componentUID]: component }, + initialComponents: { [componentUID]: component }, + contentTypes: { [contentTypeUID]: contentType }, + initialContentTypes: { [contentTypeUID]: contentType }, + modifiedData: { + components: { [componentUID]: component }, + contentType, + }, + }; + + const action = { + type: EDIT_ATTRIBUTE, + attributeToSet: { + name: 'category', + relation: 'oneToOne', + target: 'api::category.category', + targetAttribute: null, + type: 'relation', + pluginOptions: { + myplugin: { + example: 'first', + }, + }, + }, + forTarget: 'components', + targetUid: componentUID, + initialAttribute: { + name: 'category', + relation: 'oneToOne', + target: 'api::category.category', + targetAttribute: null, + type: 'relation', + }, + shouldAddComponentToData: false, + }; + + const expected = { + ...initialState, + components: { [componentUID]: component }, + initialComponents: { [componentUID]: component }, + contentTypes: { [contentTypeUID]: contentType }, + initialContentTypes: { [contentTypeUID]: contentType }, + modifiedData: { + components: { + [componentUID]: { + ...component, + schema: { + ...component.schema, + attributes: [ + { + name: 'name', + type: 'string', + required: true, + default: 'My super dish', + }, + { + name: 'description', + type: 'text', + }, + { + name: 'price', + type: 'float', + }, + { + name: 'category', + relation: 'oneToOne', + target: 'api::category.category', + targetAttribute: null, + type: 'relation', + pluginOptions: { + myplugin: { + example: 'first', + }, + }, + }, + ], + }, + }, + }, + contentType, + }, + }; + + expect(reducer(state, action)).toEqual(expected); + }); + + it('Should preserve pluginOptions if the relation is nested inside a component', () => { + const contentTypeUID = 'api::address.address'; + const componentUID = 'default.dish'; + const contentType = { + uid: contentTypeUID, + schema: { + name: 'address', + description: '', + connection: 'default', + collectionName: '', + attributes: [ + { + name: 'dishes', + component: componentUID, + type: 'component', + repeatable: true, + }, + { name: 'dynamiczone', type: 'dynamiczone', components: [componentUID] }, + ], + }, + }; + const component = { + uid: componentUID, + category: 'default', + schema: { + icon: 'book', + name: 'dish', + description: '', + connection: 'default', + collectionName: 'components_dishes', + attributes: [ + { + name: 'name', + type: 'string', + required: true, + default: 'My super dish', + }, + { + name: 'description', + type: 'text', + }, + { + name: 'price', + type: 'float', + }, + { + name: 'category', + relation: 'oneToOne', + target: 'api::category.category', + targetAttribute: null, + type: 'relation', + pluginOptions: { + myplugin: { + example: 'first', + }, + }, + }, + ], + }, + }; + + const state = { + ...initialState, + components: { [componentUID]: component }, + initialComponents: { [componentUID]: component }, + contentTypes: { [contentTypeUID]: contentType }, + initialContentTypes: { [contentTypeUID]: contentType }, + modifiedData: { + components: { [componentUID]: component }, + contentType, + }, + }; + + const action = { + type: EDIT_ATTRIBUTE, + attributeToSet: { + name: 'category-new', + relation: 'oneToOne', + target: 'api::category.category', + targetAttribute: null, + type: 'relation', + pluginOptions: { + myplugin: { + example: 'first', + }, + }, + }, + forTarget: 'components', + targetUid: componentUID, + initialAttribute: { + name: 'category', + relation: 'oneToOne', + target: 'api::category.category', + targetAttribute: null, + type: 'relation', + pluginOptions: { + myplugin: { + example: 'first', + }, + }, + }, + shouldAddComponentToData: false, + }; + + const expected = { + ...initialState, + components: { [componentUID]: component }, + initialComponents: { [componentUID]: component }, + contentTypes: { [contentTypeUID]: contentType }, + initialContentTypes: { [contentTypeUID]: contentType }, + modifiedData: { + components: { + [componentUID]: { + ...component, + schema: { + ...component.schema, + attributes: [ + { + name: 'name', + type: 'string', + required: true, + default: 'My super dish', + }, + { + name: 'description', + type: 'text', + }, + { + name: 'price', + type: 'float', + }, + { + name: 'category-new', + relation: 'oneToOne', + target: 'api::category.category', + targetAttribute: null, + type: 'relation', + pluginOptions: { + myplugin: { + example: 'first', + }, + }, + }, + ], + }, + }, + }, + contentType, + }, + }; + + expect(reducer(state, action)).toEqual(expected); + }); + + it('Should save pluginOptions if the relation is nested inside a dynamic zone', () => { + const contentTypeUID = 'api::address.address'; + const componentUID = 'default.dish'; + const contentType = { + uid: contentTypeUID, + schema: { + name: 'address', + description: '', + connection: 'default', + collectionName: '', + attributes: [{ name: 'dynamiczone', type: 'dynamiczone', components: [componentUID] }], + }, + }; + const component = { + uid: componentUID, + category: 'default', + schema: { + icon: 'book', + name: 'dish', + description: '', + connection: 'default', + collectionName: 'components_dishes', + attributes: [ + { + name: 'name', + type: 'string', + required: true, + default: 'My super dish', + }, + { + name: 'description', + type: 'text', + }, + { + name: 'price', + type: 'float', + }, + { + name: 'category', + relation: 'oneToOne', + target: 'api::category.category', + targetAttribute: null, + type: 'relation', + }, + ], + }, + }; + + const state = { + ...initialState, + components: { [componentUID]: component }, + initialComponents: { [componentUID]: component }, + contentTypes: { [contentTypeUID]: contentType }, + initialContentTypes: { [contentTypeUID]: contentType }, + modifiedData: { + components: { [componentUID]: component }, + contentType, + }, + }; + + const action = { + type: EDIT_ATTRIBUTE, + attributeToSet: { + name: 'category', + relation: 'oneToOne', + target: 'api::category.category', + targetAttribute: null, + type: 'relation', + pluginOptions: { + myplugin: { + example: 'first', + }, + }, + }, + forTarget: 'components', + targetUid: componentUID, + initialAttribute: { + name: 'category', + relation: 'oneToOne', + target: 'api::category.category', + targetAttribute: null, + type: 'relation', + }, + shouldAddComponentToData: false, + }; + + const expected = { + ...initialState, + components: { [componentUID]: component }, + initialComponents: { [componentUID]: component }, + contentTypes: { [contentTypeUID]: contentType }, + initialContentTypes: { [contentTypeUID]: contentType }, + modifiedData: { + components: { + [componentUID]: { + ...component, + schema: { + ...component.schema, + attributes: [ + { + name: 'name', + type: 'string', + required: true, + default: 'My super dish', + }, + { + name: 'description', + type: 'text', + }, + { + name: 'price', + type: 'float', + }, + { + name: 'category', + relation: 'oneToOne', + target: 'api::category.category', + targetAttribute: null, + type: 'relation', + pluginOptions: { + myplugin: { + example: 'first', + }, + }, + }, + ], + }, + }, + }, + contentType, + }, + }; + + expect(reducer(state, action)).toEqual(expected); + }); + + it('Should preserve pluginOptions if the relation is nested inside a dynamic zone', () => { + const contentTypeUID = 'api::address.address'; + const componentUID = 'default.dish'; + const contentType = { + uid: contentTypeUID, + schema: { + name: 'address', + description: '', + connection: 'default', + collectionName: '', + attributes: [{ name: 'dynamiczone', type: 'dynamiczone', components: [componentUID] }], + }, + }; + const component = { + uid: componentUID, + category: 'default', + schema: { + icon: 'book', + name: 'dish', + description: '', + connection: 'default', + collectionName: 'components_dishes', + attributes: [ + { + name: 'name', + type: 'string', + required: true, + default: 'My super dish', + }, + { + name: 'description', + type: 'text', + }, + { + name: 'price', + type: 'float', + }, + { + name: 'category', + relation: 'oneToOne', + target: 'api::category.category', + targetAttribute: null, + type: 'relation', + pluginOptions: { + myplugin: { + example: 'first', + }, + }, + }, + ], + }, + }; + + const state = { + ...initialState, + components: { [componentUID]: component }, + initialComponents: { [componentUID]: component }, + contentTypes: { [contentTypeUID]: contentType }, + initialContentTypes: { [contentTypeUID]: contentType }, + modifiedData: { + components: { [componentUID]: component }, + contentType, + }, + }; + + const action = { + type: EDIT_ATTRIBUTE, + attributeToSet: { + name: 'category-new', + relation: 'oneToOne', + target: 'api::category.category', + targetAttribute: null, + type: 'relation', + pluginOptions: { + myplugin: { + example: 'first', + }, + }, + }, + forTarget: 'components', + targetUid: componentUID, + initialAttribute: { + name: 'category', + relation: 'oneToOne', + target: 'api::category.category', + targetAttribute: null, + type: 'relation', + pluginOptions: { + myplugin: { + example: 'first', + }, + }, + }, + shouldAddComponentToData: false, + }; + + const expected = { + ...initialState, + components: { [componentUID]: component }, + initialComponents: { [componentUID]: component }, + contentTypes: { [contentTypeUID]: contentType }, + initialContentTypes: { [contentTypeUID]: contentType }, + modifiedData: { + components: { + [componentUID]: { + ...component, + schema: { + ...component.schema, + attributes: [ + { + name: 'name', + type: 'string', + required: true, + default: 'My super dish', + }, + { + name: 'description', + type: 'text', + }, + { + name: 'price', + type: 'float', + }, + { + name: 'category-new', + relation: 'oneToOne', + target: 'api::category.category', + targetAttribute: null, + type: 'relation', + pluginOptions: { + myplugin: { + example: 'first', + }, + }, + }, + ], + }, + }, + }, + contentType, + }, + }; + + expect(reducer(state, action)).toEqual(expected); + }); + }); }); }); diff --git a/packages/core/content-type-builder/server/services/schema-builder/content-type-builder.js b/packages/core/content-type-builder/server/services/schema-builder/content-type-builder.js index 667a5a2d4c..eb46b3c0ec 100644 --- a/packages/core/content-type-builder/server/services/schema-builder/content-type-builder.js +++ b/packages/core/content-type-builder/server/services/schema-builder/content-type-builder.js @@ -259,6 +259,7 @@ const generateRelation = ({ key, attribute, uid, targetAttribute = {} }) => { target: uid, autoPopulate: targetAttribute.autoPopulate, private: targetAttribute.private || undefined, + pluginOptions: targetAttribute.pluginOptions || undefined, }; switch (attribute.relation) { diff --git a/packages/utils/babel-plugin-switch-ee-ce/package.json b/packages/utils/babel-plugin-switch-ee-ce/package.json index 3584f360ef..e804ab743e 100644 --- a/packages/utils/babel-plugin-switch-ee-ce/package.json +++ b/packages/utils/babel-plugin-switch-ee-ce/package.json @@ -27,7 +27,7 @@ "@babel/cli": "7.18.10", "@babel/core": "7.18.10", "@babel/generator": "7.18.7", - "@babel/parser": "7.18.10", + "@babel/parser": "7.18.13", "@babel/plugin-syntax-dynamic-import": "7.8.3", "@babel/plugin-transform-modules-commonjs": "7.18.6", "@babel/plugin-transform-runtime": "7.18.10", diff --git a/yarn.lock b/yarn.lock index e88110b794..b4a11c8508 100644 --- a/yarn.lock +++ b/yarn.lock @@ -648,21 +648,16 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@7.18.10", "@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.17.9", "@babel/parser@^7.18.10", "@babel/parser@^7.18.5", "@babel/parser@^7.18.9", "@babel/parser@^7.7.0", "@babel/parser@^7.8.3": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.10.tgz#94b5f8522356e69e8277276adf67ed280c90ecc1" - integrity sha512-TYk3OA0HKL6qNryUayb5UUEhM/rkOQozIBEA5ITXh5DWrSp0TlUQXMyZmnWxG/DizSWBeeQ0Zbc5z8UGaaqoeg== - -"@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.19.0": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.0.tgz#497fcafb1d5b61376959c1c338745ef0577aa02c" - integrity sha512-74bEXKX2h+8rrfQUfsBfuZZHzsEs6Eql4pqy/T4Nn6Y9wNPggQOqD6z6pn5Bl8ZfysKouFZT/UXEH94ummEeQw== - -"@babel/parser@^7.18.13": +"@babel/parser@7.18.13": version "7.18.13" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.13.tgz#5b2dd21cae4a2c5145f1fbd8ca103f9313d3b7e4" integrity sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg== +"@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.14.7", "@babel/parser@^7.17.9", "@babel/parser@^7.18.10", "@babel/parser@^7.18.13", "@babel/parser@^7.18.5", "@babel/parser@^7.18.9", "@babel/parser@^7.19.0", "@babel/parser@^7.7.0", "@babel/parser@^7.8.3": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.0.tgz#497fcafb1d5b61376959c1c338745ef0577aa02c" + integrity sha512-74bEXKX2h+8rrfQUfsBfuZZHzsEs6Eql4pqy/T4Nn6Y9wNPggQOqD6z6pn5Bl8ZfysKouFZT/UXEH94ummEeQw== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2"