Skip to content

Commit aa0c59b

Browse files
fix(ui): crash when using notes nodes or missing node/field templates (#6412)
## Summary Notes nodes used some overly-strict redux selectors. The selectors are now more chill. Also fixed an issue where you couldn't edit a notes node title. Found another class of error related to the overly strict reducers that caused errors when loading a workflow that had missing templates. Fixed this with fallback wrapper component, works like an error boundary when a template isn't found. ## Related Issues / Discussions https://discord.com/channels/1020123559063990373/1149506274971631688/1242256425527545949 ## QA Instructions - Add a notes node to a workflow. Edit the notes title. - Load a workflow that has nodes that aren't installed. Should get a fallback UI for each missing node. - Load a workflow that references a node with different inputs than are in the template - like an old version of a node. Should get a fallback field warning for both missing templates, or missing inputs. ## Merge Plan n/a ## Checklist - [x] _The PR has a short but descriptive title, suitable for a changelog_ - [ ] _Tests added / updated (if applicable)_ - [ ] _Documentation added / updated (if applicable)_
2 parents 66c9f47 + e4acaa5 commit aa0c59b

18 files changed

+191
-168
lines changed

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Flex, Grid, GridItem } from '@invoke-ai/ui-library';
22
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
3-
import { useAnyOrDirectInputFieldNames } from 'features/nodes/hooks/useAnyOrDirectInputFieldNames';
4-
import { useConnectionInputFieldNames } from 'features/nodes/hooks/useConnectionInputFieldNames';
3+
import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck';
4+
import { useFieldNames } from 'features/nodes/hooks/useFieldNames';
55
import { useOutputFieldNames } from 'features/nodes/hooks/useOutputFieldNames';
66
import { useWithFooter } from 'features/nodes/hooks/useWithFooter';
77
import { memo } from 'react';
@@ -20,8 +20,7 @@ type Props = {
2020
};
2121

2222
const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
23-
const inputConnectionFieldNames = useConnectionInputFieldNames(nodeId);
24-
const inputAnyOrDirectFieldNames = useAnyOrDirectInputFieldNames(nodeId);
23+
const fieldNames = useFieldNames(nodeId);
2524
const withFooter = useWithFooter(nodeId);
2625
const outputFieldNames = useOutputFieldNames(nodeId);
2726

