From ed79980dd4fb85319c8d9f7480cb91383bf9f400 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Sat, 25 Nov 2023 21:10:22 +1100
Subject: [PATCH] feat(ui): improved UI for missing node field templates
When a node is updated with new fields and workflow needs to be updated, the fields now display "Unknown input/output: FieldName".
---
invokeai/frontend/web/public/locales/en.json | 8 ++--
.../nodes/Invocation/fields/InputField.tsx | 38 +++++++++++++++----
.../nodes/Invocation/fields/OutputField.tsx | 36 +++++++++++++++---
.../flow/panels/TopLeftPanel/TopLeftPanel.tsx | 15 +++++---
.../nodes/hooks/useFieldInputInstance.ts | 28 ++++++++++++++
.../nodes/hooks/useFieldInputTemplate.ts | 29 ++++++++++++++
.../nodes/hooks/useFieldOutputInstance.ts | 28 ++++++++++++++
.../nodes/hooks/useFieldOutputTemplate.ts | 29 ++++++++++++++
8 files changed, 188 insertions(+), 23 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useFieldInputInstance.ts
create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useFieldInputTemplate.ts
create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputInstance.ts
create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputTemplate.ts
diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json
index faa870bd32..6019854862 100644
--- a/invokeai/frontend/web/public/locales/en.json
+++ b/invokeai/frontend/web/public/locales/en.json
@@ -977,16 +977,16 @@
"unhandledInputProperty": "Unhandled input property",
"unhandledOutputProperty": "Unhandled output property",
"unknownField": "Unknown field",
- "unknownFieldType": "$(nodes.unknownField) type",
+ "unknownFieldType": "$t(nodes.unknownField) type",
"unknownNode": "Unknown Node",
"unknownNodeType":"$t(nodes.unknownNode) type",
"unknownTemplate": "Unknown Template",
- "unknownInput": "Unknown input",
+ "unknownInput": "Unknown input: {{name}}",
"unkownInvocation": "Unknown Invocation type",
- "unknownOutput": "Unknown output",
+ "unknownOutput": "Unknown output: {{name}}",
"updateNode": "Update Node",
"updateApp": "Update App",
- "updateAllNodes": "Update All Nodes",
+ "updateAllNodes": "Update Nodes",
"allNodesUpdated": "All Nodes Updated",
"unableToUpdateNodes_one": "Unable to update {{count}} node",
"unableToUpdateNodes_other": "Unable to update {{count}} nodes",
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx
index dac9404c26..4d6269e5f4 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx
@@ -1,13 +1,14 @@
import { Box, Flex, FormControl, FormLabel } from '@chakra-ui/react';
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
import { useDoesInputHaveValue } from 'features/nodes/hooks/useDoesInputHaveValue';
-import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate';
+import { useFieldInputInstance } from 'features/nodes/hooks/useFieldInputInstance';
+import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
import { PropsWithChildren, memo, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
import EditableFieldTitle from './EditableFieldTitle';
import FieldContextMenu from './FieldContextMenu';
import FieldHandle from './FieldHandle';
import InputFieldRenderer from './InputFieldRenderer';
-import { useTranslation } from 'react-i18next';
interface Props {
nodeId: string;
@@ -16,7 +17,8 @@ interface Props {
const InputField = ({ nodeId, fieldName }: Props) => {
const { t } = useTranslation();
- const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'input');
+ const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
+ const fieldInstance = useFieldInputInstance(nodeId, fieldName);
const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName);
const {
@@ -28,7 +30,7 @@ const InputField = ({ nodeId, fieldName }: Props) => {
} = useConnectionState({ nodeId, fieldName, kind: 'input' });
const isMissingInput = useMemo(() => {
- if (fieldTemplate?.fieldKind !== 'input') {
+ if (!fieldTemplate) {
return false;
}
@@ -45,13 +47,35 @@ const InputField = ({ nodeId, fieldName }: Props) => {
}
}, [fieldTemplate, isConnected, doesFieldHaveValue]);
- if (fieldTemplate?.fieldKind !== 'input') {
+ if (!fieldTemplate || !fieldInstance) {
return (
- {t('nodes.unknownInput')}: {fieldName}
+
+ {t('nodes.unknownInput', {
+ name: fieldInstance?.label ?? fieldTemplate?.title ?? fieldName,
+ })}
+
);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx
index 4b7ca647f8..994510ef99 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/OutputField.tsx
@@ -1,11 +1,12 @@
import { Flex, FormControl, FormLabel, Tooltip } from '@chakra-ui/react';
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
-import { useFieldTemplate } from 'features/nodes/hooks/useFieldTemplate';
+import { useFieldOutputInstance } from 'features/nodes/hooks/useFieldOutputInstance';
+import { useFieldOutputTemplate } from 'features/nodes/hooks/useFieldOutputTemplate';
import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants';
import { PropsWithChildren, memo } from 'react';
+import { useTranslation } from 'react-i18next';
import FieldHandle from './FieldHandle';
import FieldTooltipContent from './FieldTooltipContent';
-import { useTranslation } from 'react-i18next';
interface Props {
nodeId: string;
@@ -14,7 +15,8 @@ interface Props {
const OutputField = ({ nodeId, fieldName }: Props) => {
const { t } = useTranslation();
- const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'output');
+ const fieldTemplate = useFieldOutputTemplate(nodeId, fieldName);
+ const fieldInstance = useFieldOutputInstance(nodeId, fieldName);
const {
isConnected,
@@ -24,13 +26,35 @@ const OutputField = ({ nodeId, fieldName }: Props) => {
shouldDim,
} = useConnectionState({ nodeId, fieldName, kind: 'output' });
- if (fieldTemplate?.fieldKind !== 'output') {
+ if (!fieldTemplate || !fieldInstance) {
return (
- {t('nodes.unknownOutput')}: {fieldName}
+
+ {t('nodes.unknownOutput', {
+ name: fieldTemplate?.title ?? fieldName,
+ })}
+
);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx
index 38aa9bbad7..73d1508c93 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx
@@ -1,13 +1,13 @@
import { Flex } from '@chakra-ui/layout';
import { useAppDispatch } from 'app/store/storeHooks';
-import IAIIconButton from 'common/components/IAIIconButton';
-import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice';
-import { memo, useCallback } from 'react';
-import { FaPlus, FaSync } from 'react-icons/fa';
-import { useTranslation } from 'react-i18next';
import IAIButton from 'common/components/IAIButton';
+import IAIIconButton from 'common/components/IAIIconButton';
import { useGetNodesNeedUpdate } from 'features/nodes/hooks/useGetNodesNeedUpdate';
import { updateAllNodesRequested } from 'features/nodes/store/actions';
+import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { FaExclamationTriangle, FaPlus } from 'react-icons/fa';
const TopLeftPanel = () => {
const dispatch = useAppDispatch();
@@ -29,7 +29,10 @@ const TopLeftPanel = () => {
onClick={handleOpenAddNodePopover}
/>
{nodesNeedUpdate && (
- } onClick={handleClickUpdateNodes}>
+ }
+ onClick={handleClickUpdateNodes}
+ >
{t('nodes.updateAllNodes')}
)}
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputInstance.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputInstance.ts
new file mode 100644
index 0000000000..8e95e0fd5b
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputInstance.ts
@@ -0,0 +1,28 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { stateSelector } from 'app/store/store';
+import { useAppSelector } from 'app/store/storeHooks';
+import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
+import { useMemo } from 'react';
+import { isInvocationNode } from '../types/invocation';
+
+export const useFieldInputInstance = (nodeId: string, fieldName: string) => {
+ const selector = useMemo(
+ () =>
+ createSelector(
+ stateSelector,
+ ({ nodes }) => {
+ const node = nodes.nodes.find((node) => node.id === nodeId);
+ if (!isInvocationNode(node)) {
+ return;
+ }
+ return node.data.inputs[fieldName];
+ },
+ defaultSelectorOptions
+ ),
+ [fieldName, nodeId]
+ );
+
+ const fieldTemplate = useAppSelector(selector);
+
+ return fieldTemplate;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputTemplate.ts
new file mode 100644
index 0000000000..0f682b53b1
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputTemplate.ts
@@ -0,0 +1,29 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { stateSelector } from 'app/store/store';
+import { useAppSelector } from 'app/store/storeHooks';
+import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
+import { useMemo } from 'react';
+import { isInvocationNode } from '../types/invocation';
+
+export const useFieldInputTemplate = (nodeId: string, fieldName: string) => {
+ const selector = useMemo(
+ () =>
+ createSelector(
+ stateSelector,
+ ({ nodes }) => {
+ const node = nodes.nodes.find((node) => node.id === nodeId);
+ if (!isInvocationNode(node)) {
+ return;
+ }
+ const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? ''];
+ return nodeTemplate?.inputs[fieldName];
+ },
+ defaultSelectorOptions
+ ),
+ [fieldName, nodeId]
+ );
+
+ const fieldTemplate = useAppSelector(selector);
+
+ return fieldTemplate;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputInstance.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputInstance.ts
new file mode 100644
index 0000000000..0020d334d5
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputInstance.ts
@@ -0,0 +1,28 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { stateSelector } from 'app/store/store';
+import { useAppSelector } from 'app/store/storeHooks';
+import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
+import { useMemo } from 'react';
+import { isInvocationNode } from '../types/invocation';
+
+export const useFieldOutputInstance = (nodeId: string, fieldName: string) => {
+ const selector = useMemo(
+ () =>
+ createSelector(
+ stateSelector,
+ ({ nodes }) => {
+ const node = nodes.nodes.find((node) => node.id === nodeId);
+ if (!isInvocationNode(node)) {
+ return;
+ }
+ return node.data.outputs[fieldName];
+ },
+ defaultSelectorOptions
+ ),
+ [fieldName, nodeId]
+ );
+
+ const fieldTemplate = useAppSelector(selector);
+
+ return fieldTemplate;
+};
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputTemplate.ts
new file mode 100644
index 0000000000..e8d0f0899c
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldOutputTemplate.ts
@@ -0,0 +1,29 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { stateSelector } from 'app/store/store';
+import { useAppSelector } from 'app/store/storeHooks';
+import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
+import { useMemo } from 'react';
+import { isInvocationNode } from '../types/invocation';
+
+export const useFieldOutputTemplate = (nodeId: string, fieldName: string) => {
+ const selector = useMemo(
+ () =>
+ createSelector(
+ stateSelector,
+ ({ nodes }) => {
+ const node = nodes.nodes.find((node) => node.id === nodeId);
+ if (!isInvocationNode(node)) {
+ return;
+ }
+ const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? ''];
+ return nodeTemplate?.outputs[fieldName];
+ },
+ defaultSelectorOptions
+ ),
+ [fieldName, nodeId]
+ );
+
+ const fieldTemplate = useAppSelector(selector);
+
+ return fieldTemplate;
+};