diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index e583169af7..cd95183c7a 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -52,6 +52,7 @@ "@chakra-ui/react-use-size": "^2.1.0", "@dagrejs/graphlib": "^2.1.13", "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", "@fontsource-variable/inter": "^5.0.16", "@invoke-ai/ui-library": "^0.0.18", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index ba76a61275..1d9083d1b4 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -22,6 +22,9 @@ dependencies: '@dnd-kit/core': specifier: ^6.1.0 version: 6.1.0(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/sortable': + specifier: ^8.0.0 + version: 8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0) '@dnd-kit/utilities': specifier: ^3.2.2 version: 3.2.2(react@18.2.0) @@ -2884,6 +2887,18 @@ packages: tslib: 2.6.2 dev: false + /@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0): + resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==} + peerDependencies: + '@dnd-kit/core': ^6.1.0 + react: '>=16.8.0' + dependencies: + '@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/utilities': 3.2.2(react@18.2.0) + react: 18.2.0 + tslib: 2.6.2 + dev: false + /@dnd-kit/utilities@3.2.2(react@18.2.0): resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} peerDependencies: diff --git a/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx b/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx index f800e5c869..a28b865366 100644 --- a/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx +++ b/invokeai/frontend/web/src/features/dnd/components/AppDndContext.tsx @@ -19,6 +19,7 @@ const AppDndContext = (props: PropsWithChildren) => { const handleDragStart = useCallback( (event: DragStartEvent) => { + console.log('handling drag start', event.active.data.current); log.trace({ dragData: parseify(event.active.data.current) }, 'Drag started'); const activeData = event.active.data.current; if (!activeData) { @@ -31,6 +32,7 @@ const AppDndContext = (props: PropsWithChildren) => { const handleDragEnd = useCallback( (event: DragEndEvent) => { + console.log('handling drag end', event.active.data.current); log.trace({ dragData: parseify(event.active.data.current) }, 'Drag ended'); const overData = event.over?.data.current; if (!activeDragData || !overData) { diff --git a/invokeai/frontend/web/src/features/dnd/types/index.ts b/invokeai/frontend/web/src/features/dnd/types/index.ts index 6e680b4ba9..d4dc59de98 100644 --- a/invokeai/frontend/web/src/features/dnd/types/index.ts +++ b/invokeai/frontend/web/src/features/dnd/types/index.ts @@ -80,6 +80,14 @@ export type NodeFieldDraggableData = BaseDragData & { }; }; +export type LinearViewFieldDraggableData = BaseDragData & { + payloadType: 'LINEAR_VIEW_FIELD'; + payload: { + nodeId: string; + fieldName: string; + }; +}; + export type ImageDraggableData = BaseDragData & { payloadType: 'IMAGE_DTO'; payload: { imageDTO: ImageDTO }; @@ -90,7 +98,11 @@ export type GallerySelectionDraggableData = BaseDragData & { payload: { boardId: BoardId }; }; -export type TypesafeDraggableData = NodeFieldDraggableData | ImageDraggableData | GallerySelectionDraggableData; +export type TypesafeDraggableData = + | NodeFieldDraggableData + | LinearViewFieldDraggableData + | ImageDraggableData + | GallerySelectionDraggableData; export interface UseDroppableTypesafeArguments extends Omit { data?: TypesafeDroppableData; 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 3ec4ab1b42..49a676008a 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 @@ -1,3 +1,5 @@ +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; import { Flex, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay'; @@ -25,6 +27,13 @@ const LinearViewField = ({ nodeId, fieldName }: Props) => { dispatch(workflowExposedFieldRemoved({ nodeId, fieldName })); }, [dispatch, fieldName, nodeId]); + const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: nodeId }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + return ( { w="full" p={4} flexDir="column" + ref={setNodeRef} + style={style} + {...attributes} + {...listeners} > 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 375b271c66..fa6bfff41d 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,11 +1,15 @@ + +import { SortableContext, verticalListSortingStrategy } 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 { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; +import type { TypesafeDroppableData } from 'features/dnd/types'; import LinearViewField from 'features/nodes/components/flow/nodes/Invocation/fields/LinearViewField'; import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo'; @@ -16,20 +20,31 @@ const WorkflowLinearTab = () => { const { isLoading } = useGetOpenAPISchemaQuery(); const { t } = useTranslation(); + const droppableData = useMemo( + () => ({ + id: 'current-image', + actionType: 'SET_CURRENT_IMAGE', + }), + [] + ); + return ( + - - {isLoading ? ( - - ) : fields.length ? ( - fields.map(({ nodeId, fieldName }) => ( - - )) - ) : ( - - )} - + field.nodeId)} strategy={verticalListSortingStrategy}> + + {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 73802da54e..2418f65ceb 100644 --- a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts @@ -42,6 +42,13 @@ 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 }; + }); + state.isTouched = true; + }, workflowNameChanged: (state, action: PayloadAction) => { state.name = action.payload; state.isTouched = true;