From 42c4462edcc65fb6bb59be7ebd950d3d4f13d489 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 25 Feb 2025 16:15:01 +1000
Subject: [PATCH] refactor(ui): styling for form edit mode (maybe done?)
- Restructure components
- Let each element render its own edit mode
- arrrrghh
---
...mentComponent.tsx => ContainerElement.tsx} | 302 +++++++++++-------
.../sidePanel/builder/DividerElement.tsx | 24 ++
.../builder/DividerElementComponent.tsx | 66 +---
.../builder/DividerElementEditMode.tsx | 45 +++
.../builder/DividerElementViewMode.tsx | 38 +++
.../builder/FormElementEditModeContent.tsx | 30 ++
.../builder/FormElementEditModeHeader.tsx | 39 ++-
.../builder/FormElementEditModeWrapper.tsx | 94 ------
.../sidePanel/builder/HeadingElement.tsx | 24 ++
.../builder/HeadingElementComponent.tsx | 123 -------
.../builder/HeadingElementContent.tsx | 23 ++
.../builder/HeadingElementContentEditable.tsx | 55 ++++
.../builder/HeadingElementEditMode.tsx | 44 +++
.../builder/HeadingElementViewMode.tsx | 23 ++
.../sidePanel/builder/NodeFieldElement.tsx | 33 ++
.../builder/NodeFieldElementComponent.tsx | 195 -----------
.../NodeFieldElementDescriptionEditable.tsx | 54 ++++
.../builder/NodeFieldElementEditMode.tsx | 57 ++++
.../builder/NodeFieldElementLabel.tsx | 24 ++
.../builder/NodeFieldElementLabelEditable.tsx | 56 ++++
.../builder/NodeFieldElementViewMode.tsx | 37 +++
.../sidePanel/builder/TextElement.tsx | 23 ++
.../builder/TextElementComponent.tsx | 115 -------
.../sidePanel/builder/TextElementContent.tsx | 23 ++
.../builder/TextElementContentEditable.tsx | 52 +++
.../sidePanel/builder/TextElementEditMode.tsx | 44 +++
.../sidePanel/builder/TextElementViewMode.tsx | 17 +
.../sidePanel/builder/WorkflowBuilder.tsx | 7 +-
.../components/sidePanel/builder/contexts.tsx | 5 +
.../components/sidePanel/builder/dnd-hooks.ts | 6 +-
.../components/sidePanel/builder/shared.ts | 21 ++
.../viewMode/ViewModeLeftPanelContent.tsx | 9 +-
.../src/features/nodes/store/workflowSlice.ts | 4 +
.../web/src/features/nodes/types/workflow.ts | 11 +-
34 files changed, 985 insertions(+), 738 deletions(-)
rename invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/{ContainerElementComponent.tsx => ContainerElement.tsx} (51%)
create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElement.tsx
create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElementEditMode.tsx
create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElementViewMode.tsx
create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeContent.tsx
delete mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeWrapper.tsx
create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElement.tsx
delete mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementComponent.tsx
create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementContent.tsx
create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementContentEditable.tsx
create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementEditMode.tsx
create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementViewMode.tsx
create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElement.tsx
delete mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementComponent.tsx
create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementDescriptionEditable.tsx
create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementEditMode.tsx
create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementLabel.tsx
create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementLabelEditable.tsx
create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementViewMode.tsx
create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElement.tsx
delete mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementComponent.tsx
create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementContent.tsx
create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementContentEditable.tsx
create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementEditMode.tsx
create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementViewMode.tsx
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/ContainerElementComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/ContainerElement.tsx
similarity index 51%
rename from invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/ContainerElementComponent.tsx
rename to invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/ContainerElement.tsx
index e00b4416c0..47590b7ade 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/ContainerElementComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/ContainerElement.tsx
@@ -1,18 +1,22 @@
+import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element';
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Flex, Text } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import {
ContainerContextProvider,
DepthContextProvider,
+ useContainerContext,
useDepthContext,
} from 'features/nodes/components/sidePanel/builder/contexts';
-import { DividerElementComponent } from 'features/nodes/components/sidePanel/builder/DividerElementComponent';
-import { useIsRootElement, useRootElementDropTarget } from 'features/nodes/components/sidePanel/builder/dnd-hooks';
-import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
-import { HeadingElementComponent } from 'features/nodes/components/sidePanel/builder/HeadingElementComponent';
-import { NodeFieldElementComponent } from 'features/nodes/components/sidePanel/builder/NodeFieldElementComponent';
-import { TextElementComponent } from 'features/nodes/components/sidePanel/builder/TextElementComponent';
-import { selectWorkflowMode, useElement } from 'features/nodes/store/workflowSlice';
+import { DividerElement } from 'features/nodes/components/sidePanel/builder/DividerElement';
+import { useFormElementDnd, useRootElementDropTarget } from 'features/nodes/components/sidePanel/builder/dnd-hooks';
+import { DndListDropIndicator } from 'features/nodes/components/sidePanel/builder/DndListDropIndicator';
+import { FormElementEditModeContent } from 'features/nodes/components/sidePanel/builder/FormElementEditModeContent';
+import { FormElementEditModeHeader } from 'features/nodes/components/sidePanel/builder/FormElementEditModeHeader';
+import { HeadingElement } from 'features/nodes/components/sidePanel/builder/HeadingElement';
+import { NodeFieldElement } from 'features/nodes/components/sidePanel/builder/NodeFieldElement';
+import { TextElement } from 'features/nodes/components/sidePanel/builder/TextElement';
+import { selectFormRootElement, selectWorkflowMode, useElement } from 'features/nodes/store/workflowSlice';
import type { ContainerElement } from 'features/nodes/types/workflow';
import {
CONTAINER_CLASS_NAME,
@@ -21,29 +25,21 @@ import {
isHeadingElement,
isNodeFieldElement,
isTextElement,
+ ROOT_CONTAINER_CLASS_NAME,
} from 'features/nodes/types/workflow';
-import { memo, useRef } from 'react';
+import { memo, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
-const ContainerElementComponent = memo(({ id }: { id: string }) => {
+const ContainerElement = memo(({ id }: { id: string }) => {
const el = useElement(id);
const mode = useAppSelector(selectWorkflowMode);
- const isRootElement = useIsRootElement(id);
if (!el || !isContainerElement(el)) {
return null;
}
- if (isRootElement && mode === 'view') {
- return ;
- }
-
- if (isRootElement && mode === 'edit') {
- return ;
- }
-
if (mode === 'view') {
return ;
}
@@ -51,89 +47,21 @@ const ContainerElementComponent = memo(({ id }: { id: string }) => {
// mode === 'edit'
return ;
});
-ContainerElementComponent.displayName = 'ContainerElementComponent';
+ContainerElement.displayName = 'ContainerElementComponent';
-const rootViewModeSx: SystemStyleObject = {
- position: 'relative',
- alignItems: 'center',
- borderRadius: 'base',
- w: 'full',
- h: 'full',
+const containerViewModeSx: SystemStyleObject = {
gap: 4,
- display: 'flex',
- flex: 1,
- '&[data-container-layout="column"]': {
+ flex: '1 0 0',
+ '&[data-self-layout="column"]': {
flexDir: 'column',
+ alignItems: 'stretch',
+ },
+ '&[data-self-layout="row"]': {
+ flexDir: 'row',
alignItems: 'flex-start',
- },
- '&[data-container-layout="row"]': {
- flexDir: 'row',
- },
-};
-
-const RootContainerElementComponentViewMode = memo(({ el }: { el: ContainerElement }) => {
- const { id, data } = el;
- const { children, layout } = data;
-
- return (
-
-
-
- {children.map((childId) => (
-
- ))}
-
-
-
- );
-});
-RootContainerElementComponentViewMode.displayName = 'RootContainerElementComponentViewMode';
-
-const rootEditModeSx: SystemStyleObject = {
- ...rootViewModeSx,
- '&[data-is-dragging-over="true"]': {
- opacity: 1,
- bg: 'base.850',
- },
-};
-
-const RootContainerElementComponentEditMode = memo(({ el }: { el: ContainerElement }) => {
- const { id, data } = el;
- const { children, layout } = data;
- const ref = useRef(null);
- const isDraggingOver = useRootElementDropTarget(ref);
-
- return (
-
-
-
- {children.map((childId) => (
-
- ))}
- {children.length === 0 && }
-
-
-
- );
-});
-RootContainerElementComponentEditMode.displayName = 'RootContainerElementComponentEditMode';
-
-const sx: SystemStyleObject = {
- gap: 4,
- flex: '1 1 0',
- '&[data-container-layout="column"]': {
- flexDir: 'column',
- },
- '&[data-container-layout="row"]': {
- flexDir: 'row',
+ overflowX: 'auto',
+ overflowY: 'visible',
+ h: 'min-content',
},
};
@@ -146,7 +74,13 @@ const ContainerElementComponentViewMode = memo(({ el }: { el: ContainerElement }
return (
-
+
{children.map((childId) => (
))}
@@ -162,27 +96,163 @@ const ContainerElementComponentViewMode = memo(({ el }: { el: ContainerElement }
});
ContainerElementComponentViewMode.displayName = 'ContainerElementComponentViewMode';
+const containerEditModeSx: SystemStyleObject = {
+ borderRadius: 'base',
+ position: 'relative',
+ '&[data-active-drop-region="center"]': {
+ opacity: 1,
+ bg: 'base.850',
+ },
+ flexDir: 'column',
+ '&[data-parent-layout="column"]': {
+ w: 'full',
+ h: 'min-content',
+ },
+ '&[data-parent-layout="row"]': {
+ flex: '1 1 0',
+ h: 'min-content',
+ },
+};
+
+const containerEditModeContentSx: SystemStyleObject = {
+ gap: 4,
+ p: 4,
+ flex: '1 1 0',
+ '&[data-self-layout="column"]': {
+ flexDir: 'column',
+ },
+ '&[data-self-layout="row"]': {
+ flexDir: 'row',
+ overflowX: 'auto',
+ },
+};
+
const ContainerElementComponentEditMode = memo(({ el }: { el: ContainerElement }) => {
const depth = useDepthContext();
+ const draggableRef = useRef(null);
+ const dragHandleRef = useRef(null);
+ const autoScrollRef = useRef(null);
+ const [activeDropRegion, isDragging] = useFormElementDnd(el.id, draggableRef, dragHandleRef);
+ const { id, data } = el;
+ const { children, layout } = data;
+ const containerCtx = useContainerContext();
+
+ useEffect(() => {
+ const element = autoScrollRef.current;
+ if (!element) {
+ return;
+ }
+
+ return autoScrollForElements({
+ element,
+ });
+ });
+
+ return (
+
+
+
+
+
+
+ {children.map((childId) => (
+
+ ))}
+ {children.length === 0 && }
+
+
+
+
+
+
+ );
+});
+ContainerElementComponentEditMode.displayName = 'ContainerElementComponentEditMode';
+
+const rootViewModeSx: SystemStyleObject = {
+ position: 'relative',
+ alignItems: 'center',
+ borderRadius: 'base',
+ w: 'full',
+ h: 'full',
+ gap: 4,
+ display: 'flex',
+ flex: 1,
+ maxW: '768px',
+ '&[data-self-layout="column"]': {
+ flexDir: 'column',
+ alignItems: 'stretch',
+ },
+ '&[data-self-layout="row"]': {
+ flexDir: 'row',
+ alignItems: 'flex-start',
+ },
+};
+
+export const RootContainerElementViewMode = memo(() => {
+ const el = useAppSelector(selectFormRootElement);
const { id, data } = el;
const { children, layout } = data;
return (
-
-
-
-
- {children.map((childId) => (
-
- ))}
- {children.length === 0 && }
-
-
-
-
+
+
+
+ {children.map((childId) => (
+
+ ))}
+
+
+
);
});
-ContainerElementComponentEditMode.displayName = 'ContainerElementComponentEditMode';
+RootContainerElementViewMode.displayName = 'RootContainerElementViewMode';
+
+const rootEditModeSx: SystemStyleObject = {
+ ...rootViewModeSx,
+ '&[data-is-dragging-over="true"]': {
+ opacity: 1,
+ bg: 'base.850',
+ },
+};
+
+export const RootContainerElementEditMode = memo(() => {
+ const el = useAppSelector(selectFormRootElement);
+ const { id, data } = el;
+ const { children, layout } = data;
+ const ref = useRef(null);
+ const isDraggingOver = useRootElementDropTarget(ref);
+
+ return (
+
+
+
+ {children.map((childId) => (
+
+ ))}
+ {children.length === 0 && }
+
+
+
+ );
+});
+RootContainerElementEditMode.displayName = 'RootContainerElementEditMode';
const RootPlaceholder = memo(() => {
const { t } = useTranslation();
@@ -213,23 +283,23 @@ export const FormElementComponent = memo(({ id }: { id: string }) => {
}
if (isContainerElement(el)) {
- return ;
+ return ;
}
if (isNodeFieldElement(el)) {
- return ;
+ return ;
}
if (isDividerElement(el)) {
- return ;
+ return ;
}
if (isHeadingElement(el)) {
- return ;
+ return ;
}
if (isTextElement(el)) {
- return ;
+ return ;
}
assert>(false, `Unhandled type for element with id ${id}`);
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElement.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElement.tsx
new file mode 100644
index 0000000000..745e281a49
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElement.tsx
@@ -0,0 +1,24 @@
+import { useAppSelector } from 'app/store/storeHooks';
+import { DividerElementEditMode } from 'features/nodes/components/sidePanel/builder/DividerElementEditMode';
+import { DividerElementViewMode } from 'features/nodes/components/sidePanel/builder/DividerElementViewMode';
+import { selectWorkflowMode, useElement } from 'features/nodes/store/workflowSlice';
+import { isDividerElement } from 'features/nodes/types/workflow';
+import { memo } from 'react';
+
+export const DividerElement = memo(({ id }: { id: string }) => {
+ const el = useElement(id);
+ const mode = useAppSelector(selectWorkflowMode);
+
+ if (!el || !isDividerElement(el)) {
+ return;
+ }
+
+ if (mode === 'view') {
+ return ;
+ }
+
+ // mode === 'edit'
+ return ;
+});
+
+DividerElement.displayName = 'DividerElement';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElementComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElementComponent.tsx
index a59f97151f..712ba68a12 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElementComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElementComponent.tsx
@@ -1,81 +1,25 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
-import { useAppSelector } from 'app/store/storeHooks';
import { useContainerContext } from 'features/nodes/components/sidePanel/builder/contexts';
-import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
-import { selectWorkflowMode, useElement } from 'features/nodes/store/workflowSlice';
-import type { DividerElement } from 'features/nodes/types/workflow';
-import { DIVIDER_CLASS_NAME, isDividerElement } from 'features/nodes/types/workflow';
import { memo } from 'react';
const sx: SystemStyleObject = {
bg: 'base.700',
flexShrink: 0,
- '&[data-layout="column"]': {
+ '&[data-parent-layout="column"]': {
width: '100%',
height: '1px',
},
- '&[data-layout="row"]': {
+ '&[data-parent-layout="row"]': {
height: '100%',
width: '1px',
- minH: 32,
},
};
-export const DividerElementComponent = memo(({ id }: { id: string }) => {
- const el = useElement(id);
- const mode = useAppSelector(selectWorkflowMode);
+export const DividerElementComponent = memo(() => {
+ const containerCtx = useContainerContext();
- if (!el || !isDividerElement(el)) {
- return;
- }
-
- if (mode === 'view') {
- return ;
- }
-
- // mode === 'edit'
- return ;
+ return ;
});
DividerElementComponent.displayName = 'DividerElementComponent';
-
-const DividerElementComponentViewMode = memo(({ el }: { el: DividerElement }) => {
- const container = useContainerContext();
- const { id } = el;
-
- return (
-
- );
-});
-
-DividerElementComponentViewMode.displayName = 'DividerElementComponentViewMode';
-
-const DividerElementComponentEditMode = memo(({ el }: { el: DividerElement }) => {
- const container = useContainerContext();
- const { id } = el;
-
- return (
-
-
-
- );
-});
-
-DividerElementComponentEditMode.displayName = 'DividerElementComponentEditMode';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElementEditMode.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElementEditMode.tsx
new file mode 100644
index 0000000000..6744d704ba
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElementEditMode.tsx
@@ -0,0 +1,45 @@
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
+import { Flex } from '@invoke-ai/ui-library';
+import { useContainerContext } from 'features/nodes/components/sidePanel/builder/contexts';
+import { DividerElementComponent } from 'features/nodes/components/sidePanel/builder/DividerElementComponent';
+import { useFormElementDnd } from 'features/nodes/components/sidePanel/builder/dnd-hooks';
+import { DndListDropIndicator } from 'features/nodes/components/sidePanel/builder/DndListDropIndicator';
+import { FormElementEditModeContent } from 'features/nodes/components/sidePanel/builder/FormElementEditModeContent';
+import { FormElementEditModeHeader } from 'features/nodes/components/sidePanel/builder/FormElementEditModeHeader';
+import type { DividerElement } from 'features/nodes/types/workflow';
+import { DIVIDER_CLASS_NAME } from 'features/nodes/types/workflow';
+import { memo, useRef } from 'react';
+
+export const sx: SystemStyleObject = {
+ position: 'relative',
+ borderRadius: 'base',
+ '&[data-parent-layout="column"]': {
+ w: 'full',
+ h: 'min-content',
+ },
+ '&[data-parent-layout="row"]': {
+ w: 'min-content',
+ h: 'full',
+ },
+ flexDir: 'column',
+};
+
+export const DividerElementEditMode = memo(({ el }: { el: DividerElement }) => {
+ const draggableRef = useRef(null);
+ const dragHandleRef = useRef(null);
+ const [activeDropRegion, isDragging] = useFormElementDnd(el.id, draggableRef, dragHandleRef);
+ const containerCtx = useContainerContext();
+ const { id } = el;
+
+ return (
+
+
+
+
+
+
+
+ );
+});
+
+DividerElementEditMode.displayName = 'DividerElementEditMode';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElementViewMode.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElementViewMode.tsx
new file mode 100644
index 0000000000..9f921affd5
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/DividerElementViewMode.tsx
@@ -0,0 +1,38 @@
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
+import { Flex } from '@invoke-ai/ui-library';
+import { useContainerContext } from 'features/nodes/components/sidePanel/builder/contexts';
+import type { DividerElement } from 'features/nodes/types/workflow';
+import { DIVIDER_CLASS_NAME } from 'features/nodes/types/workflow';
+import { memo } from 'react';
+
+const sx: SystemStyleObject = {
+ bg: 'base.700',
+ flexShrink: 0,
+ '&[data-layout="column"]': {
+ width: '100%',
+ height: '1px',
+ },
+ '&[data-layout="row"]': {
+ height: '100%',
+ width: '1px',
+ },
+};
+
+export const DividerElementViewMode = memo(({ el }: { el: DividerElement }) => {
+ const container = useContainerContext();
+ const { id } = el;
+
+ return (
+
+ );
+});
+
+DividerElementViewMode.displayName = 'DividerElementViewMode';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeContent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeContent.tsx
new file mode 100644
index 0000000000..c42e5e3546
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeContent.tsx
@@ -0,0 +1,30 @@
+import type { FlexProps, SystemStyleObject } from '@invoke-ai/ui-library';
+import { Flex } from '@invoke-ai/ui-library';
+import { useDepthContext } from 'features/nodes/components/sidePanel/builder/contexts';
+import { memo } from 'react';
+
+const contentWrapperSx: SystemStyleObject = {
+ w: 'full',
+ h: 'full',
+ borderWidth: 1,
+ borderRadius: 'base',
+ borderTopRadius: 'unset',
+ borderTop: 'unset',
+ borderColor: 'baseAlpha.250',
+ '&[data-depth="0"]': { borderColor: 'baseAlpha.100' },
+ '&[data-depth="1"]': { borderColor: 'baseAlpha.150' },
+ '&[data-depth="2"]': { borderColor: 'baseAlpha.200' },
+ '&[data-is-dragging="true"]': {
+ opacity: 0.3,
+ },
+};
+
+export const FormElementEditModeContent = memo(({ children, ...rest }: FlexProps) => {
+ const depth = useDepthContext();
+ return (
+
+ {children}
+
+ );
+});
+FormElementEditModeContent.displayName = 'FormElementEditModeContent';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeHeader.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeHeader.tsx
index d6af5af8e7..56c5daa19c 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeHeader.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeHeader.tsx
@@ -1,5 +1,5 @@
-import type { SystemStyleObject } from '@invoke-ai/ui-library';
-import { Flex, forwardRef, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
+import type { FlexProps, SystemStyleObject } from '@invoke-ai/ui-library';
+import { Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { ContainerElementSettings } from 'features/nodes/components/sidePanel/builder/ContainerElementSettings';
import { useDepthContext } from 'features/nodes/components/sidePanel/builder/contexts';
@@ -9,6 +9,7 @@ import { formElementRemoved } from 'features/nodes/store/workflowSlice';
import type { FormElement, NodeFieldElement } from 'features/nodes/types/workflow';
import { isContainerElement, isNodeFieldElement } from 'features/nodes/types/workflow';
import { startCase } from 'lodash-es';
+import type { RefObject } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiGpsFixBold, PiXBold } from 'react-icons/pi';
@@ -23,27 +24,31 @@ const sx: SystemStyleObject = {
alignItems: 'center',
color: 'base.500',
bg: 'baseAlpha.250',
+ cursor: 'grab',
'&[data-depth="0"]': { bg: 'baseAlpha.100' },
'&[data-depth="1"]': { bg: 'baseAlpha.150' },
'&[data-depth="2"]': { bg: 'baseAlpha.200' },
+ '&[data-is-dragging="true"]': {
+ opacity: 0.3,
+ },
};
-export const FormElementEditModeHeader = memo(
- forwardRef(({ element }: { element: FormElement }, ref) => {
- const depth = useDepthContext();
+type Props = Omit & { element: FormElement; dragHandleRef: RefObject };
- return (
-
-
-
- {isContainerElement(element) && }
- {isNodeFieldElement(element) && }
- {isNodeFieldElement(element) && }
-
-
- );
- })
-);
+export const FormElementEditModeHeader = memo(({ element, dragHandleRef, ...rest }: Props) => {
+ const depth = useDepthContext();
+
+ return (
+
+
+
+ {isContainerElement(element) && }
+ {isNodeFieldElement(element) && }
+ {isNodeFieldElement(element) && }
+
+
+ );
+});
FormElementEditModeHeader.displayName = 'FormElementEditModeHeader';
const ZoomToNodeButton = memo(({ element }: { element: NodeFieldElement }) => {
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeWrapper.tsx
deleted file mode 100644
index 6be170c8a1..0000000000
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeWrapper.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-import type { SystemStyleObject } from '@invoke-ai/ui-library';
-import { Flex } from '@invoke-ai/ui-library';
-import { useContainerContext, useDepthContext } from 'features/nodes/components/sidePanel/builder/contexts';
-import { useFormElementDnd } from 'features/nodes/components/sidePanel/builder/dnd-hooks';
-import { DndListDropIndicator } from 'features/nodes/components/sidePanel/builder/DndListDropIndicator';
-import { FormElementEditModeHeader } from 'features/nodes/components/sidePanel/builder/FormElementEditModeHeader';
-import { getEditModeWrapperId } from 'features/nodes/components/sidePanel/builder/shared';
-import type { FormElement } from 'features/nodes/types/workflow';
-import type { PropsWithChildren } from 'react';
-import { memo, useRef } from 'react';
-
-export const EDIT_MODE_WRAPPER_CLASS_NAME = 'edit-mode-wrapper';
-
-const wrapperSx: SystemStyleObject = {
- position: 'relative',
- flex: '1 1 0',
- '&[data-element-type="divider"]&[data-layout="row"]': {
- flex: '0 1 0',
- },
- borderRadius: 'base',
-};
-
-const innerSx: SystemStyleObject = {
- position: 'relative',
- flexDir: 'column',
- alignItems: 'center',
- justifyContent: 'flex-start',
- borderRadius: 'base',
- w: 'full',
- h: 'full',
- '&[data-is-dragging="true"]': {
- opacity: 0.3,
- },
- '&[data-active-drop-region="center"]': {
- opacity: 1,
- bg: 'base.850',
- },
- '&[data-element-type="divider"]&[data-layout="row"]': {
- w: 'min-content',
- },
- '&[data-element-type="divider"]&[data-layout="column"]': {
- h: 'min-content',
- },
-};
-
-const contentWrapperSx: SystemStyleObject = {
- w: 'full',
- h: 'full',
- p: 4,
- gap: 4,
- borderWidth: 1,
- borderRadius: 'base',
- borderTopRadius: 'unset',
- borderTop: 'unset',
- borderColor: 'baseAlpha.250',
- '&[data-depth="0"]': { borderColor: 'baseAlpha.100' },
- '&[data-depth="1"]': { borderColor: 'baseAlpha.150' },
- '&[data-depth="2"]': { borderColor: 'baseAlpha.200' },
-};
-
-export const FormElementEditModeWrapper = memo(({ element, children }: PropsWithChildren<{ element: FormElement }>) => {
- const draggableRef = useRef(null);
- const dragHandleRef = useRef(null);
- const [activeDropRegion, isDragging] = useFormElementDnd(element.id, draggableRef, dragHandleRef);
- const containerCtx = useContainerContext();
- const depth = useDepthContext();
-
- return (
-
-
-
-
- {children}
-
-
-
-
- );
-});
-
-FormElementEditModeWrapper.displayName = 'FormElementEditModeWrapper';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElement.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElement.tsx
new file mode 100644
index 0000000000..6c32cc1253
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElement.tsx
@@ -0,0 +1,24 @@
+import { useAppSelector } from 'app/store/storeHooks';
+import { HeadingElementEditMode } from 'features/nodes/components/sidePanel/builder/HeadingElementEditMode';
+import { HeadingElementViewMode } from 'features/nodes/components/sidePanel/builder/HeadingElementViewMode';
+import { selectWorkflowMode, useElement } from 'features/nodes/store/workflowSlice';
+import { isHeadingElement } from 'features/nodes/types/workflow';
+import { memo } from 'react';
+
+export const HeadingElement = memo(({ id }: { id: string }) => {
+ const el = useElement(id);
+ const mode = useAppSelector(selectWorkflowMode);
+
+ if (!el || !isHeadingElement(el)) {
+ return null;
+ }
+
+ if (mode === 'view') {
+ return ;
+ }
+
+ // mode === 'edit'
+ return ;
+});
+
+HeadingElement.displayName = 'HeadingElement';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementComponent.tsx
deleted file mode 100644
index 4ac2d466d3..0000000000
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementComponent.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-import type { HeadingProps, SystemStyleObject } from '@invoke-ai/ui-library';
-import { Flex, Text } from '@invoke-ai/ui-library';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { useEditable } from 'common/hooks/useEditable';
-import { AutosizeTextarea } from 'features/nodes/components/sidePanel/builder/AutosizeTextarea';
-import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
-import { formElementHeadingDataChanged, selectWorkflowMode, useElement } from 'features/nodes/store/workflowSlice';
-import type { HeadingElement } from 'features/nodes/types/workflow';
-import { HEADING_CLASS_NAME, isHeadingElement } from 'features/nodes/types/workflow';
-import { memo, useCallback, useRef } from 'react';
-import { useTranslation } from 'react-i18next';
-
-export const HeadingElementComponent = memo(({ id }: { id: string }) => {
- const el = useElement(id);
- const mode = useAppSelector(selectWorkflowMode);
-
- if (!el || !isHeadingElement(el)) {
- return null;
- }
-
- if (mode === 'view') {
- return ;
- }
-
- // mode === 'edit'
- return ;
-});
-
-HeadingElementComponent.displayName = 'HeadingElementComponent';
-
-const HeadingElementComponentViewMode = memo(({ el }: { el: HeadingElement }) => {
- const { id, data } = el;
- const { content } = data;
-
- return (
-
-
-
- );
-});
-
-HeadingElementComponentViewMode.displayName = 'HeadingElementComponentViewMode';
-
-const HeadingElementComponentEditMode = memo(({ el }: { el: HeadingElement }) => {
- const { id } = el;
-
- return (
-
-
-
-
-
- );
-});
-
-const FONT_SIZE = '2xl';
-
-const headingSx: SystemStyleObject = {
- fontWeight: 'bold',
- fontSize: FONT_SIZE,
- '&[data-is-empty="true"]': {
- opacity: 0.3,
- },
-};
-
-const HeadingContentDisplay = memo(({ content, ...rest }: { content: string } & HeadingProps) => {
- const { t } = useTranslation();
- return (
-
- {content || t('workflows.builder.headingPlaceholder')}
-
- );
-});
-HeadingContentDisplay.displayName = 'HeadingContentDisplay';
-
-HeadingElementComponentEditMode.displayName = 'HeadingElementComponentEditMode';
-
-const EditableHeading = memo(({ el }: { el: HeadingElement }) => {
- const { t } = useTranslation();
- const dispatch = useAppDispatch();
- const { id, data } = el;
- const { content } = data;
- const ref = useRef(null);
-
- const onChange = useCallback(
- (content: string) => {
- dispatch(formElementHeadingDataChanged({ id, changes: { content } }));
- },
- [dispatch, id]
- );
-
- const editable = useEditable({
- value: content,
- defaultValue: '',
- onChange,
- inputRef: ref,
- });
-
- if (!editable.isEditing) {
- return ;
- }
-
- return (
-
- );
-});
-
-EditableHeading.displayName = 'EditableHeading';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementContent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementContent.tsx
new file mode 100644
index 0000000000..b23f2eee2b
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementContent.tsx
@@ -0,0 +1,23 @@
+import type { HeadingProps, SystemStyleObject } from '@invoke-ai/ui-library';
+import { Text } from '@invoke-ai/ui-library';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+export const headingSx: SystemStyleObject = {
+ fontWeight: 'bold',
+ fontSize: '2xl',
+ '&[data-is-empty="true"]': {
+ opacity: 0.3,
+ },
+};
+
+export const HeadingElementContent = memo(({ content, ...rest }: { content: string } & HeadingProps) => {
+ const { t } = useTranslation();
+ return (
+
+ {content || t('workflows.builder.headingPlaceholder')}
+
+ );
+});
+
+HeadingElementContent.displayName = 'HeadingElementContent';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementContentEditable.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementContentEditable.tsx
new file mode 100644
index 0000000000..252157bf83
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementContentEditable.tsx
@@ -0,0 +1,55 @@
+import { useAppDispatch } from 'app/store/storeHooks';
+import { useEditable } from 'common/hooks/useEditable';
+import { AutosizeTextarea } from 'features/nodes/components/sidePanel/builder/AutosizeTextarea';
+import { HeadingElementContent } from 'features/nodes/components/sidePanel/builder/HeadingElementContent';
+import { formElementHeadingDataChanged } from 'features/nodes/store/workflowSlice';
+import type { HeadingElement } from 'features/nodes/types/workflow';
+import { memo, useCallback, useRef } from 'react';
+import { useTranslation } from 'react-i18next';
+
+export const HeadingElementContentEditable = memo(({ el }: { el: HeadingElement }) => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const { id, data } = el;
+ const { content } = data;
+ const ref = useRef(null);
+
+ const onChange = useCallback(
+ (content: string) => {
+ dispatch(formElementHeadingDataChanged({ id, changes: { content } }));
+ },
+ [dispatch, id]
+ );
+
+ const editable = useEditable({
+ value: content,
+ defaultValue: '',
+ onChange,
+ inputRef: ref,
+ });
+
+ if (!editable.isEditing) {
+ return ;
+ }
+
+ return (
+
+ );
+});
+
+HeadingElementContentEditable.displayName = 'HeadingElementContentEditable';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementEditMode.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementEditMode.tsx
new file mode 100644
index 0000000000..6be2f6cd2d
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementEditMode.tsx
@@ -0,0 +1,44 @@
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
+import { Flex } from '@invoke-ai/ui-library';
+import { useContainerContext } from 'features/nodes/components/sidePanel/builder/contexts';
+import { useFormElementDnd } from 'features/nodes/components/sidePanel/builder/dnd-hooks';
+import { DndListDropIndicator } from 'features/nodes/components/sidePanel/builder/DndListDropIndicator';
+import { FormElementEditModeContent } from 'features/nodes/components/sidePanel/builder/FormElementEditModeContent';
+import { FormElementEditModeHeader } from 'features/nodes/components/sidePanel/builder/FormElementEditModeHeader';
+import { HeadingElementContentEditable } from 'features/nodes/components/sidePanel/builder/HeadingElementContentEditable';
+import type { HeadingElement } from 'features/nodes/types/workflow';
+import { HEADING_CLASS_NAME } from 'features/nodes/types/workflow';
+import { memo, useRef } from 'react';
+
+const sx: SystemStyleObject = {
+ position: 'relative',
+ borderRadius: 'base',
+ '&[data-parent-layout="column"]': {
+ w: 'full',
+ h: 'min-content',
+ },
+ '&[data-parent-layout="row"]': {
+ flex: '1 0 0',
+ },
+ flexDir: 'column',
+};
+
+export const HeadingElementEditMode = memo(({ el }: { el: HeadingElement }) => {
+ const draggableRef = useRef(null);
+ const dragHandleRef = useRef(null);
+ const [activeDropRegion, isDragging] = useFormElementDnd(el.id, draggableRef, dragHandleRef);
+ const containerCtx = useContainerContext();
+ const { id } = el;
+
+ return (
+
+
+
+
+
+
+
+ );
+});
+
+HeadingElementEditMode.displayName = 'HeadingElementEditMode';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementViewMode.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementViewMode.tsx
new file mode 100644
index 0000000000..d114ec05c8
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementViewMode.tsx
@@ -0,0 +1,23 @@
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
+import { Flex } from '@invoke-ai/ui-library';
+import { HeadingElementContent } from 'features/nodes/components/sidePanel/builder/HeadingElementContent';
+import type { HeadingElement } from 'features/nodes/types/workflow';
+import { HEADING_CLASS_NAME } from 'features/nodes/types/workflow';
+import { memo } from 'react';
+
+const sx: SystemStyleObject = {
+ flex: '1 0 0',
+};
+
+export const HeadingElementViewMode = memo(({ el }: { el: HeadingElement }) => {
+ const { id, data } = el;
+ const { content } = data;
+
+ return (
+
+
+
+ );
+});
+
+HeadingElementViewMode.displayName = 'HeadingElementViewMode';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElement.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElement.tsx
new file mode 100644
index 0000000000..19451e54bb
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElement.tsx
@@ -0,0 +1,33 @@
+import { useAppSelector } from 'app/store/storeHooks';
+import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
+import { NodeFieldElementEditMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementEditMode';
+import { NodeFieldElementViewMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementViewMode';
+import { selectWorkflowMode, useElement } from 'features/nodes/store/workflowSlice';
+import { isNodeFieldElement } from 'features/nodes/types/workflow';
+import { memo } from 'react';
+
+export const NodeFieldElement = memo(({ id }: { id: string }) => {
+ const el = useElement(id);
+ const mode = useAppSelector(selectWorkflowMode);
+
+ if (!el || !isNodeFieldElement(el)) {
+ return null;
+ }
+
+ if (mode === 'view') {
+ return (
+
+
+
+ );
+ }
+
+ // mode === 'edit'
+ return (
+
+
+
+ );
+});
+
+NodeFieldElement.displayName = 'NodeFieldElement';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementComponent.tsx
deleted file mode 100644
index cf54777656..0000000000
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementComponent.tsx
+++ /dev/null
@@ -1,195 +0,0 @@
-import { Flex, FormControl, FormHelperText, FormLabel, Input, Spacer, Textarea } from '@invoke-ai/ui-library';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { useEditable } from 'common/hooks/useEditable';
-import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
-import { InputFieldRenderer } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
-import { NodeFieldElementResetToInitialValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/NodeFieldElementResetToInitialValueIconButton';
-import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
-import { useInputFieldDescription } from 'features/nodes/hooks/useInputFieldDescription';
-import { useInputFieldLabel } from 'features/nodes/hooks/useInputFieldLabel';
-import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
-import { fieldDescriptionChanged, fieldLabelChanged } from 'features/nodes/store/nodesSlice';
-import { selectWorkflowMode, useElement } from 'features/nodes/store/workflowSlice';
-import type { NodeFieldElement } from 'features/nodes/types/workflow';
-import { isNodeFieldElement, NODE_FIELD_CLASS_NAME } from 'features/nodes/types/workflow';
-import { memo, useCallback, useMemo, useRef } from 'react';
-
-export const NodeFieldElementComponent = memo(({ id }: { id: string }) => {
- const el = useElement(id);
- const mode = useAppSelector(selectWorkflowMode);
-
- if (!el || !isNodeFieldElement(el)) {
- return null;
- }
-
- if (mode === 'view') {
- return (
-
-
-
- );
- }
-
- // mode === 'edit'
- return (
-
- {' '}
-
- );
-});
-
-NodeFieldElementComponent.displayName = 'NodeFieldElementComponent';
-
-const NodeFieldElementComponentViewMode = memo(({ el }: { el: NodeFieldElement }) => {
- const { id, data } = el;
- const { fieldIdentifier, showDescription } = data;
- const label = useInputFieldLabel(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
- const description = useInputFieldDescription(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
- const fieldTemplate = useInputFieldTemplate(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
-
- const _label = useMemo(() => label || fieldTemplate.title, [label, fieldTemplate.title]);
- const _description = useMemo(
- () => description || fieldTemplate.description,
- [description, fieldTemplate.description]
- );
-
- return (
-
-
-
- {_label}
-
-
-
-
-
-
- {showDescription && _description && {_description}}
-
-
- );
-});
-
-NodeFieldElementComponentViewMode.displayName = 'NodeFieldElementComponentViewMode';
-
-const NodeFieldElementComponentEditMode = memo(({ el }: { el: NodeFieldElement }) => {
- const { id, data } = el;
- const { fieldIdentifier, showDescription } = data;
-
- return (
-
-
-
-
-
-
-
- {showDescription && }
-
-
-
- );
-});
-
-NodeFieldElementComponentEditMode.displayName = 'NodeFieldElementComponentEditMode';
-
-const NodeFieldEditableLabel = memo(({ el }: { el: NodeFieldElement }) => {
- const { data } = el;
- const { fieldIdentifier } = data;
- const dispatch = useAppDispatch();
- const label = useInputFieldLabel(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
- const fieldTemplate = useInputFieldTemplate(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
- const inputRef = useRef(null);
-
- const onChange = useCallback(
- (label: string) => {
- dispatch(fieldLabelChanged({ nodeId: fieldIdentifier.nodeId, fieldName: fieldIdentifier.fieldName, label }));
- },
- [dispatch, fieldIdentifier.fieldName, fieldIdentifier.nodeId]
- );
-
- const editable = useEditable({
- value: label || fieldTemplate.title,
- defaultValue: fieldTemplate.title,
- inputRef,
- onChange,
- });
-
- if (!editable.isEditing) {
- return (
-
-
- {editable.value}
-
-
-
-
- );
- }
-
- return (
-
- );
-});
-NodeFieldEditableLabel.displayName = 'NodeFieldEditableLabel';
-
-const NodeFieldEditableDescription = memo(({ el }: { el: NodeFieldElement }) => {
- const { data } = el;
- const { fieldIdentifier } = data;
- const dispatch = useAppDispatch();
- const description = useInputFieldDescription(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
- const fieldTemplate = useInputFieldTemplate(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
- const inputRef = useRef(null);
-
- const onChange = useCallback(
- (description: string) => {
- dispatch(
- fieldDescriptionChanged({
- nodeId: fieldIdentifier.nodeId,
- fieldName: fieldIdentifier.fieldName,
- val: description,
- })
- );
- },
- [dispatch, fieldIdentifier.fieldName, fieldIdentifier.nodeId]
- );
-
- const editable = useEditable({
- value: description || fieldTemplate.description,
- defaultValue: fieldTemplate.description,
- inputRef,
- onChange,
- });
-
- if (!editable.isEditing) {
- return {editable.value};
- }
-
- return (
-
- );
-});
-NodeFieldEditableDescription.displayName = 'NodeFieldEditableDescription';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementDescriptionEditable.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementDescriptionEditable.tsx
new file mode 100644
index 0000000000..63acf2918b
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementDescriptionEditable.tsx
@@ -0,0 +1,54 @@
+import { FormHelperText, Textarea } from '@invoke-ai/ui-library';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { useEditable } from 'common/hooks/useEditable';
+import { useInputFieldDescription } from 'features/nodes/hooks/useInputFieldDescription';
+import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
+import { fieldDescriptionChanged } from 'features/nodes/store/nodesSlice';
+import type { NodeFieldElement } from 'features/nodes/types/workflow';
+import { memo, useCallback, useRef } from 'react';
+
+export const NodeFieldElementDescriptionEditable = memo(({ el }: { el: NodeFieldElement }) => {
+ const { data } = el;
+ const { fieldIdentifier } = data;
+ const dispatch = useAppDispatch();
+ const description = useInputFieldDescription(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
+ const fieldTemplate = useInputFieldTemplate(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
+ const inputRef = useRef(null);
+
+ const onChange = useCallback(
+ (description: string) => {
+ dispatch(
+ fieldDescriptionChanged({
+ nodeId: fieldIdentifier.nodeId,
+ fieldName: fieldIdentifier.fieldName,
+ val: description,
+ })
+ );
+ },
+ [dispatch, fieldIdentifier.fieldName, fieldIdentifier.nodeId]
+ );
+
+ const editable = useEditable({
+ value: description || fieldTemplate.description,
+ defaultValue: fieldTemplate.description,
+ inputRef,
+ onChange,
+ });
+
+ if (!editable.isEditing) {
+ return {editable.value};
+ }
+
+ return (
+
+ );
+});
+NodeFieldElementDescriptionEditable.displayName = 'NodeFieldElementDescriptionEditable';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementEditMode.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementEditMode.tsx
new file mode 100644
index 0000000000..db8fbbc729
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementEditMode.tsx
@@ -0,0 +1,57 @@
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
+import { Flex, FormControl } from '@invoke-ai/ui-library';
+import { InputFieldRenderer } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
+import { useContainerContext } from 'features/nodes/components/sidePanel/builder/contexts';
+import { useFormElementDnd } from 'features/nodes/components/sidePanel/builder/dnd-hooks';
+import { DndListDropIndicator } from 'features/nodes/components/sidePanel/builder/DndListDropIndicator';
+import { FormElementEditModeContent } from 'features/nodes/components/sidePanel/builder/FormElementEditModeContent';
+import { FormElementEditModeHeader } from 'features/nodes/components/sidePanel/builder/FormElementEditModeHeader';
+import { NodeFieldElementDescriptionEditable } from 'features/nodes/components/sidePanel/builder/NodeFieldElementDescriptionEditable';
+import { NodeFieldElementLabelEditable } from 'features/nodes/components/sidePanel/builder/NodeFieldElementLabelEditable';
+import type { NodeFieldElement } from 'features/nodes/types/workflow';
+import { NODE_FIELD_CLASS_NAME } from 'features/nodes/types/workflow';
+import { memo, useRef } from 'react';
+
+const sx: SystemStyleObject = {
+ position: 'relative',
+ borderRadius: 'base',
+ '&[data-parent-layout="column"]': {
+ w: 'full',
+ h: 'min-content',
+ },
+ '&[data-parent-layout="row"]': {
+ flex: '1 1 0',
+ },
+ flexDir: 'column',
+};
+
+export const NodeFieldElementEditMode = memo(({ el }: { el: NodeFieldElement }) => {
+ const draggableRef = useRef(null);
+ const dragHandleRef = useRef(null);
+ const [activeDropRegion, isDragging] = useFormElementDnd(el.id, draggableRef, dragHandleRef);
+ const containerCtx = useContainerContext();
+ const { id, data } = el;
+ const { fieldIdentifier, showDescription } = data;
+
+ return (
+
+
+
+
+
+
+
+
+ {showDescription && }
+
+
+
+
+ );
+});
+
+NodeFieldElementEditMode.displayName = 'NodeFieldElementEditMode';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementLabel.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementLabel.tsx
new file mode 100644
index 0000000000..422669c5a1
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementLabel.tsx
@@ -0,0 +1,24 @@
+import { Flex, FormLabel, Spacer } from '@invoke-ai/ui-library';
+import { NodeFieldElementResetToInitialValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/NodeFieldElementResetToInitialValueIconButton';
+import { useInputFieldLabel } from 'features/nodes/hooks/useInputFieldLabel';
+import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
+import type { NodeFieldElement } from 'features/nodes/types/workflow';
+import { memo, useMemo } from 'react';
+
+export const NodeFieldElementLabel = memo(({ el }: { el: NodeFieldElement }) => {
+ const { data } = el;
+ const { fieldIdentifier } = data;
+ const label = useInputFieldLabel(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
+ const fieldTemplate = useInputFieldTemplate(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
+
+ const _label = useMemo(() => label || fieldTemplate.title, [label, fieldTemplate.title]);
+
+ return (
+
+ {_label}
+
+
+
+ );
+});
+NodeFieldElementLabel.displayName = 'NodeFieldElementLabel';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementLabelEditable.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementLabelEditable.tsx
new file mode 100644
index 0000000000..c9d4438940
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementLabelEditable.tsx
@@ -0,0 +1,56 @@
+import { Flex, FormLabel, Input, Spacer } from '@invoke-ai/ui-library';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { useEditable } from 'common/hooks/useEditable';
+import { NodeFieldElementResetToInitialValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/NodeFieldElementResetToInitialValueIconButton';
+import { useInputFieldLabel } from 'features/nodes/hooks/useInputFieldLabel';
+import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
+import { fieldLabelChanged } from 'features/nodes/store/nodesSlice';
+import type { NodeFieldElement } from 'features/nodes/types/workflow';
+import { memo, useCallback, useRef } from 'react';
+
+export const NodeFieldElementLabelEditable = memo(({ el }: { el: NodeFieldElement }) => {
+ const { data } = el;
+ const { fieldIdentifier } = data;
+ const dispatch = useAppDispatch();
+ const label = useInputFieldLabel(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
+ const fieldTemplate = useInputFieldTemplate(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
+ const inputRef = useRef(null);
+
+ const onChange = useCallback(
+ (label: string) => {
+ dispatch(fieldLabelChanged({ nodeId: fieldIdentifier.nodeId, fieldName: fieldIdentifier.fieldName, label }));
+ },
+ [dispatch, fieldIdentifier.fieldName, fieldIdentifier.nodeId]
+ );
+
+ const editable = useEditable({
+ value: label || fieldTemplate.title,
+ defaultValue: fieldTemplate.title,
+ inputRef,
+ onChange,
+ });
+
+ if (!editable.isEditing) {
+ return (
+
+
+ {editable.value}
+
+
+
+
+ );
+ }
+
+ return (
+
+ );
+});
+NodeFieldElementLabelEditable.displayName = 'NodeFieldElementLabelEditable';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementViewMode.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementViewMode.tsx
new file mode 100644
index 0000000000..fbd5065843
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementViewMode.tsx
@@ -0,0 +1,37 @@
+import { Flex, FormControl, FormHelperText } from '@invoke-ai/ui-library';
+import { InputFieldRenderer } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
+import { NodeFieldElementLabel } from 'features/nodes/components/sidePanel/builder/NodeFieldElementLabel';
+import { useInputFieldDescription } from 'features/nodes/hooks/useInputFieldDescription';
+import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
+import type { NodeFieldElement } from 'features/nodes/types/workflow';
+import { NODE_FIELD_CLASS_NAME } from 'features/nodes/types/workflow';
+import { memo, useMemo } from 'react';
+
+export const NodeFieldElementViewMode = memo(({ el }: { el: NodeFieldElement }) => {
+ const { id, data } = el;
+ const { fieldIdentifier, showDescription } = data;
+ const description = useInputFieldDescription(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
+ const fieldTemplate = useInputFieldTemplate(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
+
+ const _description = useMemo(
+ () => description || fieldTemplate.description,
+ [description, fieldTemplate.description]
+ );
+
+ return (
+
+
+
+
+
+
+ {showDescription && _description && {_description}}
+
+
+ );
+});
+NodeFieldElementViewMode.displayName = 'NodeFieldElementViewMode';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElement.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElement.tsx
new file mode 100644
index 0000000000..7f0dd15343
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElement.tsx
@@ -0,0 +1,23 @@
+import { useAppSelector } from 'app/store/storeHooks';
+import { TextElementEditMode } from 'features/nodes/components/sidePanel/builder/TextElementEditMode';
+import { TextElementViewMode } from 'features/nodes/components/sidePanel/builder/TextElementViewMode';
+import { selectWorkflowMode, useElement } from 'features/nodes/store/workflowSlice';
+import { isTextElement } from 'features/nodes/types/workflow';
+import { memo } from 'react';
+
+export const TextElement = memo(({ id }: { id: string }) => {
+ const el = useElement(id);
+ const mode = useAppSelector(selectWorkflowMode);
+
+ if (!el || !isTextElement(el)) {
+ return null;
+ }
+
+ if (mode === 'view') {
+ return ;
+ }
+
+ // mode === 'edit'
+ return ;
+});
+TextElement.displayName = 'TextElement';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementComponent.tsx
deleted file mode 100644
index e0adba3096..0000000000
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementComponent.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import type { SystemStyleObject, TextProps } from '@invoke-ai/ui-library';
-import { Flex, Text } from '@invoke-ai/ui-library';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { useEditable } from 'common/hooks/useEditable';
-import { AutosizeTextarea } from 'features/nodes/components/sidePanel/builder/AutosizeTextarea';
-import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
-import { formElementTextDataChanged, selectWorkflowMode, useElement } from 'features/nodes/store/workflowSlice';
-import type { TextElement } from 'features/nodes/types/workflow';
-import { isTextElement, TEXT_CLASS_NAME } from 'features/nodes/types/workflow';
-import { memo, useCallback, useRef } from 'react';
-import { useTranslation } from 'react-i18next';
-
-export const TextElementComponent = memo(({ id }: { id: string }) => {
- const el = useElement(id);
- const mode = useAppSelector(selectWorkflowMode);
-
- if (!el || !isTextElement(el)) {
- return null;
- }
-
- if (mode === 'view') {
- return ;
- }
-
- // mode === 'edit'
- return ;
-});
-TextElementComponent.displayName = 'TextElementComponent';
-
-const TextElementComponentViewMode = memo(({ el }: { el: TextElement }) => {
- const { id, data } = el;
- const { content } = data;
-
- return (
-
-
-
- );
-});
-TextElementComponentViewMode.displayName = 'TextElementComponentViewMode';
-
-const textSx: SystemStyleObject = {
- fontSize: 'md',
- overflowWrap: 'anywhere',
- '&[data-is-empty="true"]': {
- opacity: 0.3,
- },
-};
-
-const TextContentDisplay = memo(({ content, ...rest }: { content: string } & TextProps) => {
- const { t } = useTranslation();
- return (
-
- {content || t('workflows.builder.textPlaceholder')}
-
- );
-});
-TextContentDisplay.displayName = 'TextContentDisplay';
-
-const TextElementComponentEditMode = memo(({ el }: { el: TextElement }) => {
- const { id } = el;
-
- return (
-
-
-
-
-
- );
-});
-TextElementComponentEditMode.displayName = 'TextElementComponentEditMode';
-
-const EditableText = memo(({ el }: { el: TextElement }) => {
- const { t } = useTranslation();
- const dispatch = useAppDispatch();
- const { id, data } = el;
- const { content } = data;
- const ref = useRef(null);
-
- const onChange = useCallback(
- (content: string) => {
- dispatch(formElementTextDataChanged({ id, changes: { content } }));
- },
- [dispatch, id]
- );
-
- const editable = useEditable({
- value: content,
- defaultValue: '',
- onChange,
- inputRef: ref,
- });
-
- if (!editable.isEditing) {
- return ;
- }
-
- return (
-
- );
-});
-
-EditableText.displayName = 'EditableText';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementContent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementContent.tsx
new file mode 100644
index 0000000000..d7fd148938
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementContent.tsx
@@ -0,0 +1,23 @@
+import type { SystemStyleObject, TextProps } from '@invoke-ai/ui-library';
+import { Text } from '@invoke-ai/ui-library';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+export const textSx: SystemStyleObject = {
+ fontSize: 'md',
+ overflowWrap: 'anywhere',
+ '&[data-is-empty="true"]': {
+ opacity: 0.3,
+ },
+};
+
+export const TextElementContent = memo(({ content, ...rest }: { content: string } & TextProps) => {
+ const { t } = useTranslation();
+ return (
+
+ {content || t('workflows.builder.textPlaceholder')}
+
+ );
+});
+
+TextElementContent.displayName = 'TextElementContent';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementContentEditable.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementContentEditable.tsx
new file mode 100644
index 0000000000..e624e03fec
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementContentEditable.tsx
@@ -0,0 +1,52 @@
+import { useAppDispatch } from 'app/store/storeHooks';
+import { useEditable } from 'common/hooks/useEditable';
+import { AutosizeTextarea } from 'features/nodes/components/sidePanel/builder/AutosizeTextarea';
+import { TextElementContent } from 'features/nodes/components/sidePanel/builder/TextElementContent';
+import { formElementTextDataChanged } from 'features/nodes/store/workflowSlice';
+import type { TextElement } from 'features/nodes/types/workflow';
+import { memo, useCallback, useRef } from 'react';
+import { useTranslation } from 'react-i18next';
+
+export const TextElementContentEditable = memo(({ el }: { el: TextElement }) => {
+ const { t } = useTranslation();
+ const dispatch = useAppDispatch();
+ const { id, data } = el;
+ const { content } = data;
+ const ref = useRef(null);
+
+ const onChange = useCallback(
+ (content: string) => {
+ dispatch(formElementTextDataChanged({ id, changes: { content } }));
+ },
+ [dispatch, id]
+ );
+
+ const editable = useEditable({
+ value: content,
+ defaultValue: '',
+ onChange,
+ inputRef: ref,
+ });
+
+ if (!editable.isEditing) {
+ return ;
+ }
+
+ return (
+
+ );
+});
+
+TextElementContentEditable.displayName = 'TextElementContentEditable';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementEditMode.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementEditMode.tsx
new file mode 100644
index 0000000000..2fa7be9fcc
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementEditMode.tsx
@@ -0,0 +1,44 @@
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
+import { Flex } from '@invoke-ai/ui-library';
+import { useContainerContext } from 'features/nodes/components/sidePanel/builder/contexts';
+import { useFormElementDnd } from 'features/nodes/components/sidePanel/builder/dnd-hooks';
+import { DndListDropIndicator } from 'features/nodes/components/sidePanel/builder/DndListDropIndicator';
+import { FormElementEditModeContent } from 'features/nodes/components/sidePanel/builder/FormElementEditModeContent';
+import { FormElementEditModeHeader } from 'features/nodes/components/sidePanel/builder/FormElementEditModeHeader';
+import { TEXT_CLASS_NAME, type TextElement } from 'features/nodes/types/workflow';
+import { memo, useRef } from 'react';
+
+import { TextElementContentEditable } from './TextElementContentEditable';
+
+export const sx: SystemStyleObject = {
+ position: 'relative',
+ borderRadius: 'base',
+ '&[data-parent-layout="column"]': {
+ w: 'full',
+ h: 'min-content',
+ },
+ '&[data-parent-layout="row"]': {
+ flex: '1 0 0',
+ },
+ flexDir: 'column',
+};
+
+export const TextElementEditMode = memo(({ el }: { el: TextElement }) => {
+ const draggableRef = useRef(null);
+ const dragHandleRef = useRef(null);
+ const [activeDropRegion, isDragging] = useFormElementDnd(el.id, draggableRef, dragHandleRef);
+ const containerCtx = useContainerContext();
+ const { id } = el;
+
+ return (
+
+
+
+
+
+
+
+ );
+});
+
+TextElementEditMode.displayName = 'TextElementEditMode';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementViewMode.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementViewMode.tsx
new file mode 100644
index 0000000000..869dc67f65
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementViewMode.tsx
@@ -0,0 +1,17 @@
+import { Flex } from '@invoke-ai/ui-library';
+import { TextElementContent } from 'features/nodes/components/sidePanel/builder/TextElementContent';
+import { TEXT_CLASS_NAME, type TextElement } from 'features/nodes/types/workflow';
+import { memo } from 'react';
+
+export const TextElementViewMode = memo(({ el }: { el: TextElement }) => {
+ const { id, data } = el;
+ const { content } = data;
+
+ return (
+
+
+
+ );
+});
+
+TextElementViewMode.displayName = 'TextElementViewMode';
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/WorkflowBuilder.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/WorkflowBuilder.tsx
index 667ca40983..bd955b7918 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/WorkflowBuilder.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/WorkflowBuilder.tsx
@@ -7,11 +7,11 @@ import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { firefoxDndFix } from 'features/dnd/util';
-import { FormElementComponent } from 'features/nodes/components/sidePanel/builder/ContainerElementComponent';
+import { RootContainerElementEditMode } from 'features/nodes/components/sidePanel/builder/ContainerElement';
import { buildFormElementDndData, useBuilderDndMonitor } from 'features/nodes/components/sidePanel/builder/dnd-hooks';
import { WorkflowBuilderEditMenu } from 'features/nodes/components/sidePanel/builder/WorkflowBuilderMenu';
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
-import { selectFormRootElementId, selectIsFormEmpty } from 'features/nodes/store/workflowSlice';
+import { selectIsFormEmpty } from 'features/nodes/store/workflowSlice';
import type { FormElement } from 'features/nodes/types/workflow';
import { buildContainer, buildDivider, buildHeading, buildText } from 'features/nodes/types/workflow';
import { startCase } from 'lodash-es';
@@ -60,7 +60,6 @@ WorkflowBuilder.displayName = 'WorkflowBuilder';
const WorkflowBuilderContent = memo(() => {
const { t } = useTranslation();
- const rootElementId = useAppSelector(selectFormRootElementId);
const isFormEmpty = useAppSelector(selectIsFormEmpty);
const openApiSchemaQuery = useGetOpenAPISchemaQuery();
const loadedTemplates = useStore($hasTemplates);
@@ -71,7 +70,7 @@ const WorkflowBuilderContent = memo(() => {
return (
-
+
);
});
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/contexts.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/contexts.tsx
index b93192b31b..f305686327 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/contexts.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/contexts.tsx
@@ -1,6 +1,7 @@
import type { ContainerElement, ElementId } from 'features/nodes/types/workflow';
import type { PropsWithChildren } from 'react';
import { createContext, memo, useContext, useMemo } from 'react';
+import { assert } from 'tsafe';
type ContainerContextValue = {
id: ElementId;
@@ -17,6 +18,10 @@ ContainerContextProvider.displayName = 'ContainerContextProvider';
export const useContainerContext = () => {
const container = useContext(ContainerContext);
+ assert(
+ container !== null,
+ 'useContainerContext must be used inside a ContainerContextProvider and cannot be used by the root container'
+ );
return container;
};
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 f70061dd93..e46ff4f2df 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
@@ -25,7 +25,6 @@ import {
getElement,
getInitialValue,
} from 'features/nodes/components/sidePanel/builder/form-manipulation';
-import { getEditModeWrapperId } from 'features/nodes/components/sidePanel/builder/shared';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import {
formElementAdded,
@@ -64,7 +63,7 @@ const isFormElementDndData = (data: Record): data is F
* @param elementId The id of the element to flash
*/
const flashElement = (elementId: ElementId) => {
- const element = document.querySelector(`#${getEditModeWrapperId(elementId)}`);
+ const element = document.querySelector(`#${elementId}`);
if (element instanceof HTMLElement) {
triggerPostMoveFlash(element, colorTokenToCssVar('base.800'));
}
@@ -358,7 +357,8 @@ export const useFormElementDnd = (
}),
dropTargetForElements({
element: draggableElement,
- getIsSticky: () => true,
+ // TODO(psyche): This causes a kinda jittery behaviour - need a better heuristic to determine stickiness
+ getIsSticky: () => false,
canDrop: ({ source }) =>
isFormElementDndData(source.data) && source.data.element.id !== getElement(elementId).parentId,
getData: ({ input }) => {
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/shared.ts b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/shared.ts
index 358749fcfd..88d240e67c 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/shared.ts
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/shared.ts
@@ -1,2 +1,23 @@
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
+
// This must be in this file to avoid circular dependencies
export const getEditModeWrapperId = (id: string) => `${id}-edit-mode-wrapper`;
+
+export const formElementDndSx: SystemStyleObject = {
+ '&[data-is-dragging="true"]': {
+ opacity: 0.3,
+ },
+ '&[data-active-drop-region="center"]': {
+ opacity: 1,
+ bg: 'base.850',
+ },
+};
+
+export const formElementIsDraggingSx: SystemStyleObject = {
+ opacity: 0.3,
+};
+
+export const formElementIsActiveDropRegionSx: SystemStyleObject = {
+ opacity: 1,
+ bg: 'base.850',
+};
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/ViewModeLeftPanelContent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/ViewModeLeftPanelContent.tsx
index 4c9628dbc2..b430293ded 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/ViewModeLeftPanelContent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/ViewModeLeftPanelContent.tsx
@@ -3,10 +3,10 @@ import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
-import { FormElementComponent } from 'features/nodes/components/sidePanel/builder/ContainerElementComponent';
+import { RootContainerElementViewMode } from 'features/nodes/components/sidePanel/builder/ContainerElement';
import { EmptyState } from 'features/nodes/components/sidePanel/viewMode/EmptyState';
import { $hasTemplates } from 'features/nodes/store/nodesSlice';
-import { selectFormRootElementId, selectIsFormEmpty } from 'features/nodes/store/workflowSlice';
+import { selectIsFormEmpty } from 'features/nodes/store/workflowSlice';
import { t } from 'i18next';
import { memo } from 'react';
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
@@ -25,7 +25,6 @@ ViewModeLeftPanelContent.displayName = 'ViewModeLeftPanelContent';
const ViewModeLeftPanelContentInner = memo(() => {
const { isLoading } = useGetOpenAPISchemaQuery();
const loadedTemplates = useStore($hasTemplates);
- const rootElementId = useAppSelector(selectFormRootElementId);
const isFormEmpty = useAppSelector(selectIsFormEmpty);
if (isLoading || !loadedTemplates) {
@@ -37,8 +36,8 @@ const ViewModeLeftPanelContentInner = memo(() => {
}
return (
-
-
+
+
);
});
diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts
index 40cd51e0fc..7dce3bfb26 100644
--- a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts
@@ -5,6 +5,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { deepClone } from 'common/util/deepClone';
import {
addElement,
+ getElement,
removeElement,
reparentElement,
} from 'features/nodes/components/sidePanel/builder/form-manipulation';
@@ -377,6 +378,9 @@ export const selectCleanEditor = createSelector([selectNodesSlice, selectWorkflo
export const selectFormRootElementId = createWorkflowSelector((workflow) => {
return workflow.form.rootElementId;
});
+export const selectFormRootElement = createWorkflowSelector((workflow) => {
+ return getElement(workflow.form, workflow.form.rootElementId, isContainerElement);
+});
export const selectIsFormEmpty = createWorkflowSelector((workflow) => {
const rootElement = workflow.form.elements[workflow.form.rootElementId];
if (!rootElement || !isContainerElement(rootElement)) {
diff --git a/invokeai/frontend/web/src/features/nodes/types/workflow.ts b/invokeai/frontend/web/src/features/nodes/types/workflow.ts
index 441886f4d0..c70df57cde 100644
--- a/invokeai/frontend/web/src/features/nodes/types/workflow.ts
+++ b/invokeai/frontend/web/src/features/nodes/types/workflow.ts
@@ -70,7 +70,7 @@ const zElementBase = z.object({
export const zNumberComponent = z.enum(['number-input', 'slider', 'number-input-and-slider']);
const NODE_FIELD_TYPE = 'node-field';
-export const NODE_FIELD_CLASS_NAME = getPrefixedId(NODE_FIELD_TYPE, '-');
+export const NODE_FIELD_CLASS_NAME = `form-builder-${NODE_FIELD_TYPE}`;
const FLOAT_FIELD_SETTINGS_TYPE = 'float-field-config';
const zNodeFieldFloatSettings = z.object({
type: z.literal(FLOAT_FIELD_SETTINGS_TYPE).default(FLOAT_FIELD_SETTINGS_TYPE),
@@ -141,7 +141,7 @@ export const buildNodeFieldElement = (
};
const HEADING_TYPE = 'heading';
-export const HEADING_CLASS_NAME = getPrefixedId(HEADING_TYPE, '-');
+export const HEADING_CLASS_NAME = `form-builder-${HEADING_TYPE}`;
const zHeadingElement = zElementBase.extend({
type: z.literal(HEADING_TYPE),
data: z.object({ content: z.string() }),
@@ -162,7 +162,7 @@ export const buildHeading = (
};
const TEXT_TYPE = 'text';
-export const TEXT_CLASS_NAME = getPrefixedId(TEXT_TYPE, '-');
+export const TEXT_CLASS_NAME = `form-builder-${TEXT_TYPE}`;
const zTextElement = zElementBase.extend({
type: z.literal(TEXT_TYPE),
data: z.object({ content: z.string() }),
@@ -183,7 +183,7 @@ export const buildText = (
};
const DIVIDER_TYPE = 'divider';
-export const DIVIDER_CLASS_NAME = getPrefixedId(DIVIDER_TYPE, '-');
+export const DIVIDER_CLASS_NAME = `form-builder-${DIVIDER_TYPE}`;
const zDividerElement = zElementBase.extend({
type: z.literal(DIVIDER_TYPE),
});
@@ -199,7 +199,8 @@ export const buildDivider = (parentId?: NodeFieldElement['parentId']): DividerEl
};
const CONTAINER_TYPE = 'container';
-export const CONTAINER_CLASS_NAME = getPrefixedId(CONTAINER_TYPE, '-');
+export const CONTAINER_CLASS_NAME = `form-builder-${CONTAINER_TYPE}`;
+export const ROOT_CONTAINER_CLASS_NAME = `form-builder-root-${CONTAINER_TYPE}`;
const zContainerElement = zElementBase.extend({
type: z.literal(CONTAINER_TYPE),
data: z.object({