mirror of
				https://github.com/strapi/strapi.git
				synced 2025-10-31 09:56:44 +00:00 
			
		
		
		
	Merge pull request #20453 from strapi/v5/expand-nested-components
Expand nested components
This commit is contained in:
		
						commit
						cf77fef5b1
					
				| @ -22,7 +22,6 @@ import { DataManagerContext } from '../../contexts/DataManagerContext'; | ||||
| import { useFormModalNavigation } from '../../hooks/useFormModalNavigation'; | ||||
| import { pluginId } from '../../pluginId'; | ||||
| import { getTrad } from '../../utils/getTrad'; | ||||
| import { makeUnique } from '../../utils/makeUnique'; | ||||
| import { useAutoReloadOverlayBlocker } from '../AutoReloadOverlayBlocker'; | ||||
| import { FormModal } from '../FormModal/FormModal'; | ||||
| 
 | ||||
| @ -420,14 +419,13 @@ const DataManagerProvider = ({ children }: DataManagerProviderProps) => { | ||||
| 
 | ||||
|     const composWithCompos = retrieveComponentsThatHaveComponents(allCompos); | ||||
| 
 | ||||
|     return makeUnique(composWithCompos); | ||||
|     return composWithCompos; | ||||
|   }; | ||||
| 
 | ||||
|   const getAllNestedComponents = () => { | ||||
|     const appNestedCompo = retrieveNestedComponents(components); | ||||
|     const editingDataNestedCompos = retrieveNestedComponents(modifiedData.components || {}); | ||||
| 
 | ||||
|     return makeUnique([...editingDataNestedCompos, ...appNestedCompo]); | ||||
|     return appNestedCompo; | ||||
|   }; | ||||
| 
 | ||||
