import '@maicol07/material-web-additions/data-table/data-table-cell.js'; import '@maicol07/material-web-additions/data-table/data-table-column.js'; import '@maicol07/material-web-additions/data-table/data-table-footer.js'; import '@maicol07/material-web-additions/data-table/data-table-row.js'; import { DataTable as MdDataTable, RowSelectionChangedDetail } from '@maicol07/material-web-additions/data-table/lib/data-table'; import {DataTableCell} from '@maicol07/material-web-additions/data-table/lib/data-table-cell'; import {mdiDeleteOutline} from '@mdi/js'; import collect, {Collection} from 'collect.js'; import { ToManyRelation, ToOneRelation } from 'coloquent'; import { Children, Vnode, VnodeDOM } from 'mithril'; import {Class} from 'type-fest'; import DataTable, {DataTableAttributes} from '~/Components/DataTable/DataTable'; import DataTableColumn, {DataTableColumnAttributes} from '~/Components/DataTable/DataTableColumn'; import RecordsTableColumn from '~/Components/DataTable/RecordsTableColumn'; import MdIcon from '~/Components/MdIcon'; import Model from '~/Models/Model'; import {isVnode} from '~/utils/misc'; export interface RecordsTableColumnAttributes extends DataTableColumnAttributes { label?: string; } export interface RecordsTableAttributes> extends DataTableAttributes { cols: Collection | Collection | Collection; records: Map; readonly?: boolean; onTableRowClick?(recordId: string, event: MouseEvent): void; onDeleteRecordButtonClick?(recordId: string, event: MouseEvent): void; onDeleteSelectedRecordsButtonClick?(recordsIds: string[], event: MouseEvent): void; onRowSelectionChanged?(selectedRecordsIds: string[], event: CustomEventInit): void; valueModifier?(value: any, attribute: string, record: M): any; selectable?: boolean; } export default class RecordsTable, A extends RecordsTableAttributes = RecordsTableAttributes> extends DataTable { element!: MdDataTable; selectedRecordsIds: string[] = []; oninit(vnode: Vnode) { super.oninit(vnode); vnode.attrs.paginated ??= true; vnode.attrs.currentPageSize ??= 10; } onupdate(vnode: VnodeDOM) { super.onupdate(vnode); // Clear selected rows if new records are not included for (const id of this.selectedRecordsIds) { if (!vnode.attrs.records.has(id)) { this.selectedRecordsIds.splice(this.selectedRecordsIds.indexOf(id), 1); } } } contents(vnode: Vnode) { return [ this.tableColumns(vnode).values().all(), vnode.attrs.records.size === 0 ? this.noRecordsContent(vnode) : this.tableRows(vnode), this.tableFooter(vnode) ]; } protected tableColumns(vnode: Vnode) { let columns: Collection = collect({}); if (vnode.attrs.selectable) { columns.put('checkbox', ); } columns = columns.merge(vnode.attrs.cols.map((column: Children | RecordsTableColumnAttributes, attribute: string) => { // If the column is a vnode, and it is a DataTableColumn or a string that matches the tag name of a DataTableColumn, then use it as is. if (isVnode>(column) && (column.tag === 'md-data-table-column' || (typeof column.tag !== 'string' && (column.tag as Class).prototype instanceof DataTableColumn))) { column.key ??= attribute; column.attrs['data-model-attribute'] ??= attribute; return column; } let attributes: DataTableColumnAttributes = {}; let children: Children | RecordsTableColumnAttributes = column; if (this.isRecordTableColumnAttributes(column)) { children = column.label ?? attribute; attributes = column; } attributes['data-model-attribute'] ??= attribute; // Otherwise, wrap it in a DataTableColumn return {children}; }).all()); if (!vnode.attrs.readonly) { columns.put('actions', this.tableActionsColumn()); } return columns; } protected tableActionsColumn(): Children { return ( {__('Azioni')} ); } noRecordsContent(vnode: Vnode): Children { const colspan = vnode.attrs.cols.count() + (vnode.attrs.selectable ? 1 : 0) + (vnode.attrs.readonly ? 0 : 1); return ( {__('Nessun record trovato')} ); } protected tableRows(vnode: Vnode) { return [...this.tableRowsData(vnode)].map(([recordId, row]): Children => ( {vnode.attrs.selectable && } {row.map((cell, attribute: string) => {cell}).values().all()} )); } protected tableRowsData(vnode: Vnode) { const rows = new Map>(); for (const record of vnode.attrs.records.values()) { const cells = collect({}); for (const attribute of vnode.attrs.cols.keys()) { const value = this.getModelValue(record, attribute, vnode); cells.put(attribute, value); } if (!vnode.attrs.readonly) { cells.put('actions', this.tableRowActions(vnode, record).values().all()); } rows.set(record.getId()!, cells); } return rows; } protected tableRowActions(vnode: Vnode, record: M) { return collect({ delete: ( ) }); } protected tableFooter(vnode: Vnode) { return ( {__('Seleziona un\'azione per la selezione multipla:')} {this.tableFooterActions(vnode).values().all()} ); } protected tableFooterActions(vnode: Vnode) { return collect({ delete: ( {__('Elimina selezionati')} ) }); } oncreate(vnode: VnodeDOM) { super.oncreate(vnode); this.element.addEventListener('rowSelectionChanged', this.onRowSelectionChanged.bind(this, vnode)); } protected onRowSelectionChanged(vnode: Vnode, event: CustomEventInit) { const recordId = event.detail!.row.dataset.modelId!; if (event.detail!.selected && !this.selectedRecordsIds.includes(recordId)) { this.selectedRecordsIds.push(recordId); } else if (!event.detail!.selected && this.selectedRecordsIds.includes(recordId)) { this.selectedRecordsIds.splice(this.selectedRecordsIds.indexOf(recordId), 1); } vnode.attrs.onRowSelectionChanged?.(this.selectedRecordsIds, event); } protected onTableRowClick(vnode: Vnode, recordId: string, event: MouseEvent) { if (event.target instanceof DataTableCell && event.target.type === 'checkbox') { event.target.dispatchEvent(new CustomEvent('checked', { detail: { checked: !event.target?.checkbox?.checked } })); return; } vnode.attrs.onTableRowClick?.(recordId, event); } protected onDeleteRecordButtonClicked(vnode: Vnode, record: M, event: MouseEvent) { event.stopPropagation(); vnode.attrs.onDeleteRecordButtonClick?.(record.getId()!, event); } protected onDeleteSelectedRecordsButtonClicked(vnode: Vnode, event: MouseEvent) { vnode.attrs.onDeleteSelectedRecordsButtonClick?.(this.selectedRecordsIds, event); } protected getModelValue(record: M, attribute: string, vnode: Vnode): unknown { // Check if is a relation let value: unknown = this.getModelRelationValue(record, attribute); if (!value) { value = record.getAttribute(attribute); } if (attribute === 'id') { value = record.getId()!; } let fallback = value; if (value instanceof ToOneRelation || value instanceof ToManyRelation) { fallback = value.getReferringObject().getApiId(); } return vnode.attrs.valueModifier?.(value, attribute, record) ?? fallback; } protected getModelRelationValue(record: M, attribute: string) { const relation = Reflect.get(record, attribute) as unknown; if (relation && typeof relation === 'function') { const relationship = relation() as unknown; if (relationship instanceof ToOneRelation || relationship instanceof ToManyRelation) { return relationship; } } } private isRecordTableColumnAttributes(column: Children | RecordsTableColumnAttributes): column is RecordsTableColumnAttributes { return typeof column === 'object' && 'label' in (column ?? {}); } }