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 0147bcaed2..baa7fc262a 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
@@ -1,7 +1,7 @@
import { Flex, Grid, GridItem } from '@invoke-ai/ui-library';
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
-import { useAnyOrDirectInputFieldNames } from 'features/nodes/hooks/useAnyOrDirectInputFieldNames';
-import { useConnectionInputFieldNames } from 'features/nodes/hooks/useConnectionInputFieldNames';
+import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck';
+import { useFieldNames } from 'features/nodes/hooks/useFieldNames';
import { useOutputFieldNames } from 'features/nodes/hooks/useOutputFieldNames';
import { useWithFooter } from 'features/nodes/hooks/useWithFooter';
import { memo } from 'react';
@@ -20,8 +20,7 @@ type Props = {
};
const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
- const inputConnectionFieldNames = useConnectionInputFieldNames(nodeId);
- const inputAnyOrDirectFieldNames = useAnyOrDirectInputFieldNames(nodeId);
+ const fieldNames = useFieldNames(nodeId);
const withFooter = useWithFooter(nodeId);
const outputFieldNames = useOutputFieldNames(nodeId);
@@ -41,9 +40,11 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
>
- {inputConnectionFieldNames.map((fieldName, i) => (
+ {fieldNames.connectionFields.map((fieldName, i) => (
-
+
+
+
))}
{outputFieldNames.map((fieldName, i) => (
@@ -52,8 +53,23 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
))}
- {inputAnyOrDirectFieldNames.map((fieldName) => (
-
+ {fieldNames.anyOrDirectFields.map((fieldName) => (
+
+
+
+ ))}
+ {fieldNames.missingFields.map((fieldName) => (
+
+
+
))}
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/MissingFallback.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/MissingFallback.tsx
deleted file mode 100644
index ca5b74b7ff..0000000000
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/MissingFallback.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { useDoesFieldExist } from 'features/nodes/hooks/useDoesFieldExist';
-import type { PropsWithChildren } from 'react';
-import { memo } from 'react';
-
-type Props = PropsWithChildren<{
- nodeId: string;
- fieldName?: string;
-}>;
-
-export const MissingFallback = memo((props: Props) => {
- // We must be careful here to avoid race conditions where a deleted node is still referenced as an exposed field
- const exists = useDoesFieldExist(props.nodeId, props.fieldName);
- if (!exists) {
- return null;
- }
-
- return props.children;
-});
-
-MissingFallback.displayName = 'MissingFallback';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx
index 474e0651f7..fd3cc6d6bf 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx
@@ -1,16 +1,14 @@
-import { Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
+import { Flex, FormControl } from '@invoke-ai/ui-library';
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
import { useDoesInputHaveValue } from 'features/nodes/hooks/useDoesInputHaveValue';
-import { useFieldInputInstance } from 'features/nodes/hooks/useFieldInputInstance';
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
-import type { PropsWithChildren } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
-import { useTranslation } from 'react-i18next';
import EditableFieldTitle from './EditableFieldTitle';
import FieldHandle from './FieldHandle';
import FieldLinearViewToggle from './FieldLinearViewToggle';
import InputFieldRenderer from './InputFieldRenderer';
+import { InputFieldWrapper } from './InputFieldWrapper';
interface Props {
nodeId: string;
@@ -18,9 +16,7 @@ interface Props {
}
const InputField = ({ nodeId, fieldName }: Props) => {
- const { t } = useTranslation();
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
- const fieldInstance = useFieldInputInstance(nodeId, fieldName);
const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName);
const [isHovered, setIsHovered] = useState(false);
@@ -55,20 +51,6 @@ const InputField = ({ nodeId, fieldName }: Props) => {
setIsHovered(false);
}, []);
- if (!fieldTemplate || !fieldInstance) {
- return (
-
-
-
- {t('nodes.unknownInput', {
- name: fieldInstance?.label ?? fieldTemplate?.title ?? fieldName,
- })}
-
-
-
- );
- }
-
if (fieldTemplate.input === 'connection' || isConnected) {
return (
@@ -134,27 +116,3 @@ const InputField = ({ nodeId, fieldName }: Props) => {
};
export default memo(InputField);
-
-type InputFieldWrapperProps = PropsWithChildren<{
- shouldDim: boolean;
-}>;
-
-const InputFieldWrapper = memo(({ shouldDim, children }: InputFieldWrapperProps) => {
- return (
-
- {children}
-
- );
-});
-
-InputFieldWrapper.displayName = 'InputFieldWrapper';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper.tsx
new file mode 100644
index 0000000000..8723538f85
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper.tsx
@@ -0,0 +1,27 @@
+import { Flex } from '@invoke-ai/ui-library';
+import type { PropsWithChildren } from 'react';
+import { memo } from 'react';
+
+type InputFieldWrapperProps = PropsWithChildren<{
+ shouldDim: boolean;
+}>;
+
+export const InputFieldWrapper = memo(({ shouldDim, children }: InputFieldWrapperProps) => {
+ return (
+
+ {children}
+
+ );
+});
+
+InputFieldWrapper.displayName = 'InputFieldWrapper';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck.tsx
new file mode 100644
index 0000000000..f4b6be0cd6
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck.tsx
@@ -0,0 +1,59 @@
+import { Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppSelector } from 'app/store/storeHooks';
+import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
+import { selectInvocationNode } from 'features/nodes/store/selectors';
+import type { PropsWithChildren } from 'react';
+import { memo, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+type Props = PropsWithChildren<{
+ nodeId: string;
+ fieldName: string;
+}>;
+
+export const InvocationInputFieldCheck = memo(({ nodeId, fieldName, children }: Props) => {
+ const { t } = useTranslation();
+ const templates = useStore($templates);
+ const selector = useMemo(
+ () =>
+ createSelector(selectNodesSlice, (nodesSlice) => {
+ const node = selectInvocationNode(nodesSlice, nodeId);
+ const instance = node.data.inputs[fieldName];
+ const template = templates[node.data.type];
+ const fieldTemplate = template?.inputs[fieldName];
+ return {
+ name: instance?.label || fieldTemplate?.title || fieldName,
+ hasInstance: Boolean(instance),
+ hasTemplate: Boolean(fieldTemplate),
+ };
+ }),
+ [fieldName, nodeId, templates]
+ );
+ const { hasInstance, hasTemplate, name } = useAppSelector(selector);
+
+ if (!hasTemplate || !hasInstance) {
+ return (
+
+
+
+ {t('nodes.unknownInput', { name })}
+
+
+
+ );
+ }
+
+ return children;
+});
+
+InvocationInputFieldCheck.displayName = 'InvocationInputFieldCheck';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx
index f7ff85f479..ef466b2882 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx
@@ -3,7 +3,7 @@ import { CSS } from '@dnd-kit/utilities';
import { Flex, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay';
-import { MissingFallback } from 'features/nodes/components/flow/nodes/Invocation/MissingFallback';
+import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck';
import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue';
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice';
@@ -102,9 +102,9 @@ const LinearViewFieldInternal = ({ nodeId, fieldName }: Props) => {
const LinearViewField = ({ nodeId, fieldName }: Props) => {
return (
-
+
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx
index 966809cb0e..76666af396 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx
@@ -48,7 +48,7 @@ const NotesNode = (props: NodeProps) => {
gap={1}
>
-
+
>
diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowField.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowField.tsx
index a30bda354d..482de6693e 100644
--- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowField.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowField.tsx
@@ -1,7 +1,7 @@
import { Flex, FormLabel, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library';
import FieldTooltipContent from 'features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent';
import InputFieldRenderer from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
-import { MissingFallback } from 'features/nodes/components/flow/nodes/Invocation/MissingFallback';
+import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck';
import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel';
import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue';
import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle';
@@ -53,9 +53,9 @@ const WorkflowFieldInternal = ({ nodeId, fieldName }: Props) => {
const WorkflowField = ({ nodeId, fieldName }: Props) => {
return (
-
+
-
+
);
};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts b/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts
deleted file mode 100644
index 7fae0de16e..0000000000
--- a/invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { EMPTY_ARRAY } from 'app/store/constants';
-import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
-import { isSingleOrCollection } from 'features/nodes/types/field';
-import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames';
-import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate';
-import { keys, map } from 'lodash-es';
-import { useMemo } from 'react';
-
-export const useAnyOrDirectInputFieldNames = (nodeId: string): string[] => {
- const template = useNodeTemplate(nodeId);
-
- const fieldNames = useMemo(() => {
- const fields = map(template.inputs).filter((field) => {
- return (
- (['any', 'direct'].includes(field.input) || isSingleOrCollection(field.type)) &&
- keys(TEMPLATE_BUILDER_MAP).includes(field.type.name)
- );
- });
- const _fieldNames = getSortedFilteredFieldNames(fields);
- if (_fieldNames.length === 0) {
- return EMPTY_ARRAY;
- }
- return _fieldNames;
- }, [template.inputs]);
-
- return fieldNames;
-};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts
deleted file mode 100644
index 16ace597c1..0000000000
--- a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { EMPTY_ARRAY } from 'app/store/constants';
-import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
-import { isSingleOrCollection } from 'features/nodes/types/field';
-import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames';
-import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate';
-import { keys, map } from 'lodash-es';
-import { useMemo } from 'react';
-
-export const useConnectionInputFieldNames = (nodeId: string): string[] => {
- const template = useNodeTemplate(nodeId);
- const fieldNames = useMemo(() => {
- // get the visible fields
- const fields = map(template.inputs).filter(
- (field) =>
- (field.input === 'connection' && !isSingleOrCollection(field.type)) ||
- !keys(TEMPLATE_BUILDER_MAP).includes(field.type.name)
- );
-
- const _fieldNames = getSortedFilteredFieldNames(fields);
-
- if (_fieldNames.length === 0) {
- return EMPTY_ARRAY;
- }
-
- return _fieldNames;
- }, [template.inputs]);
-
- return fieldNames;
-};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useDoesFieldExist.ts b/invokeai/frontend/web/src/features/nodes/hooks/useDoesFieldExist.ts
deleted file mode 100644
index 4e97b1689c..0000000000
--- a/invokeai/frontend/web/src/features/nodes/hooks/useDoesFieldExist.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { useAppSelector } from 'app/store/storeHooks';
-import { isInvocationNode } from 'features/nodes/types/invocation';
-
-export const useDoesFieldExist = (nodeId: string, fieldName?: string) => {
- const doesFieldExist = useAppSelector((s) => {
- const node = s.nodes.present.nodes.find((n) => n.id === nodeId);
- if (!isInvocationNode(node)) {
- return false;
- }
- if (fieldName === undefined) {
- return true;
- }
- if (!node.data.inputs[fieldName]) {
- return false;
- }
- return true;
- });
-
- return doesFieldExist;
-};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputTemplate.ts
index 4b70847ad1..729319e0dd 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputTemplate.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputTemplate.ts
@@ -1,9 +1,14 @@
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
import type { FieldInputTemplate } from 'features/nodes/types/field';
import { useMemo } from 'react';
+import { assert } from 'tsafe';
-export const useFieldInputTemplate = (nodeId: string, fieldName: string): FieldInputTemplate | null => {
+export const useFieldInputTemplate = (nodeId: string, fieldName: string): FieldInputTemplate => {
const template = useNodeTemplate(nodeId);
- const fieldTemplate = useMemo(() => template.inputs[fieldName] ?? null, [fieldName, template.inputs]);
+ const fieldTemplate = useMemo(() => {
+ const _fieldTemplate = template.inputs[fieldName];
+ assert(_fieldTemplate, `Field template for field ${fieldName} not found`);
+ return _fieldTemplate;
+ }, [fieldName, template.inputs]);
return fieldTemplate;
};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldNames.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldNames.ts
new file mode 100644
index 0000000000..19849fb296
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldNames.ts
@@ -0,0 +1,39 @@
+import { useNodeData } from 'features/nodes/hooks/useNodeData';
+import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
+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 { difference, filter, keys } from 'lodash-es';
+import { useMemo } from 'react';
+
+const isConnectionInputField = (field: FieldInputTemplate) => {
+ return (
+ (field.input === 'connection' && !isSingleOrCollection(field.type)) ||
+ !keys(TEMPLATE_BUILDER_MAP).includes(field.type.name)
+ );
+};
+
+const isAnyOrDirectInputField = (field: FieldInputTemplate) => {
+ return (
+ (['any', 'direct'].includes(field.input) || isSingleOrCollection(field.type)) &&
+ keys(TEMPLATE_BUILDER_MAP).includes(field.type.name)
+ );
+};
+
+export const useFieldNames = (nodeId: string) => {
+ const template = useNodeTemplate(nodeId);
+ const node = useNodeData(nodeId);
+ const fieldNames = useMemo(() => {
+ const instanceFields = keys(node.inputs);
+ const allTemplateFields = keys(template.inputs);
+ const missingFields = difference(instanceFields, allTemplateFields);
+ const connectionFields = filter(template.inputs, isConnectionInputField).map((f) => f.name);
+ const anyOrDirectFields = filter(template.inputs, isAnyOrDirectInputField).map((f) => f.name);
+ return {
+ missingFields,
+ connectionFields,
+ anyOrDirectFields,
+ };
+ }, [node.inputs, template.inputs]);
+ return fieldNames;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeLabel.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeLabel.ts
index 31dcb9c466..56e77a39e8 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeLabel.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeLabel.ts
@@ -1,14 +1,14 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
-import { selectNodeData } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
export const useNodeLabel = (nodeId: string) => {
const selector = useMemo(
() =>
- createSelector(selectNodesSlice, (nodes) => {
- return selectNodeData(nodes, nodeId)?.label ?? null;
+ createSelector(selectNodesSlice, (nodesSlice) => {
+ const node = nodesSlice.nodes.find((node) => node.id === nodeId);
+ return node?.data.label;
}),
[nodeId]
);
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts
index a63e0433aa..39ae617460 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts
@@ -1,8 +1,24 @@
-import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
+import { useStore } from '@nanostores/react';
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppSelector } from 'app/store/storeHooks';
+import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
+import { isInvocationNode } from 'features/nodes/types/invocation';
import { useMemo } from 'react';
export const useNodeTemplateTitle = (nodeId: string): string | null => {
- const template = useNodeTemplate(nodeId);
- const title = useMemo(() => template.title, [template.title]);
+ const templates = useStore($templates);
+ 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]
+ );
+ const title = useAppSelector(selector);
return title;
};
diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
index e7c1877647..5ebc5de147 100644
--- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
@@ -275,10 +275,9 @@ export const nodesSlice = createSlice({
const { nodeId, label } = action.payload;
const nodeIndex = state.nodes.findIndex((n) => n.id === nodeId);
const node = state.nodes?.[nodeIndex];
- if (!isInvocationNode(node)) {
- return;
+ if (isInvocationNode(node) || isNotesNode(node)) {
+ node.data.label = label;
}
- node.data.label = label;
},
nodeNotesChanged: (state, action: PayloadAction<{ nodeId: string; notes: string }>) => {
const { nodeId, notes } = action.payload;
diff --git a/invokeai/frontend/web/src/features/nodes/store/selectors.ts b/invokeai/frontend/web/src/features/nodes/store/selectors.ts
index 4739a77e1c..be8cfafa8b 100644
--- a/invokeai/frontend/web/src/features/nodes/store/selectors.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/selectors.ts
@@ -4,7 +4,7 @@ import type { InvocationNode, InvocationNodeData } from 'features/nodes/types/in
import { isInvocationNode } from 'features/nodes/types/invocation';
import { assert } from 'tsafe';
-const selectInvocationNode = (nodesSlice: NodesState, nodeId: string): InvocationNode => {
+export const selectInvocationNode = (nodesSlice: NodesState, nodeId: string): InvocationNode => {
const node = nodesSlice.nodes.find((node) => node.id === nodeId);
assert(isInvocationNode(node), `Node ${nodeId} is not an invocation node`);
return node;
diff --git a/invokeai/version/invokeai_version.py b/invokeai/version/invokeai_version.py
index 2e905e44da..9116e0a7eb 100644
--- a/invokeai/version/invokeai_version.py
+++ b/invokeai/version/invokeai_version.py
@@ -1 +1 @@
-__version__ = "4.2.2"
+__version__ = "4.2.2post1"