Skip to content

Commit 2671f43

Browse files
Mary Hippclaude
authored andcommitted
feat(ui): implement action router pattern for canvas workflow fields
Implements a Symbol-based action routing system to resolve field mutation conflicts between canvas workflows and nodes workflows. This allows both workflows to share the same field action creators while ensuring updates are routed to the correct slice. ## Changes ### Core Action Router System - Add `actionRouter.ts` with Symbol-based metadata injection and checking utilities - Add `workflowContext.ts` to provide workflow context and avoid circular dependencies - Enhance `useAppDispatch` to automatically inject workflow routing metadata based on context ### Canvas Workflow Integration - Add `CanvasWorkflowElementProvider` wrapping `WorkflowContext` for canvas tab - Refactor `canvasWorkflowNodesSlice` to use `extraReducers` instead of local reducers - Listen for `nodesSlice` field actions and filter by workflow routing metadata - Update persistence config to persist nodes/edges while excluding workflow metadata - Filter out input node fields from workflow form (populated by graph builder) ### Nodes Workflow Integration - Add `NodesWorkflowProvider` to mark nodes/workflow editor context - Wrap `NodeEditor` with provider in workflows tab layout - Add workflow routing check in `nodesSlice` field reducers ### DND Image Drop Fixes - Enhance `setNodeImageFieldImage` to check active tab when routing DND actions - Pass `getState` through DND handler to enable routing logic - Ensure drops are routed to correct slice based on active tab ### Validation Improvements - Refactor canvas workflow validation to use existing `getInvocationNodeErrors` utility - Skip validation for input node's image field (populated at runtime) - Remove custom validation logic in favor of shared validator ### Cleanup - Remove `canvasWorkflowFieldChanged` listener (replaced by action router) - Remove unused metadata code from canvas workflow graph builder - Fix TypeScript type assertions in workflow selection handlers ## Fixes - Field inputs now work correctly in canvas workflow panel - DND image drops now route to correct workflow based on active tab - Canvas workflow validation properly handles runtime-populated fields - Persistence config correctly saves field changes while excluding metadata 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 6299db7 commit 2671f43

File tree

17 files changed

+296
-283
lines changed

17 files changed

+296
-283
lines changed

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasWorkflowFieldChanged.ts

Lines changed: 0 additions & 112 deletions
This file was deleted.

invokeai/frontend/web/src/app/store/store.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ import { actionSanitizer } from './middleware/devtools/actionSanitizer';
5757
import { actionsDenylist } from './middleware/devtools/actionsDenylist';
5858
import { stateSanitizer } from './middleware/devtools/stateSanitizer';
5959
import { addArchivedOrDeletedBoardListener } from './middleware/listenerMiddleware/listeners/addArchivedOrDeletedBoardListener';
60-
import { addCanvasWorkflowFieldChangedListener } from './middleware/listenerMiddleware/listeners/canvasWorkflowFieldChanged';
6160
import { addCanvasWorkflowRehydratedListener } from './middleware/listenerMiddleware/listeners/canvasWorkflowRehydrated';
6261
import { addImageUploadedFulfilledListener } from './middleware/listenerMiddleware/listeners/imageUploaded';
6362

@@ -299,5 +298,4 @@ addAdHocPostProcessingRequestedListener(startAppListening);
299298
addSetDefaultSettingsListener(startAppListening);
300299

301300
// Canvas workflow fields
302-
addCanvasWorkflowFieldChangedListener(startAppListening);
303301
addCanvasWorkflowRehydratedListener(startAppListening);
Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,31 @@
11
import type { AppStore, AppThunkDispatch, RootState } from 'app/store/store';
2+
import { useIsCanvasWorkflow } from 'app/store/workflowContext';
3+
import { injectCanvasWorkflowKey, injectNodesWorkflowKey } from 'features/nodes/store/actionRouter';
4+
import { useCallback } from 'react';
25
import type { TypedUseSelectorHook } from 'react-redux';
36
import { useDispatch, useSelector, useStore } from 'react-redux';
47

