refactor(ui): styling for form edit mode (maybe done?)

- Restructure components
- Let each element render its own edit mode
- arrrrghh
This commit is contained in:
psychedelicious
2025-02-25 16:15:01 +10:00
parent 7591adebd5
commit 42c4462edc
34 changed files with 985 additions and 738 deletions

View File

@@ -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 <RootContainerElementComponentViewMode el={el} />;
}
if (isRootElement && mode === 'edit') {
return <RootContainerElementComponentEditMode el={el} />;
}
if (mode === 'view') {
return <ContainerElementComponentViewMode el={el} />;
}
@@ -51,89 +47,21 @@ const ContainerElementComponent = memo(({ id }: { id: string }) => {
// mode === 'edit'
return <ContainerElementComponentEditMode el={el} />;
});
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 (
<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',
overflowX: 'auto',
overflowY: 'visible',
h: 'min-content',
},
};
@@ -146,7 +74,13 @@ const ContainerElementComponentViewMode = memo(({ el }: { el: ContainerElement }
return (
<DepthContextProvider depth={depth + 1}>
<ContainerContextProvider id={id} layout={layout}>
<Flex id={id} className={CONTAINER_CLASS_NAME} sx={sx} data-container-layout={layout} data-depth={depth}>
<Flex
id={id}
className={CONTAINER_CLASS_NAME}
sx={containerViewModeSx}
data-self-layout={layout}
data-depth={depth}
>
{children.map((childId) => (
<FormElementComponent key={childId} id={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<HTMLDivElement>(null);
const dragHandleRef = useRef<HTMLDivElement>(null);
const autoScrollRef = useRef<HTMLDivElement>(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 (
<DepthContextProvider depth={depth + 1}>
<ContainerContextProvider id={id} layout={layout}>
<Flex
id={id}
ref={draggableRef}
className={CONTAINER_CLASS_NAME}
sx={containerEditModeSx}
data-depth={depth}
data-parent-layout={containerCtx.layout}
data-active-drop-region={activeDropRegion}
>
<FormElementEditModeHeader dragHandleRef={dragHandleRef} element={el} data-is-dragging={isDragging} />
<FormElementEditModeContent data-is-dragging={isDragging}>
<Flex ref={autoScrollRef} sx={containerEditModeContentSx} data-self-layout={layout}>
{children.map((childId) => (
<FormElementComponent key={childId} id={childId} />
))}
{children.length === 0 && <NonRootPlaceholder />}
</Flex>
</FormElementEditModeContent>
<DndListDropIndicator activeDropRegion={activeDropRegion} gap="var(--invoke-space-4)" />
</Flex>
</ContainerContextProvider>
</DepthContextProvider>
);
});
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 (
<FormElementEditModeWrapper element={el}>
<DepthContextProvider depth={depth + 1}>
<ContainerContextProvider id={id} layout={layout}>
<Flex id={id} className={CONTAINER_CLASS_NAME} sx={sx} data-container-layout={layout} data-depth={depth}>
{children.map((childId) => (
<FormElementComponent key={childId} id={childId} />
))}
{children.length === 0 && <NonRootPlaceholder />}
</Flex>
</ContainerContextProvider>
</DepthContextProvider>
</FormElementEditModeWrapper>
<DepthContextProvider depth={0}>
<ContainerContextProvider id={id} layout={layout}>
<Box id={id} className={ROOT_CONTAINER_CLASS_NAME} sx={rootViewModeSx} data-self-layout={layout} data-depth={0}>
{children.map((childId) => (
<FormElementComponent key={childId} id={childId} />
))}
</Box>
</ContainerContextProvider>
</DepthContextProvider>
);
});
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<HTMLDivElement>(null);
const isDraggingOver = useRootElementDropTarget(ref);
return (
<DepthContextProvider depth={0}>
<ContainerContextProvider id={id} layout={layout}>
<Flex
ref={ref}
id={id}
className={ROOT_CONTAINER_CLASS_NAME}
sx={rootEditModeSx}
data-self-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>
);
});
RootContainerElementEditMode.displayName = 'RootContainerElementEditMode';
const RootPlaceholder = memo(() => {
const { t } = useTranslation();
@@ -213,23 +283,23 @@ export const FormElementComponent = memo(({ id }: { id: string }) => {
}
if (isContainerElement(el)) {
return <ContainerElementComponent key={id} id={id} />;
return <ContainerElement key={id} id={id} />;
}
if (isNodeFieldElement(el)) {
return <NodeFieldElementComponent key={id} id={id} />;
return <NodeFieldElement key={id} id={id} />;
}
if (isDividerElement(el)) {
return <DividerElementComponent key={id} id={id} />;
return <DividerElement key={id} id={id} />;
}
if (isHeadingElement(el)) {
return <HeadingElementComponent key={id} id={id} />;
return <HeadingElement key={id} id={id} />;
}
if (isTextElement(el)) {
return <TextElementComponent key={id} id={id} />;
return <TextElement key={id} id={id} />;
}
assert<Equals<typeof el, never>>(false, `Unhandled type for element with id ${id}`);

View File

@@ -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 <DividerElementViewMode el={el} />;
}
// mode === 'edit'
return <DividerElementEditMode el={el} />;
});
DividerElement.displayName = 'DividerElement';

View File

@@ -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 <DividerElementComponentViewMode el={el} />;
}
// mode === 'edit'
return <DividerElementComponentEditMode el={el} />;
return <Flex sx={sx} data-parent-layout={containerCtx.layout} />;
});
DividerElementComponent.displayName = 'DividerElementComponent';
const DividerElementComponentViewMode = memo(({ el }: { el: DividerElement }) => {
const container = useContainerContext();
const { id } = el;
return (
<Flex
id={id}
className={DIVIDER_CLASS_NAME}
sx={sx}
data-layout={
// When there is no container, the layout is column by default
container?.layout || 'column'
}
/>
);
});
DividerElementComponentViewMode.displayName = 'DividerElementComponentViewMode';
const DividerElementComponentEditMode = memo(({ el }: { el: DividerElement }) => {
const container = useContainerContext();
const { id } = el;
return (
<FormElementEditModeWrapper element={el}>
<Flex
id={id}
className={DIVIDER_CLASS_NAME}
sx={sx}
data-layout={
// When there is no container, the layout is column by default
container?.layout || 'column'
}
/>
</FormElementEditModeWrapper>
);
});
DividerElementComponentEditMode.displayName = 'DividerElementComponentEditMode';

