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. *