mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): update field validation logic to handle collection sizes
This commit is contained in:
@@ -23,13 +23,13 @@ interface Props {
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
kind: 'inputs' | 'outputs';
|
||||
isMissingInput?: boolean;
|
||||
isInvalid?: boolean;
|
||||
withTooltip?: boolean;
|
||||
shouldDim?: boolean;
|
||||
}
|
||||
|
||||
const EditableFieldTitle = forwardRef((props: Props, ref) => {
|
||||
const { nodeId, fieldName, kind, isMissingInput = false, withTooltip = false, shouldDim = false } = props;
|
||||
const { nodeId, fieldName, kind, isInvalid = false, withTooltip = false, shouldDim = false } = props;
|
||||
const label = useFieldLabel(nodeId, fieldName);
|
||||
const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind);
|
||||
const { t } = useTranslation();
|
||||
@@ -78,7 +78,7 @@ const EditableFieldTitle = forwardRef((props: Props, ref) => {
|
||||
fontWeight="semibold"
|
||||
sx={editablePreviewStyles}
|
||||
noOfLines={1}
|
||||
color={isMissingInput ? 'error.300' : 'base.300'}
|
||||
color={isInvalid ? 'error.300' : 'base.300'}
|
||||
opacity={shouldDim ? 0.5 : 1}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Flex, FormControl } from '@invoke-ai/ui-library';
|
||||
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
|
||||
import { useDoesInputHaveValue } from 'features/nodes/hooks/useDoesInputHaveValue';
|
||||
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { useFieldIsInvalid } from 'features/nodes/hooks/useFieldIsInvalid';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
|
||||
import EditableFieldTitle from './EditableFieldTitle';
|
||||
import FieldHandle from './FieldHandle';
|
||||
@@ -17,32 +17,12 @@ interface Props {
|
||||
|
||||
const InputField = ({ nodeId, fieldName }: Props) => {
|
||||
const fieldTemplate = useFieldInputTemplate(nodeId, fieldName);
|
||||
const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const isInvalid = useFieldIsInvalid(nodeId, fieldName);
|
||||
|
||||
const { isConnected, isConnectionInProgress, isConnectionStartField, validationResult, shouldDim } =
|
||||
useConnectionState({ nodeId, fieldName, kind: 'inputs' });
|
||||
|
||||
const isMissingInput = useMemo(() => {
|
||||
if (!fieldTemplate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!fieldTemplate.required) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isConnected && fieldTemplate.input === 'connection') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!doesFieldHaveValue && !isConnected && fieldTemplate.input !== 'connection') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [fieldTemplate, isConnected, doesFieldHaveValue]);
|
||||
|
||||
const onMouseEnter = useCallback(() => {
|
||||
setIsHovered(true);
|
||||
}, []);
|
||||
@@ -54,12 +34,12 @@ const InputField = ({ nodeId, fieldName }: Props) => {
|
||||
if (fieldTemplate.input === 'connection' || isConnected) {
|
||||
return (
|
||||
<InputFieldWrapper shouldDim={shouldDim}>
|
||||
<FormControl isInvalid={isMissingInput} isDisabled={isConnected} px={2}>
|
||||
<FormControl isInvalid={isInvalid} isDisabled={isConnected} px={2}>
|
||||
<EditableFieldTitle
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
kind="inputs"
|
||||
isMissingInput={isMissingInput}
|
||||
isInvalid={isInvalid}
|
||||
withTooltip
|
||||
shouldDim
|
||||
/>
|
||||
@@ -79,7 +59,7 @@ const InputField = ({ nodeId, fieldName }: Props) => {
|
||||
return (
|
||||
<InputFieldWrapper shouldDim={shouldDim}>
|
||||
<FormControl
|
||||
isInvalid={isMissingInput}
|
||||
isInvalid={isInvalid}
|
||||
isDisabled={isConnected}
|
||||
// Without pointerEvents prop, disabled inputs don't trigger reactflow events. For example, when making a
|
||||
// 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) => {
|
||||
>
|
||||
<Flex flexDir="column" w="full" gap={1} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<Flex>
|
||||
<EditableFieldTitle
|
||||
nodeId={nodeId}
|
||||
fieldName={fieldName}
|
||||
kind="inputs"
|
||||
isMissingInput={isMissingInput}
|
||||
withTooltip
|
||||
/>
|
||||
<EditableFieldTitle nodeId={nodeId} fieldName={fieldName} kind="inputs" isInvalid={isInvalid} withTooltip />
|
||||
{isHovered && <FieldLinearViewToggle nodeId={nodeId} fieldName={fieldName} />}
|
||||
</Flex>
|
||||
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex, Grid, GridItem, IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { UploadMultipleImageButton } from 'common/hooks/useImageUploadButton';
|
||||
@@ -5,6 +6,7 @@ import type { AddImagesToNodeImageFieldCollection } from 'features/dnd/dnd';
|
||||
import { addImagesToNodeImageFieldCollectionDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { DndImageFromImageName } from 'features/dnd/DndImageFromImageName';
|
||||
import { useFieldIsInvalid } from 'features/nodes/hooks/useFieldIsInvalid';
|
||||
import { fieldImageCollectionValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { ImageFieldCollectionInputInstance, ImageFieldCollectionInputTemplate } from 'features/nodes/types/field';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
@@ -14,11 +16,21 @@ import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
import type { FieldComponentProps } from './types';
|
||||
|
||||
const sx = {
|
||||
'&[data-error=true]': {
|
||||
borderColor: 'error.500',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 1,
|
||||
},
|
||||
} satisfies SystemStyleObject;
|
||||
|
||||
export const ImageFieldCollectionInputComponent = memo(
|
||||
(props: FieldComponentProps<ImageFieldCollectionInputInstance, ImageFieldCollectionInputTemplate>) => {
|
||||
const { t } = useTranslation();
|
||||
const { nodeId, field, fieldTemplate } = props;
|
||||
const { nodeId, field } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const isInvalid = useFieldIsInvalid(nodeId, field.name);
|
||||
|
||||
const onReset = useCallback(() => {
|
||||
dispatch(
|
||||
fieldImageCollectionValueChanged({
|
||||
@@ -47,19 +59,6 @@ export const ImageFieldCollectionInputComponent = memo(
|
||||
[dispatch, field.name, nodeId]
|
||||
);
|
||||
|
||||
const isInvalid = useMemo(() => {
|
||||
if (!field.value) {
|
||||
if (fieldTemplate.required) {
|
||||
return true;
|
||||
}
|
||||
} else if (fieldTemplate.minLength !== undefined && field.value.length < fieldTemplate.minLength) {
|
||||
return true;
|
||||
} else if (fieldTemplate.maxLength !== undefined && field.value.length > fieldTemplate.maxLength) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [field.value, fieldTemplate.maxLength, fieldTemplate.minLength, fieldTemplate.required]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
position="relative"
|
||||
@@ -84,10 +83,14 @@ export const ImageFieldCollectionInputComponent = memo(
|
||||
<>
|
||||
<Grid
|
||||
className="nopan"
|
||||
borderRadius="base"
|
||||
w="full"
|
||||
h="full"
|
||||
templateColumns={`repeat(${Math.min(field.value.length, 3)}, 1fr)`}
|
||||
gap={2}
|
||||
gap={1}
|
||||
sx={sx}
|
||||
data-error={isInvalid}
|
||||
p={1}
|
||||
>
|
||||
{field.value.map(({ image_name }) => (
|
||||
<GridItem key={image_name}>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectNodeData, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useDoesInputHaveValue = (nodeId: string, fieldName: string): boolean => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createMemoizedSelector(selectNodesSlice, (nodes) => {
|
||||
const data = selectNodeData(nodes, nodeId);
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
return data.inputs[fieldName]?.value !== undefined;
|
||||
}),
|
||||
[fieldName, nodeId]
|
||||
);
|
||||
|
||||
const doesFieldHaveValue = useAppSelector(selector);
|
||||
|
||||
return doesFieldHaveValue;
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useConnectionState } from 'features/nodes/hooks/useConnectionState';
|
||||
import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate';
|
||||
import { selectFieldInputInstance, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { isImageFieldCollectionInputInstance, isImageFieldCollectionInputTemplate } from 'features/nodes/types/field';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useFieldIsInvalid = (nodeId: string, fieldName: string) => {
|
||||
const template = useFieldInputTemplate(nodeId, fieldName);
|
||||
const connectionState = useConnectionState({ nodeId, fieldName, kind: 'inputs' });
|
||||
|
||||
const selectIsInvalid = useMemo(() => {
|
||||
return createSelector(selectNodesSlice, (nodes) => {
|
||||
const field = selectFieldInputInstance(nodes, nodeId, fieldName);
|
||||
|
||||
// No field instance is a problem - should not happen
|
||||
if (!field) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 'connection' input fields have no data validation - only connection validation
|
||||
if (template.input === 'connection') {
|
||||
return template.required && !connectionState.isConnected;
|
||||
}
|
||||
|
||||
// 'any' input fields are valid if they are connected
|
||||
if (template.input === 'any' && connectionState.isConnected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there is no valid for the field & the field is required, it is invalid
|
||||
if (field.value === undefined) {
|
||||
return template.required;
|
||||
}
|
||||
|
||||
// Else special handling for individual field types
|
||||
if (isImageFieldCollectionInputInstance(field) && isImageFieldCollectionInputTemplate(template)) {
|
||||
// Image collections may have min or max item counts
|
||||
if (template.minItems !== undefined && field.value.length < template.minItems) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (template.maxItems !== undefined && field.value.length > template.maxItems) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Field looks OK
|
||||
return false;
|
||||
});
|
||||
}, [connectionState.isConnected, fieldName, nodeId, template]);
|
||||
|
||||
const isInvalid = useAppSelector(selectIsInvalid);
|
||||
|
||||
return isInvalid;
|
||||
};
|
||||
Reference in New Issue
Block a user