Skip to content

Commit bddccf6

Browse files
feat(ui): add graph validation for image collection size
1 parent 21ffaab commit bddccf6

File tree

4 files changed

+51
-16
lines changed

4 files changed

+51
-16
lines changed

invokeai/frontend/web/public/locales/en.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1015,8 +1015,11 @@
10151015
"addingImagesTo": "Adding images to",
10161016
"invoke": "Invoke",
10171017
"missingFieldTemplate": "Missing field template",
1018-
"missingInputForField": "{{nodeLabel}} -> {{fieldLabel}} missing input",
1018+
"missingInputForField": "{{nodeLabel}} -> {{fieldLabel}}: missing input",
10191019
"missingNodeTemplate": "Missing node template",
1020+
"collectionEmpty": "{{nodeLabel}} -> {{fieldLabel}} empty collection",
1021+
"collectionTooFewItems": "{{nodeLabel}} -> {{fieldLabel}}: too few items, minimum {{minItems}}",
1022+
"collectionTooManyItems": "{{nodeLabel}} -> {{fieldLabel}}: too many items, maximum {{maxItems}}",
10201023
"noModelSelected": "No model selected",
10211024
"noT5EncoderModelSelected": "No T5 Encoder model selected for FLUX generation",
10221025
"noFLUXVAEModelSelected": "No VAE model selected for FLUX generation",

invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { $templates } from 'features/nodes/store/nodesSlice';
1111
import { selectNodesSlice } from 'features/nodes/store/selectors';
1212
import type { Templates } from 'features/nodes/store/types';
1313
import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
14+
import { isImageFieldCollectionInputInstance, isImageFieldCollectionInputTemplate } from 'features/nodes/types/field';
1415
import { isInvocationNode } from 'features/nodes/types/invocation';
1516
import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
1617
import { selectConfigSlice } from 'features/system/store/configSlice';
@@ -103,14 +104,45 @@ const createSelector = (arg: {
103104
return;
104105
}
105106

107+
const baseTKeyOptions = {
108+
nodeLabel: node.data.label || nodeTemplate.title,
109+
fieldLabel: field.label || fieldTemplate.title,
110+
};
111+
106112
if (fieldTemplate.required && field.value === undefined && !hasConnection) {
107-
reasons.push({
108-
content: i18n.t('parameters.invoke.missingInputForField', {
109-
nodeLabel: node.data.label || nodeTemplate.title,
110-
fieldLabel: field.label || fieldTemplate.title,
111-
}),
112-
});
113+
reasons.push({ content: i18n.t('parameters.invoke.missingInputForField', baseTKeyOptions) });
113114
return;
115+
} else if (
116+
field.value &&
117+
isImageFieldCollectionInputInstance(field) &&
118+
isImageFieldCollectionInputTemplate(fieldTemplate)
119+
) {
120+
// Image collections may have min or max items to validate
121+
// TODO(psyche): generalize this to other collection types
122+
if (fieldTemplate.minItems !== undefined && fieldTemplate.minItems > 0 && field.value.length === 0) {
123+
reasons.push({ content: i18n.t('parameters.invoke.collectionEmpty', baseTKeyOptions) });
124+
return;
125+
}
126+
if (fieldTemplate.minItems !== undefined && field.value.length < fieldTemplate.minItems) {
127+
reasons.push({
128+
content: i18n.t('parameters.invoke.collectionTooFewItems', {
129+
...baseTKeyOptions,
130+
size: field.value.length,
131+
minItems: fieldTemplate.minItems,
132+
}),
133+
});
134+
return;
135+
}
136+
if (fieldTemplate.maxItems !== undefined && field.value.length > fieldTemplate.maxItems) {
137+
reasons.push({
138+
content: i18n.t('parameters.invoke.collectionTooManyItems', {
139+
...baseTKeyOptions,
140+
size: field.value.length,
141+
maxItems: fieldTemplate.maxItems,
142+
}),
143+
});
144+
return;
145+
}
114146
}
115147
});
116148
});

invokeai/frontend/web/src/features/nodes/types/field.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -399,13 +399,13 @@ const zImageFieldCollectionInputTemplate = zFieldInputTemplateBase
399399
type: zImageCollectionFieldType,
400400
originalType: zFieldType.optional(),
401401
default: zImageFieldCollectionValue,
402-
maxLength: z.number().int().gte(0).optional(),
403-
minLength: z.number().int().gte(0).optional(),
402+
maxItems: z.number().int().gte(0).optional(),
403+
minItems: z.number().int().gte(0).optional(),
404404
})
405405
.refine(
406406
(val) => {
407-
if (val.maxLength !== undefined && val.minLength !== undefined) {
408-
return val.maxLength >= val.minLength;
407+
if (val.maxItems !== undefined && val.minItems !== undefined) {
408+
return val.maxItems >= val.minItems;
409409
}
410410
return true;
411411
},

invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -417,15 +417,15 @@ const buildImageFieldCollectionInputTemplate: FieldInputTemplateBuilder<ImageFie
417417
const template: ImageFieldCollectionInputTemplate = {
418418
...baseField,
419419
type: fieldType,
420-
default: schemaObject.default ?? undefined,
420+
default: schemaObject.default ?? (schemaObject.orig_required ? [] : undefined),
421421
};
422422

423-
if (schemaObject.minLength !== undefined) {
424-
template.minLength = schemaObject.minLength;
423+
if (schemaObject.minItems !== undefined) {
424+
template.minItems = schemaObject.minItems;
425425
}
426426

427-
if (schemaObject.maxLength !== undefined) {
428-
template.maxLength = schemaObject.maxLength;
427+
if (schemaObject.maxItems !== undefined) {
428+
template.maxItems = schemaObject.maxItems;
429429
}
430430

431431
return template;

0 commit comments

Comments
 (0)