import copy from "@virgodev/bazaar/functions/copy"; import dottedGet, { dottedSet } from "../../functions/dotted_get"; export interface ErrorDict { [key: string]: string[] | ErrorDict; } /** overlays `client` onto `base` (eg. server errors), recursing into nested dicts; a non-empty array at a leaf wins over whatever `base` had there */ export function mergeErrorDicts( base: ErrorDict | undefined, client: ErrorDict | undefined, ): ErrorDict { const result: ErrorDict = { ...(base ?? {}) }; for (const key in client) { const clientValue = client[key]; if (Array.isArray(clientValue)) { if (clientValue.length) { result[key] = clientValue; } } else if (clientValue) { result[key] = mergeErrorDicts(result[key] as ErrorDict, clientValue); } } return result; } /** duck-typed subset of the Standard Schema spec (https://standardschema.dev) so FieldDef.validation can accept a zod, valibot, or any other compliant schema without this package depending on any of them */ export interface StandardSchemaLike { "~standard": { validate: ( value: unknown, ) => | { value: unknown; issues?: undefined } | { issues: ReadonlyArray<{ message: string }> } | Promise< | { value: unknown; issues?: undefined } | { issues: ReadonlyArray<{ message: string }> } >; }; } export interface FieldDef { /** string reference to data value and error values, if name is false, it is incapable of loading or saving data */ name?: string; type?: string; options?: { name: string; value: string; label?: string }[]; readonly?: boolean; placeholder?: string; label?: string | false; fields?: FieldDef[]; size?: number; min?: number; max?: number; step?: number; rows?: number; cols?: number; default?: any; required?: boolean; pattern?: string; css_classes?: string; help?: string; tabindex?: number; maxlength?: number; minlength?: number; autocomplete?: string; extra?: any; inlineDisableAdd?: boolean; ariaLabel?: string; disabled?: boolean; group?: string; focus?: (e: Event) => void; validation?: StandardSchemaLike; } export async function validateSchema( schema: StandardSchemaLike | undefined, value: unknown, ): Promise { if (!schema) { return ""; } let result = schema["~standard"].validate(value); if (result instanceof Promise) { result = await result; } return result.issues?.[0]?.message ?? ""; } /** recursively collects every named field, descending into `fields` groups (eg. "sub"/"inline" layout wrappers) so a top-level AutoForm can look up any field in its tree by name without knowing about nesting */ export function flattenFields(fields: FieldDef[]): FieldDef[] { const result: FieldDef[] = []; for (const field of fields) { if (field.name) { result.push(field); } if (field.fields) { result.push(...flattenFields(field.fields)); } } return result; } /** same as flattenFields, narrowed to fields carrying a `validation` schema */ export function flattenValidatedFields(fields: FieldDef[]): FieldDef[] { return flattenFields(fields).filter((field) => field.validation); } export function updateModelValue( currentValues: any, field: FieldDef, rawValue: any, key?: string, ) { const values = copy(currentValues); let value = rawValue; if (field.type === "number") { value = parseFloat(value); } if (field.name && key) { let current: string[] = dottedGet(currentValues, field.name); if (!(current instanceof Array)) { current = []; } if (current.includes(key) && !rawValue) { current.splice(current.indexOf(key), 1); } else if (!current.includes(key) && rawValue) { current.push(key); } value = current; } if (field.name) { dottedSet(values, field.name, value); } return values; }