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'; import type { Children, Vnode, VnodeDOM } from 'mithril'; import {sync as render} from 'mithril-node-render'; import { IModel, InstantiableModel, Model } from '../../Models'; import type { FieldT, SelectT, TextAreaT, TextFieldT } from '../../types'; 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?: (instance: IModel, property: string) => any }; export type SectionT = { heading?: string columns?: number fields: Record }; export type ColumnsT = Record; export type RowsT = Collection; export type SectionsT = Record; /** * @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) => ( {this.getModelValue(instance, (column as ColumnT).id ?? index_)} )) .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 = $('mwc-dialog#add-record-dialog'); dialog // eslint-disable-next-line sonarjs/no-duplicate-string .find('text-field, text-area, material-select') .each(async (index, field) => { field.innerHTML = await this.getFieldBody(field as HTMLFormElement); (field as HTMLInputElement).value = this.getModelValue(instance, field.id) as string; }); 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.get(0) as MWCDialog).show(); } recordDialog() { 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 .find('text-field, text-area, material-select') .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 // eslint-disable-next-line new-cap 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 = {}; data.each((value, field) => { if (typeof field === 'string' && field.includes(':')) { const [relation, fieldName] = field.split(':'); const relationModel = this.getRelation(model, relation); relationModel[fieldName] = value; relations[relation] = relationModel; } else { model[field as string] = value; } }); // Save relations for (const [relation, relatedModel] of Object.entries(relations)) { // eslint-disable-next-line no-await-in-loop const response = await relatedModel.save(); if (response.getModelId) { model.setRelation(relation, response.getModelId()); } } const response = await model.save(); return response.getModelId(); } getRelation(model: IModel, relation: string) { // eslint-disable-next-line @typescript-eslint/no-unsafe-call const relationModel = model[`get${capitalize(relation)}`]() as IModel; if (relationModel) { return relationModel; } const relationship = model[relation] as ToOneRelation | ToManyRelation; const modelClass = relationship.getType() as InstantiableModel; // eslint-disable-next-line new-cap return new modelClass(); } getModelValue(model: IModel, field: string, raw = false): any { const column = this.columns[field]; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment let value: any = model[field]; if (typeof column === 'object' && column.valueModifier) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment value = column.valueModifier(model, field); } 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) { const list = []; switch (field.type ?? field.getAttribute('type')) { case 'select': // eslint-disable-next-line no-case-declarations const section = collect(this.sections).first((s) => field.id in s.fields); // eslint-disable-next-line no-case-declarations let {options} = section.fields[field.id] as SelectT; 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: } if (field.icon) { list.push(render()); } return list.join(''); } }