mirror of
https://github.com/strapi/strapi.git
synced 2025-09-05 14:53:01 +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 {
|
||||
component: component.uid,
|
||||
childComponents: attributes
|
||||
.filter((attribute) => {
|
||||
const { type } = attribute;
|
||||
|
||||
return attributes.some((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) => {
|
||||
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,8 +35,15 @@ export const getAttributesToDisplay = (
|
||||
];
|
||||
}
|
||||
|
||||
if (canAddComponentInAnotherComponent) {
|
||||
return [defaultAttributes, ['component']];
|
||||
// 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