Skip to content
Merged
5 changes: 4 additions & 1 deletion invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<HTMLTextAreaElement>) => {
dispatch(fieldDescriptionChanged({ nodeId, fieldName, val: e.target.value }));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
@@ -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 <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>
<Flex w="full" px={1} py={1} justifyContent="center">
<Text fontWeight="semibold" color="error.300" whiteSpace="pre" textAlign="center">
{label}
</Text>
</Flex>
</InputFieldWrapper>
);
}
);

Fallback.displayName = 'Fallback';
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -43,7 +43,7 @@ interface Props {
export const InputFieldTitle = memo((props: Props) => {
const { nodeId, fieldName, isInvalid, isDragging } = props;
const inputRef = useRef<HTMLInputElement>(null);
const label = useInputFieldLabel(nodeId, fieldName);
const label = useInputFieldLabelSafe(nodeId, fieldName);
const fieldTemplateTitle = useInputFieldTemplateTitle(nodeId, fieldName);
const { t } = useTranslation();
const isConnected = useInputFieldIsConnected(nodeId, fieldName);
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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';

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { FormHelperText, Textarea } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { linkifyOptions, linkifySx } from 'common/components/linkify';
import { useEditable } from 'common/hooks/useEditable';
import { useInputFieldDescription } from 'features/nodes/hooks/useInputFieldDescription';
import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate';
import { useInputFieldDescriptionSafe } from 'features/nodes/hooks/useInputFieldDescriptionSafe';
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplate';
import { fieldDescriptionChanged } from 'features/nodes/store/nodesSlice';
import type { NodeFieldElement } from 'features/nodes/types/workflow';
import Linkify from 'linkify-react';
Expand All @@ -13,8 +13,8 @@ export const NodeFieldElementDescriptionEditable = memo(({ el }: { el: NodeField
const { data } = el;
const { fieldIdentifier } = data;
const dispatch = useAppDispatch();
const description = useInputFieldDescription(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const fieldTemplate = useInputFieldTemplate(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const description = useInputFieldDescriptionSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
const inputRef = useRef<HTMLTextAreaElement>(null);

const onChange = useCallback(
Expand Down
Loading