diff --git a/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx b/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx index 46aed952d8..9aa70a5512 100644 --- a/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx +++ b/invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx @@ -15,6 +15,8 @@ import { useDndMonitor } from 'features/dnd/useDndMonitor'; import { useDynamicPromptsWatcher } from 'features/dynamicPrompts/hooks/useDynamicPromptsWatcher'; import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast'; import { useWorkflowBuilderWatcher } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher'; +import { useSyncExecutionState } from 'features/nodes/hooks/useNodeExecutionState'; +import { useSyncNodeErrors } from 'features/nodes/store/util/fieldValidators'; import { useReadinessWatcher } from 'features/queue/store/readiness'; import { configChanged } from 'features/system/store/configSlice'; import { selectLanguage } from 'features/system/store/systemSelectors'; @@ -47,10 +49,12 @@ export const GlobalHookIsolator = memo( useCloseChakraTooltipsOnDragFix(); useNavigationApi(); useDndMonitor(); + useSyncNodeErrors(); // Persistent subscription to the queue counts query - canvas relies on this to know if there are pending // and/or in progress canvas sessions. useGetQueueCountsByDestinationQuery(queueCountArg); + useSyncExecutionState(); useEffect(() => { i18n.changeLanguage(language); diff --git a/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts b/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts index 7e32afbd3c..97bf610218 100644 --- a/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts +++ b/invokeai/frontend/web/src/app/store/createMemoizedSelector.ts @@ -18,3 +18,5 @@ export const getSelectorsOptions = { argsMemoize: lruMemoize, }), }; + +export const createLruSelector = createSelectorCreator(lruMemoize); diff --git a/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx b/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx index 839809914b..feaa7e54d2 100644 --- a/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx +++ b/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx @@ -1,7 +1,6 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; import { Combobox, ConfirmationAlertDialog, Flex, FormControl, Text } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { @@ -14,7 +13,7 @@ import { useTranslation } from 'react-i18next'; import { useListAllBoardsQuery } from 'services/api/endpoints/boards'; import { useAddImagesToBoardMutation, useRemoveImagesFromBoardMutation } from 'services/api/endpoints/images'; -const selectImagesToChange = createMemoizedSelector( +const selectImagesToChange = createSelector( selectChangeBoardModalSlice, (changeBoardModal) => changeBoardModal.image_names ); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index 24397ce3ec..517bb6c05f 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -22,7 +22,6 @@ import { useConnection } from 'features/nodes/hooks/useConnection'; import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection'; import { useIsWorkflowEditorLocked } from 'features/nodes/hooks/useIsWorkflowEditorLocked'; import { useNodeCopyPaste } from 'features/nodes/hooks/useNodeCopyPaste'; -import { useSyncExecutionState } from 'features/nodes/hooks/useNodeExecutionState'; import { $addNodeCmdk, $cursorPos, @@ -83,23 +82,16 @@ export const Flow = memo(() => { const nodes = useAppSelector(selectNodes); const edges = useAppSelector(selectEdges); const viewport = useStore($viewport); - const needsFit = useStore($needsFit); - const mayUndo = useAppSelector(selectMayUndo); - const mayRedo = useAppSelector(selectMayRedo); const shouldSnapToGrid = useAppSelector(selectShouldSnapToGrid); const selectionMode = useAppSelector(selectSelectionMode); const { onConnectStart, onConnect, onConnectEnd } = useConnection(); const flowWrapper = useRef(null); const isValidConnection = useIsValidConnection(); - const cancelConnection = useReactFlowStore(selectCancelConnection); const updateNodeInternals = useUpdateNodeInternals(); - const store = useAppStore(); - const isWorkflowsFocused = useIsRegionFocused('workflows'); const isLocked = useIsWorkflowEditorLocked(); useFocusRegion('workflows', flowWrapper); - useSyncExecutionState(); const [borderRadius] = useToken('radii', ['base']); const flowStyles = useMemo(() => ({ borderRadius }), [borderRadius]); @@ -110,12 +102,12 @@ export const Flow = memo(() => { if (!flow) { return; } - if (needsFit) { + if ($needsFit.get()) { $needsFit.set(false); flow.fitView(); } }, - [dispatch, needsFit] + [dispatch] ); const onEdgesChange: OnEdgesChange = useCallback( @@ -214,6 +206,83 @@ export const Flow = memo(() => { // #endregion + const onNodeClick = useCallback>((e, node) => { + if (!$isSelectingOutputNode.get()) { + return; + } + if (!isInvocationNode(node)) { + return; + } + const { id } = node.data; + $outputNodeId.set(id); + $isSelectingOutputNode.set(false); + }, []); + + return ( + <> + + id="workflow-editor" + ref={flowWrapper} + defaultViewport={viewport} + nodeTypes={nodeTypes} + edgeTypes={edgeTypes} + nodes={nodes} + edges={edges} + onInit={onInit} + onNodeClick={onNodeClick} + onMouseMove={onMouseMove} + onNodesChange={onNodesChange} + onEdgesChange={onEdgesChange} + onReconnect={onReconnect} + onReconnectStart={onReconnectStart} + onReconnectEnd={onReconnectEnd} + onConnectStart={onConnectStart} + onConnect={onConnect} + onConnectEnd={onConnectEnd} + onMoveEnd={handleMoveEnd} + connectionLineComponent={CustomConnectionLine} + isValidConnection={isValidConnection} + edgesFocusable={!isLocked} + edgesReconnectable={!isLocked} + nodesDraggable={!isLocked} + nodesConnectable={!isLocked} + nodesFocusable={!isLocked} + elementsSelectable={!isLocked} + minZoom={0.1} + snapToGrid={shouldSnapToGrid} + snapGrid={snapGrid} + connectionRadius={30} + proOptions={proOptions} + style={flowStyles} + onPaneClick={handlePaneClick} + deleteKeyCode={null} + selectionMode={selectionMode} + elevateEdgesOnSelect + nodeDragThreshold={1} + noDragClassName={NO_DRAG_CLASS} + noWheelClassName={NO_WHEEL_CLASS} + noPanClassName={NO_PAN_CLASS} + > + + + + + ); +}); + +Flow.displayName = 'Flow'; + +const HotkeyIsolator = memo(() => { + const isLocked = useIsWorkflowEditorLocked(); + + const mayUndo = useAppSelector(selectMayUndo); + const mayRedo = useAppSelector(selectMayRedo); + + const cancelConnection = useReactFlowStore(selectCancelConnection); + + const store = useAppStore(); + const isWorkflowsFocused = useIsRegionFocused('workflows'); + const { copySelection, pasteSelection, pasteSelectionWithEdges } = useNodeCopyPaste(); useRegisteredHotkeys({ @@ -239,12 +308,12 @@ export const Flow = memo(() => { } }); if (nodeChanges.length > 0) { - dispatch(nodesChanged(nodeChanges)); + store.dispatch(nodesChanged(nodeChanges)); } if (edgeChanges.length > 0) { - dispatch(edgesChanged(edgeChanges)); + store.dispatch(edgesChanged(edgeChanges)); } - }, [dispatch, store]); + }, [store]); useRegisteredHotkeys({ id: 'selectAll', category: 'workflows', @@ -273,20 +342,20 @@ export const Flow = memo(() => { id: 'undo', category: 'workflows', callback: () => { - dispatch(undo()); + store.dispatch(undo()); }, options: { enabled: isWorkflowsFocused && !isLocked && mayUndo, preventDefault: true }, - dependencies: [mayUndo, isLocked, isWorkflowsFocused], + dependencies: [store, mayUndo, isLocked, isWorkflowsFocused], }); useRegisteredHotkeys({ id: 'redo', category: 'workflows', callback: () => { - dispatch(redo()); + store.dispatch(redo()); }, options: { enabled: isWorkflowsFocused && !isLocked && mayRedo, preventDefault: true }, - dependencies: [mayRedo, isLocked, isWorkflowsFocused], + dependencies: [store, mayRedo, isLocked, isWorkflowsFocused], }); const onEscapeHotkey = useCallback(() => { @@ -313,12 +382,12 @@ export const Flow = memo(() => { edgeChanges.push({ type: 'remove', id }); }); if (nodeChanges.length > 0) { - dispatch(nodesChanged(nodeChanges)); + store.dispatch(nodesChanged(nodeChanges)); } if (edgeChanges.length > 0) { - dispatch(edgesChanged(edgeChanges)); + store.dispatch(edgesChanged(edgeChanges)); } - }, [dispatch, store]); + }, [store]); useRegisteredHotkeys({ id: 'deleteSelection', category: 'workflows', @@ -327,65 +396,6 @@ export const Flow = memo(() => { dependencies: [deleteSelection, isWorkflowsFocused, isLocked], }); - const onNodeClick = useCallback>((e, node) => { - if (!$isSelectingOutputNode.get()) { - return; - } - if (!isInvocationNode(node)) { - return; - } - const { id } = node.data; - $outputNodeId.set(id); - $isSelectingOutputNode.set(false); - }, []); - - return ( - - id="workflow-editor" - ref={flowWrapper} - defaultViewport={viewport} - nodeTypes={nodeTypes} - edgeTypes={edgeTypes} - nodes={nodes} - edges={edges} - onInit={onInit} - onNodeClick={onNodeClick} - onMouseMove={onMouseMove} - onNodesChange={onNodesChange} - onEdgesChange={onEdgesChange} - onReconnect={onReconnect} - onReconnectStart={onReconnectStart} - onReconnectEnd={onReconnectEnd} - onConnectStart={onConnectStart} - onConnect={onConnect} - onConnectEnd={onConnectEnd} - onMoveEnd={handleMoveEnd} - connectionLineComponent={CustomConnectionLine} - isValidConnection={isValidConnection} - edgesFocusable={!isLocked} - edgesReconnectable={!isLocked} - nodesDraggable={!isLocked} - nodesConnectable={!isLocked} - nodesFocusable={!isLocked} - elementsSelectable={!isLocked} - minZoom={0.1} - snapToGrid={shouldSnapToGrid} - snapGrid={snapGrid} - connectionRadius={30} - proOptions={proOptions} - style={flowStyles} - onPaneClick={handlePaneClick} - deleteKeyCode={null} - selectionMode={selectionMode} - elevateEdgesOnSelect - nodeDragThreshold={1} - noDragClassName={NO_DRAG_CLASS} - noWheelClassName={NO_WHEEL_CLASS} - noPanClassName={NO_PAN_CLASS} - > - - - ); + return null; }); - -Flow.displayName = 'Flow'; +HotkeyIsolator.displayName = 'HotkeyIsolator'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/buildEdgeSelectors.ts b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/buildEdgeSelectors.ts index 9e4aeea5e3..b64e5a6e6a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/buildEdgeSelectors.ts +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/buildEdgeSelectors.ts @@ -1,6 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar'; -import { selectNodesSlice } from 'features/nodes/store/selectors'; +import { selectNodes } from 'features/nodes/store/selectors'; import type { Templates } from 'features/nodes/store/types'; import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; import { isInvocationNode } from 'features/nodes/types/invocation'; @@ -8,9 +8,9 @@ import { isInvocationNode } from 'features/nodes/types/invocation'; import { getFieldColor } from './getEdgeColor'; export const buildSelectAreConnectedNodesSelected = (source: string, target: string) => - createSelector(selectNodesSlice, (nodes): boolean => { - const sourceNode = nodes.nodes.find((node) => node.id === source); - const targetNode = nodes.nodes.find((node) => node.id === target); + createSelector(selectNodes, (nodes): boolean => { + const sourceNode = nodes.find((node) => node.id === source); + const targetNode = nodes.find((node) => node.id === target); return Boolean(sourceNode?.selected || targetNode?.selected); }); @@ -22,10 +22,13 @@ export const buildSelectEdgeColor = ( target: string, targetHandleId: string | null | undefined ) => - createSelector(selectNodesSlice, selectWorkflowSettingsSlice, (nodes, workflowSettings): string => { + createSelector(selectNodes, selectWorkflowSettingsSlice, (nodes, workflowSettings): string => { const { shouldColorEdges } = workflowSettings; - const sourceNode = nodes.nodes.find((node) => node.id === source); - const targetNode = nodes.nodes.find((node) => node.id === target); + if (!shouldColorEdges) { + return colorTokenToCssVar('base.500'); + } + const sourceNode = nodes.find((node) => node.id === source); + const targetNode = nodes.find((node) => node.id === target); if (!sourceNode || !sourceHandleId || !targetNode || !targetHandleId) { return colorTokenToCssVar('base.500'); @@ -37,7 +40,7 @@ export const buildSelectEdgeColor = ( const outputFieldTemplate = sourceNodeTemplate?.outputs[sourceHandleId]; const sourceType = isInvocationToInvocationEdge ? outputFieldTemplate?.type : undefined; - return sourceType && shouldColorEdges ? getFieldColor(sourceType) : colorTokenToCssVar('base.500'); + return sourceType ? getFieldColor(sourceType) : colorTokenToCssVar('base.500'); }); export const buildSelectEdgeLabel = ( @@ -47,9 +50,9 @@ export const buildSelectEdgeLabel = ( target: string, targetHandleId: string | null | undefined ) => - createSelector(selectNodesSlice, (nodes): string | null => { - const sourceNode = nodes.nodes.find((node) => node.id === source); - const targetNode = nodes.nodes.find((node) => node.id === target); + createSelector(selectNodes, (nodes): string | null => { + const sourceNode = nodes.find((node) => node.id === source); + const targetNode = nodes.find((node) => node.id === target); if (!sourceNode || !sourceHandleId || !targetNode || !targetHandleId) { return null; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx index 5b41519c11..7bb92c1494 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx @@ -37,7 +37,7 @@ const sx: SystemStyleObject = { }; const InvocationNode = ({ nodeId, isOpen }: Props) => { - const withFooter = useWithFooter(nodeId); + const withFooter = useWithFooter(); return ( <> @@ -64,7 +64,7 @@ const InvocationNode = ({ nodeId, isOpen }: Props) => { export default memo(InvocationNode); const ConnectionFields = memo(({ nodeId }: { nodeId: string }) => { - const fieldNames = useInputFieldNamesConnection(nodeId); + const fieldNames = useInputFieldNamesConnection(); return ( <> {fieldNames.map((fieldName, i) => ( @@ -80,7 +80,7 @@ const ConnectionFields = memo(({ nodeId }: { nodeId: string }) => { ConnectionFields.displayName = 'ConnectionFields'; const AnyOrDirectFields = memo(({ nodeId }: { nodeId: string }) => { - const fieldNames = useInputFieldNamesAnyOrDirect(nodeId); + const fieldNames = useInputFieldNamesAnyOrDirect(); return ( <> {fieldNames.map((fieldName) => ( @@ -94,7 +94,7 @@ const AnyOrDirectFields = memo(({ nodeId }: { nodeId: string }) => { AnyOrDirectFields.displayName = 'AnyOrDirectFields'; const MissingFields = memo(({ nodeId }: { nodeId: string }) => { - const fieldNames = useInputFieldNamesMissing(nodeId); + const fieldNames = useInputFieldNamesMissing(); return ( <> {fieldNames.map((fieldName) => ( @@ -108,7 +108,7 @@ const MissingFields = memo(({ nodeId }: { nodeId: string }) => { MissingFields.displayName = 'MissingFields'; const OutputFields = memo(({ nodeId }: { nodeId: string }) => { - const fieldNames = useOutputFieldNames(nodeId); + const fieldNames = useOutputFieldNames(); return ( <> {fieldNames.map((fieldName, i) => ( diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeClassificationIcon.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeClassificationIcon.tsx index 8d1fdb7b2b..bfc578ede6 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeClassificationIcon.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeClassificationIcon.tsx @@ -10,7 +10,7 @@ interface Props { } const InvocationNodeClassificationIcon = ({ nodeId }: Props) => { - const classification = useNodeClassification(nodeId); + const classification = useNodeClassification(); if (!classification || classification === 'stable') { return null; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeCollapsedHandles.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeCollapsedHandles.tsx index bf1c841b59..e05e7f114e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeCollapsedHandles.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeCollapsedHandles.tsx @@ -19,7 +19,7 @@ const collapsedHandleStyles: CSSProperties = { }; const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => { - const template = useNodeTemplateOrThrow(nodeId); + const template = useNodeTemplateOrThrow(); if (!template) { return null; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeFooter.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeFooter.tsx index a2f513a325..851b85880f 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeFooter.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeFooter.tsx @@ -16,8 +16,8 @@ type Props = { const props: ChakraProps = { w: 'unset' }; const InvocationNodeFooter = ({ nodeId }: Props) => { - const hasImageOutput = useNodeHasImageOutput(nodeId); - const isExecutableNode = useIsExecutableNode(nodeId); + const hasImageOutput = useNodeHasImageOutput(); + const isExecutableNode = useIsExecutableNode(); const isCacheEnabled = useFeatureStatus('invocationCache'); return ( { - const isInvalid = useNodeIsInvalid(nodeId); + const isInvalid = useNodeIsInvalid(); return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeInfoIcon.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeInfoIcon.tsx index 7b074580df..cf7d591d1e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeInfoIcon.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeInfoIcon.tsx @@ -14,7 +14,7 @@ interface Props { } export const InvocationNodeInfoIcon = memo(({ nodeId }: Props) => { - const needsUpdate = useNodeNeedsUpdate(nodeId); + const needsUpdate = useNodeNeedsUpdate(); return ( } placement="top" shouldWrapChildren> @@ -26,10 +26,10 @@ export const InvocationNodeInfoIcon = memo(({ nodeId }: Props) => { InvocationNodeInfoIcon.displayName = 'InvocationNodeInfoIcon'; const TooltipContent = memo(({ nodeId }: { nodeId: string }) => { - const notes = useInvocationNodeNotes(nodeId); - const label = useNodeUserTitleSafe(nodeId); - const version = useNodeVersion(nodeId); - const nodeTemplate = useNodeTemplateOrThrow(nodeId); + const notes = useInvocationNodeNotes(); + const label = useNodeUserTitleSafe(); + const version = useNodeVersion(); + const nodeTemplate = useNodeTemplateOrThrow(); const { t } = useTranslation(); const title = useMemo(() => { diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeNotesTextarea.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeNotesTextarea.tsx index 36165ba9fb..a9b6a0caae 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeNotesTextarea.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeNotesTextarea.tsx @@ -13,7 +13,7 @@ type Props = { export const InvocationNodeNotesTextarea = memo(({ nodeId }: Props) => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const notes = useInvocationNodeNotes(nodeId); + const notes = useInvocationNodeNotes(); const handleNotesChanged = useCallback( (e: ChangeEvent) => { dispatch(nodeNotesChanged({ nodeId, notes: e.target.value })); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeUnknownFallback.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeUnknownFallback.tsx index 7418135c6d..f7801be11e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeUnknownFallback.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeUnknownFallback.tsx @@ -14,7 +14,7 @@ type Props = { const InvocationNodeUnknownFallback = ({ nodeId, isOpen, label, type }: Props) => { const { t } = useTranslation(); - const nodePack = useNodePack(nodeId); + const nodePack = useNodePack(); return ( <> { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const hasImageOutput = useNodeHasImageOutput(nodeId); - const isIntermediate = useNodeIsIntermediate(nodeId); + const hasImageOutput = useNodeHasImageOutput(); + const isIntermediate = useNodeIsIntermediate(); const handleChange = useCallback( (e: ChangeEvent) => { dispatch( diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/UseCacheCheckbox.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/UseCacheCheckbox.tsx index ebc25d9c46..a22bbfcc04 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/UseCacheCheckbox.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/UseCacheCheckbox.tsx @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'; const UseCacheCheckbox = ({ nodeId }: { nodeId: string }) => { const dispatch = useAppDispatch(); - const useCache = useUseCache(nodeId); + const useCache = useUseCache(); const handleChange = useCallback( (e: ChangeEvent) => { dispatch( diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/context.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/context.tsx new file mode 100644 index 0000000000..b0500c0b18 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/context.tsx @@ -0,0 +1,222 @@ +import { useStore } from '@nanostores/react'; +import type { Selector } from '@reduxjs/toolkit'; +import { createSelector } from '@reduxjs/toolkit'; +import type { RootState } from 'app/store/store'; +import { $templates } from 'features/nodes/store/nodesSlice'; +import { selectEdges, selectNodes } from 'features/nodes/store/selectors'; +import type { InvocationNode, InvocationTemplate } from 'features/nodes/types/invocation'; +import type { PropsWithChildren } from 'react'; +import { createContext, memo, useContext, useMemo } from 'react'; + +type InvocationNodeContextValue = { + nodeId: string; + + selectNodeSafe: Selector; + selectNodeDataSafe: Selector; + selectNodeTypeSafe: Selector; + selectNodeTemplateSafe: Selector; + selectNodeInputsSafe: Selector; + + buildSelectInputFieldSafe: ( + fieldName: string + ) => Selector; + buildSelectInputFieldTemplateSafe: ( + fieldName: string + ) => Selector; + buildSelectOutputFieldTemplateSafe: ( + fieldName: string + ) => Selector; + + selectNodeOrThrow: Selector; + selectNodeDataOrThrow: Selector; + selectNodeTypeOrThrow: Selector; + selectNodeTemplateOrThrow: Selector; + selectNodeInputsOrThrow: Selector; + + buildSelectInputFieldOrThrow: (fieldName: string) => Selector; + buildSelectInputFieldTemplateOrThrow: ( + fieldName: string + ) => Selector; + buildSelectOutputFieldTemplateOrThrow: ( + fieldName: string + ) => Selector; + + buildSelectIsInputFieldConnected: (fieldName: string) => Selector; +}; + +const InvocationNodeContext = createContext(null); + +const getSelectorFromCache = ( + cache: Map>, + key: string, + fallback: () => Selector +): Selector => { + let selector = cache.get(key); + if (!selector) { + selector = fallback(); + cache.set(key, selector); + } + return selector; +}; + +export const InvocationNodeContextProvider = memo(({ nodeId, children }: PropsWithChildren<{ nodeId: string }>) => { + const templates = useStore($templates); + + const value = useMemo(() => { + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + const cache: Map> = new Map(); + + const selectNodeSafe = getSelectorFromCache(cache, 'selectNodeSafe', () => + createSelector(selectNodes, (nodes) => { + return (nodes.find(({ id, type }) => type === 'invocation' && id === nodeId) ?? null) as InvocationNode | null; + }) + ); + const selectNodeDataSafe = getSelectorFromCache(cache, 'selectNodeDataSafe', () => + createSelector(selectNodeSafe, (node) => { + return node?.data ?? null; + }) + ); + const selectNodeTypeSafe = getSelectorFromCache(cache, 'selectNodeTypeSafe', () => + createSelector(selectNodeDataSafe, (data) => { + return data?.type ?? null; + }) + ); + const selectNodeTemplateSafe = getSelectorFromCache(cache, 'selectNodeTemplateSafe', () => + createSelector(selectNodeTypeSafe, (type) => { + return type ? (templates[type] ?? null) : null; + }) + ); + const selectNodeInputsSafe = getSelectorFromCache(cache, 'selectNodeInputsSafe', () => + createSelector(selectNodeDataSafe, (data) => { + return data?.inputs ?? null; + }) + ); + const buildSelectInputFieldSafe = (fieldName: string) => + getSelectorFromCache(cache, `buildSelectInputFieldSafe-${fieldName}`, () => + createSelector(selectNodeInputsSafe, (inputs) => { + return inputs?.[fieldName] ?? null; + }) + ); + const buildSelectInputFieldTemplateSafe = (fieldName: string) => + getSelectorFromCache(cache, `buildSelectInputFieldTemplateSafe-${fieldName}`, () => + createSelector(selectNodeTemplateSafe, (template) => { + return template?.inputs?.[fieldName] ?? null; + }) + ); + const buildSelectOutputFieldTemplateSafe = (fieldName: string) => + getSelectorFromCache(cache, `buildSelectOutputFieldTemplateSafe-${fieldName}`, () => + createSelector(selectNodeTemplateSafe, (template) => { + return template?.outputs?.[fieldName] ?? null; + }) + ); + + const selectNodeOrThrow = getSelectorFromCache(cache, 'selectNodeOrThrow', () => + createSelector(selectNodes, (nodes) => { + const node = nodes.find(({ id, type }) => type === 'invocation' && id === nodeId) as InvocationNode | undefined; + if (node === undefined) { + throw new Error(`Cannot find node with id ${nodeId}`); + } + return node; + }) + ); + const selectNodeDataOrThrow = getSelectorFromCache(cache, 'selectNodeDataOrThrow', () => + createSelector(selectNodeOrThrow, (node) => { + return node.data; + }) + ); + const selectNodeTypeOrThrow = getSelectorFromCache(cache, 'selectNodeTypeOrThrow', () => + createSelector(selectNodeDataOrThrow, (data) => { + return data.type; + }) + ); + const selectNodeTemplateOrThrow = getSelectorFromCache(cache, 'selectNodeTemplateOrThrow', () => + createSelector(selectNodeTypeOrThrow, (type) => { + const template = templates[type]; + if (template === undefined) { + throw new Error(`Cannot find template for node with id ${nodeId} with type ${type}`); + } + return template; + }) + ); + const selectNodeInputsOrThrow = getSelectorFromCache(cache, 'selectNodeInputsOrThrow', () => + createSelector(selectNodeDataOrThrow, (data) => { + return data.inputs; + }) + ); + const buildSelectInputFieldOrThrow = (fieldName: string) => + getSelectorFromCache(cache, `buildSelectInputFieldOrThrow-${fieldName}`, () => + createSelector(selectNodeInputsOrThrow, (inputs) => { + const field = inputs[fieldName]; + if (field === undefined) { + throw new Error(`Cannot find input field with name ${fieldName} in node ${nodeId}`); + } + return field; + }) + ); + const buildSelectInputFieldTemplateOrThrow = (fieldName: string) => + getSelectorFromCache(cache, `buildSelectInputFieldTemplateOrThrow-${fieldName}`, () => + createSelector(selectNodeTemplateOrThrow, (template) => { + const fieldTemplate = template.inputs[fieldName]; + if (fieldTemplate === undefined) { + throw new Error(`Cannot find input field template with name ${fieldName} in node ${nodeId}`); + } + return fieldTemplate; + }) + ); + const buildSelectOutputFieldTemplateOrThrow = (fieldName: string) => + getSelectorFromCache(cache, `buildSelectOutputFieldTemplateOrThrow-${fieldName}`, () => + createSelector(selectNodeTemplateOrThrow, (template) => { + const fieldTemplate = template.outputs[fieldName]; + if (fieldTemplate === undefined) { + throw new Error(`Cannot find output field template with name ${fieldName} in node ${nodeId}`); + } + return fieldTemplate; + }) + ); + + const buildSelectIsInputFieldConnected = (fieldName: string) => + getSelectorFromCache(cache, `buildSelectIsInputFieldConnected-${fieldName}`, () => + createSelector(selectEdges, (edges) => { + return edges.some((edge) => { + return edge.target === nodeId && edge.targetHandle === fieldName; + }); + }) + ); + + return { + nodeId, + + selectNodeSafe, + selectNodeDataSafe, + selectNodeTypeSafe, + selectNodeTemplateSafe, + selectNodeInputsSafe, + + buildSelectInputFieldSafe, + buildSelectInputFieldTemplateSafe, + buildSelectOutputFieldTemplateSafe, + + selectNodeOrThrow, + selectNodeDataOrThrow, + selectNodeTypeOrThrow, + selectNodeTemplateOrThrow, + selectNodeInputsOrThrow, + + buildSelectInputFieldOrThrow, + buildSelectInputFieldTemplateOrThrow, + buildSelectOutputFieldTemplateOrThrow, + + buildSelectIsInputFieldConnected, + } satisfies InvocationNodeContextValue; + }, [nodeId, templates]); + + return {children}; +}); + +export const useInvocationNodeContext = () => { + const context = useContext(InvocationNodeContext); + if (!context) { + throw new Error('useInvocationNodeContext must be used within an InvocationNodeProvider'); + } + return context; +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldDescriptionPopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldDescriptionPopover.tsx index 2aff85553e..6b020a221c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldDescriptionPopover.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldDescriptionPopover.tsx @@ -48,7 +48,7 @@ InputFieldDescriptionPopover.displayName = 'InputFieldDescriptionPopover'; const Content = memo(({ nodeId, fieldName }: Props) => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const description = useInputFieldUserDescriptionSafe(nodeId, fieldName); + const description = useInputFieldUserDescriptionSafe(fieldName); const onChange = useCallback( (e: ChangeEvent) => { dispatch(fieldDescriptionChanged({ nodeId, fieldName, val: e.target.value })); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeNodes.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeNodes.tsx index e2bede8a19..61825e0abd 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeNodes.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeNodes.tsx @@ -22,9 +22,9 @@ interface Props { } export const InputFieldEditModeNodes = memo(({ nodeId, fieldName }: Props) => { - const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName); - const isInvalid = useInputFieldIsInvalid(nodeId, fieldName); - const isConnected = useInputFieldIsConnected(nodeId, fieldName); + const fieldTemplate = useInputFieldTemplateOrThrow(fieldName); + const isInvalid = useInputFieldIsInvalid(fieldName); + const isConnected = useInputFieldIsConnected(fieldName); if (fieldTemplate.input === 'connection' || isConnected) { return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate.tsx index 1adee688cb..8ab7c6da2a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate.tsx @@ -15,8 +15,8 @@ type Props = PropsWithChildren<{ }>; export const InputFieldGate = memo(({ nodeId, fieldName, children, fallback, formatLabel }: Props) => { - const hasInstance = useInputFieldInstanceExists(nodeId, fieldName); - const hasTemplate = useInputFieldTemplateExists(nodeId, fieldName); + const hasInstance = useInputFieldInstanceExists(fieldName); + const hasTemplate = useInputFieldTemplateExists(fieldName); if (!hasTemplate || !hasInstance) { // fallback may be null, indicating we should render nothing at all - must check for undefined explicitly @@ -54,7 +54,7 @@ const Fallback = memo( hasInstance: boolean; }) => { const { t } = useTranslation(); - const name = useInputFieldNameSafe(nodeId, fieldName); + const name = useInputFieldNameSafe(fieldName); const label = useMemo(() => { if (formatLabel) { return formatLabel(name); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldHandle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldHandle.tsx index b526ca46a1..93b2518ca0 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldHandle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldHandle.tsx @@ -63,7 +63,7 @@ const handleStyles = { } satisfies CSSProperties; export const InputFieldHandle = memo(({ nodeId, fieldName }: Props) => { - const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName); + const fieldTemplate = useInputFieldTemplateOrThrow(fieldName); const fieldTypeName = useFieldTypeName(fieldTemplate.type); const fieldColor = useMemo(() => getFieldColor(fieldTemplate.type), [fieldTemplate.type]); const isModelField = useMemo(() => isModelFieldType(fieldTemplate.type), [fieldTemplate.type]); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx index 73f27a34cf..493960fdba 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx @@ -150,8 +150,8 @@ type Props = { }; export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props) => { - const field = useInputFieldInstance(nodeId, fieldName); - const template = useInputFieldTemplateOrThrow(nodeId, fieldName); + const field = useInputFieldInstance(fieldName); + const template = useInputFieldTemplateOrThrow(fieldName); // When deciding which component to render, first we check the type of the template, which is more efficient than the // instance type check. The instance type check uses zod and is slower. diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToDefaultValueIconButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToDefaultValueIconButton.tsx index a147743af7..170797cf45 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToDefaultValueIconButton.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToDefaultValueIconButton.tsx @@ -11,7 +11,7 @@ type Props = { export const InputFieldResetToDefaultValueIconButton = memo(({ nodeId, fieldName }: Props) => { const { t } = useTranslation(); - const { isValueChanged, resetToDefaultValue } = useInputFieldDefaultValue(nodeId, fieldName); + const { isValueChanged, resetToDefaultValue } = useInputFieldDefaultValue(fieldName); return ( { const { nodeId, fieldName, isInvalid, isDragging } = props; const inputRef = useRef(null); - const label = useInputFieldUserTitleSafe(nodeId, fieldName); - const fieldTemplateTitle = useInputFieldTemplateTitleOrThrow(nodeId, fieldName); + const label = useInputFieldUserTitleSafe(fieldName); + const fieldTemplateTitle = useInputFieldTemplateTitleOrThrow(fieldName); const { t } = useTranslation(); - const isConnected = useInputFieldIsConnected(nodeId, fieldName); + const isConnected = useInputFieldIsConnected(fieldName); const isConnectionStartField = useIsConnectionStartField(nodeId, fieldName, 'target'); const isConnectionInProgress = useIsConnectionInProgress(); const connectionError = useConnectionErrorTKey(nodeId, fieldName, 'target'); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTooltipContent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTooltipContent.tsx index 56765f40b2..5ef4a19aa0 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTooltipContent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTooltipContent.tsx @@ -15,10 +15,10 @@ interface Props { export const InputFieldTooltipContent = memo(({ nodeId, fieldName }: Props) => { const { t } = useTranslation(); - const fieldInstance = useInputFieldInstance(nodeId, fieldName); - const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName); + const fieldInstance = useInputFieldInstance(fieldName); + const fieldTemplate = useInputFieldTemplateOrThrow(fieldName); const fieldTypeName = useFieldTypeName(fieldTemplate.type); - const fieldErrors = useInputFieldErrors(nodeId, fieldName); + const fieldErrors = useInputFieldErrors(fieldName); const fieldTitle = useMemo(() => { if (fieldInstance.label && fieldTemplate.title) { diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldGate.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldGate.tsx index 85b01fdae9..bc3148df3f 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldGate.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldGate.tsx @@ -12,7 +12,7 @@ type Props = PropsWithChildren<{ }>; export const OutputFieldGate = memo(({ nodeId, fieldName, children }: Props) => { - const hasTemplate = useOutputFieldTemplateExists(nodeId, fieldName); + const hasTemplate = useOutputFieldTemplateExists(fieldName); if (!hasTemplate) { return ; @@ -25,7 +25,7 @@ OutputFieldGate.displayName = 'OutputFieldGate'; const Fallback = memo(({ nodeId, fieldName }: Props) => { const { t } = useTranslation(); - const name = useOutputFieldName(nodeId, fieldName); + const name = useOutputFieldName(fieldName); return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldHandle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldHandle.tsx index c76cf7e5d6..c801720e7e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldHandle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldHandle.tsx @@ -63,7 +63,7 @@ const handleStyles = { } satisfies CSSProperties; export const OutputFieldHandle = memo(({ nodeId, fieldName }: Props) => { - const fieldTemplate = useOutputFieldTemplate(nodeId, fieldName); + const fieldTemplate = useOutputFieldTemplate(fieldName); const fieldTypeName = useFieldTypeName(fieldTemplate.type); const fieldColor = useMemo(() => getFieldColor(fieldTemplate.type), [fieldTemplate.type]); const isModelField = useMemo(() => isModelFieldType(fieldTemplate.type), [fieldTemplate.type]); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldTitle.tsx index 22a4bb9f38..ac4715f71e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldTitle.tsx @@ -27,8 +27,8 @@ type Props = { }; export const OutputFieldTitle = memo(({ nodeId, fieldName }: Props) => { - const fieldTemplate = useOutputFieldTemplate(nodeId, fieldName); - const isConnected = useInputFieldIsConnected(nodeId, fieldName); + const fieldTemplate = useOutputFieldTemplate(fieldName); + const isConnected = useInputFieldIsConnected(fieldName); const isConnectionStartField = useIsConnectionStartField(nodeId, fieldName, 'source'); const isConnectionInProgress = useIsConnectionInProgress(); const connectionErrorTKey = useConnectionErrorTKey(nodeId, fieldName, 'source'); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldTooltipContent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldTooltipContent.tsx index fec23a8e62..96d8f53d65 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldTooltipContent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldTooltipContent.tsx @@ -10,7 +10,7 @@ interface Props { } export const OutputFieldTooltipContent = memo(({ nodeId, fieldName }: Props) => { - const fieldTemplate = useOutputFieldTemplate(nodeId, fieldName); + const fieldTemplate = useOutputFieldTemplate(fieldName); const fieldTypeName = useFieldTypeName(fieldTemplate.type); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/FloatFieldCollectionInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/FloatFieldCollectionInputComponent.tsx index 1f32ebf852..188fffc0ff 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/FloatFieldCollectionInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/FloatFieldCollectionInputComponent.tsx @@ -40,7 +40,7 @@ export const FloatFieldCollectionInputComponent = memo( const store = useAppStore(); const { t } = useTranslation(); - const isInvalid = useInputFieldIsInvalid(nodeId, field.name); + const isInvalid = useInputFieldIsInvalid(field.name); const onChangeValue = useCallback( (value: FloatFieldCollectionInputInstance['value']) => { diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldCollectionInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldCollectionInputComponent.tsx index 6dd4846a2a..2a0f5f06f6 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldCollectionInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/ImageFieldCollectionInputComponent.tsx @@ -40,7 +40,7 @@ export const ImageFieldCollectionInputComponent = memo( const { nodeId, field } = props; const store = useAppStore(); - const isInvalid = useInputFieldIsInvalid(nodeId, field.name); + const isInvalid = useInputFieldIsInvalid(field.name); const dndTargetData = useMemo( () => diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IntegerFieldCollectionInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IntegerFieldCollectionInputComponent.tsx index d37a8a0706..14e5027314 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IntegerFieldCollectionInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/IntegerFieldCollectionInputComponent.tsx @@ -43,7 +43,7 @@ export const IntegerFieldCollectionInputComponent = memo( const store = useAppStore(); const { t } = useTranslation(); - const isInvalid = useInputFieldIsInvalid(nodeId, field.name); + const isInvalid = useInputFieldIsInvalid(field.name); const onChangeValue = useCallback( (value: IntegerFieldCollectionInputInstance['value']) => { diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringFieldCollectionInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringFieldCollectionInputComponent.tsx index 0eb949265a..118336146c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringFieldCollectionInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/StringFieldCollectionInputComponent.tsx @@ -33,7 +33,7 @@ export const StringFieldCollectionInputComponent = memo( const { t } = useTranslation(); const store = useAppStore(); - const isInvalid = useInputFieldIsInvalid(nodeId, field.name); + const isInvalid = useInputFieldIsInvalid(field.name); const onRemoveString = useCallback( (index: number) => { diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeTitle.tsx index aca5bebf98..d9f77c2176 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeTitle.tsx @@ -17,10 +17,10 @@ type Props = { const NodeTitle = ({ nodeId, title }: Props) => { const dispatch = useAppDispatch(); - const label = useNodeUserTitleSafe(nodeId); + const label = useNodeUserTitleSafe(); const batchGroupId = useBatchGroupId(nodeId); const batchGroupColorToken = useBatchGroupColorToken(batchGroupId); - const templateTitle = useNodeTemplateTitleSafe(nodeId); + const templateTitle = useNodeTemplateTitleSafe(); const { t } = useTranslation(); const inputRef = useRef(null); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx index 0431485d99..06105d3985 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx @@ -1,6 +1,7 @@ import type { ChakraProps, SystemStyleObject } from '@invoke-ai/ui-library'; import { Box, useGlobalMenuClose } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; +import { InvocationNodeContextProvider } from 'features/nodes/components/flow/nodes/Invocation/context'; import { useIsWorkflowEditorLocked } from 'features/nodes/hooks/useIsWorkflowEditorLocked'; import { useMouseOverFormField, useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; import { useNodeExecutionState } from 'features/nodes/hooks/useNodeExecutionState'; @@ -137,24 +138,26 @@ const NodeWrapper = (props: NodeWrapperProps) => { ); return ( - - - - {children} - - + + + + + {children} + + + ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeHeader.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeHeader.tsx index 3256b3b5e9..b8f72bb593 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeHeader.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeHeader.tsx @@ -2,6 +2,7 @@ import type { FlexProps, SystemStyleObject } from '@invoke-ai/ui-library'; import { Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { camelCase } from 'es-toolkit/compat'; +import { InvocationNodeContextProvider } from 'features/nodes/components/flow/nodes/Invocation/context'; import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate'; import { ContainerElementSettings } from 'features/nodes/components/sidePanel/builder/ContainerElementSettings'; import { useDepthContext } from 'features/nodes/components/sidePanel/builder/contexts'; @@ -49,14 +50,16 @@ export const FormElementEditModeHeader = memo(({ element, dragHandleRef, ...rest {isContainerElement(element) && } {isNodeFieldElement(element) && ( - - - - + + + + + + )} diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElement.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElement.tsx index ad84817773..dbfe4bbf98 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElement.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElement.tsx @@ -1,4 +1,5 @@ import { useAppSelector } from 'app/store/storeHooks'; +import { InvocationNodeContextProvider } from 'features/nodes/components/flow/nodes/Invocation/context'; import { NodeFieldElementEditMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementEditMode'; import { NodeFieldElementViewMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementViewMode'; import { useElement } from 'features/nodes/components/sidePanel/builder/use-element'; @@ -15,11 +16,19 @@ export const NodeFieldElement = memo(({ id }: { id: string }) => { } if (mode === 'view') { - return ; + return ( + + + + ); } // mode === 'edit' - return ; + return ( + + + + ); }); NodeFieldElement.displayName = 'NodeFieldElement'; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementDescriptionEditable.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementDescriptionEditable.tsx index 08b31fe22b..19f8287960 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementDescriptionEditable.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementDescriptionEditable.tsx @@ -13,8 +13,8 @@ export const NodeFieldElementDescriptionEditable = memo(({ el }: { el: NodeField const { data } = el; const { fieldIdentifier } = data; const dispatch = useAppDispatch(); - const description = useInputFieldUserDescriptionSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName); - const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.nodeId, fieldIdentifier.fieldName); + const description = useInputFieldUserDescriptionSafe(fieldIdentifier.fieldName); + const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.fieldName); const inputRef = useRef(null); const onChange = useCallback( diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementEditMode.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementEditMode.tsx index 60fd35fd44..aacfba41c4 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementEditMode.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementEditMode.tsx @@ -1,5 +1,6 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library'; import { Box, Divider, Flex, FormControl } from '@invoke-ai/ui-library'; +import { InvocationNodeContextProvider } from 'features/nodes/components/flow/nodes/Invocation/context'; import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate'; import { InputFieldRenderer } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer'; import { useContainerContext } from 'features/nodes/components/sidePanel/builder/contexts'; @@ -63,25 +64,27 @@ const NodeFieldElementEditModeContent = memo( <> - - - - - - - {showDescription && } - {data.settings?.type === 'string-field-config' && data.settings.component === 'dropdown' && ( - <> - - - - )} - - + + + + + + + + {showDescription && } + {data.settings?.type === 'string-field-config' && data.settings.component === 'dropdown' && ( + <> + + + + )} + + + ); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementFloatSettings.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementFloatSettings.tsx index 92f52a874a..9485987ae1 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementFloatSettings.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementFloatSettings.tsx @@ -67,7 +67,7 @@ SettingComponent.displayName = 'SettingComponent'; const SettingMin = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const field = useInputFieldInstance(nodeId, fieldName); + const field = useInputFieldInstance(fieldName); const floatField = useFloatField(nodeId, fieldName, fieldTemplate); @@ -129,7 +129,7 @@ SettingMin.displayName = 'SettingMin'; const SettingMax = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const field = useInputFieldInstance(nodeId, fieldName); + const field = useInputFieldInstance(fieldName); const floatField = useFloatField(nodeId, fieldName, fieldTemplate); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementIntegerSettings.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementIntegerSettings.tsx index d7613a4a46..8fe6414ef9 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementIntegerSettings.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementIntegerSettings.tsx @@ -68,7 +68,7 @@ SettingComponent.displayName = 'SettingComponent'; const SettingMin = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const field = useInputFieldInstance(nodeId, fieldName); + const field = useInputFieldInstance(fieldName); const integerField = useIntegerField(nodeId, fieldName, fieldTemplate); @@ -131,7 +131,7 @@ SettingMin.displayName = 'SettingMin'; const SettingMax = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const field = useInputFieldInstance(nodeId, fieldName); + const field = useInputFieldInstance(fieldName); const integerField = useIntegerField(nodeId, fieldName, fieldTemplate); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementLabel.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementLabel.tsx index 45a1fe4c00..22c88ba05b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementLabel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementLabel.tsx @@ -8,8 +8,8 @@ import { memo, useMemo } from 'react'; export const NodeFieldElementLabel = memo(({ el }: { el: NodeFieldElement }) => { const { data } = el; const { fieldIdentifier } = data; - const label = useInputFieldUserTitleSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName); - const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.nodeId, fieldIdentifier.fieldName); + const label = useInputFieldUserTitleSafe(fieldIdentifier.fieldName); + const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.fieldName); const _label = useMemo(() => label || fieldTemplate.title, [label, fieldTemplate.title]); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementLabelEditable.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementLabelEditable.tsx index 570bf17570..4f5314db88 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementLabelEditable.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementLabelEditable.tsx @@ -12,8 +12,8 @@ export const NodeFieldElementLabelEditable = memo(({ el }: { el: NodeFieldElemen const { data } = el; const { fieldIdentifier } = data; const dispatch = useAppDispatch(); - const label = useInputFieldUserTitleSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName); - const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.nodeId, fieldIdentifier.fieldName); + const label = useInputFieldUserTitleSafe(fieldIdentifier.fieldName); + const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.fieldName); const inputRef = useRef(null); const onChange = useCallback( diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementSettings.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementSettings.tsx index 52bf7bab32..aade57e81a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementSettings.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementSettings.tsx @@ -36,7 +36,7 @@ export const NodeFieldElementSettings = memo(({ element }: { element: NodeFieldE const { id, data } = element; const { showDescription, fieldIdentifier } = data; const { nodeId, fieldName } = fieldIdentifier; - const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName); + const fieldTemplate = useInputFieldTemplateOrThrow(fieldName); const { t } = useTranslation(); const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementViewMode.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementViewMode.tsx index ded312661d..18d0ad1085 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementViewMode.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementViewMode.tsx @@ -37,8 +37,8 @@ const useFormatFallbackLabel = () => { export const NodeFieldElementViewMode = memo(({ el }: { el: NodeFieldElement }) => { const { id, data } = el; const { fieldIdentifier, showDescription } = data; - const description = useInputFieldUserDescriptionSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName); - const fieldTemplate = useInputFieldTemplateSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName); + const description = useInputFieldUserDescriptionSafe(fieldIdentifier.fieldName); + const fieldTemplate = useInputFieldTemplateSafe(fieldIdentifier.fieldName); const containerCtx = useContainerContext(); const formatFallbackLabel = useFormatFallbackLabel(); @@ -70,8 +70,8 @@ NodeFieldElementViewMode.displayName = 'NodeFieldElementViewMode'; const NodeFieldElementViewModeContent = memo(({ el }: { el: NodeFieldElement }) => { const { data } = el; const { fieldIdentifier, showDescription } = data; - const description = useInputFieldUserDescriptionSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName); - const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.nodeId, fieldIdentifier.fieldName); + const description = useInputFieldUserDescriptionSafe(fieldIdentifier.fieldName); + const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.fieldName); const _description = useMemo( () => description || fieldTemplate.description, diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/use-add-node-field-to-root.ts b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/use-add-node-field-to-root.ts index 6ccd97c57c..d7b64c0939 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/use-add-node-field-to-root.ts +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/use-add-node-field-to-root.ts @@ -9,8 +9,8 @@ import { useCallback } from 'react'; export const useAddNodeFieldToRoot = (nodeId: string, fieldName: string) => { const dispatch = useAppDispatch(); const rootElementId = useAppSelector(selectFormRootElementId); - const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName); - const field = useInputFieldInstance(nodeId, fieldName); + const fieldTemplate = useInputFieldTemplateOrThrow(fieldName); + const field = useInputFieldInstance(fieldName); const addNodeFieldToRoot = useCallback(() => { const element = buildNodeFieldElement(nodeId, fieldName, fieldTemplate.type); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx index 83f5713bff..53a581ca01 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx @@ -35,9 +35,9 @@ export default memo(InspectorDetailsTab); const Content = memo(({ nodeId }: { nodeId: string }) => { const { t } = useTranslation(); - const version = useNodeVersion(nodeId); - const template = useNodeTemplateOrThrow(nodeId); - const needsUpdate = useNodeNeedsUpdate(nodeId); + const version = useNodeVersion(); + const template = useNodeTemplateOrThrow(); + const needsUpdate = useNodeNeedsUpdate(); return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx index 8f23f747dc..64d706bfd5 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx @@ -37,7 +37,7 @@ const getKey = (result: AnyInvocationOutput, i: number) => `${result.type}-${i}` const Content = memo(({ nodeId }: { nodeId: string }) => { const { t } = useTranslation(); - const template = useNodeTemplateOrThrow(nodeId); + const template = useNodeTemplateOrThrow(); const nes = useNodeExecutionState(nodeId); if (!nes || nes.outputs.length === 0) { diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTabEditableNodeTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTabEditableNodeTitle.tsx index e0e1d26827..61528a2f01 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTabEditableNodeTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTabEditableNodeTitle.tsx @@ -14,8 +14,8 @@ type Props = { const InspectorTabEditableNodeTitle = ({ nodeId, title }: Props) => { const dispatch = useAppDispatch(); - const label = useNodeUserTitleSafe(nodeId); - const templateTitle = useNodeTemplateTitleSafe(nodeId); + const label = useNodeUserTitleSafe(); + const templateTitle = useNodeTemplateTitleSafe(); const { t } = useTranslation(); const inputRef = useRef(null); const onChange = useCallback( diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx index 685bd53f8e..d65a052290 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx @@ -29,7 +29,7 @@ export default memo(NodeTemplateInspector); const Content = memo(({ nodeId }: { nodeId: string }) => { const { t } = useTranslation(); - const template = useNodeTemplateOrThrow(nodeId); + const template = useNodeTemplateOrThrow(); return ; }); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/NodeTemplateGate.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/NodeTemplateGate.tsx index c6028dc1c0..3586c35e10 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/NodeTemplateGate.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/NodeTemplateGate.tsx @@ -6,8 +6,8 @@ import { memo } from 'react'; // easier to handle cases where we are missing a node template in the inspector. export const TemplateGate = memo( - ({ nodeId, fallback, children }: PropsWithChildren<{ nodeId: string; fallback: ReactNode }>) => { - const template = useNodeTemplateSafe(nodeId); + ({ fallback, children }: PropsWithChildren<{ nodeId: string; fallback: ReactNode }>) => { + const template = useNodeTemplateSafe(); if (!template) { return fallback; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/PublishWorkflowPanelContent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/PublishWorkflowPanelContent.tsx index b2de652ff8..ab688f650d 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/PublishWorkflowPanelContent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/PublishWorkflowPanelContent.tsx @@ -96,7 +96,7 @@ const OutputFields = memo(() => { OutputFields.displayName = 'OutputFields'; const OutputFieldsContent = memo(({ outputNodeId }: { outputNodeId: string }) => { - const outputFieldNames = useOutputFieldNames(outputNodeId); + const outputFieldNames = useOutputFieldNames(); return ( <> @@ -291,10 +291,10 @@ PublishWorkflowButton.displayName = 'DoValidationRunButton'; const NodeInputFieldPreview = memo(({ nodeId, fieldName }: { nodeId: string; fieldName: string }) => { const mouseOverFormField = useMouseOverFormField(nodeId); - const nodeUserTitle = useNodeUserTitleOrThrow(nodeId); - const nodeTemplateTitle = useNodeTemplateTitleOrThrow(nodeId); - const fieldUserTitle = useInputFieldUserTitleOrThrow(nodeId, fieldName); - const fieldTemplateTitle = useInputFieldTemplateTitleOrThrow(nodeId, fieldName); + const nodeUserTitle = useNodeUserTitleOrThrow(); + const nodeTemplateTitle = useNodeTemplateTitleOrThrow(); + const fieldUserTitle = useInputFieldUserTitleOrThrow(fieldName); + const fieldTemplateTitle = useInputFieldTemplateTitleOrThrow(fieldName); const zoomToNode = useZoomToNode(nodeId); return ( @@ -317,9 +317,9 @@ NodeInputFieldPreview.displayName = 'NodeInputFieldPreview'; const NodeOutputFieldPreview = memo(({ nodeId, fieldName }: { nodeId: string; fieldName: string }) => { const mouseOverFormField = useMouseOverFormField(nodeId); - const nodeUserTitle = useNodeUserTitleOrThrow(nodeId); - const nodeTemplateTitle = useNodeTemplateTitleOrThrow(nodeId); - const fieldTemplate = useOutputFieldTemplate(nodeId, fieldName); + const nodeUserTitle = useNodeUserTitleOrThrow(); + const nodeTemplateTitle = useNodeTemplateTitleOrThrow(); + const fieldTemplate = useOutputFieldTemplate(fieldName); const zoomToNode = useZoomToNode(nodeId); return ( diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useGetNodesNeedUpdate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useGetNodesNeedUpdate.ts index ef4da5bf86..76810d2e2f 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useGetNodesNeedUpdate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useGetNodesNeedUpdate.ts @@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { $templates } from 'features/nodes/store/nodesSlice'; -import { selectNodesSlice } from 'features/nodes/store/selectors'; +import { selectNodes } from 'features/nodes/store/selectors'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate'; import { useMemo } from 'react'; @@ -11,8 +11,11 @@ export const useGetNodesNeedUpdate = () => { const templates = useStore($templates); const selector = useMemo( () => - createSelector(selectNodesSlice, (nodes) => - nodes.nodes.filter(isInvocationNode).some((node) => { + createSelector(selectNodes, (nodes) => + nodes.some((node) => { + if (!isInvocationNode(node)) { + return false; // Invocation nodes do not need updates + } const template = templates[node.data.type]; if (!template) { return false; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldDefaultValue.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldDefaultValue.ts index ab3bc1319b..176f99d601 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldDefaultValue.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldDefaultValue.ts @@ -1,33 +1,34 @@ import { objectEquals } from '@observ33r/object-equals'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplateOrThrow'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import { fieldValueReset } from 'features/nodes/store/nodesSlice'; -import { selectNodesSlice } from 'features/nodes/store/selectors'; -import { isInvocationNode } from 'features/nodes/types/invocation'; import { useCallback, useMemo } from 'react'; -export const useInputFieldDefaultValue = (nodeId: string, fieldName: string) => { +export const useInputFieldDefaultValue = (fieldName: string) => { const dispatch = useAppDispatch(); + const ctx = useInvocationNodeContext(); + const selectDefaultValue = useMemo( + () => createSelector(ctx.buildSelectInputFieldTemplateOrThrow(fieldName), (fieldTemplate) => fieldTemplate.default), + [ctx, fieldName] + ); + const defaultValue = useAppSelector(selectDefaultValue); - const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName); const selectIsChanged = useMemo( () => - createSelector(selectNodesSlice, (nodes) => { - const node = nodes.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return; + createSelector( + [ctx.buildSelectInputFieldOrThrow(fieldName), selectDefaultValue], + (fieldInstance, defaultValue) => { + return !objectEquals(fieldInstance.value, defaultValue); } - const value = node.data.inputs[fieldName]?.value; - return !objectEquals(value, fieldTemplate.default); - }), - [fieldName, fieldTemplate.default, nodeId] + ), + [fieldName, selectDefaultValue, ctx] ); const isValueChanged = useAppSelector(selectIsChanged); const resetToDefaultValue = useCallback(() => { - dispatch(fieldValueReset({ nodeId, fieldName, value: fieldTemplate.default })); - }, [dispatch, fieldName, fieldTemplate.default, nodeId]); + dispatch(fieldValueReset({ nodeId: ctx.nodeId, fieldName, value: defaultValue })); + }, [dispatch, fieldName, defaultValue, ctx.nodeId]); - return { defaultValue: fieldTemplate.default, isValueChanged, resetToDefaultValue }; + return { defaultValue, isValueChanged, resetToDefaultValue }; }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldErrors.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldErrors.ts index 457a49e33e..0b3b67667d 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldErrors.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldErrors.ts @@ -1,44 +1,36 @@ import { useStore } from '@nanostores/react'; -import { createSelector } from '@reduxjs/toolkit'; -import { useDebouncedAppSelector } from 'app/store/use-debounced-app-selector'; -import { $templates } from 'features/nodes/store/nodesSlice'; -import { selectFieldInputInstance, selectInvocationNodeSafe, selectNodesSlice } from 'features/nodes/store/selectors'; -import { getFieldErrors } from 'features/nodes/store/util/fieldValidators'; +import { EMPTY_ARRAY } from 'app/store/constants'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; +import type { FieldError } from 'features/nodes/store/util/fieldValidators'; +import { $nodeErrors } from 'features/nodes/store/util/fieldValidators'; +import { computed } from 'nanostores'; import { useMemo } from 'react'; -import { assert } from 'tsafe'; /** * A hook that returns the errors for a given input field. The errors calculation is debounced. * - * @param nodeId The id of the node * @param fieldName The name of the field * @returns An array of FieldError objects */ -export const useInputFieldErrors = (nodeId: string, fieldName: string) => { - const templates = useStore($templates); - - const selectFieldErrors = useMemo( +export const useInputFieldErrors = (fieldName: string): FieldError[] => { + const ctx = useInvocationNodeContext(); + const $errors = useMemo( () => - createSelector(selectNodesSlice, (nodes) => { - const node = selectInvocationNodeSafe(nodes, nodeId); - if (!node) { - // If the node is not found, return an empty array - might happen during node deletion - return []; + computed($nodeErrors, (nodeErrors) => { + const thisNodeErrors = nodeErrors[ctx.nodeId]; + if (!thisNodeErrors) { + return EMPTY_ARRAY; } - const field = selectFieldInputInstance(nodes, nodeId, fieldName); - - const nodeTemplate = templates[node.data.type]; - assert(nodeTemplate, `Template for input node type ${node.data.type} not found.`); - - const fieldTemplate = nodeTemplate.inputs[fieldName]; - assert(fieldTemplate, `Template for input field ${fieldName} not found.`); - - return getFieldErrors(node, nodeTemplate, field, fieldTemplate, nodes); + const errors = thisNodeErrors.filter((error) => { + error.type === 'field-error' && error.fieldName === fieldName; + }); + if (errors.length === 0) { + return EMPTY_ARRAY; + } + return errors as FieldError[]; }), - [nodeId, fieldName, templates] + [ctx, fieldName] ); - const fieldErrors = useDebouncedAppSelector(selectFieldErrors); - - return fieldErrors; + return useStore($errors); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldInstance.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldInstance.ts index 4650b68857..6a5ad582bd 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldInstance.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldInstance.ts @@ -1,22 +1,12 @@ -import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectFieldInputInstanceSafe, selectNodesSlice } from 'features/nodes/store/selectors'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import type { FieldInputInstance } from 'features/nodes/types/field'; import { useMemo } from 'react'; -import { assert } from 'tsafe'; -export const useInputFieldInstance = (nodeId: string, fieldName: string): T => { - const selector = useMemo( - () => - createSelector(selectNodesSlice, (nodes) => { - const instance = selectFieldInputInstanceSafe(nodes, nodeId, fieldName); - assert(instance, `Instance for input field ${fieldName} not found`); - return instance; - }), - [fieldName, nodeId] - ); - - const instance = useAppSelector(selector); - - return instance as T; +export const useInputFieldInstance = (fieldName: string): T => { + const ctx = useInvocationNodeContext(); + const selector = useMemo(() => { + return ctx.buildSelectInputFieldOrThrow(fieldName); + }, [ctx, fieldName]); + return useAppSelector(selector) as T; }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldInstanceExists.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldInstanceExists.ts index 05748b404b..493b8f217a 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldInstanceExists.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldInstanceExists.ts @@ -1,23 +1,16 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectInvocationNodeSafe, selectNodesSlice } from 'features/nodes/store/selectors'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import { useMemo } from 'react'; -export const useInputFieldInstanceExists = (nodeId: string, fieldName: string) => { +export const useInputFieldInstanceExists = (fieldName: string): boolean => { + const ctx = useInvocationNodeContext(); const selector = useMemo( () => - createSelector(selectNodesSlice, (nodesSlice) => { - const node = selectInvocationNodeSafe(nodesSlice, nodeId); - if (!node) { - return false; - } - const instance = node.data.inputs[fieldName]; - return Boolean(instance); + createSelector(ctx.buildSelectInputFieldSafe(fieldName), (field) => { + return !!field; }), - [fieldName, nodeId] + [ctx, fieldName] ); - - const exists = useAppSelector(selector); - - return exists; + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldIsConnected.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldIsConnected.ts index bda4622db8..58a2349574 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldIsConnected.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldIsConnected.ts @@ -1,21 +1,10 @@ -import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/selectors'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import { useMemo } from 'react'; -export const useInputFieldIsConnected = (nodeId: string, fieldName: string) => { - const selector = useMemo( - () => - createSelector(selectNodesSlice, (nodes) => { - const firstConnectedEdge = nodes.edges.find((edge) => { - return edge.target === nodeId && edge.targetHandle === fieldName; - }); - return firstConnectedEdge !== undefined; - }), - [fieldName, nodeId] - ); +export const useInputFieldIsConnected = (fieldName: string) => { + const ctx = useInvocationNodeContext(); + const selector = useMemo(() => ctx.buildSelectIsInputFieldConnected(fieldName), [fieldName, ctx]); - const isConnected = useAppSelector(selector); - - return isConnected; + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldIsInvalid.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldIsInvalid.ts index 5408695a6f..89e36b2420 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldIsInvalid.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldIsInvalid.ts @@ -1,46 +1,32 @@ import { useStore } from '@nanostores/react'; -import { createSelector } from '@reduxjs/toolkit'; -import { useDebouncedAppSelector } from 'app/store/use-debounced-app-selector'; -import { $templates } from 'features/nodes/store/nodesSlice'; -import { selectFieldInputInstance, selectInvocationNodeSafe, selectNodesSlice } from 'features/nodes/store/selectors'; -import { getFieldErrors } from 'features/nodes/store/util/fieldValidators'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; +import { $nodeErrors } from 'features/nodes/store/util/fieldValidators'; +import { computed } from 'nanostores'; import { useMemo } from 'react'; -import { assert } from 'tsafe'; /** * A hook that returns a boolean representing whether the field is invalid. A field is invalid if it has any errors. * The errors calculation is debounced. * - * @param nodeId The id of the node * @param fieldName The name of the field * * @returns A boolean representing whether the field is invalid */ -export const useInputFieldIsInvalid = (nodeId: string, fieldName: string) => { - const templates = useStore($templates); - - const selectIsInvalid = useMemo( +export const useInputFieldIsInvalid = (fieldName: string) => { + const ctx = useInvocationNodeContext(); + const $isInvalid = useMemo( () => - createSelector(selectNodesSlice, (nodes) => { - const node = selectInvocationNodeSafe(nodes, nodeId); - if (!node) { - // If the node is not found, return false - might happen during node deletion + computed($nodeErrors, (nodeErrors) => { + const thisNodeErrors = nodeErrors[ctx.nodeId]; + if (!thisNodeErrors) { return false; } - const field = selectFieldInputInstance(nodes, nodeId, fieldName); - - const nodeTemplate = templates[node.data.type]; - assert(nodeTemplate, `Template for input node type ${node.data.type} not found.`); - - const fieldTemplate = nodeTemplate.inputs[fieldName]; - assert(fieldTemplate, `Template for input field ${fieldName} not found.`); - - return getFieldErrors(node, nodeTemplate, field, fieldTemplate, nodes).length > 0; + const isFieldInvalid = thisNodeErrors.some((error) => { + error.type === 'field-error' && error.fieldName === fieldName; + }); + return isFieldInvalid; }), - [nodeId, fieldName, templates] + [ctx, fieldName] ); - - const isInvalid = useDebouncedAppSelector(selectIsInvalid); - - return isInvalid; + return useStore($isInvalid); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldNameSafe.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldNameSafe.ts index 4b2ac84968..96a1aec2fd 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldNameSafe.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldNameSafe.ts @@ -1,27 +1,21 @@ -import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { $templates } from 'features/nodes/store/nodesSlice'; -import { selectInvocationNodeSafe, selectNodesSlice } from 'features/nodes/store/selectors'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import { useMemo } from 'react'; -export const useInputFieldNameSafe = (nodeId: string, fieldName: string) => { - const templates = useStore($templates); +export const useInputFieldNameSafe = (fieldName: string) => { + const ctx = useInvocationNodeContext(); const selector = useMemo( () => - createSelector(selectNodesSlice, (nodesSlice) => { - const node = selectInvocationNodeSafe(nodesSlice, nodeId); - if (!node) { - return fieldName; + createSelector( + [ctx.buildSelectInputFieldSafe(fieldName), ctx.buildSelectInputFieldTemplateSafe(fieldName)], + (fieldInstance, fieldTemplate) => { + const name = fieldInstance?.label || fieldTemplate?.title || fieldName; + return name; } - const instance = node.data.inputs[fieldName]; - const nodeTemplate = templates[node.data.type]; - const fieldTemplate = nodeTemplate?.inputs[fieldName]; - const name = instance?.label || fieldTemplate?.title || fieldName; - return name; - }), - [fieldName, nodeId, templates] + ), + [fieldName, ctx] ); const name = useAppSelector(selector); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldNamesByStatus.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldNamesByStatus.ts index f665d37e6d..2e35db9166 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldNamesByStatus.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldNamesByStatus.ts @@ -1,11 +1,11 @@ -import { useNodeData } from 'features/nodes/hooks/useNodeData'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import type { FieldInputTemplate } from 'features/nodes/types/field'; import { isSingleOrCollection } from 'features/nodes/types/field'; import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate'; import { useMemo } from 'react'; -import { useNodeTemplateOrThrow } from './useNodeTemplateOrThrow'; - const isConnectionInputField = (field: FieldInputTemplate) => { return ( (field.input === 'connection' && !isSingleOrCollection(field.type)) || !(field.type.name in TEMPLATE_BUILDER_MAP) @@ -19,41 +19,52 @@ const isAnyOrDirectInputField = (field: FieldInputTemplate) => { ); }; -export const useInputFieldNamesMissing = (nodeId: string) => { - const template = useNodeTemplateOrThrow(nodeId); - const node = useNodeData(nodeId); - const fieldNames = useMemo(() => { - const instanceFields = new Set(Object.keys(node.inputs)); - const allTemplateFields = new Set(Object.keys(template.inputs)); - return Array.from(instanceFields.difference(allTemplateFields)); - }, [node.inputs, template.inputs]); - return fieldNames; +export const useInputFieldNamesMissing = () => { + const ctx = useInvocationNodeContext(); + const selector = useMemo( + () => + createSelector([ctx.selectNodeInputsOrThrow, ctx.selectNodeTemplateSafe], (inputs, template) => { + const instanceFieldNames = new Set(Object.keys(inputs)); + const templateFieldNames = new Set(Object.keys(template?.inputs ?? {})); + return Array.from(instanceFieldNames.difference(templateFieldNames)); + }), + [ctx] + ); + return useAppSelector(selector); }; -export const useInputFieldNamesAnyOrDirect = (nodeId: string) => { - const template = useNodeTemplateOrThrow(nodeId); - const fieldNames = useMemo(() => { - const anyOrDirectFields: string[] = []; - for (const [fieldName, fieldTemplate] of Object.entries(template.inputs)) { - if (isAnyOrDirectInputField(fieldTemplate)) { - anyOrDirectFields.push(fieldName); - } - } - return anyOrDirectFields; - }, [template.inputs]); - return fieldNames; +export const useInputFieldNamesAnyOrDirect = () => { + const ctx = useInvocationNodeContext(); + const selector = useMemo( + () => + createSelector([ctx.selectNodeTemplateSafe], (template) => { + const fieldNames: string[] = []; + for (const [fieldName, fieldTemplate] of Object.entries(template?.inputs ?? {})) { + if (isAnyOrDirectInputField(fieldTemplate)) { + fieldNames.push(fieldName); + } + } + return fieldNames; + }), + [ctx] + ); + return useAppSelector(selector); }; -export const useInputFieldNamesConnection = (nodeId: string) => { - const template = useNodeTemplateOrThrow(nodeId); - const fieldNames = useMemo(() => { - const connectionFields: string[] = []; - for (const [fieldName, fieldTemplate] of Object.entries(template.inputs)) { - if (isConnectionInputField(fieldTemplate)) { - connectionFields.push(fieldName); - } - } - return connectionFields; - }, [template.inputs]); - return fieldNames; +export const useInputFieldNamesConnection = () => { + const ctx = useInvocationNodeContext(); + const selector = useMemo( + () => + createSelector([ctx.selectNodeTemplateSafe], (template) => { + const fieldNames: string[] = []; + for (const [fieldName, fieldTemplate] of Object.entries(template?.inputs ?? {})) { + if (isConnectionInputField(fieldTemplate)) { + fieldNames.push(fieldName); + } + } + return fieldNames; + }), + [ctx] + ); + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateExists.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateExists.ts index a81605dab6..b36ab5d85a 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateExists.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateExists.ts @@ -1,28 +1,13 @@ -import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { $templates } from 'features/nodes/store/nodesSlice'; -import { selectInvocationNodeSafe, selectNodesSlice } from 'features/nodes/store/selectors'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import { useMemo } from 'react'; -export const useInputFieldTemplateExists = (nodeId: string, fieldName: string) => { - const templates = useStore($templates); - +export const useInputFieldTemplateExists = (fieldName: string): boolean => { + const ctx = useInvocationNodeContext(); const selector = useMemo( - () => - createSelector(selectNodesSlice, (nodesSlice) => { - const node = selectInvocationNodeSafe(nodesSlice, nodeId); - if (!node) { - return false; - } - const nodeTemplate = templates[node.data.type]; - const fieldTemplate = nodeTemplate?.inputs[fieldName]; - return Boolean(fieldTemplate); - }), - [fieldName, nodeId, templates] + () => createSelector(ctx.buildSelectInputFieldTemplateSafe(fieldName), (fieldTemplate) => !!fieldTemplate), + [ctx, fieldName] ); - - const exists = useAppSelector(selector); - - return exists; + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateOrThrow.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateOrThrow.ts index 2683921c6f..4729f60f6f 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateOrThrow.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateOrThrow.ts @@ -1,24 +1,18 @@ +import { useAppSelector } from 'app/store/storeHooks'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import type { FieldInputTemplate } from 'features/nodes/types/field'; import { useMemo } from 'react'; -import { assert } from 'tsafe'; - -import { useNodeTemplateOrThrow } from './useNodeTemplateOrThrow'; /** * Returns the template for a specific input field of a node. * * **Note:** This hook will throw an error if the template for the input field is not found. * - * @param nodeId - The ID of the node. * @param fieldName - The name of the input field. * @throws Will throw an error if the template for the input field is not found. */ -export const useInputFieldTemplateOrThrow = (nodeId: string, fieldName: string): FieldInputTemplate => { - const template = useNodeTemplateOrThrow(nodeId); - const fieldTemplate = useMemo(() => { - const _fieldTemplate = template.inputs[fieldName]; - assert(_fieldTemplate, `Template for input field ${fieldName} not found.`); - return _fieldTemplate; - }, [fieldName, template.inputs]); - return fieldTemplate; +export const useInputFieldTemplateOrThrow = (fieldName: string): FieldInputTemplate => { + const ctx = useInvocationNodeContext(); + const selector = useMemo(() => ctx.buildSelectInputFieldTemplateOrThrow(fieldName), [ctx, fieldName]); + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateSafe.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateSafe.ts index 0dd761919c..73a6c59a98 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateSafe.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateSafe.ts @@ -1,4 +1,5 @@ -import { useNodeTemplateSafe } from 'features/nodes/hooks/useNodeTemplateSafe'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import type { FieldInputTemplate } from 'features/nodes/types/field'; import { useMemo } from 'react'; @@ -10,8 +11,8 @@ import { useMemo } from 'react'; * @param nodeId - The ID of the node. * @param fieldName - The name of the input field. */ -export const useInputFieldTemplateSafe = (nodeId: string, fieldName: string): FieldInputTemplate | null => { - const template = useNodeTemplateSafe(nodeId); - const fieldTemplate = useMemo(() => template?.inputs[fieldName] ?? null, [fieldName, template?.inputs]); - return fieldTemplate; +export const useInputFieldTemplateSafe = (fieldName: string): FieldInputTemplate | null => { + const ctx = useInvocationNodeContext(); + const selector = useMemo(() => ctx.buildSelectInputFieldTemplateSafe(fieldName), [ctx, fieldName]); + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateTitleOrThrow.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateTitleOrThrow.ts index 2c1aa1ca1c..efaf078490 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateTitleOrThrow.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldTemplateTitleOrThrow.ts @@ -1,16 +1,13 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import { useMemo } from 'react'; -import { assert } from 'tsafe'; -import { useNodeTemplateOrThrow } from './useNodeTemplateOrThrow'; - -export const useInputFieldTemplateTitleOrThrow = (nodeId: string, fieldName: string): string => { - const template = useNodeTemplateOrThrow(nodeId); - - const title = useMemo(() => { - const fieldTemplate = template.inputs[fieldName]; - assert(fieldTemplate, `Template for input field ${fieldName} not found.`); - return fieldTemplate.title; - }, [fieldName, template.inputs]); - - return title; +export const useInputFieldTemplateTitleOrThrow = (fieldName: string): string => { + const ctx = useInvocationNodeContext(); + const selector = useMemo( + () => createSelector(ctx.buildSelectInputFieldTemplateOrThrow(fieldName), (fieldTemplate) => fieldTemplate.title), + [ctx, fieldName] + ); + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldUserDescriptionSafe.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldUserDescriptionSafe.ts index 18ebcf0869..67ad5d8636 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldUserDescriptionSafe.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldUserDescriptionSafe.ts @@ -1,6 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectFieldInputInstanceSafe, selectNodesSlice } from 'features/nodes/store/selectors'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import { useMemo } from 'react'; /** @@ -8,19 +8,13 @@ import { useMemo } from 'react'; * * If the node doesn't exist or is not an invocation node, an empty string is returned. * - * @param nodeId The ID of the node * @param fieldName The name of the field */ -export const useInputFieldUserDescriptionSafe = (nodeId: string, fieldName: string) => { +export const useInputFieldUserDescriptionSafe = (fieldName: string) => { + const ctx = useInvocationNodeContext(); const selector = useMemo( - () => - createSelector( - selectNodesSlice, - (nodes) => selectFieldInputInstanceSafe(nodes, nodeId, fieldName)?.description ?? '' - ), - [fieldName, nodeId] + () => createSelector(ctx.buildSelectInputFieldSafe(fieldName), (field) => field?.description ?? ''), + [ctx, fieldName] ); - - const description = useAppSelector(selector); - return description; + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldUserTitleOrThrow.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldUserTitleOrThrow.ts index f4db341a60..c63abf86dd 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldUserTitleOrThrow.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldUserTitleOrThrow.ts @@ -1,6 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectFieldInputInstance, selectNodesSlice } from 'features/nodes/store/selectors'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import { useMemo } from 'react'; /** @@ -8,16 +8,13 @@ import { useMemo } from 'react'; * * If the node doesn't exist or is not an invocation node, an error is thrown. * - * @param nodeId The ID of the node * @param fieldName The name of the field */ -export const useInputFieldUserTitleOrThrow = (nodeId: string, fieldName: string): string => { +export const useInputFieldUserTitleOrThrow = (fieldName: string): string => { + const ctx = useInvocationNodeContext(); const selector = useMemo( - () => createSelector(selectNodesSlice, (nodes) => selectFieldInputInstance(nodes, nodeId, fieldName).label), - [fieldName, nodeId] + () => createSelector(ctx.buildSelectInputFieldOrThrow(fieldName), (field) => field.label), + [ctx, fieldName] ); - - const title = useAppSelector(selector); - - return title; + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldUserTitleSafe.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldUserTitleSafe.ts index 177357ff74..ce86f73321 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldUserTitleSafe.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldUserTitleSafe.ts @@ -1,6 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectFieldInputInstanceSafe, selectNodesSlice } from 'features/nodes/store/selectors'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import { useMemo } from 'react'; /** @@ -8,17 +8,13 @@ import { useMemo } from 'react'; * * If the node doesn't exist or is not an invocation node, an empty string is returned. * - * @param nodeId The ID of the node * @param fieldName The name of the field */ -export const useInputFieldUserTitleSafe = (nodeId: string, fieldName: string): string => { +export const useInputFieldUserTitleSafe = (fieldName: string): string => { + const ctx = useInvocationNodeContext(); const selector = useMemo( - () => - createSelector(selectNodesSlice, (nodes) => selectFieldInputInstanceSafe(nodes, nodeId, fieldName)?.label ?? ''), - [fieldName, nodeId] + () => createSelector(ctx.buildSelectInputFieldSafe(fieldName), (field) => field?.label ?? ''), + [ctx, fieldName] ); - - const title = useAppSelector(selector); - - return title; + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsBatchNode.ts b/invokeai/frontend/web/src/features/nodes/hooks/useIsBatchNode.ts index 2daaf4a11d..7f5bd6c69f 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useIsBatchNode.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useIsBatchNode.ts @@ -3,8 +3,8 @@ import { useMemo } from 'react'; import { useNodeTemplateOrThrow } from './useNodeTemplateOrThrow'; -export const useIsExecutableNode = (nodeId: string) => { - const template = useNodeTemplateOrThrow(nodeId); +export const useIsExecutableNode = () => { + const template = useNodeTemplateOrThrow(); const isExecutableNode = useMemo( () => !isBatchNodeType(template.type) && !isGeneratorNodeType(template.type), [template] diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeClassification.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeClassification.ts index 770d04462b..b145160d2b 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeClassification.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeClassification.ts @@ -3,8 +3,8 @@ import { useMemo } from 'react'; import { useNodeTemplateOrThrow } from './useNodeTemplateOrThrow'; -export const useNodeClassification = (nodeId: string): Classification => { - const template = useNodeTemplateOrThrow(nodeId); +export const useNodeClassification = (): Classification => { + const template = useNodeTemplateOrThrow(); const classification = useMemo(() => template.classification, [template]); return classification; }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts index aeaeb86548..b353f481e9 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts @@ -1,19 +1,8 @@ -import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodeData, selectNodesSlice } from 'features/nodes/store/selectors'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import type { InvocationNodeData } from 'features/nodes/types/invocation'; -import { useMemo } from 'react'; -export const useNodeData = (nodeId: string): InvocationNodeData => { - const selector = useMemo( - () => - createSelector(selectNodesSlice, (nodes) => { - return selectNodeData(nodes, nodeId); - }), - [nodeId] - ); - - const nodeData = useAppSelector(selector); - - return nodeData; +export const useNodeData = (): InvocationNodeData => { + const ctx = useInvocationNodeContext(); + return useAppSelector(ctx.selectNodeDataOrThrow); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeExecutionState.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeExecutionState.ts index 30ea97a959..1b854015f5 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeExecutionState.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeExecutionState.ts @@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { deepClone } from 'common/util/deepClone'; -import { selectNodesSlice } from 'features/nodes/store/selectors'; +import { selectNodes } from 'features/nodes/store/selectors'; import type { NodeExecutionStates } from 'features/nodes/store/types'; import type { NodeExecutionState } from 'features/nodes/types/invocation'; import { zNodeStatus } from 'features/nodes/types/invocation'; @@ -38,7 +38,7 @@ export const upsertExecutionState = (nodeId: string, updates?: Partial nodesSlice.nodes.map((node) => node.id)); +const selectNodeIds = createMemoizedSelector(selectNodes, (nodes) => nodes.map((node) => node.id)); export const useSyncExecutionState = () => { const nodeIds = useAppSelector(selectNodeIds); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeHasImageOutput.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeHasImageOutput.ts index ca9a43b8c2..523f48919f 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeHasImageOutput.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeHasImageOutput.ts @@ -1,10 +1,10 @@ import { some } from 'es-toolkit/compat'; import { useMemo } from 'react'; -import { useNodeTemplateOrThrow } from './useNodeTemplateOrThrow'; +import { useNodeTemplateSafe } from './useNodeTemplateSafe'; -export const useNodeHasImageOutput = (nodeId: string): boolean => { - const template = useNodeTemplateOrThrow(nodeId); +export const useNodeHasImageOutput = (): boolean => { + const template = useNodeTemplateSafe(); const hasImageOutput = useMemo( () => some( diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeIsIntermediate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeIsIntermediate.ts index d7b6f8a10e..e43575a929 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeIsIntermediate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeIsIntermediate.ts @@ -1,17 +1,16 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodeData, selectNodesSlice } from 'features/nodes/store/selectors'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import { useMemo } from 'react'; -export const useNodeIsIntermediate = (nodeId: string): boolean => { +export const useNodeIsIntermediate = (): boolean => { + const ctx = useInvocationNodeContext(); const selector = useMemo( () => - createSelector(selectNodesSlice, (nodes) => { - return selectNodeData(nodes, nodeId)?.isIntermediate ?? false; + createSelector(ctx.selectNodeDataSafe, (data) => { + return data?.isIntermediate ?? false; }), - [nodeId] + [ctx] ); - - const isIntermediate = useAppSelector(selector); - return isIntermediate; + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeIsInvalid.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeIsInvalid.ts index ba8be90745..3adf0bc6ad 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeIsInvalid.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeIsInvalid.ts @@ -1,21 +1,9 @@ import { useStore } from '@nanostores/react'; -import { createSelector } from '@reduxjs/toolkit'; -import { useAppSelector } from 'app/store/storeHooks'; -import { $templates } from 'features/nodes/store/nodesSlice'; -import { selectNodesSlice } from 'features/nodes/store/selectors'; -import { getInvocationNodeErrors } from 'features/nodes/store/util/fieldValidators'; -import { useMemo } from 'react'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; +import { $nodeErrors } from 'features/nodes/store/util/fieldValidators'; -export const useNodeIsInvalid = (nodeId: string) => { - const templates = useStore($templates); - const selectHasErrors = useMemo( - () => - createSelector(selectNodesSlice, (nodes) => { - const errors = getInvocationNodeErrors(nodeId, templates, nodes); - return errors.length > 0; - }), - [nodeId, templates] - ); - const hasErrors = useAppSelector(selectHasErrors); +export const useNodeIsInvalid = () => { + const ctx = useInvocationNodeContext(); + const hasErrors = useStore($nodeErrors, { keys: [ctx.nodeId] })[ctx.nodeId]; return hasErrors; }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeNeedsUpdate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeNeedsUpdate.ts index ae42f8fbd3..ed2e72055b 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeNeedsUpdate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeNeedsUpdate.ts @@ -4,10 +4,10 @@ import { useMemo } from 'react'; import { useNodeTemplateOrThrow } from './useNodeTemplateOrThrow'; -export const useNodeNeedsUpdate = (nodeId: string) => { - const type = useNodeType(nodeId); - const version = useNodeVersion(nodeId); - const template = useNodeTemplateOrThrow(nodeId); +export const useNodeNeedsUpdate = () => { + const type = useNodeType(); + const version = useNodeVersion(); + const template = useNodeTemplateOrThrow(); const needsUpdate = useMemo(() => { if (type !== template.type) { return true; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeNotes.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeNotes.ts index 5c68cc546b..d605edf51c 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeNotes.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeNotes.ts @@ -1,19 +1,16 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import { useMemo } from 'react'; -export const useInvocationNodeNotes = (nodeId: string): string => { +export const useInvocationNodeNotes = (): string => { + const ctx = useInvocationNodeContext(); const selector = useMemo( () => - createSelector(selectNodesSlice, (nodes) => { - const node = selectInvocationNode(nodes, nodeId); - return node.data.notes; + createSelector(ctx.selectNodeDataSafe, (data) => { + return data?.notes ?? ''; }), - [nodeId] + [ctx] ); - - const notes = useAppSelector(selector); - - return notes; + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodePack.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodePack.ts index b9fa480c1a..9ff6366460 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodePack.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodePack.ts @@ -1,17 +1,16 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodeData, selectNodesSlice } from 'features/nodes/store/selectors'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import { useMemo } from 'react'; -export const useNodePack = (nodeId: string): string | null => { +export const useNodePack = (): string | null => { + const ctx = useInvocationNodeContext(); const selector = useMemo( () => - createSelector(selectNodesSlice, (nodes) => { - return selectNodeData(nodes, nodeId)?.nodePack ?? null; + createSelector(ctx.selectNodeDataSafe, (data) => { + return data?.nodePack ?? null; }), - [nodeId] + [ctx] ); - - const nodePack = useAppSelector(selector); - return nodePack; + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateOrThrow.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateOrThrow.ts index dfad641b4a..b020bc6984 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateOrThrow.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateOrThrow.ts @@ -1,17 +1,8 @@ -import { useStore } from '@nanostores/react'; -import { useNodeType } from 'features/nodes/hooks/useNodeType'; -import { $templates } from 'features/nodes/store/nodesSlice'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import type { InvocationTemplate } from 'features/nodes/types/invocation'; -import { useMemo } from 'react'; -import { assert } from 'tsafe'; -export const useNodeTemplateOrThrow = (nodeId: string): InvocationTemplate => { - const templates = useStore($templates); - const type = useNodeType(nodeId); - const template = useMemo(() => { - const t = templates[type]; - assert(t, `Template for node type ${type} not found`); - return t; - }, [templates, type]); - return template; +export const useNodeTemplateOrThrow = (): InvocationTemplate => { + const ctx = useInvocationNodeContext(); + return useAppSelector(ctx.selectNodeTemplateOrThrow); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateSafe.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateSafe.ts index bbe8e9d775..a2146465f7 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateSafe.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateSafe.ts @@ -1,12 +1,8 @@ -import { useStore } from '@nanostores/react'; -import { useNodeType } from 'features/nodes/hooks/useNodeType'; -import { $templates } from 'features/nodes/store/nodesSlice'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import type { InvocationTemplate } from 'features/nodes/types/invocation'; -import { useMemo } from 'react'; -export const useNodeTemplateSafe = (nodeId: string): InvocationTemplate | null => { - const templates = useStore($templates); - const type = useNodeType(nodeId); - const template = useMemo(() => templates[type] ?? null, [templates, type]); - return template; +export const useNodeTemplateSafe = (): InvocationTemplate | null => { + const ctx = useInvocationNodeContext(); + return useAppSelector(ctx.selectNodeTemplateSafe); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitleOrThrow.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitleOrThrow.ts index a8b737e141..a7869df3da 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitleOrThrow.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitleOrThrow.ts @@ -1,25 +1,10 @@ -import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { $templates } from 'features/nodes/store/nodesSlice'; -import { selectNodesSlice } from 'features/nodes/store/selectors'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import { useMemo } from 'react'; -import { assert } from 'tsafe'; -export const useNodeTemplateTitleOrThrow = (nodeId: string): string => { - const templates = useStore($templates); - const selector = useMemo( - () => - createSelector(selectNodesSlice, (nodesSlice) => { - const node = nodesSlice.nodes.find((node) => node.id === nodeId); - assert(isInvocationNode(node), 'Node not found'); - const template = templates[node.data.type]; - assert(template, 'Template not found'); - return template.title; - }), - [nodeId, templates] - ); - const title = useAppSelector(selector); - return title; +export const useNodeTemplateTitleOrThrow = (): string => { + const ctx = useInvocationNodeContext(); + const selector = useMemo(() => createSelector(ctx.selectNodeTemplateOrThrow, (template) => template.title), [ctx]); + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitleSafe.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitleSafe.ts index 2503d79a94..2da8dd3f85 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitleSafe.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitleSafe.ts @@ -1,25 +1,13 @@ -import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { $templates } from 'features/nodes/store/nodesSlice'; -import { selectNodesSlice } from 'features/nodes/store/selectors'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import { useMemo } from 'react'; -export const useNodeTemplateTitleSafe = (nodeId: string): string | null => { - const templates = useStore($templates); +export const useNodeTemplateTitleSafe = (): string | null => { + const ctx = useInvocationNodeContext(); const selector = useMemo( - () => - createSelector(selectNodesSlice, (nodesSlice) => { - const node = nodesSlice.nodes.find((node) => node.id === nodeId); - if (!isInvocationNode(node)) { - return null; - } - const template = templates[node.data.type]; - return template?.title ?? null; - }), - [nodeId, templates] + () => createSelector(ctx.selectNodeTemplateSafe, (template) => template?.title ?? ''), + [ctx] ); - const title = useAppSelector(selector); - return title; + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeType.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeType.ts index f49f147314..62a56f0b02 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeType.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeType.ts @@ -1,18 +1,7 @@ -import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodeData, selectNodesSlice } from 'features/nodes/store/selectors'; -import { useMemo } from 'react'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; -export const useNodeType = (nodeId: string): string => { - const selector = useMemo( - () => - createSelector(selectNodesSlice, (nodes) => { - return selectNodeData(nodes, nodeId).type; - }), - [nodeId] - ); - - const type = useAppSelector(selector); - - return type; +export const useNodeType = (): string => { + const ctx = useInvocationNodeContext(); + return useAppSelector(ctx.selectNodeTypeOrThrow); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeUserTitleOrThrow.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeUserTitleOrThrow.ts index ed5688fbe9..41f7e11746 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeUserTitleOrThrow.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeUserTitleOrThrow.ts @@ -1,21 +1,10 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/selectors'; -import { isInvocationNode } from 'features/nodes/types/invocation'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import { useMemo } from 'react'; -import { assert } from 'tsafe'; -export const useNodeUserTitleOrThrow = (nodeId: string) => { - const selector = useMemo( - () => - createSelector(selectNodesSlice, (nodesSlice) => { - const node = nodesSlice.nodes.find((node) => node.id === nodeId); - assert(isInvocationNode(node), 'Node not found'); - return node.data.label; - }), - [nodeId] - ); - - const title = useAppSelector(selector); - return title; +export const useNodeUserTitleOrThrow = () => { + const ctx = useInvocationNodeContext(); + const selector = useMemo(() => createSelector(ctx.selectNodeDataOrThrow, (data) => data.label), [ctx]); + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeUserTitleSafe.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeUserTitleSafe.ts index 60679128b7..83ea87ba41 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeUserTitleSafe.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeUserTitleSafe.ts @@ -1,18 +1,10 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/selectors'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import { useMemo } from 'react'; -export const useNodeUserTitleSafe = (nodeId: string) => { - const selector = useMemo( - () => - createSelector(selectNodesSlice, (nodesSlice) => { - const node = nodesSlice.nodes.find((node) => node.id === nodeId); - return node?.data.label ?? null; - }), - [nodeId] - ); - - const title = useAppSelector(selector); - return title; +export const useNodeUserTitleSafe = () => { + const ctx = useInvocationNodeContext(); + const selector = useMemo(() => createSelector(ctx.selectNodeDataSafe, (data) => data?.label ?? ''), [ctx]); + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeVersion.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeVersion.ts index 06098a0fe5..a699acdf54 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeVersion.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeVersion.ts @@ -1,18 +1,10 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import { useMemo } from 'react'; -export const useNodeVersion = (nodeId: string) => { - const selector = useMemo( - () => - createSelector(selectNodesSlice, (nodesSlice) => { - const node = selectInvocationNode(nodesSlice, nodeId); - return node.data.version; - }), - [nodeId] - ); - - const version = useAppSelector(selector); - return version; +export const useNodeVersion = () => { + const ctx = useInvocationNodeContext(); + const selector = useMemo(() => createSelector(ctx.selectNodeDataOrThrow, (data) => data.version), [ctx]); + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldName.ts b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldName.ts index 8b237484e2..60c70cf4b3 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldName.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldName.ts @@ -1,26 +1,19 @@ -import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { $templates } from 'features/nodes/store/nodesSlice'; -import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import { useMemo } from 'react'; -export const useOutputFieldName = (nodeId: string, fieldName: string) => { - const templates = useStore($templates); +export const useOutputFieldName = (fieldName: string) => { + const ctx = useInvocationNodeContext(); const selector = useMemo( () => - createSelector(selectNodesSlice, (nodesSlice) => { - const node = selectInvocationNode(nodesSlice, nodeId); - const nodeTemplate = templates[node.data.type]; - const fieldTemplate = nodeTemplate?.outputs[fieldName]; + createSelector([ctx.buildSelectOutputFieldTemplateSafe(fieldName)], (fieldTemplate) => { const name = fieldTemplate?.title || fieldName; return name; }), - [fieldName, nodeId, templates] + [fieldName, ctx] ); - const name = useAppSelector(selector); - - return name; + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldNames.ts b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldNames.ts index 5c9b864a8d..81e89b0fe7 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldNames.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldNames.ts @@ -1,11 +1,17 @@ -import { map } from 'es-toolkit/compat'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames'; import { useMemo } from 'react'; -import { useNodeTemplateOrThrow } from './useNodeTemplateOrThrow'; - -export const useOutputFieldNames = (nodeId: string): string[] => { - const template = useNodeTemplateOrThrow(nodeId); - const fieldNames = useMemo(() => getSortedFilteredFieldNames(map(template.outputs)), [template.outputs]); - return fieldNames; +export const useOutputFieldNames = (): string[] => { + const ctx = useInvocationNodeContext(); + const selector = useMemo( + () => + createSelector([ctx.selectNodeTemplateOrThrow], (template) => + getSortedFilteredFieldNames(Object.values(template.outputs)) + ), + [ctx] + ); + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldTemplate.ts index 8554fdf12d..9ca2983db9 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldTemplate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldTemplate.ts @@ -1,15 +1,10 @@ +import { useAppSelector } from 'app/store/storeHooks'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import type { FieldOutputTemplate } from 'features/nodes/types/field'; import { useMemo } from 'react'; -import { assert } from 'tsafe'; -import { useNodeTemplateOrThrow } from './useNodeTemplateOrThrow'; - -export const useOutputFieldTemplate = (nodeId: string, fieldName: string): FieldOutputTemplate => { - const template = useNodeTemplateOrThrow(nodeId); - const fieldTemplate = useMemo(() => { - const _fieldTemplate = template.outputs[fieldName]; - assert(_fieldTemplate, `Template for output field ${fieldName} not found`); - return _fieldTemplate; - }, [fieldName, template.outputs]); - return fieldTemplate; +export const useOutputFieldTemplate = (fieldName: string): FieldOutputTemplate => { + const ctx = useInvocationNodeContext(); + const selector = useMemo(() => ctx.buildSelectOutputFieldTemplateOrThrow(fieldName), [ctx, fieldName]); + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldTemplateExists.ts b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldTemplateExists.ts index 5048628841..82eb650fe1 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldTemplateExists.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useOutputFieldTemplateExists.ts @@ -1,28 +1,16 @@ -import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { $templates } from 'features/nodes/store/nodesSlice'; -import { selectInvocationNodeSafe, selectNodesSlice } from 'features/nodes/store/selectors'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import { useMemo } from 'react'; -export const useOutputFieldTemplateExists = (nodeId: string, fieldName: string) => { - const templates = useStore($templates); - +export const useOutputFieldTemplateExists = (fieldName: string) => { + const ctx = useInvocationNodeContext(); const selector = useMemo( () => - createSelector(selectNodesSlice, (nodesSlice) => { - const node = selectInvocationNodeSafe(nodesSlice, nodeId); - if (!node) { - return false; - } - const nodeTemplate = templates[node.data.type]; - const fieldTemplate = nodeTemplate?.outputs[fieldName]; - return Boolean(fieldTemplate); + createSelector(ctx.buildSelectOutputFieldTemplateSafe(fieldName), (fieldTemplate) => { + return !!fieldTemplate; }), - [fieldName, nodeId, templates] + [ctx, fieldName] ); - - const exists = useAppSelector(selector); - - return exists; + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useUseCache.ts b/invokeai/frontend/web/src/features/nodes/hooks/useUseCache.ts index dadf8f72f5..5038e1e03e 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useUseCache.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useUseCache.ts @@ -1,17 +1,16 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodeData, selectNodesSlice } from 'features/nodes/store/selectors'; +import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context'; import { useMemo } from 'react'; -export const useUseCache = (nodeId: string) => { +export const useUseCache = () => { + const ctx = useInvocationNodeContext(); const selector = useMemo( () => - createSelector(selectNodesSlice, (nodes) => { - return selectNodeData(nodes, nodeId)?.useCache ?? false; + createSelector(ctx.selectNodeDataSafe, (data) => { + return data?.useCache ?? false; }), - [nodeId] + [ctx] ); - - const useCache = useAppSelector(selector); - return useCache; + return useAppSelector(selector); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useWithFooter.ts b/invokeai/frontend/web/src/features/nodes/hooks/useWithFooter.ts index fb3d03c59d..d3c63329ea 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useWithFooter.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useWithFooter.ts @@ -4,9 +4,9 @@ import { useMemo } from 'react'; import { useNodeHasImageOutput } from './useNodeHasImageOutput'; -export const useWithFooter = (nodeId: string) => { - const hasImageOutput = useNodeHasImageOutput(nodeId); - const isExecutableNode = useIsExecutableNode(nodeId); +export const useWithFooter = () => { + const hasImageOutput = useNodeHasImageOutput(); + const isExecutableNode = useIsExecutableNode(); const isCacheEnabled = useFeatureStatus('invocationCache'); const withFooter = useMemo( () => isExecutableNode && (hasImageOutput || isCacheEnabled), diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 7657a57012..40182f8cfc 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -223,63 +223,75 @@ export const nodesSlice = createSlice({ // // But we don't have immer as an explicit dependency so we'll just cast. state.nodes = applyNodeChanges(action.payload, state.nodes) as typeof state.nodes; - // Remove edges that are no longer valid, due to a removed or otherwise changed node - const edgeChanges: EdgeChange[] = []; - state.edges.forEach((e) => { - const sourceExists = state.nodes.some((n) => n.id === e.source); - const targetExists = state.nodes.some((n) => n.id === e.target); - if (!(sourceExists && targetExists)) { - edgeChanges.push({ type: 'remove', id: e.id }); - } - }); - state.edges = applyEdgeChanges(edgeChanges, state.edges); - // If a node was removed, we should remove any form fields that were associated with it. However, node changes - // may remove and then add the same node back. For example, when updating a workflow, we replace old nodes with - // updated nodes. In this case, we should not remove the form fields. To handle this, we find the last remove - // and add changes for each exposed field. If the remove change comes after the add change, we remove the exposed - // field. - for (const el of Object.values(state.form.elements)) { - if (!isNodeFieldElement(el)) { - continue; + // Remove edges that are no longer valid, due to a removed or otherwise changed node + const didNodesChange = action.payload.some( + (change) => change.type === 'add' || change.type === 'remove' || change.type === 'replace' + ); + + if (didNodesChange) { + const edgeChanges: EdgeChange[] = []; + for (const e of state.edges) { + const sourceExists = state.nodes.some((n) => n.id === e.source); + const targetExists = state.nodes.some((n) => n.id === e.target); + if (!(sourceExists && targetExists)) { + edgeChanges.push({ type: 'remove', id: e.id }); + } } - const { nodeId } = el.data.fieldIdentifier; - const removeIndex = action.payload.findLastIndex((change) => change.type === 'remove' && change.id === nodeId); - const addIndex = action.payload.findLastIndex((change) => change.type === 'add' && change.item.id === nodeId); - if (removeIndex > addIndex) { - removeElement({ form: state.form, id: el.id }); + if (edgeChanges.length > 0) { + state.edges = applyEdgeChanges(edgeChanges, state.edges); + } + } + + const wereNodesRemoved = action.payload.some((change) => change.type === 'remove' || change.type === 'replace'); + + if (wereNodesRemoved) { + // If a node was removed, we should remove any form fields that were associated with it. However, node changes + // may remove and then add the same node back. For example, when updating a workflow, we replace old nodes with + // updated nodes. In this case, we should not remove the form fields. To handle this, we find the last remove + // and add changes for each exposed field. If the remove change comes after the add change, we remove the exposed + // field. + for (const el of Object.values(state.form.elements)) { + if (!isNodeFieldElement(el)) { + continue; + } + const { nodeId } = el.data.fieldIdentifier; + const removeIndex = action.payload.findLastIndex( + (change) => change.type === 'remove' && change.id === nodeId + ); + const addIndex = action.payload.findLastIndex((change) => change.type === 'add' && change.item.id === nodeId); + if (removeIndex > addIndex) { + removeElement({ form: state.form, id: el.id }); + } } } }, edgesChanged: (state, action: PayloadAction[]>) => { const changes: EdgeChange[] = []; // We may need to massage the edge changes or otherwise handle them - action.payload.forEach((change) => { + for (const change of action.payload) { if (change.type === 'remove' || change.type === 'select') { const edge = state.edges.find((e) => e.id === change.id); // If we deleted or selected a collapsed edge, we need to find its "hidden" edges and do the same to them if (edge && edge.type === 'collapsed') { const hiddenEdges = state.edges.filter((e) => e.source === edge.source && e.target === edge.target); - if (change.type === 'remove') { - hiddenEdges.forEach(({ id }) => { + for (const { id } of hiddenEdges) { + if (change.type === 'remove') { changes.push({ type: 'remove', id }); - }); - } - if (change.type === 'select') { - hiddenEdges.forEach(({ id }) => { + } + if (change.type === 'select') { changes.push({ type: 'select', id, selected: change.selected }); - }); + } } } - } - if (change.type === 'add') { + } else if (change.type === 'add') { if (!change.item.type) { // We must add the edge type! change.item.type = 'default'; } } changes.push(change); - }); + } state.edges = applyEdgeChanges(changes, state.edges); }, fieldLabelChanged: ( diff --git a/invokeai/frontend/web/src/features/nodes/store/util/fieldValidators.ts b/invokeai/frontend/web/src/features/nodes/store/util/fieldValidators.ts index f238b14912..6bd97710e1 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/fieldValidators.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/fieldValidators.ts @@ -1,3 +1,9 @@ +import { useStore } from '@nanostores/react'; +import { EMPTY_ARRAY } from 'app/store/constants'; +import { useAppSelector } from 'app/store/storeHooks'; +import { debounce } from 'es-toolkit'; +import { $templates } from 'features/nodes/store/nodesSlice'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; import type { NodesState, Templates } from 'features/nodes/store/types'; import type { FieldInputInstance, @@ -32,6 +38,8 @@ import { } from 'features/nodes/types/field'; import { type InvocationNode, type InvocationTemplate, isInvocationNode } from 'features/nodes/types/invocation'; import { t } from 'i18next'; +import { map } from 'nanostores'; +import { useEffect } from 'react'; import { assert } from 'tsafe'; type FieldValidationFunc = ( @@ -164,13 +172,13 @@ const validateNumberFieldValue: FieldValidationFunc< return reasons; }; -type NodeError = { +export type NodeError = { type: 'node-error'; nodeId: string; issue: string; }; -type FieldError = { +export type FieldError = { type: 'field-error'; nodeId: string; fieldName: string; @@ -223,7 +231,7 @@ export const getFieldErrors = ( nodeId, fieldName, prefix, - issue: t('parameters.invoke.missingInputForField'), + issue: 'parameters.invoke.missingInputForField', }); } else if (isConnected) { // Connected fields have no value to validate - they are OK @@ -274,5 +282,57 @@ export const getInvocationNodeErrors = ( errors.push(...getFieldErrors(node, nodeTemplate, field, fieldTemplate, nodesState)); } + if (errors.length === 0) { + return EMPTY_ARRAY; + } + return errors; }; + +export const $nodeErrors = map>({}); + +export const syncNodeErrors = (nodesState: NodesState, templates: Templates) => { + for (const node of nodesState.nodes) { + const errors: (NodeError | FieldError)[] = []; + if (!isInvocationNode(node)) { + continue; + } + + const nodeTemplate = templates[node.data.type]; + + if (!nodeTemplate) { + errors.push({ type: 'node-error', nodeId: node.id, issue: t('parameters.invoke.missingNodeTemplate') }); + $nodeErrors.setKey(node.id, errors); + continue; + } + + for (const [fieldName, field] of Object.entries(node.data.inputs)) { + const fieldTemplate = nodeTemplate.inputs[fieldName]; + + if (!fieldTemplate) { + errors.push({ type: 'node-error', nodeId: node.id, issue: t('parameters.invoke.missingFieldTemplate') }); + $nodeErrors.setKey(node.id, errors); + continue; + } + + errors.push(...getFieldErrors(node, nodeTemplate, field, fieldTemplate, nodesState)); + } + + if (errors.length === 0) { + $nodeErrors.setKey(node.id, EMPTY_ARRAY); + continue; + } + + $nodeErrors.setKey(node.id, errors); + } +}; + +const debouncedSyncNodeErrors = debounce(syncNodeErrors, 300); + +export const useSyncNodeErrors = () => { + const nodesState = useAppSelector(selectNodesSlice); + const templates = useStore($templates); + useEffect(() => { + debouncedSyncNodeErrors(nodesState, templates); + }, [nodesState, templates]); +}; diff --git a/invokeai/frontend/web/src/features/parameters/hooks/useIsTooLargeToUpscale.ts b/invokeai/frontend/web/src/features/parameters/hooks/useIsTooLargeToUpscale.ts index 0a041c4e2f..8bc8859af5 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/useIsTooLargeToUpscale.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/useIsTooLargeToUpscale.ts @@ -1,4 +1,4 @@ -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice'; import { selectConfigSlice } from 'features/system/store/configSlice'; @@ -6,7 +6,7 @@ import { useMemo } from 'react'; import type { ImageDTO } from 'services/api/types'; const createIsTooLargeToUpscaleSelector = (imageDTO?: ImageDTO | null) => - createMemoizedSelector(selectUpscaleSlice, selectConfigSlice, (upscale, config) => { + createSelector(selectUpscaleSlice, selectConfigSlice, (upscale, config) => { const { upscaleModel, scale } = upscale; const { maxUpscaleDimension } = config; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromImage.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromImage.ts index 8825f2ec35..cd01f0aa1d 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromImage.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromImage.ts @@ -2,8 +2,8 @@ import type { WorkflowV3 } from 'features/nodes/types/workflow'; import { graphToWorkflow } from 'features/nodes/util/workflow/graphToWorkflow'; import { toast } from 'features/toast/toast'; import { useValidateAndLoadWorkflow } from 'features/workflowLibrary/hooks/useValidateAndLoadWorkflow'; +import { t } from 'i18next'; import { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; import { useLazyGetImageWorkflowQuery } from 'services/api/endpoints/images'; import type { NonNullableGraph } from 'services/api/types'; import { assert } from 'tsafe'; @@ -15,7 +15,6 @@ import { assert } from 'tsafe'; * and handles the loading process. */ export const useLoadWorkflowFromImage = () => { - const { t } = useTranslation(); const [getWorkflowAndGraphFromImage] = useLazyGetImageWorkflowQuery(); const validateAndLoadWorkflow = useValidateAndLoadWorkflow(); const loadWorkflowFromImage = useCallback( @@ -63,7 +62,7 @@ export const useLoadWorkflowFromImage = () => { onCompleted?.(); } }, - [getWorkflowAndGraphFromImage, validateAndLoadWorkflow, t] + [getWorkflowAndGraphFromImage, validateAndLoadWorkflow] ); return loadWorkflowFromImage; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useValidateAndLoadWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useValidateAndLoadWorkflow.ts index 575ef29089..e39bc23d4c 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useValidateAndLoadWorkflow.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useValidateAndLoadWorkflow.ts @@ -11,8 +11,8 @@ import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow' import { toast } from 'features/toast/toast'; import { navigationApi } from 'features/ui/layouts/navigation-api'; import { VIEWER_PANEL_ID, WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared'; +import { t } from 'i18next'; import { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; import { serializeError } from 'serialize-error'; import { checkBoardAccess, checkImageAccess, checkModelAccess } from 'services/api/hooks/accessChecks'; import { z } from 'zod/v4'; @@ -36,7 +36,6 @@ const log = logger('workflows'); * ...each of which internally uses hook. */ export const useValidateAndLoadWorkflow = () => { - const { t } = useTranslation(); const dispatch = useAppDispatch(); const validateAndLoadWorkflow = useCallback( /** @@ -144,7 +143,7 @@ export const useValidateAndLoadWorkflow = () => { return null; } }, - [dispatch, t] + [dispatch] ); return validateAndLoadWorkflow; diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts index 87070cd4e6..5767d30961 100644 --- a/invokeai/frontend/web/src/services/api/index.ts +++ b/invokeai/frontend/web/src/services/api/index.ts @@ -98,7 +98,10 @@ const dynamicBaseQuery: BaseQueryFn