From 21dddbcb7f474d88018295d7150c9be238dd81a6 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 | 42 +++++++++---------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/invokeai/app/invocations/primitives.py b/invokeai/app/invocations/primitives.py index 398be04738e..61ef9fa27c0 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 @@ def invoke(self, context: InvocationContext) -> BooleanOutput: 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 @@ def invoke(self, context: InvocationContext) -> IntegerOutput: 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 @@ def invoke(self, context: InvocationContext) -> FloatOutput: 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 @@ def invoke(self, context: InvocationContext) -> StringOutput: 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 @@ def invoke(self, context: InvocationContext) -> ImageOutput: ) -@title("Image Collection") +@title("Image Primitive Collection") @tags("primitives", "image", "collection") class ImageCollectionInvocation(BaseInvocation): """A collection of image primitive values""" @@ -357,7 +357,7 @@ def invoke(self, context: InvocationContext) -> LatentsOutput: 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 @@ def invoke(self, context: InvocationContext) -> ConditioningOutput: 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 bd93b2f952b..316ee0c085a 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: { @@ -6194,35 +6194,35 @@ export type components = { ui_type?: components["schemas"]["UIType"]; }; /** - * StableDiffusion2ModelFormat + * StableDiffusionOnnxModelFormat * @description An enumeration. * @enum {string} */ - StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; + StableDiffusionOnnxModelFormat: "olive" | "onnx"; /** - * ControlNetModelFormat + * StableDiffusion1ModelFormat * @description An enumeration. * @enum {string} */ - ControlNetModelFormat: "checkpoint" | "diffusers"; + StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; /** - * StableDiffusionXLModelFormat + * ControlNetModelFormat * @description An enumeration. * @enum {string} */ - StableDiffusionXLModelFormat: "checkpoint" | "diffusers"; + ControlNetModelFormat: "checkpoint" | "diffusers"; /** - * StableDiffusionOnnxModelFormat + * StableDiffusion2ModelFormat * @description An enumeration. * @enum {string} */ - StableDiffusionOnnxModelFormat: "olive" | "onnx"; + StableDiffusion2ModelFormat: "checkpoint" | "diffusers"; /** - * StableDiffusion1ModelFormat + * StableDiffusionXLModelFormat * @description An enumeration. * @enum {string} */ - StableDiffusion1ModelFormat: "checkpoint" | "diffusers"; + StableDiffusionXLModelFormat: "checkpoint" | "diffusers"; }; responses: never; parameters: never; From 385440baa111b1a3ceae29e90ce6428ff58642c9 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 00000000000..757faca8661 --- /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 403a6cd5c5d..aeeb3677cc2 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 ce8b4450a69..0c13b37f0c4 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 8234a6a7fad..5b33bf4a9c3 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 b485d71bddb..07fefd95e86 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 e487f080672..ca35eab300d 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 71c83b1630a..08c75a12f58 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 2651020120015b907c29f1016d0091442910919f 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 6920a2053b6..5e610cfc394 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 | 164 +++++----- .../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, 863 insertions(+), 671 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 a86b52060b4..6c610d7f345 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 d67ca10dcc6..2648e68607d 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 ece24f6f8c1..32dd554ef49 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 38c2001b99c..9f5980374d7 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 a946d215813..fa4585a4454 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 ab54ca2c44d..e6f89fdf738 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 6695c4fd3b3..d53fec4b427 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 fa6a8ea224e..d816f3cea16 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 a16c6960ece..664a788b5a8 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 fc239addf38..e9a49989f6c 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 bf5cd3cd9bb..cbe75ca5806 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 67f4369384a..47033baa7b3 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 0eae336a1ef..acec921d8e3 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,45 @@ 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') { + if ( + field?.type === 'LatentsField' && + fieldTemplate?.type === 'LatentsField' + ) { return ( @@ -110,68 +107,68 @@ const InputFieldRenderer = (props: InputFieldProps) => { } if ( - type === 'ConditioningField' && - fieldTemplate.type === 'ConditioningField' + 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') { + if ( + field?.type === 'ControlField' && + fieldTemplate?.type === 'ControlField' + ) { return ( ); } - if (type === 'MainModelField' && fieldTemplate.type === 'MainModelField') { + if ( + field?.type === 'MainModelField' && + fieldTemplate?.type === 'MainModelField' + ) { return ( @@ -179,35 +176,38 @@ const InputFieldRenderer = (props: InputFieldProps) => { } if ( - type === 'SDXLRefinerModelField' && - fieldTemplate.type === 'SDXLRefinerModelField' + field?.type === 'SDXLRefinerModelField' && + fieldTemplate?.type === 'SDXLRefinerModelField' ) { return ( ); } - if (type === 'VaeModelField' && fieldTemplate.type === 'VaeModelField') { + if ( + field?.type === 'VaeModelField' && + fieldTemplate?.type === 'VaeModelField' + ) { return ( ); } - if (type === 'LoRAModelField' && fieldTemplate.type === 'LoRAModelField') { + if ( + field?.type === 'LoRAModelField' && + fieldTemplate?.type === 'LoRAModelField' + ) { return ( @@ -215,57 +215,58 @@ const InputFieldRenderer = (props: InputFieldProps) => { } if ( - type === 'ControlNetModelField' && - fieldTemplate.type === 'ControlNetModelField' + 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') { + if ( + field?.type === 'CollectionItem' && + fieldTemplate?.type === 'CollectionItem' + ) { return ( ); } - if (type === 'ColorField' && fieldTemplate.type === 'ColorField') { + if (field?.type === 'ColorField' && fieldTemplate?.type === 'ColorField') { return ( ); } - if (type === 'ImageCollection' && fieldTemplate.type === 'ImageCollection') { + if ( + field?.type === 'ImageCollection' && + fieldTemplate?.type === 'ImageCollection' + ) { return ( @@ -273,20 +274,19 @@ const InputFieldRenderer = (props: InputFieldProps) => { } if ( - type === 'SDXLMainModelField' && - fieldTemplate.type === 'SDXLMainModelField' + field?.type === 'SDXLMainModelField' && + fieldTemplate?.type === 'SDXLMainModelField' ) { return ( ); } - return Unknown field type: {type}; + 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 98a8000b1a3..ea4bb76d62a 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 5a29d1ab7ea..2a257d741eb 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' }); - - const fieldTemplate = useMemo( - () => nodeTemplate.outputs[field.name], - [field.name, nodeTemplate] - ); + } = useConnectionState({ nodeId, fieldName, kind: 'output' }); - 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 00a2d2bd106..daf2f598ba4 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 c4a4d19a1ee..422c3ba48fb 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 f955d6f0021..492ec51d20a 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 210a83b6acd..ebf35935260 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 1ca820939b0..4efd0b7775a 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 f9f9c404d72..0391136dba1 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 8aae6ee9a41..4f8347bbe87 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 f1047f52cb2..681a597235f 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 907f90130d0..df5c3f763e3 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 4a419b51d61..0eec884de0b 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 89bd6b2b651..e904aad2464 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 8cc0cf774f2..c172e928d0f 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 a8f6a24de44..5dd639cf5cb 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 b1d14c90185..5a5e3a9dcf0 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 04e51159c60..985978f72da 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 c3b035c6f3c..7a46c119014 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 654b076eb89..587bea19ecb 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 74b16208392..084f743d19c 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 00000000000..b483158b36f --- /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 833fcc68395..cc7428a8ec8 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 625736a9332..e2154f73919 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 00000000000..948bdb7f3cc --- /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 3cc3859ce0a..29603036ab9 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 3846d2425cc..60e4877fd80 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 cc3dec05f1c1e3677a3739fa4ef675a662b1cd7e 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 9f5980374d7..c858872b572 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 fa4585a4454..ea503a8f272 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 948bdb7f3cc..231c7678ef1 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 0efd5292753..1c5c89ff2de 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 80d3f52d8ca4cc823f309f1b834c7b8b3e07d4f2 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 | 284 +++++++++--------- 1 file changed, 150 insertions(+), 134 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/CustomEdges.tsx b/invokeai/frontend/web/src/features/nodes/components/CustomEdges.tsx index e0ccc6e3239..f80f0451e4b 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); - - const isInvocationToInvocationEdge = - isInvocationNode(sourceNode) && isInvocationNode(targetNode); - - 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'); - - 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] - ); + 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 isSelected = + sourceNode?.selected || targetNode?.selected || selected; + const sourceType = isInvocationToInvocationEdge + ? sourceNode?.data?.outputs[sourceHandleId || '']?.type + : undefined; - const { isSelected, shouldAnimate } = useAppSelector(selector); + const stroke = + sourceType && nodes.shouldColorEdges + ? colorTokenToCssVar(FIELDS[sourceType].color) + : colorTokenToCssVar('base.500'); + + return { + isSelected, + shouldAnimate: nodes.shouldAnimateEdges && isSelected, + stroke, + }; + }, + defaultSelectorOptions + ); - const [edgePath, labelX, labelY] = getBezierPath({ +const CollapsedEdge = memo( + ({ 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] + ); + + const { isSelected, shouldAnimate } = useAppSelector(selector); + + 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 { base500 } = useChakraThemeTokens(); + const [edgePath] = getBezierPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }); - return ( - <> + 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 a5a47c34aa64874ab3581554e7ed907928690a06 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 58e2e3564e9..b942b2b3c05 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 432675c5cdb..1501d0270b4 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 f6c837e0449..cbb0ea58ee4 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 5d2c84fe011fa9f4fb884f6aa6f2cdf56377fe99 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 5b33bf4a9c3..3290a650547 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}