Skip to content

Commit e653837

Browse files
fix(ui): add separate wrapper components for notes and current image nodes that do not need invocation node context
1 parent 2bbfcc2 commit e653837

File tree

6 files changed

+261
-103
lines changed

6 files changed

+261
-103
lines changed

invokeai/frontend/web/src/features/nodes/components/flow/nodes/CurrentImage/CurrentImageNode.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { IAINoContentFallback } from 'common/components/IAIImageFallback';
66
import { DndImage } from 'features/dnd/DndImage';
77
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
88
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
9-
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
9+
import NonInvocationNodeWrapper from 'features/nodes/components/flow/nodes/common/NonInvocationNodeWrapper';
1010
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
1111
import type { AnimationProps } from 'framer-motion';
1212
import { motion } from 'framer-motion';
@@ -58,13 +58,14 @@ const Wrapper = (props: PropsWithChildren<{ nodeProps: NodeProps }>) => {
5858
}, []);
5959
const { t } = useTranslation();
6060
return (
61-
<NodeWrapper nodeId={props.nodeProps.id} selected={props.nodeProps.selected} width={384}>
61+
<NonInvocationNodeWrapper nodeId={props.nodeProps.id} selected={props.nodeProps.selected} width={384}>
6262
<Flex
6363
onMouseEnter={handleMouseEnter}
6464
onMouseLeave={handleMouseLeave}
6565
className={DRAG_HANDLE_CLASSNAME}
6666
position="relative"
6767
flexDirection="column"
68+
aspectRatio="1/1"
6869
>
6970
<Flex layerStyle="nodeHeader" borderTopRadius="base" alignItems="center" justifyContent="center" h={8}>
7071
<Text fontSize="sm" fontWeight="semibold" color="base.200">
@@ -80,7 +81,7 @@ const Wrapper = (props: PropsWithChildren<{ nodeProps: NodeProps }>) => {
8081
)}
8182
</Flex>
8283
</Flex>
83-
</NodeWrapper>
84+
</NonInvocationNodeWrapper>
8485
);
8586
};
8687

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Notes/NotesNode.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { createSelector } from '@reduxjs/toolkit';
33
import type { Node, NodeProps } from '@xyflow/react';
44
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
55
import NodeCollapseButton from 'features/nodes/components/flow/nodes/common/NodeCollapseButton';
6-
import NodeTitle from 'features/nodes/components/flow/nodes/common/NodeTitle';
7-
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
6+
import NonInvocationNodeTitle from 'features/nodes/components/flow/nodes/common/NonInvocationNodeTitle';
7+
import NonInvocationNodeWrapper from 'features/nodes/components/flow/nodes/common/NonInvocationNodeWrapper';
88
import { notesNodeValueChanged } from 'features/nodes/store/nodesSlice';
99
import { selectNodes } from 'features/nodes/store/selectors';
1010
import { NO_DRAG_CLASS, NO_PAN_CLASS } from 'features/nodes/types/constants';
@@ -34,7 +34,7 @@ const NotesNode = (props: NodeProps<Node<NotesNodeData>>) => {
3434
}
3535

3636
return (
37-
<NodeWrapper nodeId={nodeId} selected={selected}>
37+
<NonInvocationNodeWrapper nodeId={nodeId} selected={selected}>
3838
<Flex
3939
layerStyle="nodeHeader"
4040
borderTopRadius="base"
@@ -44,7 +44,7 @@ const NotesNode = (props: NodeProps<Node<NotesNodeData>>) => {
4444
h={8}
4545
>
4646
<NodeCollapseButton nodeId={nodeId} isOpen={isOpen} />
47-
<NodeTitle nodeId={nodeId} title="Notes" />
47+
<NonInvocationNodeTitle nodeId={nodeId} title="Notes" />
4848
<Box minW={8} />
4949
</Flex>
5050
{isOpen && (
@@ -73,7 +73,7 @@ const NotesNode = (props: NodeProps<Node<NotesNodeData>>) => {
7373
</Flex>
7474
</>
7575
)}
76-
</NodeWrapper>
76+
</NonInvocationNodeWrapper>
7777
);
7878
};
7979

invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx

Lines changed: 3 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ChakraProps, SystemStyleObject } from '@invoke-ai/ui-library';
1+
import type { ChakraProps } from '@invoke-ai/ui-library';
22
import { Box, useGlobalMenuClose } from '@invoke-ai/ui-library';
33
import { useAppSelector } from 'app/store/storeHooks';
44
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
@@ -12,107 +12,15 @@ import { zNodeStatus } from 'features/nodes/types/invocation';
1212
import type { MouseEvent, PropsWithChildren } from 'react';
1313
import { memo, useCallback } from 'react';
1414

