Name a nested fieldset readably instead of by its dotted path

A field inside a nested object whose object declares no name of its own took the
raw parent path as its fieldset legend, which the uppercasing CSS then rendered
as "LLMDESCRIPTOR.PARAMETERS".

The expression `?? parentPath(path)` appeared in four places - twice in
generic-node, twice in container-node - plus a fifth in task-step-node and a
sixth as the default in schema-driven-fields. That is how one mistake came to be
wrong in six places at once, so this is one exported helper rather than six
corrected copies.

Independent of the optional-group work it was found during, and worth having on
its own.

579 frontend tests green; the assertion fails when the helper returns the raw
path again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucio Lelii 2026-09-04 12:20:50 +02:00
parent 307552cde1
commit b53a22dcc4
6 changed files with 37 additions and 72 deletions

View File

@ -23,7 +23,7 @@ import { NodeFocusModalController } from '../node-focus-modal-controller';
import { firstValueFrom } from 'rxjs';
import { SWIMLANES_ENABLED } from '@shared/feature-flags';
import { extractSchemaRequirements, SchemaRequirements } from '../schema-requirements';
import { evaluateUiConditionRule, getValueByPath, parentPath, pathToLabel, resolveNodeIcon, resolveSchemaPath, splitTemplatedTextParts, valueToDisplayString } from '../node-utility';
import { evaluateUiConditionRule, getValueByPath, parentGroupLabel, pathToLabel, resolveNodeIcon, resolveSchemaPath, splitTemplatedTextParts, valueToDisplayString } from '../node-utility';
import {
collectSchemaFlowDataFields,
flowDataNodeCount,
@ -779,7 +779,7 @@ export class ContainerNodeComponent implements OnDestroy {
getFieldValue: (definition, nextConfig) => valueToDisplayString(getValueByPath(nextConfig, definition.path)),
isFieldWide: (definition) => definition.ui.widget === 'textarea' || definition.label.length >= 18,
getRichContentParts: (path) => this.toRichContentParts(path),
resolveGroupLabel: (path) => getSchemaPathUiMeta(this.containerSchema, path).group ?? parentPath(path)
resolveGroupLabel: (path) => getSchemaPathUiMeta(this.containerSchema, path).group ?? parentGroupLabel(path)
});
const allFields = [
@ -797,7 +797,7 @@ export class ContainerNodeComponent implements OnDestroy {
definitions: this.containerFieldDefinitions.filter((field) => !this.isContainerTypeField(field.path)),
fields: allFields,
richContentFields: allRichContentFields,
resolveGroupLabel: (path) => getSchemaPathUiMeta(this.containerSchema, path).group ?? parentPath(path)
resolveGroupLabel: (path) => getSchemaPathUiMeta(this.containerSchema, path).group ?? parentGroupLabel(path)
});
this.parameterDisplayItems = ordered.rootItems;
this.parameterFieldGroups = ordered.groups;

View File

@ -20,31 +20,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 {
type UiConditionRule,
evaluateUiConditionRule,
flattenPrimitiveValues,
formatNodeTitle,
getOutputPillClass,
getOutputsTitle,
getValueByPath,
isConditionalByPorts,
isHumanInteractiveNode,
orderedSchemaPropertyEntries,
parentPath,
pathToLabel,
readUiConditionRule,
resolveNodeIcon,
resolveSchemaRef,
resolveSchemaPath,
schemaFieldDescription,
schemaFieldLabel,
shouldSkipSchemaField,
splitTemplatedTextParts,
toStringOrNull,
validateUniqueByConstraint,
valueToDisplayString
} from '../node-utility';
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 {
buildSchemaEditableFieldDefinitions,
buildSchemaFieldViewModel,
@ -1138,7 +1114,7 @@ export class GenericNodeComponent implements OnDestroy {
getFieldValue: (definition, nextConfig) => this.fieldDisplayValue(definition, this.getByPath(nextConfig, definition.path)),
isFieldWide: (definition) => this.shouldRenderWideField(definition.label, definition.ui.widget === 'textarea'),
getRichContentParts: (path, _nextConfig) => this.toRichContentParts(path),
resolveGroupLabel: (path) => getSchemaPathUiMeta(this.blockSchema, path).group ?? parentPath(path),
resolveGroupLabel: (path) => getSchemaPathUiMeta(this.blockSchema, path).group ?? parentGroupLabel(path),
groupRichContent: false
});
const allFields = [
@ -1156,7 +1132,7 @@ export class GenericNodeComponent implements OnDestroy {
fields: allFields,
richContentFields: allRichContentFields,
arrayFields: this.arrayFields,
resolveGroupLabel: (path) => getSchemaPathUiMeta(this.blockSchema, path).group ?? parentPath(path)
resolveGroupLabel: (path) => getSchemaPathUiMeta(this.blockSchema, path).group ?? parentGroupLabel(path)
});
this.parameterDisplayItems = ordered.rootItems;
this.parameterFieldGroups = ordered.groups;

View File

