diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx index 0147bcaed2..baa7fc262a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx @@ -1,7 +1,7 @@ import { Flex, Grid, GridItem } from '@invoke-ai/ui-library'; import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper'; -import { useAnyOrDirectInputFieldNames } from 'features/nodes/hooks/useAnyOrDirectInputFieldNames'; -import { useConnectionInputFieldNames } from 'features/nodes/hooks/useConnectionInputFieldNames'; +import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck'; +import { useFieldNames } from 'features/nodes/hooks/useFieldNames'; import { useOutputFieldNames } from 'features/nodes/hooks/useOutputFieldNames'; import { useWithFooter } from 'features/nodes/hooks/useWithFooter'; import { memo } from 'react'; @@ -20,8 +20,7 @@ type Props = { }; const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => { - const inputConnectionFieldNames = useConnectionInputFieldNames(nodeId); - const inputAnyOrDirectFieldNames = useAnyOrDirectInputFieldNames(nodeId); + const fieldNames = useFieldNames(nodeId); const withFooter = useWithFooter(nodeId); const outputFieldNames = useOutputFieldNames(nodeId); @@ -41,9 +40,11 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => { > - {inputConnectionFieldNames.map((fieldName, i) => ( + {fieldNames.connectionFields.map((fieldName, i) => ( - + + + ))} {outputFieldNames.map((fieldName, i) => ( @@ -52,8 +53,23 @@ const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => { ))} - {inputAnyOrDirectFieldNames.map((fieldName) => ( - + {fieldNames.anyOrDirectFields.map((fieldName) => ( + + + + ))} + {fieldNames.missingFields.map((fieldName) => ( + + + ))} diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/MissingFallback.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/MissingFallback.tsx deleted file mode 100644 index ca5b74b7ff..0000000000 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/MissingFallback.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useDoesFieldExist } from 'features/nodes/hooks/useDoesFieldExist'; -import type { PropsWithChildren } from 'react'; -import { memo } from 'react'; - -type Props = PropsWithChildren<{ - nodeId: string; - fieldName?: string; -}>; - -export const MissingFallback = memo((props: Props) => { - // We must be careful here to avoid race conditions where a deleted node is still referenced as an exposed field - const exists = useDoesFieldExist(props.nodeId, props.fieldName); - if (!exists) { - return null; - } - - return props.children; -}); - -MissingFallback.displayName = 'MissingFallback'; 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 474e0651f7..fd3cc6d6bf 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,16 +1,14 @@ -import { Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { Flex, FormControl } from '@invoke-ai/ui-library'; import { useConnectionState } from 'features/nodes/hooks/useConnectionState'; import { useDoesInputHaveValue } from 'features/nodes/hooks/useDoesInputHaveValue'; -import { useFieldInputInstance } from 'features/nodes/hooks/useFieldInputInstance'; import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate'; -import type { PropsWithChildren } from 'react'; import { memo, useCallback, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; import EditableFieldTitle from './EditableFieldTitle'; import FieldHandle from './FieldHandle'; import FieldLinearViewToggle from './FieldLinearViewToggle'; import InputFieldRenderer from './InputFieldRenderer'; +import { InputFieldWrapper } from './InputFieldWrapper'; interface Props { nodeId: string; @@ -18,9 +16,7 @@ interface Props { } const InputField = ({ nodeId, fieldName }: Props) => { - const { t } = useTranslation(); const fieldTemplate = useFieldInputTemplate(nodeId, fieldName); - const fieldInstance = useFieldInputInstance(nodeId, fieldName); const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName); const [isHovered, setIsHovered] = useState(false); @@ -55,20 +51,6 @@ const InputField = ({ nodeId, fieldName }: Props) => { setIsHovered(false); }, []); - if (!fieldTemplate || !fieldInstance) { - return ( - - - - {t('nodes.unknownInput', { - name: fieldInstance?.label ?? fieldTemplate?.title ?? fieldName, - })} - - - - ); - } - if (fieldTemplate.input === 'connection' || isConnected) { return ( @@ -134,27 +116,3 @@ const InputField = ({ nodeId, fieldName }: Props) => { }; export default memo(InputField); - -type InputFieldWrapperProps = PropsWithChildren<{ - shouldDim: boolean; -}>; - -const InputFieldWrapper = memo(({ shouldDim, children }: InputFieldWrapperProps) => { - return ( - - {children} - - ); -}); - -InputFieldWrapper.displayName = 'InputFieldWrapper'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper.tsx new file mode 100644 index 0000000000..8723538f85 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldWrapper.tsx @@ -0,0 +1,27 @@ +import { Flex } from '@invoke-ai/ui-library'; +import type { PropsWithChildren } from 'react'; +import { memo } from 'react'; + +type InputFieldWrapperProps = PropsWithChildren<{ + shouldDim: boolean; +}>; + +export const InputFieldWrapper = memo(({ shouldDim, children }: InputFieldWrapperProps) => { + return ( + + {children} + + ); +}); + +InputFieldWrapper.displayName = 'InputFieldWrapper'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck.tsx new file mode 100644 index 0000000000..f4b6be0cd6 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck.tsx @@ -0,0 +1,59 @@ +import { Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectInvocationNode } from 'features/nodes/store/selectors'; +import type { PropsWithChildren } from 'react'; +import { memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +type Props = PropsWithChildren<{ + nodeId: string; + fieldName: string; +}>; + +export const InvocationInputFieldCheck = memo(({ nodeId, fieldName, children }: Props) => { + const { t } = useTranslation(); + const templates = useStore($templates); + const selector = useMemo( + () => + createSelector(selectNodesSlice, (nodesSlice) => { + const node = selectInvocationNode(nodesSlice, nodeId); + const instance = node.data.inputs[fieldName]; + const template = templates[node.data.type]; + const fieldTemplate = template?.inputs[fieldName]; + return { + name: instance?.label || fieldTemplate?.title || fieldName, + hasInstance: Boolean(instance), + hasTemplate: Boolean(fieldTemplate), + }; + }), + [fieldName, nodeId, templates] + ); + const { hasInstance, hasTemplate, name } = useAppSelector(selector); + + if (!hasTemplate || !hasInstance) { + return ( + + + + {t('nodes.unknownInput', { name })} + + + + ); + } + + return children; +}); + +InvocationInputFieldCheck.displayName = 'InvocationInputFieldCheck'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx index f7ff85f479..ef466b2882 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/LinearViewField.tsx @@ -3,7 +3,7 @@ import { CSS } from '@dnd-kit/utilities'; import { Flex, Icon, IconButton, Spacer, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import NodeSelectionOverlay from 'common/components/NodeSelectionOverlay'; -import { MissingFallback } from 'features/nodes/components/flow/nodes/Invocation/MissingFallback'; +import { InvocationInputFieldCheck } from 'features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck'; import { useFieldOriginalValue } from 'features/nodes/hooks/useFieldOriginalValue'; import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice'; @@ -102,9 +102,9 @@ const LinearViewFieldInternal = ({ nodeId, fieldName }: Props) => { const LinearViewField = ({ nodeId, fieldName }: Props) => { return ( - + - + ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx index 966809cb0e..76666af396 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx @@ -48,7 +48,7 @@ const NotesNode = (props: NodeProps) => { gap={1} > -