Skip to content

Commit 5e77f0d

Browse files
authored
Reorder exposed fields in workflow tab (#5711)
## What type of PR is this? (check all applicable) - [ ] Refactor - [x] Feature - [ ] Bug Fix - [ ] Optimization - [ ] Documentation Update - [ ] Community Node Submission ## Have you discussed this change with the InvokeAI team? - [x] Yes - [ ] No, because: ## Have you updated all relevant documentation? - [ ] Yes - [ ] No ## Description ## Related Tickets & Documents <!-- For pull requests that relate or close an issue, please include them below. For example having the text: "closes #1234" would connect the current pull request to issue 1234. And when we merge the pull request, Github will automatically close the issue. --> - Related Issue # - Closes # ## QA Instructions, Screenshots, Recordings <!-- Please provide steps on how to test changes, any hardware or software specifications as well as any other pertinent information. --> ## Merge Plan <!-- A merge plan describes how this PR should be handled after it is approved. Example merge plans: - "This PR can be merged when approved" - "This must be squash-merged when approved" - "DO NOT MERGE - I will rebase and tidy commits before merging" - "#dev-chat on discord needs to be advised of this change when it is merged" A merge plan is particularly important for large PRs or PRs that touch the database in any way. --> ## Added/updated tests? - [ ] Yes - [ ] No : _please replace this line with details on why tests have not been included_ ## [optional] Are there any post deployment tasks we need to perform?
2 parents fc278c5 + d3acb81 commit 5e77f0d

File tree

7 files changed

+138
-44
lines changed

7 files changed

+138
-44
lines changed

invokeai/frontend/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@chakra-ui/react-use-size": "^2.1.0",
5353
"@dagrejs/graphlib": "^2.1.13",
5454
"@dnd-kit/core": "^6.1.0",
55+
"@dnd-kit/sortable": "^8.0.0",
5556
"@dnd-kit/utilities": "^3.2.2",
5657
"@fontsource-variable/inter": "^5.0.16",
5758
"@invoke-ai/ui-library": "^0.0.18",

invokeai/frontend/web/pnpm-lock.yaml

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

invokeai/frontend/web/public/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,6 +1000,7 @@
10001000
"resetToDefaultValue": "Reset to default value",
10011001
"reloadNodeTemplates": "Reload Node Templates",
10021002
"removeLinearView": "Remove from Linear View",
1003+
"reorderLinearView": "Reorder Linear View",
10031004
"newWorkflow": "New Workflow",
10041005
"newWorkflowDesc": "Create a new workflow?",
10051006
"newWorkflowDesc2": "Your current workflow has unsaved changes.",
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { DragEndEvent } from '@dnd-kit/core';
2+
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
3+
import type { PropsWithChildren } from 'react';
4+
import { memo } from 'react';
5+
6+
import { DndContextTypesafe } from './DndContextTypesafe';
7+
8+
type Props = PropsWithChildren & {
9+
items: string[];
10+
onDragEnd(event: DragEndEvent): void;
11+
};
12+
13+
const DndSortable = (props: Props) => {
14+
return (
15+
<DndContextTypesafe onDragEnd={props.onDragEnd}>
16+
<SortableContext items={props.items} strategy={verticalListSortingStrategy}>
17+
{props.children}
18+
</SortableContext>
19+
</DndContextTypesafe>
20+
);
21+
};
22+
23+
export default memo(DndSortable);

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useSortable } from '@dnd-kit/sortable';
2+
import { CSS } from '@dnd-kit/utilities';
13
import { Flex, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library';
24
import { useAppDispatch } from 'app/store/storeHooks';
35
import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay';
@@ -7,7 +9,7 @@ import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice'
79
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
810
import { memo, useCallback } from 'react';
911
import { useTranslation } from 'react-i18next';
10-
import { PiArrowCounterClockwiseBold, PiInfoBold, PiTrashSimpleBold } from 'react-icons/pi';
12+
import { PiArrowCounterClockwiseBold, PiDotsSixVerticalBold, PiInfoBold, PiTrashSimpleBold } from 'react-icons/pi';
1113

1214
import EditableFieldTitle from './EditableFieldTitle';
1315
import FieldTooltipContent from './FieldTooltipContent';
@@ -28,50 +30,71 @@ const LinearViewField = ({ nodeId, fieldName }: Props) => {
2830
dispatch(workflowExposedFieldRemoved({ nodeId, fieldName }));
2931
}, [dispatch, fieldName, nodeId]);
3032