|   const removeComponentFromDynamicZone = (dzName: string, componentToRemoveIndex: number) => { | ||||
|  | ||||
| @ -1,18 +1,25 @@ | ||||
| import get from 'lodash/get'; | ||||
| 
 | ||||
| import { makeUnique } from '../../../utils/makeUnique'; | ||||
| 
 | ||||
| import type { Component, AttributeType, Components } from '../../../types'; | ||||
| import type { Internal } from '@strapi/types'; | ||||
| 
 | ||||
| type ChildComponent = { | ||||
|   component: Internal.UID.Component; | ||||
| }; | ||||
| 
 | ||||
| export type ComponentWithChildren = { | ||||
|   component: Internal.UID.Component; | ||||
|   childComponents: ChildComponent[]; | ||||
| }; | ||||
| 
 | ||||
| const retrieveComponentsThatHaveComponents = (allComponents: Components) => { | ||||
|   const componentsThatHaveNestedComponents = Object.keys(allComponents).reduce( | ||||
|     (acc: Internal.UID.Component[], current) => { | ||||
|     (acc: ComponentWithChildren[], current) => { | ||||
|       const currentComponent = get(allComponents, [current]); | ||||
|       const uid = currentComponent.uid; | ||||
| 
 | ||||
|       if (doesComponentHaveAComponentField(currentComponent)) { | ||||
|         acc.push(uid); | ||||
|       const compoWithChildren = getComponentWithChildComponents(currentComponent); | ||||
|       if (compoWithChildren.childComponents.length > 0) { | ||||
|         acc.push(compoWithChildren); | ||||
|       } | ||||
| 
 | ||||
|       return acc; | ||||
| @ -20,17 +27,25 @@ const retrieveComponentsThatHaveComponents = (allComponents: Components) => { | ||||
|     [] | ||||
|   ); | ||||
| 
 | ||||
|   return makeUnique(componentsThatHaveNestedComponents); | ||||
|   return componentsThatHaveNestedComponents; | ||||
| }; | ||||
| 
 | ||||
| const doesComponentHaveAComponentField = (component: Component) => { | ||||
| const getComponentWithChildComponents = (component: Component): ComponentWithChildren => { | ||||
|   const attributes = get(component, ['schema', 'attributes'], []) as AttributeType[]; | ||||
| 
 | ||||
|   return attributes.some((attribute) => { | ||||
|   return { | ||||
|     component: component.uid, | ||||
|     childComponents: attributes | ||||
|       .filter((attribute) => { | ||||
|         const { type } = attribute; | ||||
| 
 | ||||
|         return type === 'component'; | ||||
|   }); | ||||
|       }) | ||||
|       .map((attribute) => { | ||||
|         return { | ||||
|           component: attribute.component, | ||||
|         } as ChildComponent; | ||||
|       }), | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export { doesComponentHaveAComponentField, retrieveComponentsThatHaveComponents }; | ||||
| export { getComponentWithChildComponents, retrieveComponentsThatHaveComponents }; | ||||
|  | ||||
| @ -1,24 +1,60 @@ | ||||
| import { makeUnique } from '../../../utils/makeUnique'; | ||||
| import type { Components, AttributeType } from '../../../types'; | ||||
| import type { Internal } from '@strapi/types'; | ||||
| 
 | ||||
| export const retrieveNestedComponents = (appComponents: any) => { | ||||
|   const nestedComponents = Object.keys(appComponents).reduce((acc: any, current) => { | ||||
| export type NestedComponent = { | ||||
|   component: Internal.UID.Component; | ||||
|   uidsOfAllParents?: Internal.UID.Component[]; | ||||
|   parentCompoUid?: Internal.UID.Component; | ||||
| }; | ||||
| 
 | ||||
| export const retrieveNestedComponents = (appComponents: Components): NestedComponent[] => { | ||||
|   const nestedComponents = Object.keys(appComponents).reduce((acc: NestedComponent[], current) => { | ||||
|     const componentAttributes = appComponents?.[current]?.schema?.attributes ?? []; | ||||
|     const currentComponentNestedCompos = getComponentsFromComponent(componentAttributes); | ||||
| 
 | ||||
|     const currentComponentNestedCompos = getComponentsNestedWithinComponent( | ||||
|       componentAttributes, | ||||
|       current as Internal.UID.Component | ||||
|     ); | ||||
|     return [...acc, ...currentComponentNestedCompos]; | ||||
|   }, []); | ||||
| 
 | ||||
|   return makeUnique(nestedComponents); | ||||
|   return mergeComponents(nestedComponents); | ||||
| }; | ||||
| 
 | ||||
| const getComponentsFromComponent = (componentAttributes: any) => { | ||||
|   return componentAttributes.reduce((acc: any, current: any) => { | ||||
| const getComponentsNestedWithinComponent = ( | ||||
|   componentAttributes: AttributeType[], | ||||
|   parentCompoUid: Internal.UID.Component | ||||
| ) => { | ||||
|   return componentAttributes.reduce((acc: NestedComponent[], current) => { | ||||
|     const { type, component } = current; | ||||
| 
 | ||||
|     if (type === 'component') { | ||||
|       acc.push(component); | ||||
|       acc.push({ | ||||
|         component, | ||||
|         parentCompoUid, | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     return acc; | ||||
|   }, []); | ||||
| }; | ||||
| 
 | ||||
| // Merge duplicate components
 | ||||
| const mergeComponents = (originalComponents: NestedComponent[]): NestedComponent[] => { | ||||
|   const componentMap = new Map(); | ||||
|   // Populate the map with component and its parents
 | ||||
|   originalComponents.forEach(({ component, parentCompoUid }) => { | ||||
|     if (!componentMap.has(component)) { | ||||
|       componentMap.set(component, new Set()); | ||||
|     } | ||||
|     componentMap.get(component).add(parentCompoUid); | ||||
|   }); | ||||
| 
 | ||||
|   // Convert the map to the desired array format
 | ||||
|   const transformedComponents: NestedComponent[] = Array.from(componentMap.entries()).map( | ||||
|     ([component, parentCompoUidSet]) => ({ | ||||
|       component, | ||||
|       uidsOfAllParents: Array.from(parentCompoUidSet), | ||||
|     }) | ||||
|   ); | ||||
| 
 | ||||
|   return transformedComponents; | ||||
| }; | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { | ||||
|   doesComponentHaveAComponentField, | ||||
|   getComponentWithChildComponents, | ||||
|   retrieveComponentsThatHaveComponents, | ||||
| } from '../retrieveComponentsThatHaveComponents'; | ||||
| 
 | ||||
| @ -80,18 +80,37 @@ const data: any = { | ||||
| 
 | ||||
| describe('retrieveComponentsThatHaveComponents', () => { | ||||
|   describe('doesComponentHaveAComponentField', () => { | ||||
|     it('Should return true if one of its attributes is a component', () => { | ||||
|       expect(doesComponentHaveAComponentField(data['blog.slider'])).toBe(true); | ||||
|     it('Should return correct child component if component has a component', () => { | ||||
|       expect(getComponentWithChildComponents(data['blog.slider'])).toEqual({ | ||||
|         component: 'blog.slider', | ||||
|         childComponents: [ | ||||
|           { | ||||
|             component: 'default.slide', | ||||
|           }, | ||||
|         ], | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('Should return false if none of its attributes is a component', () => { | ||||
|       expect(doesComponentHaveAComponentField(data['default.dish'])).toBe(false); | ||||
|     it('Should return no child components if component has no child components', () => { | ||||
|       expect(getComponentWithChildComponents(data['default.dish'])).toEqual({ | ||||
|         component: 'default.dish', | ||||
|         childComponents: [], | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('retrievComponentsThatHaveComponents', () => { | ||||
|     it('should return an array with all the components that have nested components', () => { | ||||
|       expect(retrieveComponentsThatHaveComponents(data)).toEqual(['blog.slider']); | ||||
|       expect(retrieveComponentsThatHaveComponents(data)).toEqual([ | ||||
|         { | ||||
|           component: 'blog.slider', | ||||
|           childComponents: [ | ||||
|             { | ||||
|               component: 'default.slide', | ||||
|             }, | ||||
|           ], | ||||
|         }, | ||||
|       ]); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @ -1,8 +1,10 @@ | ||||
| import { retrieveNestedComponents } from '../retrieveNestedComponents'; | ||||
| 
 | ||||
| import type { Components } from '../../../../types'; | ||||
| 
 | ||||
| describe('CONTENT TYPE BUILDER | COMPONENTS | DataManagerProvider | utils | retrieveNestedComponents', () => { | ||||
|   it('should return an array of nested components', () => { | ||||
|     const components = { | ||||
|     const components: Components = { | ||||
|       'default.closingperiod': { | ||||
|         uid: 'default.closingperiod', | ||||
|         category: 'default', | ||||
| @ -49,7 +51,83 @@ describe('CONTENT TYPE BUILDER | COMPONENTS | DataManagerProvider | utils | retr | ||||
|       }, | ||||
|     }; | ||||
| 
 | ||||
|     const expected = ['default.dish']; | ||||
|     const expected = [ | ||||
|       { | ||||
|         component: 'default.dish', | ||||
|         uidsOfAllParents: ['default.closingperiod'], | ||||
|       }, | ||||
|     ]; | ||||
| 
 | ||||
|     expect(retrieveNestedComponents(components)).toEqual(expected); | ||||
|   }); | ||||
| 
 | ||||
|   it('should return both parents', () => { | ||||
|     const components: Components = { | ||||
|       'default.closingperiod': { | ||||
|         uid: 'default.closingperiod', | ||||
|         category: 'default', | ||||
|         apiId: 'closingperiod', | ||||
|         schema: { | ||||
|           icon: 'angry', | ||||
|           name: 'closingperiod', | ||||
|           description: '', | ||||
|           collectionName: 'components_closingperiods', | ||||
|           attributes: [ | ||||
|             { type: 'string', name: 'label' }, | ||||
|             { type: 'date', required: true, name: 'start_date' }, | ||||
|             { type: 'date', required: true, name: 'end_date' }, | ||||
|             { type: 'media', multiple: false, required: false, name: 'media' }, | ||||
|             { component: 'default.dish', type: 'component', name: 'dish' }, | ||||
|           ], | ||||
|         }, | ||||
|       }, | ||||
|       'default.dish': { | ||||
|         uid: 'default.dish', | ||||
|         category: 'default', | ||||
|         apiId: 'dish', | ||||
|         schema: { | ||||
|           icon: 'address-book', | ||||
|           name: 'dish', | ||||
|           description: '', | ||||
|           collectionName: 'components_dishes', | ||||
|           attributes: [ | ||||
|             { type: 'string', required: false, default: 'My super dish', name: 'name' }, | ||||
|             { type: 'text', name: 'description' }, | ||||
|             { type: 'float', name: 'price' }, | ||||
|             { type: 'media', multiple: false, required: false, name: 'picture' }, | ||||
|             { type: 'richtext', name: 'very_long_description' }, | ||||
|             { | ||||
|               type: 'relation', | ||||
|               relation: 'oneToOne', | ||||
|               target: 'api::category.category', | ||||
|               targetAttribute: null, | ||||
|               private: false, | ||||
|               name: 'categories', | ||||
|             }, | ||||
|           ], | ||||
|         }, | ||||
|       }, | ||||
| 
 | ||||
|       'default.openingperiod': { | ||||
|         uid: 'default.openingperiod', | ||||
|         category: 'default', | ||||
|         apiId: 'openingperiod', | ||||
|         schema: { | ||||
|           icon: 'angry', | ||||
|           name: 'openingperiod', | ||||
|           description: '', | ||||
|           collectionName: 'components_openingperiods', | ||||
|           attributes: [{ component: 'default.dish', type: 'component', name: 'dish' }], | ||||
|         }, | ||||
|       }, | ||||
|     }; | ||||
| 
 | ||||
|     const expected = [ | ||||
|       { | ||||
|         component: 'default.dish', | ||||
|         uidsOfAllParents: ['default.closingperiod', 'default.openingperiod'], | ||||
|       }, | ||||
|     ]; | ||||
| 
 | ||||
|     expect(retrieveNestedComponents(components)).toEqual(expected); | ||||
|   }); | ||||
|  | ||||
| @ -1,10 +1,14 @@ | ||||
| import { MAX_COMPONENT_DEPTH } from '../../../constants'; | ||||
| import { getComponentDepth } from '../../../utils/getMaxDepth'; | ||||
| 
 | ||||
| import type { IconByType } from '../../AttributeIcon'; | ||||
| import type { NestedComponent } from '../../DataManagerProvider/utils/retrieveNestedComponents'; | ||||
| import type { Internal } from '@strapi/types'; | ||||
| 
 | ||||
| export const getAttributesToDisplay = ( | ||||
|   dataTarget = '', | ||||
|   targetUid: Internal.UID.Schema, | ||||
|   nestedComponents: Array<Internal.UID.Schema> | ||||
|   nestedComponents: Array<NestedComponent> | ||||
| ): IconByType[][] => { | ||||
|   const defaultAttributes: IconByType[] = [ | ||||
|     'text', | ||||
| @ -22,9 +26,6 @@ export const getAttributesToDisplay = ( | ||||
|   ]; | ||||
| 
 | ||||
|   const isPickingAttributeForAContentType = dataTarget === 'contentType'; | ||||
|   const isNestedInAnotherComponent = nestedComponents.includes(targetUid); | ||||
|   const canAddComponentInAnotherComponent = | ||||
|     !isPickingAttributeForAContentType && !isNestedInAnotherComponent; | ||||
| 
 | ||||
|   if (isPickingAttributeForAContentType) { | ||||
|     return [ | ||||
| @ -34,9 +35,16 @@ export const getAttributesToDisplay = ( | ||||
|     ]; | ||||
|   } | ||||
| 
 | ||||
|   // this will only run when adding attributes to components
 | ||||
|   if (dataTarget) { | ||||
|     const componentDepth = getComponentDepth(targetUid, nestedComponents); | ||||
|     const isNestedInAnotherComponent = componentDepth >= MAX_COMPONENT_DEPTH; | ||||
|     const canAddComponentInAnotherComponent = | ||||
|       !isPickingAttributeForAContentType && !isNestedInAnotherComponent; | ||||
|     if (canAddComponentInAnotherComponent) { | ||||
|       return [defaultAttributes, ['component']]; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return [defaultAttributes]; | ||||
| }; | ||||
|  | ||||
| @ -1,8 +1,11 @@ | ||||
| import { SingleSelectOption, SingleSelect, Field } from '@strapi/design-system'; | ||||
| import { useIntl } from 'react-intl'; | ||||
| 
 | ||||
| import { MAX_COMPONENT_DEPTH } from '../constants'; | ||||
| import { useDataManager } from '../hooks/useDataManager'; | ||||
| import { getChildrenMaxDepth, getComponentDepth } from '../utils/getMaxDepth'; | ||||
| 
 | ||||
| import type { Internal } from '@strapi/types'; | ||||
| interface Option { | ||||
|   uid: string; | ||||
|   label: string; | ||||
| @ -22,7 +25,7 @@ interface SelectComponentProps { | ||||
|   isCreatingComponentWhileAddingAField: boolean; | ||||
|   name: string; | ||||
|   onChange: (value: any) => void; | ||||
|   targetUid: string; | ||||
|   targetUid: Internal.UID.Schema; | ||||
|   value: string; | ||||
|   forTarget: string; | ||||
| } | ||||
| @ -44,8 +47,11 @@ export const SelectComponent = ({ | ||||
|   const errorMessage = error ? formatMessage({ id: error, defaultMessage: error }) : ''; | ||||
|   const label = formatMessage(intlLabel); | ||||
| 
 | ||||
|   const { componentsGroupedByCategory, componentsThatHaveOtherComponentInTheirAttributes } = | ||||
|     useDataManager(); | ||||
|   const { | ||||
|     componentsGroupedByCategory, | ||||
|     componentsThatHaveOtherComponentInTheirAttributes, | ||||
|     nestedComponents, | ||||
|   } = useDataManager(); | ||||
| 
 | ||||
|   const isTargetAComponent = ['component', 'components'].includes(forTarget); | ||||
| 
 | ||||
| @ -66,8 +72,11 @@ export const SelectComponent = ({ | ||||
|   ); | ||||
| 
 | ||||
|   if (isAddingAComponentToAnotherComponent) { | ||||
|     options = options.filter((option) => { | ||||
|       return !componentsThatHaveOtherComponentInTheirAttributes.includes(option.uid); | ||||
|     options = options.filter(({ uid }: any) => { | ||||
|       const maxDepth = getChildrenMaxDepth(uid, componentsThatHaveOtherComponentInTheirAttributes); | ||||
|       const componentDepth = getComponentDepth(targetUid, nestedComponents); | ||||
|       const totalDepth = maxDepth + componentDepth; | ||||
|       return totalDepth <= MAX_COMPONENT_DEPTH; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -5,3 +5,5 @@ export const PERMISSIONS = { | ||||
|   // plugin directly in the browser
 | ||||
|   main: [{ action: 'plugin::content-type-builder.read', subject: null }], | ||||
| }; | ||||
| 
 | ||||
| export const MAX_COMPONENT_DEPTH = 6; | ||||
|  | ||||
| @ -0,0 +1,95 @@ | ||||
| import type { ComponentWithChildren } from '../components/DataManagerProvider/utils/retrieveComponentsThatHaveComponents'; | ||||
| import type { NestedComponent } from '../components/DataManagerProvider/utils/retrieveNestedComponents'; | ||||
| import type { Internal } from '@strapi/types'; | ||||
| 
 | ||||
| const findComponent = <T extends { component: Internal.UID.Component }>( | ||||
|   componentUid: Internal.UID.Schema, | ||||
|   components: Array<T> | ||||
| ) => { | ||||
|   return components.find((c) => c.component === componentUid); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Recursively calculates the maximum depth of nested child components | ||||
|  * for a given component UID. | ||||
|  * | ||||
|  * @param componentUid - The UID of the component to start from. | ||||
|  * @param components - The array of all components with their child components. | ||||
|  * @param currentDepth - The current depth of the recursion. Defaults to 0. | ||||
|  * @returns The maximum depth of the nested child components. | ||||
|  */ | ||||
| export const getChildrenMaxDepth = ( | ||||
|   componentUid: Internal.UID.Component, | ||||
|   components: Array<ComponentWithChildren>, | ||||
|   currentDepth = 0 | ||||
| ) => { | ||||
|   const component = findComponent(componentUid, components); | ||||
| 
 | ||||
|   // If the component doesn't exist or has no child components, return the current depth.
 | ||||
|   if (!component || !component.childComponents || component.childComponents.length === 0) { | ||||
|     return currentDepth; | ||||
|   } | ||||
| 
 | ||||
|   let maxDepth = currentDepth; | ||||
| 
 | ||||
|   // Iterate through each child component to calculate their respective depths.
 | ||||
|   component.childComponents.forEach((child) => { | ||||
|     // Recursively calculate the depth of the child component.
 | ||||
|     const depth = getChildrenMaxDepth(child.component, components, currentDepth + 1); | ||||
|     // Update the maximum depth if the child's depth is greater.
 | ||||
|     if (depth > maxDepth) { | ||||
|       maxDepth = depth; | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   return maxDepth; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Calculates the depth of a component within a nested component tree. | ||||
|  * Depth is defined as the level at which the component is nested. | ||||
|  * For example, a component at Depth 3 is the third nested component. | ||||
|  * | ||||
|  * @param component - The UID of the component to find the depth for. | ||||
|  * @param components - The array of all nested components. | ||||
|  * @returns The depth level of the component within the nested tree. | ||||
|  */ | ||||
| export const getComponentDepth = ( | ||||
|   component: Internal.UID.Schema, | ||||
|   components: Array<NestedComponent> | ||||
| ) => { | ||||
|   /** | ||||
|    * Helper function to recursively calculate the depth of a component. | ||||
|    * | ||||
|    * @param currentComponent - The current component being inspected. | ||||
|    * @param currentLevel - The current level of depth in the tree. | ||||
|    * @returns An array of depth levels found for the component. | ||||
|    */ | ||||
|   const getDepth = (currentComponent: NestedComponent, currentLevel: number): Array<number> => { | ||||
|     const levels = []; | ||||
|     levels.push(currentLevel); | ||||
| 
 | ||||
|     // If the component has no parent UIDs, return the current levels
 | ||||
|     if (!currentComponent.uidsOfAllParents) { | ||||
|       return levels; | ||||
|     } | ||||
| 
 | ||||
|     // Iterate over each parent UID to calculate their respective depths
 | ||||
|     for (const parentUid of currentComponent.uidsOfAllParents) { | ||||
|       const parentComponent = findComponent(parentUid, components); | ||||
|       if (parentComponent) { | ||||
|         levels.push(...getDepth(parentComponent, currentLevel + 1)); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return levels; | ||||
|   }; | ||||
| 
 | ||||
|   const nestedCompo = findComponent(component, components); | ||||
|   // return depth 0 if component is not nested
 | ||||
|   if (!nestedCompo) { | ||||
|     return 0; | ||||
|   } | ||||
|   const compoDepth = Math.max(...getDepth(nestedCompo, 1)); | ||||
|   return compoDepth; | ||||
| }; | ||||
| @ -0,0 +1,143 @@ | ||||
| import { getChildrenMaxDepth, getComponentDepth } from '../getMaxDepth'; | ||||
| 
 | ||||
| import type { ComponentWithChildren } from '../../components/DataManagerProvider/utils/retrieveComponentsThatHaveComponents'; | ||||
| import type { NestedComponent } from '../../components/DataManagerProvider/utils/retrieveNestedComponents'; | ||||
| 
 | ||||
| const componentsWithChildComponents: Array<ComponentWithChildren> = [ | ||||
|   { | ||||
|     component: 'basic.parent-compo', | ||||
|     childComponents: [ | ||||
|       { | ||||
|         component: 'basic.nested-compo1', | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     component: 'basic.nested-compo5', | ||||
|     childComponents: [ | ||||
|       { | ||||
|         component: 'basic.nested-compo6', | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     component: 'basic.nested-compo4', | ||||
|     childComponents: [ | ||||
|       { | ||||
|         component: 'basic.nested-compo5', | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     component: 'basic.nested-compo3', | ||||
|     childComponents: [ | ||||
|       { | ||||
|         component: 'basic.nested-compo4', | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     component: 'basic.nested-compo2', | ||||
|     childComponents: [ | ||||
|       { | ||||
|         component: 'basic.nested-compo3', | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     component: 'basic.nested-compo1', | ||||
|     childComponents: [ | ||||
|       { | ||||
|         component: 'basic.nested-compo2', | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     component: 'basic.another-parent-compo', | ||||
|     childComponents: [ | ||||
|       { | ||||
|         component: 'basic.nested-compo6', | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     component: 'default.openingtimes', | ||||
|     childComponents: [ | ||||
|       { | ||||
|         component: 'default.dish', | ||||
|       }, | ||||
|       { | ||||
|         component: 'basic.nested-compo3', | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     component: 'default.closingperiod', | ||||
|     childComponents: [ | ||||
|       { | ||||
|         component: 'default.dish', | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const nestedComponents: Array<NestedComponent> = [ | ||||
|   { | ||||
|     component: 'default.dish', | ||||
|     uidsOfAllParents: ['default.openingtimes', 'default.closingperiod'], | ||||
|   }, | ||||
|   { | ||||
|     component: 'basic.nested-compo1', | ||||
|     uidsOfAllParents: ['basic.parent-compo'], | ||||
|   }, | ||||
|   { | ||||
|     component: 'basic.nested-compo6', | ||||
|     uidsOfAllParents: ['basic.nested-compo5', 'basic.another-parent-compo'], | ||||
|   }, | ||||
|   { | ||||
|     component: 'basic.nested-compo5', | ||||
|     uidsOfAllParents: ['basic.nested-compo4'], | ||||
|   }, | ||||
|   { | ||||
|     component: 'basic.nested-compo4', | ||||
|     uidsOfAllParents: ['basic.nested-compo3'], | ||||
|   }, | ||||
|   { | ||||
|     component: 'basic.nested-compo3', | ||||
|     uidsOfAllParents: ['basic.nested-compo2'], | ||||
|   }, | ||||
|   { | ||||
|     component: 'basic.nested-compo2', | ||||
|     uidsOfAllParents: ['basic.nested-compo1'], | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| describe('Component Depth Calculations', () => { | ||||
|   describe('getMaxDepth', () => { | ||||
|     it('A component with no child component should have 0 max depth', () => { | ||||
|       const componentsMaxDepth = getChildrenMaxDepth( | ||||
|         'basic.nested-compo6', | ||||
|         componentsWithChildComponents | ||||
|       ); | ||||
| 
 | ||||
|       expect(componentsMaxDepth).toEqual(0); | ||||
|     }); | ||||
| 
 | ||||
|     it('should accurately give the max depth of components children', () => { | ||||
|       const componentsMaxDepth = getChildrenMaxDepth( | ||||
|         'default.openingtimes', | ||||
|         componentsWithChildComponents | ||||
|       ); | ||||
| 
 | ||||
|       expect(componentsMaxDepth).toEqual(4); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('getComponentDepth', () => { | ||||
|     it('A component depth should reflect its position in the component tree', () => { | ||||
|       expect(getComponentDepth('basic.nested-compo1', nestedComponents)).toEqual(1); | ||||
|       expect(getComponentDepth('basic.nested-compo4', nestedComponents)).toEqual(4); | ||||
|       expect(getComponentDepth('basic.nested-compo6', nestedComponents)).toEqual(6); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @ -2,9 +2,8 @@ import _ from 'lodash'; | ||||
| import { yup } from '@strapi/utils'; | ||||
| 
 | ||||
| import type { TestContext } from 'yup'; | ||||
| import type { Schema, UID, Struct } from '@strapi/types'; | ||||
| import type { Schema, Struct } from '@strapi/types'; | ||||
| 
 | ||||
| import { hasComponent } from '../../utils/attributes'; | ||||
| import { modelTypes, VALID_UID_TARGETS } from '../../services/constants'; | ||||
| import { | ||||
|   validators, | ||||
| @ -47,10 +46,7 @@ export const getTypeValidator = ( | ||||
|   } as any); | ||||
| }; | ||||
| 
 | ||||
| const getTypeShape = ( | ||||
|   attribute: Schema.Attribute.AnyAttribute, | ||||
|   { modelType, attributes }: any = {} | ||||
| ) => { | ||||
| const getTypeShape = (attribute: Schema.Attribute.AnyAttribute, { attributes }: any = {}) => { | ||||
|   switch (attribute.type) { | ||||
|     /** | ||||
|      * complex types | ||||
| @ -219,24 +215,8 @@ const getTypeShape = ( | ||||
|       return { | ||||
|         required: validators.required, | ||||
|         repeatable: yup.boolean(), | ||||
|         component: yup | ||||
|           .string() | ||||
|           .test({ | ||||
|             name: 'Check max component nesting is 1 lvl', | ||||
|             test(compoUID: unknown) { | ||||
|               const targetCompo = strapi.components[compoUID as UID.Component]; | ||||
|               if (!targetCompo) return true; // ignore this error as it will fail beforehand
 | ||||
| 
 | ||||
|               if (modelType === modelTypes.COMPONENT && hasComponent(targetCompo)) { | ||||
|                 return this.createError({ | ||||
|                   path: this.path, | ||||
|                   message: `${targetCompo.modelName} already is a nested component. You cannot have more than one level of nesting inside your components.`, | ||||
|                 }); | ||||
|               } | ||||
|               return true; | ||||
|             }, | ||||
|           }) | ||||
|           .required(), | ||||
|         // TODO: Add correct server validation for nested components
 | ||||
|         component: yup.string().required(), | ||||
|         min: yup.number(), | ||||
|         max: yup.number(), | ||||
|       }; | ||||
|  | ||||
| @ -1,17 +1,9 @@ | ||||
| import _ from 'lodash'; | ||||
| import utils, { errors } from '@strapi/utils'; | ||||
| import type { Schema, Struct } from '@strapi/types'; | ||||
| import type { Schema } from '@strapi/types'; | ||||
| 
 | ||||
| const { ApplicationError } = errors; | ||||
| 
 | ||||
| export const hasComponent = (model: Struct.Schema) => { | ||||
|   const compoKeys = Object.keys(model.attributes || {}).filter((key) => { | ||||
|     return model.attributes[key].type === 'component'; | ||||
|   }); | ||||
| 
 | ||||
|   return compoKeys.length > 0; | ||||
| }; | ||||
| 
 | ||||
| export const isConfigurable = (attribute: Schema.Attribute.AnyAttribute) => | ||||
|   _.get(attribute, 'configurable', true); | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Bassel Kanso
						Bassel Kanso