Summarise an array row by the branch it is on

An upload row taking its file from a global listed as "GLOBAL · false".
The summary takes the first two values a row holds, and the false was a
"several files" box nobody had touched - while the global the row names,
the one thing that tells it apart from the next row, never got a look in.

Two kinds of value are skipped now, both for the same reason: they
identify nothing. A false flag is an absence dressed as a fact, and a
field the row's own choice hides is irrelevant by construction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucio Lelii 2026-09-14 14:55:29 +02:00
parent f1305c16e7
commit a426a2c5e7
3 changed files with 74 additions and 5 deletions

View File

@ -1097,4 +1097,31 @@ describe('GenericNodeComponent', () => {
expect(component.optionalGroupViews()[0].setCount).toBe(0);
});
});
it('summarises an upload row by the branch it is on, not by an untouched flag', () => {
// The row taking its file from a global read "GLOBAL · false": the false was a "several files"
// box nobody had touched, and the global it names never got a look in.
const itemSchema = {
type: 'object',
'x-ui-property-order': ['source', 'name', 'kind', 'multiple', 'globalInput'],
properties: {
source: { type: 'string', enum: ['INPUT', 'GLOBAL'] },
name: { type: 'string', 'x-ui-visible-when': { field: 'source', in: ['INPUT', ''] } },
kind: { type: 'string', 'x-ui-visible-when': { field: 'source', in: ['INPUT', ''] } },
multiple: { type: 'boolean', 'x-ui-visible-when': { field: 'source', in: ['INPUT', ''] } },
globalInput: { type: 'string', 'x-ui-visible-when': { field: 'source', equals: 'GLOBAL' } }
}
};
const definition = { path: 'uploadInputs', label: 'Uploads', itemSchema, uniqueBy: null } as any;
const summary = (component as any).toArrayItemSummary.bind(component);
expect(summary(definition, { source: 'GLOBAL', multiple: false, globalInput: 'document' }, 0))
.toBe('GLOBAL · document');
// The other branch reads by its own fields, and an untouched flag still says nothing.
expect(summary(definition, { source: 'INPUT', name: 'planDoc', multiple: false }, 0))
.toBe('INPUT · planDoc');
// A flag that was touched is a fact worth showing.
expect(summary(definition, { source: 'INPUT', name: 'planDoc', multiple: true }, 0))
.toBe('INPUT · planDoc');
});
});

View File

