/* eslint-disable no-await-in-loop */ import '@maicol07/mwc-layout-grid'; import '@material/mwc-dialog'; import '@material/mwc-fab'; import '@material/mwc-snackbar'; import type {Dialog as MWCDialog} from '@material/mwc-dialog'; import type {Cash} from 'cash-dom'; import collect, {type Collection} from 'collect.js'; import { ToManyRelation, ToOneRelation } from 'coloquent'; import {capitalize} from 'lodash-es'; import type { Children, Vnode, VnodeDOM } from 'mithril'; import {sync as render} from 'mithril-node-render'; import { IModel, InstantiableModel, Model } from '../../Models'; import type { FieldT, SelectOptionsT, SelectT, TextAreaT, TextFieldT } from '../../typings'; import { getFormData, isFormValid, showSnackbar } from '../../utils'; import DataTable from '../DataTable/DataTable'; import TableCell from '../DataTable/TableCell'; import TableColumn from '../DataTable/TableColumn'; import TableRow from '../DataTable/TableRow'; import LoadingButton from '../LoadingButton'; import Mdi from '../Mdi'; import Page from '../Page'; export type ColumnT = { id?: string title: string type?: 'checkbox' | 'numeric' valueModifier?: (value: any, field: string, model: IModel) => any }; export type SectionT = { heading?: string columns?: number fields: Record }; export type ColumnsT = Record; export type RowsT = Collection; export type SectionsT = Record; const FIELDS: string = 'text-field, text-area, material-select'; /** * @abstract */ export class RecordsPage extends Page { columns: ColumnsT; rows: RowsT = collect({}); sections: SectionsT; dialogs: Children[]; recordDialogMaxWidth: string | number = 'auto'; model: typeof Model; /** * What fields should take precedence when saving the record */ fieldsPrecedence: string[] = []; async oninit(vnode: Vnode) { super.oninit(vnode); // @ts-ignore const response = await this.model.with(this.model.relationships).get(); const data = response.getData() as Model[]; if (data.length > 0) { for (const record of data) { this.rows.put(record.getId(), record); } m.redraw(); } } onupdate(vnode: VnodeDOM) { const rows: Cash = $('.mdc-data-table__row[data-model-id]'); if (rows.length > 0) { rows.on('click', async (event: PointerEvent) => { const cell = event.target as HTMLElement; if (cell.tagName === 'MWC-CHECKBOX') { return; } await this.updateRecord($(cell).parent('tr').data('model-id') as number); }); } } tableColumns(): JSX.Element[] { return collect(this.columns) // @ts-ignore .map((column: ColumnT | string, id: string) => ( {typeof column === 'string' ? column : column.title} )) .toArray(); } tableRows(): Children { if (this.rows.isEmpty()) { return ( {__('Non sono presenti dati')} ); } return this.rows .map((instance: IModel, index: string) => ( {collect(this.columns) .map((column, index_: string) => { const columnId = (column as ColumnT).id ?? index_; this.getModelValue(instance, columnId, true).then((value: string) => { $(`td#${columnId}-${index}`).text(value); }).catch(() => {}).finally(() => {}); return ; }) .toArray()} )) .toArray(); } async updateRecord(id: number) { // @ts-ignore const response = await this.model.with(this.model.relationships).find(id); const instance = response.getData() as IModel; const dialog: MWCDialog | null = document.querySelector('mwc-dialog#add-record-dialog'); if (dialog) { for (const field of dialog.querySelectorAll(FIELDS)) { const value = await this.getModelValue(instance, field.id) as string; field.innerHTML = await this.getFieldBody(field as HTMLFormElement, value); (field as HTMLInputElement).value = value; } $(dialog) .find('mwc-button#delete-button') .show() .on('click', () => { const confirmDialog = $('mwc-dialog#confirm-delete-record-dialog'); const confirmButton = confirmDialog.find('mwc-button#confirm-button'); const loading: Cash = confirmButton.find('mwc-circular-progress'); confirmButton.on('click', async () => { loading.show(); await instance.delete(); // noinspection JSUnresolvedVariable this.rows.forget(instance.getId()); m.redraw(); await showSnackbar(__('Record eliminato!'), 4000); }); loading.hide(); (confirmDialog.get(0) as MWCDialog).show(); }); dialog.show(); } } recordDialog(): JSX.Element | JSX.Element[] { return (
); } deleteRecordDialog(): Children { return (

{__('Sei sicuro di voler eliminare questo record?')}

); } view(vnode: Vnode) { return ( <>

{this.title}

{this.tableColumns()} {this.tableRows()} {this.recordDialog()} {this.deleteRecordDialog()} {this.dialogs} ); } oncreate(vnode: VnodeDOM) { super.oncreate(vnode); const fab: Cash = $('mwc-fab#add-record'); const dialog: Cash = fab.next('mwc-dialog#add-record-dialog'); const form: Cash = dialog.find('form'); // Open "New record" dialog fab.on('click', this.openNewRecordDialog.bind(this, form, dialog)); const button = dialog.find('mwc-button[type="submit"]'); button.on('click', () => form.trigger('submit')); form.on('submit', this.submitForm.bind(this, button, dialog, form)); } openNewRecordDialog(form: Cash, dialog: Cash) { form // eslint-disable-next-line unicorn/no-array-callback-reference .find(FIELDS) .each(async (index, field) => { field.innerHTML = await this.getFieldBody(field as HTMLFormElement); (field as HTMLInputElement).value = $(field) .data('default-value') as string; }); dialog.find('mwc-button[type="submit"] mwc-circular-progress').hide(); dialog.find('mwc-button#delete-button').hide(); const dialogElement: HTMLElement & Partial | undefined = dialog.get(0); if (dialogElement) { (dialogElement as MWCDialog).show(); } } async submitForm(button: Cash, dialog: Cash, form: Cash, event: SubmitEvent) { event.preventDefault(); const loading: Cash = button.find('mwc-circular-progress'); loading.show(); if (isFormValid(form)) { const data = collect(getFormData(form)); // @ts-ignore const instance = this.rows.get(data.get('id'), new this.model() as IModel) as IModel; const modelId = await this.setter(instance, data); if (modelId) { // @ts-ignore const newResponse = await this.model.with(this.model.relationships) .find(modelId); const model = newResponse.getData() as IModel; const dialogElement = dialog.get(0); if (dialogElement) { (dialogElement as MWCDialog).close(); } this.rows.put(model.getId(), model); loading.hide(); m.redraw(); await showSnackbar(__('Record salvato'), 4000); } } else { loading.hide(); await showSnackbar(__('Campi non validi. Controlla i dati inseriti')); } } async setter(model: IModel, data: Collection) { const firstFields = data.only(this.fieldsPrecedence); const fields = data.except(this.fieldsPrecedence); firstFields.each((currentItem, key) => { fields.put(key, currentItem); }); const relations: Record = {}; for (const [field, value] of Object.entries(data.all())) { if ((model as unknown as typeof Model).relationships.includes(field)) { relations[field] = await this.getRelation(model, field, false, Number(value)) as IModel; } if (field.includes(':')) { const [relation, fieldName]: (string | undefined)[] = field.split(':'); const relationModel: IModel = relation in relations ? relations[relation] : await this.getRelation(model, relation, true) as IModel; if (relationModel) { relationModel[fieldName] = value; relations[relation] = relationModel; } } else { model[field] = value; } } // Save relations for (const [relation, relatedModel] of Object.entries(relations)) { const response = await relatedModel.save(); if (response.getModelId) { model.setRelation(relation, response.getModelId()); } } const response = await model.save(); return response.getModelId(); } async getRelation( model: IModel, relation: string, createIfNotExists: boolean = false, id?: number ) { const getter = `get${capitalize(relation)}`; const relationModel: IModel | undefined = (typeof model[getter] === 'function' ? (model[getter] as Function)() : model.getRelation(relation)) as IModel; if (relationModel) { return relationModel; } const relationship = (model[relation] as Function)() as ToOneRelation | ToManyRelation; const RelationshipModel = relationship.getType() as typeof Model | InstantiableModel; if (id) { // @ts-ignore const response = await (RelationshipModel as typeof Model).find(id); return response.getData() as IModel; } return createIfNotExists ? new (RelationshipModel as InstantiableModel)() : undefined; } async getModelValue( model: IModel, field: string, useValueModifier = false, raw = false ): Promise { const column = this.columns[field]; let value: unknown; if (field.includes(':')) { const [relation, fieldName] = field.split(':'); const relationModel = await this.getRelation(model, relation); value = relationModel?.[fieldName]; } else { value = model[field]; } if (useValueModifier && typeof column === 'object' && column.valueModifier) { value = column.valueModifier(value, field, model); } return (value || raw) ? value : ''; } getElementFromType(type: string) { switch (type) { case 'text': return 'text-field'; case 'textarea': return 'text-area'; case 'select': return 'material-select'; /* Case 'checkbox': case 'radio': return Radio; */ default: return 'text-field'; } } async getFieldBody(field: HTMLFormElement & FieldT, value?: string) { const list = []; switch (field.type ?? field.getAttribute('type')) { case 'select': { const section = collect(this.sections) // (temporary) .first((s) => field.id in s.fields); .filter((s) => field.id in s.fields) .first(); const select = section.fields[field.id] as SelectT; let {options} = select; const {relationship} = select; if (Array.isArray(relationship) && relationship.length === 2) { options = this.getModelSelectOptions(relationship[0], relationship[1]); } if (options instanceof Promise) { options = await options; } if (options) { for (const option of options) { list.push(render( {option.label} )); } } break; } case 'checkbox': return ''; case 'radio': return ''; default: } // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const {icon} = field; if (icon) { list.push(render()); } return list.join(''); } async getModelSelectOptions( model: typeof Model, labelAttribute: string ): Promise { const response = await model.all(); const categories = response.getData(); return categories.map((instance: IModel) => ({ value: instance.getId() as string, label: instance[labelAttribute] as string })); } }