diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index a586aaa4aa5..9ede72d7941 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -1950,6 +1950,7 @@
"zoomToNode": "Zoom to Node",
"nodeFieldTooltip": "To add a node field, click the small plus sign button on the field in the Workflow Editor, or drag the field by its name into the form.",
"addToForm": "Add to Form",
+ "removeFromForm": "Remove from Form",
"label": "Label",
"showDescription": "Show Description",
"showShuffle": "Show Shuffle",
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldAddRemoveFormRoot.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldAddRemoveFormRoot.tsx
new file mode 100644
index 00000000000..83fb3fa3453
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldAddRemoveFormRoot.tsx
@@ -0,0 +1,41 @@
+import { IconButton } from '@invoke-ai/ui-library';
+import { useAddRemoveFormElement } from 'features/nodes/components/sidePanel/builder/use-add-remove-form-element';
+import { memo, useCallback, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiMinusBold, PiPlusBold } from 'react-icons/pi';
+
+type Props = {
+ nodeId: string;
+ fieldName: string;
+};
+
+export const InputFieldAddRemoveFormRoot = memo(({ nodeId, fieldName }: Props) => {
+ const { t } = useTranslation();
+ const { isAddedToRoot, addNodeFieldToRoot, removeNodeFieldFromRoot } = useAddRemoveFormElement(nodeId, fieldName);
+
+ const description = useMemo(() => {
+ return isAddedToRoot ? t('workflows.builder.removeFromForm') : t('workflows.builder.addToForm');
+ }, [isAddedToRoot, t]);
+
+ const icon = useMemo(() => {
+ return isAddedToRoot ? : ;
+ }, [isAddedToRoot]);
+
+ const onClick = useCallback(() => {
+ return isAddedToRoot ? removeNodeFieldFromRoot() : addNodeFieldToRoot();
+ }, [isAddedToRoot, addNodeFieldToRoot, removeNodeFieldFromRoot]);
+
+ return (
+
+ );
+});
+
+InputFieldAddRemoveFormRoot.displayName = 'InputFieldAddRemoveFormRoot';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldAddToFormRoot.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldAddToFormRoot.tsx
deleted file mode 100644
index 7e225a9a669..00000000000
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldAddToFormRoot.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { IconButton } from '@invoke-ai/ui-library';
-import { useAddNodeFieldToRoot } from 'features/nodes/components/sidePanel/builder/use-add-node-field-to-root';
-import { memo } from 'react';
-import { useTranslation } from 'react-i18next';
-import { PiPlusBold } from 'react-icons/pi';
-
-type Props = {
- nodeId: string;
- fieldName: string;
-};
-
-export const InputFieldAddToFormRoot = memo(({ nodeId, fieldName }: Props) => {
- const { t } = useTranslation();
- const { isAddedToRoot, addNodeFieldToRoot } = useAddNodeFieldToRoot(nodeId, fieldName);
-
- return (
- }
- pointerEvents="auto"
- size="xs"
- onClick={addNodeFieldToRoot}
- isDisabled={isAddedToRoot}
- />
- );
-});
-
-InputFieldAddToFormRoot.displayName = 'InputFieldAddToFormRoot';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeNodes.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeNodes.tsx
index 61825e0abd2..1597f4ede16 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeNodes.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeNodes.tsx
@@ -1,6 +1,5 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex, Spacer } from '@invoke-ai/ui-library';
-import { InputFieldAddToFormRoot } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldAddToFormRoot';
import { InputFieldDescriptionPopover } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldDescriptionPopover';
import { InputFieldHandle } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldHandle';
import { InputFieldResetToDefaultValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToDefaultValueIconButton';
@@ -12,6 +11,7 @@ import { NO_DRAG_CLASS } from 'features/nodes/types/constants';
import type { FieldInputTemplate } from 'features/nodes/types/field';
import { memo, useRef } from 'react';
+import { InputFieldAddRemoveFormRoot } from './InputFieldAddRemoveFormRoot';
import { InputFieldRenderer } from './InputFieldRenderer';
import { InputFieldTitle } from './InputFieldTitle';
import { InputFieldWrapper } from './InputFieldWrapper';
@@ -113,7 +113,7 @@ const DirectField = memo(({ nodeId, fieldName, isInvalid, isConnected, fieldTemp
-
+
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/use-add-node-field-to-root.ts b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/use-add-remove-form-element.ts
similarity index 51%
rename from invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/use-add-node-field-to-root.ts
rename to invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/use-add-remove-form-element.ts
index 0a3057a6356..e7e32345862 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/use-add-node-field-to-root.ts
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/use-add-remove-form-element.ts
@@ -1,21 +1,24 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance';
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplateOrThrow';
-import { formElementAdded } from 'features/nodes/store/nodesSlice';
-import { buildSelectWorkflowFormNodeExists, selectFormRootElementId } from 'features/nodes/store/selectors';
+import { formElementAdded, formElementRemoved } from 'features/nodes/store/nodesSlice';
+import { buildSelectWorkflowFormNodeElement, selectFormRootElementId } from 'features/nodes/store/selectors';
import { buildNodeFieldElement } from 'features/nodes/types/workflow';
import { useCallback, useMemo } from 'react';
-export const useAddNodeFieldToRoot = (nodeId: string, fieldName: string) => {
+export const useAddRemoveFormElement = (nodeId: string, fieldName: string) => {
const dispatch = useAppDispatch();
const rootElementId = useAppSelector(selectFormRootElementId);
const fieldTemplate = useInputFieldTemplateOrThrow(fieldName);
const field = useInputFieldInstance(fieldName);
- const selectWorkflowFormNodeExists = useMemo(
- () => buildSelectWorkflowFormNodeExists(nodeId, fieldName),
+ const selectWorkflowFormNodeElement = useMemo(
+ () => buildSelectWorkflowFormNodeElement(nodeId, fieldName),
[nodeId, fieldName]
);
- const isAddedToRoot = useAppSelector(selectWorkflowFormNodeExists);
+ const workflowFormNodeElement = useAppSelector(selectWorkflowFormNodeElement);
+ const isAddedToRoot = useMemo(() => {
+ return !!workflowFormNodeElement;
+ }, [workflowFormNodeElement]);
const addNodeFieldToRoot = useCallback(() => {
const element = buildNodeFieldElement(nodeId, fieldName, fieldTemplate.type);
@@ -28,5 +31,16 @@ export const useAddNodeFieldToRoot = (nodeId: string, fieldName: string) => {
);
}, [nodeId, fieldName, fieldTemplate.type, dispatch, rootElementId, field.value]);
- return { isAddedToRoot, addNodeFieldToRoot };
+ const removeNodeFieldFromRoot = useCallback(() => {
+ if (!workflowFormNodeElement) {
+ return;
+ }
+ dispatch(
+ formElementRemoved({
+ id: workflowFormNodeElement.id,
+ })
+ );
+ }, [workflowFormNodeElement, dispatch]);
+
+ return { isAddedToRoot, addNodeFieldToRoot, removeNodeFieldFromRoot };
};
diff --git a/invokeai/frontend/web/src/features/nodes/store/selectors.ts b/invokeai/frontend/web/src/features/nodes/store/selectors.ts
index 0a1ef2f0ff7..f3c35dc220f 100644
--- a/invokeai/frontend/web/src/features/nodes/store/selectors.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/selectors.ts
@@ -103,7 +103,10 @@ export const selectWorkflowFormNodeFieldFieldIdentifiersDeduped = createSelector
);
export const buildSelectElement = (id: string) => createNodesSelector((workflow) => workflow.form?.elements[id]);
-export const buildSelectWorkflowFormNodeExists = (nodeId: string, fieldName: string) =>
- createSelector(selectWorkflowFormNodeFieldFieldIdentifiersDeduped, (identifiers) =>
- identifiers.some((identifier) => identifier.nodeId === nodeId && identifier.fieldName === fieldName)
+export const buildSelectWorkflowFormNodeElement = (nodeId: string, fieldName: string) =>
+ createSelector(selectNodeFieldElements, (elements) =>
+ elements.find(
+ (element) =>
+ element.data.fieldIdentifier.nodeId === nodeId && element.data.fieldIdentifier.fieldName === fieldName
+ )
);