From 43b30355e42ff6c5c2056db9d19a568b9897e7d5 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 16 Aug 2023 17:18:58 +1000 Subject: [PATCH 1/8] feat: make primitive node titles consistent --- invokeai/app/invocations/primitives.py | 22 +++---- .../frontend/web/src/services/api/schema.d.ts | 58 +++++++++---------- 2 files changed, 40 insertions(+), 40 deletions(-) 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/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; From 70b8c3dfea7b13991ad33517859ce2dd5cbd2678 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 16 Aug 2023 18:28:51 +1000 Subject: [PATCH 2/8] fix(ui): fix context menu on workflow editor There is a tricky mouse event interaction between chakra's `useOutsideClick()` hook (used by chakra ``) and reactflow. The hook doesn't work when you click the main reactflow area. To get around this, I've used a dirty hack, copy-pasting the simple context menu component we use, and extending it slightly to respond to a global `contextMenusClosed` redux action. --- .../src/common/components/IAIContextMenu.tsx | 126 ++++++++++++++++++ .../web/src/common/components/IAIDndImage.tsx | 27 ++-- .../ImageContextMenu/ImageContextMenu.tsx | 11 +- .../src/features/nodes/components/Flow.tsx | 6 + .../features/ui/store/uiPersistDenylist.ts | 5 +- .../web/src/features/ui/store/uiSlice.ts | 5 + .../web/src/features/ui/store/uiTypes.ts | 1 + 7 files changed, 167 insertions(+), 14 deletions(-) create mode 100644 invokeai/frontend/web/src/common/components/IAIContextMenu.tsx 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/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx index 8234a6a7fa..5b33bf4a9c 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 ( { connectionRadius={30} proOptions={proOptions} style={{ borderRadius }} + onPaneClick={handlePaneClick} > 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; } From f7c92e1eff04665de751cba4a327417c6bb5eb1c Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 16 Aug 2023 18:29:26 +1000 Subject: [PATCH 3/8] fix(ui): disable awkward resize animation for `` --- .../frontend/web/src/features/nodes/components/NodeEditor.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx index 6920a2053b..5e610cfc39 100644 --- a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx @@ -46,7 +46,6 @@ const NodeEditor = () => { {isReady && ( { {!isReady && ( Date: Wed, 16 Aug 2023 22:18:48 +1000 Subject: [PATCH 4/8] fix(ui): improve node rendering performance Previously the editor was using prop-drilling node data and templates to get values deep into nodes. This ended up causing very noticeable performance degradation. For example, any text entry fields were super laggy. Refactor the whole thing to use memoized selectors via hooks. The hooks are mostly very narrow, returning only the data needed. Data objects are never passed down, only node id and field name - sometimes the field kind ('input' or 'output'). The end result is a *much* smoother node editor with very minimal rerenders. --- .../components/Invocation/InvocationNode.tsx | 68 ++-- .../Invocation/NodeCollapseButton.tsx | 9 +- .../Invocation/NodeCollapsedHandles.tsx | 24 +- .../components/Invocation/NodeFooter.tsx | 89 +++--- .../components/Invocation/NodeHeader.tsx | 32 +- .../components/Invocation/NodeNotesEdit.tsx | 92 +++--- .../Invocation/NodeStatusIndicator.tsx | 24 +- .../nodes/components/Invocation/NodeTitle.tsx | 23 +- .../components/Invocation/NodeWrapper.tsx | 19 +- .../Invocation/UnknownNodeFallback.tsx | 22 +- .../nodes/components/fields/FieldHandle.tsx | 14 +- .../nodes/components/fields/FieldTitle.tsx | 56 ++-- .../components/fields/FieldTooltipContent.tsx | 55 ++-- .../nodes/components/fields/InputField.tsx | 102 +++--- .../components/fields/InputFieldRenderer.tsx | 294 +++++++++--------- .../components/fields/LinearViewField.tsx | 50 +-- .../nodes/components/fields/OutputField.tsx | 76 ++--- .../fields/fieldTypes/BooleanInputField.tsx | 3 +- .../fields/fieldTypes/ColorInputField.tsx | 3 +- .../fieldTypes/ControlNetModelInputField.tsx | 3 +- .../fields/fieldTypes/EnumInputField.tsx | 3 +- .../fieldTypes/ImageCollectionInputField.tsx | 3 +- .../fields/fieldTypes/ImageInputField.tsx | 3 +- .../fields/fieldTypes/LoRAModelInputField.tsx | 3 +- .../fields/fieldTypes/MainModelInputField.tsx | 3 +- .../fields/fieldTypes/NumberInputField.tsx | 3 +- .../fieldTypes/RefinerModelInputField.tsx | 3 +- .../fieldTypes/SDXLMainModelInputField.tsx | 3 +- .../fields/fieldTypes/StringInputField.tsx | 3 +- .../fields/fieldTypes/VaeModelInputField.tsx | 3 +- .../components/fields/fieldTypes/types.ts | 5 +- .../components/nodes/CurrentImageNode.tsx | 6 +- .../nodes/InvocationNodeWrapper.tsx | 37 ++- .../nodes/components/nodes/NotesNode.tsx | 8 +- .../nodes/components/panel/InspectorPanel.tsx | 63 +--- .../components/panel/NodeDataInspector.tsx | 14 +- .../panel/NodeTemplateInspector.tsx | 40 +++ .../components/panel/workflow/LinearTab.tsx | 53 +--- .../nodes/hooks/useConnectionState.ts | 36 +-- .../src/features/nodes/hooks/useNodeData.ts | 289 +++++++++++++++++ .../util/makeIsConnectionValidSelector.ts | 6 +- .../web/src/features/nodes/types/types.ts | 19 +- 42 files changed, 928 insertions(+), 736 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/components/panel/NodeTemplateInspector.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/InvocationNode.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/InvocationNode.tsx index a86b52060b..6c610d7f34 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/InvocationNode.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/InvocationNode.tsx @@ -1,40 +1,34 @@ import { Flex } from '@chakra-ui/react'; -import { - InvocationNodeData, - InvocationTemplate, -} from 'features/nodes/types/types'; -import { map, some } from 'lodash-es'; -import { memo, useMemo } from 'react'; -import { NodeProps } from 'reactflow'; +import { useFieldNames, useWithFooter } from 'features/nodes/hooks/useNodeData'; +import { memo } from 'react'; import InputField from '../fields/InputField'; import OutputField from '../fields/OutputField'; -import NodeFooter, { FOOTER_FIELDS } from './NodeFooter'; +import NodeFooter from './NodeFooter'; import NodeHeader from './NodeHeader'; import NodeWrapper from './NodeWrapper'; type Props = { - nodeProps: NodeProps; - nodeTemplate: InvocationTemplate; + nodeId: string; + isOpen: boolean; + label: string; + type: string; + selected: boolean; }; -const InvocationNode = ({ nodeProps, nodeTemplate }: Props) => { - const { id: nodeId, data } = nodeProps; - const { inputs, outputs, isOpen } = data; - - const inputFields = useMemo( - () => map(inputs).filter((i) => i.name !== 'is_intermediate'), - [inputs] - ); - const outputFields = useMemo(() => map(outputs), [outputs]); - - const withFooter = useMemo( - () => some(outputs, (output) => FOOTER_FIELDS.includes(output.type)), - [outputs] - ); +const InvocationNode = ({ nodeId, isOpen, label, type, selected }: Props) => { + const inputFieldNames = useFieldNames(nodeId, 'input'); + const outputFieldNames = useFieldNames(nodeId, 'output'); + const withFooter = useWithFooter(nodeId); return ( - - + + {isOpen && ( <> { className="nopan" sx={{ flexDir: 'column', px: 2, w: 'full', h: 'full' }} > - {outputFields.map((field) => ( + {outputFieldNames.map((fieldName) => ( ))} - {inputFields.map((field) => ( + {inputFieldNames.map((fieldName) => ( ))} - {withFooter && ( - - )} + {withFooter && } )} diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeCollapseButton.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeCollapseButton.tsx index d67ca10dcc..2648e68607 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeCollapseButton.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeCollapseButton.tsx @@ -2,16 +2,15 @@ import { ChevronUpIcon } from '@chakra-ui/icons'; import { useAppDispatch } from 'app/store/storeHooks'; import IAIIconButton from 'common/components/IAIIconButton'; import { nodeIsOpenChanged } from 'features/nodes/store/nodesSlice'; -import { NodeData } from 'features/nodes/types/types'; import { memo, useCallback } from 'react'; -import { NodeProps, useUpdateNodeInternals } from 'reactflow'; +import { useUpdateNodeInternals } from 'reactflow'; interface Props { - nodeProps: NodeProps; + nodeId: string; + isOpen: boolean; } -const NodeCollapseButton = (props: Props) => { - const { id: nodeId, isOpen } = props.nodeProps.data; +const NodeCollapseButton = ({ nodeId, isOpen }: Props) => { const dispatch = useAppDispatch(); const updateNodeInternals = useUpdateNodeInternals(); diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeCollapsedHandles.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeCollapsedHandles.tsx index ece24f6f8c..32dd554ef4 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeCollapsedHandles.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeCollapsedHandles.tsx @@ -1,20 +1,17 @@ import { useColorModeValue } from '@chakra-ui/react'; import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens'; -import { - InvocationNodeData, - InvocationTemplate, -} from 'features/nodes/types/types'; +import { useNodeData } from 'features/nodes/hooks/useNodeData'; +import { isInvocationNodeData } from 'features/nodes/types/types'; import { map } from 'lodash-es'; import { CSSProperties, memo, useMemo } from 'react'; -import { Handle, NodeProps, Position } from 'reactflow'; +import { Handle, Position } from 'reactflow'; interface Props { - nodeProps: NodeProps; - nodeTemplate: InvocationTemplate; + nodeId: string; } -const NodeCollapsedHandles = (props: Props) => { - const { data } = props.nodeProps; +const NodeCollapsedHandles = ({ nodeId }: Props) => { + const data = useNodeData(nodeId); const { base400, base600 } = useChakraThemeTokens(); const backgroundColor = useColorModeValue(base400, base600); @@ -30,6 +27,10 @@ const NodeCollapsedHandles = (props: Props) => { [backgroundColor] ); + if (!isInvocationNodeData(data)) { + return null; + } + return ( <> { key={`${data.id}-${input.name}-collapsed-input-handle`} type="target" id={input.name} - isValidConnection={() => false} + isConnectable={false} position={Position.Left} style={{ visibility: 'hidden' }} /> @@ -52,7 +53,6 @@ const NodeCollapsedHandles = (props: Props) => { false} isConnectable={false} position={Position.Right} style={{ ...dummyHandleStyles, right: '-0.5rem' }} @@ -62,7 +62,7 @@ const NodeCollapsedHandles = (props: Props) => { key={`${data.id}-${output.name}-collapsed-output-handle`} type="source" id={output.name} - isValidConnection={() => false} + isConnectable={false} position={Position.Right} style={{ visibility: 'hidden' }} /> diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeFooter.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeFooter.tsx index 38c2001b99..9f5980374d 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeFooter.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeFooter.tsx @@ -6,49 +6,22 @@ import { Spacer, } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; +import { + useHasImageOutput, + useIsIntermediate, +} from 'features/nodes/hooks/useNodeData'; import { fieldBooleanValueChanged } from 'features/nodes/store/nodesSlice'; import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants'; -import { - InvocationNodeData, - InvocationTemplate, -} from 'features/nodes/types/types'; -import { some } from 'lodash-es'; -import { ChangeEvent, memo, useCallback, useMemo } from 'react'; -import { NodeProps } from 'reactflow'; +import { ChangeEvent, memo, useCallback } from 'react'; export const IMAGE_FIELDS = ['ImageField', 'ImageCollection']; export const FOOTER_FIELDS = IMAGE_FIELDS; type Props = { - nodeProps: NodeProps; - nodeTemplate: InvocationTemplate; + nodeId: string; }; -const NodeFooter = (props: Props) => { - const { nodeProps, nodeTemplate } = props; - const dispatch = useAppDispatch(); - - const hasImageOutput = useMemo( - () => - some(nodeTemplate?.outputs, (output) => - IMAGE_FIELDS.includes(output.type) - ), - [nodeTemplate?.outputs] - ); - - const handleChangeIsIntermediate = useCallback( - (e: ChangeEvent) => { - dispatch( - fieldBooleanValueChanged({ - nodeId: nodeProps.data.id, - fieldName: 'is_intermediate', - value: !e.target.checked, - }) - ); - }, - [dispatch, nodeProps.data.id] - ); - +const NodeFooter = ({ nodeId }: Props) => { return ( { }} > - {hasImageOutput && ( - - Save Output - - - )} + ); }; export default memo(NodeFooter); + +const SaveImageCheckbox = memo(({ nodeId }: { nodeId: string }) => { + const dispatch = useAppDispatch(); + const hasImageOutput = useHasImageOutput(nodeId); + const is_intermediate = useIsIntermediate(nodeId); + const handleChangeIsIntermediate = useCallback( + (e: ChangeEvent) => { + dispatch( + fieldBooleanValueChanged({ + nodeId, + fieldName: 'is_intermediate', + value: !e.target.checked, + }) + ); + }, + [dispatch, nodeId] + ); + + if (!hasImageOutput) { + return null; + } + + return ( + + Save Output + + + ); +}); + +SaveImageCheckbox.displayName = 'SaveImageCheckbox'; diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeHeader.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeHeader.tsx index a946d21581..fa4585a445 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeHeader.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeHeader.tsx @@ -1,10 +1,5 @@ import { Flex } from '@chakra-ui/react'; -import { - InvocationNodeData, - InvocationTemplate, -} from 'features/nodes/types/types'; import { memo } from 'react'; -import { NodeProps } from 'reactflow'; import NodeCollapseButton from '../Invocation/NodeCollapseButton'; import NodeCollapsedHandles from '../Invocation/NodeCollapsedHandles'; import NodeNotesEdit from '../Invocation/NodeNotesEdit'; @@ -12,14 +7,14 @@ import NodeStatusIndicator from '../Invocation/NodeStatusIndicator'; import NodeTitle from '../Invocation/NodeTitle'; type Props = { - nodeProps: NodeProps; - nodeTemplate: InvocationTemplate; + nodeId: string; + isOpen: boolean; + label: string; + type: string; + selected: boolean; }; -const NodeHeader = (props: Props) => { - const { nodeProps, nodeTemplate } = props; - const { isOpen } = nodeProps.data; - +const NodeHeader = ({ nodeId, isOpen, label, type, selected }: Props) => { return ( { _dark: { color: 'base.200' }, }} > - - + + - - + + - {!isOpen && ( - - )} + {!isOpen && } ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeNotesEdit.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeNotesEdit.tsx index ab54ca2c44..e6f89fdf73 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeNotesEdit.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeNotesEdit.tsx @@ -16,41 +16,31 @@ import { } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; import IAITextarea from 'common/components/IAITextarea'; +import { + useNodeData, + useNodeLabel, + useNodeTemplate, + useNodeTemplateTitle, +} from 'features/nodes/hooks/useNodeData'; import { nodeNotesChanged } from 'features/nodes/store/nodesSlice'; import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants'; -import { - InvocationNodeData, - InvocationTemplate, -} from 'features/nodes/types/types'; +import { isInvocationNodeData } from 'features/nodes/types/types'; import { ChangeEvent, memo, useCallback } from 'react'; import { FaInfoCircle } from 'react-icons/fa'; -import { NodeProps } from 'reactflow'; interface Props { - nodeProps: NodeProps; - nodeTemplate: InvocationTemplate; + nodeId: string; } -const NodeNotesEdit = (props: Props) => { - const { nodeProps, nodeTemplate } = props; - const { data } = nodeProps; +const NodeNotesEdit = ({ nodeId }: Props) => { const { isOpen, onOpen, onClose } = useDisclosure(); - const dispatch = useAppDispatch(); - const handleNotesChanged = useCallback( - (e: ChangeEvent) => { - dispatch(nodeNotesChanged({ nodeId: data.id, notes: e.target.value })); - }, - [data.id, dispatch] - ); + const label = useNodeLabel(nodeId); + const title = useNodeTemplateTitle(nodeId); return ( <> - ) : undefined - } + label={} placement="top" shouldWrapChildren > @@ -75,19 +65,10 @@ const NodeNotesEdit = (props: Props) => { - - {data.label || nodeTemplate?.title || 'Unknown Node'} - + {label || title || 'Unknown Node'} - - Notes - - + @@ -98,16 +79,49 @@ const NodeNotesEdit = (props: Props) => { export default memo(NodeNotesEdit); -type TooltipContentProps = Props; +const TooltipContent = memo(({ nodeId }: { nodeId: string }) => { + const data = useNodeData(nodeId); + const nodeTemplate = useNodeTemplate(nodeId); + + if (!isInvocationNodeData(data)) { + return 'Unknown Node'; + } -const TooltipContent = (props: TooltipContentProps) => { return ( - {props.nodeTemplate?.title} + {nodeTemplate?.title} - {props.nodeTemplate?.description} + {nodeTemplate?.description} - {props.nodeProps.data.notes && {props.nodeProps.data.notes}} + {data?.notes && {data.notes}} ); -}; +}); + +TooltipContent.displayName = 'TooltipContent'; + +const NotesTextarea = memo(({ nodeId }: { nodeId: string }) => { + const dispatch = useAppDispatch(); + const data = useNodeData(nodeId); + const handleNotesChanged = useCallback( + (e: ChangeEvent) => { + dispatch(nodeNotesChanged({ nodeId, notes: e.target.value })); + }, + [dispatch, nodeId] + ); + if (!isInvocationNodeData(data)) { + return null; + } + return ( + + Notes + + + ); +}); + +NotesTextarea.displayName = 'NodesTextarea'; diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeStatusIndicator.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeStatusIndicator.tsx index 6695c4fd3b..d53fec4b42 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeStatusIndicator.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeStatusIndicator.tsx @@ -11,17 +11,12 @@ import { createSelector } from '@reduxjs/toolkit'; import { stateSelector } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants'; -import { - InvocationNodeData, - NodeExecutionState, - NodeStatus, -} from 'features/nodes/types/types'; +import { NodeExecutionState, NodeStatus } from 'features/nodes/types/types'; import { memo, useMemo } from 'react'; import { FaCheck, FaEllipsisH, FaExclamation } from 'react-icons/fa'; -import { NodeProps } from 'reactflow'; type Props = { - nodeProps: NodeProps; + nodeId: string; }; const iconBoxSize = 3; @@ -33,8 +28,7 @@ const circleStyles = { '.chakra-progress__track': { stroke: 'transparent' }, }; -const NodeStatusIndicator = (props: Props) => { - const nodeId = props.nodeProps.data.id; +const NodeStatusIndicator = ({ nodeId }: Props) => { const selectNodeExecutionState = useMemo( () => createSelector( @@ -76,7 +70,7 @@ type TooltipLabelProps = { nodeExecutionState: NodeExecutionState; }; -const TooltipLabel = ({ nodeExecutionState }: TooltipLabelProps) => { +const TooltipLabel = memo(({ nodeExecutionState }: TooltipLabelProps) => { const { status, progress, progressImage } = nodeExecutionState; if (status === NodeStatus.PENDING) { return Pending; @@ -118,13 +112,15 @@ const TooltipLabel = ({ nodeExecutionState }: TooltipLabelProps) => { } return null; -}; +}); + +TooltipLabel.displayName = 'TooltipLabel'; type StatusIconProps = { nodeExecutionState: NodeExecutionState; }; -const StatusIcon = (props: StatusIconProps) => { +const StatusIcon = memo((props: StatusIconProps) => { const { progress, status } = props.nodeExecutionState; if (status === NodeStatus.PENDING) { return ( @@ -182,4 +178,6 @@ const StatusIcon = (props: StatusIconProps) => { ); } return null; -}; +}); + +StatusIcon.displayName = 'StatusIcon'; diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx index fa6a8ea224..d816f3cea1 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeTitle.tsx @@ -7,26 +7,29 @@ import { useEditableControls, } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; +import { + useNodeLabel, + useNodeTemplateTitle, +} from 'features/nodes/hooks/useNodeData'; import { nodeLabelChanged } from 'features/nodes/store/nodesSlice'; import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants'; -import { NodeData } from 'features/nodes/types/types'; import { MouseEvent, memo, useCallback, useEffect, useState } from 'react'; type Props = { - nodeData: NodeData; - title: string; + nodeId: string; + title?: string; }; -const NodeTitle = (props: Props) => { - const { title } = props; - const { id: nodeId, label } = props.nodeData; +const NodeTitle = ({ nodeId, title }: Props) => { const dispatch = useAppDispatch(); - const [localTitle, setLocalTitle] = useState(label || title); + const label = useNodeLabel(nodeId); + const templateTitle = useNodeTemplateTitle(nodeId); + const [localTitle, setLocalTitle] = useState(''); const handleSubmit = useCallback( async (newTitle: string) => { dispatch(nodeLabelChanged({ nodeId, label: newTitle })); - setLocalTitle(newTitle || title); + setLocalTitle(newTitle || title || 'Problem Setting Title'); }, [nodeId, dispatch, title] ); @@ -37,8 +40,8 @@ const NodeTitle = (props: Props) => { useEffect(() => { // Another component may change the title; sync local title with global state - setLocalTitle(label || title); - }, [label, title]); + setLocalTitle(label || title || templateTitle || 'Problem Setting Title'); + }, [label, templateTitle, title]); return ( { const dispatch = useAppDispatch(); @@ -25,14 +29,13 @@ const useNodeSelect = (nodeId: string) => { }; type NodeWrapperProps = PropsWithChildren & { - nodeProps: NodeProps; + nodeId: string; + selected: boolean; width?: NonNullable['w']; }; const NodeWrapper = (props: NodeWrapperProps) => { - const { width, children, nodeProps } = props; - const { data, selected } = nodeProps; - const nodeId = data.id; + const { width, children, nodeId, selected } = props; const [ nodeSelectedOutlineLight, @@ -93,4 +96,4 @@ const NodeWrapper = (props: NodeWrapperProps) => { ); }; -export default NodeWrapper; +export default memo(NodeWrapper); diff --git a/invokeai/frontend/web/src/features/nodes/components/Invocation/UnknownNodeFallback.tsx b/invokeai/frontend/web/src/features/nodes/components/Invocation/UnknownNodeFallback.tsx index a16c6960ec..664a788b5a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/UnknownNodeFallback.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/UnknownNodeFallback.tsx @@ -1,20 +1,26 @@ import { Box, Flex, Text } from '@chakra-ui/react'; import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants'; -import { InvocationNodeData } from 'features/nodes/types/types'; import { memo } from 'react'; -import { NodeProps } from 'reactflow'; import NodeCollapseButton from '../Invocation/NodeCollapseButton'; import NodeWrapper from '../Invocation/NodeWrapper'; type Props = { - nodeProps: NodeProps; + nodeId: string; + isOpen: boolean; + label: string; + type: string; + selected: boolean; }; -const UnknownNodeFallback = ({ nodeProps }: Props) => { - const { data } = nodeProps; - const { isOpen, label, type } = data; +const UnknownNodeFallback = ({ + nodeId, + isOpen, + label, + type, + selected, +}: Props) => { return ( - + { fontSize: 'sm', }} > - + ; - nodeTemplate: InvocationTemplate; - field: InputFieldValue | OutputFieldValue; fieldTemplate: InputFieldTemplate | OutputFieldTemplate; handleType: HandleType; isConnectionInProgress: boolean; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx index fc239addf3..e9a49989f6 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/FieldTitle.tsx @@ -8,13 +8,11 @@ import { import { useAppDispatch } from 'app/store/storeHooks'; import IAIDraggable from 'common/components/IAIDraggable'; import { NodeFieldDraggableData } from 'features/dnd/types'; -import { fieldLabelChanged } from 'features/nodes/store/nodesSlice'; import { - InputFieldTemplate, - InputFieldValue, - InvocationNodeData, - InvocationTemplate, -} from 'features/nodes/types/types'; + useFieldData, + useFieldTemplate, +} from 'features/nodes/hooks/useNodeData'; +import { fieldLabelChanged } from 'features/nodes/store/nodesSlice'; import { MouseEvent, memo, @@ -25,41 +23,43 @@ import { } from 'react'; interface Props { - nodeData: InvocationNodeData; - nodeTemplate: InvocationTemplate; - field: InputFieldValue; - fieldTemplate: InputFieldTemplate; + nodeId: string; + fieldName: string; isDraggable?: boolean; + kind: 'input' | 'output'; } const FieldTitle = (props: Props) => { - const { nodeData, field, fieldTemplate, isDraggable = false } = props; - const { label } = field; - const { title, input } = fieldTemplate; - const { id: nodeId } = nodeData; + const { nodeId, fieldName, isDraggable = false, kind } = props; + const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind); + const field = useFieldData(nodeId, fieldName); + const dispatch = useAppDispatch(); - const [localTitle, setLocalTitle] = useState(label || title); + const [localTitle, setLocalTitle] = useState( + field?.label || fieldTemplate?.title || 'Unknown Field' + ); const draggableData: NodeFieldDraggableData | undefined = useMemo( () => - input !== 'connection' && isDraggable + field && + fieldTemplate?.fieldKind === 'input' && + fieldTemplate?.input !== 'connection' && + isDraggable ? { - id: `${nodeId}-${field.name}`, + id: `${nodeId}-${fieldName}`, payloadType: 'NODE_FIELD', payload: { nodeId, field, fieldTemplate }, } : undefined, - [field, fieldTemplate, input, isDraggable, nodeId] + [field, fieldName, fieldTemplate, isDraggable, nodeId] ); const handleSubmit = useCallback( async (newTitle: string) => { - dispatch( - fieldLabelChanged({ nodeId, fieldName: field.name, label: newTitle }) - ); - setLocalTitle(newTitle || title); + dispatch(fieldLabelChanged({ nodeId, fieldName, label: newTitle })); + setLocalTitle(newTitle || fieldTemplate?.title || 'Unknown Field'); }, - [dispatch, nodeId, field.name, title] + [dispatch, nodeId, fieldName, fieldTemplate?.title] ); const handleChange = useCallback((newTitle: string) => { @@ -68,8 +68,8 @@ const FieldTitle = (props: Props) => { useEffect(() => { // Another component may change the title; sync local title with global state - setLocalTitle(label || title); - }, [label, title]); + setLocalTitle(field?.label || fieldTemplate?.title || 'Unknown Field'); + }, [field?.label, fieldTemplate?.title]); return ( { const { isEditing, getEditButtonProps } = useEditableControls(); const handleDoubleClick = useCallback( (e: MouseEvent) => { @@ -158,4 +158,6 @@ function EditableControls(props: EditableControlsProps) { cursor="text" /> ); -} +}); + +EditableControls.displayName = 'EditableControls'; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/FieldTooltipContent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/FieldTooltipContent.tsx index bf5cd3cd9b..cbe75ca580 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/FieldTooltipContent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/FieldTooltipContent.tsx @@ -1,38 +1,53 @@ import { Flex, Text } from '@chakra-ui/react'; +import { + useFieldData, + useFieldTemplate, +} from 'features/nodes/hooks/useNodeData'; import { FIELDS } from 'features/nodes/types/constants'; import { - InputFieldTemplate, - InputFieldValue, - InvocationNodeData, - InvocationTemplate, - OutputFieldTemplate, - OutputFieldValue, isInputFieldTemplate, isInputFieldValue, } from 'features/nodes/types/types'; import { startCase } from 'lodash-es'; +import { useMemo } from 'react'; interface Props { - nodeData: InvocationNodeData; - nodeTemplate: InvocationTemplate; - field: InputFieldValue | OutputFieldValue; - fieldTemplate: InputFieldTemplate | OutputFieldTemplate; + nodeId: string; + fieldName: string; + kind: 'input' | 'output'; } -const FieldTooltipContent = ({ field, fieldTemplate }: Props) => { +const FieldTooltipContent = ({ nodeId, fieldName, kind }: Props) => { + const field = useFieldData(nodeId, fieldName); + const fieldTemplate = useFieldTemplate(nodeId, fieldName, kind); const isInputTemplate = isInputFieldTemplate(fieldTemplate); + const fieldTitle = useMemo(() => { + if (isInputFieldValue(field)) { + if (field.label && fieldTemplate) { + return `${field.label} (${fieldTemplate.title})`; + } + + if (field.label && !fieldTemplate) { + return field.label; + } + + if (!field.label && fieldTemplate) { + return fieldTemplate.title; + } + + return 'Unknown Field'; + } + }, [field, fieldTemplate]); return ( - - {isInputFieldValue(field) && field.label - ? `${field.label} (${fieldTemplate.title})` - : fieldTemplate.title} - - - {fieldTemplate.description} - - Type: {FIELDS[fieldTemplate.type].title} + {fieldTitle} + {fieldTemplate && ( + + {fieldTemplate.description} + + )} + {fieldTemplate && Type: {FIELDS[fieldTemplate.type].title}} {isInputTemplate && Input: {startCase(fieldTemplate.input)}} ); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/InputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/InputField.tsx index 67f4369384..47033baa7b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/InputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/InputField.tsx @@ -1,27 +1,24 @@ import { Flex, FormControl, FormLabel, Tooltip } from '@chakra-ui/react'; import { useConnectionState } from 'features/nodes/hooks/useConnectionState'; -import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; import { - InputFieldValue, - InvocationNodeData, - InvocationTemplate, -} from 'features/nodes/types/types'; -import { PropsWithChildren, useMemo } from 'react'; -import { NodeProps } from 'reactflow'; + useDoesInputHaveValue, + useFieldTemplate, +} from 'features/nodes/hooks/useNodeData'; +import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; +import { PropsWithChildren, memo, useMemo } from 'react'; import FieldHandle from './FieldHandle'; import FieldTitle from './FieldTitle'; import FieldTooltipContent from './FieldTooltipContent'; import InputFieldRenderer from './InputFieldRenderer'; interface Props { - nodeProps: NodeProps; - nodeTemplate: InvocationTemplate; - field: InputFieldValue; + nodeId: string; + fieldName: string; } -const InputField = (props: Props) => { - const { nodeProps, nodeTemplate, field } = props; - const { id: nodeId } = nodeProps.data; +const InputField = ({ nodeId, fieldName }: Props) => { + const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'input'); + const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName); const { isConnected, @@ -29,15 +26,10 @@ const InputField = (props: Props) => { isConnectionStartField, connectionError, shouldDim, - } = useConnectionState({ nodeId, field, kind: 'input' }); - - const fieldTemplate = useMemo( - () => nodeTemplate.inputs[field.name], - [field.name, nodeTemplate.inputs] - ); + } = useConnectionState({ nodeId, fieldName, kind: 'input' }); const isMissingInput = useMemo(() => { - if (!fieldTemplate) { + if (fieldTemplate?.fieldKind !== 'input') { return false; } @@ -49,18 +41,18 @@ const InputField = (props: Props) => { return true; } - if (!field.value && !isConnected && fieldTemplate.input === 'any') { + if (!doesFieldHaveValue && !isConnected && fieldTemplate.input === 'any') { return true; } - }, [fieldTemplate, isConnected, field.value]); + }, [fieldTemplate, isConnected, doesFieldHaveValue]); - if (!fieldTemplate) { + if (fieldTemplate?.fieldKind !== 'input') { return ( - Unknown input: {field.name} + Unknown input: {fieldName} ); @@ -82,10 +74,9 @@ const InputField = (props: Props) => { } openDelay={HANDLE_TOOLTIP_OPEN_DELAY} @@ -95,27 +86,18 @@ const InputField = (props: Props) => { > - + {fieldTemplate.input !== 'direct' && ( ; -const InputFieldWrapper = ({ shouldDim, children }: InputFieldWrapperProps) => ( - - {children} - +const InputFieldWrapper = memo( + ({ shouldDim, children }: InputFieldWrapperProps) => ( + + {children} + + ) ); + +InputFieldWrapper.displayName = 'InputFieldWrapper'; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/InputFieldRenderer.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/InputFieldRenderer.tsx index 0eae336a1e..acec921d8e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/InputFieldRenderer.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/InputFieldRenderer.tsx @@ -1,11 +1,9 @@ import { Box } from '@chakra-ui/react'; -import { memo } from 'react'; import { - InputFieldTemplate, - InputFieldValue, - InvocationNodeData, - InvocationTemplate, -} from '../../types/types'; + useFieldData, + useFieldTemplate, +} from 'features/nodes/hooks/useNodeData'; +import { memo } from 'react'; import BooleanInputField from './fieldTypes/BooleanInputField'; import ClipInputField from './fieldTypes/ClipInputField'; import CollectionInputField from './fieldTypes/CollectionInputField'; @@ -29,33 +27,33 @@ import VaeInputField from './fieldTypes/VaeInputField'; import VaeModelInputField from './fieldTypes/VaeModelInputField'; type InputFieldProps = { - nodeData: InvocationNodeData; - nodeTemplate: InvocationTemplate; - field: InputFieldValue; - fieldTemplate: InputFieldTemplate; + nodeId: string; + fieldName: string; }; // build an individual input element based on the schema -const InputFieldRenderer = (props: InputFieldProps) => { - const { nodeData, nodeTemplate, field, fieldTemplate } = props; - const { type } = field; +const InputFieldRenderer = ({ nodeId, fieldName }: InputFieldProps) => { + const field = useFieldData(nodeId, fieldName); + const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'input'); - if (type === 'string' && fieldTemplate.type === 'string') { + if (fieldTemplate?.fieldKind === 'output') { + return Output field in input: {field?.type}; + } + + if (field?.type === 'string' && fieldTemplate?.type === 'string') { return ( ); } - if (type === 'boolean' && fieldTemplate.type === 'boolean') { + if (field?.type === 'boolean' && fieldTemplate?.type === 'boolean') { return ( @@ -63,46 +61,32 @@ const InputFieldRenderer = (props: InputFieldProps) => { } if ( - (type === 'integer' && fieldTemplate.type === 'integer') || - (type === 'float' && fieldTemplate.type === 'float') + (field?.type === 'integer' && fieldTemplate?.type === 'integer') || + (field?.type === 'float' && fieldTemplate?.type === 'float') ) { return ( ); } - if (type === 'enum' && fieldTemplate.type === 'enum') { + if (field?.type === 'enum' && fieldTemplate?.type === 'enum') { return ( ); } - if (type === 'ImageField' && fieldTemplate.type === 'ImageField') { + if (field?.type === 'ImageField' && fieldTemplate?.type === 'ImageField') { return ( - ); - } - - if (type === 'LatentsField' && fieldTemplate.type === 'LatentsField') { - return ( - @@ -110,68 +94,55 @@ const InputFieldRenderer = (props: InputFieldProps) => { } if ( - type === 'ConditioningField' && - fieldTemplate.type === 'ConditioningField' + field?.type === 'LatentsField' && + fieldTemplate?.type === 'LatentsField' + ) { + return ( + + ); + } + + if ( + field?.type === 'ConditioningField' && + fieldTemplate?.type === 'ConditioningField' ) { return ( ); } - if (type === 'UNetField' && fieldTemplate.type === 'UNetField') { + if (field?.type === 'UNetField' && fieldTemplate?.type === 'UNetField') { return ( ); } - if (type === 'ClipField' && fieldTemplate.type === 'ClipField') { + if (field?.type === 'ClipField' && fieldTemplate?.type === 'ClipField') { return ( ); } - if (type === 'VaeField' && fieldTemplate.type === 'VaeField') { + if (field?.type === 'VaeField' && fieldTemplate?.type === 'VaeField') { return ( - ); - } - - if (type === 'ControlField' && fieldTemplate.type === 'ControlField') { - return ( - - ); - } - - if (type === 'MainModelField' && fieldTemplate.type === 'MainModelField') { - return ( - @@ -179,35 +150,38 @@ const InputFieldRenderer = (props: InputFieldProps) => { } if ( - type === 'SDXLRefinerModelField' && - fieldTemplate.type === 'SDXLRefinerModelField' + field?.type === 'ControlField' && + fieldTemplate?.type === 'ControlField' + ) { + return ( + + ); + } + + if ( + field?.type === 'MainModelField' && + fieldTemplate?.type === 'MainModelField' + ) { + return ( + + ); + } + + if ( + field?.type === 'SDXLRefinerModelField' && + fieldTemplate?.type === 'SDXLRefinerModelField' ) { return ( - ); - } - - if (type === 'VaeModelField' && fieldTemplate.type === 'VaeModelField') { - return ( - - ); - } - - if (type === 'LoRAModelField' && fieldTemplate.type === 'LoRAModelField') { - return ( - @@ -215,57 +189,48 @@ const InputFieldRenderer = (props: InputFieldProps) => { } if ( - type === 'ControlNetModelField' && - fieldTemplate.type === 'ControlNetModelField' + field?.type === 'VaeModelField' && + fieldTemplate?.type === 'VaeModelField' + ) { + return ( + + ); + } + + if ( + field?.type === 'LoRAModelField' && + fieldTemplate?.type === 'LoRAModelField' + ) { + return ( + + ); + } + + if ( + field?.type === 'ControlNetModelField' && + fieldTemplate?.type === 'ControlNetModelField' ) { return ( ); } - if (type === 'Collection' && fieldTemplate.type === 'Collection') { + if (field?.type === 'Collection' && fieldTemplate?.type === 'Collection') { return ( - ); - } - - if (type === 'CollectionItem' && fieldTemplate.type === 'CollectionItem') { - return ( - - ); - } - - if (type === 'ColorField' && fieldTemplate.type === 'ColorField') { - return ( - - ); - } - - if (type === 'ImageCollection' && fieldTemplate.type === 'ImageCollection') { - return ( - @@ -273,20 +238,55 @@ const InputFieldRenderer = (props: InputFieldProps) => { } if ( - type === 'SDXLMainModelField' && - fieldTemplate.type === 'SDXLMainModelField' + field?.type === 'CollectionItem' && + fieldTemplate?.type === 'CollectionItem' ) { return ( - ); } - return Unknown field type: {type}; + if (field?.type === 'ColorField' && fieldTemplate?.type === 'ColorField') { + return ( + + ); + } + + if ( + field?.type === 'ImageCollection' && + fieldTemplate?.type === 'ImageCollection' + ) { + return ( + + ); + } + + if ( + field?.type === 'SDXLMainModelField' && + fieldTemplate?.type === 'SDXLMainModelField' + ) { + return ( + + ); + } + + return Unknown field type: {field?.type}; }; export default memo(InputFieldRenderer); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/LinearViewField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/LinearViewField.tsx index 98a8000b1a..ea4bb76d62 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/LinearViewField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/LinearViewField.tsx @@ -1,39 +1,16 @@ import { Flex, FormControl, FormLabel, Tooltip } from '@chakra-ui/react'; import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; -import { - InputFieldTemplate, - InputFieldValue, - InvocationNodeData, - InvocationTemplate, -} from 'features/nodes/types/types'; import { memo } from 'react'; import FieldTitle from './FieldTitle'; import FieldTooltipContent from './FieldTooltipContent'; import InputFieldRenderer from './InputFieldRenderer'; type Props = { - nodeData: InvocationNodeData; - nodeTemplate: InvocationTemplate; - field: InputFieldValue; - fieldTemplate: InputFieldTemplate; + nodeId: string; + fieldName: string; }; -const LinearViewField = ({ - nodeData, - nodeTemplate, - field, - fieldTemplate, -}: Props) => { - // const dispatch = useAppDispatch(); - // const handleRemoveField = useCallback(() => { - // dispatch( - // workflowExposedFieldRemoved({ - // nodeId: nodeData.id, - // fieldName: field.name, - // }) - // ); - // }, [dispatch, field.name, nodeData.id]); - +const LinearViewField = ({ nodeId, fieldName }: Props) => { return ( } openDelay={HANDLE_TOOLTIP_OPEN_DELAY} @@ -66,20 +42,10 @@ const LinearViewField = ({ mb: 0, }} > - + - + ); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/OutputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/OutputField.tsx index 5a29d1ab7e..2a257d741e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/OutputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/OutputField.tsx @@ -6,25 +6,19 @@ import { Tooltip, } from '@chakra-ui/react'; import { useConnectionState } from 'features/nodes/hooks/useConnectionState'; +import { useFieldTemplate } from 'features/nodes/hooks/useNodeData'; import { HANDLE_TOOLTIP_OPEN_DELAY } from 'features/nodes/types/constants'; -import { - InvocationNodeData, - InvocationTemplate, - OutputFieldValue, -} from 'features/nodes/types/types'; -import { PropsWithChildren, useMemo } from 'react'; -import { NodeProps } from 'reactflow'; +import { PropsWithChildren, memo } from 'react'; import FieldHandle from './FieldHandle'; import FieldTooltipContent from './FieldTooltipContent'; interface Props { - nodeProps: NodeProps; - nodeTemplate: InvocationTemplate; - field: OutputFieldValue; + nodeId: string; + fieldName: string; } -const OutputField = (props: Props) => { - const { nodeTemplate, nodeProps, field } = props; +const OutputField = ({ nodeId, fieldName }: Props) => { + const fieldTemplate = useFieldTemplate(nodeId, fieldName, 'output'); const { isConnected, @@ -32,20 +26,15 @@ const OutputField = (props: Props) => { isConnectionStartField, connectionError, shouldDim, - } = useConnectionState({ nodeId: nodeProps.data.id, field, kind: 'output' }); + } = useConnectionState({ nodeId, fieldName, kind: 'output' }); - const fieldTemplate = useMemo( - () => nodeTemplate.outputs[field.name], - [field.name, nodeTemplate] - ); - - if (!fieldTemplate) { + if (fieldTemplate?.fieldKind !== 'output') { return ( - Unknown output: {field.name} + Unknown output: {fieldName} ); @@ -57,10 +46,9 @@ const OutputField = (props: Props) => { } openDelay={HANDLE_TOOLTIP_OPEN_DELAY} @@ -75,9 +63,6 @@ const OutputField = (props: Props) => { { ); }; -export default OutputField; +export default memo(OutputField); type OutputFieldWrapperProps = PropsWithChildren<{ shouldDim: boolean; }>; -const OutputFieldWrapper = ({ - shouldDim, - children, -}: OutputFieldWrapperProps) => ( - - {children} - +const OutputFieldWrapper = memo( + ({ shouldDim, children }: OutputFieldWrapperProps) => ( + + {children} + + ) ); + +OutputFieldWrapper.displayName = 'OutputFieldWrapper'; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/BooleanInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/BooleanInputField.tsx index 00a2d2bd10..daf2f598ba 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/BooleanInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/BooleanInputField.tsx @@ -11,8 +11,7 @@ import { FieldComponentProps } from './types'; const BooleanInputFieldComponent = ( props: FieldComponentProps ) => { - const { nodeData, field } = props; - const nodeId = nodeData.id; + const { nodeId, field } = props; const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ColorInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ColorInputField.tsx index c4a4d19a1e..422c3ba48f 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ColorInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ColorInputField.tsx @@ -11,8 +11,7 @@ import { FieldComponentProps } from './types'; const ColorInputFieldComponent = ( props: FieldComponentProps ) => { - const { nodeData, field } = props; - const nodeId = nodeData.id; + const { nodeId, field } = props; const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ControlNetModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ControlNetModelInputField.tsx index f955d6f002..492ec51d20 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ControlNetModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ControlNetModelInputField.tsx @@ -19,8 +19,7 @@ const ControlNetModelInputFieldComponent = ( ControlNetModelInputFieldTemplate > ) => { - const { nodeData, field } = props; - const nodeId = nodeData.id; + const { nodeId, field } = props; const controlNetModel = field.value; const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/EnumInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/EnumInputField.tsx index 210a83b6ac..ebf3593526 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/EnumInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/EnumInputField.tsx @@ -11,8 +11,7 @@ import { FieldComponentProps } from './types'; const EnumInputFieldComponent = ( props: FieldComponentProps ) => { - const { nodeData, field, fieldTemplate } = props; - const nodeId = nodeData.id; + const { nodeId, field, fieldTemplate } = props; const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageCollectionInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageCollectionInputField.tsx index 1ca820939b..4efd0b7775 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageCollectionInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageCollectionInputField.tsx @@ -19,8 +19,7 @@ const ImageCollectionInputFieldComponent = ( ImageCollectionInputFieldTemplate > ) => { - const { nodeData, field } = props; - const nodeId = nodeData.id; + const { nodeId, field } = props; // const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx index f9f9c404d7..0391136dba 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/ImageInputField.tsx @@ -21,8 +21,7 @@ import { FieldComponentProps } from './types'; const ImageInputFieldComponent = ( props: FieldComponentProps ) => { - const { nodeData, field } = props; - const nodeId = nodeData.id; + const { nodeId, field } = props; const dispatch = useAppDispatch(); const { currentData: imageDTO } = useGetImageDTOQuery( diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/LoRAModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/LoRAModelInputField.tsx index 8aae6ee9a4..4f8347bbe8 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/LoRAModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/LoRAModelInputField.tsx @@ -21,8 +21,7 @@ const LoRAModelInputFieldComponent = ( LoRAModelInputFieldTemplate > ) => { - const { nodeData, field } = props; - const nodeId = nodeData.id; + const { nodeId, field } = props; const lora = field.value; const dispatch = useAppDispatch(); const { data: loraModels } = useGetLoRAModelsQuery(); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/MainModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/MainModelInputField.tsx index f1047f52cb..681a597235 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/MainModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/MainModelInputField.tsx @@ -26,8 +26,7 @@ const MainModelInputFieldComponent = ( MainModelInputFieldTemplate > ) => { - const { nodeData, field } = props; - const nodeId = nodeData.id; + const { nodeId, field } = props; const dispatch = useAppDispatch(); const isSyncModelEnabled = useFeatureStatus('syncModels').isFeatureEnabled; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/NumberInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/NumberInputField.tsx index 907f90130d..df5c3f763e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/NumberInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/NumberInputField.tsx @@ -23,8 +23,7 @@ const NumberInputFieldComponent = ( IntegerInputFieldTemplate | FloatInputFieldTemplate > ) => { - const { nodeData, field, fieldTemplate } = props; - const nodeId = nodeData.id; + const { nodeId, field, fieldTemplate } = props; const dispatch = useAppDispatch(); const [valueAsString, setValueAsString] = useState( String(field.value) diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/RefinerModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/RefinerModelInputField.tsx index 4a419b51d6..0eec884de0 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/RefinerModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/RefinerModelInputField.tsx @@ -24,8 +24,7 @@ const RefinerModelInputFieldComponent = ( SDXLRefinerModelInputFieldTemplate > ) => { - const { nodeData, field } = props; - const nodeId = nodeData.id; + const { nodeId, field } = props; const dispatch = useAppDispatch(); const { t } = useTranslation(); const isSyncModelEnabled = useFeatureStatus('syncModels').isFeatureEnabled; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SDXLMainModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SDXLMainModelInputField.tsx index 89bd6b2b65..e904aad246 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SDXLMainModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/SDXLMainModelInputField.tsx @@ -27,8 +27,7 @@ const ModelInputFieldComponent = ( SDXLMainModelInputFieldTemplate > ) => { - const { nodeData, field } = props; - const nodeId = nodeData.id; + const { nodeId, field } = props; const dispatch = useAppDispatch(); const { t } = useTranslation(); const isSyncModelEnabled = useFeatureStatus('syncModels').isFeatureEnabled; diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/StringInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/StringInputField.tsx index 8cc0cf774f..c172e928d0 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/StringInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/StringInputField.tsx @@ -12,8 +12,7 @@ import { FieldComponentProps } from './types'; const StringInputFieldComponent = ( props: FieldComponentProps ) => { - const { nodeData, field, fieldTemplate } = props; - const nodeId = nodeData.id; + const { nodeId, field, fieldTemplate } = props; const dispatch = useAppDispatch(); const handleValueChanged = useCallback( diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/VaeModelInputField.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/VaeModelInputField.tsx index a8f6a24de4..5dd639cf5c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/VaeModelInputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/VaeModelInputField.tsx @@ -20,8 +20,7 @@ const VaeModelInputFieldComponent = ( VaeModelInputFieldTemplate > ) => { - const { nodeData, field } = props; - const nodeId = nodeData.id; + const { nodeId, field } = props; const vae = field.value; const dispatch = useAppDispatch(); const { data: vaeModels } = useGetVaeModelsQuery(); diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/types.ts b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/types.ts index b1d14c9018..5a5e3a9dcf 100644 --- a/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/types.ts +++ b/invokeai/frontend/web/src/features/nodes/components/fields/fieldTypes/types.ts @@ -1,16 +1,13 @@ import { InputFieldTemplate, InputFieldValue, - InvocationNodeData, - InvocationTemplate, } from 'features/nodes/types/types'; export type FieldComponentProps< V extends InputFieldValue, T extends InputFieldTemplate > = { - nodeData: InvocationNodeData; - nodeTemplate: InvocationTemplate; + nodeId: string; field: V; fieldTemplate: T; }; diff --git a/invokeai/frontend/web/src/features/nodes/components/nodes/CurrentImageNode.tsx b/invokeai/frontend/web/src/features/nodes/components/nodes/CurrentImageNode.tsx index 04e51159c6..985978f72d 100644 --- a/invokeai/frontend/web/src/features/nodes/components/nodes/CurrentImageNode.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/nodes/CurrentImageNode.tsx @@ -55,7 +55,11 @@ const CurrentImageNode = (props: NodeProps) => { export default memo(CurrentImageNode); const Wrapper = (props: PropsWithChildren<{ nodeProps: NodeProps }>) => ( - + ) => { - const { data } = props; - const { type } = data; + const { data, selected } = props; + const { id: nodeId, type, isOpen, label } = data; - const templateSelector = useMemo(() => makeTemplateSelector(type), [type]); + const hasTemplateSelector = useMemo( + () => + createSelector(stateSelector, ({ nodes }) => + Boolean(nodes.nodeTemplates[type]) + ), + [type] + ); - const nodeTemplate = useAppSelector(templateSelector); + const nodeTemplate = useAppSelector(hasTemplateSelector); if (!nodeTemplate) { - return ; + return ( + + ); } - return ; + return ( + + ); }; export default memo(InvocationNodeWrapper); diff --git a/invokeai/frontend/web/src/features/nodes/components/nodes/NotesNode.tsx b/invokeai/frontend/web/src/features/nodes/components/nodes/NotesNode.tsx index c3b035c6f3..7a46c11901 100644 --- a/invokeai/frontend/web/src/features/nodes/components/nodes/NotesNode.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/nodes/NotesNode.tsx @@ -10,7 +10,7 @@ import NodeTitle from '../Invocation/NodeTitle'; import NodeWrapper from '../Invocation/NodeWrapper'; const NotesNode = (props: NodeProps) => { - const { id: nodeId, data } = props; + const { id: nodeId, data, selected } = props; const { notes, isOpen } = data; const dispatch = useAppDispatch(); const handleChange = useCallback( @@ -21,7 +21,7 @@ const NotesNode = (props: NodeProps) => { ); return ( - + ) => { h: 8, }} > - - + + {isOpen && ( diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/InspectorPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/panel/InspectorPanel.tsx index 654b076eb8..587bea19ec 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panel/InspectorPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/panel/InspectorPanel.tsx @@ -6,39 +6,11 @@ import { TabPanels, Tabs, } from '@chakra-ui/react'; -import { createSelector } from '@reduxjs/toolkit'; -import { stateSelector } from 'app/store/store'; -import { useAppSelector } from 'app/store/storeHooks'; -import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -import { IAINoContentFallback } from 'common/components/IAIImageFallback'; -import ImageMetadataJSON from 'features/gallery/components/ImageMetadataViewer/ImageMetadataJSON'; import { memo } from 'react'; - -const selector = createSelector( - stateSelector, - ({ nodes }) => { - const lastSelectedNodeId = - nodes.selectedNodes[nodes.selectedNodes.length - 1]; - - const lastSelectedNode = nodes.nodes.find( - (node) => node.id === lastSelectedNodeId - ); - - const lastSelectedNodeTemplate = lastSelectedNode - ? nodes.nodeTemplates[lastSelectedNode.data.type] - : undefined; - - return { - node: lastSelectedNode, - template: lastSelectedNodeTemplate, - }; - }, - defaultSelectorOptions -); +import NodeDataInspector from './NodeDataInspector'; +import NodeTemplateInspector from './NodeTemplateInspector'; const InspectorPanel = () => { - const { node, template } = useAppSelector(selector); - return ( { - {template ? ( - - - - ) : ( - - )} + - {node ? ( - - ) : ( - - )} + diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/NodeDataInspector.tsx b/invokeai/frontend/web/src/features/nodes/components/panel/NodeDataInspector.tsx index 74b1620839..084f743d19 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panel/NodeDataInspector.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/panel/NodeDataInspector.tsx @@ -17,20 +17,20 @@ const selector = createSelector( ); return { - node: lastSelectedNode, + data: lastSelectedNode?.data, }; }, defaultSelectorOptions ); const NodeDataInspector = () => { - const { node } = useAppSelector(selector); + const { data } = useAppSelector(selector); - return node ? ( - - ) : ( - - ); + if (!data) { + return ; + } + + return ; }; export default memo(NodeDataInspector); diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/NodeTemplateInspector.tsx b/invokeai/frontend/web/src/features/nodes/components/panel/NodeTemplateInspector.tsx new file mode 100644 index 0000000000..b483158b36 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/panel/NodeTemplateInspector.tsx @@ -0,0 +1,40 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { IAINoContentFallback } from 'common/components/IAIImageFallback'; +import ImageMetadataJSON from 'features/gallery/components/ImageMetadataViewer/ImageMetadataJSON'; +import { memo } from 'react'; + +const selector = createSelector( + stateSelector, + ({ nodes }) => { + const lastSelectedNodeId = + nodes.selectedNodes[nodes.selectedNodes.length - 1]; + + const lastSelectedNode = nodes.nodes.find( + (node) => node.id === lastSelectedNodeId + ); + + const lastSelectedNodeTemplate = lastSelectedNode + ? nodes.nodeTemplates[lastSelectedNode.data.type] + : undefined; + + return { + template: lastSelectedNodeTemplate, + }; + }, + defaultSelectorOptions +); + +const NodeTemplateInspector = () => { + const { template } = useAppSelector(selector); + + if (!template) { + return ; + } + + return ; +}; + +export default memo(NodeTemplateInspector); diff --git a/invokeai/frontend/web/src/features/nodes/components/panel/workflow/LinearTab.tsx b/invokeai/frontend/web/src/features/nodes/components/panel/workflow/LinearTab.tsx index 833fcc6839..cc7428a8ec 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panel/workflow/LinearTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/panel/workflow/LinearTab.tsx @@ -6,14 +6,6 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAIDroppable from 'common/components/IAIDroppable'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { AddFieldToLinearViewDropData } from 'features/dnd/types'; -import { - InputFieldTemplate, - InputFieldValue, - InvocationNodeData, - InvocationTemplate, - isInvocationNode, -} from 'features/nodes/types/types'; -import { forEach } from 'lodash-es'; import { memo } from 'react'; import LinearViewField from '../../fields/LinearViewField'; import ScrollableContent from '../ScrollableContent'; @@ -21,41 +13,8 @@ import ScrollableContent from '../ScrollableContent'; const selector = createSelector( stateSelector, ({ nodes }) => { - const fields: { - nodeData: InvocationNodeData; - nodeTemplate: InvocationTemplate; - field: InputFieldValue; - fieldTemplate: InputFieldTemplate; - }[] = []; - const { exposedFields } = nodes.workflow; - nodes.nodes.filter(isInvocationNode).forEach((node) => { - const nodeTemplate = nodes.nodeTemplates[node.data.type]; - if (!nodeTemplate) { - return; - } - forEach(node.data.inputs, (field) => { - if ( - !exposedFields.some( - (f) => f.nodeId === node.id && f.fieldName === field.name - ) - ) { - return; - } - const fieldTemplate = nodeTemplate.inputs[field.name]; - if (!fieldTemplate) { - return; - } - fields.push({ - nodeData: node.data, - nodeTemplate, - field, - fieldTemplate, - }); - }); - }); - return { - fields, + fields: nodes.workflow.exposedFields, }; }, defaultSelectorOptions @@ -89,13 +48,11 @@ const LinearTabContent = () => { }} > {fields.length ? ( - fields.map(({ nodeData, nodeTemplate, field, fieldTemplate }) => ( + fields.map(({ nodeId, fieldName }) => ( )) ) : ( diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts index 625736a933..e2154f7391 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts @@ -2,8 +2,8 @@ import { createSelector } from '@reduxjs/toolkit'; import { stateSelector } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; import { makeConnectionErrorSelector } from 'features/nodes/store/util/makeIsConnectionValidSelector'; -import { InputFieldValue, OutputFieldValue } from 'features/nodes/types/types'; import { useMemo } from 'react'; +import { useFieldType } from './useNodeData'; const selectIsConnectionInProgress = createSelector( stateSelector, @@ -12,23 +12,19 @@ const selectIsConnectionInProgress = createSelector( nodes.connectionStartParams !== null ); -export type UseConnectionStateProps = - | { - nodeId: string; - field: InputFieldValue; - kind: 'input'; - } - | { - nodeId: string; - field: OutputFieldValue; - kind: 'output'; - }; +export type UseConnectionStateProps = { + nodeId: string; + fieldName: string; + kind: 'input' | 'output'; +}; export const useConnectionState = ({ nodeId, - field, + fieldName, kind, }: UseConnectionStateProps) => { + const fieldType = useFieldType(nodeId, fieldName, kind); + const selectIsConnected = useMemo( () => createSelector(stateSelector, ({ nodes }) => @@ -37,23 +33,23 @@ export const useConnectionState = ({ return ( (kind === 'input' ? edge.target : edge.source) === nodeId && (kind === 'input' ? edge.targetHandle : edge.sourceHandle) === - field.name + fieldName ); }).length ) ), - [field.name, kind, nodeId] + [fieldName, kind, nodeId] ); const selectConnectionError = useMemo( () => makeConnectionErrorSelector( nodeId, - field.name, + fieldName, kind === 'input' ? 'target' : 'source', - field.type + fieldType ), - [nodeId, field.name, field.type, kind] + [nodeId, fieldName, kind, fieldType] ); const selectIsConnectionStartField = useMemo( @@ -61,12 +57,12 @@ export const useConnectionState = ({ createSelector(stateSelector, ({ nodes }) => Boolean( nodes.connectionStartParams?.nodeId === nodeId && - nodes.connectionStartParams?.handleId === field.name && + nodes.connectionStartParams?.handleId === fieldName && nodes.connectionStartParams?.handleType === { input: 'target', output: 'source' }[kind] ) ), - [field.name, kind, nodeId] + [fieldName, kind, nodeId] ); const isConnected = useAppSelector(selectIsConnected); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts new file mode 100644 index 0000000000..948bdb7f3c --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts @@ -0,0 +1,289 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { stateSelector } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import { map, some } from 'lodash-es'; +import { useMemo } from 'react'; +import { + FOOTER_FIELDS, + IMAGE_FIELDS, +} from '../components/Invocation/NodeFooter'; +import { isInvocationNode } from '../types/types'; + +const KIND_MAP = { + input: 'inputs' as const, + output: 'outputs' as const, +}; + +export const useNodeTemplate = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? '']; + return nodeTemplate; + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const nodeTemplate = useAppSelector(selector); + + return nodeTemplate; +}; + +export const useNodeData = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + return node?.data; + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const nodeData = useAppSelector(selector); + + return nodeData; +}; + +export const useFieldData = (nodeId: string, fieldName: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return; + } + return node?.data.inputs[fieldName]; + }, + defaultSelectorOptions + ), + [fieldName, nodeId] + ); + + const fieldData = useAppSelector(selector); + + return fieldData; +}; + +export const useFieldType = ( + nodeId: string, + fieldName: string, + kind: 'input' | 'output' +) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return; + } + return node?.data[KIND_MAP[kind]][fieldName]?.type; + }, + defaultSelectorOptions + ), + [fieldName, kind, nodeId] + ); + + const fieldType = useAppSelector(selector); + + return fieldType; +}; + +export const useFieldNames = (nodeId: string, kind: 'input' | 'output') => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return []; + } + return map(node.data[KIND_MAP[kind]], (field) => field.name).filter( + (fieldName) => fieldName !== 'is_intermediate' + ); + }, + defaultSelectorOptions + ), + [kind, nodeId] + ); + + const fieldNames = useAppSelector(selector); + return fieldNames; +}; + +export const useWithFooter = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return false; + } + return some(node.data.outputs, (output) => + FOOTER_FIELDS.includes(output.type) + ); + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const withFooter = useAppSelector(selector); + return withFooter; +}; + +export const useHasImageOutput = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return false; + } + return some(node.data.outputs, (output) => + IMAGE_FIELDS.includes(output.type) + ); + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const hasImageOutput = useAppSelector(selector); + return hasImageOutput; +}; + +export const useIsIntermediate = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return false; + } + return Boolean(node.data.inputs.is_intermediate?.value); + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const is_intermediate = useAppSelector(selector); + return is_intermediate; +}; + +export const useNodeLabel = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return false; + } + + return node.data.label; + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const label = useAppSelector(selector); + return label; +}; + +export const useNodeTemplateTitle = (nodeId: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return false; + } + const nodeTemplate = node + ? nodes.nodeTemplates[node.data.type] + : undefined; + + return nodeTemplate?.title; + }, + defaultSelectorOptions + ), + [nodeId] + ); + + const title = useAppSelector(selector); + return title; +}; + +export const useFieldTemplate = ( + nodeId: string, + fieldName: string, + kind: 'input' | 'output' +) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return; + } + const nodeTemplate = nodes.nodeTemplates[node?.data.type ?? '']; + return nodeTemplate?.[KIND_MAP[kind]][fieldName]; + }, + defaultSelectorOptions + ), + [fieldName, kind, nodeId] + ); + + const fieldTemplate = useAppSelector(selector); + + return fieldTemplate; +}; + +export const useDoesInputHaveValue = (nodeId: string, fieldName: string) => { + const selector = useMemo( + () => + createSelector( + stateSelector, + ({ nodes }) => { + const node = nodes.nodes.find((node) => node.id === nodeId); + if (!isInvocationNode(node)) { + return; + } + return Boolean(node?.data.inputs[fieldName]?.value); + }, + defaultSelectorOptions + ), + [fieldName, nodeId] + ); + + const doesFieldHaveValue = useAppSelector(selector); + + return doesFieldHaveValue; +}; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts b/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts index 3cc3859ce0..29603036ab 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts @@ -9,9 +9,13 @@ export const makeConnectionErrorSelector = ( nodeId: string, fieldName: string, handleType: HandleType, - fieldType: FieldType + fieldType?: FieldType ) => createSelector(stateSelector, (state) => { + if (!fieldType) { + return 'No field type'; + } + const { currentConnectionFieldType, connectionStartParams, nodes, edges } = state.nodes; diff --git a/invokeai/frontend/web/src/features/nodes/types/types.ts b/invokeai/frontend/web/src/features/nodes/types/types.ts index 3846d2425c..60e4877fd8 100644 --- a/invokeai/frontend/web/src/features/nodes/types/types.ts +++ b/invokeai/frontend/web/src/features/nodes/types/types.ts @@ -457,12 +457,13 @@ export type ColorInputFieldTemplate = InputFieldTemplateBase & { }; export const isInputFieldValue = ( - field: InputFieldValue | OutputFieldValue -): field is InputFieldValue => field.fieldKind === 'input'; + field?: InputFieldValue | OutputFieldValue +): field is InputFieldValue => Boolean(field && field.fieldKind === 'input'); export const isInputFieldTemplate = ( - fieldTemplate: InputFieldTemplate | OutputFieldTemplate -): fieldTemplate is InputFieldTemplate => fieldTemplate.fieldKind === 'input'; + fieldTemplate?: InputFieldTemplate | OutputFieldTemplate +): fieldTemplate is InputFieldTemplate => + Boolean(fieldTemplate && fieldTemplate.fieldKind === 'input'); /** * JANKY CUSTOMISATION OF OpenAPI SCHEMA TYPES @@ -632,20 +633,22 @@ export type NodeData = export const isInvocationNode = ( node?: Node -): node is Node => node?.type === 'invocation'; +): node is Node => + Boolean(node && node.type === 'invocation'); export const isInvocationNodeData = ( node?: NodeData ): node is InvocationNodeData => - !['notes', 'current_image'].includes(node?.type ?? ''); + Boolean(node && !['notes', 'current_image'].includes(node.type)); export const isNotesNode = ( node?: Node -): node is Node => node?.type === 'notes'; +): node is Node => Boolean(node && node.type === 'notes'); export const isProgressImageNode = ( node?: Node -): node is Node => node?.type === 'current_image'; +): node is Node => + Boolean(node && node.type === 'current_image'); export enum NodeStatus { PENDING, From 1f194e368824030f64df97d91b453d44335e4ba3 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 16 Aug 2023 22:36:03 +1000 Subject: [PATCH 5/8] chore(ui): lint --- .../src/features/nodes/components/Invocation/NodeFooter.tsx | 3 --- .../src/features/nodes/components/Invocation/NodeHeader.tsx | 2 +- .../features/nodes/components/Invocation/NodeNotesEdit.tsx | 2 +- .../frontend/web/src/features/nodes/hooks/useNodeData.ts | 5 +---- invokeai/frontend/web/src/features/nodes/types/constants.ts | 3 +++ 5 files changed, 6 insertions(+), 9 deletions(-) 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 9f5980374d..c858872b57 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeFooter.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeFooter.tsx @@ -14,9 +14,6 @@ import { fieldBooleanValueChanged } from 'features/nodes/store/nodesSlice'; import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants'; import { ChangeEvent, memo, useCallback } from 'react'; -export const IMAGE_FIELDS = ['ImageField', 'ImageCollection']; -export const FOOTER_FIELDS = IMAGE_FIELDS; - type Props = { nodeId: string; }; 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 fa4585a445..ea503a8f27 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeHeader.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Invocation/NodeHeader.tsx @@ -14,7 +14,7 @@ type Props = { selected: boolean; }; -const NodeHeader = ({ nodeId, isOpen, label, type, selected }: Props) => { +const NodeHeader = ({ nodeId, isOpen }: Props) => { return ( { const nodeTemplate = useNodeTemplate(nodeId); if (!isInvocationNodeData(data)) { - return 'Unknown Node'; + return Unknown Node; } return ( diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts index 948bdb7f3c..231c7678ef 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts @@ -4,10 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { map, some } from 'lodash-es'; import { useMemo } from 'react'; -import { - FOOTER_FIELDS, - IMAGE_FIELDS, -} from '../components/Invocation/NodeFooter'; +import { FOOTER_FIELDS, IMAGE_FIELDS } from '../types/constants'; import { isInvocationNode } from '../types/types'; const KIND_MAP = { 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', From c2e7f627011309a3a987959ecc6bdd259814927a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 16 Aug 2023 22:39:29 +1000 Subject: [PATCH 6/8] fix(ui): do not rerender edges --- .../features/nodes/components/CustomEdges.tsx | 278 +++++++++--------- 1 file changed, 147 insertions(+), 131 deletions(-) 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, From 0d36bab6cc6c294ca2df601a8252b535b331ac8d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 16 Aug 2023 22:43:31 +1000 Subject: [PATCH 7/8] fix(ui): do not rerender top panel buttons --- .../nodes/components/NodeEditorSettings.tsx | 37 +++++++++++-------- .../nodes/components/ui/ClearGraphButton.tsx | 6 ++- .../nodes/components/ui/NodeInvokeButton.tsx | 8 ++-- .../components/ui/ReloadSchemaButton.tsx | 8 ++-- 4 files changed, 35 insertions(+), 24 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditorSettings.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditorSettings.tsx index 58e2e3564e..b942b2b3c0 100644 --- a/invokeai/frontend/web/src/features/nodes/components/NodeEditorSettings.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditorSettings.tsx @@ -15,7 +15,7 @@ import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIIconButton from 'common/components/IAIIconButton'; import IAISwitch from 'common/components/IAISwitch'; -import { ChangeEvent, useCallback } from 'react'; +import { ChangeEvent, memo, useCallback } from 'react'; import { FaCog } from 'react-icons/fa'; import { shouldAnimateEdgesChanged, @@ -23,21 +23,26 @@ import { shouldSnapToGridChanged, shouldValidateGraphChanged, } from '../store/nodesSlice'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; -const selector = createSelector(stateSelector, ({ nodes }) => { - 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/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); From a7ba142ad926764303001d914d25849deba660c1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 16 Aug 2023 22:55:35 +1000 Subject: [PATCH 8/8] feat(ui): set min zoom on nodes to 0.1 --- invokeai/frontend/web/src/features/nodes/components/Flow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx index 5b33bf4a9c..3290a65054 100644 --- a/invokeai/frontend/web/src/features/nodes/components/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/Flow.tsx @@ -137,7 +137,7 @@ export const Flow = () => { connectionLineComponent={CustomConnectionLine} onSelectionChange={handleSelectionChange} isValidConnection={isValidConnection} - minZoom={0.2} + minZoom={0.1} snapToGrid={shouldSnapToGrid} snapGrid={[25, 25]} connectionRadius={30}