15+
import { containerSx, inProgressSx, shadowsSx } from './shared';
16+
1517
type NodeWrapperProps = PropsWithChildren & {
1618
nodeId: string;
1719
selected: boolean;
1820
width?: ChakraProps['w'];
1921
isMissingTemplate?: boolean;
2022
};
2123

22-
// Certain CSS transitions are disabled as a performance optimization - they can cause massive slowdowns in large
23-
// workflows even when the animations are GPU-accelerated CSS.
24-
25-
const containerSx: SystemStyleObject = {
26-
h: 'full',
27-
position: 'relative',
28-
borderRadius: 'base',
29-
transitionProperty: 'none',
30-
cursor: 'grab',
31-
'--border-color': 'var(--invoke-colors-base-500)',
32-
'--border-color-selected': 'var(--invoke-colors-blue-300)',
33-
'--header-bg-color': 'var(--invoke-colors-base-900)',
34-
'&[data-status="warning"]': {
35-
'--border-color': 'var(--invoke-colors-warning-500)',
36-
'--border-color-selected': 'var(--invoke-colors-warning-500)',
37-
'--header-bg-color': 'var(--invoke-colors-warning-700)',
38-
},
39-
'&[data-status="error"]': {
40-
'--border-color': 'var(--invoke-colors-error-500)',
41-
'--border-color-selected': 'var(--invoke-colors-error-500)',
42-
'--header-bg-color': 'var(--invoke-colors-error-700)',
43-
},
44-
// The action buttons are hidden by default and shown on hover
45-
'& .node-selection-overlay': {
46-
display: 'block',
47-
position: 'absolute',
48-
top: 0,
49-
insetInlineEnd: 0,
50-
bottom: 0,
51-
insetInlineStart: 0,
52-
borderRadius: 'base',
53-
transitionProperty: 'none',
54-
pointerEvents: 'none',
55-
shadow: '0 0 0 1px var(--border-color)',
56-
},
57-
'&[data-is-mouse-over-node="true"] .node-selection-overlay': {
58-
display: 'block',
59-
},
60-
'&[data-is-mouse-over-form-field="true"] .node-selection-overlay': {
61-
display: 'block',
62-
bg: 'invokeBlueAlpha.100',
63-
},
64-
_hover: {
65-
'& .node-selection-overlay': {
66-
display: 'block',
67-
shadow: '0 0 0 1px var(--border-color-selected)',
68-
},
69-
'&[data-is-selected="true"] .node-selection-overlay': {
70-
display: 'block',
71-
shadow: '0 0 0 2px var(--border-color-selected)',
72-
},
73-
},
74-
'&[data-is-selected="true"] .node-selection-overlay': {
75-
display: 'block',
76-
shadow: '0 0 0 2px var(--border-color-selected)',
77-
},
78-
'&[data-is-editor-locked="true"]': {
79-
'& *': {
80-
cursor: 'not-allowed',
81-
pointerEvents: 'none',
82-
},
83-
},
84-
};
85-
86-
const shadowsSx: SystemStyleObject = {
87-
position: 'absolute',
88-
top: 0,
89-
insetInlineEnd: 0,
90-
bottom: 0,
91-
insetInlineStart: 0,
92-
borderRadius: 'base',
93-
pointerEvents: 'none',
94-
zIndex: -1,
95-
shadow: 'var(--invoke-shadows-xl), var(--invoke-shadows-base), var(--invoke-shadows-base)',
96-
};
97-
98-
const inProgressSx: SystemStyleObject = {
99-
position: 'absolute',
100-
top: 0,
101-
insetInlineEnd: 0,
102-
bottom: 0,
103-
insetInlineStart: 0,
104-
borderRadius: 'md',
105-
pointerEvents: 'none',
106-
transitionProperty: 'none',
107-
opacity: 0.7,
108-
zIndex: -1,
109-
display: 'none',
110-
shadow: '0 0 0 2px var(--invoke-colors-yellow-400), 0 0 20px 2px var(--invoke-colors-orange-700)',
111-
'&[data-is-in-progress="true"]': {
112-
display: 'block',
113-
},
114-
};
115-
11624
const NodeWrapper = (props: NodeWrapperProps) => {
11725
const { nodeId, width, children, isMissingTemplate, selected } = props;
11826
const ctx = useInvocationNodeContext();
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Flex, Input, Text } from '@invoke-ai/ui-library';
2+
import { createSelector } from '@reduxjs/toolkit';
3+
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
4+
import { useEditable } from 'common/hooks/useEditable';
5+
import { nodeLabelChanged } from 'features/nodes/store/nodesSlice';
6+
import { selectNodes } from 'features/nodes/store/selectors';
7+
import { NO_FIT_ON_DOUBLE_CLICK_CLASS } from 'features/nodes/types/constants';
8+
import { memo, useCallback, useMemo, useRef } from 'react';
9+
import { useTranslation } from 'react-i18next';
10+
11+
type Props = {
12+
nodeId: string;
13+
title: string;
14+
};
15+
16+
const NonInvocationNodeTitle = ({ nodeId, title }: Props) => {
17+
const dispatch = useAppDispatch();
18+
const selectNodeLabel = useMemo(
19+
() =>
20+
createSelector(selectNodes, (nodes) => {
21+
const node = nodes.find((n) => n.id === nodeId);
22+
return node?.data?.label ?? '';
23+
}),
24+
[nodeId]
25+
);
26+
const label = useAppSelector(selectNodeLabel);
27+
const { t } = useTranslation();
28+
const inputRef = useRef<HTMLInputElement>(null);
29+
30+
const onChange = useCallback(
31+
(label: string) => {
32+
dispatch(nodeLabelChanged({ nodeId, label }));
33+
},
34+
[dispatch, nodeId]
35+
);
36+
37+
const editable = useEditable({
38+
value: label || title || t('nodes.problemSettingTitle'),
39+
defaultValue: title || t('nodes.problemSettingTitle'),
40+
onChange,
41+
inputRef,
42+
});
43+
44+
return (
45+
<Flex overflow="hidden" w="full" h="full" alignItems="center" justifyContent="center">
46+
{!editable.isEditing && (
47+
<Text
48+
className={NO_FIT_ON_DOUBLE_CLICK_CLASS}
49+
fontWeight="semibold"
50+
color="base.200"
51+
onDoubleClick={editable.startEditing}
52+
noOfLines={1}
53+
>
54+
{editable.value}
55+
</Text>
56+
)}
57+
{editable.isEditing && (
58+
<Input
59+
ref={inputRef}
60+
{...editable.inputProps}
61+
variant="outline"
62+
_focusVisible={{ borderRadius: 'base', h: 'unset' }}
63+
/>
64+
)}
65+
</Flex>
66+
);
67+
};
68+
69+
export default memo(NonInvocationNodeTitle);
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type { ChakraProps } from '@invoke-ai/ui-library';
2+
import { Box, useGlobalMenuClose } from '@invoke-ai/ui-library';
3+
import { useAppSelector } from 'app/store/storeHooks';
4+
import { useIsWorkflowEditorLocked } from 'features/nodes/hooks/useIsWorkflowEditorLocked';
5+
import { useMouseOverFormField, useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
6+
import { useNodeExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
7+
import { useZoomToNode } from 'features/nodes/hooks/useZoomToNode';
8+
import { selectNodeOpacity } from 'features/nodes/store/workflowSettingsSlice';
9+
import { DRAG_HANDLE_CLASSNAME, NO_FIT_ON_DOUBLE_CLICK_CLASS, NODE_WIDTH } from 'features/nodes/types/constants';
10+
import { zNodeStatus } from 'features/nodes/types/invocation';
11+
import type { MouseEvent, PropsWithChildren } from 'react';
12+
import { memo, useCallback } from 'react';
13+
14+
import { containerSx, inProgressSx, shadowsSx } from './shared';
15+
16+
type NonInvocationNodeWrapperProps = PropsWithChildren & {
17+
nodeId: string;
18+
selected: boolean;
19+
width?: ChakraProps['w'];
20+
isMissingTemplate?: boolean;
21+
};
22+
23+
const NonInvocationNodeWrapper = (props: NonInvocationNodeWrapperProps) => {
24+
const { nodeId, width, children, isMissingTemplate, selected } = props;
25+
// Skip needsUpdate check since we don't have invocation context
26+
const mouseOverNode = useMouseOverNode(nodeId);
27+
const mouseOverFormField = useMouseOverFormField(nodeId);
28+
const zoomToNode = useZoomToNode(nodeId);
29+
const isLocked = useIsWorkflowEditorLocked();
30+
31+
const executionState = useNodeExecutionState(nodeId);
32+
const isInProgress = executionState?.status === zNodeStatus.enum.IN_PROGRESS;
33+
34+
const opacity = useAppSelector(selectNodeOpacity);
35+
const globalMenu = useGlobalMenuClose();
36+
37+
const onDoubleClick = useCallback(
38+
(e: MouseEvent) => {
39+
if (!(e.target instanceof HTMLElement)) {
40+
// We have to manually narrow the type here thanks to a TS quirk
41+
return;
42+
}
43+
if (
44+
e.target instanceof HTMLInputElement ||
45+
e.target instanceof HTMLTextAreaElement ||
46+
e.target instanceof HTMLSelectElement ||
47+
e.target instanceof HTMLButtonElement ||
48+
e.target instanceof HTMLAnchorElement
49+
) {
50+
// Don't fit the view if the user is editing a text field, select, button, or link
51+
return;
52+
}
53+
if (e.target.closest(`.${NO_FIT_ON_DOUBLE_CLICK_CLASS}`) !== null) {
54+
// This target is marked as not fitting the view on double click
55+
return;
56+
}
57+
zoomToNode();
58+
},
59+
[zoomToNode]
60+
);
61+
62+
return (
63+
<Box
64+
onClick={globalMenu.onCloseGlobal}
65+
onDoubleClick={onDoubleClick}
66+
onMouseOver={mouseOverNode.handleMouseOver}
67+
onMouseOut={mouseOverNode.handleMouseOut}
68+
className={DRAG_HANDLE_CLASSNAME}
69+
sx={containerSx}
70+
width={width || NODE_WIDTH}
71+
opacity={opacity}
72+
data-is-editor-locked={isLocked}
73+
data-is-selected={selected}
74+
data-is-mouse-over-form-field={mouseOverFormField.isMouseOverFormField}
75+
data-status={isMissingTemplate ? 'error' : undefined}
76+
>
77+
<Box sx={shadowsSx} />
78+
<Box sx={inProgressSx} data-is-in-progress={isInProgress} />
79+
{children}
80+
<Box className="node-selection-overlay" />
81+
</Box>
82+
);
83+
};
84+
85+
export default memo(NonInvocationNodeWrapper);

0 commit comments

Comments
 (0)