diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index e412a54defd..d932e822941 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": "Workflow contains an unknown field \"{{name}}\".\nEdit the workflow to fix the issue.", "updateNode": "Update Node", "updateApp": "Update App", "loadingTemplates": "Loading {{name}}", diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldDescriptionPopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldDescriptionPopover.tsx index c3364566131..cf832ef6109 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldDescriptionPopover.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldDescriptionPopover.tsx @@ -8,7 +8,7 @@ import { Textarea, } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; -import { useInputFieldDescription } from 'features/nodes/hooks/useInputFieldDescription'; +import { useInputFieldDescriptionSafe } from 'features/nodes/hooks/useInputFieldDescriptionSafe'; import { fieldDescriptionChanged } from 'features/nodes/store/nodesSlice'; import { NO_DRAG_CLASS, NO_PAN_CLASS, NO_WHEEL_CLASS } from 'features/nodes/types/constants'; import type { ChangeEvent } from 'react'; @@ -48,7 +48,7 @@ InputFieldDescriptionPopover.displayName = 'InputFieldDescriptionPopover'; const Content = memo(({ nodeId, fieldName }: Props) => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const description = useInputFieldDescription(nodeId, fieldName); + const description = useInputFieldDescriptionSafe(nodeId, fieldName); const onChange = useCallback( (e: ChangeEvent) => { dispatch(fieldDescriptionChanged({ nodeId, fieldName, val: e.target.value })); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeNodes.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeNodes.tsx index ccc2a2fb854..34b1373853b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeNodes.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldEditModeNodes.tsx @@ -7,7 +7,7 @@ import { InputFieldResetToDefaultValueIconButton } from 'features/nodes/componen import { useNodeFieldDnd } from 'features/nodes/components/sidePanel/builder/dnd-hooks'; import { useInputFieldIsConnected } from 'features/nodes/hooks/useInputFieldIsConnected'; import { useInputFieldIsInvalid } from 'features/nodes/hooks/useInputFieldIsInvalid'; -import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate'; +import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate'; import { NO_DRAG_CLASS } from 'features/nodes/types/constants'; import type { FieldInputTemplate } from 'features/nodes/types/field'; import { memo, useRef } from 'react'; @@ -22,7 +22,7 @@ interface Props { } export const InputFieldEditModeNodes = memo(({ nodeId, fieldName }: Props) => { - const fieldTemplate = useInputFieldTemplate(nodeId, fieldName); + const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName); const isInvalid = useInputFieldIsInvalid(nodeId, fieldName); const isConnected = useInputFieldIsConnected(nodeId, fieldName); 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 5f43fc29775..1adee688cbc 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,23 +1,83 @@ -import { InputFieldUnknownPlaceholder } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldUnknownPlaceholder'; +import { Flex, Text } 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 } from 'react'; -import { memo } from 'react'; +import type { PropsWithChildren, ReactNode } from 'react'; +import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; type Props = PropsWithChildren<{ nodeId: string; fieldName: string; + fallback?: ReactNode; + formatLabel?: (name: string) => string; }>; -export const InputFieldGate = memo(({ nodeId, fieldName, children }: 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 ; + // 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/InputFieldHandle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldHandle.tsx index 6fdfe7a144b..d25664070eb 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldHandle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldHandle.tsx @@ -7,7 +7,7 @@ import { useIsConnectionInProgress, useIsConnectionStartField, } from 'features/nodes/hooks/useFieldConnectionState'; -import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate'; +import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate'; import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType'; import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; import type { FieldInputTemplate } from 'features/nodes/types/field'; @@ -62,7 +62,7 @@ const handleStyles = { } satisfies CSSProperties; export const InputFieldHandle = memo(({ nodeId, fieldName }: Props) => { - const fieldTemplate = useInputFieldTemplate(nodeId, fieldName); + const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName); const fieldTypeName = useFieldTypeName(fieldTemplate.type); const fieldColor = useMemo(() => getFieldColor(fieldTemplate.type), [fieldTemplate.type]); const isModelField = useMemo(() => isModelFieldType(fieldTemplate.type), [fieldTemplate.type]); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx index 1255f3465bc..8598be06047 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx @@ -16,7 +16,7 @@ import { IntegerFieldSlider } from 'features/nodes/components/flow/nodes/Invocat import { StringFieldInput } from 'features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldInput'; import { StringFieldTextarea } from 'features/nodes/components/flow/nodes/Invocation/fields/StringField/StringFieldTextarea'; import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance'; -import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate'; +import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate'; import { isBoardFieldInputInstance, isBoardFieldInputTemplate, @@ -135,7 +135,7 @@ type Props = { export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props) => { const field = useInputFieldInstance(nodeId, fieldName); - const template = useInputFieldTemplate(nodeId, fieldName); + const template = useInputFieldTemplateOrThrow(nodeId, fieldName); // When deciding which component to render, first we check the type of the template, which is more efficient than the // instance type check. The instance type check uses zod and is slower. diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTitle.tsx index 4c73b3aac7d..f1da09d43b5 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTitle.tsx @@ -9,7 +9,7 @@ import { useIsConnectionStartField, } from 'features/nodes/hooks/useFieldConnectionState'; import { useInputFieldIsConnected } from 'features/nodes/hooks/useInputFieldIsConnected'; -import { useInputFieldLabel } from 'features/nodes/hooks/useInputFieldLabel'; +import { useInputFieldLabelSafe } from 'features/nodes/hooks/useInputFieldLabelSafe'; import { useInputFieldTemplateTitle } from 'features/nodes/hooks/useInputFieldTemplateTitle'; import { fieldLabelChanged } from 'features/nodes/store/nodesSlice'; import { HANDLE_TOOLTIP_OPEN_DELAY, NO_FIT_ON_DOUBLE_CLICK_CLASS } from 'features/nodes/types/constants'; @@ -43,7 +43,7 @@ interface Props { export const InputFieldTitle = memo((props: Props) => { const { nodeId, fieldName, isInvalid, isDragging } = props; const inputRef = useRef(null); - const label = useInputFieldLabel(nodeId, fieldName); + const label = useInputFieldLabelSafe(nodeId, fieldName); const fieldTemplateTitle = useInputFieldTemplateTitle(nodeId, fieldName); const { t } = useTranslation(); const isConnected = useInputFieldIsConnected(nodeId, fieldName); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTooltipContent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTooltipContent.tsx index f83454ffd9c..e456f771aa5 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTooltipContent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldTooltipContent.tsx @@ -1,7 +1,7 @@ import { Flex, ListItem, Text, UnorderedList } from '@invoke-ai/ui-library'; import { useInputFieldErrors } from 'features/nodes/hooks/useInputFieldErrors'; import { useInputFieldInstance } from 'features/nodes/hooks/useInputFieldInstance'; -import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate'; +import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate'; import { useFieldTypeName } from 'features/nodes/hooks/usePrettyFieldType'; import { startCase } from 'lodash-es'; import { memo, useMemo } from 'react'; @@ -16,7 +16,7 @@ export const InputFieldTooltipContent = memo(({ nodeId, fieldName }: Props) => { const { t } = useTranslation(); const fieldInstance = useInputFieldInstance(nodeId, fieldName); - const fieldTemplate = useInputFieldTemplate(nodeId, fieldName); + const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName); const fieldTypeName = useFieldTypeName(fieldTemplate.type); const fieldErrors = useInputFieldErrors(nodeId, fieldName); 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 615034ea323..00000000000 --- 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 2b50e357063..85b01fdae92 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 9de792b9e56..00000000000 --- 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 890c904dd2b..5c24b26bc92 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