feat(ui): split workflow editor settings to separate slice

We need the undoable slice to be only undoable state - settings are not undoable.
This commit is contained in:
psychedelicious 2024-05-15 17:20:51 +10:00
parent 27826369f0
commit 9c0d44b412
15 changed files with 122 additions and 84 deletions

View File

@ -22,6 +22,7 @@ import { hrfPersistConfig, hrfSlice } from 'features/hrf/store/hrfSlice';
import { loraPersistConfig, loraSlice } from 'features/lora/store/loraSlice';
import { modelManagerV2PersistConfig, modelManagerV2Slice } from 'features/modelManagerV2/store/modelManagerV2Slice';
import { nodesPersistConfig, nodesSlice, nodesUndoableConfig } from 'features/nodes/store/nodesSlice';
import { workflowSettingsPersistConfig, workflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workflowSlice';
import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice';
import { postprocessingPersistConfig, postprocessingSlice } from 'features/parameters/store/postprocessingSlice';
@ -66,6 +67,7 @@ const allReducers = {
[workflowSlice.name]: workflowSlice.reducer,
[hrfSlice.name]: hrfSlice.reducer,
[controlLayersSlice.name]: undoable(controlLayersSlice.reducer, controlLayersUndoableConfig),
[workflowSettingsSlice.name]: workflowSettingsSlice.reducer,
[api.reducerPath]: api.reducer,
};
@ -111,6 +113,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
[modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig,
[hrfPersistConfig.name]: hrfPersistConfig,
[controlLayersPersistConfig.name]: controlLayersPersistConfig,
[workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig,
};
const unserialize: UnserializeFunction = (data, key) => {

View File

@ -10,6 +10,7 @@ import type { Layer } from 'features/controlLayers/store/types';
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
import { selectSystemSlice } from 'features/system/store/systemSlice';
@ -31,11 +32,12 @@ const selector = createMemoizedSelector(
selectGenerationSlice,
selectSystemSlice,
selectNodesSlice,
selectWorkflowSettingsSlice,
selectDynamicPromptsSlice,
selectControlLayersSlice,
activeTabNameSelector,
],
(controlAdapters, generation, system, nodes, dynamicPrompts, controlLayers, activeTabName) => {
(controlAdapters, generation, system, nodes, workflowSettings, dynamicPrompts, controlLayers, activeTabName) => {
const { model } = generation;
const { size } = controlLayers.present;
const { positivePrompt } = controlLayers.present;
@ -50,7 +52,7 @@ const selector = createMemoizedSelector(
}
if (activeTabName === 'workflows') {
if (nodes.shouldValidateGraph) {
if (workflowSettings.shouldValidateGraph) {
if (!nodes.nodes.length) {
reasons.push({ content: i18n.t('parameters.invoke.noNodesInGraph') });
}

View File

@ -75,8 +75,8 @@ export const Flow = memo(() => {
const nodes = useAppSelector((s) => s.nodes.present.nodes);
const edges = useAppSelector((s) => s.nodes.present.edges);
const viewport = useAppSelector((s) => s.nodes.present.viewport);
const shouldSnapToGrid = useAppSelector((s) => s.nodes.present.shouldSnapToGrid);
const selectionMode = useAppSelector((s) => s.nodes.present.selectionMode);
const shouldSnapToGrid = useAppSelector((s) => s.workflowSettings.shouldSnapToGrid);
const selectionMode = useAppSelector((s) => s.workflowSettings.selectionMode);
const flowWrapper = useRef<HTMLDivElement>(null);
const cursorPosition = useRef<XYPosition | null>(null);
const isValidConnection = useIsValidConnection();

View File

@ -3,17 +3,20 @@ import { useAppSelector } from 'app/store/storeHooks';
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
import type { CSSProperties } from 'react';
import { memo } from 'react';
import type { ConnectionLineComponentProps } from 'reactflow';
import { getBezierPath } from 'reactflow';
const selectStroke = createSelector(selectNodesSlice, (nodes) =>
nodes.shouldColorEdges ? getFieldColor(nodes.connectionStartFieldType) : colorTokenToCssVar('base.500')
const selectStroke = createSelector([selectNodesSlice, selectWorkflowSettingsSlice], (nodes, workflowSettings) =>
workflowSettings.shouldColorEdges ? getFieldColor(nodes.connectionStartFieldType) : colorTokenToCssVar('base.500')
);
const selectClassName = createSelector(selectNodesSlice, (nodes) =>
nodes.shouldAnimateEdges ? 'react-flow__custom_connection-path animated' : 'react-flow__custom_connection-path'
const selectClassName = createSelector(selectWorkflowSettingsSlice, (workflowSettings) =>
workflowSettings.shouldAnimateEdges
? 'react-flow__custom_connection-path animated'
: 'react-flow__custom_connection-path'
);
const pathStyles: CSSProperties = { opacity: 0.8 };

View File

@ -27,7 +27,7 @@ const InvocationDefaultEdge = ({
);
const { isSelected, shouldAnimate, stroke, label } = useAppSelector(selector);
const shouldShowEdgeLabels = useAppSelector((s) => s.nodes.present.shouldShowEdgeLabels);
const shouldShowEdgeLabels = useAppSelector((s) => s.workflowSettings.shouldShowEdgeLabels);
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,

View File

@ -2,6 +2,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectFieldOutputTemplate, selectNodeTemplate } from 'features/nodes/store/selectors';
import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { getFieldColor } from './getEdgeColor';
@ -22,7 +23,8 @@ export const makeEdgeSelector = (
) =>
createMemoizedSelector(
selectNodesSlice,
(nodes): { isSelected: boolean; shouldAnimate: boolean; stroke: string; label: string } => {
selectWorkflowSettingsSlice,
(nodes, workflowSettings): { isSelected: boolean; shouldAnimate: boolean; stroke: string; label: string } => {
const sourceNode = nodes.nodes.find((node) => node.id === source);
const targetNode = nodes.nodes.find((node) => node.id === target);
@ -36,7 +38,7 @@ export const makeEdgeSelector = (
const outputFieldTemplate = selectFieldOutputTemplate(nodes, sourceNode.id, sourceHandleId);
const sourceType = isInvocationToInvocationEdge ? outputFieldTemplate?.type : undefined;
const stroke = sourceType && nodes.shouldColorEdges ? getFieldColor(sourceType) : colorTokenToCssVar('base.500');
const stroke = sourceType && workflowSettings.shouldColorEdges ? getFieldColor(sourceType) : colorTokenToCssVar('base.500');
const sourceNodeTemplate = selectNodeTemplate(nodes, sourceNode.id);
const targetNodeTemplate = selectNodeTemplate(nodes, targetNode.id);
@ -45,7 +47,7 @@ export const makeEdgeSelector = (
return {
isSelected,
shouldAnimate: nodes.shouldAnimateEdges && isSelected,
shouldAnimate: workflowSettings.shouldAnimateEdges && isSelected,
stroke,
label,
};

View File

@ -39,7 +39,7 @@ const NodeWrapper = (props: NodeWrapperProps) => {
const dispatch = useAppDispatch();
const opacity = useAppSelector((s) => s.nodes.present.nodeOpacity);
const opacity = useAppSelector((s) => s.workflowSettings.nodeOpacity);
const { onCloseGlobal } = useGlobalMenuClose();
const handleClick = useCallback(

View File

@ -1,12 +1,12 @@
import { CompositeSlider, Flex } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { nodeOpacityChanged } from 'features/nodes/store/nodesSlice';
import { nodeOpacityChanged } from 'features/nodes/store/workflowSettingsSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const NodeOpacitySlider = () => {
const dispatch = useAppDispatch();
const nodeOpacity = useAppSelector((s) => s.nodes.present.nodeOpacity);
const nodeOpacity = useAppSelector((s) => s.workflowSettings.nodeOpacity);
const { t } = useTranslation();
const handleChange = useCallback(

View File

@ -1,9 +1,6 @@
import { ButtonGroup, IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
// shouldShowFieldTypeLegendChanged,
shouldShowMinimapPanelChanged,
} from 'features/nodes/store/nodesSlice';
import { shouldShowMinimapPanelChanged } from 'features/nodes/store/workflowSettingsSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
@ -21,7 +18,7 @@ const ViewportControls = () => {
// const shouldShowFieldTypeLegend = useAppSelector(
// (s) => s.nodes.present.shouldShowFieldTypeLegend
// );
const shouldShowMinimapPanel = useAppSelector((s) => s.nodes.present.shouldShowMinimapPanel);
const shouldShowMinimapPanel = useAppSelector((s) => s.workflowSettings.shouldShowMinimapPanel);
const handleClickedZoomIn = useCallback(() => {
zoomIn();

View File

@ -16,7 +16,7 @@ const minimapStyles: SystemStyleObject = {
};
const MinimapPanel = () => {
const shouldShowMinimapPanel = useAppSelector((s) => s.nodes.present.shouldShowMinimapPanel);
const shouldShowMinimapPanel = useAppSelector((s) => s.workflowSettings.shouldShowMinimapPanel);
return (
<Flex gap={2} position="absolute" bottom={0} insetInlineEnd={0}>

View File

@ -21,13 +21,13 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import ReloadNodeTemplatesButton from 'features/nodes/components/flow/panels/TopRightPanel/ReloadSchemaButton';
import {
selectionModeChanged,
selectNodesSlice,
selectWorkflowSettingsSlice,
shouldAnimateEdgesChanged,
shouldColorEdgesChanged,
shouldShowEdgeLabelsChanged,
shouldSnapToGridChanged,
shouldValidateGraphChanged,
} from 'features/nodes/store/nodesSlice';
} from 'features/nodes/store/workflowSettingsSlice';
import type { ChangeEvent, ReactNode } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -35,7 +35,7 @@ import { SelectionMode } from 'reactflow';
const formLabelProps: FormLabelProps = { flexGrow: 1 };
const selector = createMemoizedSelector(selectNodesSlice, (nodes) => {
const selector = createMemoizedSelector(selectWorkflowSettingsSlice, (workflowSettings) => {
const {
shouldAnimateEdges,
shouldValidateGraph,
@ -43,7 +43,7 @@ const selector = createMemoizedSelector(selectNodesSlice, (nodes) => {
shouldColorEdges,
shouldShowEdgeLabels,
selectionMode,
} = nodes;
} = workflowSettings;
return {
shouldAnimateEdges,
shouldValidateGraph,

View File

@ -13,7 +13,7 @@ import type { Connection, Node } from 'reactflow';
export const useIsValidConnection = () => {
const store = useAppStore();
const shouldValidateGraph = useAppSelector((s) => s.nodes.present.shouldValidateGraph);
const shouldValidateGraph = useAppSelector((s) => s.workflowSettings.shouldValidateGraph);
const isValidConnection = useCallback(
({ source, sourceHandle, target, targetHandle }: Connection): boolean => {
// Connection must have valid targets

View File

@ -57,15 +57,7 @@ import type {
Viewport,
XYPosition,
} from 'reactflow';
import {
addEdge,
applyEdgeChanges,
applyNodeChanges,
getConnectedEdges,
getIncomers,
getOutgoers,
SelectionMode,
} from 'reactflow';
import { addEdge, applyEdgeChanges, applyNodeChanges, getConnectedEdges, getIncomers, getOutgoers } from 'reactflow';
import type { UndoableOptions } from 'redux-undo';
import {
socketGeneratorProgress,
@ -99,21 +91,13 @@ const initialNodesState: NodesState = {
connectionMade: false,
modifyingEdge: false,
addNewNodePosition: null,
shouldShowMinimapPanel: true,
shouldValidateGraph: true,
shouldAnimateEdges: true,
shouldSnapToGrid: false,
shouldColorEdges: true,
shouldShowEdgeLabels: false,
isAddNodePopoverOpen: false,
nodeOpacity: 1,
selectedNodes: [],
selectedEdges: [],
nodeExecutionStates: {},
viewport: { x: 0, y: 0, zoom: 1 },
nodesToCopy: [],
edgesToCopy: [],
selectionMode: SelectionMode.Partial,
};
type FieldValueAction<T extends FieldValue> = PayloadAction<{
@ -538,31 +522,10 @@ export const nodesSlice = createSlice({
}
node.data.notes = value;
},
shouldShowMinimapPanelChanged: (state, action: PayloadAction<boolean>) => {
state.shouldShowMinimapPanel = action.payload;
},
nodeEditorReset: (state) => {
state.nodes = [];
state.edges = [];
},
shouldValidateGraphChanged: (state, action: PayloadAction<boolean>) => {
state.shouldValidateGraph = action.payload;
},
shouldAnimateEdgesChanged: (state, action: PayloadAction<boolean>) => {
state.shouldAnimateEdges = action.payload;
},
shouldShowEdgeLabelsChanged: (state, action: PayloadAction<boolean>) => {
state.shouldShowEdgeLabels = action.payload;
},
shouldSnapToGridChanged: (state, action: PayloadAction<boolean>) => {
state.shouldSnapToGrid = action.payload;
},
shouldColorEdgesChanged: (state, action: PayloadAction<boolean>) => {
state.shouldColorEdges = action.payload;
},
nodeOpacityChanged: (state, action: PayloadAction<number>) => {
state.nodeOpacity = action.payload;
},
viewportChanged: (state, action: PayloadAction<Viewport>) => {
state.viewport = action.payload;
},
@ -700,9 +663,6 @@ export const nodesSlice = createSlice({
state.connectionStartParams = null;
state.connectionStartFieldType = null;
},
selectionModeChanged: (state, action: PayloadAction<boolean>) => {
state.selectionMode = action.payload ? SelectionMode.Full : SelectionMode.Partial;
},
nodeTemplatesBuilt: (state, action: PayloadAction<Record<string, InvocationTemplate>>) => {
state.templates = action.payload;
},
@ -819,7 +779,6 @@ export const {
nodeIsOpenChanged,
nodeLabelChanged,
nodeNotesChanged,
nodeOpacityChanged,
nodesChanged,
nodesDeleted,
nodeUseCacheChanged,
@ -828,17 +787,10 @@ export const {
selectedEdgesChanged,
selectedNodesChanged,
selectionCopied,
selectionModeChanged,
selectionPasted,
shouldAnimateEdgesChanged,
shouldColorEdgesChanged,
shouldShowMinimapPanelChanged,
shouldSnapToGridChanged,
shouldValidateGraphChanged,
viewportChanged,
edgeAdded,
nodeTemplatesBuilt,
shouldShowEdgeLabelsChanged,
undo,
redo,
} = nodesSlice.actions;

View File

@ -6,7 +6,7 @@ import type {
NodeExecutionState,
} from 'features/nodes/types/invocation';
import type { WorkflowV3 } from 'features/nodes/types/workflow';
import type { OnConnectStartParams, SelectionMode, Viewport, XYPosition } from 'reactflow';
import type { OnConnectStartParams, Viewport, XYPosition } from 'reactflow';
export type NodesState = {
_version: 1;
@ -17,13 +17,6 @@ export type NodesState = {
connectionStartFieldType: FieldType | null;
connectionMade: boolean;
modifyingEdge: boolean;
shouldShowMinimapPanel: boolean;
shouldValidateGraph: boolean;
shouldAnimateEdges: boolean;
nodeOpacity: number;
shouldSnapToGrid: boolean;
shouldColorEdges: boolean;
shouldShowEdgeLabels: boolean;
selectedNodes: string[];
selectedEdges: string[];
nodeExecutionStates: Record<string, NodeExecutionState>;
@ -32,7 +25,6 @@ export type NodesState = {
edgesToCopy: InvocationNodeEdge[];
isAddNodePopoverOpen: boolean;
addNewNodePosition: XYPosition | null;
selectionMode: SelectionMode;
};
export type WorkflowMode = 'edit' | 'view';

View File

@ -0,0 +1,87 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { SelectionMode } from 'reactflow';
export type WorkflowSettingsState = {
_version: 1;
shouldShowMinimapPanel: boolean;
shouldValidateGraph: boolean;
shouldAnimateEdges: boolean;
nodeOpacity: number;
shouldSnapToGrid: boolean;
shouldColorEdges: boolean;
shouldShowEdgeLabels: boolean;
selectionMode: SelectionMode;
};
const initialState: WorkflowSettingsState = {
_version: 1,
shouldShowMinimapPanel: true,
shouldValidateGraph: true,
shouldAnimateEdges: true,
shouldSnapToGrid: false,
shouldColorEdges: true,
shouldShowEdgeLabels: false,
nodeOpacity: 1,
selectionMode: SelectionMode.Partial,
};
export const workflowSettingsSlice = createSlice({
name: 'workflowSettings',
initialState,
reducers: {
shouldShowMinimapPanelChanged: (state, action: PayloadAction<boolean>) => {
state.shouldShowMinimapPanel = action.payload;
},
shouldValidateGraphChanged: (state, action: PayloadAction<boolean>) => {
state.shouldValidateGraph = action.payload;
},
shouldAnimateEdgesChanged: (state, action: PayloadAction<boolean>) => {
state.shouldAnimateEdges = action.payload;
},
shouldShowEdgeLabelsChanged: (state, action: PayloadAction<boolean>) => {
state.shouldShowEdgeLabels = action.payload;
},
shouldSnapToGridChanged: (state, action: PayloadAction<boolean>) => {
state.shouldSnapToGrid = action.payload;
},
shouldColorEdgesChanged: (state, action: PayloadAction<boolean>) => {
state.shouldColorEdges = action.payload;
},
nodeOpacityChanged: (state, action: PayloadAction<number>) => {
state.nodeOpacity = action.payload;
},
selectionModeChanged: (state, action: PayloadAction<boolean>) => {
state.selectionMode = action.payload ? SelectionMode.Full : SelectionMode.Partial;
},
},
});
export const {
shouldAnimateEdgesChanged,
shouldColorEdgesChanged,
shouldShowMinimapPanelChanged,
shouldShowEdgeLabelsChanged,
shouldSnapToGridChanged,
shouldValidateGraphChanged,
nodeOpacityChanged,
selectionModeChanged,
} = workflowSettingsSlice.actions;
export const selectWorkflowSettingsSlice = (state: RootState) => state.workflowSettings;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrateWorkflowSettingsState = (state: any): any => {
if (!('_version' in state)) {
state._version = 1;
}
return state;
};
export const workflowSettingsPersistConfig: PersistConfig<WorkflowSettingsState> = {
name: workflowSettingsSlice.name,
initialState,
migrate: migrateWorkflowSettingsState,
persistDenylist: [],
};