Move the object dialog round trip into the shared schema module
An annotation honoured by one node type is worse than no annotation, so the container needs the same round trip the block editor has. It moves as it is - the characterisation tests written before the extraction still pass unchanged - with only the two genuinely component-specific parts left as callbacks: how select options are resolved, and how a dynamic sub-schema is expanded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c6060540f9
commit
0c0fac3643
|
|
@ -23,6 +23,12 @@ import { ConditionalRequiredField, extractSchemaRequirements, SchemaRequirements
|
|||
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,
|
||||
buildSchemaObjectDialog,
|
||||
buildSchemaOptionalGroupFieldDefinitions,
|
||||
countSetSchemaGroupValues,
|
||||
parseSchemaObjectDialogResult,
|
||||
type SchemaObjectDialogHooks,
|
||||
type SchemaOptionalGroupFieldDefinition,
|
||||
buildSchemaFieldViewModel,
|
||||
buildSchemaRetrieverContext,
|
||||
deleteSchemaValueByPath,
|
||||
|
|
@ -63,18 +69,7 @@ type EditableFieldDefinition = SchemaEditableFieldDefinition;
|
|||
|
||||
type EditableFieldView = SchemaParameterFieldView<FieldType>;
|
||||
|
||||
/** A nested object of optional settings, rendered as one control that opens a dialog. */
|
||||
type OptionalGroupFieldDefinition = {
|
||||
path: string;
|
||||
label: string;
|
||||
objectSchema: Record<string, any> | null;
|
||||
ui: {
|
||||
structural: boolean;
|
||||
visibleWhen: UiConditionRule[];
|
||||
enabledWhen: UiConditionRule[];
|
||||
group: string | null;
|
||||
};
|
||||
};
|
||||
type OptionalGroupFieldDefinition = SchemaOptionalGroupFieldDefinition;
|
||||
|
||||
type ArrayFieldDefinition = {
|
||||
path: string;
|
||||
|
|
@ -785,7 +780,7 @@ export class GenericNodeComponent implements OnDestroy {
|
|||
this.schemaRequirements = extractSchemaRequirements(this.blockSchema);
|
||||
this.editableFieldDefinitions = this.buildEditableFieldDefinitions(this.blockSchema);
|
||||
this.arrayFieldDefinitions = this.buildArrayFieldDefinitions(this.blockSchema);
|
||||
this.optionalGroupFieldDefinitions = this.buildOptionalGroupFieldDefinitions(this.blockSchema);
|
||||
this.optionalGroupFieldDefinitions = buildSchemaOptionalGroupFieldDefinitions(this.blockSchema);
|
||||
this.pruneInactiveConfiguration(this.ensureBlockConfiguration());
|
||||
|
||||
await this.refreshConditionalRequirements();
|
||||
|
|
@ -892,47 +887,15 @@ export class GenericNodeComponent implements OnDestroy {
|
|||
return this.optionalGroupFieldDefinitions
|
||||
.filter((definition) => this.isPathVisible(definition.path))
|
||||
.map((definition) => {
|
||||
const value = this.getByPath(config, definition.path);
|
||||
const values = value && typeof value === 'object' && !Array.isArray(value)
|
||||
? Object.values(value as Record<string, unknown>)
|
||||
: [];
|
||||
return {
|
||||
path: definition.path,
|
||||
label: definition.label,
|
||||
// A temperature of 0 counts: it is the repeatable setting, not an empty box.
|
||||
setCount: values.filter((entry) => entry !== undefined && entry !== null && entry !== '').length,
|
||||
setCount: countSetSchemaGroupValues(this.getByPath(config, definition.path)),
|
||||
enabled: this.isPathEnabled(definition.path)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private buildOptionalGroupFieldDefinitions(schema: Record<string, any> | null): OptionalGroupFieldDefinition[] {
|
||||
return collectSchemaLeafFields(schema, ({ path, schema: childResolved, ui }) => {
|
||||
if (childResolved?.['x-ui-optional-group'] !== true) return null;
|
||||
|
||||
// The label comes from the annotation when given, so a group can be named for what it is
|
||||
// rather than for the field that happens to hold it.
|
||||
const label = typeof childResolved?.['x-ui-optional-group-label'] === 'string'
|
||||
&& String(childResolved['x-ui-optional-group-label']).trim().length
|
||||
? String(childResolved['x-ui-optional-group-label']).trim()
|
||||
: schemaFieldLabel(path, childResolved);
|
||||
|
||||
return {
|
||||
path,
|
||||
label,
|
||||
objectSchema: childResolved,
|
||||
ui: {
|
||||
structural: ui.structural,
|
||||
visibleWhen: ui.visibleWhen,
|
||||
enabledWhen: ui.enabledWhen,
|
||||
group: ui.group
|
||||
}
|
||||
};
|
||||
}, {
|
||||
includeOptionalGroups: true
|
||||
});
|
||||
}
|
||||
|
||||
private isStructuralField(path: string): boolean {
|
||||
return getSchemaPathUiMeta(this.blockSchema, path).structural;
|
||||
}
|
||||
|
|
@ -1421,179 +1384,33 @@ export class GenericNodeComponent implements OnDestroy {
|
|||
this.maybeCreateBlockOnServer();
|
||||
}
|
||||
|
||||
/** Builds a dialog from an object's schema. Serves an array element and an optional group alike. */
|
||||
private async buildObjectDialog(
|
||||
itemSchema: Record<string, any> | null,
|
||||
title: string,
|
||||
item: Record<string, unknown>
|
||||
): Promise<NodeSettingsDialogInput | null> {
|
||||
const properties = itemSchema?.['properties'] as Record<string, any> | undefined;
|
||||
const schemaRoot = this.blockSchema ?? itemSchema ?? {};
|
||||
|
||||
if (!properties) {
|
||||
return {
|
||||
title,
|
||||
fields: [
|
||||
{
|
||||
key: '__raw',
|
||||
label: 'Item JSON',
|
||||
type: 'textarea' as const,
|
||||
rows: 10
|
||||
}
|
||||
],
|
||||
initial: {
|
||||
__raw: JSON.stringify(item ?? {}, null, 2)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const fields: NodeSettingField[] = [];
|
||||
const initial: Record<string, string | boolean> = {};
|
||||
|
||||
for (const { key, schema: propertySchema } of orderedSchemaPropertyEntries(itemSchema, schemaRoot)) {
|
||||
if (!propertySchema) continue;
|
||||
if (shouldSkipSchemaField(key, propertySchema)) continue;
|
||||
|
||||
const fieldUi = this.toFieldUiMeta(propertySchema);
|
||||
const visible = fieldUi.visibleWhen.every((rule) =>
|
||||
evaluateUiConditionRule(rule, item, (fieldPath) => resolveSchemaPath(itemSchema, fieldPath))
|
||||
);
|
||||
if (!visible) continue;
|
||||
|
||||
if (this.hasDynamicSchema(propertySchema)) {
|
||||
const dynamicFields = await this.buildDynamicSchemaFields(key, propertySchema, item);
|
||||
fields.push(...dynamicFields.fields);
|
||||
Object.assign(initial, dynamicFields.initial);
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = schemaFieldLabel(key, propertySchema);
|
||||
const currentValue = item[key];
|
||||
const options = await this.loadNodeSettingOptions(propertySchema, item, '');
|
||||
const isObjectLike = propertySchema?.['type'] === 'object';
|
||||
const fieldType =
|
||||
options ? 'select' :
|
||||
propertySchema?.['type'] === 'boolean' ? 'checkbox' :
|
||||
propertySchema?.['x-ui-widget'] === 'textarea' || isObjectLike ? 'textarea' :
|
||||
'text';
|
||||
|
||||
fields.push({
|
||||
key,
|
||||
label,
|
||||
type: fieldType,
|
||||
rows: fieldType === 'textarea' ? 8 : undefined,
|
||||
options,
|
||||
placeholder: typeof propertySchema?.['x-ui-placeholder'] === 'string' ? String(propertySchema['x-ui-placeholder']) : undefined,
|
||||
tip: schemaFieldDescription(propertySchema) ?? undefined,
|
||||
readonly: !fieldUi.enabledWhen.every((rule) =>
|
||||
evaluateUiConditionRule(rule, item, (fieldPath) => resolveSchemaPath(itemSchema, fieldPath))
|
||||
)
|
||||
});
|
||||
|
||||
if (fieldType === 'checkbox') {
|
||||
initial[key] = currentValue === true;
|
||||
} else if (fieldType === 'textarea' && isObjectLike) {
|
||||
initial[key] = JSON.stringify(currentValue ?? {}, null, 2);
|
||||
} else {
|
||||
initial[key] = currentValue == null ? '' : String(currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
/** The parts of the object round trip that only this component can supply. */
|
||||
private objectDialogHooks(objectSchema: Record<string, any> | null): SchemaObjectDialogHooks {
|
||||
return {
|
||||
title,
|
||||
fields,
|
||||
initial,
|
||||
// Re-derives itself from the draft, which is how conditional visibility and dependent
|
||||
// retrievers re-resolve while the dialog is open.
|
||||
onValuesChange: async (draft) => {
|
||||
const draftItem = this.parseObjectDialogResult(itemSchema, draft, item);
|
||||
const nextDialog = await this.buildObjectDialog(itemSchema, title, draftItem);
|
||||
if (!nextDialog) return null;
|
||||
return {
|
||||
fields: nextDialog.fields,
|
||||
initial: nextDialog.initial
|
||||
};
|
||||
schemaRoot: this.blockSchema ?? objectSchema ?? {},
|
||||
loadOptions: (propertySchema, draft) => this.loadNodeSettingOptions(propertySchema, draft, ''),
|
||||
dynamic: {
|
||||
isDynamic: (propertySchema) => this.hasDynamicSchema(propertySchema),
|
||||
buildFields: (key, propertySchema, draft) => this.buildDynamicSchemaFields(key, propertySchema, draft),
|
||||
readValue: (result, key) => this.extractNestedDialogValues(result, key)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a dialog result back into an object, using the object's own schema.
|
||||
*
|
||||
* Takes a schema rather than an ArrayFieldDefinition because the same round trip serves an array
|
||||
* element and an optional group; array-ness only ever showed up in the parameter type.
|
||||
*/
|
||||
private buildObjectDialog(
|
||||
objectSchema: Record<string, any> | null,
|
||||
title: string,
|
||||
value: Record<string, unknown>
|
||||
): Promise<NodeSettingsDialogInput | null> {
|
||||
return buildSchemaObjectDialog(objectSchema, title, value, this.objectDialogHooks(objectSchema));
|
||||
}
|
||||
|
||||
private parseObjectDialogResult(
|
||||
itemSchema: Record<string, any> | null,
|
||||
objectSchema: Record<string, any> | null,
|
||||
result: NodeSettingsValues,
|
||||
previousItem: Record<string, unknown>
|
||||
previousValue: Record<string, unknown>
|
||||
) {
|
||||
const properties = itemSchema?.['properties'] as Record<string, any> | undefined;
|
||||
const schemaRoot = this.blockSchema ?? itemSchema ?? {};
|
||||
const requiredKeys = new Set(Array.isArray(itemSchema?.['required']) ? itemSchema['required'] as string[] : []);
|
||||
if (!properties) {
|
||||
try {
|
||||
return JSON.parse(String(result['__raw'] ?? '{}')) as Record<string, unknown>;
|
||||
} catch {
|
||||
return previousItem;
|
||||
}
|
||||
}
|
||||
|
||||
const nextItem: Record<string, unknown> = {};
|
||||
|
||||
for (const { key, schema: propertySchema } of orderedSchemaPropertyEntries(itemSchema, schemaRoot)) {
|
||||
if (!propertySchema) continue;
|
||||
if (shouldSkipSchemaField(key, propertySchema)) continue;
|
||||
|
||||
const fieldUi = this.toFieldUiMeta(propertySchema);
|
||||
const visible = fieldUi.visibleWhen.every((rule) =>
|
||||
evaluateUiConditionRule(rule, result, (fieldPath) => resolveSchemaPath(itemSchema, fieldPath))
|
||||
);
|
||||
if (!visible) continue;
|
||||
|
||||
if (this.hasDynamicSchema(propertySchema)) {
|
||||
const dynamicValue = this.extractNestedDialogValues(result, key);
|
||||
nextItem[key] = Object.keys(dynamicValue).length ? dynamicValue : (previousItem[key] ?? {});
|
||||
continue;
|
||||
}
|
||||
|
||||
const rawValue = result[key];
|
||||
const isObjectLike = propertySchema?.['type'] === 'object';
|
||||
|
||||
if (propertySchema?.['type'] === 'boolean') {
|
||||
nextItem[key] = rawValue === true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isObjectLike) {
|
||||
try {
|
||||
nextItem[key] = JSON.parse(String(rawValue ?? '{}'));
|
||||
} catch {
|
||||
nextItem[key] = previousItem[key] ?? {};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (propertySchema?.['type'] === 'number' || propertySchema?.['type'] === 'integer') {
|
||||
const text = String(rawValue ?? '').trim();
|
||||
const numeric = Number(text);
|
||||
if (!text.length || !Number.isFinite(numeric)) {
|
||||
// Cleared. For an optional field that means "unset", and the key is left out entirely -
|
||||
// writing 0 would make the provider default unreachable on the very setting, like a
|
||||
// temperature, where 0 is itself a meaningful value. A required field keeps the old
|
||||
// behaviour: there is no "absent" for something the schema insists on.
|
||||
if (!requiredKeys.has(key)) continue;
|
||||
nextItem[key] = 0;
|
||||
continue;
|
||||
}
|
||||
nextItem[key] = propertySchema['type'] === 'integer' ? Math.trunc(numeric) : numeric;
|
||||
continue;
|
||||
}
|
||||
|
||||
nextItem[key] = String(rawValue ?? '');
|
||||
}
|
||||
|
||||
return nextItem;
|
||||
return parseSchemaObjectDialogResult(objectSchema, result, previousValue, this.objectDialogHooks(objectSchema));
|
||||
}
|
||||
|
||||
private createEmptyArrayItem(itemSchema: Record<string, any> | null) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { evaluateUiConditionRule, getValueByPath, orderedSchemaPropertyEntries, parentGroupLabel, readEffectiveUiVisibleConditionRule, readUiConditionRule, readUiGroup, readUiLabel, resolveSchemaPath, resolveSchemaRef, schemaFieldDescription, schemaFieldLabel, type UiConditionRule } from './node-utility';
|
||||
import type { NodeSettingField, NodeSettingOption, NodeSettingsDialogInput, NodeSettingsValues } from '@services/dialogs/node-settings-dialog';
|
||||
|
||||
import { evaluateUiConditionRule, getValueByPath, orderedSchemaPropertyEntries, parentGroupLabel, readEffectiveUiVisibleConditionRule, readUiConditionRule, readUiGroup, readUiLabel, resolveSchemaPath, resolveSchemaRef, schemaFieldDescription, schemaFieldLabel, shouldSkipSchemaField, type UiConditionRule } from './node-utility';
|
||||
|
||||
export type SchemaFieldType = 'string' | 'number' | 'integer' | 'boolean' | 'unknown';
|
||||
|
||||
|
|
@ -789,3 +791,245 @@ export function buildSchemaFieldViewModel<
|
|||
parameterFieldGroups: grouped.groups
|
||||
};
|
||||
}
|
||||
|
||||
/** A nested object of optional settings, rendered as one control that opens a dialog. */
|
||||
export type SchemaOptionalGroupFieldDefinition = {
|
||||
path: string;
|
||||
label: string;
|
||||
objectSchema: Record<string, any> | null;
|
||||
ui: {
|
||||
structural: boolean;
|
||||
visibleWhen: UiConditionRule[];
|
||||
enabledWhen: UiConditionRule[];
|
||||
group: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
export function buildSchemaOptionalGroupFieldDefinitions(
|
||||
root: Record<string, any> | null | undefined
|
||||
): SchemaOptionalGroupFieldDefinition[] {
|
||||
return collectSchemaLeafFields(root, ({ path, schema, ui }) => {
|
||||
if (schema?.['x-ui-optional-group'] !== true) return null;
|
||||
|
||||
// The label comes from the annotation when given, so a group can be named for what it is
|
||||
// rather than for the field that happens to hold it.
|
||||
const annotated = schema?.['x-ui-optional-group-label'];
|
||||
const label = typeof annotated === 'string' && annotated.trim().length
|
||||
? annotated.trim()
|
||||
: schemaFieldLabel(path, schema);
|
||||
|
||||
return {
|
||||
path,
|
||||
label,
|
||||
objectSchema: schema,
|
||||
ui: {
|
||||
structural: ui.structural,
|
||||
visibleWhen: ui.visibleWhen,
|
||||
enabledWhen: ui.enabledWhen,
|
||||
group: ui.group
|
||||
}
|
||||
};
|
||||
}, { includeOptionalGroups: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* How many of a group's settings are set. A collapsed control must never hide a value, so this is
|
||||
* what the button reports - and 0 counts, because a temperature of 0 is the repeatable setting and
|
||||
* not an absence.
|
||||
*/
|
||||
export function countSetSchemaGroupValues(value: unknown): number {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return 0;
|
||||
return Object.values(value as Record<string, unknown>)
|
||||
.filter((entry) => entry !== undefined && entry !== null && entry !== '')
|
||||
.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* What a component lends to the shared object round trip below.
|
||||
*
|
||||
* The two node components resolve select options differently, and only the block editor knows how
|
||||
* to expand a dynamic sub-schema, so those stay callbacks; everything else - ordering, conditional
|
||||
* visibility, types, and the cleared-value semantics - is the same in both and lives here.
|
||||
*/
|
||||
export type SchemaObjectDialogHooks = {
|
||||
/** The schema the object belongs to, used to resolve $refs while ordering properties. */
|
||||
schemaRoot: Record<string, any> | null;
|
||||
loadOptions: (
|
||||
propertySchema: Record<string, any>,
|
||||
draft: Record<string, unknown>
|
||||
) => Promise<NodeSettingOption[] | undefined> | NodeSettingOption[] | undefined;
|
||||
/** Optional: a property whose schema is fetched at runtime rather than declared inline. */
|
||||
dynamic?: {
|
||||
isDynamic: (propertySchema: Record<string, any> | null | undefined) => boolean;
|
||||
buildFields: (
|
||||
key: string,
|
||||
propertySchema: Record<string, any>,
|
||||
draft: Record<string, unknown>
|
||||
) => Promise<{ fields: NodeSettingField[]; initial: Record<string, string | boolean> }>;
|
||||
readValue: (result: NodeSettingsValues, key: string) => Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
|
||||
/** Builds a dialog from an object's schema. Serves an array element and an optional group alike. */
|
||||
export async function buildSchemaObjectDialog(
|
||||
objectSchema: Record<string, any> | null,
|
||||
title: string,
|
||||
value: Record<string, unknown>,
|
||||
hooks: SchemaObjectDialogHooks
|
||||
): Promise<NodeSettingsDialogInput> {
|
||||
const properties = objectSchema?.['properties'] as Record<string, any> | undefined;
|
||||
const schemaRoot = hooks.schemaRoot ?? objectSchema ?? {};
|
||||
|
||||
if (!properties) {
|
||||
return {
|
||||
title,
|
||||
fields: [{ key: '__raw', label: 'Item JSON', type: 'textarea' as const, rows: 10 }],
|
||||
initial: { __raw: JSON.stringify(value ?? {}, null, 2) }
|
||||
};
|
||||
}
|
||||
|
||||
const fields: NodeSettingField[] = [];
|
||||
const initial: Record<string, string | boolean> = {};
|
||||
|
||||
for (const { key, schema: propertySchema } of orderedSchemaPropertyEntries(objectSchema, schemaRoot)) {
|
||||
if (!propertySchema) continue;
|
||||
if (shouldSkipSchemaField(key, propertySchema)) continue;
|
||||
|
||||
const fieldUi = toSchemaFieldUiMeta(propertySchema);
|
||||
const visible = fieldUi.visibleWhen.every((rule) =>
|
||||
evaluateUiConditionRule(rule, value, (fieldPath) => resolveSchemaPath(objectSchema, fieldPath))
|
||||
);
|
||||
if (!visible) continue;
|
||||
|
||||
if (hooks.dynamic?.isDynamic(propertySchema)) {
|
||||
const dynamicFields = await hooks.dynamic.buildFields(key, propertySchema, value);
|
||||
fields.push(...dynamicFields.fields);
|
||||
Object.assign(initial, dynamicFields.initial);
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentValue = value[key];
|
||||
const options = await hooks.loadOptions(propertySchema, value);
|
||||
const isObjectLike = propertySchema?.['type'] === 'object';
|
||||
const fieldType =
|
||||
options ? 'select' :
|
||||
propertySchema?.['type'] === 'boolean' ? 'checkbox' :
|
||||
propertySchema?.['x-ui-widget'] === 'textarea' || isObjectLike ? 'textarea' :
|
||||
'text';
|
||||
|
||||
fields.push({
|
||||
key,
|
||||
label: schemaFieldLabel(key, propertySchema),
|
||||
type: fieldType,
|
||||
rows: fieldType === 'textarea' ? 8 : undefined,
|
||||
options,
|
||||
placeholder: typeof propertySchema?.['x-ui-placeholder'] === 'string' ? String(propertySchema['x-ui-placeholder']) : undefined,
|
||||
tip: schemaFieldDescription(propertySchema) ?? undefined,
|
||||
readonly: !fieldUi.enabledWhen.every((rule) =>
|
||||
evaluateUiConditionRule(rule, value, (fieldPath) => resolveSchemaPath(objectSchema, fieldPath))
|
||||
)
|
||||
});
|
||||
|
||||
if (fieldType === 'checkbox') {
|
||||
initial[key] = currentValue === true;
|
||||
} else if (fieldType === 'textarea' && isObjectLike) {
|
||||
initial[key] = JSON.stringify(currentValue ?? {}, null, 2);
|
||||
} else {
|
||||
initial[key] = currentValue == null ? '' : String(currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
fields,
|
||||
initial,
|
||||
// Re-derives itself from the draft, which is how conditional visibility and dependent
|
||||
// retrievers re-resolve while the dialog is open.
|
||||
onValuesChange: async (draft) => {
|
||||
const draftValue = parseSchemaObjectDialogResult(objectSchema, draft, value, hooks);
|
||||
const nextDialog = await buildSchemaObjectDialog(objectSchema, title, draftValue, hooks);
|
||||
return { fields: nextDialog.fields, initial: nextDialog.initial };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a dialog result back into an object, using the object's own schema.
|
||||
*
|
||||
* Takes a schema rather than a field definition because the same round trip serves an array element
|
||||
* and an optional group; array-ness only ever showed up in the parameter type.
|
||||
*/
|
||||
export function parseSchemaObjectDialogResult(
|
||||
objectSchema: Record<string, any> | null,
|
||||
result: NodeSettingsValues,
|
||||
previousValue: Record<string, unknown>,
|
||||
hooks: SchemaObjectDialogHooks
|
||||
): Record<string, unknown> {
|
||||
const properties = objectSchema?.['properties'] as Record<string, any> | undefined;
|
||||
const schemaRoot = hooks.schemaRoot ?? objectSchema ?? {};
|
||||
const requiredKeys = new Set(Array.isArray(objectSchema?.['required']) ? objectSchema['required'] as string[] : []);
|
||||
|
||||
if (!properties) {
|
||||
try {
|
||||
return JSON.parse(String(result['__raw'] ?? '{}')) as Record<string, unknown>;
|
||||
} catch {
|
||||
return previousValue;
|
||||
}
|
||||
}
|
||||
|
||||
const next: Record<string, unknown> = {};
|
||||
|
||||
for (const { key, schema: propertySchema } of orderedSchemaPropertyEntries(objectSchema, schemaRoot)) {
|
||||
if (!propertySchema) continue;
|
||||
if (shouldSkipSchemaField(key, propertySchema)) continue;
|
||||
|
||||
const fieldUi = toSchemaFieldUiMeta(propertySchema);
|
||||
const visible = fieldUi.visibleWhen.every((rule) =>
|
||||
evaluateUiConditionRule(rule, result, (fieldPath) => resolveSchemaPath(objectSchema, fieldPath))
|
||||
);
|
||||
if (!visible) continue;
|
||||
|
||||
if (hooks.dynamic?.isDynamic(propertySchema)) {
|
||||
const dynamicValue = hooks.dynamic.readValue(result, key);
|
||||
next[key] = Object.keys(dynamicValue).length ? dynamicValue : (previousValue[key] ?? {});
|
||||
continue;
|
||||
}
|
||||
|
||||
const rawValue = result[key];
|
||||
const isObjectLike = propertySchema?.['type'] === 'object';
|
||||
|
||||
if (propertySchema?.['type'] === 'boolean') {
|
||||
next[key] = rawValue === true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isObjectLike) {
|
||||
try {
|
||||
next[key] = JSON.parse(String(rawValue ?? '{}'));
|
||||
} catch {
|
||||
next[key] = previousValue[key] ?? {};
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (propertySchema?.['type'] === 'number' || propertySchema?.['type'] === 'integer') {
|
||||
const text = String(rawValue ?? '').trim();
|
||||
const numeric = Number(text);
|
||||
if (!text.length || !Number.isFinite(numeric)) {
|
||||
// Cleared. For an optional field that means "unset", and the key is left out entirely -
|
||||
// writing 0 would make the provider default unreachable on the very setting, like a
|
||||
// temperature, where 0 is itself a meaningful value. A required field keeps the old
|
||||
// behaviour: there is no "absent" for something the schema insists on.
|
||||
if (!requiredKeys.has(key)) continue;
|
||||
next[key] = 0;
|
||||
continue;
|
||||
}
|
||||
next[key] = propertySchema['type'] === 'integer' ? Math.trunc(numeric) : numeric;
|
||||
continue;
|
||||
}
|
||||
|
||||
next[key] = String(rawValue ?? '');
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue