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 ( - - - ); - }) -); +export const FormElementEditModeHeader = memo(({ element, dragHandleRef, ...rest }: Props) => { + const depth = useDepthContext(); + + return ( + + + ); +}); 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 ( -