From 6cf5b402c6a96ba1ac106eb3cec9f25825e74794 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 16 May 2024 21:10:43 +1000 Subject: [PATCH] feat(ui): remove extraneous selectedEdges and selectedNodes state --- .../features/nodes/components/flow/Flow.tsx | 12 -- .../sidePanel/inspector/InspectorDataTab.tsx | 16 +-- .../inspector/InspectorDetailsTab.tsx | 4 +- .../inspector/InspectorOutputsTab.tsx | 6 +- .../inspector/InspectorTemplateTab.tsx | 4 +- .../src/features/nodes/store/nodesSlice.ts | 126 ++++++++++-------- .../web/src/features/nodes/store/selectors.ts | 8 ++ .../web/src/features/nodes/store/types.ts | 2 - 8 files changed, 92 insertions(+), 86 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 44c7e1ce7b..dd10eb8cba 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -15,8 +15,6 @@ import { nodesDeleted, redo, selectedAll, - selectedEdgesChanged, - selectedNodesChanged, undo, viewportChanged, } from 'features/nodes/store/nodesSlice'; @@ -32,7 +30,6 @@ import type { OnMoveEnd, OnNodesChange, OnNodesDelete, - OnSelectionChangeFunc, ProOptions, ReactFlowProps, } from 'reactflow'; @@ -111,14 +108,6 @@ export const Flow = memo(() => { [dispatch] ); - const handleSelectionChange: OnSelectionChangeFunc = useCallback( - ({ nodes, edges }) => { - dispatch(selectedNodesChanged(nodes ? nodes.map((n) => n.id) : [])); - dispatch(selectedEdgesChanged(edges ? edges.map((e) => e.id) : [])); - }, - [dispatch] - ); - const handleMoveEnd: OnMoveEnd = useCallback( (e, viewport) => { dispatch(viewportChanged(viewport)); @@ -258,7 +247,6 @@ export const Flow = memo(() => { onConnectEnd={onConnectEnd} onMoveEnd={handleMoveEnd} connectionLineComponent={CustomConnectionLine} - onSelectionChange={handleSelectionChange} isValidConnection={isValidConnection} minZoom={0.1} snapToGrid={shouldSnapToGrid} diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDataTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDataTab.tsx index 8f1a3249ee..af0ea710d6 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDataTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDataTab.tsx @@ -3,27 +3,21 @@ import { useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectLastSelectedNode } from 'features/nodes/store/selectors'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -const selector = createMemoizedSelector(selectNodesSlice, (nodes) => { - const lastSelectedNodeId = nodes.selectedNodes[nodes.selectedNodes.length - 1]; - const lastSelectedNode = nodes.nodes.find((node) => node.id === lastSelectedNodeId); - - return { - data: lastSelectedNode?.data, - }; -}); +const selector = createMemoizedSelector(selectNodesSlice, (nodes) => selectLastSelectedNode(nodes)); const InspectorDataTab = () => { const { t } = useTranslation(); - const { data } = useAppSelector(selector); + const lastSelectedNode = useAppSelector(selector); - if (!data) { + if (!lastSelectedNode) { return ; } - return ; + return ; }; export default memo(InspectorDataTab); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx index 354a0ed179..f38fa819dd 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx @@ -7,6 +7,7 @@ import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableCon import NotesTextarea from 'features/nodes/components/flow/nodes/Invocation/NotesTextarea'; import { useNodeNeedsUpdate } from 'features/nodes/hooks/useNodeNeedsUpdate'; import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectLastSelectedNode } from 'features/nodes/store/selectors'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -18,8 +19,7 @@ const InspectorDetailsTab = () => { const selector = useMemo( () => createMemoizedSelector(selectNodesSlice, (nodes) => { - const lastSelectedNodeId = nodes.selectedNodes[nodes.selectedNodes.length - 1]; - const lastSelectedNode = nodes.nodes.find((node) => node.id === lastSelectedNodeId); + const lastSelectedNode = selectLastSelectedNode(nodes); const lastSelectedNodeTemplate = lastSelectedNode ? templates[lastSelectedNode.data.type] : undefined; if (!isInvocationNode(lastSelectedNode) || !lastSelectedNodeTemplate) { diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx index 381a510b8b..17a1dd33f1 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx @@ -6,6 +6,7 @@ import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectLastSelectedNode } from 'features/nodes/store/selectors'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -19,11 +20,10 @@ const InspectorOutputsTab = () => { const selector = useMemo( () => createMemoizedSelector(selectNodesSlice, (nodes) => { - const lastSelectedNodeId = nodes.selectedNodes[nodes.selectedNodes.length - 1]; - const lastSelectedNode = nodes.nodes.find((node) => node.id === lastSelectedNodeId); + const lastSelectedNode = selectLastSelectedNode(nodes); const lastSelectedNodeTemplate = lastSelectedNode ? templates[lastSelectedNode.data.type] : undefined; - const nes = nodes.nodeExecutionStates[lastSelectedNodeId ?? '__UNKNOWN_NODE__']; + const nes = nodes.nodeExecutionStates[lastSelectedNode?.id ?? '__UNKNOWN_NODE__']; if (!isInvocationNode(lastSelectedNode) || !nes || !lastSelectedNodeTemplate) { return; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx index fbe86ba32c..d95b215dd6 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx @@ -4,6 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectLastSelectedNode } from 'features/nodes/store/selectors'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,8 +13,7 @@ const NodeTemplateInspector = () => { const selector = useMemo( () => createMemoizedSelector(selectNodesSlice, (nodes) => { - const lastSelectedNodeId = nodes.selectedNodes[nodes.selectedNodes.length - 1]; - const lastSelectedNode = nodes.nodes.find((node) => node.id === lastSelectedNodeId); + const lastSelectedNode = selectLastSelectedNode(nodes); const lastSelectedNodeTemplate = lastSelectedNode ? templates[lastSelectedNode.data.type] : undefined; return lastSelectedNodeTemplate; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 69530902a4..47bf3a5ab1 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -74,8 +74,6 @@ const initialNodesState: NodesState = { _version: 1, nodes: [], edges: [], - selectedNodes: [], - selectedEdges: [], nodeExecutionStates: {}, viewport: { x: 0, y: 0, zoom: 1 }, }; @@ -351,12 +349,6 @@ export const nodesSlice = createSlice({ state.nodes ); }, - selectedNodesChanged: (state, action: PayloadAction) => { - state.selectedNodes = action.payload; - }, - selectedEdgesChanged: (state, action: PayloadAction) => { - state.selectedEdges = action.payload; - }, fieldValueReset: (state, action: FieldValueAction) => { fieldValueReducer(state, action, zStatefulFieldValue); }, @@ -593,8 +585,6 @@ export const { nodeUseCacheChanged, notesNodeValueChanged, selectedAll, - selectedEdgesChanged, - selectedNodesChanged, selectionPasted, viewportChanged, edgeAdded, @@ -602,6 +592,78 @@ export const { redo, } = nodesSlice.actions; +export const $cursorPos = atom(null); +export const $templates = atom({}); +export const $copiedNodes = atom([]); +export const $copiedEdges = atom([]); +export const $pendingConnection = atom(null); +export const $isModifyingEdge = atom(false); +export const $isAddNodePopoverOpen = atom(false); +export const closeAddNodePopover = () => { + $isAddNodePopoverOpen.set(false); + $pendingConnection.set(null); +}; +export const openAddNodePopover = () => { + $isAddNodePopoverOpen.set(true); +}; + +export const selectNodesSlice = (state: RootState) => state.nodes.present; + +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const migrateNodesState = (state: any): any => { + if (!('_version' in state)) { + state._version = 1; + } + return state; +}; + +export const nodesPersistConfig: PersistConfig = { + name: nodesSlice.name, + initialState: initialNodesState, + migrate: migrateNodesState, + persistDenylist: [], +}; + +const selectionMatcher = isAnyOf(selectedAll, selectionPasted, nodeExclusivelySelected); + +const isSelectionAction = (action: UnknownAction) => { + if (selectionMatcher(action)) { + return true; + } + if (nodesChanged.match(action)) { + if (action.payload.every((change) => change.type === 'select')) { + return true; + } + } + return false; +}; + +const individualGroupByMatcher = isAnyOf(nodesChanged, viewportChanged); + +export const nodesUndoableConfig: UndoableOptions = { + limit: 64, + undoType: nodesSlice.actions.undo.type, + redoType: nodesSlice.actions.redo.type, + groupBy: (action, state, history) => { + if (isSelectionAction(action)) { + // Changes to selection should never be recorded on their own + return history.group; + } + if (individualGroupByMatcher(action)) { + return action.type; + } + return null; + }, + filter: (action, _state, _history) => { + if (nodesChanged.match(action)) { + if (action.payload.every((change) => change.type === 'dimensions')) { + return false; + } + } + return true; + }, +}; + // This is used for tracking `state.workflow.isTouched` export const isAnyNodeOrEdgeMutation = isAnyOf( connectionMade, @@ -636,47 +698,3 @@ export const isAnyNodeOrEdgeMutation = isAnyOf( selectionPasted, edgeAdded ); - -export const $cursorPos = atom(null); -export const $templates = atom({}); -export const $copiedNodes = atom([]); -export const $copiedEdges = atom([]); -export const $pendingConnection = atom(null); -export const $isModifyingEdge = atom(false); -export const $isAddNodePopoverOpen = atom(false); -export const closeAddNodePopover = () => { - $isAddNodePopoverOpen.set(false); - $pendingConnection.set(null); -}; -export const openAddNodePopover = () => { - $isAddNodePopoverOpen.set(true); -}; - -export const selectNodesSlice = (state: RootState) => state.nodes.present; - -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -const migrateNodesState = (state: any): any => { - if (!('_version' in state)) { - state._version = 1; - } - return state; -}; - -export const nodesPersistConfig: PersistConfig = { - name: nodesSlice.name, - initialState: initialNodesState, - migrate: migrateNodesState, - persistDenylist: ['selectedNodes', 'selectedEdges'], -}; - -export const nodesUndoableConfig: UndoableOptions = { - limit: 64, - undoType: nodesSlice.actions.undo.type, - redoType: nodesSlice.actions.redo.type, - groupBy: (action, state, history) => { - return null; - }, - filter: (action, _state, _history) => { - return true; - }, -}; diff --git a/invokeai/frontend/web/src/features/nodes/store/selectors.ts b/invokeai/frontend/web/src/features/nodes/store/selectors.ts index 6d1e5e38ec..be8cfafa8b 100644 --- a/invokeai/frontend/web/src/features/nodes/store/selectors.ts +++ b/invokeai/frontend/web/src/features/nodes/store/selectors.ts @@ -28,3 +28,11 @@ export const selectFieldInputInstance = ( const data = selectNodeData(nodesSlice, nodeId); return data?.inputs[fieldName] ?? null; }; + +export const selectLastSelectedNode = (nodesSlice: NodesState) => { + const selectedNodes = nodesSlice.nodes.filter((n) => n.selected); + if (selectedNodes.length === 1) { + return selectedNodes[0]; + } + return null; +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/types.ts b/invokeai/frontend/web/src/features/nodes/store/types.ts index e2249b177e..47e9ecaf85 100644 --- a/invokeai/frontend/web/src/features/nodes/store/types.ts +++ b/invokeai/frontend/web/src/features/nodes/store/types.ts @@ -26,8 +26,6 @@ export type NodesState = { _version: 1; nodes: AnyNode[]; edges: InvocationNodeEdge[]; - selectedNodes: string[]; - selectedEdges: string[]; nodeExecutionStates: Record; viewport: Viewport; };