View File

@@ -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<HTMLDivElement>(null);
const dragHandleRef = useRef<HTMLDivElement>(null);
const [activeDropRegion, isDragging] = useFormElementDnd(el.id, draggableRef, dragHandleRef);
const containerCtx = useContainerContext();
const { id } = el;
return (
<Flex ref={draggableRef} id={id} className={DIVIDER_CLASS_NAME} sx={sx} data-parent-layout={containerCtx.layout}>
<FormElementEditModeHeader dragHandleRef={dragHandleRef} element={el} data-is-dragging={isDragging} />
<FormElementEditModeContent data-is-dragging={isDragging} p={4}>
<DividerElementComponent />
</FormElementEditModeContent>
<DndListDropIndicator activeDropRegion={activeDropRegion} gap="var(--invoke-space-4)" />
</Flex>
);
});
DividerElementEditMode.displayName = 'DividerElementEditMode';

View File

@@ -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 (
<Flex
id={id}
className={DIVIDER_CLASS_NAME}
sx={sx}
data-layout={
// When there is no container, the layout is column by default
container?.layout || 'column'
}
/>
);
});
DividerElementViewMode.displayName = 'DividerElementViewMode';

View File

@@ -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 (
<Flex sx={contentWrapperSx} data-depth={depth} {...rest}>
{children}
</Flex>
);
});
FormElementEditModeContent.displayName = 'FormElementEditModeContent';

View File

