1 import * as React from 'react';
\r
2 import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles';
\r
4 import Table from '@material-ui/core/Table';
\r
5 import TableBody from '@material-ui/core/TableBody';
\r
6 import TableCell from '@material-ui/core/TableCell';
\r
7 import TablePagination from '@material-ui/core/TablePagination';
\r
8 import TableRow from '@material-ui/core/TableRow';
\r
9 import Paper from '@material-ui/core/Paper';
\r
10 import Checkbox from '@material-ui/core/Checkbox';
\r
12 import { TableToolbar } from './tableToolbar';
\r
13 import { EnhancedTableHead } from './tableHead';
\r
14 import { EnhancedTableFilter } from './tableFilter';
\r
16 import { ColumnModel, ColumnType } from './columnModel';
\r
17 import { Omit } from '@material-ui/core';
\r
18 import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon';
\r
19 export { ColumnModel, ColumnType } from './columnModel';
\r
21 type propType = string | number | null | undefined | (string|number)[];
\r
22 type dataType = { [prop: string]: propType };
\r
23 type resultType<TData = dataType> = { page: number, rowCount: number, rows: TData[] };
\r
25 export type DataCallback<TData = dataType> = (page?: number, rowsPerPage?: number, orderBy?: string | null, order?: 'asc' | 'desc' | null, filter?: { [property: string]: string }) =>resultType<TData> | Promise<resultType<TData>>;
\r
27 function desc(a: dataType, b: dataType, orderBy: string) {
\r
28 if ((b[orderBy] || "") < (a[orderBy] || "") ) {
\r
31 if ((b[orderBy] || "") > (a[orderBy] || "") ) {
\r
37 function stableSort(array: dataType[], cmp: (a: dataType, b: dataType) => number) {
\r
38 const stabilizedThis = array.map((el, index) => [el, index]) as [dataType, number][];
\r
39 stabilizedThis.sort((a, b) => {
\r
40 const order = cmp(a[0], b[0]);
\r
41 if (order !== 0) return order;
\r
44 return stabilizedThis.map(el => el[0]);
\r
47 function getSorting(order: 'asc' | 'desc' | null, orderBy: string) {
\r
48 return order === 'desc' ? (a: dataType, b: dataType) => desc(a, b, orderBy) : (a: dataType, b: dataType) => -desc(a, b, orderBy);
\r
51 const styles = (theme: Theme) => createStyles({
\r
54 marginTop: theme.spacing.unit * 3,
\r
64 export type MaterialTableComponentState<TData = {}> = {
\r
65 order: 'asc' | 'desc';
\r
66 orderBy: string | null;
\r
67 selected: any[] | null;
\r
71 rowsPerPage: number;
\r
73 showFilter: boolean;
\r
74 filter: { [property: string]: string };
\r
77 export type TableApi = { forceRefresh?: () => Promise<void> };
\r
79 type MaterialTableComponentBaseProps<TData> = WithStyles<typeof styles> & {
\r
80 columns: ColumnModel<TData>[];
\r
81 idProperty: keyof TData | ((data: TData) => React.Key );
\r
83 enableSelection?: boolean;
\r
84 disableSorting?: boolean;
\r
85 disableFilter?: boolean;
\r
86 customActionButtons?: { icon: React.ComponentType<SvgIconProps>, tooltip?: string, onClick: () => void }[];
\r
87 onHandleClick?(event: React.MouseEvent<HTMLTableRowElement>, rowData: TData): void;
\r
90 type MaterialTableComponentPropsWithRows<TData={}> = MaterialTableComponentBaseProps<TData> & { rows: TData[]; asynchronus?: boolean; };
\r
91 type MaterialTableComponentPropsWithRequestData<TData={}> = MaterialTableComponentBaseProps<TData> & { onRequestData: DataCallback; tableApi?: TableApi; };
\r
92 type MaterialTableComponentPropsWithExternalState<TData={}> = MaterialTableComponentBaseProps<TData> & MaterialTableComponentState & {
\r
93 onToggleFilter: () => void;
\r
94 onFilterChanged: (property: string, filterTerm: string) => void;
\r
95 onHandleChangePage: (page: number) => void;
\r
96 onHandleChangeRowsPerPage: (rowsPerPage: number | null) => void;
\r
97 onHandleRequestSort: (property: string) => void;
\r
100 type MaterialTableComponentProps<TData = {}> =
\r
101 MaterialTableComponentPropsWithRows<TData> |
\r
102 MaterialTableComponentPropsWithRequestData<TData> |
\r
103 MaterialTableComponentPropsWithExternalState<TData>;
\r
105 function isMaterialTableComponentPropsWithRows(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithRows {
\r
106 return (props as MaterialTableComponentPropsWithRows).rows !== undefined && (props as MaterialTableComponentPropsWithRows).rows instanceof Array;
\r
109 function isMaterialTableComponentPropsWithRequestData(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithRequestData {
\r
110 return (props as MaterialTableComponentPropsWithRequestData).onRequestData !== undefined && (props as MaterialTableComponentPropsWithRequestData).onRequestData instanceof Function;
\r
113 function isMaterialTableComponentPropsWithRowsAndRequestData(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithExternalState {
\r
114 const propsWithExternalState = (props as MaterialTableComponentPropsWithExternalState)
\r
115 return propsWithExternalState.onFilterChanged instanceof Function ||
\r
116 propsWithExternalState.onHandleChangePage instanceof Function ||
\r
117 propsWithExternalState.onHandleChangeRowsPerPage instanceof Function ||
\r
118 propsWithExternalState.onToggleFilter instanceof Function ||
\r
119 propsWithExternalState.onHandleRequestSort instanceof Function
\r
122 class MaterialTableComponent<TData extends {} = {}> extends React.Component<MaterialTableComponentProps, MaterialTableComponentState> {
\r
124 constructor(props: MaterialTableComponentProps) {
\r
127 const page = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.page : 0;
\r
128 const rowsPerPage = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.rowsPerPage || 10 : 10;
\r
131 filter: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.filter || {} : {},
\r
132 showFilter: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.showFilter : false,
\r
133 loading: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.loading : false,
\r
134 order: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.order : 'asc',
\r
135 orderBy: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.orderBy : null,
\r
136 selected: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.selected : null,
\r
137 rows: isMaterialTableComponentPropsWithRows(this.props) && this.props.rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) || [],
\r
138 rowCount: isMaterialTableComponentPropsWithRows(this.props) && this.props.rows.length || 0,
\r
143 if (isMaterialTableComponentPropsWithRequestData(this.props)) {
\r
146 if (this.props.tableApi) {
\r
147 this.props.tableApi.forceRefresh = () => this.update();
\r
151 render(): JSX.Element {
\r
152 const { classes, columns } = this.props;
\r
153 const { rows, rowCount, order, orderBy, selected, rowsPerPage, page, showFilter, filter } = this.state;
\r
154 const emptyRows = rowsPerPage - Math.min(rowsPerPage, rowCount - page * rowsPerPage);
\r
155 const getId = typeof this.props.idProperty !== "function" ? (data: TData) => ((data as {[key:string]: any })[this.props.idProperty as any as string] as string | number) : this.props.idProperty;
\r
156 const toggleFilter = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.onToggleFilter : () => { !this.props.disableFilter && this.setState({ showFilter: !showFilter }, this.update) }
\r
158 <Paper className={ classes.root }>
\r
159 <TableToolbar numSelected={ selected && selected.length } title={ this.props.title } customActionButtons={ this.props.customActionButtons } onExportToCsv={ this.exportToCsv }
\r
160 onToggleFilter={ toggleFilter } />
\r
161 <div className={ classes.tableWrapper }>
\r
162 <Table className={ classes.table } aria-labelledby="tableTitle">
\r
164 columns={ columns }
\r
165 numSelected={ selected && selected.length }
\r
167 orderBy={ orderBy }
\r
168 onSelectAllClick={ this.handleSelectAllClick }
\r
169 onRequestSort={ this.onHandleRequestSort }
\r
170 rowCount={ rows.length }
\r
171 enableSelection={ this.props.enableSelection }
\r
174 { showFilter && <EnhancedTableFilter columns={ columns } filter={ filter } onFilterChanged={ this.onFilterChanged } enableSelection={this.props.enableSelection} /> || null }
\r
175 { rows // may need ordering here
\r
176 .map((entry: TData & { [key: string]: any }) => {
\r
177 const entryId = getId(entry);
\r
178 const isSelected = this.isSelected(entryId);
\r
182 onClick={ event => this.handleClick(event, entry, entryId) }
\r
184 aria-checked={ isSelected }
\r
187 selected={ isSelected }
\r
189 { this.props.enableSelection
\r
190 ? <TableCell padding="checkbox" style={ { width: "50px" } }>
\r
191 <Checkbox checked={ isSelected } />
\r
196 this.props.columns.map(
\r
198 const style = col.width ? { width: col.width } : { };
\r
200 <TableCell key={ col.property } align={ col.type === ColumnType.numeric && !col.align ? "right": col.align } style={ style }>
\r
201 { col.type === ColumnType.custom && col.customControl
\r
202 ? <col.customControl className={col.className} style={col.style} rowData={ entry } />
\r
203 : col.type === ColumnType.boolean
\r
204 ? <span className={col.className} style={col.style}>{col.labels ? col.labels[entry[col.property] ? "true": "false"] : String(entry[col.property]) }</span>
\r
205 : <span className={col.className} style={col.style}>{String(entry[col.property])}</span>
\r
215 { emptyRows > 0 && (
\r
216 <TableRow style={ { height: 49 * emptyRows } }>
\r
217 <TableCell colSpan={ this.props.columns.length } />
\r
224 rowsPerPageOptions={ [5, 10, 25] }
\r
227 rowsPerPage={ rowsPerPage }
\r
229 backIconButtonProps={ {
\r
230 'aria-label': 'Previous Page',
\r
232 nextIconButtonProps={ {
\r
233 'aria-label': 'Next Page',
\r
235 onChangePage={ this.onHandleChangePage }
\r
236 onChangeRowsPerPage={ this.onHandleChangeRowsPerPage }
\r
242 static getDerivedStateFromProps(props: MaterialTableComponentProps, state: MaterialTableComponentState & { _rawRows: {}[] }): MaterialTableComponentState & { _rawRows: {}[] } {
\r
243 if (isMaterialTableComponentPropsWithRowsAndRequestData(props)) {
\r
247 rowCount: props.rowCount,
\r
248 orderBy: props.orderBy,
\r
249 order: props.order,
\r
250 filter: props.filter,
\r
251 loading: props.loading,
\r
252 showFilter: props.showFilter,
\r
254 rowsPerPage: props.rowsPerPage
\r
256 } else if (isMaterialTableComponentPropsWithRows(props) && props.asynchronus && state._rawRows !== props.rows) {
\r
257 const newState = MaterialTableComponent.updateRows(props, state);
\r
261 _rawRows: props.rows || []
\r
267 private static updateRows(props: MaterialTableComponentPropsWithRows, state: MaterialTableComponentState): { rows: {}[], rowCount: number } {
\r
269 const { page, rowsPerPage, order, orderBy, filter } = state;
\r
270 let data: dataType[] = props.rows || [];
\r
271 let filtered = false;
\r
272 if (state.showFilter) {
\r
273 Object.keys(filter).forEach(prop => {
\r
274 const exp = filter[prop];
\r
275 filtered = filtered || exp !== undefined;
\r
276 data = exp !== undefined ? data.filter((val) => {
\r
277 const value = val[prop];
\r
278 return (value == exp) || (value && value.toString().indexOf(String(exp)) > -1);
\r
283 const rowCount = data.length;
\r
285 data = (orderBy && order
\r
286 ? stableSort(data, getSorting(order, orderBy))
\r
287 : data).slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
\r
301 private async update() {
\r
302 if (isMaterialTableComponentPropsWithRequestData(this.props)) {
\r
303 const response = await Promise.resolve(
\r
304 this.props.onRequestData(
\r
305 this.state.page, this.state.rowsPerPage, this.state.orderBy, this.state.order, this.state.showFilter && this.state.filter || {})
\r
307 this.setState(response);
\r
309 this.setState(MaterialTableComponent.updateRows(this.props, this.state));
\r
313 private onFilterChanged = (property: string, filterTerm: string) => {
\r
314 if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
\r
315 this.props.onFilterChanged(property, filterTerm);
\r
318 if (this.props.disableFilter) return;
\r
319 const colDefinition = this.props.columns && this.props.columns.find(col => col.property === property);
\r
320 if (colDefinition && colDefinition.disableFilter) return;
\r
322 const filter = { ...this.state.filter, [property]: filterTerm };
\r
328 private onHandleRequestSort = (event: React.SyntheticEvent, property: string) => {
\r
329 if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
\r
330 this.props.onHandleRequestSort(property);
\r
333 if (this.props.disableSorting) return;
\r
334 const colDefinition = this.props.columns && this.props.columns.find(col => col.property === property);
\r
335 if (colDefinition && colDefinition.disableSorting) return;
\r
337 const orderBy = this.state.orderBy === property && this.state.order === 'desc' ? null : property;
\r
338 const order = this.state.orderBy === property && this.state.order === 'asc' ? 'desc' : 'asc';
\r
345 handleSelectAllClick: () => {};
\r
347 private onHandleChangePage = (event: React.MouseEvent<HTMLButtonElement> | null, page: number) => {
\r
348 if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
\r
349 this.props.onHandleChangePage(page);
\r
357 private onHandleChangeRowsPerPage = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
\r
358 if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
\r
359 this.props.onHandleChangeRowsPerPage(+(event && event.target.value));
\r
362 const rowsPerPage = +(event && event.target.value);
\r
363 if (rowsPerPage && rowsPerPage > 0) {
\r
370 private isSelected(id: string | number): boolean {
\r
371 let selected = this.state.selected || [];
\r
372 const selectedIndex = selected.indexOf(id);
\r
373 return (selectedIndex > -1);
\r
376 private handleClick(event: React.MouseEvent<HTMLTableRowElement>, rowData: TData, id: string | number): void {
\r
377 if (this.props.onHandleClick instanceof Function) {
\r
378 this.props.onHandleClick(event, rowData);
\r
381 if (!this.props.enableSelection){
\r
384 let selected = this.state.selected || [];
\r
385 const selectedIndex = selected.indexOf(id);
\r
386 if (selectedIndex > -1) {
\r
388 ...selected.slice(0, selectedIndex),
\r
389 ...selected.slice(selectedIndex + 1)
\r
402 private exportToCsv = () => {
\r
404 const data: string[] = [];
\r
405 data.push(this.props.columns.map(col => col.title || col.property).join(',')+"\r\n");
\r
406 this.state.rows && this.state.rows.forEach((row : any)=> {
\r
407 data.push(this.props.columns.map(col => row[col.property]).join(',') + "\r\n");
\r
409 const properties = { type: 'text/csv' }; // Specify the file's mime-type.
\r
411 // Specify the filename using the File constructor, but ...
\r
412 file = new File(data, "export.csv", properties);
\r
414 // ... fall back to the Blob constructor if that isn't supported.
\r
415 file = new Blob(data, properties);
\r
417 const url = URL.createObjectURL(file);
\r
418 window.location.replace(url);
\r
422 export type MaterialTableCtorType<TData extends {} = {}> = new () => React.Component<Omit<MaterialTableComponentProps<TData>, 'classes'>>;
\r
424 export const MaterialTable = withStyles(styles)(MaterialTableComponent);
\r
425 export default MaterialTable;