diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index c8c5699931..28b658a785 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1015,8 +1015,11 @@ "addingImagesTo": "Adding images to", "invoke": "Invoke", "missingFieldTemplate": "Missing field template", - "missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} missing input", + "missingInputForField": "{{nodeLabel}} -> {{fieldLabel}}: missing input", "missingNodeTemplate": "Missing node template", + "collectionEmpty": "{{nodeLabel}} -> {{fieldLabel}} empty collection", + "collectionTooFewItems": "{{nodeLabel}} -> {{fieldLabel}}: too few items, minimum {{minItems}}", + "collectionTooManyItems": "{{nodeLabel}} -> {{fieldLabel}}: too many items, maximum {{maxItems}}", "noModelSelected": "No model selected", "noT5EncoderModelSelected": "No T5 Encoder model selected for FLUX generation", "noFLUXVAEModelSelected": "No VAE model selected for FLUX generation", diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index a6b8cc671d..8d51b455a1 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -11,6 +11,7 @@ import { $templates } from 'features/nodes/store/nodesSlice'; import { selectNodesSlice } from 'features/nodes/store/selectors'; import type { Templates } from 'features/nodes/store/types'; import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; +import { isImageFieldCollectionInputInstance, isImageFieldCollectionInputTemplate } from 'features/nodes/types/field'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice'; import { selectConfigSlice } from 'features/system/store/configSlice'; @@ -103,14 +104,45 @@ const createSelector = (arg: { return; } + const baseTKeyOptions = { + nodeLabel: node.data.label || nodeTemplate.title, + fieldLabel: field.label || fieldTemplate.title, + }; + if (fieldTemplate.required && field.value === undefined && !hasConnection) { - reasons.push({ - content: i18n.t('parameters.invoke.missingInputForField', { - nodeLabel: node.data.label || nodeTemplate.title, - fieldLabel: field.label || fieldTemplate.title, - }), - }); + reasons.push({ content: i18n.t('parameters.invoke.missingInputForField', baseTKeyOptions) }); return; + } else if ( + field.value && + isImageFieldCollectionInputInstance(field) && + isImageFieldCollectionInputTemplate(fieldTemplate) + ) { + // Image collections may have min or max items to validate + // TODO(psyche): generalize this to other collection types + if (fieldTemplate.minItems !== undefined && fieldTemplate.minItems > 0 && field.value.length === 0) { + reasons.push({ content: i18n.t('parameters.invoke.collectionEmpty', baseTKeyOptions) }); + return; + } + if (fieldTemplate.minItems !== undefined && field.value.length < fieldTemplate.minItems) { + reasons.push({ + content: i18n.t('parameters.invoke.collectionTooFewItems', { + ...baseTKeyOptions, + size: field.value.length, + minItems: fieldTemplate.minItems, + }), + }); + return; + } + if (fieldTemplate.maxItems !== undefined && field.value.length > fieldTemplate.maxItems) { + reasons.push({ + content: i18n.t('parameters.invoke.collectionTooManyItems', { + ...baseTKeyOptions, + size: field.value.length, + maxItems: fieldTemplate.maxItems, + }), + }); + return; + } } }); }); diff --git a/invokeai/frontend/web/src/features/nodes/types/field.ts b/invokeai/frontend/web/src/features/nodes/types/field.ts index 37da8ea504..721370aa4f 100644 --- a/invokeai/frontend/web/src/features/nodes/types/field.ts +++ b/invokeai/frontend/web/src/features/nodes/types/field.ts @@ -399,13 +399,13 @@ const zImageFieldCollectionInputTemplate = zFieldInputTemplateBase type: zImageCollectionFieldType, originalType: zFieldType.optional(), default: zImageFieldCollectionValue, - maxLength: z.number().int().gte(0).optional(), - minLength: z.number().int().gte(0).optional(), + maxItems: z.number().int().gte(0).optional(), + minItems: z.number().int().gte(0).optional(), }) .refine( (val) => { - if (val.maxLength !== undefined && val.minLength !== undefined) { - return val.maxLength >= val.minLength; + if (val.maxItems !== undefined && val.minItems !== undefined) { + return val.maxItems >= val.minItems; } return true; }, diff --git a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts index 0cd0027441..69f91cf99c 100644 --- a/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts +++ b/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts @@ -417,15 +417,15 @@ const buildImageFieldCollectionInputTemplate: FieldInputTemplateBuilder