feat(ui): split out workflow redux state

The `nodes` slice is a rather complicated slice. Removing `workflow` makes it a bit more reasonable.

Also helps to flatten state out a bit.
This commit is contained in:
psychedelicious 2023-12-02 23:06:34 +11:00
parent 92bc04dc87
commit 0f32d260b7
16 changed files with 174 additions and 117 deletions

View File

@ -1646,5 +1646,8 @@
"clearWorkflowSearchFilter": "Clear Workflow Search Filter", "clearWorkflowSearchFilter": "Clear Workflow Search Filter",
"workflowName": "Workflow Name", "workflowName": "Workflow Name",
"workflowEditorReset": "Workflow Editor Reset" "workflowEditorReset": "Workflow Editor Reset"
},
"app": {
"storeNotInitialized": "Store is not initialized"
} }
} }

View File

@ -11,12 +11,18 @@ export const addEnqueueRequestedNodes = () => {
enqueueRequested.match(action) && action.payload.tabName === 'nodes', enqueueRequested.match(action) && action.payload.tabName === 'nodes',
effect: async (action, { getState, dispatch }) => { effect: async (action, { getState, dispatch }) => {
const state = getState(); const state = getState();
const { nodes, edges } = state.nodes;
const workflow = state.workflow;
const graph = buildNodesGraph(state.nodes); const graph = buildNodesGraph(state.nodes);
const workflow = buildWorkflow(state.nodes); const builtWorkflow = buildWorkflow({
nodes,
edges,
workflow,
});
const batchConfig: BatchConfig = { const batchConfig: BatchConfig = {
batch: { batch: {
graph, graph,
workflow, workflow: builtWorkflow,
runs: state.generation.iterations, runs: state.generation.iterations,
}, },
prepend: action.payload.prepend, prepend: action.payload.prepend,

View File

@ -11,13 +11,11 @@ import {
TypesafeDroppableData, TypesafeDroppableData,
} from 'features/dnd/types'; } from 'features/dnd/types';
import { imageSelected } from 'features/gallery/store/gallerySlice'; import { imageSelected } from 'features/gallery/store/gallerySlice';
import { import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
fieldImageValueChanged,
workflowExposedFieldAdded,
} from 'features/nodes/store/nodesSlice';
import { initialImageChanged } from 'features/parameters/store/generationSlice'; import { initialImageChanged } from 'features/parameters/store/generationSlice';
import { imagesApi } from 'services/api/endpoints/images'; import { imagesApi } from 'services/api/endpoints/images';
import { startAppListening } from '../'; import { startAppListening } from '../';
import { workflowExposedFieldAdded } from 'features/nodes/store/workflowSlice';
export const dndDropped = createAction<{ export const dndDropped = createAction<{
overData: TypesafeDroppableData; overData: TypesafeDroppableData;

View File

@ -1,5 +1,6 @@
import { Store } from '@reduxjs/toolkit'; import { createStore } from 'app/store/store';
import { atom } from 'nanostores'; import { atom } from 'nanostores';
// eslint-disable-next-line @typescript-eslint/no-explicit-any export const $store = atom<
export const $store = atom<Store<any> | undefined>(); Readonly<ReturnType<typeof createStore>> | undefined
>();

View File

@ -14,6 +14,7 @@ import galleryReducer from 'features/gallery/store/gallerySlice';
import loraReducer from 'features/lora/store/loraSlice'; import loraReducer from 'features/lora/store/loraSlice';
import modelmanagerReducer from 'features/modelManager/store/modelManagerSlice'; import modelmanagerReducer from 'features/modelManager/store/modelManagerSlice';
import nodesReducer from 'features/nodes/store/nodesSlice'; import nodesReducer from 'features/nodes/store/nodesSlice';
import workflowReducer from 'features/nodes/store/workflowSlice';
import generationReducer from 'features/parameters/store/generationSlice'; import generationReducer from 'features/parameters/store/generationSlice';
import postprocessingReducer from 'features/parameters/store/postprocessingSlice'; import postprocessingReducer from 'features/parameters/store/postprocessingSlice';
import queueReducer from 'features/queue/store/queueSlice'; import queueReducer from 'features/queue/store/queueSlice';
@ -22,6 +23,7 @@ import configReducer from 'features/system/store/configSlice';
import systemReducer from 'features/system/store/systemSlice'; import systemReducer from 'features/system/store/systemSlice';
import hotkeysReducer from 'features/ui/store/hotkeysSlice'; import hotkeysReducer from 'features/ui/store/hotkeysSlice';
import uiReducer from 'features/ui/store/uiSlice'; import uiReducer from 'features/ui/store/uiSlice';
import { createStore as createIDBKeyValStore, get, set } from 'idb-keyval';
import dynamicMiddlewares from 'redux-dynamic-middlewares'; import dynamicMiddlewares from 'redux-dynamic-middlewares';
import { Driver, rememberEnhancer, rememberReducer } from 'redux-remember'; import { Driver, rememberEnhancer, rememberReducer } from 'redux-remember';
import { api } from 'services/api'; import { api } from 'services/api';
@ -32,7 +34,6 @@ import { actionSanitizer } from './middleware/devtools/actionSanitizer';
import { actionsDenylist } from './middleware/devtools/actionsDenylist'; import { actionsDenylist } from './middleware/devtools/actionsDenylist';
import { stateSanitizer } from './middleware/devtools/stateSanitizer'; import { stateSanitizer } from './middleware/devtools/stateSanitizer';
import { listenerMiddleware } from './middleware/listenerMiddleware'; import { listenerMiddleware } from './middleware/listenerMiddleware';
import { createStore as createIDBKeyValStore, get, set } from 'idb-keyval';
const allReducers = { const allReducers = {
canvas: canvasReducer, canvas: canvasReducer,
@ -52,6 +53,7 @@ const allReducers = {
modelmanager: modelmanagerReducer, modelmanager: modelmanagerReducer,
sdxl: sdxlReducer, sdxl: sdxlReducer,
queue: queueReducer, queue: queueReducer,
workflow: workflowReducer,
[api.reducerPath]: api.reducer, [api.reducerPath]: api.reducer,
}; };
@ -65,6 +67,7 @@ const rememberedKeys: (keyof typeof allReducers)[] = [
'generation', 'generation',
'sdxl', 'sdxl',
'nodes', 'nodes',
'workflow',
'postprocessing', 'postprocessing',
'system', 'system',
'ui', 'ui',

View File

@ -13,7 +13,7 @@ import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitl
import { import {
workflowExposedFieldAdded, workflowExposedFieldAdded,
workflowExposedFieldRemoved, workflowExposedFieldRemoved,
} from 'features/nodes/store/nodesSlice'; } from 'features/nodes/store/workflowSlice';
import { MouseEvent, ReactNode, memo, useCallback, useMemo } from 'react'; import { MouseEvent, ReactNode, memo, useCallback, useMemo } from 'react';
import { FaMinus, FaPlus } from 'react-icons/fa'; import { FaMinus, FaPlus } from 'react-icons/fa';
import { MENU_LIST_MOTION_PROPS } from 'theme/components/menu'; import { MENU_LIST_MOTION_PROPS } from 'theme/components/menu';
@ -41,9 +41,9 @@ const FieldContextMenu = ({ nodeId, fieldName, kind, children }: Props) => {
() => () =>
createSelector( createSelector(
stateSelector, stateSelector,
({ nodes }) => { ({ workflow }) => {
const isExposed = Boolean( const isExposed = Boolean(
nodes.workflow.exposedFields.find( workflow.exposedFields.find(
(f) => f.nodeId === nodeId && f.fieldName === fieldName (f) => f.nodeId === nodeId && f.fieldName === fieldName
) )
); );

View File

@ -10,7 +10,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay'; import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay';
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
import { workflowExposedFieldRemoved } from 'features/nodes/store/nodesSlice'; import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { FaInfoCircle, FaTrash } from 'react-icons/fa'; import { FaInfoCircle, FaTrash } from 'react-icons/fa';

View File

@ -5,6 +5,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIInput from 'common/components/IAIInput'; import IAIInput from 'common/components/IAIInput';
import IAITextarea from 'common/components/IAITextarea'; import IAITextarea from 'common/components/IAITextarea';
import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent';
import { import {
workflowAuthorChanged, workflowAuthorChanged,
workflowContactChanged, workflowContactChanged,
@ -13,16 +14,15 @@ import {
workflowNotesChanged, workflowNotesChanged,
workflowTagsChanged, workflowTagsChanged,
workflowVersionChanged, workflowVersionChanged,
} from 'features/nodes/store/nodesSlice'; } from 'features/nodes/store/workflowSlice';
import { ChangeEvent, memo, useCallback } from 'react'; import { ChangeEvent, memo, useCallback } from 'react';
import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const selector = createSelector( const selector = createSelector(
stateSelector, stateSelector,
({ nodes }) => { ({ workflow }) => {
const { author, name, description, tags, version, contact, notes } = const { author, name, description, tags, version, contact, notes } =
nodes.workflow; workflow;
return { return {
name, name,

View File

@ -11,9 +11,9 @@ import { useTranslation } from 'react-i18next';
const selector = createSelector( const selector = createSelector(
stateSelector, stateSelector,
({ nodes }) => { ({ workflow: workflows }) => {
return { return {
fields: nodes.workflow.exposedFields, fields: workflows.exposedFields,
}; };
}, },
defaultSelectorOptions defaultSelectorOptions

View File

@ -5,12 +5,16 @@ import { useMemo } from 'react';
import { useDebounce } from 'use-debounce'; import { useDebounce } from 'use-debounce';
export const useWorkflow = () => { export const useWorkflow = () => {
const nodes = useAppSelector((state: RootState) => state.nodes); const nodes_ = useAppSelector((state: RootState) => state.nodes.nodes);
const [debouncedNodes] = useDebounce(nodes, 300); const edges_ = useAppSelector((state: RootState) => state.nodes.edges);
const workflow = useMemo( const workflow_ = useAppSelector((state: RootState) => state.workflow);
() => buildWorkflow(debouncedNodes), const [nodes] = useDebounce(nodes_, 300);
[debouncedNodes] const [edges] = useDebounce(edges_, 300);
const [workflow] = useDebounce(workflow_, 300);
const builtWorkflow = useMemo(
() => buildWorkflow({ nodes, edges, workflow }),
[nodes, edges, workflow]
); );
return workflow; return builtWorkflow;
}; };

View File

@ -29,7 +29,7 @@ import {
zNodeStatus, zNodeStatus,
} from 'features/nodes/types/invocation'; } from 'features/nodes/types/invocation';
import { WorkflowV2 } from 'features/nodes/types/workflow'; import { WorkflowV2 } from 'features/nodes/types/workflow';
import { cloneDeep, forEach, isEqual, uniqBy } from 'lodash-es'; import { cloneDeep, forEach } from 'lodash-es';
import { import {
addEdge, addEdge,
applyEdgeChanges, applyEdgeChanges,
@ -70,20 +70,6 @@ const initialNodeExecutionState: Omit<NodeExecutionState, 'nodeId'> = {
outputs: [], outputs: [],
}; };
const INITIAL_WORKFLOW: WorkflowV2 = {
name: '',
author: '',
description: '',
version: '',
contact: '',
tags: '',
notes: '',
nodes: [],
edges: [],
exposedFields: [],
meta: { version: '2.0.0', category: 'user' },
};
export const initialNodesState: NodesState = { export const initialNodesState: NodesState = {
nodes: [], nodes: [],
edges: [], edges: [],
@ -103,7 +89,7 @@ export const initialNodesState: NodesState = {
nodeOpacity: 1, nodeOpacity: 1,
selectedNodes: [], selectedNodes: [],
selectedEdges: [], selectedEdges: [],
workflow: INITIAL_WORKFLOW, // workflow: INITIAL_WORKFLOW,
nodeExecutionStates: {}, nodeExecutionStates: {},
viewport: { x: 0, y: 0, zoom: 1 }, viewport: { x: 0, y: 0, zoom: 1 },
mouseOverField: null, mouseOverField: null,
@ -308,23 +294,6 @@ const nodesSlice = createSlice({
} }
state.modifyingEdge = false; state.modifyingEdge = false;
}, },
workflowExposedFieldAdded: (
state,
action: PayloadAction<FieldIdentifier>
) => {
state.workflow.exposedFields = uniqBy(
state.workflow.exposedFields.concat(action.payload),
(field) => `${field.nodeId}-${field.fieldName}`
);
},
workflowExposedFieldRemoved: (
state,
action: PayloadAction<FieldIdentifier>
) => {
state.workflow.exposedFields = state.workflow.exposedFields.filter(
(field) => !isEqual(field, action.payload)
);
},
fieldLabelChanged: ( fieldLabelChanged: (
state, state,
action: PayloadAction<{ action: PayloadAction<{
@ -508,9 +477,6 @@ const nodesSlice = createSlice({
}, },
nodesDeleted: (state, action: PayloadAction<AnyNode[]>) => { nodesDeleted: (state, action: PayloadAction<AnyNode[]>) => {
action.payload.forEach((node) => { action.payload.forEach((node) => {
state.workflow.exposedFields = state.workflow.exposedFields.filter(
(f) => f.nodeId !== node.id
);
if (!isInvocationNode(node)) { if (!isInvocationNode(node)) {
return; return;
} }
@ -673,7 +639,6 @@ const nodesSlice = createSlice({
nodeEditorReset: (state) => { nodeEditorReset: (state) => {
state.nodes = []; state.nodes = [];
state.edges = []; state.edges = [];
state.workflow = cloneDeep(INITIAL_WORKFLOW);
}, },
shouldValidateGraphChanged: (state, action: PayloadAction<boolean>) => { shouldValidateGraphChanged: (state, action: PayloadAction<boolean>) => {
state.shouldValidateGraph = action.payload; state.shouldValidateGraph = action.payload;
@ -690,34 +655,8 @@ const nodesSlice = createSlice({
nodeOpacityChanged: (state, action: PayloadAction<number>) => { nodeOpacityChanged: (state, action: PayloadAction<number>) => {
state.nodeOpacity = action.payload; state.nodeOpacity = action.payload;
}, },
workflowNameChanged: (state, action: PayloadAction<string>) => {
state.workflow.name = action.payload;
},
workflowDescriptionChanged: (state, action: PayloadAction<string>) => {
state.workflow.description = action.payload;
},
workflowTagsChanged: (state, action: PayloadAction<string>) => {
state.workflow.tags = action.payload;
},
workflowAuthorChanged: (state, action: PayloadAction<string>) => {
state.workflow.author = action.payload;
},
workflowNotesChanged: (state, action: PayloadAction<string>) => {
state.workflow.notes = action.payload;
},
workflowVersionChanged: (state, action: PayloadAction<string>) => {
state.workflow.version = action.payload;
},
workflowContactChanged: (state, action: PayloadAction<string>) => {
state.workflow.contact = action.payload;
},
workflowIDChanged: (state, action: PayloadAction<string>) => {
state.workflow.id = action.payload;
},
workflowLoaded: (state, action: PayloadAction<WorkflowV2>) => { workflowLoaded: (state, action: PayloadAction<WorkflowV2>) => {
const { nodes, edges, ...workflow } = action.payload; const { nodes, edges } = action.payload;
state.workflow = workflow;
state.nodes = applyNodeChanges( state.nodes = applyNodeChanges(
nodes.map((node) => ({ nodes.map((node) => ({
item: { ...node, ...SHARED_NODE_PROPERTIES }, item: { ...node, ...SHARED_NODE_PROPERTIES },
@ -740,9 +679,6 @@ const nodesSlice = createSlice({
return acc; return acc;
}, {}); }, {});
}, },
workflowReset: (state) => {
state.workflow = cloneDeep(INITIAL_WORKFLOW);
},
viewportChanged: (state, action: PayloadAction<Viewport>) => { viewportChanged: (state, action: PayloadAction<Viewport>) => {
state.viewport = action.payload; state.viewport = action.payload;
}, },
@ -996,17 +932,7 @@ export const {
shouldSnapToGridChanged, shouldSnapToGridChanged,
shouldValidateGraphChanged, shouldValidateGraphChanged,
viewportChanged, viewportChanged,
workflowAuthorChanged,
workflowContactChanged,
workflowDescriptionChanged,
workflowExposedFieldAdded,
workflowExposedFieldRemoved,
workflowLoaded, workflowLoaded,
workflowNameChanged,
workflowNotesChanged,
workflowTagsChanged,
workflowVersionChanged,
workflowIDChanged,
edgeAdded, edgeAdded,
} = nodesSlice.actions; } = nodesSlice.actions;

View File

@ -29,7 +29,6 @@ export type NodesState = {
shouldColorEdges: boolean; shouldColorEdges: boolean;
selectedNodes: string[]; selectedNodes: string[];
selectedEdges: string[]; selectedEdges: string[];
workflow: Omit<WorkflowV2, 'nodes' | 'edges'>;
nodeExecutionStates: Record<string, NodeExecutionState>; nodeExecutionStates: Record<string, NodeExecutionState>;
viewport: Viewport; viewport: Viewport;
isReady: boolean; isReady: boolean;
@ -41,3 +40,5 @@ export type NodesState = {
addNewNodePosition: XYPosition | null; addNewNodePosition: XYPosition | null;
selectionMode: SelectionMode; selectionMode: SelectionMode;
}; };
export type WorkflowsState = Omit<WorkflowV2, 'nodes' | 'edges'>;

View File

@ -0,0 +1,99 @@
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import { nodeEditorReset, nodesDeleted } from 'features/nodes/store/nodesSlice';
import { WorkflowsState as WorkflowState } from 'features/nodes/store/types';
import { FieldIdentifier } from 'features/nodes/types/field';
import { WorkflowV2 } from 'features/nodes/types/workflow';
import { cloneDeep, isEqual, uniqBy } from 'lodash-es';
export const initialWorkflowState: WorkflowState = {
name: '',
author: '',
description: '',
version: '',
contact: '',
tags: '',
notes: '',
exposedFields: [],
meta: { version: '2.0.0', category: 'user' },
};
const workflowSlice = createSlice({
name: 'workflow',
initialState: initialWorkflowState,
reducers: {
workflowExposedFieldAdded: (
state,
action: PayloadAction<FieldIdentifier>
) => {
state.exposedFields = uniqBy(
state.exposedFields.concat(action.payload),
(field) => `${field.nodeId}-${field.fieldName}`
);
},
workflowExposedFieldRemoved: (
state,
action: PayloadAction<FieldIdentifier>
) => {
state.exposedFields = state.exposedFields.filter(
(field) => !isEqual(field, action.payload)
);
},
workflowNameChanged: (state, action: PayloadAction<string>) => {
state.name = action.payload;
},
workflowDescriptionChanged: (state, action: PayloadAction<string>) => {
state.description = action.payload;
},
workflowTagsChanged: (state, action: PayloadAction<string>) => {
state.tags = action.payload;
},
workflowAuthorChanged: (state, action: PayloadAction<string>) => {
state.author = action.payload;
},
workflowNotesChanged: (state, action: PayloadAction<string>) => {
state.notes = action.payload;
},
workflowVersionChanged: (state, action: PayloadAction<string>) => {
state.version = action.payload;
},
workflowContactChanged: (state, action: PayloadAction<string>) => {
state.contact = action.payload;
},
workflowIDChanged: (state, action: PayloadAction<string>) => {
state.id = action.payload;
},
workflowLoaded: (state, action: PayloadAction<WorkflowV2>) => {
const { nodes: _nodes, edges: _edges, ...workflow } = action.payload;
return cloneDeep(workflow);
},
workflowReset: () => cloneDeep(initialWorkflowState),
},
extraReducers: (builder) => {
builder.addCase(nodesDeleted, (state, action) => {
action.payload.forEach((node) => {
state.exposedFields = state.exposedFields.filter(
(f) => f.nodeId !== node.id
);
});
});
builder.addCase(nodeEditorReset, () => cloneDeep(initialWorkflowState));
},
});
export const {
workflowExposedFieldAdded,
workflowExposedFieldRemoved,
workflowNameChanged,
workflowDescriptionChanged,
workflowTagsChanged,
workflowAuthorChanged,
workflowNotesChanged,
workflowVersionChanged,
workflowContactChanged,
workflowIDChanged,
workflowLoaded,
workflowReset,
} = workflowSlice.actions;
export default workflowSlice.reducer;

View File

@ -1,27 +1,39 @@
import { logger } from 'app/logging/logger'; import { logger } from 'app/logging/logger';
import { parseify } from 'common/util/serialize';
import { NodesState } from 'features/nodes/store/types'; import { NodesState } from 'features/nodes/store/types';
import { import {
WorkflowV2, WorkflowV2,
zWorkflowEdge, zWorkflowEdge,
zWorkflowNode, zWorkflowNode,
} from 'features/nodes/types/workflow'; } from 'features/nodes/types/workflow';
import { fromZodError } from 'zod-validation-error';
import { parseify } from 'common/util/serialize';
import i18n from 'i18next'; import i18n from 'i18next';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import { fromZodError } from 'zod-validation-error';
export const buildWorkflow = (nodesState: NodesState): WorkflowV2 => { type BuildWorkflowArg = {
const workflow = cloneDeep(nodesState.workflow); nodes: NodesState['nodes'];
const nodes = cloneDeep(nodesState.nodes); edges: NodesState['edges'];
const edges = cloneDeep(nodesState.edges); workflow: Omit<WorkflowV2, 'nodes' | 'edges'>;
};
type BuildWorkflowFunction = (arg: BuildWorkflowArg) => WorkflowV2;
export const buildWorkflow: BuildWorkflowFunction = ({
nodes,
edges,
workflow,
}) => {
const clonedWorkflow = cloneDeep(workflow);
const clonedNodes = cloneDeep(nodes);
const clonedEdges = cloneDeep(edges);
const newWorkflow: WorkflowV2 = { const newWorkflow: WorkflowV2 = {
...workflow, ...clonedWorkflow,
nodes: [], nodes: [],
edges: [], edges: [],
}; };
nodes clonedNodes
.filter((n) => .filter((n) =>
['invocation', 'notes'].includes(n.type ?? '__UNKNOWN_NODE_TYPE__') ['invocation', 'notes'].includes(n.type ?? '__UNKNOWN_NODE_TYPE__')
) )
@ -37,7 +49,7 @@ export const buildWorkflow = (nodesState: NodesState): WorkflowV2 => {
newWorkflow.nodes.push(result.data); newWorkflow.nodes.push(result.data);
}); });
edges.forEach((edge) => { clonedEdges.forEach((edge) => {
const result = zWorkflowEdge.safeParse(edge); const result = zWorkflowEdge.safeParse(edge);
if (!result.success) { if (!result.success) {
const { message } = fromZodError(result.error, { const { message } = fromZodError(result.error, {

View File

@ -1,5 +1,4 @@
import { $store } from 'app/store/nanostores/store'; import { $store } from 'app/store/nanostores/store';
import { RootState } from 'app/store/store';
import { import {
WorkflowMigrationError, WorkflowMigrationError,
WorkflowVersionError, WorkflowVersionError,
@ -32,8 +31,12 @@ const zWorkflowMetaVersion = z.object({
* - Workflow schema version bumped to 2.0.0 * - Workflow schema version bumped to 2.0.0
*/ */
const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => { const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => {
const invocationTemplates = ($store.get()?.getState() as RootState).nodes const invocationTemplates = $store.get()?.getState().nodes.nodeTemplates;
.nodeTemplates;
if (!invocationTemplates) {
throw new Error(t('app.storeNotInitialized'));
}
workflowToMigrate.nodes.forEach((node) => { workflowToMigrate.nodes.forEach((node) => {
if (node.type === 'invocation') { if (node.type === 'invocation') {
// Migrate field types // Migrate field types
@ -66,6 +69,7 @@ const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => {
}); });
// Bump version // Bump version
(workflowToMigrate as unknown as WorkflowV2).meta.version = '2.0.0'; (workflowToMigrate as unknown as WorkflowV2).meta.version = '2.0.0';
// Add category - should always be 'user', 'default' workflows are only created by the backend
(workflowToMigrate as unknown as WorkflowV2).meta.category = 'user'; (workflowToMigrate as unknown as WorkflowV2).meta.category = 'user';
// Parsing strips out any extra properties not in the latest version // Parsing strips out any extra properties not in the latest version
return zWorkflowV2.parse(workflowToMigrate); return zWorkflowV2.parse(workflowToMigrate);

View File

@ -19,7 +19,7 @@ import { useTranslation } from 'react-i18next';
import { FaClone } from 'react-icons/fa'; import { FaClone } from 'react-icons/fa';
const SaveWorkflowAsButton = () => { const SaveWorkflowAsButton = () => {
const currentName = useAppSelector((state) => state.nodes.workflow.name); const currentName = useAppSelector((state) => state.workflow.name);
const { t } = useTranslation(); const { t } = useTranslation();
const { saveWorkflowAs, isLoading } = useSaveWorkflowAs(); const { saveWorkflowAs, isLoading } = useSaveWorkflowAs();
const [name, setName] = useState(currentName.trim()); const [name, setName] = useState(currentName.trim());