@@ -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<FlexProps, 'sx'> & { element: FormElement; dragHandleRef: RefObject<HTMLDivElement> };
return (
<Flex ref={ref} sx={sx} data-depth={depth}>
<Label element={element} />
<Spacer />
{isContainerElement(element) && <ContainerElementSettings element={element} />}
{isNodeFieldElement(element) && <ZoomToNodeButton element={element} />}
{isNodeFieldElement(element) && <NodeFieldElementSettings element={element} />}
<RemoveElementButton element={element} />
</Flex>
);
})
);
export const FormElementEditModeHeader = memo(({ element, dragHandleRef, ...rest }: Props) => {
const depth = useDepthContext();
return (
<Flex ref={dragHandleRef} sx={sx} data-depth={depth} {...rest}>
<Label element={element} />
<Spacer />
{isContainerElement(element) && <ContainerElementSettings element={element} />}
{isNodeFieldElement(element) && <ZoomToNodeButton element={element} />}
{isNodeFieldElement(element) && <NodeFieldElementSettings element={element} />}
<RemoveElementButton element={element} />
</Flex>
);
});
FormElementEditModeHeader.displayName = 'FormElementEditModeHeader';
const ZoomToNodeButton = memo(({ element }: { element: NodeFieldElement }) => {

View File

@@ -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<HTMLDivElement>(null);
const dragHandleRef = useRef<HTMLDivElement>(null);
const [activeDropRegion, isDragging] = useFormElementDnd(element.id, draggableRef, dragHandleRef);
const containerCtx = useContainerContext();
const depth = useDepthContext();
return (
<Flex
id={getEditModeWrapperId(element.id)}
ref={draggableRef}
className={EDIT_MODE_WRAPPER_CLASS_NAME}
sx={wrapperSx}
data-element-type={element.type}
data-layout={containerCtx?.layout}
>
<Flex
sx={innerSx}
data-is-dragging={isDragging}
data-active-drop-region={activeDropRegion}
data-element-type={element.type}
data-layout={containerCtx?.layout}
>
<FormElementEditModeHeader ref={dragHandleRef} element={element} />
<Flex sx={contentWrapperSx} data-depth={depth}>
{children}
</Flex>
</Flex>
<DndListDropIndicator activeDropRegion={activeDropRegion} gap="var(--invoke-space-4)" />
</Flex>
);
});
FormElementEditModeWrapper.displayName = 'FormElementEditModeWrapper';

View File

@@ -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 <HeadingElementViewMode el={el} />;
}
// mode === 'edit'
return <HeadingElementEditMode el={el} />;
});
HeadingElement.displayName = 'HeadingElement';

View File

@@ -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 <HeadingElementComponentViewMode el={el} />;
}
// mode === 'edit'
return <HeadingElementComponentEditMode el={el} />;
});
HeadingElementComponent.displayName = 'HeadingElementComponent';
const HeadingElementComponentViewMode = memo(({ el }: { el: HeadingElement }) => {
const { id, data } = el;
const { content } = data;
return (
<Flex id={id} className={HEADING_CLASS_NAME}>
<HeadingContentDisplay content={content} />
</Flex>
);
});
HeadingElementComponentViewMode.displayName = 'HeadingElementComponentViewMode';
const HeadingElementComponentEditMode = memo(({ el }: { el: HeadingElement }) => {
const { id } = el;
return (
<FormElementEditModeWrapper element={el}>
<Flex id={id} className={HEADING_CLASS_NAME} w="full">
<EditableHeading el={el} />
</Flex>
</FormElementEditModeWrapper>
);
});
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 (
<Text sx={headingSx} data-is-empty={content === ''} {...rest}>
{content || t('workflows.builder.headingPlaceholder')}
</Text>
);
});
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<HTMLTextAreaElement>(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 <HeadingContentDisplay content={editable.value} onDoubleClick={editable.startEditing} />;
}
return (
<AutosizeTextarea
ref={ref}
placeholder={t('workflows.builder.headingPlaceholder')}
{...editable.inputProps}
variant="outline"
overflowWrap="anywhere"
w="full"
minRows={1}
maxRows={10}
resize="none"
p={1}
px={2}
fontWeight="bold"
fontSize={FONT_SIZE}
_focusVisible={{ borderRadius: 'base', h: 'unset' }}
/>
);
});
EditableHeading.displayName = 'EditableHeading';

View File

@@ -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 (
<Text sx={headingSx} data-is-empty={content === ''} {...rest}>
{content || t('workflows.builder.headingPlaceholder')}
</Text>
);
});
HeadingElementContent.displayName = 'HeadingElementContent';

View File

