diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/InvocationNode.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/InvocationNode.tsx index a86b52060b..6c610d7f34 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/InvocationNode.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/InvocationNode.tsx @@ -1,40 +1,34 @@ import { Flex } from '@chakra-ui/react'; -import { - InvocationNodeData, - InvocationTemplate, -} from 'features/nodes/types/types'; -import { map, some } from 'lodash-es'; -import { memo, useMemo } from 'react'; -import { NodeProps } from 'reactflow'; +import { useFieldNames, useWithFooter } from 'features/nodes/hooks/useNodeData'; +import { memo } from 'react'; import InputField from '../fields/InputField'; import OutputField from '../fields/OutputField'; -import NodeFooter, { FOOTER_FIELDS } from './NodeFooter'; +import NodeFooter from './NodeFooter'; import NodeHeader from './NodeHeader'; import NodeWrapper from './NodeWrapper'; type Props = { - nodeProps: NodeProps; - nodeTemplate: InvocationTemplate; + nodeId: string; + isOpen: boolean; + label: string; + type: string; + selected: boolean; }; -const InvocationNode = ({ nodeProps, nodeTemplate }: Props) => { - const { id: nodeId, data } = nodeProps; - const { inputs, outputs, isOpen } = data; - - const inputFields = useMemo( - () => map(inputs).filter((i) => i.name !== 'is_intermediate'), - [inputs] - ); - const outputFields = useMemo(() => map(outputs), [outputs]); - - const withFooter = useMemo( - () => some(outputs, (output) => FOOTER_FIELDS.includes(output.type)), - [outputs] - ); +const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => { + const inputFieldNames = useFieldNames(nodeId, 'input'); + const outputFieldNames = useFieldNames(nodeId, 'output'); + const withFooter = useWithFooter(nodeId); return ( - - + + {isOpen && ( <> { className="nopan" sx={{ flexDir: 'column', px: 2, w: 'full', h: 'full' }} > - {outputFields.map((field) => ( + {outputFieldNames.map((fieldName) => ( ))} - {inputFields.map((field) => ( + {inputFieldNames.map((fieldName) => ( ))} - {withFooter && ( - - )} + {withFooter && } )} diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeCollapseButton.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeCollapseButton.tsx index d67ca10dcc..2648e68607 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeCollapseButton.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeCollapseButton.tsx @@ -2,16 +2,15 @@ import { ChevronUpIcon } from '@chakra-ui/icons'; import { useAppDispatch } from 'app/store/storeHooks'; import IAIIconButton from 'common/components/IAIIconButton'; import { nodeIsOpenChanged } from 'features/nodes/store/nodesSlice'; -import { NodeData } from 'features/nodes/types/types'; import { memo, useCallback } from 'react'; -import { NodeProps, useUpdateNodeInternals } from 'reactflow'; +import { useUpdateNodeInternals } from 'reactflow'; interface Props { - nodeProps: NodeProps; + nodeId: string; + isOpen: boolean; } -const NodeCollapseButton = (props: Props) => { - const { id: nodeId, isOpen } = props.nodeProps.data; +const NodeCollapseButton = ({ nodeId, isOpen }: Props) => { const dispatch = useAppDispatch(); const updateNodeInternals = useUpdateNodeInternals(); diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeCollapsedHandles.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeCollapsedHandles.tsx index ece24f6f8c..32dd554ef4 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeCollapsedHandles.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeCollapsedHandles.tsx @@ -1,20 +1,17 @@ import { useColorModeValue } from '@chakra-ui/react'; import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens'; -import { - InvocationNodeData, - InvocationTemplate, -} from 'features/nodes/types/types'; +import { useNodeData } from 'features/nodes/hooks/useNodeData'; +import { isInvocationNodeData } from 'features/nodes/types/types'; import { map } from 'lodash-es'; import { CSSProperties, memo, useMemo } from 'react'; -import { Handle, NodeProps, Position } from 'reactflow'; +import { Handle, Position } from 'reactflow'; interface Props { - nodeProps: NodeProps; - nodeTemplate: InvocationTemplate; + nodeId: string; } -const NodeCollapsedHandles = (props: Props) => { - const { data } = props.nodeProps; +const NodeCollapsedHandles = ({ nodeId }: Props) => { + const data = useNodeData(nodeId); const { base400, base600 } = useChakraThemeTokens(); const backgroundColor = useColorModeValue(base400, base600); @@ -30,6 +27,10 @@ const NodeCollapsedHandles = (props: Props) => { [backgroundColor] ); + if (!isInvocationNodeData(data)) { + return null; + } + return ( <> { key={`${data.id}-${input.name}-collapsed-input-handle`} type="target" id={input.name} - isValidConnection={() => false} + isConnectable={false} position={Position.Left} style={{ visibility: 'hidden' }} /> @@ -52,7 +53,6 @@ const NodeCollapsedHandles = (props: Props) => { false} isConnectable={false} position={Position.Right} style={{ ...dummyHandleStyles, right: '-0.5rem' }} @@ -62,7 +62,7 @@ const NodeCollapsedHandles = (props: Props) => { key={`${data.id}-${output.name}-collapsed-output-handle`} type="source" id={output.name} - isValidConnection={() => false} + isConnectable={false} position={Position.Right} style={{ visibility: 'hidden' }} /> diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeFooter.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeFooter.tsx index 38c2001b99..9f5980374d 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeFooter.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeFooter.tsx @@ -6,49 +6,22 @@ import { Spacer, } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; +import { + useHasImageOutput, + useIsIntermediate, +} from 'features/nodes/hooks/useNodeData'; import { fieldBooleanValueChanged } from 'features/nodes/store/nodesSlice'; import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants'; -import { - InvocationNodeData, - InvocationTemplate, -} from 'features/nodes/types/types'; -import { some } from 'lodash-es'; -import { ChangeEvent, memo, useCallback, useMemo } from 'react'; -import { NodeProps } from 'reactflow'; +import { ChangeEvent, memo, useCallback } from 'react'; export const IMAGE_FIELDS = ['ImageField', 'ImageCollection']; export const FOOTER_FIELDS = IMAGE_FIELDS; type Props = { - nodeProps: NodeProps; - nodeTemplate: InvocationTemplate; + nodeId: string; }; -const NodeFooter = (props: Props) => { - const { nodeProps, nodeTemplate } = props; - const dispatch = useAppDispatch(); - - const hasImageOutput = useMemo( - () => - some(nodeTemplate?.outputs, (output) => - IMAGE_FIELDS.includes(output.type) - ), - [nodeTemplate?.outputs] - ); - - const handleChangeIsIntermediate = useCallback( - (e: ChangeEvent) => { - dispatch( - fieldBooleanValueChanged({ - nodeId: nodeProps.data.id, - fieldName: 'is_intermediate', - value: !e.target.checked, - }) - ); - }, - [dispatch, nodeProps.data.id] - ); - +const NodeFooter = ({ nodeId }: Props) => { return ( { }} > - {hasImageOutput && ( - - Save Output - - - )} + ); }; export default memo(NodeFooter); + +const SaveImageCheckbox = memo(({ nodeId }: { nodeId: string }) => { + const dispatch = useAppDispatch(); + const hasImageOutput = useHasImageOutput(nodeId); + const is_intermediate = useIsIntermediate(nodeId); + const handleChangeIsIntermediate = useCallback( + (e: ChangeEvent) => { + dispatch( + fieldBooleanValueChanged({ + nodeId, + fieldName: 'is_intermediate', + value: !e.target.checked, + }) + ); + }, + [dispatch, nodeId] + ); + + if (!hasImageOutput) { + return null; + } + + return ( + + Save Output + + + ); +}); + +SaveImageCheckbox.displayName = 'SaveImageCheckbox'; diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeHeader.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeHeader.tsx index a946d21581..fa4585a445 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeHeader.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeHeader.tsx @@ -1,10 +1,5 @@ import { Flex } from '@chakra-ui/react'; -import { - InvocationNodeData, - InvocationTemplate, -} from 'features/nodes/types/types'; import { memo } from 'react'; -import { NodeProps } from 'reactflow'; import NodeCollapseButton from '../Invocation/NodeCollapseButton'; import NodeCollapsedHandles from '../Invocation/NodeCollapsedHandles'; import NodeNotesEdit from '../Invocation/NodeNotesEdit'; @@ -12,14 +7,14 @@ import NodeStatusIndicator from '../Invocation/NodeStatusIndicator'; import NodeTitle from '../Invocation/NodeTitle'; type Props = { - nodeProps: NodeProps; - nodeTemplate: InvocationTemplate; + nodeId: string; + isOpen: boolean; + label: string; + type: string; + selected: boolean; }; -const NodeHeader = (props: Props) => { - const { nodeProps, nodeTemplate } = props; - const { isOpen } = nodeProps.data; - +const NodeHeader = ({ nodeId, isOpen, label, type, selected }: Props) => { return ( { _dark: { color: 'base.200' }, }} > - - + + - - + + - {!isOpen && ( - - )} + {!isOpen && } ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeNotesEdit.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeNotesEdit.tsx index ab54ca2c44..e6f89fdf73 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeNotesEdit.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeNotesEdit.tsx @@ -16,41 +16,31 @@ import { } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; import IAITextarea from 'common/components/IAITextarea'; +import { + useNodeData, + useNodeLabel, + useNodeTemplate, + useNodeTemplateTitle, +} from 'features/nodes/hooks/useNodeData'; import { nodeNotesChanged } from 'features/nodes/store/nodesSlice'; import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants'; -import { - InvocationNodeData, - InvocationTemplate, -} from 'features/nodes/types/types'; +import { isInvocationNodeData } from 'features/nodes/types/types'; import { ChangeEvent, memo, useCallback } from 'react'; import { FaInfoCircle } from 'react-icons/fa'; -import { NodeProps } from 'reactflow'; interface Props { - nodeProps: NodeProps; - nodeTemplate: InvocationTemplate; + nodeId: string; } -const NodeNotesEdit = (props: Props) => { - const { nodeProps, nodeTemplate } = props; - const { data } = nodeProps; +const NodeNotesEdit = ({ nodeId }: Props) => { const { isOpen, onOpen, onClose } = useDisclosure(); - const dispatch = useAppDispatch(); - const handleNotesChanged = useCallback( - (e: ChangeEvent) => { - dispatch(nodeNotesChanged({ nodeId: data.id, notes: e.target.value })); - }, - [data.id, dispatch] - ); + const label = useNodeLabel(nodeId); + const title = useNodeTemplateTitle(nodeId); return ( <> - ) : undefined - } + label={} placement="top" shouldWrapChildren > @@ -75,19 +65,10 @@ const NodeNotesEdit = (props: Props) => { - - {data.label || nodeTemplate?.title || 'Unknown Node'} - + {label || title || 'Unknown Node'} - - Notes - - + @@ -98,16 +79,49 @@ const NodeNotesEdit = (props: Props) => { export default memo(NodeNotesEdit); -type TooltipContentProps = Props; +const TooltipContent = memo(({ nodeId }: { nodeId: string }) => { + const data = useNodeData(nodeId); + const nodeTemplate = useNodeTemplate(nodeId); + + if (!isInvocationNodeData(data)) { + return 'Unknown Node'; + } -const TooltipContent = (props: TooltipContentProps) => { return ( - {props.nodeTemplate?.title} + {nodeTemplate?.title} - {props.nodeTemplate?.description} + {nodeTemplate?.description} - {props.nodeProps.data.notes && {props.nodeProps.data.notes}} + {data?.notes && {data.notes}} ); -}; +}); + +TooltipContent.displayName = 'TooltipContent'; + +const NotesTextarea = memo(({ nodeId }: { nodeId: string }) => { + const dispatch = useAppDispatch(); + const data = useNodeData(nodeId); + const handleNotesChanged = useCallback( + (e: ChangeEvent) => { + dispatch(nodeNotesChanged({ nodeId, notes: e.target.value })); + }, + [dispatch, nodeId] + ); + if (!isInvocationNodeData(data)) { + return null; + } + return ( + + Notes + + + ); +}); + +NotesTextarea.displayName = 'NodesTextarea'; diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeStatusIndicator.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeStatusIndicator.tsx index 6695c4fd3b..d53fec4b42 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeStatusIndicator.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeStatusIndicator.tsx @@ -11,17 +11,12 @@ import { createSelector } from '@reduxjs/toolkit'; import { stateSelector } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants'; -import { - InvocationNodeData, - NodeExecutionState, - NodeStatus, -} from 'features/nodes/types/types'; +import { NodeExecutionState, NodeStatus } from 'features/nodes/types/types'; import { memo, useMemo } from 'react'; import { FaCheck, FaEllipsisH, FaExclamation } from 'react-icons/fa'; -import { NodeProps } from 'reactflow'; type Props = { - nodeProps: NodeProps; + nodeId: string; }; const iconBoxSize = 3; @@ -33,8 +28,7 @@ const circleStyles = { '.chakra-progress__track': { stroke: 'transparent' }, }; -const NodeStatusIndicator = (props: Props) => { - const nodeId = props.nodeProps.data.id; +const NodeStatusIndicator = ({ nodeId }: Props) => { const selectNodeExecutionState = useMemo( () => createSelector( @@ -76,7 +70,7 @@ type TooltipLabelProps = { nodeExecutionState: NodeExecutionState; }; -const TooltipLabel = ({ nodeExecutionState }: TooltipLabelProps) => { +const TooltipLabel = memo(({ nodeExecutionState }: TooltipLabelProps) => { const { status, progress, progressImage } = nodeExecutionState; if (status === NodeStatus.PENDING) { return Pending; @@ -118,13 +112,15 @@ const TooltipLabel = ({ nodeExecutionState }: TooltipLabelProps) => { } return null; -}; +}); + +TooltipLabel.displayName = 'TooltipLabel'; type StatusIconProps = { nodeExecutionState: NodeExecutionState; }; -const StatusIcon = (props: StatusIconProps) => { +const StatusIcon = memo((props: StatusIconProps) => { const { progress, status } = props.nodeExecutionState; if (status === NodeStatus.PENDING) { return ( @@ -182,4 +178,6 @@ const StatusIcon = (props: StatusIconProps) => { ); } return null; -}; +}); + +StatusIcon.displayName = 'StatusIcon'; diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx index fa6a8ea224..d816f3cea1 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx @@ -7,26 +7,29 @@ import { useEditableControls, } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; +import { + useNodeLabel, + useNodeTemplateTitle, +} from 'features/nodes/hooks/useNodeData'; import { nodeLabelChanged } from 'features/nodes/store/nodesSlice'; import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants'; -import { NodeData } from 'features/nodes/types/types'; import { MouseEvent, memo, useCallback, useEffect, useState } from 'react'; type Props = { - nodeData: NodeData; - title: string; + nodeId: string; + title?: string; }; -const NodeTitle = (props: Props) => { - const { title } = props; - const { id: nodeId, label } = props.nodeData; +const NodeTitle = ({ nodeId, title }: Props) => { const dispatch = useAppDispatch(); - const [localTitle, setLocalTitle] = useState(label || title); + const label = useNodeLabel(nodeId); + const templateTitle = useNodeTemplateTitle(nodeId); + const [localTitle, setLocalTitle] = useState(''); const handleSubmit = useCallback( async (newTitle: string) => { dispatch(nodeLabelChanged({ nodeId, label: newTitle })); - setLocalTitle(newTitle || title); + setLocalTitle(newTitle || title || 'Problem Setting Title'); }, [nodeId, dispatch, title] ); @@ -37,8 +40,8 @@ const NodeTitle = (props: Props) => { useEffect(() => { // Another component may change the title; sync local title with global state - setLocalTitle(label || title); - }, [label, title]); + setLocalTitle(label || title || templateTitle || 'Problem Setting Title'); + }, [label, templateTitle, title]); return ( { const dispatch = useAppDispatch(); @@ -25,14 +29,13 @@ const useNodeSelect = (nodeId: string) => { }; type NodeWrapperProps = PropsWithChildren & { - nodeProps: NodeProps; + nodeId: string; + selected: boolean; width?: NonNullable['w']; }; const NodeWrapper = (props: NodeWrapperProps) => { - const { width, children, nodeProps } = props; - const { data, selected } = nodeProps; - const nodeId = data.id; + const { width, children, nodeId, selected } = props; const [ nodeSelectedOutlineLight, @@ -93,4 +96,4 @@ const NodeWrapper = (props: NodeWrapperProps) => { ); }; -export default NodeWrapper; +export default memo(NodeWrapper); diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/UnknownNodeFallback.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/UnknownNodeFallback.tsx index a16c6960ec..664a788b5a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/UnknownNodeFallback.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/UnknownNodeFallback.tsx @@ -1,20 +1,26 @@ import { Box, Flex, Text } from '@chakra-ui/react'; import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants'; -import { InvocationNodeData } from 'features/nodes/types/types'; import { memo } from 'react'; -import { NodeProps } from 'reactflow'; import NodeCollapseButton from '../Invocation/NodeCollapseButton'; import NodeWrapper from '../Invocation/NodeWrapper'; type Props = { - nodeProps: NodeProps; + nodeId: string; + isOpen: boolean; + label: string; + type: string; + selected: boolean; }; -const UnknownNodeFallback = ({ nodeProps }: Props) => { - const { data } = nodeProps; - const { isOpen, label, type } = data; +const UnknownNodeFallback = ({ + nodeId, + isOpen, + label, + type, + selected, +}: Props) => { return ( - + { fontSize: 'sm', }} > - + ; - nodeTemplate: InvocationTemplate; - field: InputFieldValue | OutputFieldValue; fieldTemplate: InputFieldTemplate | OutputFieldTemplate; handleType: HandleType; isConnectionInProgress: boolean; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx index fc239addf3..e9a49989f6 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx @@ -8,13 +8,11 @@ import { import { useAppDispatch } from 'app/store/storeHooks'; import IAIDraggable from 'common/components/IAIDraggable'; import { NodeFieldDraggableData } from 'features/dnd/types'; -import { fieldLabelChanged } from 'features/nodes/store/nodesSlice'; import { - InputFieldTemplate, - InputFieldValue, - InvocationNodeData, - InvocationTemplate, -} from 'features/nodes/types/types'; + useFieldData, + useFieldTemplate, +} from 'features/nodes/hooks/useNodeData'; +import { fieldLabelChanged } from 'features/nodes/store/nodesSlice'; import { MouseEvent, memo, @@ -25,41 +23,43 @@ import { } from 'react'; interface Props { - nodeData: InvocationNodeData; - nodeTemplate: InvocationTemplate; - field: InputFieldValue; - fieldTemplate: InputFieldTemplate; + nodeId: string; + fieldName: string; isDraggable?: boolean; + kind: 'input' | 'output'; } const FieldTitle = (props: Props) => { - const { nodeData, field, fieldTemplate, isDraggable = false } = props; - const { label } = field; - const { title, input } = fieldTemplate; - const { id: nodeId } = nodeData; + const { nodeId, fieldName, isDraggable = false, kind } = props; + const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind); + const field = useFieldData(nodeId, fieldName); + const dispatch = useAppDispatch(); - const [localTitle, setLocalTitle] = useState(label || title); + const [localTitle, setLocalTitle] = useState( + field?.label || fieldTemplate?.title || 'Unknown Field' + ); const draggableData: NodeFieldDraggableData | undefined = useMemo( () => - input !== 'connection' && isDraggable + field && + fieldTemplate?.fieldKind === 'input' && + fieldTemplate?.input !== 'connection' && + isDraggable ? { - id: `${nodeId}-${field.name}`, + id: `${nodeId}-${fieldName}`, payloadType: 'NODE_FIELD', payload: { nodeId, field, fieldTemplate }, } : undefined, - [field, fieldTemplate, input, isDraggable, nodeId] + [field, fieldName, fieldTemplate, isDraggable, nodeId] ); const handleSubmit = useCallback( async (newTitle: string) => { - dispatch( - fieldLabelChanged({ nodeId, fieldName: field.name, label: newTitle }) - ); - setLocalTitle(newTitle || title); + dispatch(fieldLabelChanged({ nodeId, fieldName, label: newTitle })); + setLocalTitle(newTitle || fieldTemplate?.title || 'Unknown Field'); }, - [dispatch, nodeId, field.name, title] + [dispatch, nodeId, fieldName, fieldTemplate?.title] ); const handleChange = useCallback((newTitle: string) => { @@ -68,8 +68,8 @@ const FieldTitle = (props: Props) => { useEffect(() => { // Another component may change the title; sync local title with global state - setLocalTitle(label || title); - }, [label, title]); + setLocalTitle(field?.label || fieldTemplate?.title || 'Unknown Field'); + }, [field?.label, fieldTemplate?.title]); return ( { const { isEditing, getEditButtonProps } = useEditableControls(); const handleDoubleClick = useCallback( (e: MouseEvent) => { @@ -158,4 +158,6 @@ function EditableControls(props: EditableControlsProps) { cursor="text" /> ); -} +}); + +EditableControls.displayName = 'EditableControls'; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/FieldTooltipContent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/FieldTooltipContent.tsx index bf5cd3cd9b..cbe75ca580 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/FieldTooltipContent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/FieldTooltipContent.tsx @@ -1,38 +1,53 @@ import { Flex, Text } from '@chakra-ui/react'; +import { + useFieldData, + useFieldTemplate, +} from 'features/nodes/hooks/useNodeData'; import { FIELDS } from 'features/nodes/types/constants'; import { - InputFieldTemplate, - InputFieldValue, - InvocationNodeData, - InvocationTemplate, - OutputFieldTemplate, - OutputFieldValue, isInputFieldTemplate, isInputFieldValue, } from 'features/nodes/types/types'; import { startCase } from 'lodash-es'; +import { useMemo } from 'react'; interface Props { - nodeData: InvocationNodeData; - nodeTemplate: InvocationTemplate; - field: InputFieldValue | OutputFieldValue; - fieldTemplate: InputFieldTemplate | OutputFieldTemplate; + nodeId: string; + fieldName: string; + kind: 'input' | 'output'; } -const FieldTooltipContent = ({ field, fieldTemplate }: Props) => { +const FieldTooltipContent = ({ nodeId, fieldName, kind }: Props) => { + const field = useFieldData(nodeId, fieldName); + const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind); const isInputTemplate = isInputFieldTemplate(fieldTemplate); + const fieldTitle = useMemo(() => { + if (isInputFieldValue(field)) { + if (field.label && fieldTemplate) { + return `${field.label} (${fieldTemplate.title})`; + } + + if (field.label && !fieldTemplate) { + return field.label; + } + + if (!field.label && fieldTemplate) { + return fieldTemplate.title; + } + + return 'Unknown Field'; + } + }, [field, fieldTemplate]); return ( - - {isInputFieldValue(field) && field.label - ? `${field.label} (${fieldTemplate.title})` - : fieldTemplate.title} - - - {fieldTemplate.description} - - Type: {FIELDS[fieldTemplate.type].title} + {fieldTitle} + {fieldTemplate && ( + + {fieldTemplate.description} + + )} + {fieldTemplate && Type: {FIELDS[fieldTemplate.type].title}} {isInputTemplate && Input: {startCase(fieldTemplate.input)}} ); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/InputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/InputField.tsx index 67f4369384..47033baa7b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/InputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/InputField.tsx @@ -1,27 +1,24 @@ import { Flex, FormControl, FormLabel, Tooltip } from '@chakra-ui/react'; import { useConnectionState } from 'features/nodes/hooks/useConnectionState'; -import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; import { - InputFieldValue, - InvocationNodeData, - InvocationTemplate, -} from 'features/nodes/types/types'; -import { PropsWithChildren, useMemo } from 'react'; -import { NodeProps } from 'reactflow'; + useDoesInputHaveValue, + useFieldTemplate, +} from 'features/nodes/hooks/useNodeData'; +import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; +import { PropsWithChildren, memo, useMemo } from 'react'; import FieldHandle from './FieldHandle'; import FieldTitle from './FieldTitle'; import FieldTooltipContent from './FieldTooltipContent'; import InputFieldRenderer from './InputFieldRenderer'; interface Props { - nodeProps: NodeProps; - nodeTemplate: InvocationTemplate; - field: InputFieldValue; + nodeId: string; + fieldName: string; } -const InputField = (props: Props) => { - const { nodeProps, nodeTemplate, field } = props; - const { id: nodeId } = nodeProps.data; +const InputField = ({ nodeId, fieldName }: Props) => { + const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'input'); + const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName); const { isConnected, @@ -29,15 +26,10 @@ const InputField = (props: Props) => { isConnectionStartField, connectionError, shouldDim, - } = useConnectionState({ nodeId, field, kind: 'input' }); - - const fieldTemplate = useMemo( - () => nodeTemplate.inputs[field.name], - [field.name, nodeTemplate.inputs] - ); + } = useConnectionState({ nodeId, fieldName, kind: 'input' }); const isMissingInput = useMemo(() => { - if (!fieldTemplate) { + if (fieldTemplate?.fieldKind !== 'input') { return false; } @@ -49,18 +41,18 @@ const InputField = (props: Props) => { return true; } - if (!field.value && !isConnected && fieldTemplate.input === 'any') { + if (!doesFieldHaveValue && !isConnected && fieldTemplate.input === 'any') { return true; } - }, [fieldTemplate, isConnected, field.value]); + }, [fieldTemplate, isConnected, doesFieldHaveValue]); - if (!fieldTemplate) { + if (fieldTemplate?.fieldKind !== 'input') { return ( - Unknown input: {field.name} + Unknown input: {fieldName} ); @@ -82,10 +74,9 @@ const InputField = (props: Props) => { } openDelay={HANDLE_TOOLTIP_OPEN_DELAY} @@ -95,27 +86,18 @@ const InputField = (props: Props) => { > - + {fieldTemplate.input !== 'direct' && ( ; -const InputFieldWrapper = ({ shouldDim, children }: InputFieldWrapperProps) => ( - - {children} - +const InputFieldWrapper = memo( + ({ shouldDim, children }: InputFieldWrapperProps) => ( + + {children} + + ) ); + +InputFieldWrapper.displayName = 'InputFieldWrapper'; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/InputFieldRenderer.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/InputFieldRenderer.tsx index 0eae336a1e..acec921d8e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/InputFieldRenderer.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/InputFieldRenderer.tsx @@ -1,11 +1,9 @@ import { Box } from '@chakra-ui/react'; -import { memo } from 'react'; import { - InputFieldTemplate, - InputFieldValue, - InvocationNodeData, - InvocationTemplate, -} from '../../types/types'; + useFieldData, + useFieldTemplate, +} from 'features/nodes/hooks/useNodeData'; +import { memo } from 'react'; import BooleanInputField from './fieldTypes/BooleanInputField'; import ClipInputField from './fieldTypes/ClipInputField'; import CollectionInputField from './fieldTypes/CollectionInputField'; @@ -29,33 +27,33 @@ import VaeInputField from './fieldTypes/VaeInputField'; import VaeModelInputField from './fieldTypes/VaeModelInputField'; type InputFieldProps = { - nodeData: InvocationNodeData; - nodeTemplate: InvocationTemplate; - field: InputFieldValue; - fieldTemplate: InputFieldTemplate; + nodeId: string; + fieldName: string; }; // build an individual input element based on the schema -const InputFieldRenderer = (props: InputFieldProps) => { - const { nodeData, nodeTemplate, field, fieldTemplate } = props; - const { type } = field; +const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => { + const field = useFieldData(nodeId, fieldName); + const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'input'); - if (type === 'string' && fieldTemplate.type === 'string') { + if (fieldTemplate?.fieldKind === 'output') { + return Output field in input: {field?.type}; + } + + if (field?.type === 'string' && fieldTemplate?.type === 'string') { return ( ); } - if (type === 'boolean' && fieldTemplate.type === 'boolean') { + if (field?.type === 'boolean' && fieldTemplate?.type === 'boolean') { return ( @@ -63,46 +61,32 @@ const InputFieldRenderer = (props: InputFieldProps) => { } if ( - (type === 'integer' && fieldTemplate.type === 'integer') || - (type === 'float' && fieldTemplate.type === 'float') + (field?.type === 'integer' && fieldTemplate?.type === 'integer') || + (field?.type === 'float' && fieldTemplate?.type === 'float') ) { return ( ); } - if (type === 'enum' && fieldTemplate.type === 'enum') { + if (field?.type === 'enum' && fieldTemplate?.type === 'enum') { return ( ); } - if (type === 'ImageField' && fieldTemplate.type === 'ImageField') { + if (field?.type === 'ImageField' && fieldTemplate?.type === 'ImageField') { return ( - ); - } - - if (type === 'LatentsField' && fieldTemplate.type === 'LatentsField') { - return ( - @@ -110,68 +94,55 @@ const InputFieldRenderer = (props: InputFieldProps) => { } if ( - type === 'ConditioningField' && - fieldTemplate.type === 'ConditioningField' + field?.type === 'LatentsField' && + fieldTemplate?.type === 'LatentsField' + ) { + return ( + + ); + } + + if ( + field?.type === 'ConditioningField' && + fieldTemplate?.type === 'ConditioningField' ) { return ( ); } - if (type === 'UNetField' && fieldTemplate.type === 'UNetField') { + if (field?.type === 'UNetField' && fieldTemplate?.type === 'UNetField') { return ( ); } - if (type === 'ClipField' && fieldTemplate.type === 'ClipField') { + if (field?.type === 'ClipField' && fieldTemplate?.type === 'ClipField') { return ( ); } - if (type === 'VaeField' && fieldTemplate.type === 'VaeField') { + if (field?.type === 'VaeField' && fieldTemplate?.type === 'VaeField') { return ( - ); - } - - if (type === 'ControlField' && fieldTemplate.type === 'ControlField') { - return ( - - ); - } - - if (type === 'MainModelField' && fieldTemplate.type === 'MainModelField') { - return ( - @@ -179,35 +150,38 @@ const InputFieldRenderer = (props: InputFieldProps) => { } if ( - type === 'SDXLRefinerModelField' && - fieldTemplate.type === 'SDXLRefinerModelField' + field?.type === 'ControlField' && + fieldTemplate?.type === 'ControlField' + ) { + return ( + + ); + } + + if ( + field?.type === 'MainModelField' && + fieldTemplate?.type === 'MainModelField' + ) { + return ( + + ); + } + + if ( + field?.type === 'SDXLRefinerModelField' && + fieldTemplate?.type === 'SDXLRefinerModelField' ) { return ( - ); - } - - if (type === 'VaeModelField' && fieldTemplate.type === 'VaeModelField') { - return ( - - ); - } - - if (type === 'LoRAModelField' && fieldTemplate.type === 'LoRAModelField') { - return ( - @@ -215,57 +189,48 @@ const InputFieldRenderer = (props: InputFieldProps) => { } if ( - type === 'ControlNetModelField' && - fieldTemplate.type === 'ControlNetModelField' + field?.type === 'VaeModelField' && + fieldTemplate?.type === 'VaeModelField' + ) { + return ( + + ); + } + + if ( + field?.type === 'LoRAModelField' && + fieldTemplate?.type === 'LoRAModelField' + ) { + return ( + + ); + } + + if ( + field?.type === 'ControlNetModelField' && + fieldTemplate?.type === 'ControlNetModelField' ) { return ( ); } - if (type === 'Collection' && fieldTemplate.type === 'Collection') { + if (field?.type === 'Collection' && fieldTemplate?.type === 'Collection') { return ( - ); - } - - if (type === 'CollectionItem' && fieldTemplate.type === 'CollectionItem') { - return ( - - ); - } - - if (type === 'ColorField' && fieldTemplate.type === 'ColorField') { - return ( - - ); - } - - if (type === 'ImageCollection' && fieldTemplate.type === 'ImageCollection') { - return ( - @@ -273,20 +238,55 @@ const InputFieldRenderer = (props: InputFieldProps) => { } if ( - type === 'SDXLMainModelField' && - fieldTemplate.type === 'SDXLMainModelField' + field?.type === 'CollectionItem' && + fieldTemplate?.type === 'CollectionItem' ) { return ( - ); } - return Unknown field type: {type}; + if (field?.type === 'ColorField' && fieldTemplate?.type === 'ColorField') { + return ( + + ); + } + + if ( + field?.type === 'ImageCollection' && + fieldTemplate?.type === 'ImageCollection' + ) { + return ( + + ); + } + + if ( + field?.type === 'SDXLMainModelField' && + fieldTemplate?.type === 'SDXLMainModelField' + ) { + return ( + + ); + } + + return Unknown field type: {field?.type}; }; export default memo(InputFieldRenderer); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/LinearViewField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/LinearViewField.tsx index 98a8000b1a..ea4bb76d62 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/LinearViewField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/LinearViewField.tsx @@ -1,39 +1,16 @@ import { Flex, FormControl, FormLabel, Tooltip } from '@chakra-ui/react'; import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; -import { - InputFieldTemplate, - InputFieldValue, - InvocationNodeData, - InvocationTemplate, -} from 'features/nodes/types/types'; import { memo } from 'react'; import FieldTitle from './FieldTitle'; import FieldTooltipContent from './FieldTooltipContent'; import InputFieldRenderer from './InputFieldRenderer'; type Props = { - nodeData: InvocationNodeData; - nodeTemplate: InvocationTemplate; - field: InputFieldValue; - fieldTemplate: InputFieldTemplate; + nodeId: string; + fieldName: string; }; -const LinearViewField = ({ - nodeData, - nodeTemplate, - field, - fieldTemplate, -}: Props) => { - // const dispatch = useAppDispatch(); - // const handleRemoveField = useCallback(() => { - // dispatch( - // workflowExposedFieldRemoved({ - // nodeId: nodeData.id, - // fieldName: field.name, - // }) - // ); - // }, [dispatch, field.name, nodeData.id]); - +const LinearViewField = ({ nodeId, fieldName }: Props) => { return ( } openDelay={HANDLE_TOOLTIP_OPEN_DELAY} @@ -66,20 +42,10 @@ const LinearViewField = ({ mb: 0, }} > - + - + ); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/OutputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/OutputField.tsx index 5a29d1ab7e..2a257d741e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/OutputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/OutputField.tsx @@ -6,25 +6,19 @@ import { Tooltip, } from '@chakra-ui/react'; import { useConnectionState } from 'features/nodes/hooks/useConnectionState'; +import { useFieldTemplate } from 'features/nodes/hooks/useNodeData'; import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; -import { - InvocationNodeData, - InvocationTemplate, - OutputFieldValue, -} from 'features/nodes/types/types'; -import { PropsWithChildren, useMemo } from 'react'; -import { NodeProps } from 'reactflow'; +import { PropsWithChildren, memo } from 'react'; import FieldHandle from './FieldHandle'; import FieldTooltipContent from './FieldTooltipContent'; interface Props { - nodeProps: NodeProps; - nodeTemplate: InvocationTemplate; - field: OutputFieldValue; + nodeId: string; + fieldName: string; } -const OutputField = (props: Props) => { - const { nodeTemplate, nodeProps, field } = props; +const OutputField = ({ nodeId, fieldName }: Props) => { + const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'output'); const { isConnected, @@ -32,20 +26,15 @@ const OutputField = (props: Props) => { isConnectionStartField, connectionError, shouldDim, - } = useConnectionState({ nodeId: nodeProps.data.id, field, kind: 'output' }); + } = useConnectionState({ nodeId, fieldName, kind: 'output' }); - const fieldTemplate = useMemo( - () => nodeTemplate.outputs[field.name], - [field.name, nodeTemplate] - ); - - if (!fieldTemplate) { + if (fieldTemplate?.fieldKind !== 'output') { return ( - Unknown output: {field.name} + Unknown output: {fieldName} ); @@ -57,10 +46,9 @@ const OutputField = (props: Props) => { } openDelay={HANDLE_TOOLTIP_OPEN_DELAY} @@ -75,9 +63,6 @@ const OutputField = (props: Props) => { { ); }; -export default OutputField; +export default memo(OutputField); type OutputFieldWrapperProps = PropsWithChildren<{ shouldDim: boolean; }>; -const OutputFieldWrapper = ({ - shouldDim, - children, -}: OutputFieldWrapperProps) => ( - - {children} - +const OutputFieldWrapper = memo( + ({ shouldDim, children }: OutputFieldWrapperProps) => ( + + {children} + + ) ); + +OutputFieldWrapper.displayName = 'OutputFieldWrapper'; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/BooleanInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/BooleanInputField.tsx index 00a2d2bd10..daf2f598ba 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/BooleanInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/BooleanInputField.tsx @@ -11,8 +11,7 @@ import { FieldComponentProps } from './types'; const BooleanInputFieldComponent = ( props: FieldComponentProps ) => { - const { nodeData, field } = props; - const nodeId = nodeData.id; + const { nodeId, field } = props; const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ColorInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ColorInputField.tsx index c4a4d19a1e..422c3ba48f 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ColorInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ColorInputField.tsx @@ -11,8 +11,7 @@ import { FieldComponentProps } from './types'; const ColorInputFieldComponent = ( props: FieldComponentProps ) => { - const { nodeData, field } = props; - const nodeId = nodeData.id; + const { nodeId, field } = props; const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ControlNetModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ControlNetModelInputField.tsx index f955d6f002..492ec51d20 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ControlNetModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ControlNetModelInputField.tsx @@ -19,8 +19,7 @@ const ControlNetModelInputFieldComponent = ( ControlNetModelInputFieldTemplate > ) => { - const { nodeData, field } = props; - const nodeId = nodeData.id; + const { nodeId, field } = props; const controlNetModel = field.value; const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/EnumInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/EnumInputField.tsx index 210a83b6ac..ebf3593526 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/EnumInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/EnumInputField.tsx @@ -11,8 +11,7 @@ import { FieldComponentProps } from './types'; const EnumInputFieldComponent = ( props: FieldComponentProps ) => { - const { nodeData, field, fieldTemplate } = props; - const nodeId = nodeData.id; + const { nodeId, field, fieldTemplate } = props; const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageCollectionInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageCollectionInputField.tsx index 1ca820939b..4efd0b7775 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageCollectionInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageCollectionInputField.tsx @@ -19,8 +19,7 @@ const ImageCollectionInputFieldComponent = ( ImageCollectionInputFieldTemplate > ) => { - const { nodeData, field } = props; - const nodeId = nodeData.id; + const { nodeId, field } = props; // const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx index f9f9c404d7..0391136dba 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx @@ -21,8 +21,7 @@ import { FieldComponentProps } from './types'; const ImageInputFieldComponent = ( props: FieldComponentProps ) => { - const { nodeData, field } = props; - const nodeId = nodeData.id; + const { nodeId, field } = props; const dispatch = useAppDispatch(); const { currentData: imageDTO } = useGetImageDTOQuery( diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/LoRAModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/LoRAModelInputField.tsx index 8aae6ee9a4..4f8347bbe8 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/LoRAModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/LoRAModelInputField.tsx @@ -21,8 +21,7 @@ const LoRAModelInputFieldComponent = ( LoRAModelInputFieldTemplate > ) => { - const { nodeData, field } = props; - const nodeId = nodeData.id; + const { nodeId, field } = props; const lora = field.value; const dispatch = useAppDispatch(); const { data: loraModels } = useGetLoRAModelsQuery(); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/MainModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/MainModelInputField.tsx index f1047f52cb..681a597235 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/MainModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/MainModelInputField.tsx @@ -26,8 +26,7 @@ const MainModelInputFieldComponent = ( MainModelInputFieldTemplate > ) => { - const { nodeData, field } = props; - const nodeId = nodeData.id; + const { nodeId, field } = props; const dispatch = useAppDispatch(); const isSyncModelEnabled = useFeatureStatus('syncModels').isFeatureEnabled; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/NumberInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/NumberInputField.tsx index 907f90130d..df5c3f763e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/NumberInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/NumberInputField.tsx @@ -23,8 +23,7 @@ const NumberInputFieldComponent = ( IntegerInputFieldTemplate | FloatInputFieldTemplate > ) => { - const { nodeData, field, fieldTemplate } = props; - const nodeId = nodeData.id; + const { nodeId, field, fieldTemplate } = props; const dispatch = useAppDispatch(); const [valueAsString, setValueAsString] = useState( String(field.value) diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/RefinerModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/RefinerModelInputField.tsx index 4a419b51d6..0eec884de0 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/RefinerModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/RefinerModelInputField.tsx @@ -24,8 +24,7 @@ const RefinerModelInputFieldComponent = ( SDXLRefinerModelInputFieldTemplate > ) => { - const { nodeData, field } = props; - const nodeId = nodeData.id; + const { nodeId, field } = props; const dispatch = useAppDispatch(); const { t } = useTranslation(); const isSyncModelEnabled = useFeatureStatus('syncModels').isFeatureEnabled; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SDXLMainModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SDXLMainModelInputField.tsx index 89bd6b2b65..e904aad246 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SDXLMainModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SDXLMainModelInputField.tsx @@ -27,8 +27,7 @@ const ModelInputFieldComponent = ( SDXLMainModelInputFieldTemplate > ) => { - const { nodeData, field } = props; - const nodeId = nodeData.id; + const { nodeId, field } = props; const dispatch = useAppDispatch(); const { t } = useTranslation(); const isSyncModelEnabled = useFeatureStatus('syncModels').isFeatureEnabled; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/StringInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/StringInputField.tsx index 8cc0cf774f..c172e928d0 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/StringInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/StringInputField.tsx @@ -12,8 +12,7 @@ import { FieldComponentProps } from './types'; const StringInputFieldComponent = ( props: FieldComponentProps ) => { - const { nodeData, field, fieldTemplate } = props; - const nodeId = nodeData.id; + const { nodeId, field, fieldTemplate } = props; const dispatch = useAppDispatch(); const handleValueChanged = useCallback( diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/VaeModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/VaeModelInputField.tsx index a8f6a24de4..5dd639cf5c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/VaeModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/VaeModelInputField.tsx @@ -20,8 +20,7 @@ const VaeModelInputFieldComponent = ( VaeModelInputFieldTemplate > ) => { - const { nodeData, field } = props; - const nodeId = nodeData.id; + const { nodeId, field } = props; const vae = field.value; const dispatch = useAppDispatch(); const { data: vaeModels } = useGetVaeModelsQuery(); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/types.ts b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/types.ts index b1d14c9018..5a5e3a9dcf 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/types.ts +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/types.ts @@ -1,16 +1,13 @@ import { InputFieldTemplate, InputFieldValue, - InvocationNodeData, - InvocationTemplate, } from 'features/nodes/types/types'; export type FieldComponentProps< V extends InputFieldValue, T extends InputFieldTemplate > = { - nodeData: InvocationNodeData; - nodeTemplate: InvocationTemplate; + nodeId: string; field: V; fieldTemplate: T; }; diff --git a/invokeai/frontend/web/src/features/nodes/components/nodes/CurrentImageNode.tsx b/invokeai/frontend/web/src/features/nodes/components/nodes/CurrentImageNode.tsx index 04e51159c6..985978f72d 100644 --- a/invokeai/frontend/web/src/features/nodes/components/nodes/CurrentImageNode.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/nodes/CurrentImageNode.tsx @@ -55,7 +55,11 @@ const CurrentImageNode = (props: NodeProps) => { export default memo(CurrentImageNode); const Wrapper = (props: PropsWithChildren<{ nodeProps: NodeProps }>) => ( - + ) => { - const { data } = props; - const { type } = data; + const { data, selected } = props; + const { id: nodeId, type, isOpen, label } = data; - const templateSelector = useMemo(() => makeTemplateSelector(type), [type]); + const hasTemplateSelector = useMemo( + () => + createSelector(stateSelector, ({ nodes }) => + Boolean(nodes.nodeTemplates[type]) + ), + [type] + ); - const nodeTemplate = useAppSelector(templateSelector); + const nodeTemplate = useAppSelector(hasTemplateSelector); if (!nodeTemplate) { - return ; + return ( + + ); } - return ; + return ( + + ); }; export default memo(InvocationNodeWrapper); diff --git a/invokeai/frontend/web/src/features/nodes/components/nodes/NotesNode.tsx b/invokeai/frontend/web/src/features/nodes/components/nodes/NotesNode.tsx index c3b035c6f3..7a46c11901 100644 --- a/invokeai/frontend/web/src/features/nodes/components/nodes/NotesNode.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/nodes/NotesNode.tsx @@ -10,7 +10,7 @@ import NodeTitle from '../Invocation/NodeTitle'; import NodeWrapper from '../Invocation/NodeWrapper'; const NotesNode = (props: NodeProps) => { - const { id: nodeId, data } = props; + const { id: nodeId, data, selected } = props; const { notes, isOpen } = data; const dispatch = useAppDispatch(); const handleChange = useCallback( @@ -21,7 +21,7 @@ const NotesNode = (props: NodeProps) => { ); return ( - + ) => { h: 8, }} > - - + + {isOpen && ( diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/InspectorPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/panel/InspectorPanel.tsx index 654b076eb8..587bea19ec 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panel/InspectorPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/panel/InspectorPanel.tsx @@ -6,39 +6,11 @@ import { TabPanels, Tabs, } from '@chakra-ui/react'; -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 { IAINoContentFallback } from 'common/components/IAIImageFallback'; -import ImageMetadataJSON from 'features/gallery/components/ImageMetadataViewer/ImageMetadataJSON'; import { memo } from 'react'; - -const selector = createSelector( - stateSelector, - ({ nodes }) => { - const lastSelectedNodeId = - nodes.selectedNodes[nodes.selectedNodes.length - 1]; - - const lastSelectedNode = nodes.nodes.find( - (node) => node.id === lastSelectedNodeId - ); - - const lastSelectedNodeTemplate = lastSelectedNode - ? nodes.nodeTemplates[lastSelectedNode.data.type] - : undefined; - - return { - node: lastSelectedNode, - template: lastSelectedNodeTemplate, - }; - }, - defaultSelectorOptions -); +import NodeDataInspector from './NodeDataInspector'; +import NodeTemplateInspector from './NodeTemplateInspector'; const InspectorPanel = () => { - const { node, template } = useAppSelector(selector); - return ( { - {template ? ( - - - - ) : ( - - )} + - {node ? ( - - ) : ( - - )} + diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/NodeDataInspector.tsx b/invokeai/frontend/web/src/features/nodes/components/panel/NodeDataInspector.tsx index 74b1620839..084f743d19 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panel/NodeDataInspector.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/panel/NodeDataInspector.tsx @@ -17,20 +17,20 @@ const selector = createSelector( ); return { - node: lastSelectedNode, + data: lastSelectedNode?.data, }; }, defaultSelectorOptions ); const NodeDataInspector = () => { - const { node } = useAppSelector(selector); + const { data } = useAppSelector(selector); - return node ? ( - - ) : ( - - ); + if (!data) { + return ; + } + + return ; }; export default memo(NodeDataInspector); diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/NodeTemplateInspector.tsx b/invokeai/frontend/web/src/features/nodes/components/panel/NodeTemplateInspector.tsx new file mode 100644 index 0000000000..b483158b36 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/panel/NodeTemplateInspector.tsx @@ -0,0 +1,40 @@ +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 { IAINoContentFallback } from 'common/components/IAIImageFallback'; +import ImageMetadataJSON from 'features/gallery/components/ImageMetadataViewer/ImageMetadataJSON'; +import { memo } from 'react'; + +const selector = createSelector( + stateSelector, + ({ nodes }) => { + const lastSelectedNodeId = + nodes.selectedNodes[nodes.selectedNodes.length - 1]; + + const lastSelectedNode = nodes.nodes.find( + (node) => node.id === lastSelectedNodeId + ); + + const lastSelectedNodeTemplate = lastSelectedNode + ? nodes.nodeTemplates[lastSelectedNode.data.type] + : undefined; + + return { + template: lastSelectedNodeTemplate, + }; + }, + defaultSelectorOptions +); + +const NodeTemplateInspector = () => { + const { template } = useAppSelector(selector); + + if (!template) { + return ; + } + + return ; +}; + +export default memo(NodeTemplateInspector); diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/workflow/LinearTab.tsx b/invokeai/frontend/web/src/features/nodes/components/panel/workflow/LinearTab.tsx index 833fcc6839..cc7428a8ec 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panel/workflow/LinearTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/panel/workflow/LinearTab.tsx @@ -6,14 +6,6 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAIDroppable from 'common/components/IAIDroppable'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { AddFieldToLinearViewDropData } from 'features/dnd/types'; -import { - InputFieldTemplate, - InputFieldValue, - InvocationNodeData, - InvocationTemplate, - isInvocationNode, -} from 'features/nodes/types/types'; -import { forEach } from 'lodash-es'; import { memo } from 'react'; import LinearViewField from '../../fields/LinearViewField'; import ScrollableContent from '../ScrollableContent'; @@ -21,41 +13,8 @@ import ScrollableContent from '../ScrollableContent'; const selector = createSelector( stateSelector, ({ nodes }) => { - const fields: { - nodeData: InvocationNodeData; - nodeTemplate: InvocationTemplate; - field: InputFieldValue; - fieldTemplate: InputFieldTemplate; - }[] = []; - const { exposedFields } = nodes.workflow; - nodes.nodes.filter(isInvocationNode).forEach((node) => { - const nodeTemplate = nodes.nodeTemplates[node.data.type]; - if (!nodeTemplate) { - return; - } - forEach(node.data.inputs, (field) => { - if ( - !exposedFields.some( - (f) => f.nodeId === node.id && f.fieldName === field.name - ) - ) { - return; - } - const fieldTemplate = nodeTemplate.inputs[field.name]; - if (!fieldTemplate) { - return; - } - fields.push({ - nodeData: node.data, - nodeTemplate, - field, - fieldTemplate, - }); - }); - }); - return { - fields, + fields: nodes.workflow.exposedFields, }; }, defaultSelectorOptions @@ -89,13 +48,11 @@ const LinearTabContent = () => { }} > {fields.length ? ( - fields.map(({ nodeData, nodeTemplate, field, fieldTemplate }) => ( + fields.map(({ nodeId, fieldName }) => ( )) ) : ( diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts index 625736a933..e2154f7391 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts @@ -2,8 +2,8 @@ import { createSelector } from '@reduxjs/toolkit'; import { stateSelector } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; import { makeConnectionErrorSelector } from 'features/nodes/store/util/makeIsConnectionValidSelector'; -import { InputFieldValue, OutputFieldValue } from 'features/nodes/types/types'; import { useMemo } from 'react'; +import { useFieldType } from './useNodeData'; const selectIsConnectionInProgress = createSelector( stateSelector, @@ -12,23 +12,19 @@ const selectIsConnectionInProgress = createSelector( nodes.connectionStartParams !== null ); -export type UseConnectionStateProps = - | { - nodeId: string; - field: InputFieldValue; - kind: 'input'; - } - | { - nodeId: string; - field: OutputFieldValue; - kind: 'output'; - }; +export type UseConnectionStateProps = { + nodeId: string; + fieldName: string; + kind: 'input' | 'output'; +}; export const useConnectionState = ({ nodeId, - field, + fieldName, kind, }: UseConnectionStateProps) => { + const fieldType = useFieldType(nodeId, fieldName, kind); + const selectIsConnected = useMemo( () => createSelector(stateSelector, ({ nodes }) => @@ -37,23 +33,23 @@ export const useConnectionState = ({ return ( (kind === 'input' ? edge.target : edge.source) === nodeId && (kind === 'input' ? edge.targetHandle : edge.sourceHandle) === - field.name + fieldName ); }).length ) ), - [field.name, kind, nodeId] + [fieldName, kind, nodeId] ); const selectConnectionError = useMemo( () => makeConnectionErrorSelector( nodeId, - field.name, + fieldName, kind === 'input' ? 'target' : 'source', - field.type + fieldType ), - [nodeId, field.name, field.type, kind] + [nodeId, fieldName, kind, fieldType] ); const selectIsConnectionStartField = useMemo( @@ -61,12 +57,12 @@ export const useConnectionState = ({ createSelector(stateSelector, ({ nodes }) => Boolean( nodes.connectionStartParams?.nodeId === nodeId && - nodes.connectionStartParams?.handleId === field.name && + nodes.connectionStartParams?.handleId === fieldName && nodes.connectionStartParams?.handleType === { input: 'target', output: 'source' }[kind] ) ), - [field.name, kind, nodeId] + [fieldName, kind, nodeId] ); const isConnected = useAppSelector(selectIsConnected); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts new file mode 100644 index 0000000000..948bdb7f3c --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts @@ -0,0 +1,289 @@ +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 { map, some } from 'lodash-es'; +import { useMemo } from 'react'; +import { + FOOTER_FIELDS, + IMAGE_FIELDS, +} from '../components/Invocation/NodeFooter'; +import { isInvocationNode } from '../types/types'; + +const KIND_MAP = { + input: 'inputs' as const, + output: 'outputs' as const, +}; + +export const useNodeTemplate = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? '']; + return nodeTemplate; + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const nodeTemplate = useAppSelector(selector); + + return nodeTemplate; +}; + +export const useNodeData = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + return node?.data; + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const nodeData = useAppSelector(selector); + + return nodeData; +}; + +export const useFieldData = (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 fieldData = useAppSelector(selector); + + return fieldData; +}; + +export const useFieldType = ( + nodeId: string, + fieldName: string, + kind: 'input' | 'output' +) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return; + } + return node?.data[KIND_MAP[kind]][fieldName]?.type; + }, + defaultSelectorOptions + ), + [fieldName, kind, nodeId] + ); + + const fieldType = useAppSelector(selector); + + return fieldType; +}; + +export const useFieldNames = (nodeId: string, kind: 'input' | 'output') => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return []; + } + return map(node.data[KIND_MAP[kind]], (field) => field.name).filter( + (fieldName) => fieldName !== 'is_intermediate' + ); + }, + defaultSelectorOptions + ), + [kind, nodeId] + ); + + const fieldNames = useAppSelector(selector); + return fieldNames; +}; + +export const useWithFooter = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return false; + } + return some(node.data.outputs, (output) => + FOOTER_FIELDS.includes(output.type) + ); + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const withFooter = useAppSelector(selector); + return withFooter; +}; + +export const useHasImageOutput = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return false; + } + return some(node.data.outputs, (output) => + IMAGE_FIELDS.includes(output.type) + ); + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const hasImageOutput = useAppSelector(selector); + return hasImageOutput; +}; + +export const useIsIntermediate = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return false; + } + return Boolean(node.data.inputs.is_intermediate?.value); + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const is_intermediate = useAppSelector(selector); + return is_intermediate; +}; + +export const useNodeLabel = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return false; + } + + return node.data.label; + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const label = useAppSelector(selector); + return label; +}; + +export const useNodeTemplateTitle = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return false; + } + const nodeTemplate = node + ? nodes.nodeTemplates[node.data.type] + : undefined; + + return nodeTemplate?.title; + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const title = useAppSelector(selector); + return title; +}; + +export const useFieldTemplate = ( + nodeId: string, + fieldName: string, + kind: 'input' | 'output' +) => { + 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?.[KIND_MAP[kind]][fieldName]; + }, + defaultSelectorOptions + ), + [fieldName, kind, nodeId] + ); + + const fieldTemplate = useAppSelector(selector); + + return fieldTemplate; +}; + +export const useDoesInputHaveValue = (nodeId: string, fieldName: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return; + } + return Boolean(node?.data.inputs[fieldName]?.value); + }, + defaultSelectorOptions + ), + [fieldName, nodeId] + ); + + const doesFieldHaveValue = useAppSelector(selector); + + return doesFieldHaveValue; +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts b/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts index 3cc3859ce0..29603036ab 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts @@ -9,9 +9,13 @@ export const makeConnectionErrorSelector = ( nodeId: string, fieldName: string, handleType: HandleType, - fieldType: FieldType + fieldType?: FieldType ) => createSelector(stateSelector, (state) => { + if (!fieldType) { + return 'No field type'; + } + const { currentConnectionFieldType, connectionStartParams, nodes, edges } = state.nodes; diff --git a/invokeai/frontend/web/src/features/nodes/types/types.ts b/invokeai/frontend/web/src/features/nodes/types/types.ts index 3846d2425c..60e4877fd8 100644 --- a/invokeai/frontend/web/src/features/nodes/types/types.ts +++ b/invokeai/frontend/web/src/features/nodes/types/types.ts @@ -457,12 +457,13 @@ export type ColorInputFieldTemplate = InputFieldTemplateBase & { }; export const isInputFieldValue = ( - field: InputFieldValue | OutputFieldValue -): field is InputFieldValue => field.fieldKind === 'input'; + field?: InputFieldValue | OutputFieldValue +): field is InputFieldValue => Boolean(field && field.fieldKind === 'input'); export const isInputFieldTemplate = ( - fieldTemplate: InputFieldTemplate | OutputFieldTemplate -): fieldTemplate is InputFieldTemplate => fieldTemplate.fieldKind === 'input'; + fieldTemplate?: InputFieldTemplate | OutputFieldTemplate +): fieldTemplate is InputFieldTemplate => + Boolean(fieldTemplate && fieldTemplate.fieldKind === 'input'); /** * JANKY CUSTOMISATION OF OpenAPI SCHEMA TYPES @@ -632,20 +633,22 @@ export type NodeData = export const isInvocationNode = ( node?: Node -): node is Node => node?.type === 'invocation'; +): node is Node => + Boolean(node && node.type === 'invocation'); export const isInvocationNodeData = ( node?: NodeData ): node is InvocationNodeData => - !['notes', 'current_image'].includes(node?.type ?? ''); + Boolean(node && !['notes', 'current_image'].includes(node.type)); export const isNotesNode = ( node?: Node -): node is Node => node?.type === 'notes'; +): node is Node => Boolean(node && node.type === 'notes'); export const isProgressImageNode = ( node?: Node -): node is Node => node?.type === 'current_image'; +): node is Node => + Boolean(node && node.type === 'current_image'); export enum NodeStatus { PENDING,