diff --git a/src/app/shared/nodes/generic-node/generic-node.spec.ts b/src/app/shared/nodes/generic-node/generic-node.spec.ts index a005a3c..79ba54f 100644 --- a/src/app/shared/nodes/generic-node/generic-node.spec.ts +++ b/src/app/shared/nodes/generic-node/generic-node.spec.ts @@ -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'); + }); }); diff --git a/src/app/shared/nodes/generic-node/generic-node.ts b/src/app/shared/nodes/generic-node/generic-node.ts index adb4c94..0716be2 100644 --- a/src/app/shared/nodes/generic-node/generic-node.ts +++ b/src/app/shared/nodes/generic-node/generic-node.ts @@ -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)[key]; - if (this.isMissingValue(value)) continue; + if (this.isMissingValue(value) || value === false) continue; + if (!this.isArrayItemFieldVisible(schema, item as Record, 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. + * + *

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 | null | undefined, + item: Record, + 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[], diff --git a/src/app/shared/nodes/task-step-node/task-step-node.ts b/src/app/shared/nodes/task-step-node/task-step-node.ts index 0304ec1..3a00bdc 100644 --- a/src/app/shared/nodes/task-step-node/task-step-node.ts +++ b/src/app/shared/nodes/task-step-node/task-step-node.ts @@ -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)[key]; - if (value == null) continue; + if (value == null || value === false) continue; + if (!this.isArrayItemFieldVisible(schema, item as Record, 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. + * + *

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 | null | undefined, + item: Record, + definition: ArrayFieldDefinition + ): boolean { + const rule = readEffectiveUiVisibleConditionRule(propertySchema); + if (!rule) return true; + return evaluateUiConditionRule(rule, item, (fieldPath) => + resolveSchemaPath(definition.itemSchema ?? {}, fieldPath)); + } + private resolveArrayItemSchema(node: Record | null | undefined, root: Record) { const items = node?.['items']; if (!items || typeof items !== 'object') return null;