diff --git a/invokeai/app/invocations/primitives.py b/invokeai/app/invocations/primitives.py index 398be04738..61ef9fa27c 100644 --- a/invokeai/app/invocations/primitives.py +++ b/invokeai/app/invocations/primitives.py @@ -48,7 +48,7 @@ class BooleanCollectionOutput(BaseInvocationOutput): ) -@title("Boolean") +@title("Boolean Primitive") @tags("primitives", "boolean") class BooleanInvocation(BaseInvocation): """A boolean primitive value""" @@ -62,7 +62,7 @@ class BooleanInvocation(BaseInvocation): return BooleanOutput(a=self.a) -@title("Boolean Collection") +@title("Boolean Primitive Collection") @tags("primitives", "boolean", "collection") class BooleanCollectionInvocation(BaseInvocation): """A collection of boolean primitive values""" @@ -101,7 +101,7 @@ class IntegerCollectionOutput(BaseInvocationOutput): ) -@title("Integer") +@title("Integer Primitive") @tags("primitives", "integer") class IntegerInvocation(BaseInvocation): """An integer primitive value""" @@ -115,7 +115,7 @@ class IntegerInvocation(BaseInvocation): return IntegerOutput(a=self.a) -@title("Integer Collection") +@title("Integer Primitive Collection") @tags("primitives", "integer", "collection") class IntegerCollectionInvocation(BaseInvocation): """A collection of integer primitive values""" @@ -154,7 +154,7 @@ class FloatCollectionOutput(BaseInvocationOutput): ) -@title("Float") +@title("Float Primitive") @tags("primitives", "float") class FloatInvocation(BaseInvocation): """A float primitive value""" @@ -168,7 +168,7 @@ class FloatInvocation(BaseInvocation): return FloatOutput(a=self.param) -@title("Float Collection") +@title("Float Primitive Collection") @tags("primitives", "float", "collection") class FloatCollectionInvocation(BaseInvocation): """A collection of float primitive values""" @@ -207,7 +207,7 @@ class StringCollectionOutput(BaseInvocationOutput): ) -@title("String") +@title("String Primitive") @tags("primitives", "string") class StringInvocation(BaseInvocation): """A string primitive value""" @@ -221,7 +221,7 @@ class StringInvocation(BaseInvocation): return StringOutput(text=self.text) -@title("String Collection") +@title("String Primitive Collection") @tags("primitives", "string", "collection") class StringCollectionInvocation(BaseInvocation): """A collection of string primitive values""" @@ -289,7 +289,7 @@ class ImageInvocation(BaseInvocation): ) -@title("Image Collection") +@title("Image Primitive Collection") @tags("primitives", "image", "collection") class ImageCollectionInvocation(BaseInvocation): """A collection of image primitive values""" @@ -357,7 +357,7 @@ class LatentsInvocation(BaseInvocation): return build_latents_output(self.latents.latents_name, latents) -@title("Latents Collection") +@title("Latents Primitive Collection") @tags("primitives", "latents", "collection") class LatentsCollectionInvocation(BaseInvocation): """A collection of latents tensor primitive values""" @@ -475,7 +475,7 @@ class ConditioningInvocation(BaseInvocation): return ConditioningOutput(conditioning=self.conditioning) -@title("Conditioning Collection") +@title("Conditioning Primitive Collection") @tags("primitives", "conditioning", "collection") class ConditioningCollectionInvocation(BaseInvocation): """A collection of conditioning tensor primitive values""" diff --git a/invokeai/frontend/web/src/common/components/IAIContextMenu.tsx b/invokeai/frontend/web/src/common/components/IAIContextMenu.tsx new file mode 100644 index 0000000000..757faca866 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/IAIContextMenu.tsx @@ -0,0 +1,126 @@ +/** + * This is a copy-paste of https://github.com/lukasbach/chakra-ui-contextmenu with a small change. + * + * The reactflow background element somehow prevents the chakra `useOutsideClick()` hook from working. + * With a menu open, clicking on the reactflow background element doesn't close the menu. + * + * Reactflow does provide an `onPaneClick` to handle clicks on the background element, but it is not + * straightforward to programatically close the menu. + * + * As a (hopefully temporary) workaround, we will use a dirty hack: + * - create `globalContextMenuCloseTrigger: number` in `ui` slice + * - increment it in `onPaneClick` + * - `useEffect()` to close the menu when `globalContextMenuCloseTrigger` changes + */ + +import { + Menu, + MenuButton, + MenuButtonProps, + MenuProps, + Portal, + PortalProps, + useEventListener, +} from '@chakra-ui/react'; +import { useAppSelector } from 'app/store/storeHooks'; +import * as React from 'react'; +import { + MutableRefObject, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; + +export interface IAIContextMenuProps { + renderMenu: () => JSX.Element | null; + children: (ref: MutableRefObject) => JSX.Element | null; + menuProps?: Omit & { children?: React.ReactNode }; + portalProps?: Omit & { children?: React.ReactNode }; + menuButtonProps?: MenuButtonProps; +} + +export function IAIContextMenu( + props: IAIContextMenuProps +) { + const [isOpen, setIsOpen] = useState(false); + const [isRendered, setIsRendered] = useState(false); + const [isDeferredOpen, setIsDeferredOpen] = useState(false); + const [position, setPosition] = useState<[number, number]>([0, 0]); + const targetRef = useRef(null); + + const globalContextMenuCloseTrigger = useAppSelector( + (state) => state.ui.globalContextMenuCloseTrigger + ); + + useEffect(() => { + if (isOpen) { + setTimeout(() => { + setIsRendered(true); + setTimeout(() => { + setIsDeferredOpen(true); + }); + }); + } else { + setIsDeferredOpen(false); + const timeout = setTimeout(() => { + setIsRendered(isOpen); + }, 1000); + return () => clearTimeout(timeout); + } + }, [isOpen]); + + useEffect(() => { + setIsOpen(false); + setIsDeferredOpen(false); + setIsRendered(false); + }, [globalContextMenuCloseTrigger]); + + useEventListener('contextmenu', (e) => { + if ( + targetRef.current?.contains(e.target as HTMLElement) || + e.target === targetRef.current + ) { + e.preventDefault(); + setIsOpen(true); + setPosition([e.pageX, e.pageY]); + } else { + setIsOpen(false); + } + }); + + const onCloseHandler = useCallback(() => { + props.menuProps?.onClose?.(); + setIsOpen(false); + }, [props.menuProps]); + + return ( + <> + {props.children(targetRef)} + {isRendered && ( + + + + {props.renderMenu()} + + + )} + + ); +} diff --git a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx index 403a6cd5c5..aeeb3677cc 100644 --- a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx +++ b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx @@ -16,6 +16,7 @@ import ImageContextMenu from 'features/gallery/components/ImageContextMenu/Image import { MouseEvent, ReactElement, + ReactNode, SyntheticEvent, memo, useCallback, @@ -32,6 +33,17 @@ import { TypesafeDroppableData, } from 'features/dnd/types'; +const defaultUploadElement = ( + +); + +const defaultNoContentFallback = ; + type IAIDndImageProps = FlexProps & { imageDTO: ImageDTO | undefined; onError?: (event: SyntheticEvent) => void; @@ -47,13 +59,14 @@ type IAIDndImageProps = FlexProps & { fitContainer?: boolean; droppableData?: TypesafeDroppableData; draggableData?: TypesafeDraggableData; - dropLabel?: string; + dropLabel?: ReactNode; isSelected?: boolean; thumbnail?: boolean; noContentFallback?: ReactElement; useThumbailFallback?: boolean; withHoverOverlay?: boolean; children?: JSX.Element; + uploadElement?: ReactNode; }; const IAIDndImage = (props: IAIDndImageProps) => { @@ -74,7 +87,8 @@ const IAIDndImage = (props: IAIDndImageProps) => { dropLabel, isSelected = false, thumbnail = false, - noContentFallback = , + noContentFallback = defaultNoContentFallback, + uploadElement = defaultUploadElement, useThumbailFallback, withHoverOverlay = false, children, @@ -193,12 +207,7 @@ const IAIDndImage = (props: IAIDndImageProps) => { {...getUploadButtonProps()} > - + {uploadElement} )} @@ -210,6 +219,7 @@ const IAIDndImage = (props: IAIDndImageProps) => { onClick={onClick} /> )} + {children} {!isDropDisabled && ( { dropLabel={dropLabel} /> )} - {children} )} diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx index ce8b4450a6..0c13b37f0c 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx @@ -1,5 +1,8 @@ import { MenuList } from '@chakra-ui/react'; -import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu'; +import { + IAIContextMenu, + IAIContextMenuProps, +} from 'common/components/IAIContextMenu'; import { MouseEvent, memo, useCallback } from 'react'; import { ImageDTO } from 'services/api/types'; import { menuListMotionProps } from 'theme/components/menu'; @@ -12,7 +15,7 @@ import MultipleSelectionMenuItems from './MultipleSelectionMenuItems'; type Props = { imageDTO: ImageDTO | undefined; - children: ContextMenuProps['children']; + children: IAIContextMenuProps['children']; }; const selector = createSelector( @@ -33,7 +36,7 @@ const ImageContextMenu = ({ imageDTO, children }: Props) => { }, []); return ( - + menuProps={{ size: 'sm', isLazy: true }} menuButtonProps={{ bg: 'transparent', @@ -68,7 +71,7 @@ const ImageContextMenu = ({ imageDTO, children }: Props) => { }} > {children} - + ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/CustomEdges.tsx b/invokeai/frontend/web/src/features/nodes/components/CustomEdges.tsx index e0ccc6e323..f80f0451e4 100644 --- a/invokeai/frontend/web/src/features/nodes/components/CustomEdges.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/CustomEdges.tsx @@ -2,8 +2,9 @@ import { Badge, Flex } 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 { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens'; -import { useMemo } from 'react'; +import { memo, useMemo } from 'react'; import { BaseEdge, EdgeLabelRenderer, @@ -20,78 +21,165 @@ const makeEdgeSelector = ( targetHandleId: string | null | undefined, selected?: boolean ) => - createSelector(stateSelector, ({ nodes }) => { - const sourceNode = nodes.nodes.find((node) => node.id === source); - const targetNode = nodes.nodes.find((node) => node.id === target); + createSelector( + stateSelector, + ({ nodes }) => { + const sourceNode = nodes.nodes.find((node) => node.id === source); + const targetNode = nodes.nodes.find((node) => node.id === target); - const isInvocationToInvocationEdge = - isInvocationNode(sourceNode) && isInvocationNode(targetNode); + const isInvocationToInvocationEdge = + isInvocationNode(sourceNode) && isInvocationNode(targetNode); - const isSelected = sourceNode?.selected || targetNode?.selected || selected; - const sourceType = isInvocationToInvocationEdge - ? sourceNode?.data?.outputs[sourceHandleId || '']?.type - : undefined; + const isSelected = + sourceNode?.selected || targetNode?.selected || selected; + const sourceType = isInvocationToInvocationEdge + ? sourceNode?.data?.outputs[sourceHandleId || '']?.type + : undefined; - const stroke = - sourceType && nodes.shouldColorEdges - ? colorTokenToCssVar(FIELDS[sourceType].color) - : colorTokenToCssVar('base.500'); + const stroke = + sourceType && nodes.shouldColorEdges + ? colorTokenToCssVar(FIELDS[sourceType].color) + : colorTokenToCssVar('base.500'); - return { - isSelected, - shouldAnimate: nodes.shouldAnimateEdges && isSelected, - stroke, - }; - }); - -const CollapsedEdge = ({ - sourceX, - sourceY, - targetX, - targetY, - sourcePosition, - targetPosition, - markerEnd, - data, - selected, - source, - target, - sourceHandleId, - targetHandleId, -}: EdgeProps<{ count: number }>) => { - const selector = useMemo( - () => - makeEdgeSelector( - source, - sourceHandleId, - target, - targetHandleId, - selected - ), - [selected, source, sourceHandleId, target, targetHandleId] + return { + isSelected, + shouldAnimate: nodes.shouldAnimateEdges && isSelected, + stroke, + }; + }, + defaultSelectorOptions ); - const { isSelected, shouldAnimate } = useAppSelector(selector); - - const [edgePath, labelX, labelY] = getBezierPath({ +const CollapsedEdge = memo( + ({ sourceX, sourceY, - sourcePosition, targetX, targetY, + sourcePosition, targetPosition, - }); + markerEnd, + data, + selected, + source, + target, + sourceHandleId, + targetHandleId, + }: EdgeProps<{ count: number }>) => { + const selector = useMemo( + () => + makeEdgeSelector( + source, + sourceHandleId, + target, + targetHandleId, + selected + ), + [selected, source, sourceHandleId, target, targetHandleId] + ); - const { base500 } = useChakraThemeTokens(); + const { isSelected, shouldAnimate } = useAppSelector(selector); - return ( - <> + const [edgePath, labelX, labelY] = getBezierPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }); + + const { base500 } = useChakraThemeTokens(); + + return ( + <> + + {data?.count && data.count > 1 && ( + + + + {data.count} + + + + )} + + ); + } +); + +CollapsedEdge.displayName = 'CollapsedEdge'; + +const DefaultEdge = memo( + ({ + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + markerEnd, + selected, + source, + target, + sourceHandleId, + targetHandleId, + }: EdgeProps) => { + const selector = useMemo( + () => + makeEdgeSelector( + source, + sourceHandleId, + target, + targetHandleId, + selected + ), + [source, sourceHandleId, target, targetHandleId, selected] + ); + + const { isSelected, shouldAnimate, stroke } = useAppSelector(selector); + + const [edgePath] = getBezierPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }); + + return ( - {data?.count && data.count > 1 && ( - - - - {data.count} - - - - )} - - ); -}; + ); + } +); -const DefaultEdge = ({ - sourceX, - sourceY, - targetX, - targetY, - sourcePosition, - targetPosition, - markerEnd, - selected, - source, - target, - sourceHandleId, - targetHandleId, -}: EdgeProps) => { - const selector = useMemo( - () => - makeEdgeSelector( - source, - sourceHandleId, - target, - targetHandleId, - selected - ), - [source, sourceHandleId, target, targetHandleId, selected] - ); - - const { isSelected, shouldAnimate, stroke } = useAppSelector(selector); - - const [edgePath] = getBezierPath({ - sourceX, - sourceY, - sourcePosition, - targetX, - targetY, - targetPosition, - }); - - return ( - - ); -}; +DefaultEdge.displayName = 'DefaultEdge'; export const edgeTypes = { collapsed: CollapsedEdge, diff --git a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx index 8234a6a7fa..3290a65054 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx @@ -1,5 +1,6 @@ import { useToken } from '@chakra-ui/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { contextMenusClosed } from 'features/ui/store/uiSlice'; import { useCallback } from 'react'; import { Background, @@ -114,6 +115,10 @@ export const Flow = () => { [dispatch] ); + const handlePaneClick = useCallback(() => { + dispatch(contextMenusClosed()); + }, [dispatch]); + return ( { connectionLineComponent={CustomConnectionLine} onSelectionChange={handleSelectionChange} isValidConnection={isValidConnection} - minZoom={0.2} + minZoom={0.1} snapToGrid={shouldSnapToGrid} snapGrid={[25, 25]} connectionRadius={30} proOptions={proOptions} style={{ borderRadius }} + onPaneClick={handlePaneClick} > 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..c858872b57 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,19 @@ 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'; - -export const IMAGE_FIELDS = ['ImageField', 'ImageCollection']; -export const FOOTER_FIELDS = IMAGE_FIELDS; +import { ChangeEvent, memo, useCallback } from 'react'; 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..ea503a8f27 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 }: 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..fa5a9d76fb 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', }} > - + { {isReady && ( { {!isReady && ( { - const { - shouldAnimateEdges, - shouldValidateGraph, - shouldSnapToGrid, - shouldColorEdges, - } = nodes; - return { - shouldAnimateEdges, - shouldValidateGraph, - shouldSnapToGrid, - shouldColorEdges, - }; -}); +const selector = createSelector( + stateSelector, + ({ nodes }) => { + const { + shouldAnimateEdges, + shouldValidateGraph, + shouldSnapToGrid, + shouldColorEdges, + } = nodes; + return { + shouldAnimateEdges, + shouldValidateGraph, + shouldSnapToGrid, + shouldColorEdges, + }; + }, + defaultSelectorOptions +); const NodeEditorSettings = () => { const { isOpen, onOpen, onClose } = useDisclosure(); @@ -136,4 +141,4 @@ const NodeEditorSettings = () => { ); }; -export default NodeEditorSettings; +export default memo(NodeEditorSettings); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/FieldHandle.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/FieldHandle.tsx index f47e68976d..f79a57a4eb 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/FieldHandle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/FieldHandle.tsx @@ -1,19 +1,12 @@ import { Tooltip } from '@chakra-ui/react'; import { CSSProperties, memo, useMemo } from 'react'; -import { Handle, HandleType, NodeProps, Position } from 'reactflow'; +import { Handle, HandleType, Position } from 'reactflow'; import { FIELDS, HANDLE_TOOLTIP_OPEN_DELAY, colorTokenToCssVar, } from '../../types/constants'; -import { - InputFieldTemplate, - InputFieldValue, - InvocationNodeData, - InvocationTemplate, - OutputFieldTemplate, - OutputFieldValue, -} from '../../types/types'; +import { InputFieldTemplate, OutputFieldTemplate } from '../../types/types'; export const handleBaseStyles: CSSProperties = { position: 'absolute', @@ -32,9 +25,6 @@ export const outputHandleStyles: CSSProperties = { }; type FieldHandleProps = { - nodeProps: NodeProps; - 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/components/ui/ClearGraphButton.tsx b/invokeai/frontend/web/src/features/nodes/components/ui/ClearGraphButton.tsx index 432675c5cd..1501d0270b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/ui/ClearGraphButton.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/ui/ClearGraphButton.tsx @@ -25,7 +25,9 @@ const ClearGraphButton = () => { const { isOpen, onOpen, onClose } = useDisclosure(); const cancelRef = useRef(null); - const nodes = useAppSelector((state: RootState) => state.nodes.nodes); + const nodesCount = useAppSelector( + (state: RootState) => state.nodes.nodes.length + ); const handleConfirmClear = useCallback(() => { dispatch(nodeEditorReset()); @@ -49,7 +51,7 @@ const ClearGraphButton = () => { tooltip={t('nodes.clearGraph')} aria-label={t('nodes.clearGraph')} onClick={onOpen} - isDisabled={nodes.length === 0} + isDisabled={!nodesCount} /> { const { iconButton = false, ...rest } = props; const dispatch = useAppDispatch(); const activeTabName = useAppSelector(activeTabNameSelector); @@ -92,4 +92,6 @@ export default function NodeInvokeButton(props: InvokeButton) { ); -} +}; + +export default memo(NodeInvokeButton); diff --git a/invokeai/frontend/web/src/features/nodes/components/ui/ReloadSchemaButton.tsx b/invokeai/frontend/web/src/features/nodes/components/ui/ReloadSchemaButton.tsx index f6c837e044..cbb0ea58ee 100644 --- a/invokeai/frontend/web/src/features/nodes/components/ui/ReloadSchemaButton.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/ui/ReloadSchemaButton.tsx @@ -1,11 +1,11 @@ import { useAppDispatch } from 'app/store/storeHooks'; import IAIIconButton from 'common/components/IAIIconButton'; -import { useCallback } from 'react'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { FaSyncAlt } from 'react-icons/fa'; import { receivedOpenAPISchema } from 'services/api/thunks/schema'; -export default function ReloadSchemaButton() { +const ReloadSchemaButton = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); @@ -21,4 +21,6 @@ export default function ReloadSchemaButton() { onClick={handleReloadSchema} /> ); -} +}; + +export default memo(ReloadSchemaButton); 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..231c7678ef --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts @@ -0,0 +1,286 @@ +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 '../types/constants'; +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/constants.ts b/invokeai/frontend/web/src/features/nodes/types/constants.ts index 0efd529275..1c5c89ff2d 100644 --- a/invokeai/frontend/web/src/features/nodes/types/constants.ts +++ b/invokeai/frontend/web/src/features/nodes/types/constants.ts @@ -6,6 +6,9 @@ export const NODE_WIDTH = 320; export const NODE_MIN_WIDTH = 320; export const DRAG_HANDLE_CLASSNAME = 'node-drag-handle'; +export const IMAGE_FIELDS = ['ImageField', 'ImageCollection']; +export const FOOTER_FIELDS = IMAGE_FIELDS; + export const COLLECTION_TYPES: FieldType[] = [ 'Collection', 'IntegerCollection', 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, diff --git a/invokeai/frontend/web/src/features/ui/store/uiPersistDenylist.ts b/invokeai/frontend/web/src/features/ui/store/uiPersistDenylist.ts index b485d71bdd..07fefd95e8 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiPersistDenylist.ts @@ -3,4 +3,7 @@ import { UIState } from './uiTypes'; /** * UI slice persist denylist */ -export const uiPersistDenylist: (keyof UIState)[] = ['shouldShowImageDetails']; +export const uiPersistDenylist: (keyof UIState)[] = [ + 'shouldShowImageDetails', + 'globalContextMenuCloseTrigger', +]; diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index e487f08067..ca35eab300 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -20,6 +20,7 @@ export const initialUIState: UIState = { shouldShowProgressInViewer: true, shouldShowEmbeddingPicker: false, favoriteSchedulers: [], + globalContextMenuCloseTrigger: 0, }; export const uiSlice = createSlice({ @@ -96,6 +97,9 @@ export const uiSlice = createSlice({ toggleEmbeddingPicker: (state) => { state.shouldShowEmbeddingPicker = !state.shouldShowEmbeddingPicker; }, + contextMenusClosed: (state) => { + state.globalContextMenuCloseTrigger += 1; + }, }, extraReducers(builder) { builder.addCase(initialImageChanged, (state) => { @@ -122,6 +126,7 @@ export const { setShouldShowProgressInViewer, favoriteSchedulersChanged, toggleEmbeddingPicker, + contextMenusClosed, } = uiSlice.actions; export default uiSlice.reducer; diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index 71c83b1630..08c75a12f5 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -26,4 +26,5 @@ export interface UIState { shouldShowProgressInViewer: boolean; shouldShowEmbeddingPicker: boolean; favoriteSchedulers: SchedulerParam[]; + globalContextMenuCloseTrigger: number; } diff --git a/invokeai/frontend/web/src/services/api/schema.d.ts b/invokeai/frontend/web/src/services/api/schema.d.ts index bd93b2f952..316ee0c085 100644 --- a/invokeai/frontend/web/src/services/api/schema.d.ts +++ b/invokeai/frontend/web/src/services/api/schema.d.ts @@ -573,7 +573,7 @@ export type components = { file: Blob; }; /** - * Boolean Collection + * Boolean Primitive Collection * @description A collection of boolean primitive values */ BooleanCollectionInvocation: { @@ -619,7 +619,7 @@ export type components = { collection?: (boolean)[]; }; /** - * Boolean + * Boolean Primitive * @description A boolean primitive value */ BooleanInvocation: { @@ -1002,7 +1002,7 @@ export type components = { clip?: components["schemas"]["ClipField"]; }; /** - * Conditioning Collection + * Conditioning Primitive Collection * @description A collection of conditioning tensor primitive values */ ConditioningCollectionInvocation: { @@ -1770,7 +1770,7 @@ export type components = { field: string; }; /** - * Float Collection + * Float Primitive Collection * @description A collection of float primitive values */ FloatCollectionInvocation: { @@ -1816,7 +1816,7 @@ export type components = { collection?: (number)[]; }; /** - * Float + * Float Primitive * @description A float primitive value */ FloatInvocation: { @@ -2161,7 +2161,7 @@ export type components = { channel?: "A" | "R" | "G" | "B"; }; /** - * Image Collection + * Image Primitive Collection * @description A collection of image primitive values */ ImageCollectionInvocation: { @@ -3113,7 +3113,7 @@ export type components = { seed?: number; }; /** - * Integer Collection + * Integer Primitive Collection * @description A collection of integer primitive values */ IntegerCollectionInvocation: { @@ -3159,7 +3159,7 @@ export type components = { collection?: (number)[]; }; /** - * Integer + * Integer Primitive * @description An integer primitive value */ IntegerInvocation: { @@ -3256,7 +3256,7 @@ export type components = { item?: unknown; }; /** - * Latents Collection + * Latents Primitive Collection * @description A collection of latents tensor primitive values */ LatentsCollectionInvocation: { @@ -5786,7 +5786,7 @@ export type components = { show_easing_plot?: boolean; }; /** - * String Collection + * String Primitive Collection * @description A collection of string primitive values */ StringCollectionInvocation: { @@ -5832,7 +5832,7 @@ export type components = { collection?: (string)[]; }; /** - * String + * String Primitive * @description A string primitive value */ StringInvocation: { @@ -6193,24 +6193,6 @@ export type components = { ui_hidden: boolean; ui_type?: components["schemas"]["UIType"]; }; - /** - * StableDiffusion2ModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; - /** - * ControlNetModelFormat - * @description An enumeration. - * @enum {string} - */ - ControlNetModelFormat: "checkpoint" | "diffusers"; - /** - * StableDiffusionXLModelFormat - * @description An enumeration. - * @enum {string} - */ - StableDiffusionXLModelFormat: "checkpoint" | "diffusers"; /** * StableDiffusionOnnxModelFormat * @description An enumeration. @@ -6223,6 +6205,24 @@ export type components = { * @enum {string} */ StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; + /** + * ControlNetModelFormat + * @description An enumeration. + * @enum {string} + */ + ControlNetModelFormat: "checkpoint" | "diffusers"; + /** + * StableDiffusion2ModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; + /** + * StableDiffusionXLModelFormat + * @description An enumeration. + * @enum {string} + */ + StableDiffusionXLModelFormat: "checkpoint" | "diffusers"; }; responses: never; parameters: never;