mirror of
https://github.com/strapi/strapi.git
synced 2025-09-06 07:12:26 +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 { useFormModalNavigation } from '../../hooks/useFormModalNavigation';
|
||||||
import { pluginId } from '../../pluginId';
|
import { pluginId } from '../../pluginId';
|
||||||
import { getTrad } from '../../utils/getTrad';
|
import { getTrad } from '../../utils/getTrad';
|
||||||
import { makeUnique } from '../../utils/makeUnique';
|
|
||||||
import { useAutoReloadOverlayBlocker } from '../AutoReloadOverlayBlocker';
|
import { useAutoReloadOverlayBlocker } from '../AutoReloadOverlayBlocker';
|
||||||
import { FormModal } from '../FormModal/FormModal';
|
import { FormModal } from '../FormModal/FormModal';
|
||||||
|
|
||||||
@ -420,14 +419,13 @@ const DataManagerProvider = ({ children }: DataManagerProviderProps) => {
|
|||||||
|
|
||||||
const composWithCompos = retrieveComponentsThatHaveComponents(allCompos);
|
const composWithCompos = retrieveComponentsThatHaveComponents(allCompos);
|
||||||
|
|
||||||
return makeUnique(composWithCompos);
|
return composWithCompos;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAllNestedComponents = () => {
|
const getAllNestedComponents = () => {
|
||||||
const appNestedCompo = retrieveNestedComponents(components);
|
const appNestedCompo = retrieveNestedComponents(components);
|
||||||
const editingDataNestedCompos = retrieveNestedComponents(modifiedData.components || {});
|
|
||||||
|
|
||||||
return makeUnique([...editingDataNestedCompos, ...appNestedCompo]);
|
return appNestedCompo;
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeComponentFromDynamicZone = (dzName: string, componentToRemoveIndex: number) => {
|
const removeComponentFromDynamicZone = (dzName: string, componentToRemoveIndex: number) => {
|
||||||
|
@ -1,18 +1,25 @@
|
|||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
|
|
||||||
import { makeUnique } from '../../../utils/makeUnique';
|
|
||||||
|
|
||||||
import type { Component, AttributeType, Components } from '../../../types';
|
import type { Component, AttributeType, Components } from '../../../types';
|
||||||
import type { Internal } from '@strapi/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 retrieveComponentsThatHaveComponents = (allComponents: Components) => {
|
||||||
const componentsThatHaveNestedComponents = Object.keys(allComponents).reduce(
|
const componentsThatHaveNestedComponents = Object.keys(allComponents).reduce(
|
||||||
(acc: Internal.UID.Component[], current) => {
|
(acc: ComponentWithChildren[], current) => {
|
||||||
const currentComponent = get(allComponents, [current]);
|
const currentComponent = get(allComponents, [current]);
|
||||||
const uid = currentComponent.uid;
|
|
||||||
|
|
||||||
if (doesComponentHaveAComponentField(currentComponent)) {
|
const compoWithChildren = getComponentWithChildComponents(currentComponent);
|
||||||
acc.push(uid);
|
if (compoWithChildren.childComponents.length > 0) {
|
||||||
|
acc.push(compoWithChildren);
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
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[];
|
const attributes = get(component, ['schema', 'attributes'], []) as AttributeType[];
|
||||||
|
return {
|
||||||
return attributes.some((attribute) => {
|
component: component.uid,
|
||||||
|
childComponents: attributes
|
||||||
|
.filter((attribute) => {
|
||||||
const { type } = attribute;
|
const { type } = attribute;
|
||||||
|
|
||||||
return type === 'component';
|
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) => {
|
export type NestedComponent = {
|
||||||
const nestedComponents = Object.keys(appComponents).reduce((acc: any, current) => {
|
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 componentAttributes = appComponents?.[current]?.schema?.attributes ?? [];
|
||||||
const currentComponentNestedCompos = getComponentsFromComponent(componentAttributes);
|
const currentComponentNestedCompos = getComponentsNestedWithinComponent(
|
||||||
|
componentAttributes,
|
||||||
|
current as Internal.UID.Component
|
||||||
|
);
|
||||||
return [...acc, ...currentComponentNestedCompos];
|
return [...acc, ...currentComponentNestedCompos];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return makeUnique(nestedComponents);
|
return mergeComponents(nestedComponents);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getComponentsFromComponent = (componentAttributes: any) => {
|
const getComponentsNestedWithinComponent = (
|
||||||
return componentAttributes.reduce((acc: any, current: any) => {
|
componentAttributes: AttributeType[],
|
||||||
|
parentCompoUid: Internal.UID.Component
|
||||||
|
) => {
|
||||||
|
return componentAttributes.reduce((acc: NestedComponent[], current) => {
|
||||||
const { type, component } = current;
|
const { type, component } = current;
|
||||||
|
|
||||||
if (type === 'component') {
|
if (type === 'component') {
|
||||||
acc.push(component);
|
acc.push({
|
||||||
|
component,
|
||||||
|
parentCompoUid,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
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 {
|
import {
|
||||||
doesComponentHaveAComponentField,
|
getComponentWithChildComponents,
|
||||||
retrieveComponentsThatHaveComponents,
|
retrieveComponentsThatHaveComponents,
|
||||||
} from '../retrieveComponentsThatHaveComponents';
|
} from '../retrieveComponentsThatHaveComponents';
|
||||||
|
|
||||||
@ -80,18 +80,37 @@ const data: any = {
|
|||||||
|
|
||||||
describe('retrieveComponentsThatHaveComponents', () => {
|
describe('retrieveComponentsThatHaveComponents', () => {
|
||||||
describe('doesComponentHaveAComponentField', () => {
|
describe('doesComponentHaveAComponentField', () => {
|
||||||
it('Should return true if one of its attributes is a component', () => {
|
it('Should return correct child component if component has a component', () => {
|
||||||
expect(doesComponentHaveAComponentField(data['blog.slider'])).toBe(true);
|
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', () => {
|
it('Should return no child components if component has no child components', () => {
|
||||||
expect(doesComponentHaveAComponentField(data['default.dish'])).toBe(false);
|
expect(getComponentWithChildComponents(data['default.dish'])).toEqual({
|
||||||
|
component: 'default.dish',
|
||||||
|
childComponents: [],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('retrievComponentsThatHaveComponents', () => {
|
describe('retrievComponentsThatHaveComponents', () => {
|
||||||
it('should return an array with all the components that have nested components', () => {
|
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 { retrieveNestedComponents } from '../retrieveNestedComponents';
|
||||||
|
|
||||||
|
import type { Components } from '../../../../types';
|
||||||
|
|
||||||
describe('CONTENT TYPE BUILDER | COMPONENTS | DataManagerProvider | utils | retrieveNestedComponents', () => {
|
describe('CONTENT TYPE BUILDER | COMPONENTS | DataManagerProvider | utils | retrieveNestedComponents', () => {
|
||||||
it('should return an array of nested components', () => {
|
it('should return an array of nested components', () => {
|
||||||
const components = {
|
const components: Components = {
|
||||||
'default.closingperiod': {
|
'default.closingperiod': {
|
||||||
uid: 'default.closingperiod',
|
uid: 'default.closingperiod',
|
||||||
category: 'default',
|
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);
|
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 { IconByType } from '../../AttributeIcon';
|
||||||
|
import type { NestedComponent } from '../../DataManagerProvider/utils/retrieveNestedComponents';
|
||||||
import type { Internal } from '@strapi/types';
|
import type { Internal } from '@strapi/types';
|
||||||
|
|
||||||
export const getAttributesToDisplay = (
|
export const getAttributesToDisplay = (
|
||||||
dataTarget = '',
|
dataTarget = '',
|
||||||
targetUid: Internal.UID.Schema,
|
targetUid: Internal.UID.Schema,
|
||||||
nestedComponents: Array<Internal.UID.Schema>
|
nestedComponents: Array<NestedComponent>
|
||||||
): IconByType[][] => {
|
): IconByType[][] => {
|
||||||
const defaultAttributes: IconByType[] = [
|
const defaultAttributes: IconByType[] = [
|
||||||
'text',
|
'text',
|
||||||
@ -22,9 +26,6 @@ export const getAttributesToDisplay = (
|
|||||||
];
|
];
|
||||||
|
|
||||||
const isPickingAttributeForAContentType = dataTarget === 'contentType';
|
const isPickingAttributeForAContentType = dataTarget === 'contentType';
|
||||||
const isNestedInAnotherComponent = nestedComponents.includes(targetUid);
|
|
||||||
const canAddComponentInAnotherComponent =
|
|
||||||
!isPickingAttributeForAContentType && !isNestedInAnotherComponent;
|
|
||||||
|
|
||||||
if (isPickingAttributeForAContentType) {
|
if (isPickingAttributeForAContentType) {
|
||||||
return [
|
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) {
|
if (canAddComponentInAnotherComponent) {
|
||||||
return [defaultAttributes, ['component']];
|
return [defaultAttributes, ['component']];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [defaultAttributes];
|
return [defaultAttributes];
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { SingleSelectOption, SingleSelect, Field } from '@strapi/design-system';
|
import { SingleSelectOption, SingleSelect, Field } from '@strapi/design-system';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { MAX_COMPONENT_DEPTH } from '../constants';
|
||||||
import { useDataManager } from '../hooks/useDataManager';
|
import { useDataManager } from '../hooks/useDataManager';
|
||||||
|
import { getChildrenMaxDepth, getComponentDepth } from '../utils/getMaxDepth';
|
||||||
|
|
||||||
|
import type { Internal } from '@strapi/types';
|
||||||
interface Option {
|
interface Option {
|
||||||
uid: string;
|
uid: string;
|
||||||
label: string;
|
label: string;
|
||||||
@ -22,7 +25,7 @@ interface SelectComponentProps {
|
|||||||
isCreatingComponentWhileAddingAField: boolean;
|
isCreatingComponentWhileAddingAField: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
onChange: (value: any) => void;
|
onChange: (value: any) => void;
|
||||||
targetUid: string;
|
targetUid: Internal.UID.Schema;
|
||||||
value: string;
|
value: string;
|
||||||
forTarget: string;
|
forTarget: string;
|
||||||
}
|
}
|
||||||
@ -44,8 +47,11 @@ export const SelectComponent = ({
|
|||||||
const errorMessage = error ? formatMessage({ id: error, defaultMessage: error }) : '';
|
const errorMessage = error ? formatMessage({ id: error, defaultMessage: error }) : '';
|
||||||
const label = formatMessage(intlLabel);
|
const label = formatMessage(intlLabel);
|
||||||
|
|
||||||
const { componentsGroupedByCategory, componentsThatHaveOtherComponentInTheirAttributes } =
|
const {
|
||||||
useDataManager();
|
componentsGroupedByCategory,
|
||||||
|
componentsThatHaveOtherComponentInTheirAttributes,
|
||||||
|
nestedComponents,
|
||||||
|
} = useDataManager();
|
||||||
|
|
||||||
const isTargetAComponent = ['component', 'components'].includes(forTarget);
|
const isTargetAComponent = ['component', 'components'].includes(forTarget);
|
||||||
|
|
||||||
@ -66,8 +72,11 @@ export const SelectComponent = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isAddingAComponentToAnotherComponent) {
|
if (isAddingAComponentToAnotherComponent) {
|
||||||
options = options.filter((option) => {
|
options = options.filter(({ uid }: any) => {
|
||||||
return !componentsThatHaveOtherComponentInTheirAttributes.includes(option.uid);
|
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
|
// plugin directly in the browser
|
||||||
main: [{ action: 'plugin::content-type-builder.read', subject: null }],
|
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 { yup } from '@strapi/utils';
|
||||||
|
|
||||||
import type { TestContext } from 'yup';
|
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 { modelTypes, VALID_UID_TARGETS } from '../../services/constants';
|
||||||
import {
|
import {
|
||||||
validators,
|
validators,
|
||||||
@ -47,10 +46,7 @@ export const getTypeValidator = (
|
|||||||
} as any);
|
} as any);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTypeShape = (
|
const getTypeShape = (attribute: Schema.Attribute.AnyAttribute, { attributes }: any = {}) => {
|
||||||
attribute: Schema.Attribute.AnyAttribute,
|
|
||||||
{ modelType, attributes }: any = {}
|
|
||||||
) => {
|
|
||||||
switch (attribute.type) {
|
switch (attribute.type) {
|
||||||
/**
|
/**
|
||||||
* complex types
|
* complex types
|
||||||
@ -219,24 +215,8 @@ const getTypeShape = (
|
|||||||
return {
|
return {
|
||||||
required: validators.required,
|
required: validators.required,
|
||||||
repeatable: yup.boolean(),
|
repeatable: yup.boolean(),
|
||||||
component: yup
|
// TODO: Add correct server validation for nested components
|
||||||
.string()
|
component: yup.string().required(),
|
||||||
.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(),
|
|
||||||
min: yup.number(),
|
min: yup.number(),
|
||||||
max: yup.number(),
|
max: yup.number(),
|
||||||
};
|
};
|
||||||
|
@ -1,17 +1,9 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import utils, { errors } from '@strapi/utils';
|
import utils, { errors } from '@strapi/utils';
|
||||||
import type { Schema, Struct } from '@strapi/types';
|
import type { Schema } from '@strapi/types';
|
||||||
|
|
||||||
const { ApplicationError } = errors;
|
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) =>
|
export const isConfigurable = (attribute: Schema.Attribute.AnyAttribute) =>
|
||||||
_.get(attribute, 'configurable', true);
|
_.get(attribute, 'configurable', true);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user