@ -2,6 +2,7 @@ import {
evaluateUiConditionRule,
flattenPrimitiveValues,
orderedSchemaPropertyEntries,
parentGroupLabel,
parentPath,
pathToLabel,
readUiConditionRule,
@ -222,4 +223,16 @@ describe('node-utility', () => {
{ text: '!', isDynamicInput: false }
]);
});
it('parentGroupLabel names a nested group readably, not as a dotted path', () => {
// Four call sites used parentPath directly, so a field under llmDescriptor.parameters gave a
// fieldset legend reading "LLMDESCRIPTOR.PARAMETERS" once the CSS uppercased it.
expect(parentGroupLabel('llmDescriptor.parameters.temperature')).toBe('Parameters');
expect(parentGroupLabel('llmDescriptor.provider')).toBe('Llm Descriptor');
});
it('parentGroupLabel gives a root-level field no group at all', () => {
expect(parentGroupLabel('prompt')).toBeNull();
expect(parentGroupLabel('')).toBeNull();
});
});

View File

@ -78,6 +78,20 @@ export function parentPath(path: string): string | null {
return path.slice(0, index);
}
/**
* The fieldset legend for a field that belongs to a nested object but whose object declared no
* name of its own.
*
* It is the readable form, not the raw path: four call sites used `parentPath` directly, so a
* temperature under `llmDescriptor.parameters` produced a legend reading
* "LLMDESCRIPTOR.PARAMETERS". One definition, because four copies of the same expression is how
* that came to be wrong in four places at once.
*/
export function parentGroupLabel(path: string): string | null {
const parent = parentPath(path);
return parent ? pathToLabel(parent) : null;
}
export function resolveSchemaRef(node: Record<string, any>, root: Record<string, any>) {
if (!node || typeof node !== 'object') return node;
const ref = node['$ref'];

View File

@ -1,18 +1,4 @@
import {
orderedSchemaPropertyEntries,
type UiConditionRule,
evaluateUiConditionRule,
getValueByPath,
parentPath,
readEffectiveUiVisibleConditionRule,
readUiConditionRule,
readUiGroup,
readUiLabel,
resolveSchemaRef,
resolveSchemaPath,
schemaFieldLabel,
schemaFieldDescription
} from './node-utility';
import { evaluateUiConditionRule, getValueByPath, orderedSchemaPropertyEntries, parentGroupLabel, readEffectiveUiVisibleConditionRule, readUiConditionRule, readUiGroup, readUiLabel, resolveSchemaPath, resolveSchemaRef, schemaFieldDescription, schemaFieldLabel, type UiConditionRule } from './node-utility';
export type SchemaFieldType = 'string' | 'number' | 'integer' | 'boolean' | 'unknown';
@ -772,7 +758,7 @@ export function buildSchemaFieldViewModel<
const grouped = groupSchemaFields({
fields: parameterFields,
richContentFields: params.groupRichContent ? richContentFields : undefined,
resolveGroupLabel: (path) => params.resolveGroupLabel?.(path) ?? parentPath(path),
resolveGroupLabel: (path) => params.resolveGroupLabel?.(path) ?? parentGroupLabel(path),
resolveLegend: params.resolveLegend
});

View File

@ -26,31 +26,7 @@ import {
normalizeFlowDataValue,
type SchemaFlowDataFieldDefinition
} from '../flow-data-schema-fields';
import {
type UiConditionRule,
evaluateUiConditionRule,
flattenPrimitiveValues,
formatNodeTitle,
getOutputPillClass,
getOutputsTitle,
isConditionalByPorts,
isHumanInteractiveNode,
orderedSchemaPropertyEntries,
parentPath,
pathToLabel,
readUiConditionRule,
readEffectiveUiVisibleConditionRule,
readUiGroup,
readUiLabel,
resolveNodeIcon,
resolveSchemaRef,
resolveSchemaPath,
schemaFieldLabel,
shouldSkipSchemaField,
splitTemplatedTextParts,
toStringOrNull,
valueToDisplayString
} from '../node-utility';
import { evaluateUiConditionRule, flattenPrimitiveValues, formatNodeTitle, getOutputPillClass, getOutputsTitle, isConditionalByPorts, isHumanInteractiveNode, orderedSchemaPropertyEntries, parentGroupLabel, pathToLabel, readEffectiveUiVisibleConditionRule, readUiConditionRule, readUiGroup, readUiLabel, resolveNodeIcon, resolveSchemaPath, resolveSchemaRef, schemaFieldLabel, shouldSkipSchemaField, splitTemplatedTextParts, toStringOrNull, type UiConditionRule, valueToDisplayString } from '../node-utility';
type DisplayField = {
path: string;
@ -1374,7 +1350,7 @@ export class TaskStepNodeComponent {
}
private groupLabelForPath(path: string): string | null {
return this.getFieldUiMeta(path).group ?? parentPath(path);
return this.getFieldUiMeta(path).group ?? parentGroupLabel(path);
}
private resolveFieldSchema(path: string): Record<string, any> | null {