Skip to content

Commit b1359b6

Browse files
feat(ui): update field validation logic to handle collection sizes
1 parent bddccf6 commit b1359b6

File tree

5 files changed

+85
-73
lines changed

5 files changed

+85
-73
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ interface Props {
2323
nodeId: string;
2424
fieldName: string;
2525
kind: 'inputs' | 'outputs';
26-
isMissingInput?: boolean;
26+
isInvalid?: boolean;
2727
withTooltip?: boolean;
2828
shouldDim?: boolean;
2929
}
3030

3131
const EditableFieldTitle = forwardRef((props: Props, ref) => {
32-
const { nodeId, fieldName, kind, isMissingInput = false, withTooltip = false, shouldDim = false } = props;
32+
const { nodeId, fieldName, kind, isInvalid = false, withTooltip = false, shouldDim = false } = props;
3333
const label = useFieldLabel(nodeId, fieldName);
3434
const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind);
3535
const { t } = useTranslation();
@@ -78,7 +78,7 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
7878
fontWeight="semibold"
7979
sx={editablePreviewStyles}
8080
noOfLines={1}
81-
color={isMissingInput ? 'error.300' : 'base.300'}
81+
color={isInvalid ? 'error.300' : 'base.300'}
8282
opacity={shouldDim ? 0.5 : 1}
8383
/>
8484
</Tooltip>

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

Lines changed: 7 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Flex, FormControl } from '@invoke-ai/ui-library';
22
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
3-
import { useDoesInputHaveValue } from 'features/nodes/hooks/useDoesInputHaveValue';
43
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
5-
import { memo, useCallback, useMemo, useState } from 'react';
4+
import { useFieldIsInvalid } from 'features/nodes/hooks/useFieldIsInvalid';
5+
import { memo, useCallback, useState } from 'react';
66

