From b98bf041ae9e215df67f4fbe288f2f53fa1bbfd8 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Mon, 25 Aug 2025 12:33:32 +0200 Subject: [PATCH 1/2] node field dnd logic updatedto prevent duplicates --- .../components/sidePanel/builder/dnd-hooks.ts | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/dnd-hooks.ts b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/dnd-hooks.ts index c9156c6782c..e6b12446cef 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/dnd-hooks.ts +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/dnd-hooks.ts @@ -34,7 +34,7 @@ import { import { selectFormRootElementId, selectNodesSlice, selectWorkflowForm } from 'features/nodes/store/selectors'; import type { FieldInputTemplate, StatefulFieldValue } from 'features/nodes/types/field'; import type { ElementId, FormElement } from 'features/nodes/types/workflow'; -import { buildNodeFieldElement, isContainerElement } from 'features/nodes/types/workflow'; +import { buildNodeFieldElement, isContainerElement, isNodeFieldElement } from 'features/nodes/types/workflow'; import type { RefObject } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { flushSync } from 'react-dom'; @@ -121,6 +121,29 @@ const useElementExists = () => { return _elementExists; }; +/** + * Checks if a node field element exists in the form. + * + * @param form The form to check + * @param nodeId The id of the node + * @param fieldName The name of field + * + * @returns True if the element exists, false otherwise + */ +export const useNodeFieldElementExists = () => { + const store = useAppStore(); + const nodeFieldElementExists = useCallback( + (nodeId: string, fieldName: string): boolean => { + const form = selectWorkflowForm(store.getState()); + return Object.values(form.elements) + .filter(isNodeFieldElement) + .some((el) => el.data.fieldIdentifier.nodeId === nodeId && el.data.fieldIdentifier.fieldName === fieldName); + }, + [store] + ); + return nodeFieldElementExists; +}; + /** * Wrapper around `getAllowedDropRegions` that provides the form state from the store. * @see {@link getAllowedDropRegions} @@ -368,6 +391,7 @@ export const useFormElementDnd = ( const [activeDropRegion, setActiveDropRegion] = useState(null); const getElement = useGetElement(); const getAllowedDropRegions = useGetAllowedDropRegions(); + const nodeFieldElementExists = useNodeFieldElementExists(); useEffect(() => { if (isRootElement) { @@ -401,7 +425,7 @@ export const useFormElementDnd = ( // TODO(psyche): This causes a kinda jittery behaviour - need a better heuristic to determine stickiness getIsSticky: () => false, canDrop: ({ source }) => { - if (isNodeFieldDndData(source.data)) { + if (isNodeFieldDndData(source.data) && !nodeFieldElementExists(source.data.nodeId, source.data.fieldName)) { return true; } if (isFormElementDndData(source.data)) { @@ -449,7 +473,15 @@ export const useFormElementDnd = ( }, }) ); - }, [dragHandleRef, draggableRef, elementId, getAllowedDropRegions, getElement, isRootElement]); + }, [ + dragHandleRef, + draggableRef, + elementId, + getAllowedDropRegions, + getElement, + nodeFieldElementExists, + isRootElement, + ]); return [activeDropRegion, isDragging] as const; }; From 310057b7759d1f5a3494018483e9387e9bb5f531 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Mon, 25 Aug 2025 12:52:16 +0200 Subject: [PATCH 2/2] useNodeFieldElementExists turned private --- .../features/nodes/components/sidePanel/builder/dnd-hooks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/dnd-hooks.ts b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/dnd-hooks.ts index e6b12446cef..788a613a7a5 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/dnd-hooks.ts +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/dnd-hooks.ts @@ -130,7 +130,7 @@ const useElementExists = () => { * * @returns True if the element exists, false otherwise */ -export const useNodeFieldElementExists = () => { +const useNodeFieldElementExists = () => { const store = useAppStore(); const nodeFieldElementExists = useCallback( (nodeId: string, fieldName: string): boolean => {