Skip to content

Commit

Permalink
feat(ui): update field validation logic to handle collection sizes
Browse files Browse the repository at this point in the history
  • Loading branch information
psychedelicious committed Nov 19, 2024
1 parent bddccf6 commit b1359b6
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
}, []);
Expand All @@ -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
/>
Expand All @@ -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.
Expand All @@ -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} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
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';
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';
Expand All @@ -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({
Expand Down Expand Up @@ -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"
Expand All @@ -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}>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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;
};

0 comments on commit b1359b6

Please sign in to comment.