@@ -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<HTMLTextAreaElement>(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 <HeadingElementContent content={editable.value} onDoubleClick={editable.startEditing} />;
}
return (
<AutosizeTextarea
ref={ref}
placeholder={t('workflows.builder.headingPlaceholder')}
{...editable.inputProps}
variant="outline"
overflowWrap="anywhere"
w="full"
minRows={1}
maxRows={10}
resize="none"
p={1}
px={2}
fontWeight="bold"
fontSize="2xl"
_focusVisible={{ borderRadius: 'base', h: 'unset' }}
/>
);
});
HeadingElementContentEditable.displayName = 'HeadingElementContentEditable';

View File

@@ -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<HTMLDivElement>(null);
const dragHandleRef = useRef<HTMLDivElement>(null);
const [activeDropRegion, isDragging] = useFormElementDnd(el.id, draggableRef, dragHandleRef);
const containerCtx = useContainerContext();
const { id } = el;
return (
<Flex ref={draggableRef} id={id} className={HEADING_CLASS_NAME} sx={sx} data-parent-layout={containerCtx.layout}>
<FormElementEditModeHeader dragHandleRef={dragHandleRef} element={el} data-is-dragging={isDragging} />
<FormElementEditModeContent data-is-dragging={isDragging} p={4}>
<HeadingElementContentEditable el={el} />
</FormElementEditModeContent>
<DndListDropIndicator activeDropRegion={activeDropRegion} gap="var(--invoke-space-4)" />
</Flex>
);
});
HeadingElementEditMode.displayName = 'HeadingElementEditMode';

View File

@@ -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 (
<Flex id={id} className={HEADING_CLASS_NAME} sx={sx}>
<HeadingElementContent content={content} />
</Flex>
);
});
HeadingElementViewMode.displayName = 'HeadingElementViewMode';

View File

@@ -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 (
<InputFieldGate nodeId={el.data.fieldIdentifier.nodeId} fieldName={el.data.fieldIdentifier.fieldName}>
<NodeFieldElementViewMode el={el} />
</InputFieldGate>
);
}
// mode === 'edit'
return (
<InputFieldGate nodeId={el.data.fieldIdentifier.nodeId} fieldName={el.data.fieldIdentifier.fieldName}>
<NodeFieldElementEditMode el={el} />
</InputFieldGate>
);
});
NodeFieldElement.displayName = 'NodeFieldElement';

View File

@@ -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 (
<InputFieldGate nodeId={el.data.fieldIdentifier.nodeId} fieldName={el.data.fieldIdentifier.fieldName}>
<NodeFieldElementComponentViewMode el={el} />
</InputFieldGate>
);
}
// mode === 'edit'
return (
<InputFieldGate nodeId={el.data.fieldIdentifier.nodeId} fieldName={el.data.fieldIdentifier.fieldName}>
<NodeFieldElementComponentEditMode el={el} />{' '}
</InputFieldGate>
);
});
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 (
<Flex id={id} className={NODE_FIELD_CLASS_NAME} flex={1}>
<FormControl flex="1 1 0" orientation="vertical">
<Flex w="full" gap={4}>
<FormLabel>{_label}</FormLabel>
<Spacer />
<NodeFieldElementResetToInitialValueIconButton element={el} />
</Flex>
<Flex w="full" gap={4}>
<InputFieldRenderer
nodeId={fieldIdentifier.nodeId}
fieldName={fieldIdentifier.fieldName}
settings={data.settings}
/>
</Flex>
{showDescription && _description && <FormHelperText>{_description}</FormHelperText>}
</FormControl>
</Flex>
);
});
NodeFieldElementComponentViewMode.displayName = 'NodeFieldElementComponentViewMode';
const NodeFieldElementComponentEditMode = memo(({ el }: { el: NodeFieldElement }) => {
const { id, data } = el;
const { fieldIdentifier, showDescription } = data;
return (
<FormElementEditModeWrapper element={el}>
<Flex id={id} className={NODE_FIELD_CLASS_NAME} flex="1 1 0">
<FormControl flex="1 1 0" orientation="vertical">
<NodeFieldEditableLabel el={el} />
<Flex w="full" gap={4}>
<InputFieldRenderer
nodeId={fieldIdentifier.nodeId}
fieldName={fieldIdentifier.fieldName}
settings={data.settings}
/>
</Flex>
{showDescription && <NodeFieldEditableDescription el={el} />}
</FormControl>
</Flex>
</FormElementEditModeWrapper>
);
});
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<HTMLInputElement>(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 (
<Flex w="full" gap={4}>
<FormLabel onDoubleClick={editable.startEditing} cursor="text">
{editable.value}
</FormLabel>
<Spacer />
<NodeFieldElementResetToInitialValueIconButton element={el} />
</Flex>
);
}
return (
<Input
ref={inputRef}
variant="outline"
p={1}
px={2}
_focusVisible={{ borderRadius: 'base', h: 'unset' }}
{...editable.inputProps}
/>
);
});
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<HTMLTextAreaElement>(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 <FormHelperText onDoubleClick={editable.startEditing}>{editable.value}</FormHelperText>;
}
return (
<Textarea
ref={inputRef}
variant="outline"
fontSize="sm"
p={1}
px={2}
_focusVisible={{ borderRadius: 'base', h: 'unset' }}
{...editable.inputProps}
/>
);
});
NodeFieldEditableDescription.displayName = 'NodeFieldEditableDescription';

