import React from 'react'; import { Hook } from '../../hook'; import { equals, Query, MemoryQuery, MemoryQueryRequest, Expression, MemorySelect, isDate, } from 'sync-ql'; import { Table } from '@mui/material'; import { Column, ColumnProps } from '../declaration/column'; import { DataColumnExtra, TableContext, TableMessages, TableReference } from '../types/table.type'; import { DataTableHead } from './head'; import { DataTableBody } from './body'; import { DataTableBodyVirtualized } from './body.virtualized'; import { DataRow } from '../types/body.type'; import { Filter } from '../declaration/filter'; import { DataFilterSet, DataFilterType, DataOption } from '../types/filter.type'; import { NoData, QueryRequest, QueryResponse } from '../../services'; import { DataTableActionBar } from './action.bar'; import { DataTablePaginator } from './paginator'; import { Cell, TextCell } from '../declaration/cell'; import { CellFactory } from '../types/cell.type'; import { DataSet } from '../declaration/set'; import { ActionBar } from '../declaration/action.bar'; import { SaveAction } from '../declaration/action.save'; import { InsertAction } from '../declaration/action.insert'; import { DeleteAction } from '../declaration/action.delete'; import { UploadAction } from '../declaration/action.upload'; import { DownloadAction } from '../declaration/action.download'; import { CustomAction } from '../declaration/action.custom'; import { ActionSeparator } from '../declaration/action.separator'; import { DataTableToolBar } from './action.toolbar'; import { SummaryContext } from '../types/summary.types'; import { Summary } from '../declaration/summary'; import { Data } from './filter.data'; import { TablePanel } from './style'; interface DataColumn extends DataColumnExtra { sticky?: number, } export interface DataTableProps { reference?: React.MutableRefObject, messages: TableMessages, selectable?: boolean, virtualized?: boolean, filters?: Expression, children?: React.ReactElement[] | React.ReactElement, data?: (query: QueryRequest) => Promise } export const DataTable: React.FC = ({ reference, messages, selectable, virtualized, children, filters, data: getData }) => { const [tableRef, { height: tableHeight }] = Hook.useDimensions(); const [context] = React.useState({ data: NoData, paginator: { currentPage: 1, pageSize: 0, }, filters: {}, editting: { row: '', column: '' }, hasSummary: false, selection: {}, modifications: {}, columns: undefined as any, defaultColumns: undefined as any, actions: [], forceUpdateTable: undefined as any, forceUpdateHead: undefined as any, forceUpdateBody: undefined as any, forceUpdateToolbar: undefined as any, forceUpdatePaginator: undefined as any, }); const [forceUpdate, setForceUpdate] = React.useState(false); context.forceUpdateTable = () => { setForceUpdate(!forceUpdate); } if (reference) { reference.current = { update: (callback?: (row: any) => void) => { if (callback && context.data && context.data.rows && context.data.rows.length > 0) { context.data.rows.forEach(row => callback(row)) } context.forceUpdateBody ? context.forceUpdateBody() : context.forceUpdateTable() } }; } React.useEffect(() => { if (children) { let hasSummary = false; const columns: DataColumn[] = []; context.actions = []; delete context.externalFilter; React.Children.map(children, child => { if (child.type === Column) { const columnName = child.props.name; let cellComponent: CellFactory | undefined = undefined; let filterType: DataFilterSet | 'none' = 'none'; let setOptions: ((value: [string, string]) => Promise) | undefined = NoData; let filterOptions: (text: string) => Promise = NoData; let filterToQuery: (value: DataFilterType | [DataFilterType, DataFilterType]) => DataFilterType | [DataFilterType, DataFilterType] = NoData; let columnSummary: SummaryContext | undefined = undefined; if (child.props.children) { React.Children.map(child.props.children, child => { if (child.type === DataSet) { setOptions = child.props.options; } else if (child.type === Filter) { filterType = child.props.type; filterOptions = child.props.options; filterToQuery = child.props.toQuery; if (!context.filters[columnName]) { context.filters[columnName] = { value: child.props.defaultValue, range: child.props.range, operation: child.props.defaultOperation ? child.props.defaultOperation : filterType === 'date' ? 'between' : filterType === 'set' ? 'auto' : 'equal', } } } else if (child.type === TextCell) { cellComponent = { style: child.props.style, align: child.props.align, }; } else if (child.type === Cell && child.props) { cellComponent = { style: child.props.style, align: child.props.align, factory: child.props.children, }; } else if (child.type === Summary && child.props) { columnSummary = { style: child.props.style, align: child.props.align, label: child.props.label, formula: child.props.formula, }; hasSummary = true; } }); } const column = context && context.columns ? context.columns.find(c => c.name === columnName) : null; columns.push({ name: columnName, label: child.props.label ? child.props.label : child.props.as ? child.props.as : columnName, as: child.props.as, type: filterType, cell: cellComponent, table: child.props.table, schema: child.props.schema, fix: child.props.fix, align: child.props.align, width: column ? column.width : child.props.width, minWidth: child.props.minWidth, maxWidth: child.props.maxWidth, hidden: child.props.hidden, disabled: child.props.disabled, readonly: child.props.readonly, sort: column ? column.sort : child.props.sort, filter: child.props.filter, summary: columnSummary, set: setOptions ? setOptions : ([text]) => filterOptions(text), options: filterOptions, toQuery: filterToQuery, history: child.props.history, }) } else if (child.type === Filter) { const filterType = child.props.type; const filterOptions = child.props.options; const filterToQuery = child.props.toQuery; if (!context.externalFilter) { context.externalFilter = { type: filterType, value: child.props.defaultValue, range: child.props.range, operation: child.props.defaultOperation ? child.props.defaultOperation : filterType === 'date' ? 'between' : filterType === 'set' ? 'auto' : 'equal', set: ([text]) => filterOptions(text), options: filterOptions, toQuery: filterToQuery, } } else throw `Only one table level filter can be defined`; } else if (child.type === ActionBar) { if (child.props.children) { let id = 0; React.Children.map(child.props.children, child => { id++; if (child.type === SaveAction) { if (child.props && child.props.onClick) { context.actions.push({ id: `save·${id}`, type: 'save', disabled: child.props.disabled, tooltip: child.props.tooltip, icon: child.props.children, handler: child.props.onClick }) } } else if (child.type === InsertAction) { if (child.props && child.props.onClick) { context.actions.push({ id: `insert·${id}`, type: 'insert', disabled: child.props.disabled, tooltip: child.props.tooltip, icon: child.props.children, handler: child.props.onClick }) } } else if (child.type === DeleteAction) { if (child.props && child.props.onClick) { context.actions.push({ id: `delete·${id}`, type: 'delete', disabled: child.props.disabled, tooltip: child.props.tooltip, icon: child.props.children, handler: child.props.onClick }) } } else if (child.type === UploadAction) { if (child.props && child.props.onClick) { context.actions.push({ id: `upload·${id}`, type: 'upload', disabled: child.props.disabled, tooltip: child.props.tooltip, icon: child.props.children, handler: child.props.onClick }) } } else if (child.type === DownloadAction) { if (child.props && child.props.onClick) { context.actions.push({ id: `download·${id}`, type: 'download', disabled: child.props.disabled, tooltip: child.props.tooltip, icon: child.props.children, handler: child.props.onClick }) } } else if (child.type === CustomAction) { if (child.props && child.props.onClick) { context.actions.push({ id: `custom·${id}`, type: 'custom', disabled: child.props.disabled, tooltip: child.props.tooltip, icon: child.props.children, handler: child.props.onClick }) } } else if (child.type === ActionSeparator) { context.actions.push({ id: `separator·${id}`, type: 'separator', handler: undefined }) } }); } } }) const columnsChanged = !equals(context.defaultColumns, columns); if (columnsChanged) { context.columns = columns; context.defaultColumns = cloneColumns(columns); } if (columnsChanged || context.hasSummary != hasSummary) { context.hasSummary = hasSummary; context.data = { rows: [] }; if (context.paginator) { context.paginator.currentPage = 1; } context.data.next = undefined; context.data.rows = []; handleLoadData(); context.forceUpdateTable(); } } }, [children]) React.useEffect(() => { if (!context.paginator.pageSize) { context.paginator.pageSize = Math.max(0, Math.round(tableHeight / 55)) if (context.forceUpdatePaginator) { context.forceUpdatePaginator(); } if (context.paginator.pageSize > 0) { handleFirstLoad(); } } }, [tableHeight]) React.useEffect(() => { if (filters && context.paginator.pageSize > 0) { context.data = NoData; handleFirstLoad(); } }, [filters]) const cloneColumns = (columns: DataColumn[]) => { return columns.map(x => ({ ...x })) } const handleRestore = () => { context.columns = cloneColumns(context.defaultColumns); if (context.forceUpdateHead) { context.forceUpdateHead(); } if (context.forceUpdateBody) { context.forceUpdateBody(); } } const handleFirstLoad = () => { if ((!context.data || !context.data.rows) && getData) { context.data = { rows: [] }; if (context.paginator) { context.paginator.currentPage = 1; } context.data.next = undefined; context.data.rows = []; handleLoadData(); } } const handleLoadData = () => { const query = Data.createQueryRequest(context); { query['LIMIT'] = { next: context.data.next, size: context.paginator.pageSize } } Query.Where.Include(query, filters); getData(query) .then(result => { if (result) { context.data.rows.push(...result.rows.map(r => ({ ...r }))); context.data.next = result.next; if (context.forceUpdatePaginator) { context.forceUpdatePaginator(); } if (context.forceUpdateBody) { context.forceUpdateBody(); } } }) } const handleFiltersChange = (query: QueryRequest) => { if (context.data && context.data.rows && getData) { query['LIMIT'] = { size: context.paginator.pageSize } if (context.externalFilter && context.externalFilter.operation && context.externalFilter.value) { Query.Where.Include(query, { column: { name: '*' }, operation: context.externalFilter.operation, value: context.externalFilter.value }) } Query.Where.Include(query, filters); getData(query) .then(result => { if (result) { context.data.rows = result.rows.map(r => ({ ...r })); context.data.next = result.next; if (context.forceUpdatePaginator) { context.forceUpdatePaginator(); } if (context.forceUpdateBody) { context.forceUpdateBody(); } } }) } } const handlePageChange = (page: number, last: number) => { context.paginator.currentPage = page; if (context.data && context.data.rows && page >= last && context.data && context.data.next && getData) { handleLoadData(); } else { if (context.forceUpdatePaginator) { context.forceUpdatePaginator(); } if (context.forceUpdateBody) { context.forceUpdateBody(); } } } const handleDataChange = (column: DataColumn, row: DataRow, value: any) => { if (row) { const original = { ...row }; { row[column.name] = value; } if (context.modifications[(row as any)['']]) { const changed = context.modifications[(row as any)['']].changed = row; if (equals(context.modifications[(row as any)['']].original, changed)) { delete context.modifications[(row as any)['']]; } } else context.modifications[(row as any)['']] = { original, changed: row } context.forceUpdateHead(); context.forceUpdatePaginator(); context.forceUpdateToolbar(); } } const handleDataSave = () => { let hasChanges = false; if (context.forceUpdateBody) { Object.values(context.modifications).forEach(modification => { if (!modification.changed['$deleted']) { const index = context.data.rows.findIndex(x => x && x[''] === modification.changed['']); if (index >= 0) { hasChanges = true; context.data.rows[index] = modification.changed; } } }) } context.modifications = {} if (hasChanges) { context.forceUpdateBody(); } context.forceUpdateHead(); context.forceUpdatePaginator(); context.forceUpdateToolbar(); } const handleInsertRow = (row: DataRow) => { if (row) { if (!context.data || !context.data.rows) { context.data = { rows: [] } } const query = Data.createQueryRequest(context) as MemoryQueryRequest; { query['LIMIT'] = { size: -1 } } for (const key in row) { if (Object.prototype.hasOwnProperty.call(row, key)) { const value = row[key]; const type = typeof value === 'string' ? 'text' : typeof value === 'boolean' ? 'boolean' : typeof value === 'number' ? 'number' : isDate(value) ? 'date' : Array.isArray(value) ? 'array' : 'object' Query.Select.Include(query, [[key, type]]); } } Query.Where.Include(query, filters); context.data.rows.push(row); context.data.rows = MemoryQuery.load({ 'SELECT': query['SELECT'] as MemorySelect, 'FROM': context.data.rows, 'WHERE': query['WHERE'], 'ORDER BY': query['ORDER BY'], 'LIMIT': query['LIMIT'] }).rows; context.modifications[(row as any)['']] = { original: {}, changed: row }; context.forceUpdateHead(); context.forceUpdateBody(); context.forceUpdatePaginator(); context.forceUpdateToolbar(); } } const handleDeleteRows = (rows: DataRow[]) => { if (rows && rows.length > 0 && context.data && context.data.rows) { context.data.rows = context.data.rows.filter(x => x && !rows.find(y => y && x[''] === y[''])) context.selection = {} rows.forEach(r => { if (r) { context.modifications[(r as any)['']] = { original: r, changed: { '$deleted': r } as any }; } }) context.forceUpdateHead(); context.forceUpdateBody(); context.forceUpdatePaginator(); context.forceUpdateToolbar(); } } const showActionBar = true; return ( <> { context.columns && { virtualized ? ( ) : ( ) }
}
{ showActionBar && } ) }