77
import EditableFieldTitle from './EditableFieldTitle';
88
import FieldHandle from './FieldHandle';
@@ -17,32 +17,12 @@ interface Props {
1717

1818
const InputField = ({ nodeId, fieldName }: Props) => {
1919
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
20-
const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName);
2120
const [isHovered, setIsHovered] = useState(false);
21+
const isInvalid = useFieldIsInvalid(nodeId, fieldName);
2222

2323
const { isConnected, isConnectionInProgress, isConnectionStartField, validationResult, shouldDim } =
2424
useConnectionState({ nodeId, fieldName, kind: 'inputs' });
2525

26-
const isMissingInput = useMemo(() => {
27-
if (!fieldTemplate) {
28-
return false;
29-
}
30-
31-
if (!fieldTemplate.required) {
32-
return false;
33-
}
34-
35-
if (!isConnected && fieldTemplate.input === 'connection') {
36-
return true;
37-
}
38-
39-
if (!doesFieldHaveValue && !isConnected && fieldTemplate.input !== 'connection') {
40-
return true;
41-
}
42-
43-
return false;
44-
}, [fieldTemplate, isConnected, doesFieldHaveValue]);
45-
4626
const onMouseEnter = useCallback(() => {
4727
setIsHovered(true);
4828
}, []);
@@ -54,12 +34,12 @@ const InputField = ({ nodeId, fieldName }: Props) => {
5434
if (fieldTemplate.input === 'connection' || isConnected) {
5535
return (
5636
<InputFieldWrapper shouldDim={shouldDim}>
57-
<FormControl isInvalid={isMissingInput} isDisabled={isConnected} px={2}>
37+
<FormControl isInvalid={isInvalid} isDisabled={isConnected} px={2}>
5838
<EditableFieldTitle
5939
nodeId={nodeId}
6040
fieldName={fieldName}
6141
kind="inputs"
62-
isMissingInput={isMissingInput}
42+
isInvalid={isInvalid}
6343
withTooltip
6444
shouldDim
6545
/>
@@ -79,7 +59,7 @@ const InputField = ({ nodeId, fieldName }: Props) => {
7959
return (
8060
<InputFieldWrapper shouldDim={shouldDim}>
8161
<FormControl
82-
isInvalid={isMissingInput}
62+
isInvalid={isInvalid}
8363
isDisabled={isConnected}
8464
// Without pointerEvents prop, disabled inputs don't trigger reactflow events. For example, when making a
8565
// connection, the mouse up to end the connection won't fire, leaving the connection in-progress.
@@ -89,13 +69,7 @@ const InputField = ({ nodeId, fieldName }: Props) => {
8969
>
9070
<Flex flexDir="column" w="full" gap={1} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
9171
<Flex>
92-
<EditableFieldTitle
93-
nodeId={nodeId}
94-
fieldName={fieldName}
95-
kind="inputs"
96-
isMissingInput={isMissingInput}
97-
withTooltip
98-
/>
72+
<EditableFieldTitle nodeId={nodeId} fieldName={fieldName} kind="inputs" isInvalid={isInvalid} withTooltip />
9973
{isHovered && <FieldLinearViewToggle nodeId={nodeId} fieldName={fieldName} />}
10074
</Flex>
10175
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />

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

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import type { SystemStyleObject } from '@invoke-ai/ui-library';
12
import { Flex, Grid, GridItem, IconButton } from '@invoke-ai/ui-library';
23
import { useAppDispatch } from 'app/store/storeHooks';
34
import { UploadMultipleImageButton } from 'common/hooks/useImageUploadButton';
45
import type { AddImagesToNodeImageFieldCollection } from 'features/dnd/dnd';
56
import { addImagesToNodeImageFieldCollectionDndTarget } from 'features/dnd/dnd';
67
import { DndDropTarget } from 'features/dnd/DndDropTarget';
78
import { DndImageFromImageName } from 'features/dnd/DndImageFromImageName';
9+
import { useFieldIsInvalid } from 'features/nodes/hooks/useFieldIsInvalid';
810
import { fieldImageCollectionValueChanged } from 'features/nodes/store/nodesSlice';
911
import type { ImageFieldCollectionInputInstance, ImageFieldCollectionInputTemplate } from 'features/nodes/types/field';
1012
import { memo, useCallback, useMemo } from 'react';
@@ -14,11 +16,21 @@ import type { ImageDTO } from 'services/api/types';
1416

1517
import type { FieldComponentProps } from './types';
1618

19+
const sx = {
20+
'&[data-error=true]': {
21+
borderColor: 'error.500',
22+
borderStyle: 'solid',
23+
borderWidth: 1,
24+
},
25+
} satisfies SystemStyleObject;
26+
1727
export const ImageFieldCollectionInputComponent = memo(
1828
(props: FieldComponentProps<ImageFieldCollectionInputInstance, ImageFieldCollectionInputTemplate>) => {
1929
const { t } = useTranslation();
20-
const { nodeId, field, fieldTemplate } = props;
30+
const { nodeId, field } = props;
2131
const dispatch = useAppDispatch();
32+
const isInvalid = useFieldIsInvalid(nodeId, field.name);
33+
2234
const onReset = useCallback(() => {
2335
dispatch(
2436
fieldImageCollectionValueChanged({
@@ -47,19 +59,6 @@ export const ImageFieldCollectionInputComponent = memo(
4759
[dispatch, field.name, nodeId]
4860
);
4961

50-
const isInvalid = useMemo(() => {
51-
if (!field.value) {
52-
if (fieldTemplate.required) {
53-
return true;
54-
}
55-
} else if (fieldTemplate.minLength !== undefined && field.value.length < fieldTemplate.minLength) {
56-
return true;
57-
} else if (fieldTemplate.maxLength !== undefined && field.value.length > fieldTemplate.maxLength) {
58-
return true;
59-
}
60-
return false;
61-
}, [field.value, fieldTemplate.maxLength, fieldTemplate.minLength, fieldTemplate.required]);
62-
6362
return (
6463
<Flex
6564
position="relative"
@@ -84,10 +83,14 @@ export const ImageFieldCollectionInputComponent = memo(
8483
<>
8584
<Grid
8685
className="nopan"
86+
borderRadius="base"
8787
w="full"
8888
h="full"
8989
templateColumns={`repeat(${Math.min(field.value.length, 3)}, 1fr)`}
90-
gap={2}
90+
gap={1}
91+
sx={sx}
92+
data-error={isInvalid}
93+
p={1}
9194
>
9295
{field.value.map(({ image_name }) => (
9396
<GridItem key={image_name}>

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

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { createSelector } from '@reduxjs/toolkit';
2+
import { useAppSelector } from 'app/store/storeHooks';
3+
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
4+
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
5+
import { selectFieldInputInstance, selectNodesSlice } from 'features/nodes/store/selectors';
6+
import { isImageFieldCollectionInputInstance, isImageFieldCollectionInputTemplate } from 'features/nodes/types/field';
7+
import { useMemo } from 'react';
8+
9+
export const useFieldIsInvalid = (nodeId: string, fieldName: string) => {
10+
const template = useFieldInputTemplate(nodeId, fieldName);
11+
const connectionState = useConnectionState({ nodeId, fieldName, kind: 'inputs' });
12+
13+
const selectIsInvalid = useMemo(() => {
14+
return createSelector(selectNodesSlice, (nodes) => {
15+
const field = selectFieldInputInstance(nodes, nodeId, fieldName);
16+
17+
// No field instance is a problem - should not happen
18+
if (!field) {
19+
return true;
20+
}
21+
22+
// 'connection' input fields have no data validation - only connection validation
23+
if (template.input === 'connection') {
24+
return template.required && !connectionState.isConnected;
25+
}
26+
27+
// 'any' input fields are valid if they are connected
28+
if (template.input === 'any' && connectionState.isConnected) {
29+
return false;
30+
}
31+
32+
// If there is no valid for the field & the field is required, it is invalid
33+
if (field.value === undefined) {
34+
return template.required;
35+
}
36+
37+
// Else special handling for individual field types
38+
if (isImageFieldCollectionInputInstance(field) && isImageFieldCollectionInputTemplate(template)) {
39+
// Image collections may have min or max item counts
40+
if (template.minItems !== undefined && field.value.length < template.minItems) {
41+
return true;
42+
}
43+
44+
if (template.maxItems !== undefined && field.value.length > template.maxItems) {
45+
return true;
46+
}
47+
}
48+
49+
// Field looks OK
50+
return false;
51+
});
52+
}, [connectionState.isConnected, fieldName, nodeId, template]);
53+
54+
const isInvalid = useAppSelector(selectIsInvalid);
55+
56+
return isInvalid;
57+
};

0 commit comments

Comments
 (0)