Skip to content

Commit 946a622

Browse files
feat(ui): better labels for missing/unexpected fields
1 parent 384e67d commit 946a622

File tree

10 files changed

+161
-95
lines changed

10 files changed

+161
-95
lines changed

Diff for: invokeai/frontend/web/public/locales/en.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -1014,7 +1014,10 @@
10141014
"unknownNodeType": "Unknown node type",
10151015
"unknownTemplate": "Unknown Template",
10161016
"unknownInput": "Unknown input: {{name}}",
1017-
"unknownOutput": "Unknown output: {{name}}",
1017+
"missingField_withName": "Missing field \"{{name}}\"",
1018+
"unexpectedField_withName": "Unexpected field \"{{name}}\"",
1019+
"unknownField_withName": "Unknown field \"{{name}}\"",
1020+
"unknownFieldEditWorkflowToFix_withName": "Unknown field \"{{name}}\" (edit workflow to fix)",
10181021
"updateNode": "Update Node",
10191022
"updateApp": "Update App",
10201023
"loadingTemplates": "Loading {{name}}",
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,83 @@
1-
import { InputFieldUnknownPlaceholder } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldUnknownPlaceholder';
1+
import { FormControl, FormLabel } from '@invoke-ai/ui-library';
2+
import { InputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper';
23
import { useInputFieldInstanceExists } from 'features/nodes/hooks/useInputFieldInstanceExists';
4+
import { useInputFieldNameSafe } from 'features/nodes/hooks/useInputFieldNameSafe';
35
import { useInputFieldTemplateExists } from 'features/nodes/hooks/useInputFieldTemplateExists';
46
import type { PropsWithChildren, ReactNode } from 'react';
5-
import { memo } from 'react';
7+
import { memo, useMemo } from 'react';
8+
import { useTranslation } from 'react-i18next';
69

710
type Props = PropsWithChildren<{
811
nodeId: string;
912
fieldName: string;
10-
placeholder?: ReactNode;
13+
fallback?: ReactNode;
14+
formatLabel?: (name: string) => string;
1115
}>;
1216

13-
export const InputFieldGate = memo(({ nodeId, fieldName, children, placeholder }: Props) => {
17+
export const InputFieldGate = memo(({ nodeId, fieldName, children, fallback, formatLabel }: Props) => {
1418
const hasInstance = useInputFieldInstanceExists(nodeId, fieldName);
1519
const hasTemplate = useInputFieldTemplateExists(nodeId, fieldName);
1620

1721
if (!hasTemplate || !hasInstance) {
18-
return placeholder ?? <InputFieldUnknownPlaceholder nodeId={nodeId} fieldName={fieldName} />;
22+
// fallback may be null, indicating we should render nothing at all - must check for undefined explicitly
23+
if (fallback !== undefined) {
24+
return fallback;
25+
}
26+
return (
27+
<Fallback
28+
nodeId={nodeId}
29+
fieldName={fieldName}
30+
formatLabel={formatLabel}
31+
hasInstance={hasInstance}
32+
hasTemplate={hasTemplate}
33+
/>
34+
);
1935
}
2036

2137
return children;
2238
});
2339

2440
InputFieldGate.displayName = 'InputFieldGate';
41+
42+
const Fallback = memo(
43+
({
44+
nodeId,
45+
fieldName,
46+
formatLabel,
47+
hasTemplate,
48+
hasInstance,
49+
}: {
50+
nodeId: string;
51+
fieldName: string;
52+
formatLabel?: (name: string) => string;
53+
hasTemplate: boolean;
54+
hasInstance: boolean;
55+
}) => {
56+
const { t } = useTranslation();
57+
const name = useInputFieldNameSafe(nodeId, fieldName);
58+
const label = useMemo(() => {
59+
if (formatLabel) {
60+
return formatLabel(name);
61+
}
62+
if (hasTemplate && !hasInstance) {
63+
return t('nodes.missingField_withName', { name });
64+
}
65+
if (!hasTemplate && hasInstance) {
66+
return t('nodes.unexpectedField_withName', { name });
67+
}
68+
return t('nodes.unknownField_withName', { name });
69+
}, [formatLabel, hasInstance, hasTemplate, name, t]);
70+
71+
return (
72+
<InputFieldWrapper>
73+
<FormControl isInvalid={true} alignItems="stretch" justifyContent="center" gap={2} h="full" w="full">
74+
<FormLabel display="flex" mb={0} px={1} py={2} gap={2}>
75+
{label}
76+
</FormLabel>
77+
</FormControl>
78+
</InputFieldWrapper>
79+
);
80+
}
81+
);
82+
83+
Fallback.displayName = 'Fallback';

Diff for: invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldUnknownPlaceholder.tsx

-27
This file was deleted.

Diff for: invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldGate.tsx

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import { OutputFieldUnknownPlaceholder } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldUnknownPlaceholder';
1+
import { FormControl, FormLabel } from '@invoke-ai/ui-library';
2+
import { OutputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldWrapper';
3+
import { useOutputFieldName } from 'features/nodes/hooks/useOutputFieldName';
24
import { useOutputFieldTemplateExists } from 'features/nodes/hooks/useOutputFieldTemplateExists';
35
import type { PropsWithChildren } from 'react';
46
import { memo } from 'react';
7+
import { useTranslation } from 'react-i18next';
58

69
type Props = PropsWithChildren<{
710
nodeId: string;
@@ -12,10 +15,27 @@ export const OutputFieldGate = memo(({ nodeId, fieldName, children }: Props) =>
1215
const hasTemplate = useOutputFieldTemplateExists(nodeId, fieldName);
1316

1417
if (!hasTemplate) {
15-
return <OutputFieldUnknownPlaceholder nodeId={nodeId} fieldName={fieldName} />;
18+
return <Fallback nodeId={nodeId} fieldName={fieldName} />;
1619
}
1720

1821
return children;
1922
});
2023

2124
OutputFieldGate.displayName = 'OutputFieldGate';
25+
26+
const Fallback = memo(({ nodeId, fieldName }: Props) => {
27+
const { t } = useTranslation();
28+
const name = useOutputFieldName(nodeId, fieldName);
29+
30+
return (
31+
<OutputFieldWrapper>
32+
<FormControl isInvalid={true} alignItems="stretch" justifyContent="space-between" gap={2} h="full" w="full">
33+
<FormLabel display="flex" alignItems="center" h="full" color="error.300" mb={0} px={1} gap={2}>
34+
{t('nodes.unexpectedField_withName', { name })}
35+
</FormLabel>
36+
</FormControl>
37+
</OutputFieldWrapper>
38+
);
39+
});
40+
41+
Fallback.displayName = 'Fallback';

Diff for: invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldUnknownPlaceholder.tsx

-27
This file was deleted.

Diff for: invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/FormElementEditModeHeader.tsx

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { FlexProps, SystemStyleObject } from '@invoke-ai/ui-library';
22
import { Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
33
import { useAppDispatch } from 'app/store/storeHooks';
4+
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
45
import { ContainerElementSettings } from 'features/nodes/components/sidePanel/builder/ContainerElementSettings';
56
import { useDepthContext } from 'features/nodes/components/sidePanel/builder/contexts';
67
import { NodeFieldElementSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementSettings';
@@ -47,8 +48,16 @@ export const FormElementEditModeHeader = memo(({ element, dragHandleRef, ...rest
4748
<Label element={element} />
4849
<Spacer />
4950
{isContainerElement(element) && <ContainerElementSettings element={element} />}
50-
{isNodeFieldElement(element) && <ZoomToNodeButton element={element} />}
51-
{isNodeFieldElement(element) && <NodeFieldElementSettings element={element} />}
51+
{isNodeFieldElement(element) && (
52+
<InputFieldGate
53+
nodeId={element.data.fieldIdentifier.nodeId}
54+
fieldName={element.data.fieldIdentifier.fieldName}
55+
fallback={null} // Do not render these buttons if the field is not found
56+
>
57+
<ZoomToNodeButton element={element} />
58+
<NodeFieldElementSettings element={element} />
59+
</InputFieldGate>
60+
)}
5261
<RemoveElementButton element={element} />
5362
</Flex>
5463
);
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useAppSelector } from 'app/store/storeHooks';
2-
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
32
import { NodeFieldElementEditMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementEditMode';
43
import { NodeFieldElementViewMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementViewMode';
54
import { selectWorkflowMode, useElement } from 'features/nodes/store/workflowSlice';
@@ -15,19 +14,11 @@ export const NodeFieldElement = memo(({ id }: { id: string }) => {
1514
}
1615

1716
if (mode === 'view') {
18-
return (
19-
<InputFieldGate nodeId={el.data.fieldIdentifier.nodeId} fieldName={el.data.fieldIdentifier.fieldName}>
20-
<NodeFieldElementViewMode el={el} />
21-
</InputFieldGate>
22-
);
17+
return <NodeFieldElementViewMode el={el} />;
2318
}
2419

2520
// mode === 'edit'
26-
return (
27-
<InputFieldGate nodeId={el.data.fieldIdentifier.nodeId} fieldName={el.data.fieldIdentifier.fieldName}>
28-
<NodeFieldElementEditMode el={el} />
29-
</InputFieldGate>
30-
);
21+
return <NodeFieldElementEditMode el={el} />;
3122
});
3223

3324
NodeFieldElement.displayName = 'NodeFieldElement';

Diff for: invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementEditMode.tsx

+40-17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { SystemStyleObject } from '@invoke-ai/ui-library';
22
import { Box, Flex, FormControl } from '@invoke-ai/ui-library';
3+
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
34
import { InputFieldRenderer } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
45
import { useContainerContext } from 'features/nodes/components/sidePanel/builder/contexts';
56
import { useFormElementDnd } from 'features/nodes/components/sidePanel/builder/dnd-hooks';
@@ -11,6 +12,7 @@ import { NodeFieldElementLabelEditable } from 'features/nodes/components/sidePan
1112
import { useMouseOverFormField, useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
1213
import type { NodeFieldElement } from 'features/nodes/types/workflow';
1314
import { NODE_FIELD_CLASS_NAME } from 'features/nodes/types/workflow';
15+
import type { RefObject } from 'react';
1416
import { memo, useRef } from 'react';
1517

1618
const sx: SystemStyleObject = {
@@ -31,33 +33,54 @@ export const NodeFieldElementEditMode = memo(({ el }: { el: NodeFieldElement })
3133
const dragHandleRef = useRef<HTMLDivElement>(null);
3234
const [activeDropRegion, isDragging] = useFormElementDnd(el.id, draggableRef, dragHandleRef);
3335
const containerCtx = useContainerContext();
34-
const { id, data } = el;
35-
const { fieldIdentifier, showDescription } = data;
36+
const { id } = el;
3637

3738
return (
3839
<Flex ref={draggableRef} id={id} className={NODE_FIELD_CLASS_NAME} sx={sx} data-parent-layout={containerCtx.layout}>
39-
<FormElementEditModeHeader dragHandleRef={dragHandleRef} element={el} data-is-dragging={isDragging} />
40-
<FormElementEditModeContent data-is-dragging={isDragging} p={4}>
41-
<FormControl flex="1 1 0" orientation="vertical">
42-
<NodeFieldElementLabelEditable el={el} />
43-
<Flex w="full" gap={4}>
44-
<InputFieldRenderer
45-
nodeId={fieldIdentifier.nodeId}
46-
fieldName={fieldIdentifier.fieldName}
47-
settings={data.settings}
48-
/>
49-
</Flex>
50-
{showDescription && <NodeFieldElementDescriptionEditable el={el} />}
51-
</FormControl>
52-
</FormElementEditModeContent>
53-
<NodeFieldElementOverlay element={el} />
40+
<NodeFieldElementEditModeContent dragHandleRef={dragHandleRef} el={el} isDragging={isDragging} />
5441
<DndListDropIndicator activeDropRegion={activeDropRegion} gap="var(--invoke-space-4)" />
5542
</Flex>
5643
);
5744
});
5845

5946
NodeFieldElementEditMode.displayName = 'NodeFieldElementEditMode';
6047

48+
const NodeFieldElementEditModeContent = memo(
49+
({
50+
el,
51+
dragHandleRef,
52+
isDragging,
53+
}: {
54+
el: NodeFieldElement;
55+
dragHandleRef: RefObject<HTMLDivElement>;
56+
isDragging: boolean;
57+
}) => {
58+
const { data } = el;
59+
const { fieldIdentifier, showDescription } = data;
60+
return (
61+
<>
62+
<FormElementEditModeHeader dragHandleRef={dragHandleRef} element={el} data-is-dragging={isDragging} />
63+
<FormElementEditModeContent data-is-dragging={isDragging} p={4}>
64+
<InputFieldGate nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName}>
65+
<FormControl flex="1 1 0" orientation="vertical">
66+
<NodeFieldElementLabelEditable el={el} />
67+
<Flex w="full" gap={4}>
68+
<InputFieldRenderer
69+
nodeId={fieldIdentifier.nodeId}
70+
fieldName={fieldIdentifier.fieldName}
71+
settings={data.settings}
72+
/>
73+
</Flex>
74+
{showDescription && <NodeFieldElementDescriptionEditable el={el} />}
75+
</FormControl>
76+
</InputFieldGate>
77+
</FormElementEditModeContent>
78+
</>
79+
);
80+
}
81+
);
82+
NodeFieldElementEditModeContent.displayName = 'NodeFieldElementEditModeContent';
83+
6184
const nodeFieldOverlaySx: SystemStyleObject = {
6285
position: 'absolute',
6386
top: 0,

Diff for: invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementViewMode.tsx

+17-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { SystemStyleObject } from '@invoke-ai/ui-library';
22
import { Flex, FormControl, FormHelperText } from '@invoke-ai/ui-library';
33
import { linkifyOptions, linkifySx } from 'common/components/linkify';
4+
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
45
import { InputFieldRenderer } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
56
import { useContainerContext } from 'features/nodes/components/sidePanel/builder/contexts';
67
import { NodeFieldElementLabel } from 'features/nodes/components/sidePanel/builder/NodeFieldElementLabel';
@@ -9,7 +10,8 @@ import { useInputFieldTemplateOrThrow, useInputFieldTemplateSafe } from 'feature
910
import type { NodeFieldElement } from 'features/nodes/types/workflow';
1011
import { NODE_FIELD_CLASS_NAME } from 'features/nodes/types/workflow';
1112
import Linkify from 'linkify-react';
12-
import { memo, useMemo } from 'react';
13+
import { memo, useCallback, useMemo } from 'react';
14+
import { useTranslation } from 'react-i18next';
1315

1416
const sx: SystemStyleObject = {
1517
'&[data-parent-layout="column"]': {
@@ -25,12 +27,19 @@ const sx: SystemStyleObject = {
2527
},
2628
};
2729

30+
const useFormatFallbackLabel = () => {
31+
const { t } = useTranslation();
32+
const formatLabel = useCallback((name: string) => t('nodes.unknownFieldEditWorkflowToFix_withName', { name }), [t]);
33+
return formatLabel;
34+
};
35+
2836
export const NodeFieldElementViewMode = memo(({ el }: { el: NodeFieldElement }) => {
2937
const { id, data } = el;
3038
const { fieldIdentifier, showDescription } = data;
3139
const description = useInputFieldDescriptionSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
3240
const fieldTemplate = useInputFieldTemplateSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
3341
const containerCtx = useContainerContext();
42+
const formatFallbackLabel = useFormatFallbackLabel();
3443

3544
const _description = useMemo(
3645
() => description || fieldTemplate?.description || '',
@@ -45,7 +54,13 @@ export const NodeFieldElementViewMode = memo(({ el }: { el: NodeFieldElement })
4554
data-parent-layout={containerCtx.layout}
4655
data-with-description={showDescription && !!_description}
4756
>
48-
<NodeFieldElementViewModeContent el={el} />
57+
<InputFieldGate
58+
nodeId={el.data.fieldIdentifier.nodeId}
59+
fieldName={el.data.fieldIdentifier.fieldName}
60+
formatLabel={formatFallbackLabel}
61+
>
62+
<NodeFieldElementViewModeContent el={el} />
63+
</InputFieldGate>
4964
</Flex>
5065
);
5166
});

0 commit comments

Comments
 (0)