58
// Use throughout your app instead of plain `useDispatch` and `useSelector`
6-
export const useAppDispatch = () => useDispatch<AppThunkDispatch>();
9+
export const useAppDispatch = (): AppThunkDispatch => {
10+
const isCanvasWorkflow = useIsCanvasWorkflow();
11+
const dispatch = useDispatch<AppThunkDispatch>();
12+
13+
return useCallback(
14+
((action: Parameters<AppThunkDispatch>[0]) => {
15+
// Inject workflow routing metadata into actions
16+
if (typeof action === 'object' && action !== null && 'type' in action) {
17+
if (isCanvasWorkflow) {
18+
injectCanvasWorkflowKey(action);
19+
} else {
20+
injectNodesWorkflowKey(action);
21+
}
22+
}
23+
24+
return dispatch(action);
25+
}) as AppThunkDispatch,
26+
[dispatch, isCanvasWorkflow]
27+
);
28+
};
29+
730
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
831
export const useAppStore = () => useStore.withTypes<AppStore>()();
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { createContext, useContext } from 'react';
2+
3+
/**
4+
* Context to track whether we're in a canvas workflow or nodes workflow.
5+
* This is used by the useAppDispatch hook to inject the appropriate action routing metadata.
6+
*/
7+
export const WorkflowContext = createContext<{ isCanvasWorkflow: boolean } | null>(null);
8+
9+
/**
10+
* Hook to check if we're in a canvas workflow context.
11+
*/
12+
export const useIsCanvasWorkflow = (): boolean => {
13+
const context = useContext(WorkflowContext);
14+
return context?.isCanvasWorkflow ?? false;
15+
};

invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowElementContext.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useAppSelector } from 'app/store/storeHooks';
2+
import { WorkflowContext } from 'app/store/workflowContext';
23
import { selectCanvasWorkflowNodesSlice } from 'features/controlLayers/store/canvasWorkflowNodesSlice';
34
import type { FormElement } from 'features/nodes/types/workflow';
45
import type { PropsWithChildren } from 'react';
@@ -15,17 +16,23 @@ type CanvasWorkflowElementContextValue = {
1516

1617
const CanvasWorkflowElementContext = createContext<CanvasWorkflowElementContextValue | null>(null);
1718

18-
const CanvasWorkflowElementProvider = memo(({ children }: PropsWithChildren) => {
19+
export const CanvasWorkflowElementProvider = memo(({ children }: PropsWithChildren) => {
1920
const nodesState = useAppSelector(selectCanvasWorkflowNodesSlice);
2021

21-
const value = useMemo<CanvasWorkflowElementContextValue>(
22+
const elementValue = useMemo<CanvasWorkflowElementContextValue>(
2223
() => ({
2324
getElement: (id: string) => nodesState.form.elements[id],
2425
}),
2526
[nodesState.form.elements]
2627
);
2728

28-
return <CanvasWorkflowElementContext.Provider value={value}>{children}</CanvasWorkflowElementContext.Provider>;
29+
const workflowValue = useMemo(() => ({ isCanvasWorkflow: true }), []);
30+
31+
return (
32+
<WorkflowContext.Provider value={workflowValue}>
33+
<CanvasWorkflowElementContext.Provider value={elementValue}>{children}</CanvasWorkflowElementContext.Provider>
34+
</WorkflowContext.Provider>
35+
);
2936
});
3037
CanvasWorkflowElementProvider.displayName = 'CanvasWorkflowElementProvider';
3138

invokeai/frontend/web/src/features/controlLayers/components/CanvasWorkflowFieldsPanel.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Flex, Text } from '@invoke-ai/ui-library';
22
import { useAppSelector } from 'app/store/storeHooks';
3+
import { CanvasWorkflowElementProvider } from 'features/controlLayers/components/CanvasWorkflowElementContext';
34
import { CanvasWorkflowModeProvider } from 'features/controlLayers/components/CanvasWorkflowModeContext';
45
import { CanvasWorkflowRootContainer } from 'features/controlLayers/components/CanvasWorkflowRootContainer';
56
import { selectCanvasWorkflowNodesSlice } from 'features/controlLayers/store/canvasWorkflowNodesSlice';
@@ -32,11 +33,13 @@ export const CanvasWorkflowFieldsPanel = memo(() => {
3233
}
3334

3435
return (
35-
<CanvasWorkflowModeProvider>
36-
<Flex w="full" justifyContent="center" p={4}>
37-
<CanvasWorkflowRootContainer />
38-
</Flex>
39-
</CanvasWorkflowModeProvider>
36+
<CanvasWorkflowElementProvider>
37+
<CanvasWorkflowModeProvider>
38+
<Flex w="full" justifyContent="center" p={4}>
39+
<CanvasWorkflowRootContainer />
40+
</Flex>
41+
</CanvasWorkflowModeProvider>
42+
</CanvasWorkflowElementProvider>
4043
);
4144
});
4245
CanvasWorkflowFieldsPanel.displayName = 'CanvasWorkflowFieldsPanel';

0 commit comments

Comments
 (0)