View File

@@ -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<HTMLTextAreaElement>(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 <FormHelperText onDoubleClick={editable.startEditing}>{editable.value}</FormHelperText>;
}
return (
<Textarea
ref={inputRef}
variant="outline"
fontSize="sm"
p={1}
px={2}
_focusVisible={{ borderRadius: 'base', h: 'unset' }}
{...editable.inputProps}
/>
);
});
NodeFieldElementDescriptionEditable.displayName = 'NodeFieldElementDescriptionEditable';

View File

@@ -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<HTMLDivElement>(null);
const dragHandleRef = useRef<HTMLDivElement>(null);
const [activeDropRegion, isDragging] = useFormElementDnd(el.id, draggableRef, dragHandleRef);
const containerCtx = useContainerContext();
const { id, data } = el;
const { fieldIdentifier, showDescription } = data;
return (
<Flex ref={draggableRef} id={id} className={NODE_FIELD_CLASS_NAME} sx={sx} data-parent-layout={containerCtx.layout}>
<FormElementEditModeHeader dragHandleRef={dragHandleRef} element={el} data-is-dragging={isDragging} />
<FormElementEditModeContent data-is-dragging={isDragging} p={4}>
<FormControl flex="1 1 0" orientation="vertical">
<NodeFieldElementLabelEditable el={el} />
<Flex w="full" gap={4}>
<InputFieldRenderer
nodeId={fieldIdentifier.nodeId}
fieldName={fieldIdentifier.fieldName}
settings={data.settings}
/>
</Flex>
{showDescription && <NodeFieldElementDescriptionEditable el={el} />}
</FormControl>
</FormElementEditModeContent>
<DndListDropIndicator activeDropRegion={activeDropRegion} gap="var(--invoke-space-4)" />
</Flex>
);
});
NodeFieldElementEditMode.displayName = 'NodeFieldElementEditMode';

View File

@@ -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 (
<Flex w="full" gap={4}>
<FormLabel>{_label}</FormLabel>
<Spacer />
<NodeFieldElementResetToInitialValueIconButton element={el} />
</Flex>
);
});
NodeFieldElementLabel.displayName = 'NodeFieldElementLabel';

View File

@@ -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<HTMLInputElement>(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 (
<Flex w="full" gap={4}>
<FormLabel onDoubleClick={editable.startEditing} cursor="text">
{editable.value}
</FormLabel>
<Spacer />
<NodeFieldElementResetToInitialValueIconButton element={el} />
</Flex>
);
}
return (
<Input
ref={inputRef}
variant="outline"
p={1}
px={2}
_focusVisible={{ borderRadius: 'base', h: 'unset' }}
{...editable.inputProps}
/>
);
});
NodeFieldElementLabelEditable.displayName = 'NodeFieldElementLabelEditable';

View File

@@ -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 (
<Flex id={id} className={NODE_FIELD_CLASS_NAME} flex="1 0 0">
<FormControl flex="1 1 0" orientation="vertical">
<NodeFieldElementLabel el={el} />
<Flex w="full" gap={4}>
<InputFieldRenderer
nodeId={fieldIdentifier.nodeId}
fieldName={fieldIdentifier.fieldName}
settings={data.settings}
/>
</Flex>
{showDescription && _description && <FormHelperText>{_description}</FormHelperText>}
</FormControl>
</Flex>
);
});
NodeFieldElementViewMode.displayName = 'NodeFieldElementViewMode';

