/* eslint @typescript-eslint/no-unused-vars: off, @typescript-eslint/no-explicit-any: off */ import { Predicate, predicates } from "@sealcode/ts-predicates"; import { Context } from "koa"; import { Collection, CollectionItem } from "sealious"; import { t } from "../../utils/translate.js"; import { FormControl, FormControlContext } from "../controls/form-control.js"; import { FormDataValue, FormData } from "../form-types.js"; import { ExtractedFieldInfo } from "../../utils/extract-fields-from-collection.js"; import { wrapAttribute, wrapKeyName } from "../../utils/wrap-attributes.js"; export type FormFieldValidationResponse = { valid: boolean; message: string }; export type FormDisplayInfo = { field: keyof C["fields"]; label: string; format?: (value: unknown, item: CollectionItem) => JSX.Element; }; export type FormFieldValidationFn = ( ctx: Context, value: ValueType, field: FormField ) => Promise; export type FieldParsedValue = T extends FormField ? R : never; export type GetPredicate = F extends FormField ? F["predicate"] : F["predicate"] | typeof predicates.undefined; // for quickly testing GetPredicate conditonally applying the undefined predicate: // const f = new SimpleFormField(true); // type p = GetPredicate; // const f2 = new SimpleFormField(false); // type p2 = GetPredicate; export type FieldsToShape> = { [property in keyof T]: GetPredicate; }; export function fieldsToShape>( obj: Fields ): FieldsToShape { return Object.fromEntries( Object.entries(obj).map(([key, value]) => [key, value.predicate]) ) as FieldsToShape; } export type FieldParseResult = | { parsable: true; error: null; parsed_value: T; } | { parsable: false; error: string; parsed_value: null; }; export abstract class FormField< Required extends boolean = boolean, ParsedValue = unknown, DefaultFormControl extends FormControl = FormControl, SealiousValue = any, > { name: string; label: string; constructor(public readonly required: Required) {} predicate: Predicate = predicates.unknown; mapToFilter: (parsed: ParsedValue) => unknown = (value) => value; disabled = false; async init(): Promise {} setName(name: string): this { this.name = name; return this; } setDisabled(status: boolean) { this.disabled = status; return this; } setLabel(label: string): this { this.label = label; return this; } public async _validate( ctx: Context, value: ParsedValue ): Promise { if (value === "" || value === null || value === undefined) { if (this.required) { return { valid: false, message: t( ctx, "field_is_required", [], "This field is required" ), }; } else { return { valid: true, message: "", }; } } return this.isValueValid(ctx, value); } public abstract getEmptyValue(): ParsedValue; public async isValueValid( _ctx: Context, _value: ParsedValue ): Promise { return { valid: true, message: "" }; } abstract parse( ctx: Context, raw_value: FormDataValue ): Promise>; async getParsedValue( ctx: Context, all_form_values: Record, validate: true ): Promise<{ valid: boolean; message: string; parsed: ParsedValue | null; raw: FormDataValue; }>; async getParsedValue( ctx: Context, all_form_values: Record, validate?: false ): Promise<{ message: string; parsed: ParsedValue | null; raw: FormDataValue; }>; async getParsedValue( ctx: Context, all_form_values: Record, validate = false ): Promise<{ valid?: boolean; message: string; parsed: ParsedValue | null; raw: FormDataValue; }> { const raw = all_form_values[this.name]; if (raw == undefined || raw == null || raw == "") { return { parsed: null, message: this.required ? "empty value, but field is required" : "", raw, ...(validate ? { valid: !this.required } : {}), }; } const { parsable, parsed_value, error: parse_error, } = await this.parse(ctx, raw); if (!parsable) { return { ...(validate ? { valid: false } : {}), parsed: this.getEmptyValue(), raw, message: parse_error || "Error", }; } if (validate) { const { valid, message } = await this._validate(ctx, parsed_value); return { parsed: parsed_value, ...(validate ? { valid } : {}), message, raw: all_form_values[this.name], }; } else { return { parsed: parsed_value, raw, message: "parsed" }; } } setMapToFilter(fn: (parsed: ParsedValue) => unknown) { this.mapToFilter = fn; return this; } //return undefined here to delete a value async getDatabaseValue( ctx: Context, data: Record ): Promise { const { parsed } = await this.getParsedValue(ctx, data); return parsed; } abstract getControl(): DefaultFormControl; getSealiousCreateValue(fctx: FormControlContext): Promise { throw new Error("This field does not have a default sealious mapping"); } sealiousValueToForm( ctx: Context, sealiousValue: SealiousValue | null, item: CollectionItem ): Promise { throw new Error("This field does not have a default sealious mapping"); } async postSealiousCreate( ctx: Context, created_item: CollectionItem, form_data: FormData ): Promise {} async postSealiousEdit( ctx: Context, edited_item: CollectionItem, form_data: FormData ): Promise {} generateFieldDeclaration( field_info: ExtractedFieldInfo, vars: { form_field_types: string; sealious_field: string } ): string { return `new ${vars.form_field_types}.${this.constructor.name}(${String( field_info.is_required )})`; } generateCreateValueGetter( field_info: ExtractedFieldInfo, vars: { form_field_types: string; sealious_field: string; form_fields: string; } ): string { return `${wrapKeyName(field_info.name)}: await ${ vars.form_fields }${wrapAttribute(field_info.name)}.getSealiousCreateValue(fctx)`; } generateInitialValue( field_info: ExtractedFieldInfo, vars: { form_field_types: string; form_fields: string; } ): string { return `${wrapKeyName(field_info.name)}: await ${ vars.form_fields }${wrapAttribute(field_info.name)}.sealiousValueToForm(ctx, item.get("${ field_info.name }"))`; } generateImportsForFieldList(field_info: ExtractedFieldInfo): { what: string; from: string; as?: string; type?: boolean; }[] { return []; } }