Refactor schema-driven node fields and fix validation highlights
This commit is contained in:
parent
e6f6cda845
commit
a377c1d793
|
|
@ -0,0 +1,65 @@
|
|||
import { normalizeFlowValidationErrors } from './flow';
|
||||
|
||||
describe('normalizeFlowValidationErrors', () => {
|
||||
it('keeps explicit related node ids when provided', () => {
|
||||
expect(
|
||||
normalizeFlowValidationErrors([{
|
||||
entity: 'flow',
|
||||
id: 'flow-1',
|
||||
message: 'Validation error',
|
||||
relatedNodeIds: ['node-1', 'node-2', 'node-1']
|
||||
}])
|
||||
).toEqual([{
|
||||
code: null,
|
||||
entity: 'flow',
|
||||
id: 'flow-1',
|
||||
field: null,
|
||||
message: 'Validation error',
|
||||
relatedNodeIds: ['node-1', 'node-2']
|
||||
}]);
|
||||
});
|
||||
|
||||
it('derives node ids from block and container validation errors', () => {
|
||||
expect(
|
||||
normalizeFlowValidationErrors([
|
||||
{ entity: 'block', id: 'block-1', message: 'Broken block' },
|
||||
{ entity: 'container', id: 'container-1', message: 'Broken container' }
|
||||
])
|
||||
).toEqual([
|
||||
{
|
||||
code: null,
|
||||
entity: 'block',
|
||||
id: 'block-1',
|
||||
field: null,
|
||||
message: 'Broken block',
|
||||
relatedNodeIds: ['block-1']
|
||||
},
|
||||
{
|
||||
code: null,
|
||||
entity: 'container',
|
||||
id: 'container-1',
|
||||
field: null,
|
||||
message: 'Broken container',
|
||||
relatedNodeIds: ['container-1']
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not highlight flow-level validation errors without node references', () => {
|
||||
expect(
|
||||
normalizeFlowValidationErrors([{
|
||||
entity: 'flow',
|
||||
id: 'flow-1',
|
||||
field: 'globalInputs',
|
||||
message: 'Missing global input'
|
||||
}])
|
||||
).toEqual([{
|
||||
code: null,
|
||||
entity: 'flow',
|
||||
id: 'flow-1',
|
||||
field: 'globalInputs',
|
||||
message: 'Missing global input',
|
||||
relatedNodeIds: []
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
|
@ -145,12 +145,29 @@ export function normalizeFlowValidationErrors(raw: unknown): FlowValidationError
|
|||
id: typeof item['id'] === 'string' ? item['id'] : null,
|
||||
field: typeof item['field'] === 'string' ? item['field'] : null,
|
||||
message: String(item['message'] ?? item['error'] ?? 'Validation error'),
|
||||
relatedNodeIds: Array.isArray(item['relatedNodeIds'])
|
||||
? item['relatedNodeIds'].map((value) => String(value)).filter((value) => value.length > 0)
|
||||
: []
|
||||
relatedNodeIds: normalizeRelatedNodeIds(item)
|
||||
}));
|
||||
}
|
||||
|
||||
function normalizeRelatedNodeIds(item: Record<string, unknown>): string[] {
|
||||
const explicitNodeIds = Array.isArray(item['relatedNodeIds'])
|
||||
? item['relatedNodeIds'].map((value) => String(value)).filter((value) => value.length > 0)
|
||||
: [];
|
||||
|
||||
if (explicitNodeIds.length > 0) {
|
||||
return Array.from(new Set(explicitNodeIds));
|
||||
}
|
||||
|
||||
const entity = typeof item['entity'] === 'string' ? item['entity'].toLowerCase() : '';
|
||||
const id = typeof item['id'] === 'string' ? item['id'].trim() : '';
|
||||
|
||||
if ((entity === 'block' || entity === 'container') && id.length > 0) {
|
||||
return [id];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export type FlowBlockConfiguration =
|
||||
| LLMBlockConfiguration
|
||||
| HumanInteractiveBlockConfiguration
|
||||
|
|
|
|||
|
|
@ -84,11 +84,29 @@
|
|||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.06);
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.validation-item-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.validation-item-clickable:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(220, 38, 38, 0.42);
|
||||
box-shadow: 0 14px 28px rgba(127, 29, 29, 0.12);
|
||||
}
|
||||
|
||||
.validation-item-clickable:focus-visible {
|
||||
outline: none;
|
||||
border-color: #dc2626;
|
||||
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.18), 0 14px 28px rgba(127, 29, 29, 0.12);
|
||||
}
|
||||
|
||||
.validation-item-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
|
|
@ -98,12 +116,15 @@
|
|||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
max-width: 100%;
|
||||
border-radius: 999px;
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.06em;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
|
|
|
|||
|
|
@ -22,14 +22,17 @@
|
|||
|
||||
<section class="validation-list">
|
||||
@for (error of errors(); track trackByError($index, error)) {
|
||||
<article class="validation-item">
|
||||
<article
|
||||
class="validation-item"
|
||||
[class.validation-item-clickable]="canFocusError(error)"
|
||||
[attr.role]="canFocusError(error) ? 'button' : null"
|
||||
[attr.tabindex]="canFocusError(error) ? 0 : null"
|
||||
[attr.aria-label]="canFocusError(error) ? 'Highlight related nodes for this validation error' : null"
|
||||
(click)="focusError(error)"
|
||||
(keydown.enter)="focusError(error)"
|
||||
(keydown.space)="focusError(error); $event.preventDefault()">
|
||||
<div class="validation-item-head">
|
||||
<span class="validation-code">{{ error.code || 'VALIDATION_ERROR' }}</span>
|
||||
@if ((error.relatedNodeIds?.length ?? 0) > 0) {
|
||||
<button type="button" class="validation-focus" mat-stroked-button (click)="focusError(error)">
|
||||
Highlight
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<p class="validation-message">{{ error.message }}</p>
|
||||
</article>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,12 @@ export class FlowValidationPanel {
|
|||
return `${error.code ?? 'VALIDATION_ERROR'}:${error.entity ?? ''}:${error.id ?? ''}:${error.field ?? ''}:${error.message}`;
|
||||
}
|
||||
|
||||
canFocusError(error: FlowValidationError): boolean {
|
||||
return (error.relatedNodeIds?.length ?? 0) > 0;
|
||||
}
|
||||
|
||||
focusError(error: FlowValidationError) {
|
||||
if (!this.canFocusError(error)) return;
|
||||
const nodeIds = Array.isArray(error.relatedNodeIds) ? error.relatedNodeIds : [];
|
||||
this.editorState.setHighlightedValidationNodes(nodeIds);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -397,6 +397,34 @@
|
|||
padding: 0 14px 14px;
|
||||
}
|
||||
|
||||
.container-node__params--grouped {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.container-node__param-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.container-node__param-fieldset {
|
||||
margin: 0 14px;
|
||||
padding: 12px;
|
||||
border: 1px solid #dbe2ea;
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.container-node__param-legend {
|
||||
padding: 0 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.container-node__param-chip {
|
||||
min-width: 0;
|
||||
border: 1px solid #dbe2ea;
|
||||
|
|
|
|||
|
|
@ -208,6 +208,94 @@
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (parameterFieldGroups.length) {
|
||||
<div class="container-node__param-groups">
|
||||
@for (group of parameterFieldGroups; track group.key) {
|
||||
<fieldset class="container-node__param-fieldset">
|
||||
<legend class="container-node__param-legend">{{ group.legend }}</legend>
|
||||
@if (group.fields.length) {
|
||||
<div class="container-node__params container-node__params--grouped">
|
||||
@for (field of group.fields; track field.path) {
|
||||
<div class="container-node__param-chip" [class.container-node__param-chip--wide]="field.wide" [class.container-node__param-chip--disabled]="!field.enabled">
|
||||
<div class="container-node__param-head">
|
||||
<span class="container-node__param-key">{{ field.label }}</span>
|
||||
@if (field.type === 'boolean' && !isReadonly) {
|
||||
<button
|
||||
type="button"
|
||||
class="container-node__bool-toggle"
|
||||
[class.container-node__bool-toggle--on]="field.booleanValue"
|
||||
[disabled]="!field.enabled"
|
||||
[attr.aria-pressed]="field.booleanValue"
|
||||
title="Toggle field"
|
||||
(pointerdown)="$event.stopPropagation()"
|
||||
(click)="toggleBooleanParameter(field.path, $event)">
|
||||
<span class="container-node__bool-toggle-thumb"></span>
|
||||
</button>
|
||||
} @else if (!isReadonly) {
|
||||
<button
|
||||
type="button"
|
||||
class="container-node__param-edit"
|
||||
[disabled]="!field.enabled"
|
||||
title="Edit field"
|
||||
(pointerdown)="$event.stopPropagation()"
|
||||
(click)="openParameterEditor(field.path, $event)">
|
||||
<i class="bi bi-pen"></i>
|
||||
</button>
|
||||
} @else if (field.expandable) {
|
||||
<button
|
||||
type="button"
|
||||
class="llm-param-view-btn"
|
||||
aria-label="View full value"
|
||||
(pointerdown)="$event.stopPropagation()"
|
||||
(click)="openFieldPreview(field, $event)">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<span class="container-node__param-value" [class.container-node__param-value--clamped]="field.expandable">{{ field.type === 'boolean' ? (field.booleanValue ? 'Enabled' : 'Disabled') : field.value }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@for (contentField of group.richContentFields; track contentField.path) {
|
||||
<div class="container-node__param-chip container-node__param-chip--wide">
|
||||
<div class="container-node__param-head">
|
||||
<span class="container-node__param-key">{{ contentField.label }}</span>
|
||||
@if (contentField.expandable && isReadonly) {
|
||||
<button
|
||||
type="button"
|
||||
class="llm-param-view-btn"
|
||||
aria-label="View full value"
|
||||
(pointerdown)="$event.stopPropagation()"
|
||||
(click)="openMainContentPreview(contentField, $event)">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
} @else if (!isReadonly) {
|
||||
<button
|
||||
type="button"
|
||||
class="container-node__param-edit"
|
||||
title="Edit field"
|
||||
(pointerdown)="$event.stopPropagation()"
|
||||
(click)="openParameterEditor(contentField.path, $event)">
|
||||
<i class="bi bi-pen"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="container-node__param-value" [class.container-node__param-value--clamped]="contentField.expandable">
|
||||
@for (part of contentField.parts; track $index) {
|
||||
@if (part.isDynamicInput) {
|
||||
<span class="llm-inline-token">{{ formatDynamicInputToken(part.text) }}</span>
|
||||
} @else {
|
||||
<span>{{ part.text }}</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</fieldset>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@for (field of parameterFields; track field.path) {
|
||||
<div class="container-node__param-chip" [class.container-node__param-chip--wide]="field.wide" [class.container-node__param-chip--disabled]="!field.enabled">
|
||||
<div class="container-node__param-head">
|
||||
|
|
@ -261,6 +349,15 @@
|
|||
(click)="openMainContentPreview(contentField, $event)">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
} @else if (!isReadonly) {
|
||||
<button
|
||||
type="button"
|
||||
class="container-node__param-edit"
|
||||
title="Edit field"
|
||||
(pointerdown)="$event.stopPropagation()"
|
||||
(click)="openParameterEditor(contentField.path, $event)">
|
||||
<i class="bi bi-pen"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="container-node__param-value" [class.container-node__param-value--clamped]="contentField.expandable">
|
||||
|
|
|
|||
|
|
@ -13,15 +13,28 @@ import { EditorStateHolder } from '@stores/flow-editor';
|
|||
import { CONTAINER_SUBFLOW_DRAG_MIME } from './container-node-drag';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { extractSchemaRequirements, SchemaRequirements } from '../schema-requirements';
|
||||
import { evaluateUiConditionRule, getValueByPath, parentPath, pathToLabel, readEffectiveUiVisibleConditionRule, readUiConditionRule, resolveNodeIcon, resolveSchemaPath, resolveSchemaRef, schemaFieldLabel, shouldSkipSchemaField, splitTemplatedTextParts, valueToDisplayString } from '../node-utility';
|
||||
import { type UiConditionRule, evaluateUiConditionRule, getValueByPath, parentPath, pathToLabel, resolveNodeIcon, resolveSchemaPath, schemaFieldLabel, splitTemplatedTextParts, valueToDisplayString } from '../node-utility';
|
||||
import {
|
||||
type SchemaFieldType,
|
||||
type SchemaFieldGroup,
|
||||
type SchemaFieldUiMeta,
|
||||
type SchemaNodeOptionsSource,
|
||||
buildTemplatedRichContentParts,
|
||||
collectSchemaLeafFields,
|
||||
getSchemaPathUiMeta,
|
||||
groupSchemaFields,
|
||||
isLongTextValue,
|
||||
isSchemaPathEnabled,
|
||||
isSchemaPathVisible,
|
||||
schemaEnumOptions,
|
||||
schemaFieldTypeFromSchema,
|
||||
schemaNodeOptionsSource,
|
||||
toSchemaFieldUiMeta
|
||||
} from '../schema-driven-fields';
|
||||
|
||||
type ContainerFieldType = 'string' | 'number' | 'integer' | 'boolean' | 'unknown';
|
||||
type ContainerFieldType = SchemaFieldType;
|
||||
|
||||
type NodeOptionsSource = {
|
||||
collection: 'inputs' | 'outputs';
|
||||
valueField: string;
|
||||
labelField: string;
|
||||
};
|
||||
type NodeOptionsSource = SchemaNodeOptionsSource;
|
||||
|
||||
type ContainerFieldDefinition = {
|
||||
path: string;
|
||||
|
|
@ -31,7 +44,12 @@ type ContainerFieldDefinition = {
|
|||
nodeOptionsSource: NodeOptionsSource | null;
|
||||
widget: 'textarea' | null;
|
||||
structural: boolean;
|
||||
enabledWhen: ReturnType<typeof readUiConditionRule>[];
|
||||
visibleWhen: UiConditionRule[];
|
||||
enabledWhen: UiConditionRule[];
|
||||
group: string | null;
|
||||
placeholder?: string;
|
||||
tip?: string;
|
||||
rows?: number;
|
||||
};
|
||||
|
||||
type ContainerFieldView = {
|
||||
|
|
@ -53,6 +71,8 @@ type RichContentView = {
|
|||
parts: { text: string; isDynamicInput: boolean }[];
|
||||
};
|
||||
|
||||
type ContainerFieldGroupView = SchemaFieldGroup<ContainerFieldView, RichContentView>;
|
||||
|
||||
type StructuredRetrieverConfig = {
|
||||
retrieverName: string;
|
||||
retrieverUrl: string;
|
||||
|
|
@ -86,6 +106,7 @@ export class ContainerNodeComponent {
|
|||
importLoading = false;
|
||||
private importErrorMessage: string | null = null;
|
||||
parameterFields: ContainerFieldView[] = [];
|
||||
parameterFieldGroups: ContainerFieldGroupView[] = [];
|
||||
richContentFields: RichContentView[] = [];
|
||||
schemaReady = false;
|
||||
private schemaLoading = false;
|
||||
|
|
@ -268,11 +289,11 @@ export class ContainerNodeComponent {
|
|||
}
|
||||
|
||||
hasParameterFields() {
|
||||
return this.parameterFields.length > 0;
|
||||
return this.parameterFields.length > 0 || this.parameterFieldGroups.some((group) => group.fields.length > 0);
|
||||
}
|
||||
|
||||
hasMainContent() {
|
||||
return this.richContentFields.length > 0;
|
||||
return this.richContentFields.length > 0 || this.parameterFieldGroups.some((group) => group.richContentFields.length > 0);
|
||||
}
|
||||
|
||||
formatDynamicInputToken(token: string): string {
|
||||
|
|
@ -440,7 +461,9 @@ export class ContainerNodeComponent {
|
|||
label: definition.label,
|
||||
type: this.toDialogFieldType(definition),
|
||||
required: this.missingRequiredParams.includes(definition.label),
|
||||
rows: definition.widget === 'textarea' ? 6 : undefined,
|
||||
placeholder: definition.placeholder,
|
||||
tip: definition.tip,
|
||||
rows: definition.widget === 'textarea' ? definition.rows ?? 6 : undefined,
|
||||
options: this.resolveSelectableOptions(definition)
|
||||
};
|
||||
|
||||
|
|
@ -760,7 +783,7 @@ export class ContainerNodeComponent {
|
|||
private refreshParameterFields() {
|
||||
const config = this.configuration ?? {};
|
||||
const richContentPaths = new Set(this.richContentPaths());
|
||||
this.richContentFields = this.richContentPaths()
|
||||
const allRichContentFields = this.richContentPaths()
|
||||
.filter((path) => this.isFieldVisible(path))
|
||||
.map((path) => {
|
||||
const rawValue = String(getValueByPath(config, path) ?? '');
|
||||
|
|
@ -768,13 +791,13 @@ export class ContainerNodeComponent {
|
|||
path,
|
||||
label: this.containerFieldDefinitions.find((field) => field.path === path)?.label ?? pathToLabel(path),
|
||||
rawValue,
|
||||
expandable: this.isLongTextValue(rawValue),
|
||||
expandable: isLongTextValue(rawValue),
|
||||
parts: this.toRichContentParts(path)
|
||||
};
|
||||
})
|
||||
.filter((field) => field.parts.length > 0);
|
||||
|
||||
this.parameterFields = this.containerFieldDefinitions
|
||||
const orderedFields = this.containerFieldDefinitions
|
||||
.filter((field) => !this.isContainerTypeField(field.path))
|
||||
.filter((field) => this.isFieldVisible(field.path))
|
||||
.filter((field) => !richContentPaths.has(field.path))
|
||||
|
|
@ -783,55 +806,44 @@ export class ContainerNodeComponent {
|
|||
label: field.label,
|
||||
value: valueToDisplayString(getValueByPath(config, field.path)),
|
||||
wide: field.widget === 'textarea' || field.label.length >= 18,
|
||||
expandable: this.isLongTextValue(valueToDisplayString(getValueByPath(config, field.path))),
|
||||
expandable: isLongTextValue(valueToDisplayString(getValueByPath(config, field.path))),
|
||||
enabled: this.isFieldEnabled(field.path),
|
||||
type: field.type,
|
||||
booleanValue: getValueByPath(config, field.path) === true
|
||||
}));
|
||||
|
||||
const grouped = groupSchemaFields({
|
||||
fields: orderedFields,
|
||||
richContentFields: allRichContentFields,
|
||||
resolveGroupLabel: (path) => getSchemaPathUiMeta(this.containerSchema, path).group ?? parentPath(path)
|
||||
});
|
||||
|
||||
this.parameterFields = grouped.rootFields;
|
||||
this.richContentFields = grouped.rootRichContentFields;
|
||||
this.parameterFieldGroups = grouped.groups;
|
||||
}
|
||||
|
||||
private buildContainerFieldDefinitions(schema: Record<string, any> | null): ContainerFieldDefinition[] {
|
||||
if (!schema) return [];
|
||||
return collectSchemaLeafFields(schema, ({ key, path, schema: childResolved, ui }) => {
|
||||
if (key.startsWith('__')) return null;
|
||||
if (path === 'name' || path === 'subFlow' || this.isContainerTypeField(path)) return null;
|
||||
|
||||
const definitions: ContainerFieldDefinition[] = [];
|
||||
const walk = (node: Record<string, any>, pathPrefix: string) => {
|
||||
const resolved = resolveSchemaRef(node, schema);
|
||||
if (!resolved || typeof resolved !== 'object') return;
|
||||
|
||||
const properties = resolved.properties as Record<string, any> | undefined;
|
||||
if (!properties) return;
|
||||
|
||||
for (const [key, childSchema] of Object.entries(properties)) {
|
||||
const childResolved = resolveSchemaRef(childSchema as Record<string, any>, schema);
|
||||
if (shouldSkipSchemaField(key, childResolved)) continue;
|
||||
const path = pathPrefix ? `${pathPrefix}.${key}` : key;
|
||||
if (path === 'name' || path === 'subFlow' || this.isContainerTypeField(path)) continue;
|
||||
|
||||
const hasChildren = !!childResolved?.properties || childResolved?.type === 'object';
|
||||
if (hasChildren) {
|
||||
walk(childResolved as Record<string, any>, path);
|
||||
continue;
|
||||
}
|
||||
|
||||
definitions.push({
|
||||
path,
|
||||
label: schemaFieldLabel(path, childResolved),
|
||||
type: this.toFieldType(childResolved),
|
||||
enumOptions: Array.isArray(childResolved?.enum)
|
||||
? childResolved.enum.filter((item: unknown): item is string => typeof item === 'string')
|
||||
: [],
|
||||
nodeOptionsSource: this.toNodeOptionsSource(childResolved),
|
||||
widget: typeof childResolved?.['x-ui-widget'] === 'string' && String(childResolved['x-ui-widget']).toLowerCase() === 'textarea'
|
||||
? 'textarea'
|
||||
: null,
|
||||
structural: childResolved?.['x-ui-structural'] === true,
|
||||
enabledWhen: [readUiConditionRule(childResolved?.['x-ui-enabled-when'])].filter((rule) => !!rule)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
walk(schema, '');
|
||||
return definitions;
|
||||
return {
|
||||
path,
|
||||
label: schemaFieldLabel(path, childResolved),
|
||||
type: this.toFieldType(childResolved),
|
||||
enumOptions: schemaEnumOptions(childResolved),
|
||||
nodeOptionsSource: this.toNodeOptionsSource(childResolved),
|
||||
widget: ui.widget,
|
||||
structural: ui.structural,
|
||||
visibleWhen: ui.visibleWhen,
|
||||
enabledWhen: ui.enabledWhen,
|
||||
group: ui.group,
|
||||
placeholder: ui.placeholder,
|
||||
tip: ui.tip,
|
||||
rows: ui.rows
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private richContentPaths(): string[] {
|
||||
|
|
@ -852,49 +864,23 @@ export class ContainerNodeComponent {
|
|||
}
|
||||
|
||||
private toRichContentParts(path: string): { text: string; isDynamicInput: boolean }[] {
|
||||
const content = String(getValueByPath(this.configuration ?? {}, path) ?? '').trim();
|
||||
if (!content) return [];
|
||||
|
||||
const schema = this.resolveFieldSchema(path);
|
||||
if (schema?.['x-ui-accept-variable-as-placeholder'] === true) {
|
||||
return splitTemplatedTextParts(content);
|
||||
}
|
||||
|
||||
return [{ text: content, isDynamicInput: false }];
|
||||
return buildTemplatedRichContentParts(this.configuration ?? {}, path, this.containerSchema, splitTemplatedTextParts);
|
||||
}
|
||||
|
||||
private isFieldVisible(path: string, visited = new Set<string>()): boolean {
|
||||
if (visited.has(path)) return true;
|
||||
visited.add(path);
|
||||
|
||||
const schema = this.resolveFieldSchema(path);
|
||||
const rule = readEffectiveUiVisibleConditionRule(schema);
|
||||
if (!rule) return true;
|
||||
|
||||
return evaluateUiConditionRule(rule, this.configuration ?? {}, (fieldPath) => this.resolveFieldSchema(fieldPath));
|
||||
return isSchemaPathVisible(this.containerSchema, path, this.configuration ?? {});
|
||||
}
|
||||
|
||||
private isFieldEnabled(path: string, visited = new Set<string>()): boolean {
|
||||
if (visited.has(path)) return true;
|
||||
visited.add(path);
|
||||
|
||||
const parent = parentPath(path);
|
||||
if (parent && !this.isFieldEnabled(parent, visited)) return false;
|
||||
|
||||
const schema = this.resolveFieldSchema(path);
|
||||
const rules = [readUiConditionRule(schema?.['x-ui-enabled-when'])].filter((rule) => !!rule);
|
||||
return rules.every((rule) => {
|
||||
if (!rule) return true;
|
||||
return evaluateUiConditionRule(rule, this.configuration ?? {}, (fieldPath) => this.resolveFieldSchema(fieldPath));
|
||||
});
|
||||
return isSchemaPathEnabled(this.containerSchema, path, this.configuration ?? {});
|
||||
}
|
||||
|
||||
private toFieldType(schema: Record<string, any> | null): ContainerFieldType {
|
||||
const type = typeof schema?.['type'] === 'string' ? String(schema['type']) : 'unknown';
|
||||
if (type === 'string' || type === 'number' || type === 'integer' || type === 'boolean') {
|
||||
return type;
|
||||
}
|
||||
return 'unknown';
|
||||
return schemaFieldTypeFromSchema(schema);
|
||||
}
|
||||
|
||||
private toDialogFieldType(definition: ContainerFieldDefinition): NodeSettingField['type'] {
|
||||
|
|
@ -905,26 +891,7 @@ export class ContainerNodeComponent {
|
|||
}
|
||||
|
||||
private toNodeOptionsSource(schema: Record<string, any> | null | undefined): NodeOptionsSource | null {
|
||||
if (!schema || typeof schema !== 'object') return null;
|
||||
const raw = schema['x-ui-options-from-node'];
|
||||
if (!raw || typeof raw !== 'object') return null;
|
||||
|
||||
const collection = (raw as Record<string, unknown>)['collection'];
|
||||
const valueField = (raw as Record<string, unknown>)['valueField'];
|
||||
const labelField = (raw as Record<string, unknown>)['labelField'];
|
||||
if ((collection !== 'inputs' && collection !== 'outputs')
|
||||
|| typeof valueField !== 'string'
|
||||
|| typeof labelField !== 'string'
|
||||
|| valueField.trim().length === 0
|
||||
|| labelField.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
collection,
|
||||
valueField: valueField.trim(),
|
||||
labelField: labelField.trim()
|
||||
};
|
||||
return schemaNodeOptionsSource(schema);
|
||||
}
|
||||
|
||||
private resolveSelectableOptions(definition: ContainerFieldDefinition): NodeSettingOption[] | undefined {
|
||||
|
|
@ -953,10 +920,6 @@ export class ContainerNodeComponent {
|
|||
.filter((option: NodeSettingOption | null): option is NodeSettingOption => option != null);
|
||||
}
|
||||
|
||||
private isLongTextValue(value: string): boolean {
|
||||
return String(value ?? '').trim().length > 80;
|
||||
}
|
||||
|
||||
private getEditorInitialValue(definition: ContainerFieldDefinition): string | boolean {
|
||||
const raw = getValueByPath(this.configuration ?? {}, definition.path);
|
||||
if (definition.type === 'boolean') return raw === true;
|
||||
|
|
@ -1120,4 +1083,15 @@ export class ContainerNodeComponent {
|
|||
return resolveSchemaPath(this.containerSchema, path);
|
||||
}
|
||||
|
||||
private toFieldUiMeta(
|
||||
schema: Record<string, any> | null | undefined,
|
||||
inheritedUi?: Pick<SchemaFieldUiMeta, 'visibleWhen' | 'enabledWhen' | 'group'>
|
||||
) {
|
||||
return toSchemaFieldUiMeta(schema, inheritedUi);
|
||||
}
|
||||
|
||||
private getFieldUiMeta(path: string) {
|
||||
return getSchemaPathUiMeta(this.containerSchema, path);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,9 +29,6 @@ import {
|
|||
parentPath,
|
||||
pathToLabel,
|
||||
readUiConditionRule,
|
||||
readEffectiveUiVisibleConditionRule,
|
||||
readUiLabel,
|
||||
readUiGroup,
|
||||
resolveNodeIcon,
|
||||
resolveSchemaRef,
|
||||
resolveSchemaPath,
|
||||
|
|
@ -43,8 +40,24 @@ import {
|
|||
validateUniqueByConstraint,
|
||||
valueToDisplayString
|
||||
} from '../node-utility';
|
||||
import {
|
||||
type SchemaFieldType,
|
||||
type SchemaFieldUiMeta,
|
||||
type SchemaNodeOptionsSource,
|
||||
buildTemplatedRichContentParts,
|
||||
collectSchemaLeafFields,
|
||||
getSchemaPathUiMeta,
|
||||
groupSchemaFields,
|
||||
isLongTextValue,
|
||||
isSchemaPathEnabled,
|
||||
isSchemaPathVisible,
|
||||
schemaEnumOptions,
|
||||
schemaFieldTypeFromSchema,
|
||||
schemaNodeOptionsSource,
|
||||
toSchemaFieldUiMeta
|
||||
} from '../schema-driven-fields';
|
||||
|
||||
type FieldType = 'string' | 'number' | 'integer' | 'boolean' | 'unknown';
|
||||
type FieldType = SchemaFieldType;
|
||||
|
||||
type RetrieverDependency = {
|
||||
key: string;
|
||||
|
|
@ -52,11 +65,7 @@ type RetrieverDependency = {
|
|||
source: 'field' | 'context';
|
||||
};
|
||||
|
||||
type NodeOptionsSource = {
|
||||
collection: 'inputs' | 'outputs';
|
||||
valueField: string;
|
||||
labelField: string;
|
||||
};
|
||||
type NodeOptionsSource = SchemaNodeOptionsSource;
|
||||
|
||||
type EditableFieldDefinition = {
|
||||
path: string;
|
||||
|
|
@ -69,23 +78,7 @@ type EditableFieldDefinition = {
|
|||
retrieverUrl: string | null;
|
||||
retrieverStructuredData: boolean;
|
||||
retrieverDependsOn: RetrieverDependency[];
|
||||
ui: {
|
||||
widget: 'textarea' | null;
|
||||
acceptVariableAsPlaceholder: boolean;
|
||||
structural: boolean;
|
||||
bindableAsInput: boolean;
|
||||
inputName: string | null;
|
||||
inputType: string | null;
|
||||
inputMultiple: boolean | null;
|
||||
structuralReason?: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
tip?: string;
|
||||
rows?: number;
|
||||
visibleWhen: UiConditionRule[];
|
||||
enabledWhen: UiConditionRule[];
|
||||
group: string | null;
|
||||
};
|
||||
ui: SchemaFieldUiMeta;
|
||||
};
|
||||
|
||||
type EditableFieldView = {
|
||||
|
|
@ -670,221 +663,52 @@ export class GenericNodeComponent {
|
|||
}
|
||||
|
||||
private buildEditableFieldDefinitions(schema: Record<string, any> | null): EditableFieldDefinition[] {
|
||||
if (!schema) return [];
|
||||
return collectSchemaLeafFields(schema, ({ key, path, pathPrefix, schema: childResolved, ui }) => {
|
||||
if (childResolved?.['type'] === 'array') return null;
|
||||
if (key === 'type' || key === 'name' || key.startsWith('__')) return null;
|
||||
|
||||
const definitions: EditableFieldDefinition[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
const walk = (
|
||||
node: Record<string, any>,
|
||||
pathPrefix: string,
|
||||
inheritedUi?: { visibleWhen: UiConditionRule[]; enabledWhen: UiConditionRule[]; group: string | null }
|
||||
) => {
|
||||
const resolved = resolveSchemaRef(node, schema);
|
||||
if (!resolved || typeof resolved !== 'object') return;
|
||||
|
||||
const properties = resolved.properties as Record<string, any> | undefined;
|
||||
if (!properties) return;
|
||||
|
||||
for (const [key, childSchema] of Object.entries(properties)) {
|
||||
const childResolved = resolveSchemaRef(childSchema as Record<string, any>, schema);
|
||||
const path = pathPrefix ? `${pathPrefix}.${key}` : key;
|
||||
if (childResolved?.type === 'array') {
|
||||
continue;
|
||||
}
|
||||
const hasChildren = !!childResolved?.properties || childResolved?.type === 'object';
|
||||
const childUi = this.toFieldUiMeta(childResolved, inheritedUi);
|
||||
|
||||
if (hasChildren) {
|
||||
walk(childResolved as Record<string, any>, path, {
|
||||
visibleWhen: childUi.visibleWhen,
|
||||
enabledWhen: childUi.enabledWhen,
|
||||
group: childUi.group
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === 'type' || key === 'name' || key.startsWith('__')) continue;
|
||||
if (seen.has(path)) continue;
|
||||
|
||||
seen.add(path);
|
||||
definitions.push({
|
||||
path,
|
||||
label: schemaFieldLabel(path, childResolved),
|
||||
type: this.toFieldType(childResolved?.type),
|
||||
enumOptions: this.toEnumOptions(childResolved),
|
||||
nodeOptionsSource: this.toNodeOptionsSource(childResolved),
|
||||
retrieverBlockType: this.toRetrieverBlockType(childResolved),
|
||||
retrieverKey: this.toRetrieverKey(childResolved),
|
||||
retrieverUrl: this.toRetrieverUrl(childResolved),
|
||||
retrieverStructuredData: childResolved?.['x-retriever-structured-data'] === true,
|
||||
retrieverDependsOn: this.toRetrieverDependsOn(childResolved, pathPrefix),
|
||||
ui: childUi
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
walk(schema, '');
|
||||
return definitions;
|
||||
return {
|
||||
path,
|
||||
label: schemaFieldLabel(path, childResolved),
|
||||
type: this.toFieldType(childResolved),
|
||||
enumOptions: this.toEnumOptions(childResolved),
|
||||
nodeOptionsSource: this.toNodeOptionsSource(childResolved),
|
||||
retrieverBlockType: this.toRetrieverBlockType(childResolved),
|
||||
retrieverKey: this.toRetrieverKey(childResolved),
|
||||
retrieverUrl: this.toRetrieverUrl(childResolved),
|
||||
retrieverStructuredData: childResolved?.['x-retriever-structured-data'] === true,
|
||||
retrieverDependsOn: this.toRetrieverDependsOn(childResolved, pathPrefix),
|
||||
ui
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private buildArrayFieldDefinitions(schema: Record<string, any> | null): ArrayFieldDefinition[] {
|
||||
if (!schema) return [];
|
||||
return collectSchemaLeafFields(schema, ({ key, path, schema: childResolved, ui }) => {
|
||||
if (childResolved?.['type'] !== 'array') return null;
|
||||
if (key === 'type' || key === 'name' || key.startsWith('__')) return null;
|
||||
|
||||
const definitions: ArrayFieldDefinition[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
const walk = (
|
||||
node: Record<string, any>,
|
||||
pathPrefix: string,
|
||||
inheritedUi?: { visibleWhen: UiConditionRule[]; enabledWhen: UiConditionRule[]; group: string | null }
|
||||
) => {
|
||||
const resolved = resolveSchemaRef(node, schema);
|
||||
if (!resolved || typeof resolved !== 'object') return;
|
||||
|
||||
const properties = resolved.properties as Record<string, any> | undefined;
|
||||
if (!properties) return;
|
||||
|
||||
for (const [key, childSchema] of Object.entries(properties)) {
|
||||
const childResolved = resolveSchemaRef(childSchema as Record<string, any>, schema);
|
||||
const path = pathPrefix ? `${pathPrefix}.${key}` : key;
|
||||
const childUi = this.toFieldUiMeta(childResolved, inheritedUi);
|
||||
|
||||
if (childResolved?.type === 'array') {
|
||||
if (key === 'type' || key === 'name' || key.startsWith('__') || seen.has(path)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(path);
|
||||
definitions.push({
|
||||
path,
|
||||
label: schemaFieldLabel(path, childResolved),
|
||||
itemSchema: this.resolveArrayItemSchema(childResolved, schema),
|
||||
uniqueBy: typeof childResolved?.['x-ui-unique-by'] === 'string' && String(childResolved['x-ui-unique-by']).trim().length > 0
|
||||
? String(childResolved['x-ui-unique-by']).trim()
|
||||
: null,
|
||||
ui: {
|
||||
structural: childUi.structural,
|
||||
visibleWhen: childUi.visibleWhen,
|
||||
enabledWhen: childUi.enabledWhen,
|
||||
group: childUi.group
|
||||
}
|
||||
});
|
||||
continue;
|
||||
return {
|
||||
path,
|
||||
label: schemaFieldLabel(path, childResolved),
|
||||
itemSchema: this.resolveArrayItemSchema(childResolved, schema ?? {}),
|
||||
uniqueBy: typeof childResolved?.['x-ui-unique-by'] === 'string' && String(childResolved['x-ui-unique-by']).trim().length > 0
|
||||
? String(childResolved['x-ui-unique-by']).trim()
|
||||
: null,
|
||||
ui: {
|
||||
structural: ui.structural,
|
||||
visibleWhen: ui.visibleWhen,
|
||||
enabledWhen: ui.enabledWhen,
|
||||
group: ui.group
|
||||
}
|
||||
|
||||
const hasChildren = !!childResolved?.properties || childResolved?.type === 'object';
|
||||
if (hasChildren) {
|
||||
walk(childResolved as Record<string, any>, path, {
|
||||
visibleWhen: childUi.visibleWhen,
|
||||
enabledWhen: childUi.enabledWhen,
|
||||
group: childUi.group
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
walk(schema, '');
|
||||
return definitions;
|
||||
}
|
||||
|
||||
private toFieldUiMeta(
|
||||
schema: Record<string, any> | null | undefined,
|
||||
inheritedUi?: { visibleWhen: UiConditionRule[]; enabledWhen: UiConditionRule[]; group: string | null }
|
||||
) {
|
||||
const rawWidget = typeof schema?.['x-ui-widget'] === 'string'
|
||||
? String(schema['x-ui-widget']).toLowerCase().trim()
|
||||
: '';
|
||||
const normalizedWidget: 'textarea' | null =
|
||||
rawWidget === 'textarea' || rawWidget === 'text-area' ? 'textarea' : null;
|
||||
const placeholder = typeof schema?.['x-ui-placeholder'] === 'string'
|
||||
? String(schema['x-ui-placeholder'])
|
||||
: undefined;
|
||||
const tip = schemaFieldDescription(schema) ?? undefined;
|
||||
const rowsRaw = schema?.['x-ui-rows'];
|
||||
const rows = typeof rowsRaw === 'number' && Number.isFinite(rowsRaw) && rowsRaw > 0
|
||||
? Math.trunc(rowsRaw)
|
||||
: undefined;
|
||||
const acceptVariableAsPlaceholder = schema?.['x-ui-accept-variable-as-placeholder'] === true;
|
||||
const structural = schema?.['x-ui-structural'] === true;
|
||||
const bindableAsInput = schema?.['x-ui-bindable-as-input'] === true;
|
||||
const inputName = typeof schema?.['x-ui-input-name'] === 'string'
|
||||
? String(schema['x-ui-input-name'])
|
||||
: null;
|
||||
const inputType = typeof schema?.['x-ui-input-type'] === 'string'
|
||||
? String(schema['x-ui-input-type']).toUpperCase()
|
||||
: null;
|
||||
const inputMultiple = typeof schema?.['x-ui-input-multiple'] === 'boolean'
|
||||
? Boolean(schema['x-ui-input-multiple'])
|
||||
: null;
|
||||
const structuralReason = typeof schema?.['x-ui-structural-reason'] === 'string'
|
||||
? String(schema['x-ui-structural-reason'])
|
||||
: undefined;
|
||||
const visibleWhen = readEffectiveUiVisibleConditionRule(schema);
|
||||
const enabledWhen = readUiConditionRule(schema?.['x-ui-enabled-when']);
|
||||
const label = readUiLabel(schema?.['x-ui-label']) ?? undefined;
|
||||
const isObjectLike = schema?.['type'] === 'object' || !!schema?.['properties'];
|
||||
const group = readUiGroup(schema?.['x-ui-group'])
|
||||
?? (isObjectLike ? label ?? null : null)
|
||||
?? inheritedUi?.group
|
||||
?? null;
|
||||
|
||||
return {
|
||||
widget: normalizedWidget,
|
||||
acceptVariableAsPlaceholder,
|
||||
structural,
|
||||
bindableAsInput,
|
||||
inputName,
|
||||
inputType,
|
||||
inputMultiple,
|
||||
structuralReason,
|
||||
label,
|
||||
placeholder,
|
||||
tip,
|
||||
rows,
|
||||
visibleWhen: [
|
||||
...(inheritedUi?.visibleWhen ?? []),
|
||||
...(visibleWhen ? [visibleWhen] : [])
|
||||
],
|
||||
enabledWhen: [
|
||||
...(inheritedUi?.enabledWhen ?? []),
|
||||
...(enabledWhen ? [enabledWhen] : [])
|
||||
],
|
||||
group
|
||||
};
|
||||
}
|
||||
|
||||
private getFieldUiMeta(path: string) {
|
||||
const root = this.blockSchema;
|
||||
if (!root) return this.toFieldUiMeta(null);
|
||||
|
||||
let current: Record<string, any> | null = root;
|
||||
let inheritedUi = { visibleWhen: [] as UiConditionRule[], enabledWhen: [] 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);
|
||||
if (/^\d+$/.test(segment)) {
|
||||
const items = resolved?.items;
|
||||
if (!items || typeof items !== 'object') return this.toFieldUiMeta(null, inheritedUi);
|
||||
current = resolveSchemaRef(items as Record<string, any>, root);
|
||||
} else {
|
||||
const properties = resolved?.properties as Record<string, unknown> | undefined;
|
||||
if (!properties || !properties[segment]) return this.toFieldUiMeta(null, inheritedUi);
|
||||
current = resolveSchemaRef(properties[segment] as Record<string, any>, root);
|
||||
}
|
||||
const nextUi = this.toFieldUiMeta(current, inheritedUi);
|
||||
inheritedUi = {
|
||||
visibleWhen: nextUi.visibleWhen,
|
||||
enabledWhen: nextUi.enabledWhen,
|
||||
group: nextUi.group
|
||||
};
|
||||
}
|
||||
|
||||
return this.toFieldUiMeta(current, inheritedUi);
|
||||
}, {
|
||||
includeArrays: true
|
||||
});
|
||||
}
|
||||
|
||||
private isStructuralField(path: string): boolean {
|
||||
return this.getFieldUiMeta(path).structural;
|
||||
return getSchemaPathUiMeta(this.blockSchema, path).structural;
|
||||
}
|
||||
|
||||
private resolveFieldSchema(path: string): Record<string, any> | null {
|
||||
|
|
@ -892,10 +716,12 @@ export class GenericNodeComponent {
|
|||
}
|
||||
|
||||
private toFieldType(type: unknown): FieldType {
|
||||
if (type === 'string') return 'string';
|
||||
if (type === 'number') return 'number';
|
||||
if (type === 'integer') return 'integer';
|
||||
if (type === 'boolean') return 'boolean';
|
||||
if (type && typeof type === 'object' && !Array.isArray(type)) {
|
||||
return schemaFieldTypeFromSchema(type as Record<string, any>);
|
||||
}
|
||||
if (type === 'string' || type === 'number' || type === 'integer' || type === 'boolean') {
|
||||
return type;
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
|
|
@ -918,32 +744,11 @@ export class GenericNodeComponent {
|
|||
}
|
||||
|
||||
private toEnumOptions(schema: Record<string, any> | null | undefined): string[] {
|
||||
const raw = schema?.['enum'];
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw.filter((value): value is string => typeof value === 'string');
|
||||
return schemaEnumOptions(schema);
|
||||
}
|
||||
|
||||
private toNodeOptionsSource(schema: Record<string, any> | null | undefined): NodeOptionsSource | null {
|
||||
if (!schema || typeof schema !== 'object') return null;
|
||||
const raw = schema['x-ui-options-from-node'];
|
||||
if (!raw || typeof raw !== 'object') return null;
|
||||
|
||||
const collection = (raw as Record<string, unknown>)['collection'];
|
||||
const valueField = (raw as Record<string, unknown>)['valueField'];
|
||||
const labelField = (raw as Record<string, unknown>)['labelField'];
|
||||
if ((collection !== 'inputs' && collection !== 'outputs')
|
||||
|| typeof valueField !== 'string'
|
||||
|| typeof labelField !== 'string'
|
||||
|| valueField.trim().length === 0
|
||||
|| labelField.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
collection,
|
||||
valueField: valueField.trim(),
|
||||
labelField: labelField.trim()
|
||||
};
|
||||
return schemaNodeOptionsSource(schema);
|
||||
}
|
||||
|
||||
private toRetrieverUrl(schema: Record<string, any> | null | undefined): string | null {
|
||||
|
|
@ -1160,10 +965,6 @@ export class GenericNodeComponent {
|
|||
.filter((option): option is NodeSettingOption => option != null);
|
||||
}
|
||||
|
||||
private isLongTextValue(value: string): boolean {
|
||||
return String(value ?? '').trim().length > 80;
|
||||
}
|
||||
|
||||
private async openReadonlyTextDialog(label: string, value: string) {
|
||||
await this.settingsDialog.open({
|
||||
title: label,
|
||||
|
|
@ -1186,8 +987,6 @@ export class GenericNodeComponent {
|
|||
private refreshParameterFields() {
|
||||
const config = this.blockConfiguration ?? {};
|
||||
const richContentPaths = new Set(this.richContentPaths());
|
||||
const grouped = new Map<string, EditableFieldView[]>();
|
||||
const rootFields: EditableFieldView[] = [];
|
||||
|
||||
this.richContentFields = this.richContentPaths()
|
||||
.filter((path) => this.isPathVisible(path))
|
||||
|
|
@ -1197,7 +996,7 @@ export class GenericNodeComponent {
|
|||
path,
|
||||
label: this.fieldDisplayLabel(path),
|
||||
rawValue,
|
||||
expandable: this.isLongTextValue(rawValue),
|
||||
expandable: isLongTextValue(rawValue),
|
||||
parts: this.toRichContentParts(path)
|
||||
};
|
||||
});
|
||||
|
|
@ -1218,7 +1017,7 @@ export class GenericNodeComponent {
|
|||
label: definition.label,
|
||||
value: this.fieldDisplayValue(definition, value),
|
||||
wide: this.shouldRenderWideField(definition.label, definition.ui.widget === 'textarea'),
|
||||
expandable: this.isLongTextValue(this.fieldDisplayValue(definition, value)),
|
||||
expandable: isLongTextValue(this.fieldDisplayValue(definition, value)),
|
||||
enabled: this.isPathEnabled(definition.path),
|
||||
type: definition.type,
|
||||
booleanValue: value === true
|
||||
|
|
@ -1226,23 +1025,15 @@ export class GenericNodeComponent {
|
|||
}).filter((field) => !richContentPaths.has(field.path))
|
||||
.filter((field) => this.isPathVisible(field.path));
|
||||
|
||||
for (const field of orderedFields) {
|
||||
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(groupKey)) {
|
||||
grouped.set(groupKey, []);
|
||||
}
|
||||
grouped.get(groupKey)!.push(field);
|
||||
}
|
||||
this.parameterFields = rootFields;
|
||||
this.parameterFieldGroups = Array.from(grouped.entries()).map(([key, fields]) => ({
|
||||
key,
|
||||
legend: key.startsWith('group:') ? key.slice('group:'.length) : this.fieldDisplayLabel(key),
|
||||
fields
|
||||
const groupedFields = groupSchemaFields({
|
||||
fields: orderedFields,
|
||||
resolveGroupLabel: (path) => getSchemaPathUiMeta(this.blockSchema, path).group ?? parentPath(path)
|
||||
});
|
||||
this.parameterFields = groupedFields.rootFields;
|
||||
this.parameterFieldGroups = groupedFields.groups.map((group) => ({
|
||||
key: group.key,
|
||||
legend: group.legend,
|
||||
fields: group.fields
|
||||
}));
|
||||
this.refreshView();
|
||||
return;
|
||||
|
|
@ -1256,29 +1047,23 @@ export class GenericNodeComponent {
|
|||
label: this.fieldDisplayLabel(entry.path),
|
||||
value: valueToDisplayString(entry.value),
|
||||
wide: this.shouldRenderWideField(this.fieldDisplayLabel(entry.path), false),
|
||||
expandable: this.isLongTextValue(valueToDisplayString(entry.value)),
|
||||
expandable: isLongTextValue(valueToDisplayString(entry.value)),
|
||||
enabled: this.isPathEnabled(entry.path),
|
||||
type: (typeof entry.value === 'boolean' ? 'boolean' : 'unknown') as FieldType,
|
||||
booleanValue: entry.value === true
|
||||
}));
|
||||
|
||||
for (const field of fallbackFields) {
|
||||
const parentKey = parentPath(field.path);
|
||||
if (!parentKey) {
|
||||
rootFields.push(field);
|
||||
continue;
|
||||
}
|
||||
if (!grouped.has(parentKey)) {
|
||||
grouped.set(parentKey, []);
|
||||
}
|
||||
grouped.get(parentKey)!.push(field);
|
||||
}
|
||||
const groupedFallback = groupSchemaFields({
|
||||
fields: fallbackFields,
|
||||
resolveGroupLabel: (path) => parentPath(path),
|
||||
resolveLegend: (groupLabel) => this.fieldDisplayLabel(groupLabel)
|
||||
});
|
||||
|
||||
this.parameterFields = rootFields;
|
||||
this.parameterFieldGroups = Array.from(grouped.entries()).map(([key, fields]) => ({
|
||||
key,
|
||||
legend: this.fieldDisplayLabel(key),
|
||||
fields
|
||||
this.parameterFields = groupedFallback.rootFields;
|
||||
this.parameterFieldGroups = groupedFallback.groups.map((group) => ({
|
||||
key: group.key,
|
||||
legend: group.legend,
|
||||
fields: group.fields
|
||||
}));
|
||||
|
||||
this.refreshView();
|
||||
|
|
@ -1908,57 +1693,16 @@ export class GenericNodeComponent {
|
|||
return textareaPaths;
|
||||
}
|
||||
|
||||
return this.findMainContentCandidatePaths(this.blockSchema);
|
||||
return collectSchemaLeafFields(this.blockSchema, ({ path, schema }) => {
|
||||
if (schema?.['type'] === 'array') return null;
|
||||
return getSchemaPathUiMeta(this.blockSchema, path).widget === 'textarea' ? path : null;
|
||||
});
|
||||
}
|
||||
|
||||
private toRichContentParts(path: string): { text: string; isDynamicInput: boolean }[] {
|
||||
const content = toStringOrNull(this.getByPath(this.blockConfiguration ?? {}, path));
|
||||
if (!content) return [];
|
||||
|
||||
const ui = this.getFieldUiMeta(path);
|
||||
if (ui.widget === 'textarea' && ui.acceptVariableAsPlaceholder) {
|
||||
return splitTemplatedTextParts(content);
|
||||
}
|
||||
|
||||
return [{ text: content, isDynamicInput: false }];
|
||||
}
|
||||
|
||||
private findMainContentCandidatePaths(schema: Record<string, any> | null): string[] {
|
||||
if (!schema) return [];
|
||||
|
||||
const paths: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
const walk = (node: Record<string, any>, pathPrefix: string) => {
|
||||
const resolved = resolveSchemaRef(node, schema);
|
||||
if (!resolved || typeof resolved !== 'object') return;
|
||||
|
||||
const properties = resolved.properties as Record<string, any> | undefined;
|
||||
if (!properties) return;
|
||||
|
||||
for (const [key, childSchema] of Object.entries(properties)) {
|
||||
const childResolved = resolveSchemaRef(childSchema as Record<string, any>, schema);
|
||||
const path = pathPrefix ? `${pathPrefix}.${key}` : key;
|
||||
const hasChildren = !!childResolved?.properties || childResolved?.type === 'object';
|
||||
|
||||
if (hasChildren) {
|
||||
walk(childResolved as Record<string, any>, path);
|
||||
continue;
|
||||
}
|
||||
|
||||
const rawWidget = typeof childResolved?.['x-ui-widget'] === 'string'
|
||||
? String(childResolved['x-ui-widget']).toLowerCase().trim()
|
||||
: '';
|
||||
const isTextarea = rawWidget === 'textarea' || rawWidget === 'text-area';
|
||||
if (!isTextarea || seen.has(path)) continue;
|
||||
|
||||
seen.add(path);
|
||||
paths.push(path);
|
||||
}
|
||||
};
|
||||
|
||||
walk(schema, '');
|
||||
return paths;
|
||||
return buildTemplatedRichContentParts(this.blockConfiguration ?? {}, path, this.blockSchema, splitTemplatedTextParts);
|
||||
}
|
||||
|
||||
private getByPath(source: Record<string, any>, path: string): unknown {
|
||||
|
|
@ -2171,7 +1915,7 @@ export class GenericNodeComponent {
|
|||
}
|
||||
|
||||
private isPathVisible(path: string): boolean {
|
||||
return this.isFieldConditionSatisfied(path);
|
||||
return isSchemaPathVisible(this.blockSchema, path, this.blockConfiguration);
|
||||
}
|
||||
|
||||
private editorFlowId(): string | null {
|
||||
|
|
@ -2271,12 +2015,7 @@ export class GenericNodeComponent {
|
|||
private isPathEnabled(path: string, visited = new Set<string>()): boolean {
|
||||
if (visited.has(path)) return true;
|
||||
visited.add(path);
|
||||
|
||||
const ui = this.getFieldUiMeta(path);
|
||||
return ui.enabledWhen.every((rule) => {
|
||||
if (!rule) return true;
|
||||
return evaluateUiConditionRule(rule, this.blockConfiguration, (fieldPath) => this.resolveFieldSchema(fieldPath));
|
||||
});
|
||||
return isSchemaPathEnabled(this.blockSchema, path, this.blockConfiguration);
|
||||
}
|
||||
|
||||
private isFieldVisible(field: EditableFieldDefinition): boolean {
|
||||
|
|
@ -2311,11 +2050,17 @@ export class GenericNodeComponent {
|
|||
private isFieldConditionSatisfied(path: string, visited = new Set<string>()): boolean {
|
||||
if (visited.has(path)) return true;
|
||||
visited.add(path);
|
||||
return isSchemaPathVisible(this.blockSchema, path, this.blockConfiguration);
|
||||
}
|
||||
|
||||
const ui = this.getFieldUiMeta(path);
|
||||
return ui.visibleWhen.every((rule) => {
|
||||
if (!rule) return true;
|
||||
return evaluateUiConditionRule(rule, this.blockConfiguration, (fieldPath) => this.resolveFieldSchema(fieldPath));
|
||||
});
|
||||
private toFieldUiMeta(
|
||||
schema: Record<string, any> | null | undefined,
|
||||
inheritedUi?: Pick<SchemaFieldUiMeta, 'visibleWhen' | 'enabledWhen' | 'group'>
|
||||
) {
|
||||
return toSchemaFieldUiMeta(schema, inheritedUi);
|
||||
}
|
||||
|
||||
private getFieldUiMeta(path: string) {
|
||||
return getSchemaPathUiMeta(this.blockSchema, path);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,358 @@
|
|||
import {
|
||||
type UiConditionRule,
|
||||
evaluateUiConditionRule,
|
||||
getValueByPath,
|
||||
readEffectiveUiVisibleConditionRule,
|
||||
readUiConditionRule,
|
||||
readUiGroup,
|
||||
readUiLabel,
|
||||
resolveSchemaRef,
|
||||
resolveSchemaPath,
|
||||
schemaFieldDescription
|
||||
} from './node-utility';
|
||||
|
||||
export type SchemaFieldType = 'string' | 'number' | 'integer' | 'boolean' | 'unknown';
|
||||
|
||||
export type SchemaNodeOptionsSource = {
|
||||
collection: 'inputs' | 'outputs';
|
||||
valueField: string;
|
||||
labelField: string;
|
||||
};
|
||||
|
||||
export type SchemaFieldUiMeta = {
|
||||
widget: 'textarea' | null;
|
||||
acceptVariableAsPlaceholder: boolean;
|
||||
structural: boolean;
|
||||
bindableAsInput: boolean;
|
||||
inputName: string | null;
|
||||
inputType: string | null;
|
||||
inputMultiple: boolean | null;
|
||||
structuralReason?: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
tip?: string;
|
||||
rows?: number;
|
||||
visibleWhen: UiConditionRule[];
|
||||
enabledWhen: UiConditionRule[];
|
||||
group: string | null;
|
||||
};
|
||||
|
||||
type SchemaUiInheritance = Pick<SchemaFieldUiMeta, 'visibleWhen' | 'enabledWhen' | 'group'>;
|
||||
|
||||
type SchemaLeafFieldContext = {
|
||||
key: string;
|
||||
path: string;
|
||||
pathPrefix: string;
|
||||
schema: Record<string, any> | null;
|
||||
ui: SchemaFieldUiMeta;
|
||||
};
|
||||
|
||||
export type SchemaFieldGroup<TField, TRichContent = never> = {
|
||||
key: string;
|
||||
legend: string;
|
||||
fields: TField[];
|
||||
richContentFields: TRichContent[];
|
||||
};
|
||||
|
||||
export function toSchemaFieldUiMeta(
|
||||
schema: Record<string, any> | null | undefined,
|
||||
inheritedUi?: SchemaUiInheritance
|
||||
): SchemaFieldUiMeta {
|
||||
const rawWidget = typeof schema?.['x-ui-widget'] === 'string'
|
||||
? String(schema['x-ui-widget']).toLowerCase().trim()
|
||||
: '';
|
||||
const normalizedWidget: 'textarea' | null =
|
||||
rawWidget === 'textarea' || rawWidget === 'text-area' ? 'textarea' : null;
|
||||
const placeholder = typeof schema?.['x-ui-placeholder'] === 'string'
|
||||
? String(schema['x-ui-placeholder'])
|
||||
: undefined;
|
||||
const tip = schemaFieldDescription(schema) ?? undefined;
|
||||
const rowsRaw = schema?.['x-ui-rows'];
|
||||
const rows = typeof rowsRaw === 'number' && Number.isFinite(rowsRaw) && rowsRaw > 0
|
||||
? Math.trunc(rowsRaw)
|
||||
: undefined;
|
||||
const acceptVariableAsPlaceholder = schema?.['x-ui-accept-variable-as-placeholder'] === true;
|
||||
const structural = schema?.['x-ui-structural'] === true;
|
||||
const bindableAsInput = schema?.['x-ui-bindable-as-input'] === true;
|
||||
const inputName = typeof schema?.['x-ui-input-name'] === 'string'
|
||||
? String(schema['x-ui-input-name'])
|
||||
: null;
|
||||
const inputType = typeof schema?.['x-ui-input-type'] === 'string'
|
||||
? String(schema['x-ui-input-type']).toUpperCase()
|
||||
: null;
|
||||
const inputMultiple = typeof schema?.['x-ui-input-multiple'] === 'boolean'
|
||||
? Boolean(schema['x-ui-input-multiple'])
|
||||
: null;
|
||||
const structuralReason = typeof schema?.['x-ui-structural-reason'] === 'string'
|
||||
? String(schema['x-ui-structural-reason'])
|
||||
: undefined;
|
||||
const visibleWhen = readEffectiveUiVisibleConditionRule(schema);
|
||||
const enabledWhen = readUiConditionRule(schema?.['x-ui-enabled-when']);
|
||||
const label = readUiLabel(schema?.['x-ui-label']) ?? undefined;
|
||||
const isObjectLike = schema?.['type'] === 'object' || !!schema?.['properties'];
|
||||
const group = readUiGroup(schema?.['x-ui-group'])
|
||||
?? (isObjectLike ? label ?? null : null)
|
||||
?? inheritedUi?.group
|
||||
?? null;
|
||||
|
||||
return {
|
||||
widget: normalizedWidget,
|
||||
acceptVariableAsPlaceholder,
|
||||
structural,
|
||||
bindableAsInput,
|
||||
inputName,
|
||||
inputType,
|
||||
inputMultiple,
|
||||
structuralReason,
|
||||
label,
|
||||
placeholder,
|
||||
tip,
|
||||
rows,
|
||||
visibleWhen: [
|
||||
...(inheritedUi?.visibleWhen ?? []),
|
||||
...(visibleWhen ? [visibleWhen] : [])
|
||||
],
|
||||
enabledWhen: [
|
||||
...(inheritedUi?.enabledWhen ?? []),
|
||||
...(enabledWhen ? [enabledWhen] : [])
|
||||
],
|
||||
group
|
||||
};
|
||||
}
|
||||
|
||||
export function getSchemaPathUiMeta(root: Record<string, any> | null | undefined, path: string): SchemaFieldUiMeta {
|
||||
if (!root) return toSchemaFieldUiMeta(null);
|
||||
|
||||
let current: Record<string, any> | null = root;
|
||||
let inheritedUi: SchemaUiInheritance = {
|
||||
visibleWhen: [],
|
||||
enabledWhen: [],
|
||||
group: null
|
||||
};
|
||||
|
||||
for (const segment of path.split('.')) {
|
||||
if (!current) return toSchemaFieldUiMeta(null, inheritedUi);
|
||||
|
||||
const resolved = resolveSchemaRef(current, root);
|
||||
if (/^\d+$/.test(segment)) {
|
||||
const items = resolved?.items;
|
||||
if (!items || typeof items !== 'object') return toSchemaFieldUiMeta(null, inheritedUi);
|
||||
current = resolveSchemaRef(items as Record<string, any>, root);
|
||||
} else {
|
||||
const properties = resolved?.properties as Record<string, unknown> | undefined;
|
||||
if (!properties || !properties[segment]) return toSchemaFieldUiMeta(null, inheritedUi);
|
||||
current = resolveSchemaRef(properties[segment] as Record<string, any>, root);
|
||||
}
|
||||
|
||||
const nextUi = toSchemaFieldUiMeta(current, inheritedUi);
|
||||
inheritedUi = {
|
||||
visibleWhen: nextUi.visibleWhen,
|
||||
enabledWhen: nextUi.enabledWhen,
|
||||
group: nextUi.group
|
||||
};
|
||||
}
|
||||
|
||||
return toSchemaFieldUiMeta(current, inheritedUi);
|
||||
}
|
||||
|
||||
export function collectSchemaLeafFields<T>(
|
||||
root: Record<string, any> | null | undefined,
|
||||
mapLeaf: (context: SchemaLeafFieldContext) => T | null,
|
||||
options?: {
|
||||
includeArrays?: boolean;
|
||||
shouldSkip?: (context: { key: string; path: string; schema: Record<string, any> | null }) => boolean;
|
||||
}
|
||||
): T[] {
|
||||
if (!root) return [];
|
||||
|
||||
const fields: T[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
const walk = (
|
||||
node: Record<string, any>,
|
||||
pathPrefix: string,
|
||||
inheritedUi?: SchemaUiInheritance
|
||||
) => {
|
||||
const resolved = resolveSchemaRef(node, root);
|
||||
if (!resolved || typeof resolved !== 'object') return;
|
||||
|
||||
const properties = resolved.properties as Record<string, any> | undefined;
|
||||
if (!properties) return;
|
||||
|
||||
for (const [key, childSchema] of Object.entries(properties)) {
|
||||
const childResolved = resolveSchemaRef(childSchema as Record<string, any>, root);
|
||||
const path = pathPrefix ? `${pathPrefix}.${key}` : key;
|
||||
if (options?.shouldSkip?.({ key, path, schema: childResolved })) continue;
|
||||
|
||||
const childUi = toSchemaFieldUiMeta(childResolved, inheritedUi);
|
||||
const isArray = childResolved?.type === 'array';
|
||||
const hasChildren = !!childResolved?.properties || childResolved?.type === 'object';
|
||||
|
||||
if (isArray && options?.includeArrays !== true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasChildren && !isArray) {
|
||||
walk(childResolved as Record<string, any>, path, {
|
||||
visibleWhen: childUi.visibleWhen,
|
||||
enabledWhen: childUi.enabledWhen,
|
||||
group: childUi.group
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (seen.has(path)) continue;
|
||||
seen.add(path);
|
||||
|
||||
const mapped = mapLeaf({
|
||||
key,
|
||||
path,
|
||||
pathPrefix,
|
||||
schema: childResolved,
|
||||
ui: childUi
|
||||
});
|
||||
if (mapped != null) {
|
||||
fields.push(mapped);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
walk(root, '');
|
||||
return fields;
|
||||
}
|
||||
|
||||
export function groupSchemaFields<TField extends { path: string }, TRichContent extends { path: string }>(
|
||||
params: {
|
||||
fields: TField[];
|
||||
richContentFields?: TRichContent[];
|
||||
resolveGroupLabel: (path: string) => string | null;
|
||||
resolveLegend?: (groupLabel: string) => string;
|
||||
}
|
||||
): {
|
||||
rootFields: TField[];
|
||||
rootRichContentFields: TRichContent[];
|
||||
groups: Array<SchemaFieldGroup<TField, TRichContent>>;
|
||||
} {
|
||||
const grouped = new Map<string, SchemaFieldGroup<TField, TRichContent>>();
|
||||
const rootFields: TField[] = [];
|
||||
const rootRichContentFields: TRichContent[] = [];
|
||||
const resolveLegend = params.resolveLegend ?? ((groupLabel: string) => groupLabel);
|
||||
|
||||
for (const field of params.fields) {
|
||||
const groupLabel = params.resolveGroupLabel(field.path);
|
||||
if (!groupLabel) {
|
||||
rootFields.push(field);
|
||||
continue;
|
||||
}
|
||||
const groupKey = `group:${groupLabel}`;
|
||||
if (!grouped.has(groupKey)) {
|
||||
grouped.set(groupKey, {
|
||||
key: groupKey,
|
||||
legend: resolveLegend(groupLabel),
|
||||
fields: [],
|
||||
richContentFields: []
|
||||
});
|
||||
}
|
||||
grouped.get(groupKey)!.fields.push(field);
|
||||
}
|
||||
|
||||
for (const field of params.richContentFields ?? []) {
|
||||
const groupLabel = params.resolveGroupLabel(field.path);
|
||||
if (!groupLabel) {
|
||||
rootRichContentFields.push(field);
|
||||
continue;
|
||||
}
|
||||
const groupKey = `group:${groupLabel}`;
|
||||
if (!grouped.has(groupKey)) {
|
||||
grouped.set(groupKey, {
|
||||
key: groupKey,
|
||||
legend: resolveLegend(groupLabel),
|
||||
fields: [],
|
||||
richContentFields: []
|
||||
});
|
||||
}
|
||||
grouped.get(groupKey)!.richContentFields.push(field);
|
||||
}
|
||||
|
||||
return {
|
||||
rootFields,
|
||||
rootRichContentFields,
|
||||
groups: Array.from(grouped.values()).filter((group) => group.fields.length > 0 || group.richContentFields.length > 0)
|
||||
};
|
||||
}
|
||||
|
||||
export function isSchemaPathVisible(
|
||||
root: Record<string, any> | null | undefined,
|
||||
path: string,
|
||||
config: Record<string, any> | null | undefined
|
||||
): boolean {
|
||||
const ui = getSchemaPathUiMeta(root, path);
|
||||
return ui.visibleWhen.every((rule) => evaluateUiConditionRule(rule, config, (fieldPath) => resolveSchemaPath(root, fieldPath)));
|
||||
}
|
||||
|
||||
export function isSchemaPathEnabled(
|
||||
root: Record<string, any> | null | undefined,
|
||||
path: string,
|
||||
config: Record<string, any> | null | undefined
|
||||
): boolean {
|
||||
const ui = getSchemaPathUiMeta(root, path);
|
||||
return ui.enabledWhen.every((rule) => evaluateUiConditionRule(rule, config, (fieldPath) => resolveSchemaPath(root, fieldPath)));
|
||||
}
|
||||
|
||||
export function schemaFieldTypeFromSchema(schema: Record<string, any> | null | undefined): SchemaFieldType {
|
||||
const type = typeof schema?.['type'] === 'string' ? String(schema['type']) : 'unknown';
|
||||
if (type === 'string' || type === 'number' || type === 'integer' || type === 'boolean') {
|
||||
return type;
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
export function schemaEnumOptions(schema: Record<string, any> | null | undefined): string[] {
|
||||
const raw = schema?.['enum'];
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw.filter((value): value is string => typeof value === 'string');
|
||||
}
|
||||
|
||||
export function schemaNodeOptionsSource(schema: Record<string, any> | null | undefined): SchemaNodeOptionsSource | null {
|
||||
if (!schema || typeof schema !== 'object') return null;
|
||||
const raw = schema['x-ui-options-from-node'];
|
||||
if (!raw || typeof raw !== 'object') return null;
|
||||
|
||||
const collection = (raw as Record<string, unknown>)['collection'];
|
||||
const valueField = (raw as Record<string, unknown>)['valueField'];
|
||||
const labelField = (raw as Record<string, unknown>)['labelField'];
|
||||
if ((collection !== 'inputs' && collection !== 'outputs')
|
||||
|| typeof valueField !== 'string'
|
||||
|| typeof labelField !== 'string'
|
||||
|| valueField.trim().length === 0
|
||||
|| labelField.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
collection,
|
||||
valueField: valueField.trim(),
|
||||
labelField: labelField.trim()
|
||||
};
|
||||
}
|
||||
|
||||
export function isLongTextValue(value: string): boolean {
|
||||
return String(value ?? '').trim().length > 80;
|
||||
}
|
||||
|
||||
export function buildTemplatedRichContentParts(
|
||||
config: Record<string, any> | null | undefined,
|
||||
path: string,
|
||||
root: Record<string, any> | null | undefined,
|
||||
splitParts: (content: string) => Array<{ text: string; isDynamicInput: boolean }>
|
||||
): Array<{ text: string; isDynamicInput: boolean }> {
|
||||
const content = String(getValueByPath(config ?? {}, path) ?? '').trim();
|
||||
if (!content) return [];
|
||||
|
||||
const ui = getSchemaPathUiMeta(root, path);
|
||||
if (ui.widget === 'textarea' && ui.acceptVariableAsPlaceholder) {
|
||||
return splitParts(content);
|
||||
}
|
||||
|
||||
return [{ text: content, isDynamicInput: false }];
|
||||
}
|
||||
Loading…
Reference in New Issue