Skip to content

Commit 1a24396

Browse files
feat(ui): styling when nodes have error
1 parent d97e73a commit 1a24396

File tree

9 files changed

+85
-54
lines changed

9 files changed

+85
-54
lines changed

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNode.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { memo } from 'react';
1515
import { InputFieldEditModeNodes } from './fields/InputFieldEditModeNodes';
1616
import InvocationNodeFooter from './InvocationNodeFooter';
1717
import InvocationNodeHeader from './InvocationNodeHeader';
18+
import { useInvocationNodeContext } from './context';
19+
import { useAppSelector } from 'app/store/storeHooks';
1820

1921
type Props = {
2022
nodeId: string;
@@ -37,7 +39,9 @@ const sx: SystemStyleObject = {
3739
};
3840

3941
const InvocationNode = ({ nodeId, isOpen }: Props) => {
42+
const ctx = useInvocationNodeContext();
4043
const withFooter = useWithFooter();
44+
const needsUpdate = useAppSelector(ctx.selectNodeNeedsUpdate);
4145

4246
return (
4347
<>

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeHeader.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Flex } from '@invoke-ai/ui-library';
33
import NodeCollapseButton from 'features/nodes/components/flow/nodes/common/NodeCollapseButton';
44
import NodeTitle from 'features/nodes/components/flow/nodes/common/NodeTitle';
55
import InvocationNodeClassificationIcon from 'features/nodes/components/flow/nodes/Invocation/InvocationNodeClassificationIcon';
6-
import { useNodeIsInvalid } from 'features/nodes/hooks/useNodeIsInvalid';
6+
import { useNodeHasErrors } from 'features/nodes/hooks/useNodeIsInvalid';
77
import { memo } from 'react';
88

99
import InvocationNodeCollapsedHandles from './InvocationNodeCollapsedHandles';
@@ -16,26 +16,23 @@ type Props = {
1616
};
1717

1818
const sx: SystemStyleObject = {
19+
bg: 'var(--header-bg-color)',
1920
borderTopRadius: 'base',
2021
alignItems: 'center',
2122
justifyContent: 'space-between',
2223
h: 8,
2324
textAlign: 'center',
24-
color: 'base.200',
2525
borderBottomRadius: 'base',
2626
'&[data-is-open="true"]': {
2727
borderBottomRadius: 0,
2828
},
29-
'&[data-is-invalid="true"]': {
30-
color: 'error.300',
31-
},
3229
};
3330

3431
const InvocationNodeHeader = ({ nodeId, isOpen }: Props) => {
35-
const isInvalid = useNodeIsInvalid();
32+
const isInvalid = useNodeHasErrors();
3633

3734
return (
38-
<Flex layerStyle="nodeHeader" sx={sx} data-is-open={isOpen} data-is-invalid={isInvalid}>
35+
<Flex sx={sx} data-is-open={isOpen} data-is-invalid={isInvalid}>
3936
<NodeCollapseButton nodeId={nodeId} isOpen={isOpen} />
4037
<InvocationNodeClassificationIcon nodeId={nodeId} />
4138
<NodeTitle nodeId={nodeId} />

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeInfoIcon.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,9 @@ interface Props {
1414
}
1515

1616
export const InvocationNodeInfoIcon = memo(({ nodeId }: Props) => {
17-
const needsUpdate = useNodeNeedsUpdate();
18-
1917
return (
2018
<Tooltip label={<TooltipContent nodeId={nodeId} />} placement="top" shouldWrapChildren>
21-
<Icon as={PiInfoBold} display="block" boxSize={4} w={8} color={needsUpdate ? 'error.400' : 'base.400'} />
19+
<Icon as={PiInfoBold} display="block" boxSize={4} w={8} />
2220
</Tooltip>
2321
);
2422
});

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeUnknownFallback.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ const InvocationNodeUnknownFallback = ({ nodeId, isOpen, label, type }: Props) =
2626
h={8}
2727
fontWeight="semibold"
2828
fontSize="sm"
29+
bg="error.700"
2930
>
3031
<NodeCollapseButton nodeId={nodeId} isOpen={isOpen} />
31-
<Text w="full" textAlign="center" pe={8} color="error.300">
32+
<Text w="full" textAlign="center" pe={8}>
3233
{label ? `${label} (${type})` : type}
3334
</Text>
3435
</Flex>

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { InvocationNodeData } from 'features/nodes/types/invocation';
1010
import { memo, useMemo } from 'react';
1111

1212
import InvocationNodeUnknownFallback from './InvocationNodeUnknownFallback';
13+
import { InvocationNodeContextProvider } from './context';
1314

1415
const InvocationNodeWrapper = (props: NodeProps<Node<InvocationNodeData>>) => {
1516
const { data, selected } = props;
@@ -28,16 +29,20 @@ const InvocationNodeWrapper = (props: NodeProps<Node<InvocationNodeData>>) => {
2829

2930
if (!hasTemplate) {
3031
return (
31-
<NodeWrapper nodeId={nodeId} selected={selected}>
32-
<InvocationNodeUnknownFallback nodeId={nodeId} isOpen={isOpen} label={label} type={type} />
33-
</NodeWrapper>
32+
<InvocationNodeContextProvider nodeId={nodeId}>
33+
<NodeWrapper nodeId={nodeId} selected={selected} isMissingTemplate>
34+
<InvocationNodeUnknownFallback nodeId={nodeId} isOpen={isOpen} label={label} type={type} />
35+
</NodeWrapper>
36+
</InvocationNodeContextProvider>
3437
);
3538
}
3639

3740
return (
38-
<NodeWrapper nodeId={nodeId} selected={selected}>
39-
<InvocationNode nodeId={nodeId} isOpen={isOpen} />
40-
</NodeWrapper>
41+
<InvocationNodeContextProvider nodeId={nodeId}>
42+
<NodeWrapper nodeId={nodeId} selected={selected}>
43+
<InvocationNode nodeId={nodeId} isOpen={isOpen} />
44+
</NodeWrapper>
45+
</InvocationNodeContextProvider>
4146
);
4247
};
4348

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/context.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { RootState } from 'app/store/store';
55
import { $templates } from 'features/nodes/store/nodesSlice';
66
import { selectEdges, selectNodes } from 'features/nodes/store/selectors';
77
import type { InvocationNode, InvocationTemplate } from 'features/nodes/types/invocation';
8+
import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate';
89
import type { PropsWithChildren } from 'react';
910
import { createContext, memo, useContext, useMemo } from 'react';
1011

@@ -42,21 +43,18 @@ type InvocationNodeContextValue = {
4243
) => Selector<RootState, InvocationTemplate['outputs'][string]>;
4344

4445
buildSelectIsInputFieldConnected: (fieldName: string) => Selector<RootState, boolean>;
46+
selectNodeNeedsUpdate: Selector<RootState, boolean>;
4547
};
4648

4749
const InvocationNodeContext = createContext<InvocationNodeContextValue | null>(null);
4850

49-
const getSelectorFromCache = <T,>(
50-
cache: Map<string, Selector<RootState, T>>,
51-
key: string,
52-
fallback: () => Selector<RootState, T>
53-
): Selector<RootState, T> => {
51+
const getSelectorFromCache = <T extends Selector>(cache: Map<string, Selector>, key: string, fallback: () => T): T => {
5452
let selector = cache.get(key);
5553
if (!selector) {
5654
selector = fallback();
5755
cache.set(key, selector);
5856
}
59-
return selector;
57+
return selector as T;
6058
};
6159

6260
export const InvocationNodeContextProvider = memo(({ nodeId, children }: PropsWithChildren<{ nodeId: string }>) => {
@@ -183,6 +181,15 @@ export const InvocationNodeContextProvider = memo(({ nodeId, children }: PropsWi
183181
})
184182
);
185183

184+
const selectNodeNeedsUpdate = getSelectorFromCache(cache, 'selectNodeNeedsUpdate', () =>
185+
createSelector([selectNodeDataSafe, selectNodeTemplateSafe], (data, template) => {
186+
if (!data || !template) {
187+
return false; // If there's no data or template, no update is possible
188+
}
189+
return getNeedsUpdate(data, template);
190+
})
191+
);
192+
186193
return {
187194
nodeId,
188195

@@ -207,6 +214,7 @@ export const InvocationNodeContextProvider = memo(({ nodeId, children }: PropsWi
207214
buildSelectOutputFieldTemplateOrThrow,
208215

209216
buildSelectIsInputFieldConnected,
217+
selectNodeNeedsUpdate,
210218
} satisfies InvocationNodeContextValue;
211219
}, [nodeId, templates]);
212220

invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ const Fallback = memo(
6969

7070
return (
7171
<InputFieldWrapper>
72-
<Flex w="full" px={1} py={1} justifyContent="center">
73-
<Text fontWeight="semibold" color="error.300" whiteSpace="pre" textAlign="center">
72+
<Flex w="full" px={1} py={1}>
73+
<Text fontWeight="semibold" color="error.300" whiteSpace="pre">
7474
{label}
7575
</Text>
7676
</Flex>

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

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import type { ChakraProps, SystemStyleObject } from '@invoke-ai/ui-library';
22
import { Box, useGlobalMenuClose } from '@invoke-ai/ui-library';
33
import { useAppSelector } from 'app/store/storeHooks';
4-
import { InvocationNodeContextProvider } from 'features/nodes/components/flow/nodes/Invocation/context';
4+
import {
5+
InvocationNodeContextProvider,
6+
useInvocationNodeContext,
7+
} from 'features/nodes/components/flow/nodes/Invocation/context';
58
import { useIsWorkflowEditorLocked } from 'features/nodes/hooks/useIsWorkflowEditorLocked';
69
import { useMouseOverFormField, useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
710
import { useNodeExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
@@ -16,6 +19,7 @@ type NodeWrapperProps = PropsWithChildren & {
1619
nodeId: string;
1720
selected: boolean;
1821
width?: ChakraProps['w'];
22+
isMissingTemplate?: boolean;
1923
};
2024

2125
// Certain CSS transitions are disabled as a performance optimization - they can cause massive slowdowns in large
@@ -27,6 +31,19 @@ const containerSx: SystemStyleObject = {
2731
borderRadius: 'base',
2832
transitionProperty: 'none',
2933
cursor: 'grab',
34+
'--border-color': 'var(--invoke-colors-base-500)',
35+
'--border-color-selected': 'var(--invoke-colors-blue-300)',
36+
'--header-bg-color': 'var(--invoke-colors-base-900)',
37+
'&[data-status="warning"]': {
38+
'--border-color': 'var(--invoke-colors-warning-500)',
39+
'--border-color-selected': 'var(--invoke-colors-warning-500)',
40+
'--header-bg-color': 'var(--invoke-colors-warning-700)',
41+
},
42+
'&[data-status="error"]': {
43+
'--border-color': 'var(--invoke-colors-error-500)',
44+
'--border-color-selected': 'var(--invoke-colors-error-500)',
45+
'--header-bg-color': 'var(--invoke-colors-error-700)',
46+
},
3047
// The action buttons are hidden by default and shown on hover
3148
'& .node-selection-overlay': {
3249
display: 'block',
@@ -38,7 +55,7 @@ const containerSx: SystemStyleObject = {
3855
borderRadius: 'base',
3956
transitionProperty: 'none',
4057
pointerEvents: 'none',
41-
shadow: '0 0 0 1px var(--invoke-colors-base-500)',
58+
shadow: '0 0 0 1px var(--border-color)',
4259
},
4360
'&[data-is-mouse-over-node="true"] .node-selection-overlay': {
4461
display: 'block',
@@ -50,16 +67,16 @@ const containerSx: SystemStyleObject = {
5067
_hover: {
5168
'& .node-selection-overlay': {
5269
display: 'block',
53-
shadow: '0 0 0 1px var(--invoke-colors-blue-300)',
70+
shadow: '0 0 0 1px var(--border-color-selected)',
5471
},
5572
'&[data-is-selected="true"] .node-selection-overlay': {
5673
display: 'block',
57-
shadow: '0 0 0 2px var(--invoke-colors-blue-300)',
74+
shadow: '0 0 0 2px var(--border-color-selected)',
5875
},
5976
},
6077
'&[data-is-selected="true"] .node-selection-overlay': {
6178
display: 'block',
62-
shadow: '0 0 0 2px var(--invoke-colors-blue-300)',
79+
shadow: '0 0 0 2px var(--border-color-selected)',
6380
},
6481
'&[data-is-editor-locked="true"]': {
6582
'& *': {
@@ -100,7 +117,9 @@ const inProgressSx: SystemStyleObject = {
100117
};
101118

102119
const NodeWrapper = (props: NodeWrapperProps) => {
103-
const { nodeId, width, children, selected } = props;
120+
const { nodeId, width, children, isMissingTemplate, selected } = props;
121+
const ctx = useInvocationNodeContext();
122+
const needsUpdate = useAppSelector(ctx.selectNodeNeedsUpdate);
104123
const mouseOverNode = useMouseOverNode(nodeId);
105124
const mouseOverFormField = useMouseOverFormField(nodeId);
106125
const zoomToNode = useZoomToNode(nodeId);
@@ -138,26 +157,25 @@ const NodeWrapper = (props: NodeWrapperProps) => {
138157
);
139158

140159
return (
141-
<InvocationNodeContextProvider nodeId={nodeId}>
142-
<Box
143-
onClick={globalMenu.onCloseGlobal}
144-
onDoubleClick={onDoubleClick}
145-
onMouseOver={mouseOverNode.handleMouseOver}
146-
onMouseOut={mouseOverNode.handleMouseOut}
147-
className={DRAG_HANDLE_CLASSNAME}
148-
sx={containerSx}
149-
width={width || NODE_WIDTH}
150-
opacity={opacity}
151-
data-is-editor-locked={isLocked}
152-
data-is-selected={selected}
153-
data-is-mouse-over-form-field={mouseOverFormField.isMouseOverFormField}
154-
>
155-
<Box sx={shadowsSx} />
156-
<Box sx={inProgressSx} data-is-in-progress={isInProgress} />
157-
{children}
158-
<Box className="node-selection-overlay" />
159-
</Box>
160-
</InvocationNodeContextProvider>
160+
<Box
161+
onClick={globalMenu.onCloseGlobal}
162+
onDoubleClick={onDoubleClick}
163+
onMouseOver={mouseOverNode.handleMouseOver}
164+
onMouseOut={mouseOverNode.handleMouseOut}
165+
className={DRAG_HANDLE_CLASSNAME}
166+
sx={containerSx}
167+
width={width || NODE_WIDTH}
168+
opacity={opacity}
169+
data-is-editor-locked={isLocked}
170+
data-is-selected={selected}
171+
data-is-mouse-over-form-field={mouseOverFormField.isMouseOverFormField}
172+
data-status={isMissingTemplate ? 'error' : needsUpdate ? 'warning' : undefined}
173+
>
174+
<Box sx={shadowsSx} />
175+
<Box sx={inProgressSx} data-is-in-progress={isInProgress} />
176+
{children}
177+
<Box className="node-selection-overlay" />
178+
</Box>
161179
);
162180
};
163181

invokeai/frontend/web/src/features/nodes/hooks/useNodeIsInvalid.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { useStore } from '@nanostores/react';
22
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
33
import { $nodeErrors } from 'features/nodes/store/util/fieldValidators';
44

5-
export const useNodeIsInvalid = () => {
5+
export const useNodeHasErrors = () => {
66
const ctx = useInvocationNodeContext();
7-
const hasErrors = useStore($nodeErrors, { keys: [ctx.nodeId] })[ctx.nodeId];
8-
return hasErrors;
7+
const errors = useStore($nodeErrors, { keys: [ctx.nodeId] })[ctx.nodeId];
8+
return errors ? errors.length > 0 : false;
99
};

0 commit comments

Comments
 (0)