diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/ContainerElementComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/ContainerElementComponent.tsx
index 4333b5beff..e00b4416c0 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/ContainerElementComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/ContainerElementComponent.tsx
@@ -1,5 +1,5 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
-import { Flex, Text } from '@invoke-ai/ui-library';
+import { Box, Flex, Text } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import {
ContainerContextProvider,
@@ -7,7 +7,7 @@ import {
useDepthContext,
} from 'features/nodes/components/sidePanel/builder/contexts';
import { DividerElementComponent } from 'features/nodes/components/sidePanel/builder/DividerElementComponent';
-import { useIsRootElement } from 'features/nodes/components/sidePanel/builder/dnd-hooks';
+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';
@@ -22,33 +22,28 @@ import {
isNodeFieldElement,
isTextElement,
} from 'features/nodes/types/workflow';
-import { memo } from 'react';
+import { memo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
-const sx: SystemStyleObject = {
- gap: 4,
- flex: '1 1 0',
- '&[data-depth="0"]': {
- flex: 1,
- },
- '&[data-container-layout="column"]': {
- flexDir: 'column',
- },
- '&[data-container-layout="row"]': {
- flexDir: 'row',
- },
-};
-
const ContainerElementComponent = 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 ;
}
@@ -58,6 +53,90 @@ const ContainerElementComponent = memo(({ id }: { id: string }) => {
});
ContainerElementComponent.displayName = 'ContainerElementComponent';
+const rootViewModeSx: SystemStyleObject = {
+ position: 'relative',
+ alignItems: 'center',
+ borderRadius: 'base',
+ w: 'full',
+ h: 'full',
+ gap: 4,
+ display: 'flex',
+ flex: 1,
+ '&[data-container-layout="column"]': {
+ flexDir: 'column',
+ 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',
+ },
+};
+
const ContainerElementComponentViewMode = memo(({ el }: { el: ContainerElement }) => {
const { t } = useTranslation();
const depth = useDepthContext();
@@ -87,7 +166,6 @@ const ContainerElementComponentEditMode = memo(({ el }: { el: ContainerElement }
const depth = useDepthContext();
const { id, data } = el;
const { children, layout } = data;
- const isRootElement = useIsRootElement(id);
return (
@@ -97,8 +175,7 @@ const ContainerElementComponentEditMode = memo(({ el }: { el: ContainerElement }
{children.map((childId) => (
))}
- {children.length === 0 && isRootElement && }
- {children.length === 0 && !isRootElement && }
+ {children.length === 0 && }
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
index 6ee5734872..6be170c8a1 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeWrapper.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeWrapper.tsx
@@ -1,6 +1,5 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
-import { getPrefixedId } from 'features/controlLayers/konva/util';
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';
@@ -10,9 +9,7 @@ import type { FormElement } from 'features/nodes/types/workflow';
import type { PropsWithChildren } from 'react';
import { memo, useRef } from 'react';
-import { useIsRootElement } from './dnd-hooks';
-
-const EDIT_MODE_WRAPPER_CLASS_NAME = getPrefixedId('edit-mode-wrapper', '-');
+export const EDIT_MODE_WRAPPER_CLASS_NAME = 'edit-mode-wrapper';
const wrapperSx: SystemStyleObject = {
position: 'relative',
@@ -20,10 +17,6 @@ const wrapperSx: SystemStyleObject = {
'&[data-element-type="divider"]&[data-layout="row"]': {
flex: '0 1 0',
},
- '&[data-is-root="true"]': {
- w: 'full',
- h: 'full',
- },
borderRadius: 'base',
};
@@ -71,7 +64,6 @@ export const FormElementEditModeWrapper = memo(({ element, children }: PropsWith
const [activeDropRegion, isDragging] = useFormElementDnd(element.id, draggableRef, dragHandleRef);
const containerCtx = useContainerContext();
const depth = useDepthContext();
- const isRootElement = useIsRootElement(element.id);
return (
@@ -90,21 +81,10 @@ export const FormElementEditModeWrapper = memo(({ element, children }: PropsWith
data-element-type={element.type}
data-layout={containerCtx?.layout}
>
- {!isRootElement && (
- // Non-root elements get the header and content wrapper
- <>
-
-
- {children}
-
- >
- )}
- {isRootElement && (
- // But the root does not - helps the builder to look less busy
-
- {children}
-
- )}
+
+
+ {children}
+
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 b336320f37..667ca40983 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
@@ -23,9 +23,9 @@ import { assert } from 'tsafe';
const sx: SystemStyleObject = {
pt: 3,
+ w: 'full',
+ h: 'full',
'&[data-is-empty="true"]': {
- w: 'full',
- h: 'full',
pt: 0,
},
};
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 63761d2da5..f70061dd93 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
@@ -41,6 +41,7 @@ import type { RefObject } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { flushSync } from 'react-dom';
import type { Param0 } from 'tsafe';
+import { assert } from 'tsafe';
const log = logger('dnd');
@@ -329,6 +330,9 @@ export const useFormElementDnd = (
const getAllowedDropRegions = useGetAllowedDropRegions();
useEffect(() => {
+ if (isRootElement) {
+ assert(false, 'Root element should not be draggable');
+ }
const draggableElement = draggableRef.current;
const dragHandleElement = dragHandleRef.current;
@@ -339,8 +343,6 @@ export const useFormElementDnd = (
return combine(
firefoxDndFix(draggableElement),
draggable({
- // Don't allow dragging the root element
- canDrag: () => !isRootElement,
element: draggableElement,
dragHandle: dragHandleElement,
getInitialData: () => {
@@ -356,7 +358,7 @@ export const useFormElementDnd = (
}),
dropTargetForElements({
element: draggableElement,
- getIsSticky: () => !isRootElement,
+ getIsSticky: () => true,
canDrop: ({ source }) =>
isFormElementDndData(source.data) && source.data.element.id !== getElement(elementId).parentId,
getData: ({ input }) => {
@@ -404,6 +406,52 @@ export const useFormElementDnd = (
return [activeDropRegion, isDragging] as const;
};
+export const useRootElementDropTarget = (droppableRef: RefObject) => {
+ const [isDraggingOver, setIsDraggingOver] = useState(false);
+ const getElement = useGetElement();
+ const getAllowedDropRegions = useGetAllowedDropRegions();
+ const rootElementId = useAppSelector(selectFormRootElementId);
+
+ useEffect(() => {
+ const droppableElement = droppableRef.current;
+
+ if (!droppableElement) {
+ return;
+ }
+
+ return combine(
+ dropTargetForElements({
+ element: droppableElement,
+ getIsSticky: () => false,
+ canDrop: ({ source }) =>
+ getElement(rootElementId, isContainerElement).data.children.length === 0 && isFormElementDndData(source.data),
+ getData: ({ input }) => {
+ const element = getElement(rootElementId, isContainerElement);
+
+ const targetData = buildFormElementDndData(element);
+
+ return attachClosestCenterOrEdge(targetData, {
+ element: droppableElement,
+ input,
+ allowedCenterOrEdge: ['center'],
+ });
+ },
+ onDrag: () => {
+ setIsDraggingOver(true);
+ },
+ onDragLeave: () => {
+ setIsDraggingOver(false);
+ },
+ onDrop: () => {
+ setIsDraggingOver(false);
+ },
+ })
+ );
+ }, [droppableRef, getAllowedDropRegions, getElement, rootElementId]);
+
+ return isDraggingOver;
+};
+
/**
* Hook that provides dnd functionality for node fields.
*