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,