mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
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:
parent
92bc04dc87
commit
0f32d260b7
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
>();
|
||||||
|
@ -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',
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -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';
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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'>;
|
||||||
|
@ -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;
|
@ -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, {
|
||||||
|
@ -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);
|
||||||
|
@ -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());
|
||||||
|
Loading…
Reference in New Issue
Block a user