From efaa20a7a112252bb660f2445ce8e22dc3d60ba9 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Fri, 21 Mar 2025 10:22:56 +1000
Subject: [PATCH] feat(ui): better labels for missing/unexpected fields
---
invokeai/frontend/web/public/locales/en.json | 5 +-
.../Invocation/fields/InputFieldGate.tsx | 69 +++++++++++++++++--
.../fields/InputFieldUnknownPlaceholder.tsx | 27 --------
.../Invocation/fields/OutputFieldGate.tsx | 24 ++++++-
.../fields/OutputFieldUnknownPlaceholder.tsx | 27 --------
.../builder/FormElementEditModeHeader.tsx | 13 +++-
.../sidePanel/builder/NodeFieldElement.tsx | 13 +---
.../builder/NodeFieldElementEditMode.tsx | 57 ++++++++++-----
.../builder/NodeFieldElementViewMode.tsx | 19 ++++-
...tFieldName.ts => useInputFieldNameSafe.ts} | 2 +-
10 files changed, 161 insertions(+), 95 deletions(-)
delete mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldUnknownPlaceholder.tsx
delete mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldUnknownPlaceholder.tsx
rename invokeai/frontend/web/src/features/nodes/hooks/{useInputFieldName.ts => useInputFieldNameSafe.ts} (92%)
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index e412a54def..2c79ba872e 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -1014,7 +1014,10 @@
"unknownNodeType": "Unknown node type",
"unknownTemplate": "Unknown Template",
"unknownInput": "Unknown input: {{name}}",
- "unknownOutput": "Unknown output: {{name}}",
+ "missingField_withName": "Missing field \"{{name}}\"",
+ "unexpectedField_withName": "Unexpected field \"{{name}}\"",
+ "unknownField_withName": "Unknown field \"{{name}}\"",
+ "unknownFieldEditWorkflowToFix_withName": "Unknown field \"{{name}}\" (edit workflow to fix)",
"updateNode": "Update Node",
"updateApp": "Update App",
"loadingTemplates": "Loading {{name}}",
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 60080208e5..191be5edcd 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
@@ -1,24 +1,83 @@
-import { InputFieldUnknownPlaceholder } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldUnknownPlaceholder';
+import { FormControl, FormLabel } from '@invoke-ai/ui-library';
+import { InputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper';
import { useInputFieldInstanceExists } from 'features/nodes/hooks/useInputFieldInstanceExists';
+import { useInputFieldNameSafe } from 'features/nodes/hooks/useInputFieldNameSafe';
import { useInputFieldTemplateExists } from 'features/nodes/hooks/useInputFieldTemplateExists';
import type { PropsWithChildren, ReactNode } from 'react';
-import { memo } from 'react';
+import { memo, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
type Props = PropsWithChildren<{
nodeId: string;
fieldName: string;
- placeholder?: ReactNode;
+ fallback?: ReactNode;
+ formatLabel?: (name: string) => string;
}>;
-export const InputFieldGate = memo(({ nodeId, fieldName, children, placeholder }: Props) => {
+export const InputFieldGate = memo(({ nodeId, fieldName, children, fallback, formatLabel }: Props) => {
const hasInstance = useInputFieldInstanceExists(nodeId, fieldName);
const hasTemplate = useInputFieldTemplateExists(nodeId, fieldName);
if (!hasTemplate || !hasInstance) {
- return placeholder ?? ;
+ // fallback may be null, indicating we should render nothing at all - must check for undefined explicitly
+ if (fallback !== undefined) {
+ return fallback;
+ }
+ return (
+
+ );
}
return children;
});
InputFieldGate.displayName = 'InputFieldGate';
+
+const Fallback = memo(
+ ({
+ nodeId,
+ fieldName,
+ formatLabel,
+ hasTemplate,
+ hasInstance,
+ }: {
+ nodeId: string;
+ fieldName: string;
+ formatLabel?: (name: string) => string;
+ hasTemplate: boolean;
+ hasInstance: boolean;
+ }) => {
+ const { t } = useTranslation();
+ const name = useInputFieldNameSafe(nodeId, fieldName);
+ const label = useMemo(() => {
+ if (formatLabel) {
+ return formatLabel(name);
+ }
+ if (hasTemplate && !hasInstance) {
+ return t('nodes.missingField_withName', { name });
+ }
+ if (!hasTemplate && hasInstance) {
+ return t('nodes.unexpectedField_withName', { name });
+ }
+ return t('nodes.unknownField_withName', { name });
+ }, [formatLabel, hasInstance, hasTemplate, name, t]);
+
+ return (
+
+
+
+ {label}
+
+
+
+ );
+ }
+);
+
+Fallback.displayName = 'Fallback';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldUnknownPlaceholder.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldUnknownPlaceholder.tsx
deleted file mode 100644
index 615034ea32..0000000000
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldUnknownPlaceholder.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { FormControl, FormLabel } from '@invoke-ai/ui-library';
-import { InputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper';
-import { useInputFieldName } from 'features/nodes/hooks/useInputFieldName';
-import { memo } from 'react';
-import { useTranslation } from 'react-i18next';
-
-type Props = {
- nodeId: string;
- fieldName: string;
-};
-
-export const InputFieldUnknownPlaceholder = memo(({ nodeId, fieldName }: Props) => {
- const { t } = useTranslation();
- const name = useInputFieldName(nodeId, fieldName);
-
- return (
-
-
-
- {t('nodes.unknownInput', { name })}
-
-
-
- );
-});
-
-InputFieldUnknownPlaceholder.displayName = 'InputFieldUnknownPlaceholder';
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 2b50e35706..85b01fdae9 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
@@ -1,7 +1,10 @@
-import { OutputFieldUnknownPlaceholder } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldUnknownPlaceholder';
+import { FormControl, FormLabel } from '@invoke-ai/ui-library';
+import { OutputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldWrapper';
+import { useOutputFieldName } from 'features/nodes/hooks/useOutputFieldName';
import { useOutputFieldTemplateExists } from 'features/nodes/hooks/useOutputFieldTemplateExists';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
type Props = PropsWithChildren<{
nodeId: string;
@@ -12,10 +15,27 @@ export const OutputFieldGate = memo(({ nodeId, fieldName, children }: Props) =>
const hasTemplate = useOutputFieldTemplateExists(nodeId, fieldName);
if (!hasTemplate) {
- return ;
+ return ;
}
return children;
});
OutputFieldGate.displayName = 'OutputFieldGate';
+
+const Fallback = memo(({ nodeId, fieldName }: Props) => {
+ const { t } = useTranslation();
+ const name = useOutputFieldName(nodeId, fieldName);
+
+ return (
+
+
+
+ {t('nodes.unexpectedField_withName', { name })}
+
+
+
+ );
+});
+
+Fallback.displayName = 'Fallback';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldUnknownPlaceholder.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldUnknownPlaceholder.tsx
deleted file mode 100644
index 9de792b9e5..0000000000
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputFieldUnknownPlaceholder.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { FormControl, FormLabel } from '@invoke-ai/ui-library';
-import { OutputFieldWrapper } from 'features/nodes/components/flow/nodes/Invocation/fields/OutputFieldWrapper';
-import { useOutputFieldName } from 'features/nodes/hooks/useOutputFieldName';
-import { memo } from 'react';
-import { useTranslation } from 'react-i18next';
-
-type Props = {
- nodeId: string;
- fieldName: string;
-};
-
-export const OutputFieldUnknownPlaceholder = memo(({ nodeId, fieldName }: Props) => {
- const { t } = useTranslation();
- const name = useOutputFieldName(nodeId, fieldName);
-
- return (
-
-
-
- {t('nodes.unknownOutput', { name })}
-
-
-
- );
-});
-
-OutputFieldUnknownPlaceholder.displayName = 'OutputFieldUnknownPlaceholder';
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 890c904dd2..5c24b26bc9 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
@@ -1,6 +1,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 { 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';
import { NodeFieldElementSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementSettings';
@@ -47,8 +48,16 @@ export const FormElementEditModeHeader = memo(({ element, dragHandleRef, ...rest
{isContainerElement(element) && }
- {isNodeFieldElement(element) && }
- {isNodeFieldElement(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 19451e54bb..06c095a799 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,5 +1,4 @@
import { useAppSelector } from 'app/store/storeHooks';
-import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
import { NodeFieldElementEditMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementEditMode';
import { NodeFieldElementViewMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementViewMode';
import { selectWorkflowMode, useElement } from 'features/nodes/store/workflowSlice';
@@ -15,19 +14,11 @@ 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/NodeFieldElementEditMode.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementEditMode.tsx
index 9e3add1763..9a93b77a8f 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, Flex, FormControl } from '@invoke-ai/ui-library';
+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';
import { useFormElementDnd } from 'features/nodes/components/sidePanel/builder/dnd-hooks';
@@ -11,6 +12,7 @@ import { NodeFieldElementLabelEditable } from 'features/nodes/components/sidePan
import { useMouseOverFormField, useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
import type { NodeFieldElement } from 'features/nodes/types/workflow';
import { NODE_FIELD_CLASS_NAME } from 'features/nodes/types/workflow';
+import type { RefObject } from 'react';
import { memo, useRef } from 'react';
const sx: SystemStyleObject = {
@@ -31,26 +33,11 @@ export const NodeFieldElementEditMode = memo(({ el }: { el: NodeFieldElement })
const dragHandleRef = useRef(null);
const [activeDropRegion, isDragging] = useFormElementDnd(el.id, draggableRef, dragHandleRef);
const containerCtx = useContainerContext();
- const { id, data } = el;
- const { fieldIdentifier, showDescription } = data;
+ const { id } = el;
return (
-
-
-
-
-
-
-
- {showDescription && }
-
-
-
+
);
@@ -58,6 +45,42 @@ export const NodeFieldElementEditMode = memo(({ el }: { el: NodeFieldElement })
NodeFieldElementEditMode.displayName = 'NodeFieldElementEditMode';
+const NodeFieldElementEditModeContent = memo(
+ ({
+ el,
+ dragHandleRef,
+ isDragging,
+ }: {
+ el: NodeFieldElement;
+ dragHandleRef: RefObject;
+ isDragging: boolean;
+ }) => {
+ const { data } = el;
+ const { fieldIdentifier, showDescription } = data;
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {showDescription && }
+
+
+
+ >
+ );
+ }
+);
+NodeFieldElementEditModeContent.displayName = 'NodeFieldElementEditModeContent';
+
const nodeFieldOverlaySx: SystemStyleObject = {
position: 'absolute',
top: 0,
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 3eae306578..5f6dc87dbc 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
@@ -1,6 +1,7 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex, FormControl, FormHelperText } from '@invoke-ai/ui-library';
import { linkifyOptions, linkifySx } from 'common/components/linkify';
+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';
import { NodeFieldElementLabel } from 'features/nodes/components/sidePanel/builder/NodeFieldElementLabel';
@@ -9,7 +10,8 @@ import { useInputFieldTemplateOrThrow, useInputFieldTemplateSafe } from 'feature
import type { NodeFieldElement } from 'features/nodes/types/workflow';
import { NODE_FIELD_CLASS_NAME } from 'features/nodes/types/workflow';
import Linkify from 'linkify-react';
-import { memo, useMemo } from 'react';
+import { memo, useCallback, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
const sx: SystemStyleObject = {
'&[data-parent-layout="column"]': {
@@ -25,12 +27,19 @@ const sx: SystemStyleObject = {
},
};
+const useFormatFallbackLabel = () => {
+ const { t } = useTranslation();
+ const formatLabel = useCallback((name: string) => t('nodes.unknownFieldEditWorkflowToFix_withName', { name }), [t]);
+ return formatLabel;
+};
+
export const NodeFieldElementViewMode = memo(({ el }: { el: NodeFieldElement }) => {
const { id, data } = el;
const { fieldIdentifier, showDescription } = data;
const description = useInputFieldDescriptionSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const fieldTemplate = useInputFieldTemplateSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const containerCtx = useContainerContext();
+ const formatFallbackLabel = useFormatFallbackLabel();
const _description = useMemo(
() => description || fieldTemplate?.description || '',
@@ -45,7 +54,13 @@ export const NodeFieldElementViewMode = memo(({ el }: { el: NodeFieldElement })
data-parent-layout={containerCtx.layout}
data-with-description={showDescription && !!_description}
>
-
+
+
+
);
});
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldName.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldNameSafe.ts
similarity index 92%
rename from invokeai/frontend/web/src/features/nodes/hooks/useInputFieldName.ts
rename to invokeai/frontend/web/src/features/nodes/hooks/useInputFieldNameSafe.ts
index 35f553e27c..4b2ac84968 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldName.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldNameSafe.ts
@@ -5,7 +5,7 @@ import { $templates } from 'features/nodes/store/nodesSlice';
import { selectInvocationNodeSafe, selectNodesSlice } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
-export const useInputFieldName = (nodeId: string, fieldName: string) => {
+export const useInputFieldNameSafe = (nodeId: string, fieldName: string) => {
const templates = useStore($templates);
const selector = useMemo(