mirror of
				https://github.com/strapi/strapi.git
				synced 2025-10-31 01:47:13 +00:00 
			
		
		
		
	Merge branch 'develop' into fix/relations-read-rbac
This commit is contained in:
		
						commit
						ec04445427
					
				| @ -59,6 +59,10 @@ import { getDisplayName } from '../../utils/users'; | ||||
| import { getData, getDataSucceeded } from '../ListViewLayoutManager'; | ||||
| 
 | ||||
| import { AdminUsersFilter } from './components/AdminUsersFilter'; | ||||
| import { | ||||
|   AutoCloneFailureModal, | ||||
|   type ProhibitedCloningField, | ||||
| } from './components/AutoCloneFailureModal'; | ||||
| import { BulkActionButtons } from './components/BulkActions/Buttons'; | ||||
| import { Filter } from './components/Filter'; | ||||
| import { Table } from './components/Table'; | ||||
| @ -597,6 +601,11 @@ const ListViewPage = ({ | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   const [clonedEntryId, setClonedEntryId] = React.useState<Entity.ID | null>(null); | ||||
|   const [prohibitedCloningFields, setProhibitedCloningFields] = React.useState< | ||||
|     ProhibitedCloningField[] | ||||
|   >([]); | ||||
| 
 | ||||
|   const handleCloneClick = | ||||
|     (id: Contracts.CollectionTypes.AutoClone.Params['sourceId']) => async () => { | ||||
|       try { | ||||
| @ -613,11 +622,9 @@ const ListViewPage = ({ | ||||
|         } | ||||
|       } catch (err) { | ||||
|         if (err instanceof AxiosError) { | ||||
|           push({ | ||||
|             pathname: `${pathname}/create/clone/${id}`, | ||||
|             state: { from: pathname, error: formatAPIError(err) }, | ||||
|             search: pluginsQueryParams, | ||||
|           }); | ||||
|           const { prohibitedFields } = err.response?.data.error.details; | ||||
|           setClonedEntryId(id); | ||||
|           setProhibitedCloningFields(prohibitedFields); | ||||
|         } | ||||
|       } | ||||
|     }; | ||||
| @ -746,6 +753,12 @@ const ListViewPage = ({ | ||||
|                     ) : null | ||||
|                   } | ||||
|                 /> | ||||
|                 <AutoCloneFailureModal | ||||
|                   entryId={clonedEntryId} | ||||
|                   onClose={() => setClonedEntryId(null)} | ||||
|                   prohibitedFields={prohibitedCloningFields} | ||||
|                   pluginQueryParams={pluginsQueryParams} | ||||
|                 /> | ||||
|                 {/* Content */} | ||||
|                 <Table.Root | ||||
|                   onConfirmDelete={handleConfirmDeleteData} | ||||
|  | ||||
| @ -0,0 +1,147 @@ | ||||
| import * as React from 'react'; | ||||
| 
 | ||||
| import { | ||||
|   Box, | ||||
|   Button, | ||||
|   Flex, | ||||
|   Icon, | ||||
|   ModalBody, | ||||
|   ModalFooter, | ||||
|   ModalHeader, | ||||
|   ModalLayout, | ||||
|   Typography, | ||||
| } from '@strapi/design-system'; | ||||
| import { LinkButton } from '@strapi/design-system/v2'; | ||||
| import { ChevronRight } from '@strapi/icons'; | ||||
| import { useIntl } from 'react-intl'; | ||||
| import { useLocation, NavLink } from 'react-router-dom'; | ||||
| 
 | ||||
| import { getTranslation } from '../../../utils/translations'; | ||||
| 
 | ||||
| import type { Entity } from '@strapi/types'; | ||||
| 
 | ||||
| type Reason = 'relation' | 'unique'; | ||||
| type ProhibitedCloningField = [string[], Reason]; | ||||
| 
 | ||||
| interface AutoCloneFailureModalProps { | ||||
|   onClose: () => void; | ||||
|   entryId: Entity.ID | null; | ||||
|   prohibitedFields: ProhibitedCloningField[]; | ||||
|   pluginQueryParams: string; | ||||
| } | ||||
| 
 | ||||
| const AutoCloneFailureModal = ({ | ||||
|   onClose, | ||||
|   entryId, | ||||
|   prohibitedFields, | ||||
|   pluginQueryParams, | ||||
| }: AutoCloneFailureModalProps) => { | ||||
|   const { formatMessage } = useIntl(); | ||||
|   const { pathname } = useLocation(); | ||||
| 
 | ||||
|   if (!entryId) { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   const editPath = `${pathname}/create/clone/${entryId}?${pluginQueryParams}`; | ||||
| 
 | ||||
|   const getDefaultErrorMessage = (reason: Reason) => { | ||||
|     switch (reason) { | ||||
|       case 'relation': | ||||
|         return 'Duplicating the relation could remove it from the original entry.'; | ||||
|       case 'unique': | ||||
|         return 'Identical values in a unique field are not allowed'; | ||||
|       default: | ||||
|         return reason; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <ModalLayout onClose={onClose} labelledBy="title"> | ||||
|       <ModalHeader> | ||||
|         <Typography variant="omega" fontWeight="bold" as="h2" id="title"> | ||||
|           {formatMessage({ | ||||
|             id: getTranslation('containers.ListPage.autoCloneModal.header'), | ||||
|             defaultMessage: 'Duplicate', | ||||
|           })} | ||||
|         </Typography> | ||||
|       </ModalHeader> | ||||
|       <ModalBody> | ||||
|         <Typography variant="beta"> | ||||
|           {formatMessage({ | ||||
|             id: getTranslation('containers.ListPage.autoCloneModal.title'), | ||||
|             defaultMessage: "This entry can't be duplicated directly.", | ||||
|           })} | ||||
|         </Typography> | ||||
|         <Box marginTop={2}> | ||||
|           <Typography textColor="neutral600"> | ||||
|             {formatMessage({ | ||||
|               id: getTranslation('containers.ListPage.autoCloneModal.description'), | ||||
|               defaultMessage: | ||||
|                 "A new entry will be created with the same content, but you'll have to change the following fields to save it.", | ||||
|             })} | ||||
|           </Typography> | ||||
|         </Box> | ||||
|         <Flex marginTop={6} gap={2} direction="column" alignItems="stretch"> | ||||
|           {prohibitedFields.map(([fieldPath, reason]) => ( | ||||
|             <Flex | ||||
|               direction="column" | ||||
|               gap={2} | ||||
|               alignItems="flex-start" | ||||
|               borderColor="neutral200" | ||||
|               hasRadius | ||||
|               padding={6} | ||||
|               key={fieldPath.join()} | ||||
|             > | ||||
|               <Flex direction="row" as="ol"> | ||||
|                 {fieldPath.map((pathSegment, index) => ( | ||||
|                   <Typography fontWeight="semiBold" as="li" key={index}> | ||||
|                     {pathSegment} | ||||
|                     {index !== fieldPath.length - 1 && ( | ||||
|                       <Icon | ||||
|                         as={ChevronRight} | ||||
|                         color="neutral500" | ||||
|                         height={2} | ||||
|                         width={2} | ||||
|                         marginLeft={2} | ||||
|                         marginRight={2} | ||||
|                       /> | ||||
|                     )} | ||||
|                   </Typography> | ||||
|                 ))} | ||||
|               </Flex> | ||||
|               <Typography as="p" textColor="neutral600"> | ||||
|                 {formatMessage({ | ||||
|                   id: getTranslation(`containers.ListPage.autoCloneModal.error.${reason}`), | ||||
|                   defaultMessage: getDefaultErrorMessage(reason), | ||||
|                 })} | ||||
|               </Typography> | ||||
|             </Flex> | ||||
|           ))} | ||||
|         </Flex> | ||||
|       </ModalBody> | ||||
|       <ModalFooter | ||||
|         startActions={ | ||||
|           <Button onClick={onClose} variant="tertiary"> | ||||
|             {formatMessage({ | ||||
|               id: 'cancel', | ||||
|               defaultMessage: 'Cancel', | ||||
|             })} | ||||
|           </Button> | ||||
|         } | ||||
|         endActions={ | ||||
|           // @ts-expect-error - types are not inferred correctly through the as prop.
 | ||||
|           <LinkButton as={NavLink} to={editPath}> | ||||
|             {formatMessage({ | ||||
|               id: getTranslation('containers.ListPage.autoCloneModal.create'), | ||||
|               defaultMessage: 'Create', | ||||
|             })} | ||||
|           </LinkButton> | ||||
|         } | ||||
|       /> | ||||
|     </ModalLayout> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export { AutoCloneFailureModal }; | ||||
| export type { ProhibitedCloningField }; | ||||
| @ -0,0 +1,85 @@ | ||||
| import * as React from 'react'; | ||||
| 
 | ||||
| import { within } from '@testing-library/react'; | ||||
| import userEvent from '@testing-library/user-event'; | ||||
| import { render as renderRTL, screen } from '@tests/utils'; | ||||
| import { Route } from 'react-router-dom'; | ||||
| 
 | ||||
| import { AutoCloneFailureModal } from '../AutoCloneFailureModal'; | ||||
| 
 | ||||
| import type { Location } from 'history'; | ||||
| 
 | ||||
| const user = userEvent.setup(); | ||||
| 
 | ||||
| let testLocation: Location = null!; | ||||
| 
 | ||||
| const render = (props: React.ComponentProps<typeof AutoCloneFailureModal>) => | ||||
|   renderRTL(<AutoCloneFailureModal {...props} />, { | ||||
|     renderOptions: { | ||||
|       wrapper({ children }) { | ||||
|         return ( | ||||
|           <> | ||||
|             {children} | ||||
|             <Route | ||||
|               path="*" | ||||
|               render={({ location }) => { | ||||
|                 testLocation = location; | ||||
| 
 | ||||
|                 return null; | ||||
|               }} | ||||
|             /> | ||||
|           </> | ||||
|         ); | ||||
|       }, | ||||
|     }, | ||||
|     initialEntries: ['/content-manager/collection-types/api::model.model?plugins[i18n][locale]=en'], | ||||
|   }); | ||||
| 
 | ||||
| describe('AutoCloneFailureModal', () => { | ||||
|   it('renders nothing if there is no entryId', () => { | ||||
|     render({ entryId: null, onClose: jest.fn(), prohibitedFields: [], pluginQueryParams: '' }); | ||||
| 
 | ||||
|     expect(screen.queryByText(/duplicate/i)).not.toBeInTheDocument(); | ||||
|   }); | ||||
| 
 | ||||
|   it('toggles the modal', async () => { | ||||
|     const onClose = jest.fn(); | ||||
|     render({ entryId: 1, onClose, prohibitedFields: [], pluginQueryParams: '' }); | ||||
| 
 | ||||
|     await user.click(screen.getByRole('button', { name: /cancel/i })); | ||||
|     expect(onClose).toHaveBeenCalledTimes(1); | ||||
|   }); | ||||
| 
 | ||||
|   it('shows the fields that prevent duplication', async () => { | ||||
|     render({ | ||||
|       entryId: 1, | ||||
|       onClose: jest.fn(), | ||||
|       prohibitedFields: [ | ||||
|         [['dynZoneAttrName', 'Unique Component', 'componentAttrName', 'text'], 'unique'], | ||||
|         [['oneToOneRelation'], 'relation'], | ||||
|       ], | ||||
|       pluginQueryParams: 'plugins[i18n][locale]=en', | ||||
|     }); | ||||
| 
 | ||||
|     const lists = screen.getAllByRole('list'); | ||||
|     expect(lists).toHaveLength(2); | ||||
|     screen.getByText(/identical values in a unique field are not allowed/i); | ||||
|     screen.getByText(/duplicating the relation could remove it/i); | ||||
| 
 | ||||
|     const uniqueSegments = within(lists[0]).getAllByRole('listitem'); | ||||
|     expect(uniqueSegments).toHaveLength(4); | ||||
|     within(uniqueSegments[1]).getByText('Unique Component'); | ||||
|     within(uniqueSegments[3]).getByText('text'); | ||||
| 
 | ||||
|     const relationSegments = within(lists[1]).getAllByRole('listitem'); | ||||
|     expect(relationSegments).toHaveLength(1); | ||||
|     within(relationSegments[0]).getByText('oneToOneRelation'); | ||||
| 
 | ||||
|     // Links to the edit cloned entry page
 | ||||
|     await user.click(screen.getByRole('link', { name: /create/i })); | ||||
|     expect(testLocation.pathname).toBe( | ||||
|       '/content-manager/collection-types/api::model.model/create/clone/1' | ||||
|     ); | ||||
|     expect(testLocation.search).toBe('?plugins[i18n][locale]=en'); | ||||
|   }); | ||||
| }); | ||||
| @ -779,6 +779,12 @@ | ||||
|   "content-manager.containers.ListPage.selectedEntriesModal.title": "Publish entries", | ||||
|   "content-manager.containers.ListPage.selectedEntriesModal.selectedCount": "<b>{alreadyPublishedCount}</b> {alreadyPublishedCount, plural, =0 {entries} one {entry} other {entries}} already published. <b>{readyToPublishCount}</b> {readyToPublishCount, plural, =0 {entries} one {entry} other {entries}} ready to publish. <b>{withErrorsCount}</b> {withErrorsCount, plural, =0 {entries} one {entry} other {entries}} waiting for action.", | ||||
|   "content-manager.containers.ListPage.selectedEntriesModal.publishedCount": "<b>{publishedCount}</b> {publishedCount, plural, =0 {entries} one {entry} other {entries}} published. <b>{withErrorsCount}</b> {withErrorsCount, plural, =0 {entries} one {entry} other {entries}} waiting for action.", | ||||
|   "content-manager.containers.ListPage.autoCloneModal.header": "Duplicate", | ||||
|   "content-manager.containers.ListPage.autoCloneModal.title": "This entry can't be duplicated directly.", | ||||
|   "content-manager.containers.ListPage.autoCloneModal.description": "A new entry will be created with the same content, but you'll have to change the following fields to save it.", | ||||
|   "content-manager.containers.ListPage.autoCloneModal.create": "Create", | ||||
|   "content-manager.containers.ListPage.autoCloneModal.error.unique": "Identical values in a unique field are not allowed.", | ||||
|   "content-manager.containers.ListPage.autoCloneModal.error.relation": "Duplicating the relation could remove it from the original entry.", | ||||
|   "content-manager.containers.ListSettingsView.modal-form.edit-label": "Edit {fieldName}", | ||||
|   "content-manager.containers.SettingPage.add.field": "Insert another field", | ||||
|   "content-manager.containers.SettingPage.add.relational-field": "Insert another related field", | ||||
|  | ||||
| @ -1,9 +1,7 @@ | ||||
| import { setCreatorFields, pipeAsync, errors } from '@strapi/utils'; | ||||
| import { setCreatorFields, pipeAsync } from '@strapi/utils'; | ||||
| import { getService } from '../utils'; | ||||
| import { validateBulkActionInput } from './validation'; | ||||
| import { hasProhibitedCloningFields, excludeNotCreatableFields } from './utils/clone'; | ||||
| 
 | ||||
| const { ApplicationError } = errors; | ||||
| import { getProhibitedCloningFields, excludeNotCreatableFields } from './utils/clone'; | ||||
| 
 | ||||
| export default { | ||||
|   async find(ctx: any) { | ||||
| @ -192,11 +190,16 @@ export default { | ||||
|   async autoClone(ctx: any) { | ||||
|     const { model } = ctx.params; | ||||
| 
 | ||||
|     // Trying to automatically clone the entity and model has unique or relational fields
 | ||||
|     if (hasProhibitedCloningFields(model)) { | ||||
|       throw new ApplicationError( | ||||
|     // Check if the model has fields that prevent auto cloning
 | ||||
|     const prohibitedFields = getProhibitedCloningFields(model); | ||||
| 
 | ||||
|     if (prohibitedFields.length > 0) { | ||||
|       return ctx.badRequest( | ||||
|         'Entity could not be cloned as it has unique and/or relational fields. ' + | ||||
|           'Please edit those fields manually and save to complete the cloning.' | ||||
|           'Please edit those fields manually and save to complete the cloning.', | ||||
|         { | ||||
|           prohibitedFields, | ||||
|         } | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -1,9 +1,12 @@ | ||||
| import { hasProhibitedCloningFields } from '../clone'; | ||||
| import { getProhibitedCloningFields } from '../clone'; | ||||
| 
 | ||||
| describe('Populate', () => { | ||||
|   const fakeModels = { | ||||
|     simple: { | ||||
|       modelName: 'Fake simple model', | ||||
|       info: { | ||||
|         displayName: 'Simple', | ||||
|       }, | ||||
|       attributes: { | ||||
|         text: { | ||||
|           type: 'string', | ||||
| @ -21,6 +24,9 @@ describe('Populate', () => { | ||||
|     }, | ||||
|     component: { | ||||
|       modelName: 'Fake component model', | ||||
|       info: { | ||||
|         displayName: 'Fake component', | ||||
|       }, | ||||
|       attributes: { | ||||
|         componentAttrName: { | ||||
|           type: 'component', | ||||
| @ -30,6 +36,9 @@ describe('Populate', () => { | ||||
|     }, | ||||
|     componentUnique: { | ||||
|       modelName: 'Fake component model', | ||||
|       info: { | ||||
|         displayName: 'Unique Component', | ||||
|       }, | ||||
|       attributes: { | ||||
|         componentAttrName: { | ||||
|           type: 'component', | ||||
| @ -55,12 +64,51 @@ describe('Populate', () => { | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     relation: { | ||||
|     relations: { | ||||
|       modelName: 'Fake relation oneToMany model', | ||||
|       attributes: { | ||||
|         relationAttrName: { | ||||
|         one_way: { | ||||
|           type: 'relation', | ||||
|           relation: 'oneToOne', | ||||
|           target: 'simple', | ||||
|         }, | ||||
|         one_to_one: { | ||||
|           type: 'relation', | ||||
|           relation: 'oneToOne', | ||||
|           target: 'simple', | ||||
|           private: true, | ||||
|           inversedBy: 'one_to_one_kitchensink', | ||||
|         }, | ||||
|         one_to_many: { | ||||
|           type: 'relation', | ||||
|           relation: 'oneToMany', | ||||
|           target: 'simple', | ||||
|           mappedBy: 'many_to_one_kitchensink', | ||||
|         }, | ||||
|         many_to_one: { | ||||
|           type: 'relation', | ||||
|           relation: 'manyToOne', | ||||
|           target: 'simple', | ||||
|           inversedBy: 'one_to_many_kitchensinks', | ||||
|         }, | ||||
|         many_to_manys: { | ||||
|           type: 'relation', | ||||
|           relation: 'manyToMany', | ||||
|           target: 'simple', | ||||
|           inversedBy: 'many_to_many_kitchensinks', | ||||
|         }, | ||||
|         many_way: { | ||||
|           type: 'relation', | ||||
|           relation: 'oneToMany', | ||||
|           target: 'simple', | ||||
|         }, | ||||
|         morph_to_one: { | ||||
|           type: 'relation', | ||||
|           relation: 'morphToOne', | ||||
|         }, | ||||
|         morph_to_many: { | ||||
|           type: 'relation', | ||||
|           relation: 'morphToMany', | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
| @ -86,48 +134,48 @@ describe('Populate', () => { | ||||
|     }); | ||||
| 
 | ||||
|     test('model without unique fields', () => { | ||||
|       const hasProhibitedFields = hasProhibitedCloningFields('simple'); | ||||
|       expect(hasProhibitedFields).toEqual(false); | ||||
|       const prohibitedFields = getProhibitedCloningFields('simple'); | ||||
|       expect(prohibitedFields).toHaveLength(0); | ||||
|     }); | ||||
| 
 | ||||
|     test('model with unique fields', () => { | ||||
|       const hasProhibitedFields = hasProhibitedCloningFields('simpleUnique'); | ||||
|       expect(hasProhibitedFields).toEqual(true); | ||||
|       const prohibitedFields = getProhibitedCloningFields('simpleUnique'); | ||||
|       expect(prohibitedFields).toEqual([[['text'], 'unique']]); | ||||
|     }); | ||||
| 
 | ||||
|     test('model with component', () => { | ||||
|       const hasProhibitedFields = hasProhibitedCloningFields('component'); | ||||
|       expect(hasProhibitedFields).toEqual(false); | ||||
|       const prohibitedFields = getProhibitedCloningFields('component'); | ||||
|       expect(prohibitedFields).toHaveLength(0); | ||||
|     }); | ||||
| 
 | ||||
|     test('model with component & unique fields', () => { | ||||
|       const hasProhibitedFields = hasProhibitedCloningFields('componentUnique'); | ||||
|       expect(hasProhibitedFields).toEqual(true); | ||||
|     }); | ||||
| 
 | ||||
|     test('model with component & unique fields', () => { | ||||
|       const hasProhibitedFields = hasProhibitedCloningFields('componentUnique'); | ||||
|       expect(hasProhibitedFields).toEqual(true); | ||||
|       const prohibitedFields = getProhibitedCloningFields('componentUnique'); | ||||
|       expect(prohibitedFields).toEqual([[['componentAttrName', 'text'], 'unique']]); | ||||
|     }); | ||||
| 
 | ||||
|     test('model with dynamic zone', () => { | ||||
|       const hasProhibitedFields = hasProhibitedCloningFields('dynZone'); | ||||
|       expect(hasProhibitedFields).toEqual(false); | ||||
|       const prohibitedFields = getProhibitedCloningFields('dynZone'); | ||||
|       expect(prohibitedFields).toHaveLength(0); | ||||
|     }); | ||||
| 
 | ||||
|     test('model with dynamic zone', () => { | ||||
|       const hasProhibitedFields = hasProhibitedCloningFields('dynZoneUnique'); | ||||
|       expect(hasProhibitedFields).toEqual(true); | ||||
|     test('model with unique component in dynamic zone', () => { | ||||
|       const prohibitedFields = getProhibitedCloningFields('dynZoneUnique'); | ||||
|       expect(prohibitedFields).toEqual([ | ||||
|         [['dynZoneAttrName', 'Unique Component', 'componentAttrName', 'text'], 'unique'], | ||||
|       ]); | ||||
|     }); | ||||
| 
 | ||||
|     test('model with relation', () => { | ||||
|       const hasProhibitedFields = hasProhibitedCloningFields('relation'); | ||||
|       expect(hasProhibitedFields).toEqual(true); | ||||
|     test('model with relations', () => { | ||||
|       const prohibitedFields = getProhibitedCloningFields('relations'); | ||||
|       expect(prohibitedFields).toEqual([ | ||||
|         [['one_to_one'], 'relation'], | ||||
|         [['one_to_many'], 'relation'], | ||||
|       ]); | ||||
|     }); | ||||
| 
 | ||||
|     test('model with media', () => { | ||||
|       const hasProhibitedFields = hasProhibitedCloningFields('media'); | ||||
|       expect(hasProhibitedFields).toEqual(false); | ||||
|       const prohibitedFields = getProhibitedCloningFields('media'); | ||||
|       expect(prohibitedFields).toHaveLength(0); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @ -3,36 +3,75 @@ import strapiUtils from '@strapi/utils'; | ||||
| 
 | ||||
| const { isVisibleAttribute } = strapiUtils.contentTypes; | ||||
| 
 | ||||
| function isProhibitedRelation(model: any, attributeName: any) { | ||||
| /** | ||||
|  * Use an array of strings to represent the path to a field, so we can show breadcrumbs in the admin | ||||
|  * We can't use special characters as delimiters, because the path includes display names | ||||
|  * for dynamic zone components, which can contain any character. | ||||
|  */ | ||||
| type ProhibitedCloningField = [string[], 'unique' | 'relation']; | ||||
| 
 | ||||
| function checkRelation(model: any, attributeName: any, path: string[]): ProhibitedCloningField[] { | ||||
|   // we don't care about createdBy, updatedBy, localizations etc.
 | ||||
|   if (!isVisibleAttribute(model, attributeName)) { | ||||
|     return false; | ||||
|     // Return empty array and not null so we can always spread the result
 | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   return true; | ||||
|   /** | ||||
|    * Only one-to-many and one-to-one (when they're reversed, not one-way) are dangerous, | ||||
|    * because the other relations don't "steal" the relation from the entry we're cloning | ||||
|    */ | ||||
|   const { relation, inversedBy, mappedBy } = model.attributes[attributeName]; | ||||
| 
 | ||||
|   if ( | ||||
|     ['oneToOne', 'oneToMany'].includes(relation) && | ||||
|     [mappedBy, inversedBy].some((key) => key != null) | ||||
|   ) { | ||||
|     return [[[...path, attributeName], 'relation']]; | ||||
|   } | ||||
| 
 | ||||
| const hasProhibitedCloningFields = (uid: any): boolean => { | ||||
|   return []; | ||||
| } | ||||
| 
 | ||||
| const getProhibitedCloningFields = ( | ||||
|   uid: any, | ||||
|   pathPrefix: string[] = [] | ||||
| ): ProhibitedCloningField[] => { | ||||
|   const model = strapi.getModel(uid); | ||||
| 
 | ||||
|   return Object.keys(model.attributes).some((attributeName: any) => { | ||||
|   const prohibitedFields = Object.keys(model.attributes).reduce<ProhibitedCloningField[]>( | ||||
|     (acc, attributeName) => { | ||||
|       const attribute: any = model.attributes[attributeName]; | ||||
|       const attributePath = [...pathPrefix, attributeName]; | ||||
| 
 | ||||
|       switch (attribute.type) { | ||||
|         case 'relation': | ||||
|         return isProhibitedRelation(model, attributeName); | ||||
|           return [...acc, ...checkRelation(model, attributeName, pathPrefix)]; | ||||
|         case 'component': | ||||
|         return hasProhibitedCloningFields(attribute.component); | ||||
|           return [...acc, ...getProhibitedCloningFields(attribute.component, attributePath)]; | ||||
|         case 'dynamiczone': | ||||
|         return (attribute.components || []).some((componentUID: any) => | ||||
|           hasProhibitedCloningFields(componentUID) | ||||
|         ); | ||||
|           return [ | ||||
|             ...acc, | ||||
|             ...(attribute.components || []).flatMap((componentUID: any) => | ||||
|               getProhibitedCloningFields(componentUID, [ | ||||
|                 ...attributePath, | ||||
|                 strapi.getModel(componentUID).info.displayName, | ||||
|               ]) | ||||
|             ), | ||||
|           ]; | ||||
|         case 'uid': | ||||
|         return true; | ||||
|           return [...acc, [attributePath, 'unique']]; | ||||
|         default: | ||||
|         return attribute?.unique ?? false; | ||||
|           if (attribute?.unique) { | ||||
|             return [...acc, [attributePath, 'unique']]; | ||||
|           } | ||||
|   }); | ||||
|           return acc; | ||||
|       } | ||||
|     }, | ||||
|     [] | ||||
|   ); | ||||
| 
 | ||||
|   return prohibitedFields; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
| @ -79,4 +118,4 @@ const excludeNotCreatableFields = | ||||
|     }, body); | ||||
|   }; | ||||
| 
 | ||||
| export { hasProhibitedCloningFields, excludeNotCreatableFields }; | ||||
| export { getProhibitedCloningFields, excludeNotCreatableFields }; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Bassel Kanso
						Bassel Kanso