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;
};