From 38cc7676cd4581c0cfc3fedb3257cf40eab991e6 Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Tue, 10 Mar 2026 15:00:59 +0100 Subject: [PATCH] Add schema-driven conditional node rendering --- .../nodes/generic-node/generic-node.html | 12 +- .../shared/nodes/generic-node/generic-node.ts | 184 ++++++++++++++---- src/app/shared/nodes/node-utility.spec.ts | 68 ++++++- src/app/shared/nodes/node-utility.ts | 64 +++++- .../nodes/task-step-node/task-step-node.ts | 88 ++++++++- 5 files changed, 353 insertions(+), 63 deletions(-) diff --git a/src/app/shared/nodes/generic-node/generic-node.html b/src/app/shared/nodes/generic-node/generic-node.html index 5c35f07..5145b8f 100644 --- a/src/app/shared/nodes/generic-node/generic-node.html +++ b/src/app/shared/nodes/generic-node/generic-node.html @@ -186,16 +186,8 @@ @if (localEditorLoading) { - } @else if (localEditorDisabled) { - - {{ localEditorDisabledHint }} } @else if (localEditorType === 'boolean') { - @@ -217,7 +209,7 @@
- +
diff --git a/src/app/shared/nodes/generic-node/generic-node.ts b/src/app/shared/nodes/generic-node/generic-node.ts index 6ebc713..3eec25c 100644 --- a/src/app/shared/nodes/generic-node/generic-node.ts +++ b/src/app/shared/nodes/generic-node/generic-node.ts @@ -10,9 +10,14 @@ import { BlocksService } from '@services/blocks/blocks'; import { firstValueFrom, take } from 'rxjs'; import { ConditionalRequiredField, extractSchemaRequirements, SchemaRequirements } from '../schema-requirements'; import { + type UiConditionRule, + evaluateUiConditionRule, flattenPrimitiveValues, + getValueByPath, parentPath, pathToLabel, + readUiConditionRule, + readUiGroup, resolveSchemaRef, splitTemplatedTextParts, toStringOrNull, @@ -41,6 +46,8 @@ type EditableFieldDefinition = { placeholder?: string; tip?: string; rows?: number; + visibleWhen: UiConditionRule[]; + group: string | null; }; }; @@ -94,8 +101,6 @@ export class GenericNodeComponent { localEditorOptions: string[] = []; localEditorLoading = false; localEditorHasRetriever = false; - localEditorDisabled = false; - localEditorDisabledHint = ''; localEditorType: FieldType = 'string'; localEditorMaxLength: number | null = null; @@ -145,8 +150,6 @@ export class GenericNodeComponent { this.localEditorOptions = []; this.localEditorLoading = false; this.localEditorHasRetriever = false; - this.localEditorDisabled = false; - this.localEditorDisabledHint = ''; this.localEditorOpen = true; } @@ -156,6 +159,7 @@ export class GenericNodeComponent { const definition = this.editableFieldDefinitions.find((field) => field.path === path); if (!definition) return; + if (!this.isFieldVisible(definition)) return; if (definition.ui.widget === 'textarea') { const currentValue = this.valueToEditorString( @@ -174,8 +178,6 @@ export class GenericNodeComponent { this.localEditorOptions = []; this.localEditorLoading = !!definition.retrieverKey; this.localEditorHasRetriever = !!definition.retrieverKey; - this.localEditorDisabled = false; - this.localEditorDisabledHint = ''; this.localEditorOpen = true; if (definition.retrieverKey) { @@ -188,8 +190,6 @@ export class GenericNodeComponent { if (missingDependencies.length > 0) { this.localEditorLoading = false; - this.localEditorDisabled = true; - this.localEditorDisabledHint = `Select ${missingDependencies.join(', ')} first`; return; } @@ -207,8 +207,6 @@ export class GenericNodeComponent { this.localEditorOptions = []; this.localEditorLoading = false; this.localEditorHasRetriever = false; - this.localEditorDisabled = false; - this.localEditorDisabledHint = ''; this.localEditorType = 'string'; this.localEditorMaxLength = null; } @@ -218,8 +216,6 @@ export class GenericNodeComponent { event?.stopPropagation(); if (!this.localEditorPath) return; - if (this.localEditorDisabled) return; - const config = this.ensureBlockConfiguration(); if (this.localEditorPath === 'name') { @@ -251,6 +247,7 @@ export class GenericNodeComponent { const contentKey = this.mainContentKey(); if (!contentKey) return; + if (!this.isPathVisible(contentKey)) return; const contentLabel = this.mainContentLabel(); const ui = this.getFieldUiMeta(contentKey); @@ -276,7 +273,13 @@ export class GenericNodeComponent { } nodeTitle(): string { - return this.isHumanNode() ? 'Human Task' : 'LLM Node'; + const type = this.blockType; + if (!type) return 'Node'; + if (type === 'HumanInteractionBlock') return 'Human Task'; + return type + .replace(/Block$/, '') + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') + .trim(); } mainContentLabel(): string { @@ -286,12 +289,13 @@ export class GenericNodeComponent { } hasMainContent(): boolean { - return !!this.mainContentKey(); + const contentKey = this.mainContentKey(); + return !!contentKey && this.isPathVisible(contentKey); } mainContentParts(): { text: string; isDynamicInput: boolean }[] { const contentKey = this.mainContentKey(); - if (!contentKey) return []; + if (!contentKey || !this.isPathVisible(contentKey)) return []; const content = toStringOrNull(this.getByPath(this.blockConfiguration ?? {}, contentKey)); if (!content) return []; const ui = this.getFieldUiMeta(contentKey); @@ -403,9 +407,12 @@ export class GenericNodeComponent { const definitions: EditableFieldDefinition[] = []; const seen = new Set(); - const contentKey = this.mainContentKey(); - const walk = (node: Record, pathPrefix: string) => { + const walk = ( + node: Record, + pathPrefix: string, + inheritedUi?: { visibleWhen: UiConditionRule[]; group: string | null } + ) => { const resolved = resolveSchemaRef(node, schema); if (!resolved || typeof resolved !== 'object') return; @@ -416,14 +423,17 @@ export class GenericNodeComponent { const childResolved = resolveSchemaRef(childSchema as Record, schema); const path = pathPrefix ? `${pathPrefix}.${key}` : key; const hasChildren = !!childResolved?.properties || childResolved?.type === 'object'; + const childUi = this.toFieldUiMeta(childResolved, inheritedUi); if (hasChildren) { - walk(childResolved as Record, path); + walk(childResolved as Record, path, { + visibleWhen: childUi.visibleWhen, + group: childUi.group + }); continue; } if (key === 'type' || key === 'name' || key.startsWith('__')) continue; - if (contentKey && path === contentKey) continue; if (seen.has(path)) continue; seen.add(path); @@ -434,7 +444,7 @@ export class GenericNodeComponent { retrieverBlockType: this.toRetrieverBlockType(childResolved), retrieverKey: this.toRetrieverKey(childResolved), retrieverDependsOn: this.toRetrieverDependsOn(childResolved, pathPrefix), - ui: this.toFieldUiMeta(childResolved) + ui: childUi }); } }; @@ -443,7 +453,10 @@ export class GenericNodeComponent { return definitions; } - private toFieldUiMeta(schema: Record | null | undefined) { + private toFieldUiMeta( + schema: Record | null | undefined, + inheritedUi?: { visibleWhen: UiConditionRule[]; group: string | null } + ) { const rawWidget = typeof schema?.['x-ui-widget'] === 'string' ? String(schema['x-ui-widget']).toLowerCase().trim() : ''; @@ -464,6 +477,8 @@ export class GenericNodeComponent { const structuralReason = typeof schema?.['x-ui-structural-reason'] === 'string' ? String(schema['x-ui-structural-reason']) : undefined; + const visibleWhen = readUiConditionRule(schema?.['x-ui-visible-when']); + const group = readUiGroup(schema?.['x-ui-group']) ?? inheritedUi?.group ?? null; return { widget: normalizedWidget, @@ -472,12 +487,36 @@ export class GenericNodeComponent { structuralReason, placeholder, tip, - rows + rows, + visibleWhen: [ + ...(inheritedUi?.visibleWhen ?? []), + ...(visibleWhen ? [visibleWhen] : []) + ], + group }; } private getFieldUiMeta(path: string) { - return this.toFieldUiMeta(this.resolveFieldSchema(path)); + const root = this.blockSchema; + if (!root) return this.toFieldUiMeta(null); + + let current: Record | null = root; + let inheritedUi = { visibleWhen: [] as UiConditionRule[], group: null as string | null }; + + for (const segment of path.split('.')) { + if (!current) return this.toFieldUiMeta(null, inheritedUi); + const resolved = resolveSchemaRef(current, root); + const properties = resolved?.properties as Record | undefined; + if (!properties || !properties[segment]) return this.toFieldUiMeta(null, inheritedUi); + current = resolveSchemaRef(properties[segment] as Record, root); + const nextUi = this.toFieldUiMeta(current, inheritedUi); + inheritedUi = { + visibleWhen: nextUi.visibleWhen, + group: nextUi.group + }; + } + + return this.toFieldUiMeta(current, inheritedUi); } private isStructuralField(path: string): boolean { @@ -580,6 +619,7 @@ export class GenericNodeComponent { private refreshParameterFields() { const config = this.blockConfiguration ?? {}; + const contentKey = this.mainContentKey(); const grouped = new Map(); const rootFields: EditableFieldView[] = []; @@ -591,32 +631,34 @@ export class GenericNodeComponent { label: definition.label, value: valueToDisplayString(value) }; - }); + }).filter((field) => field.path !== contentKey) + .filter((field) => this.isPathVisible(field.path)); for (const field of orderedFields) { - const parentKey = parentPath(field.path); - if (!parentKey) { + const groupLabel = this.getFieldUiMeta(field.path).group ?? parentPath(field.path); + const groupKey = groupLabel ? `group:${groupLabel}` : null; + if (!groupKey || !groupLabel) { rootFields.push(field); continue; } - if (!grouped.has(parentKey)) { - grouped.set(parentKey, []); + if (!grouped.has(groupKey)) { + grouped.set(groupKey, []); } - grouped.get(parentKey)!.push(field); + grouped.get(groupKey)!.push(field); } this.parameterFields = rootFields; this.parameterFieldGroups = Array.from(grouped.entries()).map(([key, fields]) => ({ key, - legend: pathToLabel(key), + legend: key.startsWith('group:') ? key.slice('group:'.length) : pathToLabel(key), fields })); this.refreshView(); return; } - const contentKey = this.mainContentKey(); const fallbackFields = flattenPrimitiveValues(config) .filter((entry) => entry.path !== 'name' && entry.path !== 'type' && entry.path !== contentKey) + .filter((entry) => this.isPathVisible(entry.path)) .map((entry) => ({ path: entry.path, label: pathToLabel(entry.path), @@ -710,24 +752,64 @@ export class GenericNodeComponent { } private mainContentKey(): string | null { - const config = this.blockConfiguration ?? {}; - if (Object.prototype.hasOwnProperty.call(config, 'actionDescription')) return 'actionDescription'; - if (Object.prototype.hasOwnProperty.call(config, 'prompt')) return 'prompt'; + const preferredPath = this.editableFieldDefinitions.find((field) => + field.ui.widget === 'textarea' + && field.ui.acceptVariableAsPlaceholder + && this.isPathVisible(field.path) + )?.path; + if (preferredPath) { + return preferredPath; + } - if (this.blockSchema?.['properties'] && typeof this.blockSchema['properties'] === 'object') { - const props = this.blockSchema['properties'] as Record; - if (Object.prototype.hasOwnProperty.call(props, 'actionDescription')) return 'actionDescription'; - if (Object.prototype.hasOwnProperty.call(props, 'prompt')) return 'prompt'; + const schemaCandidates = this.findMainContentCandidatePaths(this.blockSchema); + for (const candidate of schemaCandidates) { + if (this.isPathVisible(candidate)) return candidate; } return null; } + private findMainContentCandidatePaths(schema: Record | null): string[] { + if (!schema) return []; + + const paths: string[] = []; + const seen = new Set(); + + const walk = (node: Record, pathPrefix: string) => { + const resolved = resolveSchemaRef(node, schema); + if (!resolved || typeof resolved !== 'object') return; + + const properties = resolved.properties as Record | undefined; + if (!properties) return; + + for (const [key, childSchema] of Object.entries(properties)) { + const childResolved = resolveSchemaRef(childSchema as Record, schema); + const path = pathPrefix ? `${pathPrefix}.${key}` : key; + const hasChildren = !!childResolved?.properties || childResolved?.type === 'object'; + + if (hasChildren) { + walk(childResolved as Record, path); + continue; + } + + const rawWidget = typeof childResolved?.['x-ui-widget'] === 'string' + ? String(childResolved['x-ui-widget']).toLowerCase().trim() + : ''; + const isTextarea = rawWidget === 'textarea' || rawWidget === 'text-area'; + const acceptsVariable = childResolved?.['x-ui-accept-variable-as-placeholder'] === true; + if (!isTextarea || !acceptsVariable || seen.has(path)) continue; + + seen.add(path); + paths.push(path); + } + }; + + walk(schema, ''); + return paths; + } + private getByPath(source: Record, path: string): unknown { - return path.split('.').reduce((acc, key) => { - if (acc == null || typeof acc !== 'object') return undefined; - return (acc as Record)[key]; - }, source); + return getValueByPath(source, path); } private isMissingValue(value: unknown): boolean { @@ -741,7 +823,8 @@ export class GenericNodeComponent { const requiredFields = [ ...this.schemaRequirements.required, ...this.schemaRequirements.conditional.filter((field) => this.conditionalRequiredByPath.get(field.path)) - ].filter((field) => field.path !== 'name'); + ].filter((field) => field.path !== 'name') + .filter((field) => this.isFieldConditionSatisfied(field.path)); const missingFields = requiredFields .filter((field) => this.isMissingValue(this.getByPath(config, field.path))); @@ -872,4 +955,19 @@ export class GenericNodeComponent { nodeData['__createdOnServer'] = false; nodeData['__updateBlockError'] = null; } + + private isPathVisible(path: string): boolean { + return this.isFieldConditionSatisfied(path); + } + + private isFieldVisible(field: EditableFieldDefinition): boolean { + return this.isPathVisible(field.path); + } + + private isFieldConditionSatisfied(path: string): boolean { + const ui = this.getFieldUiMeta(path); + return ui.visibleWhen.every((rule) => + evaluateUiConditionRule(rule, this.blockConfiguration, (fieldPath) => this.resolveFieldSchema(fieldPath)) + ); + } } diff --git a/src/app/shared/nodes/node-utility.spec.ts b/src/app/shared/nodes/node-utility.spec.ts index 9b2a760..effec8a 100644 --- a/src/app/shared/nodes/node-utility.spec.ts +++ b/src/app/shared/nodes/node-utility.spec.ts @@ -1,9 +1,13 @@ import { + evaluateUiConditionRule, flattenPrimitiveValues, parentPath, pathToLabel, + readUiConditionRule, + readUiGroup, resolveSchemaRef, splitTemplatedTextParts, + getValueByPath, toStringOrNull, valueToDisplayString } from './node-utility'; @@ -55,10 +59,72 @@ describe('node-utility', () => { }; const refNode = { $ref: '#/definitions/Message' }; - expect(resolveSchemaRef(refNode, root)).toEqual({ type: 'string' }); + expect(resolveSchemaRef(refNode, root)).toEqual({ type: 'string', $ref: '#/definitions/Message' }); expect(resolveSchemaRef({ $ref: '#/definitions/Missing' }, root)).toEqual({ $ref: '#/definitions/Missing' }); }); + it('resolveSchemaRef keeps local x-ui metadata declared next to $ref', () => { + const root = { + definitions: { + LLMDescriptor: { + type: 'object', + properties: { + provider: { type: 'string' } + } + } + } + }; + + expect(resolveSchemaRef({ + $ref: '#/definitions/LLMDescriptor', + 'x-ui-group': 'llm', + 'x-ui-visible-when': { field: 'useLlm', equals: 'true' } + }, root)).toEqual({ + $ref: '#/definitions/LLMDescriptor', + type: 'object', + properties: { + provider: { type: 'string' } + }, + 'x-ui-group': 'llm', + 'x-ui-visible-when': { field: 'useLlm', equals: 'true' } + }); + }); + + it('getValueByPath resolves nested values', () => { + expect(getValueByPath({ llm: { enabled: true } }, 'llm.enabled')).toBe(true); + expect(getValueByPath({ llm: { enabled: true } }, 'llm.missing')).toBeUndefined(); + }); + + it('readUiConditionRule parses valid conditional metadata', () => { + expect(readUiConditionRule({ field: 'useLlm', equals: 'true' })).toEqual({ + field: 'useLlm', + equals: 'true' + }); + expect(readUiConditionRule({ field: '', equals: 'true' })).toBeNull(); + expect(readUiConditionRule({ field: 'useLlm', equals: true })).toBeNull(); + }); + + it('readUiGroup normalizes logical group labels', () => { + expect(readUiGroup(' llm ')).toBe('llm'); + expect(readUiGroup(' ')).toBeNull(); + expect(readUiGroup(10)).toBeNull(); + }); + + it('evaluateUiConditionRule uses schema semantics for booleans and numbers', () => { + const schemaByPath: Record> = { + useLlm: { type: 'boolean' }, + retries: { type: 'integer' }, + provider: { type: 'string' } + }; + const resolveFieldSchema = (path: string) => (schemaByPath[path] as Record) ?? null; + const config = { useLlm: true, retries: 3, provider: 'openai' }; + + expect(evaluateUiConditionRule({ field: 'useLlm', equals: 'true' }, config, resolveFieldSchema)).toBe(true); + expect(evaluateUiConditionRule({ field: 'useLlm', equals: 'false' }, config, resolveFieldSchema)).toBe(false); + expect(evaluateUiConditionRule({ field: 'retries', equals: '3' }, config, resolveFieldSchema)).toBe(true); + expect(evaluateUiConditionRule({ field: 'provider', equals: 'openai' }, config, resolveFieldSchema)).toBe(true); + }); + it('splitTemplatedTextParts identifies dynamic placeholders', () => { const parts = splitTemplatedTextParts('Hello ${{ user.name }}!'); expect(parts).toEqual([ diff --git a/src/app/shared/nodes/node-utility.ts b/src/app/shared/nodes/node-utility.ts index 9c58f71..2d3d069 100644 --- a/src/app/shared/nodes/node-utility.ts +++ b/src/app/shared/nodes/node-utility.ts @@ -1,4 +1,5 @@ export type TemplatedTextPart = { text: string; isDynamicInput: boolean }; +export type UiConditionRule = { field: string; equals: string }; export function toStringOrNull(value: unknown): string | null { if (typeof value === 'string' && value.trim().length > 0) return value; @@ -59,7 +60,68 @@ export function resolveSchemaRef(node: Record, root: Record)['field']; + const equals = (value as Record)['equals']; + if (typeof field !== 'string' || field.trim().length === 0) return null; + if (typeof equals !== 'string') return null; + + return { + field: field.trim(), + equals + }; +} + +export function readUiGroup(value: unknown): string | null { + if (typeof value !== 'string') return null; + const normalized = value.trim(); + return normalized.length ? normalized : null; +} + +export function getValueByPath(source: Record | null | undefined, path: string): unknown { + return path.split('.').reduce((acc, key) => { + if (acc == null || typeof acc !== 'object') return undefined; + return (acc as Record)[key]; + }, source ?? {}); +} + +export function evaluateUiConditionRule( + rule: UiConditionRule | null | undefined, + config: Record | null | undefined, + resolveFieldSchema?: (path: string) => Record | null +): boolean { + if (!rule) return true; + + const actualValue = getValueByPath(config, rule.field); + const schema = resolveFieldSchema?.(rule.field); + const schemaType = schema?.['type']; + const type = typeof schemaType === 'string' ? schemaType : null; + + if (type === 'boolean' || typeof actualValue === 'boolean') { + return actualValue === parseBooleanCondition(rule.equals); + } + + if (type === 'number' || type === 'integer' || typeof actualValue === 'number') { + const expected = Number(rule.equals); + return Number.isFinite(expected) && actualValue === expected; + } + + return String(actualValue ?? '') === rule.equals; +} + +function parseBooleanCondition(value: string): boolean { + return value.trim().toLowerCase() === 'true'; } export function splitTemplatedTextParts(text: string | null): TemplatedTextPart[] { 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 56979a9..42ecb84 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 @@ -5,9 +5,13 @@ import { ReteModule } from 'rete-angular-plugin/21'; import { BlocksService } from '@services/blocks/blocks'; import { NodeSettingsDialogService } from '@services/dialogs/node-settings-dialog'; import { + type UiConditionRule, + evaluateUiConditionRule, flattenPrimitiveValues, parentPath, pathToLabel, + readUiConditionRule, + readUiGroup, resolveSchemaRef, splitTemplatedTextParts, toStringOrNull, @@ -87,7 +91,8 @@ export class TaskStepNodeComponent { const config = this.blockConfiguration ?? {}; this.name = toStringOrNull(config['name']) ?? this.name; const primitiveEntries = flattenPrimitiveValues(config); - const contentEntry = this.pickMainContentEntry(primitiveEntries); + const visibleEntries = primitiveEntries.filter((entry) => this.isPathVisible(entry.path)); + const contentEntry = this.pickMainContentEntry(visibleEntries); this.mainContent = contentEntry ? { @@ -99,7 +104,7 @@ export class TaskStepNodeComponent { const grouped = new Map(); const rootFields: DisplayField[] = []; - const orderedFields = primitiveEntries + const orderedFields = visibleEntries .filter((entry) => !['name', 'type'].includes(entry.path)) .filter((entry) => entry.path !== contentEntry?.path) .map((entry) => ({ @@ -109,21 +114,22 @@ export class TaskStepNodeComponent { })); for (const field of orderedFields) { - const parentKey = parentPath(field.path); - if (!parentKey) { + const groupLabel = this.groupLabelForPath(field.path); + const groupKey = groupLabel ? `group:${groupLabel}` : null; + if (!groupKey || !groupLabel) { rootFields.push(field); continue; } - if (!grouped.has(parentKey)) { - grouped.set(parentKey, []); + if (!grouped.has(groupKey)) { + grouped.set(groupKey, []); } - grouped.get(parentKey)!.push(field); + grouped.get(groupKey)!.push(field); } this.parameterFields = rootFields; this.parameterFieldGroups = Array.from(grouped.entries()).map(([key, fields]) => ({ key, - legend: pathToLabel(key), + legend: key.startsWith('group:') ? key.slice('group:'.length) : pathToLabel(key), fields })); @@ -409,4 +415,70 @@ export class TaskStepNodeComponent { }); } + private isPathVisible(path: string): boolean { + const ui = this.getFieldUiMeta(path); + return ui.visibleWhen.every((rule) => + evaluateUiConditionRule(rule, this.blockConfiguration, (fieldPath) => this.resolveFieldSchema(fieldPath)) + ); + } + + private groupLabelForPath(path: string): string | null { + return this.getFieldUiMeta(path).group ?? parentPath(path); + } + + private resolveFieldSchema(path: string): Record | null { + const root = this.blockSchema; + if (!root) return null; + + let current: Record | null = root; + for (const segment of path.split('.')) { + if (!current) return null; + const resolved = resolveSchemaRef(current, root); + const properties = resolved?.properties as Record | undefined; + if (!properties || !properties[segment]) return null; + current = resolveSchemaRef(properties[segment] as Record, root); + } + + return current; + } + + private getFieldUiMeta(path: string) { + const root = this.blockSchema; + if (!root) return this.toFieldUiMeta(null); + + let current: Record | null = root; + let inheritedUi = { visibleWhen: [] as UiConditionRule[], group: null as string | null }; + + for (const segment of path.split('.')) { + if (!current) return this.toFieldUiMeta(null, inheritedUi); + const resolved = resolveSchemaRef(current, root); + const properties = resolved?.properties as Record | undefined; + if (!properties || !properties[segment]) return this.toFieldUiMeta(null, inheritedUi); + current = resolveSchemaRef(properties[segment] as Record, root); + const nextUi = this.toFieldUiMeta(current, inheritedUi); + inheritedUi = { + visibleWhen: nextUi.visibleWhen, + group: nextUi.group + }; + } + + return this.toFieldUiMeta(current, inheritedUi); + } + + private toFieldUiMeta( + schema: Record | null | undefined, + inheritedUi?: { visibleWhen: UiConditionRule[]; group: string | null } + ) { + const visibleWhen = readUiConditionRule(schema?.['x-ui-visible-when']); + const group = readUiGroup(schema?.['x-ui-group']) ?? inheritedUi?.group ?? null; + + return { + visibleWhen: [ + ...(inheritedUi?.visibleWhen ?? []), + ...(visibleWhen ? [visibleWhen] : []) + ], + group + }; + } + }