mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-15 06:58:13 -05:00
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:
@@ -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}`);
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user