diff --git a/invokeai/frontend/web/src/common/components/IAISortableItem.tsx b/invokeai/frontend/web/src/common/components/IAISortableItem.tsx
new file mode 100644
index 0000000000..fd390b6f1d
--- /dev/null
+++ b/invokeai/frontend/web/src/common/components/IAISortableItem.tsx
@@ -0,0 +1,45 @@
+import { Box } from '@invoke-ai/ui-library';
+import { useDroppableTypesafe } from 'features/dnd/hooks/typesafeHooks';
+import type { TypesafeDroppableData } from 'features/dnd/types';
+import { isValidDrop } from 'features/dnd/util/isValidDrop';
+import { AnimatePresence } from 'framer-motion';
+import type { ReactNode } from 'react';
+import { memo, useRef } from 'react';
+import { v4 as uuidv4 } from 'uuid';
+
+import IAIDropOverlay from './IAIDropOverlay';
+
+type IAISortableItemProps = {
+ dropLabel?: ReactNode;
+ disabled?: boolean;
+ data?: TypesafeDroppableData;
+};
+
+const IAISortableItem = (props: IAISortableItemProps) => {
+ const { dropLabel, data, disabled } = props;
+ const dndId = useRef(uuidv4());
+
+ const { isOver, setNodeRef, active } = useDroppableTypesafe({
+ id: dndId.current,
+ disabled,
+ data,
+ });
+
+ return (
+
+
+ {isValidDrop(data, active) && }
+
+
+ );
+};
+
+export default memo(IAISortableItem);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx
index 49a676008a..c4fbbb2cd7 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx
@@ -23,11 +23,12 @@ const LinearViewField = ({ nodeId, fieldName }: Props) => {
const dispatch = useAppDispatch();
const { isMouseOverNode, handleMouseOut, handleMouseOver } = useMouseOverNode(nodeId);
const { t } = useTranslation();
+
const handleRemoveField = useCallback(() => {
dispatch(workflowExposedFieldRemoved({ nodeId, fieldName }));
}, [dispatch, fieldName, nodeId]);
- const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: nodeId });
+ const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: `${nodeId}.${fieldName}` });
const style = {
transform: CSS.Transform.toString(transform),
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx
index fa6bfff41d..3c7dae463c 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLinearTab.tsx
@@ -1,15 +1,15 @@
-
-import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
+import { DndContext } from '@dnd-kit/core';
+import { arrayMove, SortableContext } from '@dnd-kit/sortable';
import { Box, Flex } from '@invoke-ai/ui-library';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
-import { useAppSelector } from 'app/store/storeHooks';
-import IAIDroppable from 'common/components/IAIDroppable';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
-import type { TypesafeDroppableData } from 'features/dnd/types';
+import type { DragEndEvent } from 'features/dnd/types';
import LinearViewField from 'features/nodes/components/flow/nodes/Invocation/fields/LinearViewField';
-import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
-import { memo, useMemo } from 'react';
+import { selectWorkflowSlice, workflowExposedFieldsReordered } from 'features/nodes/store/workflowSlice';
+import { FieldIdentifier } from 'features/nodes/types/field';
+import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
@@ -19,32 +19,46 @@ const WorkflowLinearTab = () => {
const fields = useAppSelector(selector);
const { isLoading } = useGetOpenAPISchemaQuery();
const { t } = useTranslation();
+ const dispatch = useAppDispatch();
- const droppableData = useMemo(
- () => ({
- id: 'current-image',
- actionType: 'SET_CURRENT_IMAGE',
- }),
- []
+ const handleDragEnd = useCallback(
+ (event: DragEndEvent) => {
+ const { active, over } = event;
+ console.log({ active, over });
+ const fieldsStrings = fields.map((field) => `${field.nodeId}.${field.fieldName}`);
+
+ if (over && active.id !== over.id) {
+ const oldIndex = fieldsStrings.indexOf(active.id as string);
+ const newIndex = fieldsStrings.indexOf(over.id as string);
+
+ const newFields = arrayMove(fieldsStrings, oldIndex, newIndex)
+ .map((field) => fields.find((obj) => `${obj.nodeId}.${obj.fieldName}` === field))
+ .filter((field) => field) as FieldIdentifier[];
+
+ dispatch(workflowExposedFieldsReordered(newFields));
+ }
+ },
+ [dispatch, fields]
);
return (
-
- field.nodeId)} strategy={verticalListSortingStrategy}>
-
- {isLoading ? (
-
- ) : fields.length ? (
- fields.map(({ nodeId, fieldName }) => (
-
- ))
- ) : (
-
- )}
-
-
+
+ `${field.nodeId}.${field.fieldName}`)}>
+
+ {isLoading ? (
+
+ ) : fields.length ? (
+ fields.map(({ nodeId, fieldName }) => (
+
+ ))
+ ) : (
+
+ )}
+
+
+
);
diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts
index 2418f65ceb..f4dad3f668 100644
--- a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts
@@ -42,11 +42,8 @@ export const workflowSlice = createSlice({
state.exposedFields = state.exposedFields.filter((field) => !isEqual(field, action.payload));
state.isTouched = true;
},
- workflowExposedFieldsReordered: (state, action: PayloadAction) => {
- state.exposedFields = action.payload.split(',').map((id) => {
- const [nodeId, fieldName] = id.split('.');
- return { nodeId, fieldName };
- });
+ workflowExposedFieldsReordered: (state, action: PayloadAction) => {
+ state.exposedFields = action.payload;
state.isTouched = true;
},
workflowNameChanged: (state, action: PayloadAction) => {
@@ -113,6 +110,7 @@ export const workflowSlice = createSlice({
export const {
workflowExposedFieldAdded,
workflowExposedFieldRemoved,
+ workflowExposedFieldsReordered,
workflowNameChanged,
workflowCategoryChanged,
workflowDescriptionChanged,