/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import Router from "@koa/router"; import { hasShape, is, predicates } from "@sealcode/ts-predicates"; import { Context } from "koa"; import qs from "qs"; import { FlatTemplatable, tempstream } from "tempstream"; import { FormField } from "../fields/field.js"; import { ExtractFormFieldParsed, Table as TableField, } from "../fields/table.js"; import { FormReaction } from "../form-types.js"; import { Form } from "../form.js"; import { FormControl, FormControlContext } from "./form-control.js"; export type TableControlOptions> = { subfield_controls: { [field_name in keyof F]: FormControl; }; allow_removing?: boolean; allow_reordering?: boolean; label_add?: string; label: string; label_remove?: string; } & ( | { allow_adding: false } | { allow_adding: true; make_new_row: ( ctx: Context ) => Promise<{ [field_name in keyof F]: unknown }>; } ); export const TABLE_COLUMN_FIELD_INDEX_PLACEHOLDER = "TABLE_ROW_INDEX________________"; export class Table> extends FormControl { role = "input"; constructor( public table_field: TableField, public options: TableControlOptions ) { super(); } async render(fctx: FormControlContext): Promise { let rows = fctx.data.raw_values?.[this.table_field.name] as any[]; if (!rows) { rows = []; } const get_row_fctx = ( row: | { [field_name in keyof F]: ExtractFormFieldParsed< F[field_name] >; } | null, row_index: number | string ) => { return typeof row_index == "number" ? { data: { raw_values: (Object.fromEntries( Object.entries( row as Record ).map(([key, value]) => [ this.table_field.columns[key].name, value, ]) ) as any) || {}, messages: [], field_messages: {}, }, } : {}; }; const make_row = async ( row: | { [field_name in keyof F]: ExtractFormFieldParsed< F[field_name] >; } | null, row_index: number | string // when string, it's a placeholder, it will not parse as a number ) => { return /* HTML */ tempstream` ${Promise.all( Object.entries(this.table_field.columns) .map(([column_name]) => { const inner_fctx = get_row_fctx(row, row_index); return tempstream`${this.options.subfield_controls[ column_name ].render({ ...fctx, ...inner_fctx })}`; }) .map(async (html) => (await html).replaceAll( TABLE_COLUMN_FIELD_INDEX_PLACEHOLDER, row_index.toString() ) ) )} ${ this.options.allow_removing ? /* HTML */ ` ` : "" }${ this.options.allow_reordering ? /* HTML */ ` ` : "" } `; }; return tempstream /* HTML */ `= 5 ? ["form-input__wrapper--options-count--5-or-more"] : []), ...(rows.length >= 10 ? ["form-input__wrapper--options-count--10-or-more"] : []), ...(rows.length >= 15 ? ["form-input__wrapper--options-count--15-or-more"] : []), ...(rows.length >= 20 ? ["form-input__wrapper--options-count--20-or-more"] : []), ].join(" ")}" >
${rows?.map((row, index) => make_row(row, index))}
${ /* because of https://github.com/ljharb/qs/issues/252 we have to put the indexes in the field names */ this.options.allow_adding ? /* HTML */ ` ` : "" }
`; } getFrameID() { return `array-frame-${this.table_field.name}`; } getActionURL(field_name: string, action: Record) { return `./?${qs.stringify({ action, field_name, })}`; } mount(router: Router, form: Form) { router.post("/", async (ctx, next) => { const action = ctx.$body.action; const field_name = ctx.$body.field_name; if ( !is(action, predicates.object) || !is(field_name, predicates.string) || field_name !== this.table_field.name ) { await next(); return; } if ( this.options.allow_adding && hasShape( { insert: predicates.shape({ index: predicates.string }), }, action ) ) { if (!ctx.$body[this.table_field.name]) { ctx.$body[this.table_field.name] = {}; } (ctx.$body[this.table_field.name] as any)[action.insert.index] = await this.options.make_new_row(ctx); } if ( hasShape( { remove: predicates.shape({ index: predicates.string }), }, action ) ) { if (!ctx.$body[this.table_field.name]) { ctx.$body[this.table_field.name] = {}; } (ctx.$body[this.table_field.name] as any) = Object.fromEntries( Object.entries( Object.values( ctx.$body[this.table_field.name] as any ).filter( (_, index) => index != parseInt(action.remove.index) ) ) ); } ctx.override_reaction = { action: "stay", content: await form.render( ctx, { raw_values: await form.extractRawValues(ctx), messages: [], field_messages: {}, }, false ), } as FormReaction; await next(); }); } setAllowRemoving(value: boolean) { this.options.allow_removing = value; return this; } setAllowAdding(value: boolean) { this.options.allow_adding = value; return this; } setLabel(value: string) { this.options.label = value; return this; } setRemoveLabel(value: string) { this.options.label_remove = value; return this; } setAddLabel(value: string) { this.options.label_add = value; return this; } setAllowReordering(value: boolean) { this.options.allow_reordering = value; return this; } }