diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index e7a47e62a4..fd86414924 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1646,5 +1646,8 @@ "clearWorkflowSearchFilter": "Clear Workflow Search Filter", "workflowName": "Workflow Name", "workflowEditorReset": "Workflow Editor Reset" + }, + "app": { + "storeNotInitialized": "Store is not initialized" } } diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts index 6b6063ee1c..dce4835418 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts @@ -11,12 +11,18 @@ export const addEnqueueRequestedNodes = () => { enqueueRequested.match(action) && action.payload.tabName === 'nodes', effect: async (action, { getState, dispatch }) => { const state = getState(); + const { nodes, edges } = state.nodes; + const workflow = state.workflow; const graph = buildNodesGraph(state.nodes); - const workflow = buildWorkflow(state.nodes); + const builtWorkflow = buildWorkflow({ + nodes, + edges, + workflow, + }); const batchConfig: BatchConfig = { batch: { graph, - workflow, + workflow: builtWorkflow, runs: state.generation.iterations, }, prepend: action.payload.prepend, diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts index 584ec18f26..0ea5caf1d6 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDropped.ts @@ -11,13 +11,11 @@ import { TypesafeDroppableData, } from 'features/dnd/types'; import { imageSelected } from 'features/gallery/store/gallerySlice'; -import { - fieldImageValueChanged, - workflowExposedFieldAdded, -} from 'features/nodes/store/nodesSlice'; +import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice'; import { initialImageChanged } from 'features/parameters/store/generationSlice'; import { imagesApi } from 'services/api/endpoints/images'; import { startAppListening } from '../'; +import { workflowExposedFieldAdded } from 'features/nodes/store/workflowSlice'; export const dndDropped = createAction<{ overData: TypesafeDroppableData; diff --git a/invokeai/frontend/web/src/app/store/nanostores/store.ts b/invokeai/frontend/web/src/app/store/nanostores/store.ts index c9f041fa5d..4e16245c6c 100644 --- a/invokeai/frontend/web/src/app/store/nanostores/store.ts +++ b/invokeai/frontend/web/src/app/store/nanostores/store.ts @@ -1,5 +1,6 @@ -import { Store } from '@reduxjs/toolkit'; +import { createStore } from 'app/store/store'; import { atom } from 'nanostores'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const $store = atom | undefined>(); +export const $store = atom< + Readonly> | undefined +>(); diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 0e3634468b..d9366b0083 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -14,6 +14,7 @@ import galleryReducer from 'features/gallery/store/gallerySlice'; import loraReducer from 'features/lora/store/loraSlice'; import modelmanagerReducer from 'features/modelManager/store/modelManagerSlice'; import nodesReducer from 'features/nodes/store/nodesSlice'; +import workflowReducer from 'features/nodes/store/workflowSlice'; import generationReducer from 'features/parameters/store/generationSlice'; import postprocessingReducer from 'features/parameters/store/postprocessingSlice'; 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 hotkeysReducer from 'features/ui/store/hotkeysSlice'; import uiReducer from 'features/ui/store/uiSlice'; +import { createStore as createIDBKeyValStore, get, set } from 'idb-keyval'; import dynamicMiddlewares from 'redux-dynamic-middlewares'; import { Driver, rememberEnhancer, rememberReducer } from 'redux-remember'; import { api } from 'services/api'; @@ -32,7 +34,6 @@ import { actionSanitizer } from './middleware/devtools/actionSanitizer'; import { actionsDenylist } from './middleware/devtools/actionsDenylist'; import { stateSanitizer } from './middleware/devtools/stateSanitizer'; import { listenerMiddleware } from './middleware/listenerMiddleware'; -import { createStore as createIDBKeyValStore, get, set } from 'idb-keyval'; const allReducers = { canvas: canvasReducer, @@ -52,6 +53,7 @@ const allReducers = { modelmanager: modelmanagerReducer, sdxl: sdxlReducer, queue: queueReducer, + workflow: workflowReducer, [api.reducerPath]: api.reducer, }; @@ -65,6 +67,7 @@ const rememberedKeys: (keyof typeof allReducers)[] = [ 'generation', 'sdxl', 'nodes', + 'workflow', 'postprocessing', 'system', 'ui', diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldContextMenu.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldContextMenu.tsx index a9354121b4..18d8cd871b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldContextMenu.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldContextMenu.tsx @@ -13,7 +13,7 @@ import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitl import { workflowExposedFieldAdded, workflowExposedFieldRemoved, -} from 'features/nodes/store/nodesSlice'; +} from 'features/nodes/store/workflowSlice'; import { MouseEvent, ReactNode, memo, useCallback, useMemo } from 'react'; import { FaMinus, FaPlus } from 'react-icons/fa'; import { MENU_LIST_MOTION_PROPS } from 'theme/components/menu'; @@ -41,9 +41,9 @@ const FieldContextMenu = ({ nodeId, fieldName, kind, children }: Props) => { () => createSelector( stateSelector, - ({ nodes }) => { + ({ workflow }) => { const isExposed = Boolean( - nodes.workflow.exposedFields.find( + workflow.exposedFields.find( (f) => f.nodeId === nodeId && f.fieldName === fieldName ) ); 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 a9416380d4..0abfb41655 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 @@ -10,7 +10,7 @@ import { useAppDispatch } from 'app/store/storeHooks'; import IAIIconButton from 'common/components/IAIIconButton'; import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay'; 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 { memo, useCallback } from 'react'; import { FaInfoCircle, FaTrash } from 'react-icons/fa'; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowGeneralTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowGeneralTab.tsx index d913d80e77..d17ad6c826 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowGeneralTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowGeneralTab.tsx @@ -5,6 +5,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAIInput from 'common/components/IAIInput'; import IAITextarea from 'common/components/IAITextarea'; +import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent'; import { workflowAuthorChanged, workflowContactChanged, @@ -13,16 +14,15 @@ import { workflowNotesChanged, workflowTagsChanged, workflowVersionChanged, -} from 'features/nodes/store/nodesSlice'; +} from 'features/nodes/store/workflowSlice'; import { ChangeEvent, memo, useCallback } from 'react'; -import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent'; import { useTranslation } from 'react-i18next'; const selector = createSelector( stateSelector, - ({ nodes }) => { + ({ workflow }) => { const { author, name, description, tags, version, contact, notes } = - nodes.workflow; + workflow; return { name, 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 15d6ec938f..0a93e4de11 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 @@ -11,9 +11,9 @@ import { useTranslation } from 'react-i18next'; const selector = createSelector( stateSelector, - ({ nodes }) => { + ({ workflow: workflows }) => { return { - fields: nodes.workflow.exposedFields, + fields: workflows.exposedFields, }; }, defaultSelectorOptions diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useWorkflow.ts b/invokeai/frontend/web/src/features/nodes/hooks/useWorkflow.ts index b0799630c9..d90d2a824c 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useWorkflow.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useWorkflow.ts @@ -5,12 +5,16 @@ import { useMemo } from 'react'; import { useDebounce } from 'use-debounce'; export const useWorkflow = () => { - const nodes = useAppSelector((state: RootState) => state.nodes); - const [debouncedNodes] = useDebounce(nodes, 300); - const workflow = useMemo( - () => buildWorkflow(debouncedNodes), - [debouncedNodes] + const nodes_ = useAppSelector((state: RootState) => state.nodes.nodes); + const edges_ = useAppSelector((state: RootState) => state.nodes.edges); + const workflow_ = useAppSelector((state: RootState) => state.workflow); + const [nodes] = useDebounce(nodes_, 300); + const [edges] = useDebounce(edges_, 300); + const [workflow] = useDebounce(workflow_, 300); + const builtWorkflow = useMemo( + () => buildWorkflow({ nodes, edges, workflow }), + [nodes, edges, workflow] ); - return workflow; + return builtWorkflow; }; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 562e32108f..ba88296a57 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -29,7 +29,7 @@ import { zNodeStatus, } from 'features/nodes/types/invocation'; import { WorkflowV2 } from 'features/nodes/types/workflow'; -import { cloneDeep, forEach, isEqual, uniqBy } from 'lodash-es'; +import { cloneDeep, forEach } from 'lodash-es'; import { addEdge, applyEdgeChanges, @@ -70,20 +70,6 @@ const initialNodeExecutionState: Omit = { 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 = { nodes: [], edges: [], @@ -103,7 +89,7 @@ export const initialNodesState: NodesState = { nodeOpacity: 1, selectedNodes: [], selectedEdges: [], - workflow: INITIAL_WORKFLOW, + // workflow: INITIAL_WORKFLOW, nodeExecutionStates: {}, viewport: { x: 0, y: 0, zoom: 1 }, mouseOverField: null, @@ -308,23 +294,6 @@ const nodesSlice = createSlice({ } state.modifyingEdge = false; }, - workflowExposedFieldAdded: ( - state, - action: PayloadAction - ) => { - state.workflow.exposedFields = uniqBy( - state.workflow.exposedFields.concat(action.payload), - (field) => `${field.nodeId}-${field.fieldName}` - ); - }, - workflowExposedFieldRemoved: ( - state, - action: PayloadAction - ) => { - state.workflow.exposedFields = state.workflow.exposedFields.filter( - (field) => !isEqual(field, action.payload) - ); - }, fieldLabelChanged: ( state, action: PayloadAction<{ @@ -508,9 +477,6 @@ const nodesSlice = createSlice({ }, nodesDeleted: (state, action: PayloadAction) => { action.payload.forEach((node) => { - state.workflow.exposedFields = state.workflow.exposedFields.filter( - (f) => f.nodeId !== node.id - ); if (!isInvocationNode(node)) { return; } @@ -673,7 +639,6 @@ const nodesSlice = createSlice({ nodeEditorReset: (state) => { state.nodes = []; state.edges = []; - state.workflow = cloneDeep(INITIAL_WORKFLOW); }, shouldValidateGraphChanged: (state, action: PayloadAction) => { state.shouldValidateGraph = action.payload; @@ -690,34 +655,8 @@ const nodesSlice = createSlice({ nodeOpacityChanged: (state, action: PayloadAction) => { state.nodeOpacity = action.payload; }, - workflowNameChanged: (state, action: PayloadAction) => { - state.workflow.name = action.payload; - }, - workflowDescriptionChanged: (state, action: PayloadAction) => { - state.workflow.description = action.payload; - }, - workflowTagsChanged: (state, action: PayloadAction) => { - state.workflow.tags = action.payload; - }, - workflowAuthorChanged: (state, action: PayloadAction) => { - state.workflow.author = action.payload; - }, - workflowNotesChanged: (state, action: PayloadAction) => { - state.workflow.notes = action.payload; - }, - workflowVersionChanged: (state, action: PayloadAction) => { - state.workflow.version = action.payload; - }, - workflowContactChanged: (state, action: PayloadAction) => { - state.workflow.contact = action.payload; - }, - workflowIDChanged: (state, action: PayloadAction) => { - state.workflow.id = action.payload; - }, workflowLoaded: (state, action: PayloadAction) => { - const { nodes, edges, ...workflow } = action.payload; - state.workflow = workflow; - + const { nodes, edges } = action.payload; state.nodes = applyNodeChanges( nodes.map((node) => ({ item: { ...node, ...SHARED_NODE_PROPERTIES }, @@ -740,9 +679,6 @@ const nodesSlice = createSlice({ return acc; }, {}); }, - workflowReset: (state) => { - state.workflow = cloneDeep(INITIAL_WORKFLOW); - }, viewportChanged: (state, action: PayloadAction) => { state.viewport = action.payload; }, @@ -996,17 +932,7 @@ export const { shouldSnapToGridChanged, shouldValidateGraphChanged, viewportChanged, - workflowAuthorChanged, - workflowContactChanged, - workflowDescriptionChanged, - workflowExposedFieldAdded, - workflowExposedFieldRemoved, workflowLoaded, - workflowNameChanged, - workflowNotesChanged, - workflowTagsChanged, - workflowVersionChanged, - workflowIDChanged, edgeAdded, } = nodesSlice.actions; diff --git a/invokeai/frontend/web/src/features/nodes/store/types.ts b/invokeai/frontend/web/src/features/nodes/store/types.ts index bfd351ac5d..bba93e270a 100644 --- a/invokeai/frontend/web/src/features/nodes/store/types.ts +++ b/invokeai/frontend/web/src/features/nodes/store/types.ts @@ -29,7 +29,6 @@ export type NodesState = { shouldColorEdges: boolean; selectedNodes: string[]; selectedEdges: string[]; - workflow: Omit; nodeExecutionStates: Record; viewport: Viewport; isReady: boolean; @@ -41,3 +40,5 @@ export type NodesState = { addNewNodePosition: XYPosition | null; selectionMode: SelectionMode; }; + +export type WorkflowsState = Omit; diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts new file mode 100644 index 0000000000..c2dbbe56ab --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts @@ -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 + ) => { + state.exposedFields = uniqBy( + state.exposedFields.concat(action.payload), + (field) => `${field.nodeId}-${field.fieldName}` + ); + }, + workflowExposedFieldRemoved: ( + state, + action: PayloadAction + ) => { + state.exposedFields = state.exposedFields.filter( + (field) => !isEqual(field, action.payload) + ); + }, + workflowNameChanged: (state, action: PayloadAction) => { + state.name = action.payload; + }, + workflowDescriptionChanged: (state, action: PayloadAction) => { + state.description = action.payload; + }, + workflowTagsChanged: (state, action: PayloadAction) => { + state.tags = action.payload; + }, + workflowAuthorChanged: (state, action: PayloadAction) => { + state.author = action.payload; + }, + workflowNotesChanged: (state, action: PayloadAction) => { + state.notes = action.payload; + }, + workflowVersionChanged: (state, action: PayloadAction) => { + state.version = action.payload; + }, + workflowContactChanged: (state, action: PayloadAction) => { + state.contact = action.payload; + }, + workflowIDChanged: (state, action: PayloadAction) => { + state.id = action.payload; + }, + workflowLoaded: (state, action: PayloadAction) => { + 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; diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts index b5c0e918c6..ff57c2d2b1 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts @@ -1,27 +1,39 @@ import { logger } from 'app/logging/logger'; +import { parseify } from 'common/util/serialize'; import { NodesState } from 'features/nodes/store/types'; import { WorkflowV2, zWorkflowEdge, zWorkflowNode, } from 'features/nodes/types/workflow'; -import { fromZodError } from 'zod-validation-error'; -import { parseify } from 'common/util/serialize'; import i18n from 'i18next'; import { cloneDeep } from 'lodash-es'; +import { fromZodError } from 'zod-validation-error'; -export const buildWorkflow = (nodesState: NodesState): WorkflowV2 => { - const workflow = cloneDeep(nodesState.workflow); - const nodes = cloneDeep(nodesState.nodes); - const edges = cloneDeep(nodesState.edges); +type BuildWorkflowArg = { + nodes: NodesState['nodes']; + edges: NodesState['edges']; + workflow: Omit; +}; + +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 = { - ...workflow, + ...clonedWorkflow, nodes: [], edges: [], }; - nodes + clonedNodes .filter((n) => ['invocation', 'notes'].includes(n.type ?? '__UNKNOWN_NODE_TYPE__') ) @@ -37,7 +49,7 @@ export const buildWorkflow = (nodesState: NodesState): WorkflowV2 => { newWorkflow.nodes.push(result.data); }); - edges.forEach((edge) => { + clonedEdges.forEach((edge) => { const result = zWorkflowEdge.safeParse(edge); if (!result.success) { const { message } = fromZodError(result.error, { diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts index 80458ac8af..fddd4de495 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts @@ -1,5 +1,4 @@ import { $store } from 'app/store/nanostores/store'; -import { RootState } from 'app/store/store'; import { WorkflowMigrationError, WorkflowVersionError, @@ -32,8 +31,12 @@ const zWorkflowMetaVersion = z.object({ * - Workflow schema version bumped to 2.0.0 */ const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => { - const invocationTemplates = ($store.get()?.getState() as RootState).nodes - .nodeTemplates; + const invocationTemplates = $store.get()?.getState().nodes.nodeTemplates; + + if (!invocationTemplates) { + throw new Error(t('app.storeNotInitialized')); + } + workflowToMigrate.nodes.forEach((node) => { if (node.type === 'invocation') { // Migrate field types @@ -66,6 +69,7 @@ const migrateV1toV2 = (workflowToMigrate: WorkflowV1): WorkflowV2 => { }); // Bump version (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'; // Parsing strips out any extra properties not in the latest version return zWorkflowV2.parse(workflowToMigrate); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsButton.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsButton.tsx index bad8c01df6..a0cd3037b2 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsButton.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsButton.tsx @@ -19,7 +19,7 @@ import { useTranslation } from 'react-i18next'; import { FaClone } from 'react-icons/fa'; const SaveWorkflowAsButton = () => { - const currentName = useAppSelector((state) => state.nodes.workflow.name); + const currentName = useAppSelector((state) => state.workflow.name); const { t } = useTranslation(); const { saveWorkflowAs, isLoading } = useSaveWorkflowAs(); const [name, setName] = useState(currentName.trim());