mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-14 20:58:04 -05:00
feat(ui): better labels for missing/unexpected fields
This commit is contained in:
@@ -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}}",
|
||||
|
||||
@@ -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 ?? <InputFieldUnknownPlaceholder nodeId={nodeId} fieldName={fieldName} />;
|
||||
// fallback may be null, indicating we should render nothing at all - must check for undefined explicitly
|
||||
if (fallback !== undefined) {
|
||||
return fallback;
|
||||
}
|
||||
return (
|
||||
<Fallback
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
formatLabel={formatLabel}
|
||||
hasInstance={hasInstance}
|
||||
hasTemplate={hasTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<InputFieldWrapper>
|
||||
<FormControl isInvalid={true} alignItems="stretch" justifyContent="center" gap={2} h="full" w="full">
|
||||
<FormLabel display="flex" mb={0} px={1} py={2} gap={2}>
|
||||
{label}
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
</InputFieldWrapper>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Fallback.displayName = 'Fallback';
|
||||
|
||||
@@ -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 (
|
||||
<InputFieldWrapper>
|
||||
<FormControl isInvalid={true} alignItems="stretch" justifyContent="center" gap={2} h="full" w="full">
|
||||
<FormLabel display="flex" mb={0} px={1} py={2} gap={2}>
|
||||
{t('nodes.unknownInput', { name })}
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
</InputFieldWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
InputFieldUnknownPlaceholder.displayName = 'InputFieldUnknownPlaceholder';
|
||||
@@ -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 <OutputFieldUnknownPlaceholder nodeId={nodeId} fieldName={fieldName} />;
|
||||
return <Fallback nodeId={nodeId} fieldName={fieldName} />;
|
||||
}
|
||||
|
||||
return children;
|
||||
});
|
||||
|
||||
OutputFieldGate.displayName = 'OutputFieldGate';
|
||||
|
||||
const Fallback = memo(({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const name = useOutputFieldName(nodeId, fieldName);
|
||||
|
||||
return (
|
||||
<OutputFieldWrapper>
|
||||
<FormControl isInvalid={true} alignItems="stretch" justifyContent="space-between" gap={2} h="full" w="full">
|
||||
<FormLabel display="flex" alignItems="center" h="full" color="error.300" mb={0} px={1} gap={2}>
|
||||
{t('nodes.unexpectedField_withName', { name })}
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
</OutputFieldWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
Fallback.displayName = 'Fallback';
|
||||
|
||||
@@ -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 (
|
||||
<OutputFieldWrapper>
|
||||
<FormControl isInvalid={true} alignItems="stretch" justifyContent="space-between" gap={2} h="full" w="full">
|
||||
<FormLabel display="flex" alignItems="center" h="full" color="error.300" mb={0} px={1} gap={2}>
|
||||
{t('nodes.unknownOutput', { name })}
|
||||
</FormLabel>
|
||||
</FormControl>
|
||||
</OutputFieldWrapper>
|
||||
);
|
||||
});
|
||||
|
||||
OutputFieldUnknownPlaceholder.displayName = 'OutputFieldUnknownPlaceholder';
|
||||
@@ -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
|
||||
<Label element={element} />
|
||||
<Spacer />
|
||||
{isContainerElement(element) && <ContainerElementSettings element={element} />}
|
||||
{isNodeFieldElement(element) && <ZoomToNodeButton element={element} />}
|
||||
{isNodeFieldElement(element) && <NodeFieldElementSettings element={element} />}
|
||||
{isNodeFieldElement(element) && (
|
||||
<InputFieldGate
|
||||
nodeId={element.data.fieldIdentifier.nodeId}
|
||||
fieldName={element.data.fieldIdentifier.fieldName}
|
||||
fallback={null} // Do not render these buttons if the field is not found
|
||||
>
|
||||
<ZoomToNodeButton element={element} />
|
||||
<NodeFieldElementSettings element={element} />
|
||||
</InputFieldGate>
|
||||
)}
|
||||
<RemoveElementButton element={element} />
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<InputFieldGate nodeId={el.data.fieldIdentifier.nodeId} fieldName={el.data.fieldIdentifier.fieldName}>
|
||||
<NodeFieldElementViewMode el={el} />
|
||||
</InputFieldGate>
|
||||
);
|
||||
return <NodeFieldElementViewMode el={el} />;
|
||||
}
|
||||
|
||||
// mode === 'edit'
|
||||
return (
|
||||
<InputFieldGate nodeId={el.data.fieldIdentifier.nodeId} fieldName={el.data.fieldIdentifier.fieldName}>
|
||||
<NodeFieldElementEditMode el={el} />
|
||||
</InputFieldGate>
|
||||
);
|
||||
return <NodeFieldElementEditMode el={el} />;
|
||||
});
|
||||
|
||||
NodeFieldElement.displayName = 'NodeFieldElement';
|
||||
|
||||
@@ -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<HTMLDivElement>(null);
|
||||
const [activeDropRegion, isDragging] = useFormElementDnd(el.id, draggableRef, dragHandleRef);
|
||||
const containerCtx = useContainerContext();
|
||||
const { id, data } = el;
|
||||
const { fieldIdentifier, showDescription } = data;
|
||||
const { id } = el;
|
||||
|
||||
return (
|
||||
<Flex ref={draggableRef} id={id} className={NODE_FIELD_CLASS_NAME} sx={sx} data-parent-layout={containerCtx.layout}>
|
||||
<FormElementEditModeHeader dragHandleRef={dragHandleRef} element={el} data-is-dragging={isDragging} />
|
||||
<FormElementEditModeContent data-is-dragging={isDragging} p={4}>
|
||||
<FormControl flex="1 1 0" orientation="vertical">
|
||||
<NodeFieldElementLabelEditable el={el} />
|
||||
<Flex w="full" gap={4}>
|
||||
<InputFieldRenderer
|
||||
nodeId={fieldIdentifier.nodeId}
|
||||
fieldName={fieldIdentifier.fieldName}
|
||||
settings={data.settings}
|
||||
/>
|
||||
</Flex>
|
||||
{showDescription && <NodeFieldElementDescriptionEditable el={el} />}
|
||||
</FormControl>
|
||||
</FormElementEditModeContent>
|
||||
<NodeFieldElementOverlay element={el} />
|
||||
<NodeFieldElementEditModeContent dragHandleRef={dragHandleRef} el={el} isDragging={isDragging} />
|
||||
<DndListDropIndicator activeDropRegion={activeDropRegion} gap="var(--invoke-space-4)" />
|
||||
</Flex>
|
||||
);
|
||||
@@ -58,6 +45,42 @@ export const NodeFieldElementEditMode = memo(({ el }: { el: NodeFieldElement })
|
||||
|
||||
NodeFieldElementEditMode.displayName = 'NodeFieldElementEditMode';
|
||||
|
||||
const NodeFieldElementEditModeContent = memo(
|
||||
({
|
||||
el,
|
||||
dragHandleRef,
|
||||
isDragging,
|
||||
}: {
|
||||
el: NodeFieldElement;
|
||||
dragHandleRef: RefObject<HTMLDivElement>;
|
||||
isDragging: boolean;
|
||||
}) => {
|
||||
const { data } = el;
|
||||
const { fieldIdentifier, showDescription } = data;
|
||||
return (
|
||||
<>
|
||||
<FormElementEditModeHeader dragHandleRef={dragHandleRef} element={el} data-is-dragging={isDragging} />
|
||||
<FormElementEditModeContent data-is-dragging={isDragging} p={4}>
|
||||
<InputFieldGate nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName}>
|
||||
<FormControl flex="1 1 0" orientation="vertical">
|
||||
<NodeFieldElementLabelEditable el={el} />
|
||||
<Flex w="full" gap={4}>
|
||||
<InputFieldRenderer
|
||||
nodeId={fieldIdentifier.nodeId}
|
||||
fieldName={fieldIdentifier.fieldName}
|
||||
settings={data.settings}
|
||||
/>
|
||||
</Flex>
|
||||
{showDescription && <NodeFieldElementDescriptionEditable el={el} />}
|
||||
</FormControl>
|
||||
</InputFieldGate>
|
||||
</FormElementEditModeContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
NodeFieldElementEditModeContent.displayName = 'NodeFieldElementEditModeContent';
|
||||
|
||||
const nodeFieldOverlaySx: SystemStyleObject = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<NodeFieldElementViewModeContent el={el} />
|
||||
<InputFieldGate
|
||||
nodeId={el.data.fieldIdentifier.nodeId}
|
||||
fieldName={el.data.fieldIdentifier.fieldName}
|
||||
formatLabel={formatFallbackLabel}
|
||||
>
|
||||
<NodeFieldElementViewModeContent el={el} />
|
||||
</InputFieldGate>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
Reference in New Issue
Block a user