diff --git a/src/app/shared/nodes/container-node/container-node.ts b/src/app/shared/nodes/container-node/container-node.ts index 006a633..4d29c16 100644 --- a/src/app/shared/nodes/container-node/container-node.ts +++ b/src/app/shared/nodes/container-node/container-node.ts @@ -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; diff --git a/src/app/shared/nodes/generic-node/generic-node.ts b/src/app/shared/nodes/generic-node/generic-node.ts index ec6b744..72780e2 100644 --- a/src/app/shared/nodes/generic-node/generic-node.ts +++ b/src/app/shared/nodes/generic-node/generic-node.ts @@ -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; diff --git a/src/app/shared/nodes/node-utility.spec.ts b/src/app/shared/nodes/node-utility.spec.ts index dc19ff1..c37f2f5 100644 --- a/src/app/shared/nodes/node-utility.spec.ts +++ b/src/app/shared/nodes/node-utility.spec.ts @@ -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(); + }); }); diff --git a/src/app/shared/nodes/node-utility.ts b/src/app/shared/nodes/node-utility.ts index 672e8fc..ce7ea55 100644 --- a/src/app/shared/nodes/node-utility.ts +++ b/src/app/shared/nodes/node-utility.ts @@ -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, root: Record) { if (!node || typeof node !== 'object') return node; const ref = node['$ref']; diff --git a/src/app/shared/nodes/schema-driven-fields.ts b/src/app/shared/nodes/schema-driven-fields.ts index 886c5ae..f89b8fb 100644 --- a/src/app/shared/nodes/schema-driven-fields.ts +++ b/src/app/shared/nodes/schema-driven-fields.ts @@ -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 }); 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 e37fa96..d331ee9 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 @@ -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 | null {