@@ -41,9 +40,11 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
4140
>
4241
<Flex flexDir="column" px={2} w="full" h="full">
4342
<Grid gridTemplateColumns="1fr auto" gridAutoRows="1fr">
44-
{inputConnectionFieldNames.map((fieldName, i) => (
43+
{fieldNames.connectionFields.map((fieldName, i) => (
4544
<GridItem gridColumnStart={1} gridRowStart={i + 1} key={`${nodeId}.${fieldName}.input-field`}>
46-
<InputField nodeId={nodeId} fieldName={fieldName} />
45+
<InvocationInputFieldCheck nodeId={nodeId} fieldName={fieldName}>
46+
<InputField nodeId={nodeId} fieldName={fieldName} />
47+
</InvocationInputFieldCheck>
4748
</GridItem>
4849
))}
4950
{outputFieldNames.map((fieldName, i) => (
@@ -52,8 +53,23 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => {
5253
</GridItem>
5354
))}
5455
</Grid>
55-
{inputAnyOrDirectFieldNames.map((fieldName) => (
56-
<InputField key={`${nodeId}.${fieldName}.input-field`} nodeId={nodeId} fieldName={fieldName} />
56+
{fieldNames.anyOrDirectFields.map((fieldName) => (
57+
<InvocationInputFieldCheck
58+
key={`${nodeId}.${fieldName}.input-field`}
59+
nodeId={nodeId}
60+
fieldName={fieldName}
61+
>
62+
<InputField nodeId={nodeId} fieldName={fieldName} />
63+
</InvocationInputFieldCheck>
64+
))}
65+
{fieldNames.missingFields.map((fieldName) => (
66+
<InvocationInputFieldCheck
67+
key={`${nodeId}.${fieldName}.input-field`}
68+
nodeId={nodeId}
69+
fieldName={fieldName}
70+
>
71+
<InputField nodeId={nodeId} fieldName={fieldName} />
72+
</InvocationInputFieldCheck>
5773
))}
5874
</Flex>
5975
</Flex>

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/MissingFallback.tsx

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

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,22 @@
1-
import { Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
1+
import { Flex, FormControl } from '@invoke-ai/ui-library';
22
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
33
import { useDoesInputHaveValue } from 'features/nodes/hooks/useDoesInputHaveValue';
4-
import { useFieldInputInstance } from 'features/nodes/hooks/useFieldInputInstance';
54
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
6-
import type { PropsWithChildren } from 'react';
75
import { memo, useCallback, useMemo, useState } from 'react';
8-
import { useTranslation } from 'react-i18next';
96

107
import EditableFieldTitle from './EditableFieldTitle';
118
import FieldHandle from './FieldHandle';
129
import FieldLinearViewToggle from './FieldLinearViewToggle';
1310
import InputFieldRenderer from './InputFieldRenderer';
11+
import { InputFieldWrapper } from './InputFieldWrapper';
1412

1513
interface Props {
1614
nodeId: string;
1715
fieldName: string;
1816
}
1917

2018
const InputField = ({ nodeId, fieldName }: Props) => {
21-
const { t } = useTranslation();
2219
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
23-
const fieldInstance = useFieldInputInstance(nodeId, fieldName);
2420
const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName);
2521
const [isHovered, setIsHovered] = useState(false);
2622

@@ -55,20 +51,6 @@ const InputField = ({ nodeId, fieldName }: Props) => {
5551
setIsHovered(false);
5652
}, []);
5753

58-
if (!fieldTemplate || !fieldInstance) {
59-
return (
60-
<InputFieldWrapper shouldDim={shouldDim}>
61-
<FormControl alignItems="stretch" justifyContent="space-between" flexDir="column" gap={2} h="full" w="full">
62-
<FormLabel display="flex" alignItems="center" mb={0} px={1} gap={2} h="full">
63-
{t('nodes.unknownInput', {
64-
name: fieldInstance?.label ?? fieldTemplate?.title ?? fieldName,
65-
})}
66-
</FormLabel>
67-
</FormControl>
68-
</InputFieldWrapper>
69-
);
70-
}
71-
7254
if (fieldTemplate.input === 'connection' || isConnected) {
7355
return (
7456
<InputFieldWrapper shouldDim={shouldDim}>
@@ -134,27 +116,3 @@ const InputField = ({ nodeId, fieldName }: Props) => {
134116
};
135117

136118
export default memo(InputField);
137-
138-
type InputFieldWrapperProps = PropsWithChildren<{
139-
shouldDim: boolean;
140-
}>;
141-
142-
const InputFieldWrapper = memo(({ shouldDim, children }: InputFieldWrapperProps) => {
143-
return (
144-
<Flex
145-
position="relative"
146-
minH={8}
147-
py={0.5}
148-
alignItems="center"
149-
opacity={shouldDim ? 0.5 : 1}
150-
transitionProperty="opacity"
151-
transitionDuration="0.1s"
152-
w="full"
153-
h="full"
154-
>
155-
{children}
156-
</Flex>
157-
);
158-
});
159-
160-
InputFieldWrapper.displayName = 'InputFieldWrapper';
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Flex } from '@invoke-ai/ui-library';
2+
import type { PropsWithChildren } from 'react';
3+
import { memo } from 'react';
4+
5+
type InputFieldWrapperProps = PropsWithChildren<{
6+
shouldDim: boolean;
7+
}>;
8+
9+
export const InputFieldWrapper = memo(({ shouldDim, children }: InputFieldWrapperProps) => {
10+
return (
11+
<Flex
12+
position="relative"
13+
minH={8}
14+
py={0.5}
15+
alignItems="center"
16+
opacity={shouldDim ? 0.5 : 1}
17+
transitionProperty="opacity"
18+
transitionDuration="0.1s"
19+
w="full"
20+
h="full"
21+
>
22+
{children}
23+
</Flex>
24+
);
25+
});
26+
27+
InputFieldWrapper.displayName = 'InputFieldWrapper';
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
2+
import { useStore } from '@nanostores/react';
3+
import { createSelector } from '@reduxjs/toolkit';
4+
import { useAppSelector } from 'app/store/storeHooks';
5+
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
6+
import { selectInvocationNode } from 'features/nodes/store/selectors';
7+
import type { PropsWithChildren } from 'react';
8+
import { memo, useMemo } from 'react';
9+
import { useTranslation } from 'react-i18next';
10+
11+
type Props = PropsWithChildren<{
12+
nodeId: string;
13+
fieldName: string;
14+
}>;
15+
16+
export const InvocationInputFieldCheck = memo(({ nodeId, fieldName, children }: Props) => {
17+
const { t } = useTranslation();
18+
const templates = useStore($templates);
19+
const selector = useMemo(
20+
() =>
21+
createSelector(selectNodesSlice, (nodesSlice) => {
22+
const node = selectInvocationNode(nodesSlice, nodeId);
23+
const instance = node.data.inputs[fieldName];
24+
const template = templates[node.data.type];
25+
const fieldTemplate = template?.inputs[fieldName];
26+
return {
27+
name: instance?.label || fieldTemplate?.title || fieldName,
28+
hasInstance: Boolean(instance),
29+
hasTemplate: Boolean(fieldTemplate),
30+
};
31+
}),
32+
[fieldName, nodeId, templates]
33+
);
34+
const { hasInstance, hasTemplate, name } = useAppSelector(selector);
35+
36+
if (!hasTemplate || !hasInstance) {
37+
return (
38+
<Flex position="relative" minH={8} py={0.5} alignItems="center" w="full" h="full">
39+
<FormControl
40+
isInvalid={true}
41+
alignItems="stretch"
42+
justifyContent="center"
43+
flexDir="column"
44+
gap={2}
45+
h="full"
46+
w="full"
47+
>
48+
<FormLabel display="flex" mb={0} px={1} py={2} gap={2}>
49+
{t('nodes.unknownInput', { name })}
50+
</FormLabel>
51+
</FormControl>
52+
</Flex>
53+
);
54+
}
55+
56+
return children;
57+
});
58+
59+
InvocationInputFieldCheck.displayName = 'InvocationInputFieldCheck';

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { CSS } from '@dnd-kit/utilities';
33
import { Flex, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library';
44
import { useAppDispatch } from 'app/store/storeHooks';
55
import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay';
6-
import { MissingFallback } from 'features/nodes/components/flow/nodes/Invocation/MissingFallback';
6+
import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck';
77
import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue';
88
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
99
import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice';
@@ -102,9 +102,9 @@ const LinearViewFieldInternal = ({ nodeId, fieldName }: Props) => {
102102

103103
const LinearViewField = ({ nodeId, fieldName }: Props) => {
104104
return (
105-
<MissingFallback nodeId={nodeId} fieldName={fieldName}>
105+
<InvocationInputFieldCheck nodeId={nodeId} fieldName={fieldName}>
106106
<LinearViewFieldInternal nodeId={nodeId} fieldName={fieldName} />
107-
</MissingFallback>
107+
</InvocationInputFieldCheck>
108108
);
109109
};
110110

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const NotesNode = (props: NodeProps<NotesNodeData>) => {
4848
gap={1}
4949
>
5050
<Flex className="nopan" w="full" h="full" flexDir="column">
51-
<Textarea value={notes} onChange={handleChange} rows={8} resize="none" fontSize="sm" />
51+
<Textarea className="nodrag" value={notes} onChange={handleChange} rows={8} resize="none" fontSize="sm" />
5252
</Flex>
5353
</Flex>
5454
</>

invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/WorkflowField.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Flex, FormLabel, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library';
22
import FieldTooltipContent from 'features/nodes/components/flow/nodes/Invocation/fields/FieldTooltipContent';
33
import InputFieldRenderer from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
4-
import { MissingFallback } from 'features/nodes/components/flow/nodes/Invocation/MissingFallback';
4+
import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck';
55
import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel';
66
import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue';
77
import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle';
@@ -53,9 +53,9 @@ const WorkflowFieldInternal = ({ nodeId, fieldName }: Props) => {
5353

5454
const WorkflowField = ({ nodeId, fieldName }: Props) => {
5555
return (
56-
<MissingFallback nodeId={nodeId} fieldName={fieldName}>
56+
<InvocationInputFieldCheck nodeId={nodeId} fieldName={fieldName}>
5757
<WorkflowFieldInternal nodeId={nodeId} fieldName={fieldName} />
58-
</MissingFallback>
58+
</InvocationInputFieldCheck>
5959
);
6060
};
6161

invokeai/frontend/web/src/features/nodes/hooks/useAnyOrDirectInputFieldNames.ts

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

invokeai/frontend/web/src/features/nodes/hooks/useConnectionInputFieldNames.ts

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

0 commit comments

Comments
 (0)