View File

@@ -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 <TextElementViewMode el={el} />;
}
// mode === 'edit'
return <TextElementEditMode el={el} />;
});
TextElement.displayName = 'TextElement';

View File

@@ -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 <TextElementComponentViewMode el={el} />;
}
// mode === 'edit'
return <TextElementComponentEditMode el={el} />;
});
TextElementComponent.displayName = 'TextElementComponent';
const TextElementComponentViewMode = memo(({ el }: { el: TextElement }) => {
const { id, data } = el;
const { content } = data;
return (
<Flex id={id} className={TEXT_CLASS_NAME} w="full">
<TextContentDisplay content={content} />
</Flex>
);
});
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 (
<Text sx={textSx} data-is-empty={content === ''} {...rest}>
{content || t('workflows.builder.textPlaceholder')}
</Text>
);
});
TextContentDisplay.displayName = 'TextContentDisplay';
const TextElementComponentEditMode = memo(({ el }: { el: TextElement }) => {
const { id } = el;
return (
<FormElementEditModeWrapper element={el}>
<Flex id={id} className={TEXT_CLASS_NAME} w="full">
<EditableText el={el} />
</Flex>
</FormElementEditModeWrapper>
);
});
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<HTMLTextAreaElement>(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 <TextContentDisplay content={editable.value} onDoubleClick={editable.startEditing} />;
}
return (
<AutosizeTextarea
ref={ref}
placeholder={t('workflows.builder.textPlaceholder')}
{...editable.inputProps}
fontSize="md"
variant="outline"
overflowWrap="anywhere"
w="full"
minRows={1}
maxRows={10}
resize="none"
p={2}
/>
);
});
EditableText.displayName = 'EditableText';

View File

@@ -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 (
<Text sx={textSx} data-is-empty={content === ''} {...rest}>
{content || t('workflows.builder.textPlaceholder')}
</Text>
);
});
TextElementContent.displayName = 'TextElementContent';

View File

@@ -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<HTMLTextAreaElement>(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 <TextElementContent content={editable.value} onDoubleClick={editable.startEditing} />;
}
return (
<AutosizeTextarea
ref={ref}
placeholder={t('workflows.builder.textPlaceholder')}
{...editable.inputProps}
fontSize="md"
variant="outline"
overflowWrap="anywhere"
w="full"
minRows={1}
maxRows={10}
resize="none"
p={2}
/>
);
});
TextElementContentEditable.displayName = 'TextElementContentEditable';

View File

@@ -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<HTMLDivElement>(null);
const dragHandleRef = useRef<HTMLDivElement>(null);
const [activeDropRegion, isDragging] = useFormElementDnd(el.id, draggableRef, dragHandleRef);
const containerCtx = useContainerContext();
const { id } = el;
return (
<Flex ref={draggableRef} id={id} className={TEXT_CLASS_NAME} sx={sx} data-parent-layout={containerCtx.layout}>
<FormElementEditModeHeader dragHandleRef={dragHandleRef} element={el} data-is-dragging={isDragging} />
<FormElementEditModeContent data-is-dragging={isDragging} p={4}>
<TextElementContentEditable el={el} />
</FormElementEditModeContent>
<DndListDropIndicator activeDropRegion={activeDropRegion} gap="var(--invoke-space-4)" />
</Flex>
);
});
TextElementEditMode.displayName = 'TextElementEditMode';

View File

@@ -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 (
<Flex id={id} className={TEXT_CLASS_NAME} w="full">
<TextElementContent content={content} />
</Flex>
);
});
TextElementViewMode.displayName = 'TextElementViewMode';

View File

@@ -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 (
<Flex sx={sx} data-is-empty={isFormEmpty}>
<FormElementComponent id={rootElementId} />
<RootContainerElementEditMode />
</Flex>
);
});

View File

@@ -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;
};

View File

@@ -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<string | symbol, unknown>): 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 }) => {

View File

@@ -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',
};

View File

@@ -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 (
<Flex flexDir="column" w="full" maxW="768px">
<FormElementComponent id={rootElementId} />
<Flex w="full" h="full" justifyContent="center">
<RootContainerElementViewMode />
</Flex>
);
});

View File

@@ -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)) {

View File

@@ -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({