@ -21,7 +21,7 @@ import { BlocksService } from '@services/blocks/blocks';
import { firstValueFrom, take } from 'rxjs';
import { SWIMLANES_ENABLED } from '@shared/feature-flags';
import { ConditionalRequiredField, extractSchemaRequirements, SchemaRequirements } from '../schema-requirements';
import { evaluateUiConditionRule, flattenPrimitiveValues, formatNodeTitle, getOutputPillClass, getOutputsTitle, getValueByPath, isConditionalByPorts, isHumanInteractiveNode, orderedSchemaPropertyEntries, parentGroupLabel, parentPath, pathToLabel, readUiConditionRule, resolveNodeIcon, resolveSchemaPath, resolveSchemaRef, schemaFieldDescription, schemaFieldLabel, shouldSkipSchemaField, splitTemplatedTextParts, toStringOrNull, type UiConditionRule, validateUniqueByConstraint, valueToDisplayString } from '../node-utility';
import { evaluateUiConditionRule, flattenPrimitiveValues, readEffectiveUiVisibleConditionRule, formatNodeTitle, getOutputPillClass, getOutputsTitle, getValueByPath, isConditionalByPorts, isHumanInteractiveNode, orderedSchemaPropertyEntries, parentGroupLabel, parentPath, pathToLabel, readUiConditionRule, resolveNodeIcon, resolveSchemaPath, resolveSchemaRef, schemaFieldDescription, schemaFieldLabel, shouldSkipSchemaField, splitTemplatedTextParts, toStringOrNull, type UiConditionRule, validateUniqueByConstraint, valueToDisplayString } from '../node-utility';
import {
buildSchemaEditableFieldDefinitions,
buildSchemaObjectDialog,
@ -1829,9 +1829,10 @@ export class GenericNodeComponent implements OnDestroy {
if (!properties) return `Item ${index + 1}`;
const summaryParts: string[] = [];
for (const { key } of orderedSchemaPropertyEntries(definition.itemSchema, this.blockSchema ?? definition.itemSchema ?? {})) {
for (const { key, schema } of orderedSchemaPropertyEntries(definition.itemSchema, this.blockSchema ?? definition.itemSchema ?? {})) {
const value = (item as Record<string, unknown>)[key];
if (this.isMissingValue(value)) continue;
if (this.isMissingValue(value) || value === false) continue;
if (!this.isArrayItemFieldVisible(schema, item as Record<string, unknown>, definition)) continue;
if (typeof value === 'string') {
summaryParts.push(value);
} else if (typeof value === 'number' || typeof value === 'boolean') {
@ -1843,6 +1844,26 @@ export class GenericNodeComponent implements OnDestroy {
return summaryParts.length ? summaryParts.join(' · ') : `Item ${index + 1}`;
}
/**
* What identifies one row of an array field, for the collapsed list on the node.
*
* <p>Two kinds of value are skipped because they identify nothing. A false flag is an absence
* dressed as a fact - an upload row taking its file from a global read "GLOBAL · false", the
* false being an untouched "several files" box. And a field the row's own choice hides is
* irrelevant by construction: the summary should read the live branch, which for that row is the
* global it names.
*/
private isArrayItemFieldVisible(
propertySchema: Record<string, any> | null | undefined,
item: Record<string, unknown>,
definition: ArrayFieldDefinition
): boolean {
const rule = readEffectiveUiVisibleConditionRule(propertySchema);
if (!rule) return true;
return evaluateUiConditionRule(rule, item, (fieldPath) =>
resolveSchemaPath(definition.itemSchema ?? {}, fieldPath));
}
private validateUniqueArrayItem(
definition: ArrayFieldDefinition,
items: unknown[],

View File

@ -1306,9 +1306,10 @@ export class TaskStepNodeComponent {
if (!properties) return `Item ${index + 1}`;
const summaryParts: string[] = [];
for (const { key } of orderedSchemaPropertyEntries(definition.itemSchema, this.blockSchema ?? definition.itemSchema ?? {})) {
for (const { key, schema } of orderedSchemaPropertyEntries(definition.itemSchema, this.blockSchema ?? definition.itemSchema ?? {})) {
const value = (item as Record<string, unknown>)[key];
if (value == null) continue;
if (value == null || value === false) continue;
if (!this.isArrayItemFieldVisible(schema, item as Record<string, unknown>, definition)) continue;
if (typeof value === 'string' && value.trim().length > 0) {
summaryParts.push(value);
} else if (typeof value === 'number' || typeof value === 'boolean') {
@ -1320,6 +1321,26 @@ export class TaskStepNodeComponent {
return summaryParts.length ? summaryParts.join(' · ') : `Item ${index + 1}`;
}
/**
* What identifies one row of an array field, for the collapsed list on the node.
*
* <p>Two kinds of value are skipped because they identify nothing. A false flag is an absence
* dressed as a fact - an upload row taking its file from a global read "GLOBAL · false", the
* false being an untouched "several files" box. And a field the row's own choice hides is
* irrelevant by construction: the summary should read the live branch, which for that row is the
* global it names.
*/
private isArrayItemFieldVisible(
propertySchema: Record<string, any> | null | undefined,
item: Record<string, unknown>,
definition: ArrayFieldDefinition
): boolean {
const rule = readEffectiveUiVisibleConditionRule(propertySchema);
if (!rule) return true;
return evaluateUiConditionRule(rule, item, (fieldPath) =>
resolveSchemaPath(definition.itemSchema ?? {}, fieldPath));
}
private resolveArrayItemSchema(node: Record<string, any> | null | undefined, root: Record<string, any>) {
const items = node?.['items'];
if (!items || typeof items !== 'object') return null;