33+
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: `${nodeId}.${fieldName}` });
34+
35+
const style = {
36+
transform: CSS.Translate.toString(transform),
37+
transition,
38+
};
39+
3140
return (
3241
<Flex
3342
onMouseEnter={handleMouseOver}
3443
onMouseLeave={handleMouseOut}
3544
layerStyle="second"
45+
alignItems="center"
3646
position="relative"
3747
borderRadius="base"
3848
w="full"
3949
p={4}
40-
flexDir="column"
50+
paddingLeft={0}
51+
ref={setNodeRef}
52+
style={style}
4153
>
42-
<Flex>
43-
<EditableFieldTitle nodeId={nodeId} fieldName={fieldName} kind="input" />
44-
<Spacer />
45-
{isValueChanged && (
54+
<IconButton
55+
aria-label={t('nodes.reorderLinearView')}
56+
variant="ghost"
57+
icon={<PiDotsSixVerticalBold />}
58+
{...listeners}
59+
{...attributes}
60+
mx={2}
61+
height="full"
62+
/>
63+
<Flex flexDir="column" w="full">
64+
<Flex alignItems="center">
65+
<EditableFieldTitle nodeId={nodeId} fieldName={fieldName} kind="input" />
66+
<Spacer />
67+
{isValueChanged && (
68+
<IconButton
69+
aria-label={t('nodes.resetToDefaultValue')}
70+
tooltip={t('nodes.resetToDefaultValue')}
71+
variant="ghost"
72+
size="sm"
73+
onClick={onReset}
74+
icon={<PiArrowCounterClockwiseBold />}
75+
/>
76+
)}
77+
<Tooltip
78+
label={<FieldTooltipContent nodeId={nodeId} fieldName={fieldName} kind="input" />}
79+
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
80+
placement="top"
81+
>
82+
<Flex h="full" alignItems="center">
83+
<Icon fontSize="sm" color="base.300" as={PiInfoBold} />
84+
</Flex>
85+
</Tooltip>
4686
<IconButton
47-
aria-label={t('nodes.resetToDefaultValue')}
48-
tooltip={t('nodes.resetToDefaultValue')}
87+
aria-label={t('nodes.removeLinearView')}
88+
tooltip={t('nodes.removeLinearView')}
4989
variant="ghost"
5090
size="sm"
51-
onClick={onReset}
52-
icon={<PiArrowCounterClockwiseBold />}
91+
onClick={handleRemoveField}
92+
icon={<PiTrashSimpleBold />}
5393
/>
54-
)}
55-
<Tooltip
56-
label={<FieldTooltipContent nodeId={nodeId} fieldName={fieldName} kind="input" />}
57-
openDelay={HANDLE_TOOLTIP_OPEN_DELAY}
58-
placement="top"
59-
>
60-
<Flex h="full" alignItems="center">
61-
<Icon fontSize="sm" color="base.300" as={PiInfoBold} />
62-
</Flex>
63-
</Tooltip>
64-
<IconButton
65-
aria-label={t('nodes.removeLinearView')}
66-
tooltip={t('nodes.removeLinearView')}
67-
variant="ghost"
68-
size="sm"
69-
onClick={handleRemoveField}
70-
icon={<PiTrashSimpleBold />}
71-
/>
94+
</Flex>
95+
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
96+
<NodeSelectionOverlay isSelected={false} isHovered={isMouseOverNode} />
7297
</Flex>
73-
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
74-
<NodeSelectionOverlay isSelected={false} isHovered={isMouseOverNode} />
7598
</Flex>
7699
);
77100
};

invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import { arrayMove } from '@dnd-kit/sortable';
12
import { Box, Flex } from '@invoke-ai/ui-library';
23
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
3-
import { useAppSelector } from 'app/store/storeHooks';
4+
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
45
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
56
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
7+
import DndSortable from 'features/dnd/components/DndSortable';
8+
import type { DragEndEvent } from 'features/dnd/types';
69
import LinearViewField from 'features/nodes/components/flow/nodes/Invocation/fields/LinearViewField';
7-
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
8-
import { memo } from 'react';
10+
import { selectWorkflowSlice, workflowExposedFieldsReordered } from 'features/nodes/store/workflowSlice';
11+
import type { FieldIdentifier } from 'features/nodes/types/field';
12+
import { memo, useCallback } from 'react';
913
import { useTranslation } from 'react-i18next';
1014
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
1115

@@ -15,21 +19,43 @@ const WorkflowLinearTab = () => {
1519
const fields = useAppSelector(selector);
1620
const { isLoading } = useGetOpenAPISchemaQuery();
1721
const { t } = useTranslation();
22+
const dispatch = useAppDispatch();
23+
24+
const handleDragEnd = useCallback(
25+
(event: DragEndEvent) => {
26+
const { active, over } = event;
27+
const fieldsStrings = fields.map((field) => `${field.nodeId}.${field.fieldName}`);
28+
29+
if (over && active.id !== over.id) {
30+
const oldIndex = fieldsStrings.indexOf(active.id as string);
31+
const newIndex = fieldsStrings.indexOf(over.id as string);
32+
33+
const newFields = arrayMove(fieldsStrings, oldIndex, newIndex)
34+
.map((field) => fields.find((obj) => `${obj.nodeId}.${obj.fieldName}` === field))
35+
.filter((field) => field) as FieldIdentifier[];
36+
37+
dispatch(workflowExposedFieldsReordered(newFields));
38+
}
39+
},
40+
[dispatch, fields]
41+
);
1842

1943
return (
2044
<Box position="relative" w="full" h="full">
2145
<ScrollableContent>
22-
<Flex position="relative" flexDir="column" alignItems="flex-start" p={1} gap={2} h="full" w="full">
23-
{isLoading ? (
24-
<IAINoContentFallback label={t('nodes.loadingNodes')} icon={null} />
25-
) : fields.length ? (
26-
fields.map(({ nodeId, fieldName }) => (
27-
<LinearViewField key={`${nodeId}.${fieldName}`} nodeId={nodeId} fieldName={fieldName} />
28-
))
29-
) : (
30-
<IAINoContentFallback label={t('nodes.noFieldsLinearview')} icon={null} />
31-
)}
32-
</Flex>
46+
<DndSortable onDragEnd={handleDragEnd} items={fields.map((field) => `${field.nodeId}.${field.fieldName}`)}>
47+
<Flex position="relative" flexDir="column" alignItems="flex-start" p={1} gap={2} h="full" w="full">
48+
{isLoading ? (
49+
<IAINoContentFallback label={t('nodes.loadingNodes')} icon={null} />
50+
) : fields.length ? (
51+
fields.map(({ nodeId, fieldName }) => (
52+
<LinearViewField key={`${nodeId}.${fieldName}`} nodeId={nodeId} fieldName={fieldName} />
53+
))
54+
) : (
55+
<IAINoContentFallback label={t('nodes.noFieldsLinearview')} icon={null} />
56+
)}
57+
</Flex>
58+
</DndSortable>
3359
</ScrollableContent>
3460
</Box>
3561
);

invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ export const workflowSlice = createSlice({
5959
);
6060
state.isTouched = true;
6161
},
62+
workflowExposedFieldsReordered: (state, action: PayloadAction<FieldIdentifier[]>) => {
63+
state.exposedFields = action.payload;
64+
state.isTouched = true;
65+
},
6266
workflowNameChanged: (state, action: PayloadAction<string>) => {
6367
state.name = action.payload;
6468
state.isTouched = true;
@@ -175,6 +179,7 @@ export const {
175179
workflowModeChanged,
176180
workflowExposedFieldAdded,
177181
workflowExposedFieldRemoved,
182+
workflowExposedFieldsReordered,
178183
workflowNameChanged,
179184
workflowCategoryChanged,
180185
workflowDescriptionChanged,

0 commit comments

Comments
 (0)