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; +};