feat(ui): iterate on builder (WIP)

This commit is contained in:
psychedelicious
2025-01-23 16:35:57 +11:00
parent 1f995d0257
commit 9f07e83a23
13 changed files with 499 additions and 124 deletions

View File

@@ -1,9 +0,0 @@
import type { ContainerElement } from 'features/nodes/types/workflow';
import { createContext, useContext } from 'react';
export const ContainerContext = createContext<ContainerElement['data'] | null>(null);
export const useContainerContext = () => {
const containerDirection = useContext(ContainerContext);
return containerDirection;
};

View File

@@ -1,63 +1,120 @@
import { Flex, type SystemStyleObject } from '@invoke-ai/ui-library';
import { ContainerContext } from 'features/nodes/components/sidePanel/builder/ContainerContext';
import { Flex, IconButton, type SystemStyleObject } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { ContainerContext, DepthContext } from 'features/nodes/components/sidePanel/builder/contexts';
import { DividerElementComponent } from 'features/nodes/components/sidePanel/builder/DividerElementComponent';
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 { useElement } from 'features/nodes/store/workflowSlice';
import { formElementAdded, selectWorkflowFormMode, useElement } from 'features/nodes/store/workflowSlice';
import type { ContainerElement } from 'features/nodes/types/workflow';
import {
container,
CONTAINER_CLASS_NAME,
DIVIDER_CLASS_NAME,
isContainerElement,
isDividerElement,
isHeadingElement,
isNodeFieldElement,
isTextElement,
} from 'features/nodes/types/workflow';
import { memo } from 'react';
import { memo, useCallback, useContext } from 'react';
import { PiPlusBold } from 'react-icons/pi';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
const sx: SystemStyleObject = {
gap: 4,
flex: '1 1 0',
'&[data-container-direction="column"]': {
flexDir: 'column',
'> :last-child': {
flex: '1 0 0',
alignItems: 'flex-start',
},
},
'&[data-container-direction="row"]': {
'> *': {
flex: '1 1 0',
},
},
[`& > .${DIVIDER_CLASS_NAME}`]: {
flex: '0 0 1px',
flexDir: 'row',
},
};
export const ContainerElementComponent = memo(({ id }: { id: string }) => {
const el = useElement(id);
const mode = useAppSelector(selectWorkflowFormMode);
if (!el || !isContainerElement(el)) {
return null;
}
const { children, direction } = el.data;
if (mode === 'view') {
return <ContainerElementComponentViewMode el={el} />;
}
return (
<ContainerContext.Provider value={el.data}>
<Flex id={id} className={CONTAINER_CLASS_NAME} sx={sx} data-container-direction={direction}>
{children.map((childId) => (
<FormElementComponent key={childId} id={childId} />
))}
</Flex>
</ContainerContext.Provider>
);
// mode === 'edit'
return <ContainerElementComponentEditMode el={el} />;
});
ContainerElementComponent.displayName = 'ContainerElementComponent';
export const ContainerElementComponentViewMode = memo(({ el }: { el: ContainerElement }) => {
const depth = useContext(DepthContext);
const { id, data } = el;
const { children, direction } = data;
return (
<DepthContext.Provider value={depth + 1}>
<ContainerContext.Provider value={data}>
<Flex id={id} className={CONTAINER_CLASS_NAME} sx={sx} data-container-direction={direction}>
{children.map((childId) => (
<FormElementComponent key={childId} id={childId} />
))}
</Flex>
</ContainerContext.Provider>{' '}
</DepthContext.Provider>
);
});
ContainerElementComponentViewMode.displayName = 'ContainerElementComponentViewMode';
export const ContainerElementComponentEditMode = memo(({ el }: { el: ContainerElement }) => {
const depth = useContext(DepthContext);
const { id, data } = el;
const { children, direction } = data;
return (
<FormElementEditModeWrapper element={el}>
<DepthContext.Provider value={depth + 1}>
<ContainerContext.Provider value={data}>
<Flex id={id} className={CONTAINER_CLASS_NAME} sx={sx} data-container-direction={direction}>
{children.map((childId) => (
<FormElementComponent key={childId} id={childId} />
))}
{direction === 'row' && children.length < 3 && depth < 2 && <AddColumnButton containerId={id} />}
{direction === 'column' && depth < 1 && <AddRowButton containerId={id} />}
</Flex>
</ContainerContext.Provider>
</DepthContext.Provider>
</FormElementEditModeWrapper>
);
});
ContainerElementComponentEditMode.displayName = 'ContainerElementComponentEditMode';
const AddColumnButton = ({ containerId }: { containerId: string }) => {
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
const element = container('column', []);
dispatch(formElementAdded({ element, containerId }));
}, [containerId, dispatch]);
return (
<IconButton onClick={onClick} aria-label="add column" icon={<PiPlusBold />} h="unset" variant="ghost" size="sm" />
);
};
const AddRowButton = ({ containerId }: { containerId: string }) => {
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
const element = container('row', []);
dispatch(formElementAdded({ element, containerId }));
}, [containerId, dispatch]);
return (
<IconButton onClick={onClick} aria-label="add row" icon={<PiPlusBold />} w="unset" variant="ghost" size="sm" />
);
};
// TODO(psyche): Can we move this into a separate file and avoid circular dependencies between it and ContainerElementComponent?
export const FormElementComponent = memo(({ id }: { id: string }) => {
const el = useElement(id);

View File

@@ -1,22 +1,74 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
import { useElement } from 'features/nodes/store/workflowSlice';
import { useAppSelector } from 'app/store/storeHooks';
import { ContainerContext } from 'features/nodes/components/sidePanel/builder/contexts';
import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
import { selectWorkflowFormMode, 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';
import { memo, useContext } from 'react';
const sx: SystemStyleObject = {
bg: 'base.700',
flexShrink: 0,
'&[data-orientation="horizontal"]': {
width: '100%',
height: '1px',
},
'&[data-orientation="vertical"]': {
height: '100%',
width: '1px',
},
};
export const DividerElementComponent = memo(({ id }: { id: string }) => {
const el = useElement(id);
const mode = useAppSelector(selectWorkflowFormMode);
if (!el || !isDividerElement(el)) {
return;
}
return <Flex id={id} className={DIVIDER_CLASS_NAME} sx={sx} />;
if (mode === 'view') {
return <DividerElementComponentViewMode el={el} />;
}
// mode === 'edit'
return <DividerElementComponentEditMode el={el} />;
});
DividerElementComponent.displayName = 'DividerElementComponent';
export const DividerElementComponentViewMode = memo(({ el }: { el: DividerElement }) => {
const container = useContext(ContainerContext);
const { id } = el;
return (
<Flex
id={id}
className={DIVIDER_CLASS_NAME}
sx={sx}
data-orientation={container?.direction === 'column' ? 'horizontal' : 'vertical'}
/>
);
});
DividerElementComponentViewMode.displayName = 'DividerElementComponentViewMode';
export const DividerElementComponentEditMode = memo(({ el }: { el: DividerElement }) => {
const container = useContext(ContainerContext);
const { id } = el;
return (
<FormElementEditModeWrapper element={el}>
<Flex
id={id}
className={DIVIDER_CLASS_NAME}
sx={sx}
data-orientation={container?.direction === 'column' ? 'horizontal' : 'vertical'}
/>
</FormElementEditModeWrapper>
);
});
DividerElementComponentEditMode.displayName = 'DividerElementComponentEditMode';

View File

@@ -0,0 +1,87 @@
import { Flex, type FlexProps, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { DepthContext } from 'features/nodes/components/sidePanel/builder/contexts';
import { formElementRemoved } from 'features/nodes/store/workflowSlice';
import { type FormElement, isContainerElement } from 'features/nodes/types/workflow';
import { startCase } from 'lodash-es';
import { memo, useCallback, useContext } from 'react';
import { PiXBold } from 'react-icons/pi';
export const EDIT_MODE_WRAPPER_CLASS_NAME = getPrefixedId('edit-mode-wrapper', '-');
const getHeaderBgColor = (depth: number) => {
if (depth <= 1) {
return 'base.800';
}
if (depth === 2) {
return 'base.750';
}
return 'base.700';
};
const getHeaderLabel = (el: FormElement) => {
if (isContainerElement(el)) {
if (el.data.direction === 'column') {
return 'Column';
}
return 'Row';
}
return startCase(el.type);
};
export const FormElementEditModeWrapper = memo(
({ element, children, ...rest }: { element: FormElement } & FlexProps) => {
const depth = useContext(DepthContext);
const dispatch = useAppDispatch();
const removeElement = useCallback(() => {
dispatch(formElementRemoved({ id: element.id }));
}, [dispatch, element.id]);
return (
<Flex
className={EDIT_MODE_WRAPPER_CLASS_NAME}
flexDir="column"
borderWidth={1}
borderRadius="base"
borderColor="base.750"
alignItems="center"
justifyContent="flex-start"
w="full"
h="full"
{...rest}
>
<Flex
w="full"
ps={2}
h={8}
bg={getHeaderBgColor(depth)}
borderTopRadius="inherit"
borderBottomWidth={1}
borderColor="inherit"
alignItems="center"
cursor="grab"
>
<Text fontWeight="semibold" noOfLines={1} wordBreak="break-all">
{getHeaderLabel(element)}
</Text>
<Spacer />
<IconButton
aria-label="delete"
onClick={removeElement}
icon={<PiXBold />}
variant="link"
size="sm"
alignSelf="stretch"
colorScheme="error"
/>
</Flex>
<Flex w="full" p={4} alignItems="center" gap={4}>
{children}
</Flex>
</Flex>
);
}
);
FormElementEditModeWrapper.displayName = 'FormElementEditModeWrapper';

View File

@@ -1,5 +1,8 @@
import { Flex, Heading } from '@invoke-ai/ui-library';
import { useElement } from 'features/nodes/store/workflowSlice';
import { useAppSelector } from 'app/store/storeHooks';
import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
import { selectWorkflowFormMode, 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 } from 'react';
@@ -13,12 +16,25 @@ const LEVEL_TO_SIZE = {
export const HeadingElementComponent = memo(({ id }: { id: string }) => {
const el = useElement(id);
const mode = useAppSelector(selectWorkflowFormMode);
if (!el || !isHeadingElement(el)) {
return null;
}
const { content, level } = el.data;
if (mode === 'view') {
return <HeadingElementComponentViewMode el={el} />;
}
// mode === 'edit'
return <HeadingElementComponentEditMode el={el} />;
});
HeadingElementComponent.displayName = 'HeadingElementComponent';
export const HeadingElementComponentViewMode = memo(({ el }: { el: HeadingElement }) => {
const { id, data } = el;
const { content, level } = data;
return (
<Flex id={id} className={HEADING_CLASS_NAME}>
@@ -27,4 +43,19 @@ export const HeadingElementComponent = memo(({ id }: { id: string }) => {
);
});
HeadingElementComponent.displayName = 'HeadingElementComponent';
HeadingElementComponentViewMode.displayName = 'HeadingElementComponentViewMode';
export const HeadingElementComponentEditMode = memo(({ el }: { el: HeadingElement }) => {
const { id, data } = el;
const { content, level } = data;
return (
<FormElementEditModeWrapper element={el}>
<Flex id={id} className={HEADING_CLASS_NAME}>
<Heading size={LEVEL_TO_SIZE[level]}>{content}</Heading>
</Flex>
</FormElementEditModeWrapper>
);
});
HeadingElementComponentEditMode.displayName = 'HeadingElementComponentEditMode';

View File

@@ -1,18 +1,34 @@
import { Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
import { InputFieldViewMode } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldViewMode';
import { useElement } from 'features/nodes/store/workflowSlice';
import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
import { selectWorkflowFormMode, 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 } from 'react';
export const NodeFieldElementComponent = memo(({ id }: { id: string }) => {
const el = useElement(id);
const mode = useAppSelector(selectWorkflowFormMode);
if (!el || !isNodeFieldElement(el)) {
return null;
}
const { fieldIdentifier } = el.data;
if (mode === 'view') {
return <NodeFieldElementComponentViewMode el={el} />;
}
// mode === 'edit'
return <NodeFieldElementComponentEditMode el={el} />;
});
NodeFieldElementComponent.displayName = 'NodeFieldElementComponent';
export const NodeFieldElementComponentViewMode = memo(({ el }: { el: NodeFieldElement }) => {
const { id, data } = el;
const { fieldIdentifier } = data;
return (
<Flex id={id} className={NODE_FIELD_CLASS_NAME}>
@@ -23,4 +39,21 @@ export const NodeFieldElementComponent = memo(({ id }: { id: string }) => {
);
});
NodeFieldElementComponent.displayName = 'NodeFieldElementComponent';
NodeFieldElementComponentViewMode.displayName = 'NodeFieldElementComponentViewMode';
export const NodeFieldElementComponentEditMode = memo(({ el }: { el: NodeFieldElement }) => {
const { id, data } = el;
const { fieldIdentifier } = data;
return (
<FormElementEditModeWrapper element={el}>
<Flex id={id} className={NODE_FIELD_CLASS_NAME} w='full'>
<InputFieldGate nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName}>
<InputFieldViewMode nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName} />
</InputFieldGate>
</Flex>
</FormElementEditModeWrapper>
);
});
NodeFieldElementComponentEditMode.displayName = 'NodeFieldElementComponentEditMode';

View File

@@ -1,16 +1,31 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { useElement } from 'features/nodes/store/workflowSlice';
import { useAppSelector } from 'app/store/storeHooks';
import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
import { selectWorkflowFormMode, 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 } from 'react';
export const TextElementComponent = memo(({ id }: { id: string }) => {
const el = useElement(id);
const mode = useAppSelector(selectWorkflowFormMode);
if (!el || !isTextElement(el)) {
return null;
}
const { content, fontSize } = el.data;
if (mode === 'view') {
return <TextElementComponentViewMode el={el} />;
}
// mode === 'edit'
return <TextElementComponentEditMode el={el} />;
});
TextElementComponent.displayName = 'TextElementComponent';
export const TextElementComponentViewMode = memo(({ el }: { el: TextElement }) => {
const { id, data } = el;
const { content, fontSize } = data;
return (
<Flex id={id} className={TEXT_CLASS_NAME}>
@@ -18,5 +33,18 @@ export const TextElementComponent = memo(({ id }: { id: string }) => {
</Flex>
);
});
TextElementComponentViewMode.displayName = 'TextElementComponentViewMode';
TextElementComponent.displayName = 'TextElementComponent';
export const TextElementComponentEditMode = memo(({ el }: { el: TextElement }) => {
const { id, data } = el;
const { content, fontSize } = data;
return (
<Flex id={id} className={TEXT_CLASS_NAME}>
<FormElementEditModeWrapper element={el}>
<Text fontSize={fontSize}>{content}</Text>
</FormElementEditModeWrapper>
</Flex>
);
});
TextElementComponentEditMode.displayName = 'TextElementComponentEditMode';

View File

@@ -1,20 +1,23 @@
import { Flex } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { Button, Flex } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { FormElementComponent } from 'features/nodes/components/sidePanel/builder/ContainerElementComponent';
import { formLoaded } from 'features/nodes/store/workflowSlice';
import { formLoaded, formModeToggled, selectWorkflowFormMode } from 'features/nodes/store/workflowSlice';
import { elements, rootElementId } from 'features/nodes/types/workflow';
import { memo, useEffect } from 'react';
import { memo, useCallback, useEffect } from 'react';
export const WorkflowBuilder = memo(() => {
const dispatch = useAppDispatch();
const mode = useAppSelector(selectWorkflowFormMode);
useEffect(() => {
dispatch(formLoaded({ elements, rootElementId }));
}, [dispatch]);
return (
<ScrollableContent>
<Flex w="full" justifyContent="center">
<Flex w="full" maxW={512}>
<Flex flexDir="column" w={mode === 'view' ? '768px' : 'min-content'} minW='768px'>
<ToggleModeButton />
{rootElementId && <FormElementComponent id={rootElementId} />}
</Flex>
</Flex>
@@ -23,3 +26,15 @@ export const WorkflowBuilder = memo(() => {
});
WorkflowBuilder.displayName = 'WorkflowBuilder';
const ToggleModeButton = memo(() => {
const dispatch = useAppDispatch();
const mode = useAppSelector(selectWorkflowFormMode);
const onClick = useCallback(() => {
dispatch(formModeToggled());
}, [dispatch]);
return <Button onClick={onClick}>{mode === 'view' ? 'Edit' : 'View'}</Button>;
});
ToggleModeButton.displayName = 'ToggleModeButton';

View File

@@ -0,0 +1,5 @@
import type { ContainerElement } from 'features/nodes/types/workflow';
import { createContext } from 'react';
export const ContainerContext = createContext<ContainerElement['data'] | null>(null);
export const DepthContext = createContext<number>(0);

View File

@@ -13,7 +13,7 @@ const WorkflowFieldsLinearViewPanel = () => {
<Flex layerStyle="first" flexDir="column" w="full" h="full" borderRadius="base" p={2} gap={2}>
<Tabs variant="line" display="flex" w="full" h="full" flexDir="column">
<TabList>
<Tab>Builder</Tab>
<Tab>{t('common.builder')}</Tab>
<Tab>{t('common.linear')}</Tab>
<Tab>{t('common.details')}</Tab>
<Tab>JSON</Tab>

View File

@@ -39,6 +39,7 @@ export type WorkflowsState = Omit<WorkflowV3, 'nodes' | 'edges'> & {
_version: 1;
isTouched: boolean;
mode: WorkflowMode;
formMode: WorkflowMode;
originalExposedFieldValues: FieldIdentifierWithValue[];
searchTerm: string;
orderBy?: WorkflowRecordOrderBy;

View File

@@ -12,7 +12,13 @@ import type {
} from 'features/nodes/store/types';
import type { FieldIdentifier } from 'features/nodes/types/field';
import { isInvocationNode } from 'features/nodes/types/invocation';
import type { ContainerElement, FormElement, WorkflowCategory, WorkflowV3 } from 'features/nodes/types/workflow';
import {
type ContainerElement,
type FormElement,
isContainerElement,
type WorkflowCategory,
type WorkflowV3,
} from 'features/nodes/types/workflow';
import { isEqual, omit, uniqBy } from 'lodash-es';
import { useMemo } from 'react';
import type { SQLiteDirection, WorkflowRecordOrderBy } from 'services/api/types';
@@ -42,6 +48,7 @@ const initialWorkflowState: WorkflowState = {
orderBy: undefined, // initial value is decided in component
orderDirection: 'DESC',
categorySections: {},
formMode: 'view',
...blankWorkflow,
};
@@ -138,31 +145,39 @@ export const workflowSlice = createSlice({
rootElementId: container.id,
};
},
formElementAdded: (state, action: PayloadAction<{ element: FormElement; containerId: string; index: number }>) => {
formElementAdded: (state, action: PayloadAction<{ element: FormElement; containerId: string; index?: number }>) => {
if (!state.form) {
// Cannot add an element if the form has not been created
return;
}
const { elements } = state.form;
const { element, containerId, index } = action.payload;
const containerElement = state.form.elements[containerId];
if (containerElement?.type !== 'container') {
return;
}
state.form.elements[element.id] = element;
containerElement.data.children.splice(index, 0, element.id);
addElement(elements, element, containerId, index);
},
formElementRemoved: (state, action: PayloadAction<{ id: string }>) => {
if (!state.form) {
// Cannot remove an element if the form has not been created
return;
}
const form = state.form;
const { elements, rootElementId } = state.form;
const { id } = action.payload;
buildRemoveElementById(form.elements)(id, form.rootElementId);
removeElement(elements, id, rootElementId);
},
formElementMoved: (state, action: PayloadAction<{ id: string; containerId: string; index: number }>) => {
if (!state.form) {
// Cannot move an element if the form has not been created
return;
}
const { elements } = state.form;
const { id, containerId, index } = action.payload;
moveElement(elements, id, containerId, index);
},
formReset: (state) => {
state.form = undefined;
},
formModeToggled: (state) => {
state.formMode = state.formMode === 'edit' ? 'view' : 'edit';
},
},
extraReducers: (builder) => {
builder.addCase(workflowLoaded, (state, action) => {
@@ -281,6 +296,7 @@ export const {
formElementAdded,
formElementRemoved,
formReset,
formModeToggled,
} = workflowSlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
@@ -305,6 +321,7 @@ const createWorkflowSelector = <T>(selector: Selector<WorkflowState, T>) =>
export const selectWorkflowName = createWorkflowSelector((workflow) => workflow.name);
export const selectWorkflowId = createWorkflowSelector((workflow) => workflow.id);
export const selectWorkflowMode = createWorkflowSelector((workflow) => workflow.mode);
export const selectWorkflowFormMode = createWorkflowSelector((workflow) => workflow.formMode);
export const selectWorkflowIsTouched = createWorkflowSelector((workflow) => workflow.isTouched);
export const selectWorkflowSearchTerm = createWorkflowSelector((workflow) => workflow.searchTerm);
export const selectWorkflowOrderBy = createWorkflowSelector((workflow) => workflow.orderBy);
@@ -326,28 +343,66 @@ export const useElement = (id: string): FormElement | undefined => {
return element;
};
const buildRemoveElementById = (elements: NonNullable<WorkflowV3['form']>['elements']) => {
const removeElementById = (id: string, containerId: string): boolean => {
const container = elements[containerId] as ContainerElement;
const addElement = (
elements: NonNullable<WorkflowV3['form']>['elements'],
element: FormElement,
containerId: string,
index?: number
) => {
const container = elements[containerId];
if (!container || !isContainerElement(container)) {
return;
}
elements[element.id] = element;
if (index === undefined) {
container.data.children.push(element.id);
} else {
container.data.children.splice(index, 0, element.id);
}
};
if (container.type !== 'container') {
return false;
}
const removeElement = (
elements: NonNullable<WorkflowV3['form']>['elements'],
id: string,
containerId: string
): boolean => {
const container = elements[containerId];
const index = container.data.children.indexOf(id);
if (index !== -1) {
container.data.children.splice(index, 1);
delete elements[id];
if (!container || !isContainerElement(container)) {
return false;
}
const index = container.data.children.indexOf(id);
if (index !== -1) {
container.data.children.splice(index, 1);
delete elements[id];
return true;
}
for (const childId of container.data.children) {
if (removeElement(elements, id, childId)) {
return true;
}
}
for (const childId of container.data.children) {
if (removeElementById(id, childId)) {
return true;
}
}
return false;
};
return removeElementById;
return false;
};
const moveElement = (
elements: NonNullable<WorkflowV3['form']>['elements'],
id: string,
containerId: string,
index: number
) => {
const element = elements[id];
if (!element) {
return;
}
const container = elements[containerId];
if (!container || !isContainerElement(container)) {
return;
}
removeElement(elements, id, containerId);
addElement(elements, element, containerId, index);
};

View File

@@ -118,6 +118,10 @@ const nodeField = (
fieldIdentifier: { nodeId, fieldName },
},
};
return element;
};
const _nodeField = (...args: Parameters<typeof nodeField>): NodeFieldElement => {
const element = nodeField(...args);
addElement(element);
return element;
};
@@ -145,6 +149,10 @@ const heading = (
level,
},
};
return element;
};
const _heading = (...args: Parameters<typeof heading>): HeadingElement => {
const element = heading(...args);
addElement(element);
return element;
};
@@ -172,6 +180,11 @@ const text = (content: TextElement['data']['content'], fontSize: TextElement['da
addElement(element);
return element;
};
const _text = (...args: Parameters<typeof text>): TextElement => {
const element = text(...args);
addElement(element);
return element;
};
const DIVIDER_TYPE = 'divider';
export const DIVIDER_CLASS_NAME = getPrefixedId(DIVIDER_TYPE, '-');
@@ -188,6 +201,11 @@ const divider = (): DividerElement => {
addElement(element);
return element;
};
const _divider = (...args: Parameters<typeof divider>): DividerElement => {
const element = divider(...args);
addElement(element);
return element;
};
export type ContainerElement = {
id: string;
@@ -208,7 +226,7 @@ const zContainerElement: z.ZodType<ContainerElement> = zElementBase.extend({
}),
});
export const isContainerElement = (el: FormElement): el is ContainerElement => el.type === CONTAINER_TYPE;
const container = (
export const container = (
direction: ContainerElement['data']['direction'],
children: ContainerElement['data']['children']
): ContainerElement => {
@@ -220,6 +238,10 @@ const container = (
children,
},
};
return element;
};
export const _container = (...args: Parameters<typeof container>): ContainerElement => {
const element = container(...args);
addElement(element);
return element;
};
@@ -228,55 +250,53 @@ const zFormElement = z.union([zContainerElement, zNodeFieldElement, zHeadingElem
export type FormElement = z.infer<typeof zFormElement>;
export const rootElementId: string = container('column', [
heading('My Cool Workflow', 1).id,
text('This is a description of what my workflow does. It does things.', 'md').id,
divider().id,
heading('First Section', 2).id,
text('The first section includes fields relevant to the first section. This note describes that fact.', 'sm').id,
divider().id,
container('row', [
nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image').id,
divider().id,
nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image').id,
divider().id,
nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image').id,
export const rootElementId: string = _container('column', [
_heading('My Cool Workflow', 1).id,
_text('This is a description of what my workflow does. It does things.', 'md').id,
_divider().id,
_heading('First Section', 2).id,
_text('The first section includes fields relevant to the first section. This note describes that fact.', 'sm').id,
_divider().id,
_container('row', [
_nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image').id,
_nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image').id,
_nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image').id,
]).id,
nodeField('9c058600-8d73-4702-912b-0ccf37403bfd', 'value').id,
nodeField('7a8bbab2-6919-4cfc-bd7c-bcfda3c79ecf', 'value').id,
nodeField('4e16cbf6-457c-46fb-9ab7-9cb262fa1e03', 'value').id,
nodeField('39cb5272-a9d7-4da9-9c35-32e02b46bb34', 'color').id,
container('row', [
container('column', [
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value').id,
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value').id,
_nodeField('9c058600-8d73-4702-912b-0ccf37403bfd', 'value').id,
_nodeField('7a8bbab2-6919-4cfc-bd7c-bcfda3c79ecf', 'value').id,
_nodeField('4e16cbf6-457c-46fb-9ab7-9cb262fa1e03', 'value').id,
_nodeField('39cb5272-a9d7-4da9-9c35-32e02b46bb34', 'color').id,
_container('row', [
_container('column', [
_nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value').id,
_nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value').id,
]).id,
container('column', [
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value').id,
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value').id,
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value').id,
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value').id,
_container('column', [
_nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value').id,
_nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value').id,
_nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value').id,
_nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value').id,
]).id,
container('column', [
container('row', [
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value').id,
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value').id,
_container('column', [
_container('row', [
_nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value').id,
_nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value').id,
]).id,
container('row', [
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value').id,
nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value').id,
_container('row', [
_nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value').id,
_nodeField('4f609a81-0e25-47d1-ba0d-f24fedd5273f', 'value').id,
]).id,
]).id,
]).id,
nodeField('14744f68-9000-4694-b4d6-cbe83ee231ee', 'model').id,
divider().id,
text('These are some text that are definitely super helpful.', 'sm').id,
divider().id,
container('row', [
container('column', [
nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image').id,
nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image').id,
_nodeField('14744f68-9000-4694-b4d6-cbe83ee231ee', 'model').id,
_divider().id,
_text('These are some text that are definitely super helpful.', 'sm').id,
_divider().id,
_container('row', [
_container('column', [
_nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image').id,
_nodeField('7aed1a5f-7fd7-4184-abe8-ddea0ea5e706', 'image').id,
]).id,
container('column', [nodeField('7a8bbab2-6919-4cfc-bd7c-bcfda3c79ecf', 'value').id]).id,
_container('column', [_nodeField('7a8bbab2-6919-4cfc-bd7c-bcfda3c79ecf', 'value').id]).id,
]).id,
]).id;