From e6b7a3ce3590936f69f911e60ae5ca0b4dbee9ff Mon Sep 17 00:00:00 2001 From: Thomas Kellermeier Date: Wed, 22 Jan 2025 17:46:00 +0100 Subject: [PATCH] feat: add circularity error message, wrappingPatternIds and handle entryId as rootBlockId --- .../components/DraggableBlock/Dropzone.tsx | 23 ++++++++++- .../DraggableBlock/DropzoneClone.tsx | 3 ++ .../components/DraggableBlock/EditorBlock.tsx | 3 ++ .../DraggableBlock/EditorBlockClone.tsx | 3 ++ .../CircularDependencyErrorPlaceholder.tsx | 41 +++++++++++++++++++ .../src/hooks/useComponent.spec.ts | 1 + .../visual-editor/src/hooks/useComponent.tsx | 37 ++++++++++++----- 7 files changed, 98 insertions(+), 13 deletions(-) create mode 100644 packages/visual-editor/src/components/DraggableHelpers/CircularDependencyErrorPlaceholder.tsx diff --git a/packages/visual-editor/src/components/DraggableBlock/Dropzone.tsx b/packages/visual-editor/src/components/DraggableBlock/Dropzone.tsx index 7c609807e..22338a86e 100644 --- a/packages/visual-editor/src/components/DraggableBlock/Dropzone.tsx +++ b/packages/visual-editor/src/components/DraggableBlock/Dropzone.tsx @@ -31,6 +31,7 @@ type DropzoneProps = { className?: string; WrapperComponent?: ElementType | string; dragProps?: DragWrapperProps; + wrappingPatternIds?: Set; }; export function Dropzone({ @@ -40,6 +41,7 @@ export function Dropzone({ className, WrapperComponent = 'div', dragProps, + wrappingPatternIds: parentWrappingPatternIds = new Set(), ...rest }: DropzoneProps) { const userIsDragging = useDraggedItemStore((state) => state.isDraggingOnCanvas); @@ -66,6 +68,19 @@ export function Dropzone({ const isRootAssembly = node?.type === ASSEMBLY_NODE_TYPE; const htmlDraggableProps = getHtmlDragProps(dragProps); const htmlProps = getHtmlComponentProps(rest); + + const wrappingPatternIds = useMemo(() => { + // On the top level, the node is not defined. If the root blockId is not the default string, + // we assume that it is the entry ID of the experience/ pattern to properly detect circular dependencies + if (!node && tree.root.data.blockId && tree.root.data.blockId !== ROOT_ID) { + return new Set([tree.root.data.blockId, ...parentWrappingPatternIds]); + } + if (isRootAssembly && node?.data.blockId) { + return new Set([node.data.blockId, ...parentWrappingPatternIds]); + } + return parentWrappingPatternIds; + }, [isRootAssembly, node, parentWrappingPatternIds, tree.root.data.blockId]); + // To avoid a circular dependency, we create the recursive rendering function here and trickle it down const renderDropzone: RenderDropzoneFunction = useCallback( (node, props) => { @@ -74,11 +89,12 @@ export function Dropzone({ zoneId={node.data.id} node={node} resolveDesignValue={resolveDesignValue} + wrappingPatternIds={wrappingPatternIds} {...props} /> ); }, - [resolveDesignValue], + [wrappingPatternIds, resolveDesignValue], ); const renderClonedDropzone: RenderDropzoneFunction = useCallback( @@ -89,11 +105,12 @@ export function Dropzone({ node={node} resolveDesignValue={resolveDesignValue} renderDropzone={renderClonedDropzone} + wrappingPatternIds={wrappingPatternIds} {...props} /> ); }, - [resolveDesignValue], + [resolveDesignValue, wrappingPatternIds], ); const isDropzoneEnabled = useMemo(() => { @@ -152,6 +169,7 @@ export function Dropzone({ provided={provided} snapshot={snapshot} renderDropzone={renderClonedDropzone} + wrappingPatternIds={wrappingPatternIds} /> )}> {(provided, snapshot) => { @@ -200,6 +218,7 @@ export function Dropzone({ node={item} resolveDesignValue={resolveDesignValue} renderDropzone={renderDropzone} + wrappingPatternIds={wrappingPatternIds} /> )) )} diff --git a/packages/visual-editor/src/components/DraggableBlock/DropzoneClone.tsx b/packages/visual-editor/src/components/DraggableBlock/DropzoneClone.tsx index 86a574618..feadbf68b 100644 --- a/packages/visual-editor/src/components/DraggableBlock/DropzoneClone.tsx +++ b/packages/visual-editor/src/components/DraggableBlock/DropzoneClone.tsx @@ -18,6 +18,7 @@ type DropzoneProps = { WrapperComponent?: ElementType | string; renderDropzone: RenderDropzoneFunction; dragProps?: DragWrapperProps; + wrappingPatternIds: Set; }; export function DropzoneClone({ @@ -27,6 +28,7 @@ export function DropzoneClone({ WrapperComponent = 'div', renderDropzone, dragProps, + wrappingPatternIds, ...rest }: DropzoneProps) { const tree = useTreeStore((state) => state.tree); @@ -72,6 +74,7 @@ export function DropzoneClone({ node={item} resolveDesignValue={resolveDesignValue} renderDropzone={renderDropzone} + wrappingPatternIds={wrappingPatternIds} /> ); })} diff --git a/packages/visual-editor/src/components/DraggableBlock/EditorBlock.tsx b/packages/visual-editor/src/components/DraggableBlock/EditorBlock.tsx index c88d3fb4c..39ffd0891 100644 --- a/packages/visual-editor/src/components/DraggableBlock/EditorBlock.tsx +++ b/packages/visual-editor/src/components/DraggableBlock/EditorBlock.tsx @@ -41,6 +41,7 @@ type EditorBlockProps = { resolveDesignValue: ResolveDesignValueType; renderDropzone: RenderDropzoneFunction; zoneId: string; + wrappingPatternIds: Set; }; export const EditorBlock: React.FC = ({ @@ -51,6 +52,7 @@ export const EditorBlock: React.FC = ({ zoneId, userIsDragging, placeholder, + wrappingPatternIds, }) => { const { slotId } = parseZoneId(zoneId); const ref = useRef(null); @@ -69,6 +71,7 @@ export const EditorBlock: React.FC = ({ resolveDesignValue, renderDropzone, userIsDragging, + wrappingPatternIds, }); const { isSingleColumn, isWrapped } = useSingleColumn(node, resolveDesignValue); const setDomRect = useDraggedItemStore((state) => state.setDomRect); diff --git a/packages/visual-editor/src/components/DraggableBlock/EditorBlockClone.tsx b/packages/visual-editor/src/components/DraggableBlock/EditorBlockClone.tsx index fff9832e7..4bf3096e9 100644 --- a/packages/visual-editor/src/components/DraggableBlock/EditorBlockClone.tsx +++ b/packages/visual-editor/src/components/DraggableBlock/EditorBlockClone.tsx @@ -29,6 +29,7 @@ type EditorBlockCloneProps = { provided?: DraggableProvided; snapshot?: DraggableStateSnapshot; renderDropzone: RenderDropzoneFunction; + wrappingPatternIds: Set; }; export const EditorBlockClone: React.FC = ({ @@ -37,6 +38,7 @@ export const EditorBlockClone: React.FC = ({ snapshot, provided, renderDropzone, + wrappingPatternIds, }) => { const userIsDragging = useDraggedItemStore((state) => state.isDraggingOnCanvas); @@ -45,6 +47,7 @@ export const EditorBlockClone: React.FC = ({ resolveDesignValue, renderDropzone, userIsDragging, + wrappingPatternIds, }); const isAssemblyBlock = node.type === ASSEMBLY_BLOCK_NODE_TYPE; diff --git a/packages/visual-editor/src/components/DraggableHelpers/CircularDependencyErrorPlaceholder.tsx b/packages/visual-editor/src/components/DraggableHelpers/CircularDependencyErrorPlaceholder.tsx new file mode 100644 index 000000000..272cbd027 --- /dev/null +++ b/packages/visual-editor/src/components/DraggableHelpers/CircularDependencyErrorPlaceholder.tsx @@ -0,0 +1,41 @@ +import { useEntityStore } from '@/store/entityStore'; +import React, { forwardRef, HTMLAttributes } from 'react'; + +type CircularDependencyErrorPlaceholderProperties = HTMLAttributes & { + wrappingPatternIds: Set; +}; + +export const CircularDependencyErrorPlaceholder = forwardRef< + HTMLDivElement, + CircularDependencyErrorPlaceholderProperties +>(({ wrappingPatternIds, ...props }, ref) => { + const entityStore = useEntityStore((state) => state.entityStore); + + return ( +
+ Circular usage of patterns detected: +
    + {Array.from(wrappingPatternIds).map((patternId) => { + const entryLink = { sys: { type: 'Link', linkType: 'Entry', id: patternId } } as const; + const entry = entityStore.getEntityFromLink(entryLink); + const entryTitle = entry?.fields?.title; + const text = entryTitle ? `${entryTitle} (${patternId})` : patternId; + return
  • {text}
  • ; + })} +
+
+ ); +}); + +CircularDependencyErrorPlaceholder.displayName = 'CircularDependencyErrorPlaceholder'; diff --git a/packages/visual-editor/src/hooks/useComponent.spec.ts b/packages/visual-editor/src/hooks/useComponent.spec.ts index 1abb2f80e..0ad46b838 100644 --- a/packages/visual-editor/src/hooks/useComponent.spec.ts +++ b/packages/visual-editor/src/hooks/useComponent.spec.ts @@ -69,6 +69,7 @@ describe('useComponent', () => { resolveDesignValue, renderDropzone, userIsDragging, + wrappingPatternIds: new Set(), }), ); diff --git a/packages/visual-editor/src/hooks/useComponent.tsx b/packages/visual-editor/src/hooks/useComponent.tsx index 730d291a0..423099e10 100644 --- a/packages/visual-editor/src/hooks/useComponent.tsx +++ b/packages/visual-editor/src/hooks/useComponent.tsx @@ -20,12 +20,14 @@ import { isContentfulStructureComponent } from '@contentful/experiences-core'; import { MissingComponentPlaceholder } from '@components/DraggableHelpers/MissingComponentPlaceholder'; import { useTreeStore } from '@/store/tree'; import { getItem } from '@/utils/getItem'; +import { CircularDependencyErrorPlaceholder } from '@components/DraggableHelpers/CircularDependencyErrorPlaceholder'; type UseComponentProps = { node: ExperienceTreeNode; resolveDesignValue: ResolveDesignValueType; renderDropzone: RenderDropzoneFunction; userIsDragging: boolean; + wrappingPatternIds: Set; }; export const useComponent = ({ @@ -33,6 +35,7 @@ export const useComponent = ({ resolveDesignValue, renderDropzone, userIsDragging, + wrappingPatternIds, }: UseComponentProps) => { const areEntitiesFetched = useEntityStore((state) => state.areEntitiesFetched); const tree = useTreeStore((state) => state.tree); @@ -79,11 +82,32 @@ export const useComponent = ({ }); const elementToRender = (props?: { dragProps?: DragWrapperProps; rest?: unknown }) => { + const { dragProps = {} } = props || {}; + const { children, innerRef, Tag = 'div', ToolTipAndPlaceholder, style, ...rest } = dragProps; + const { + 'data-cf-node-block-id': dataCfNodeBlockId, + 'data-cf-node-block-type': dataCfNodeBlockType, + 'data-cf-node-id': dataCfNodeId, + } = componentProps; + const refCallback = (refNode: HTMLElement | null) => { + if (innerRef && refNode) innerRef(refNode); + }; + if (!componentRegistration) { return ; } - const { dragProps = {} } = props || {}; + if (node.data.blockId && wrappingPatternIds.has(node.data.blockId)) { + return ( + + ); + } const element = React.createElement( ImportedComponentErrorBoundary, @@ -98,20 +122,11 @@ export const useComponent = ({ return element; } - const { children, innerRef, Tag = 'div', ToolTipAndPlaceholder, style, ...rest } = dragProps; - const { - 'data-cf-node-block-id': dataCfNodeBlockId, - 'data-cf-node-block-type': dataCfNodeBlockType, - 'data-cf-node-id': dataCfNodeId, - } = componentProps; - return ( { - if (innerRef && refNode) innerRef(refNode); - }} + ref={refCallback} data-cf-node-id={dataCfNodeId} data-cf-node-block-id={dataCfNodeBlockId} data-cf-node-block-type={dataCfNodeBlockType}>