mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-07 08:54:58 -05:00
refactor(ui): styling for form edit mode (wip)
This commit is contained in:
@@ -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 <RootContainerElementComponentViewMode el={el} />;
|
||||
}
|
||||
|
||||
if (isRootElement && mode === 'edit') {
|
||||
return <RootContainerElementComponentEditMode el={el} />;
|
||||
}
|
||||
|
||||
if (mode === 'view') {
|
||||
return <ContainerElementComponentViewMode el={el} />;
|
||||
}
|
||||
@@ -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 (
|
||||
<DepthContextProvider depth={0}>
|
||||
<ContainerContextProvider id={id} layout={layout}>
|
||||
<Box id={id} className={CONTAINER_CLASS_NAME} sx={rootViewModeSx} data-container-layout={layout} data-depth={0}>
|
||||
{children.map((childId) => (
|
||||
<FormElementComponent key={childId} id={childId} />
|
||||
))}
|
||||
</Box>
|
||||
</ContainerContextProvider>
|
||||
</DepthContextProvider>
|
||||
);
|
||||
});
|
||||
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<HTMLDivElement>(null);
|
||||
const isDraggingOver = useRootElementDropTarget(ref);
|
||||
|
||||
return (
|
||||
<DepthContextProvider depth={0}>
|
||||
<ContainerContextProvider id={id} layout={layout}>
|
||||
<Flex
|
||||
ref={ref}
|
||||
id={id}
|
||||
className={CONTAINER_CLASS_NAME}
|
||||
sx={rootEditModeSx}
|
||||
data-container-layout={layout}
|
||||
data-depth={0}
|
||||
data-is-dragging-over={isDraggingOver}
|
||||
>
|
||||
{children.map((childId) => (
|
||||
<FormElementComponent key={childId} id={childId} />
|
||||
))}
|
||||
{children.length === 0 && <RootPlaceholder />}
|
||||
</Flex>
|
||||
</ContainerContextProvider>
|
||||
</DepthContextProvider>
|
||||
);
|
||||
});
|
||||
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 (
|
||||
<FormElementEditModeWrapper element={el}>
|
||||
@@ -97,8 +175,7 @@ const ContainerElementComponentEditMode = memo(({ el }: { el: ContainerElement }
|
||||
{children.map((childId) => (
|
||||
<FormElementComponent key={childId} id={childId} />
|
||||
))}
|
||||
{children.length === 0 && isRootElement && <RootPlaceholder />}
|
||||
{children.length === 0 && !isRootElement && <NonRootPlaceholder />}
|
||||
{children.length === 0 && <NonRootPlaceholder />}
|
||||
</Flex>
|
||||
</ContainerContextProvider>
|
||||
</DepthContextProvider>
|
||||
|
||||
@@ -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 (
|
||||
<Flex
|
||||
@@ -79,7 +71,6 @@ export const FormElementEditModeWrapper = memo(({ element, children }: PropsWith
|
||||
ref={draggableRef}
|
||||
className={EDIT_MODE_WRAPPER_CLASS_NAME}
|
||||
sx={wrapperSx}
|
||||
data-is-root={isRootElement}
|
||||
data-element-type={element.type}
|
||||
data-layout={containerCtx?.layout}
|
||||
>
|
||||
@@ -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
|
||||
<>
|
||||
<FormElementEditModeHeader ref={dragHandleRef} element={element} />
|
||||
<Flex sx={contentWrapperSx} data-depth={depth}>
|
||||
{children}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
{isRootElement && (
|
||||
// But the root does not - helps the builder to look less busy
|
||||
<Flex ref={dragHandleRef} w="full" h="full">
|
||||
{children}
|
||||
</Flex>
|
||||
)}
|
||||
<FormElementEditModeHeader ref={dragHandleRef} element={element} />
|
||||
<Flex sx={contentWrapperSx} data-depth={depth}>
|
||||
{children}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<DndListDropIndicator activeDropRegion={activeDropRegion} gap="var(--invoke-space-4)" />
|
||||
</Flex>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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<HTMLDivElement>) => {
|
||||
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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user