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