• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gregnb / mui-datatables / #1880

01 Aug 2019 07:29AM UTC coverage: 74.963% (+3.7%) from 71.311%
#1880

push

gabrielliwerant
2.7.0

444 of 611 branches covered (72.67%)

Branch coverage included in aggregate %.

568 of 739 relevant lines covered (76.86%)

85.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.94
/src/MUIDataTable.js
1
import Paper from '@material-ui/core/Paper';
2
import { withStyles } from '@material-ui/core/styles';
3
import MuiTable from '@material-ui/core/Table';
4
import classnames from 'classnames';
5
import cloneDeep from 'lodash.clonedeep';
6
import find from 'lodash.find';
7
import isUndefined from 'lodash.isundefined';
8
import merge from 'lodash.merge';
9
import PropTypes from 'prop-types';
10
import React from 'react';
11
import TableBody from './components/TableBody';
12
import TableFilterList from './components/TableFilterList';
13
import TableFooter from './components/TableFooter';
14
import TableHead from './components/TableHead';
15
import TableResize from './components/TableResize';
16
import TableToolbar from './components/TableToolbar';
17
import TableToolbarSelect from './components/TableToolbarSelect';
18
import textLabels from './textLabels';
19
import { buildMap, getCollatorComparator, sortCompare } from './utils';
20

21
const defaultTableStyles = theme => ({
1✔
22
  root: {},
23
  paper: {},
24
  tableRoot: {
25
    outline: 'none',
26
  },
27
  responsiveScroll: {
28
    overflowX: 'auto',
29
    overflow: 'auto',
30
    height: '100%',
31
    maxHeight: '499px',
32
  },
33
  responsiveStacked: {
34
    overflowX: 'auto',
35
    overflow: 'auto',
36
    [theme.breakpoints.down('sm')]: {
37
      overflowX: 'hidden',
38
      overflow: 'hidden',
39
    },
40
  },
41
  caption: {
42
    position: 'absolute',
43
    left: '-3000px',
44
  },
45
  liveAnnounce: {
46
    border: '0',
47
    clip: 'rect(0 0 0 0)',
48
    height: '1px',
49
    margin: '-1px',
50
    overflow: 'hidden',
51
    padding: '0',
52
    position: 'absolute',
53
    width: '1px',
54
  },
55
  '@global': {
56
    '@media print': {
57
      '.datatables-noprint': {
58
        display: 'none',
59
      },
60
    },
61
  },
62
});
63

64
const TABLE_LOAD = {
1✔
65
  INITIAL: 1,
66
  UPDATE: 2,
67
};
68

69
// Populate this list with anything that might render in the toolbar to determine if we hide the toolbar
70
const TOOLBAR_ITEMS = ['title', 'filter', 'search', 'print', 'download', 'viewColumns', 'customToolbar'];
1✔
71

72
const hasToolbarItem = (options, title) => {
1✔
73
  options.title = title;
107✔
74

75
  return !isUndefined(find(TOOLBAR_ITEMS, i => options[i]));
219✔
76
};
77

78
class MUIDataTable extends React.Component {
79
  static propTypes = {
80
    /** Title of the table */
81
    title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
82
    /** Data used to describe table */
83
    data: PropTypes.array.isRequired,
84
    /** Columns used to describe table */
85
    columns: PropTypes.PropTypes.arrayOf(
86
      PropTypes.oneOfType([
87
        PropTypes.string,
88
        PropTypes.shape({
89
          label: PropTypes.string,
90
          name: PropTypes.string.isRequired,
91
          options: PropTypes.shape({
92
            display: PropTypes.string, // enum('true', 'false', 'excluded')
93
            empty: PropTypes.bool,
94
            filter: PropTypes.bool,
95
            sort: PropTypes.bool,
96
            print: PropTypes.bool,
97
            searchable: PropTypes.bool,
98
            download: PropTypes.bool,
99
            viewColumns: PropTypes.bool,
100
            filterList: PropTypes.array,
101
            filterOptions: PropTypes.oneOfType([
102
              PropTypes.array,
103
              PropTypes.shape({
104
                names: PropTypes.array,
105
                logic: PropTypes.func,
106
                display: PropTypes.func,
107
              }),
108
            ]),
109
            filterType: PropTypes.oneOf(['dropdown', 'checkbox', 'multiselect', 'textField', 'custom']),
110
            customHeadRender: PropTypes.func,
111
            customBodyRender: PropTypes.func,
112
            customFilterListRender: PropTypes.func,
113
          }),
114
        }),
115
      ]),
116
    ).isRequired,
117
    /** Options used to describe table */
118
    options: PropTypes.shape({
119
      responsive: PropTypes.oneOf(['stacked', 'scroll']),
120
      filterType: PropTypes.oneOf(['dropdown', 'checkbox', 'multiselect', 'textField', 'custom']),
121
      textLabels: PropTypes.object,
122
      pagination: PropTypes.bool,
123
      expandableRows: PropTypes.bool,
124
      expandableRowsOnClick: PropTypes.bool,
125
      renderExpandableRow: PropTypes.func,
126
      customToolbar: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
127
      customToolbarSelect: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
128
      customFooter: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
129
      customRowRender: PropTypes.func,
130
      onRowClick: PropTypes.func,
131
      resizableColumns: PropTypes.bool,
132
      selectableRows: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['none', 'single', 'multiple'])]),
133
      selectableRowsOnClick: PropTypes.bool,
134
      isRowSelectable: PropTypes.func,
135
      serverSide: PropTypes.bool,
136
      onTableChange: PropTypes.func,
137
      onTableInit: PropTypes.func,
138
      caseSensitive: PropTypes.bool,
139
      rowHover: PropTypes.bool,
140
      fixedHeader: PropTypes.bool,
141
      page: PropTypes.number,
142
      count: PropTypes.number,
143
      rowsSelected: PropTypes.array,
144
      rowsExpanded: PropTypes.array,
145
      rowsPerPage: PropTypes.number,
146
      rowsPerPageOptions: PropTypes.array,
147
      filter: PropTypes.bool,
148
      sort: PropTypes.bool,
149
      customSort: PropTypes.func,
150
      customSearch: PropTypes.func,
151
      search: PropTypes.bool,
152
      searchText: PropTypes.string,
153
      print: PropTypes.bool,
154
      viewColumns: PropTypes.bool,
155
      download: PropTypes.bool,
156
      downloadOptions: PropTypes.shape({
157
        filename: PropTypes.string,
158
        separator: PropTypes.string,
159
        filterOptions: PropTypes.shape({
160
          useDisplayedColumnsOnly: PropTypes.bool,
161
          useDisplayedRowsOnly: PropTypes.bool,
162
        }),
163
      }),
164
      onDownload: PropTypes.func,
165
    }),
166
    /** Pass and use className to style MUIDataTable as desired */
167
    className: PropTypes.string,
168
  };
169

170
  static defaultProps = {
171
    title: '',
172
    options: {},
173
    data: [],
174
    columns: [],
175
  };
176

177
  state = {
178
    announceText: null,
179
    activeColumn: null,
180
    data: [],
181
    displayData: [],
182
    page: 0,
183
    rowsPerPage: 0,
184
    count: 0,
185
    columns: [],
186
    filterData: [],
187
    filterList: [],
188
    selectedRows: {
189
      data: [],
190
      lookup: {},
191
    },
192
    expandedRows: {
193
      data: [],
194
      lookup: {},
195
    },
196
    showResponsive: false,
197
    searchText: null,
198
  };
199

200
  constructor() {
201
    super();
54✔
202
    this.tableRef = false;
54✔
203
    this.tableContent = React.createRef();
54✔
204
    this.headCellRefs = {};
54✔
205
    this.setHeadResizeable = () => {};
54✔
206
    this.updateDividers = () => {};
54✔
207
  }
208

209
  componentWillMount() {
210
    this.initializeTable(this.props);
54✔
211
  }
212

213
  componentDidMount() {
214
    this.setHeadResizeable(this.headCellRefs, this.tableRef);
54✔
215

216
    // When we have a search, we must reset page to view it
217
    if (this.props.options.searchText) this.setState({ page: 0 });
54✔
218
  }
219

220
  componentDidUpdate(prevProps) {
221
    if (this.props.data !== prevProps.data || this.props.columns !== prevProps.columns) {
53✔
222
      this.setTableData(this.props, TABLE_LOAD.INITIAL, () => {
2✔
223
        this.setTableAction('propsUpdate');
2✔
224
      });
225
      this.updateOptions(this.props);
2✔
226
    }
227

228
    if (this.props.options.searchText !== prevProps.options.searchText) {
53!
229
      // When we have a search, we must reset page to view it
230
      this.setState({ page: 0 });
×
231
    }
232

233
    if (this.options.resizableColumns) {
53!
234
      this.setHeadResizeable(this.headCellRefs, this.tableRef);
×
235
      this.updateDividers();
×
236
    }
237
  }
238

239
  updateOptions(props) {
240
    this.options = merge(this.options, props.options);
2✔
241
  }
242

243
  initializeTable(props) {
244
    this.getDefaultOptions(props);
54✔
245
    this.setTableOptions(props);
54✔
246
    this.setTableData(props, TABLE_LOAD.INITIAL, () => {
54✔
247
      this.setTableInit('tableInitialized');
54✔
248
    });
249
  }
250

251
  /*
252
   * React currently does not support deep merge for defaultProps. Objects are overwritten
253
   */
254
  getDefaultOptions(props) {
255
    const defaultOptions = {
54✔
256
      responsive: 'stacked',
257
      filterType: 'dropdown',
258
      pagination: true,
259
      textLabels,
260
      expandableRows: false,
261
      expandableRowsOnClick: false,
262
      resizableColumns: false,
263
      selectableRows: 'multiple',
264
      selectableRowsOnClick: false,
265
      caseSensitive: false,
266
      serverSide: false,
267
      rowHover: true,
268
      fixedHeader: true,
269
      elevation: 4,
270
      rowsPerPage: 10,
271
      rowsPerPageOptions: [10, 15, 100],
272
      filter: true,
273
      sortFilterList: true,
274
      sort: true,
275
      search: true,
276
      print: true,
277
      viewColumns: true,
278
      download: true,
279
      downloadOptions: {
280
        filename: 'tableDownload.csv',
281
        separator: ',',
282
      },
283
    };
284

285
    const extra = {};
54✔
286
    if (typeof props.options.selectableRows === 'boolean') {
54✔
287
      extra.selectableRows = props.options.selectableRows ? 'multiple' : 'none';
5!
288
    }
289
    this.options = merge(defaultOptions, props.options, extra);
54✔
290
    if (props.options.rowsPerPageOptions) {
54✔
291
      this.options.rowsPerPageOptions = props.options.rowsPerPageOptions;
2✔
292
    }
293
  }
294

295
  validateOptions(options) {
296
    if (options.serverSide && options.onTableChange === undefined) {
54!
297
      throw Error('onTableChange callback must be provided when using serverSide option');
×
298
    }
299
    if (options.expandableRows && options.renderExpandableRow === undefined) {
54!
300
      throw Error('renderExpandableRow must be provided when using expandableRows option');
×
301
    }
302
    if (this.props.options.filterList) {
54!
303
      console.error(
×
304
        'Deprecated: filterList must now be provided under each column option. see https://github.com/gregnb/mui-datatables/tree/master/examples/column-filters example',
305
      );
306
    }
307
  }
308

309
  setTableAction = action => {
310
    if (typeof this.options.onTableChange === 'function') {
48✔
311
      this.options.onTableChange(action, this.state);
3✔
312
    }
313
  };
314

315
  setTableInit = action => {
316
    if (typeof this.options.onTableInit === 'function') {
54✔
317
      this.options.onTableInit(action, this.state);
2✔
318
    }
319
  };
320

321
  setTableOptions(props) {
322
    const optionNames = ['rowsPerPage', 'page', 'rowsSelected', 'rowsPerPageOptions'];
54✔
323
    const optState = optionNames.reduce((acc, cur) => {
54✔
324
      if (this.options[cur] !== undefined) {
216✔
325
        acc[cur] = this.options[cur];
110✔
326
      }
327
      return acc;
216✔
328
    }, {});
329

330
    this.validateOptions(optState);
54✔
331
    this.setState(optState);
54✔
332
  }
333

334
  setHeadCellRef = (index, el) => {
335
    this.headCellRefs[index] = el;
135✔
336
  };
337

338
  getTableContentRef = () => {
339
    return this.tableContent.current;
×
340
  };
341

342
  rawColumns = cols => {
343
    return cols.map(item => {
×
344
      if (typeof item !== 'object') return item;
×
345

346
      let otherOptions = {};
×
347
      const { options, ...otherProps } = item;
×
348

349
      if (options) {
×
350
        const { customHeadRender, customBodyRender, customFilterListRender, setCellProps, ...nonFnOpts } = options;
×
351
        otherOptions = nonFnOpts;
×
352
      }
353

354
      return { ...otherOptions, ...otherProps };
×
355
    });
356
  };
357

358
  /*
359
   *  Build the source table data
360
   */
361

362
  buildColumns = newColumns => {
363
    let columnData = [];
57✔
364
    let filterData = [];
57✔
365
    let filterList = [];
57✔
366

367
    newColumns.forEach((column, colIndex) => {
57✔
368
      let columnOptions = {
273✔
369
        display: 'true',
370
        empty: false,
371
        filter: true,
372
        sort: true,
373
        print: true,
374
        searchable: true,
375
        download: true,
376
        viewColumns: true,
377
        sortDirection: null,
378
      };
379

380
      if (typeof column === 'object') {
273✔
381
        if (column.options && column.options.display !== undefined) {
220!
382
          column.options.display = column.options.display.toString();
×
383
        }
384

385
        columnOptions = {
220✔
386
          name: column.name,
387
          label: column.label ? column.label : column.name,
220✔
388
          ...columnOptions,
389
          ...(column.options ? column.options : {}),
220✔
390
        };
391
      } else {
392
        columnOptions = { ...columnOptions, name: column, label: column };
53✔
393
      }
394

395
      columnData.push(columnOptions);
273✔
396

397
      filterData[colIndex] = [];
273✔
398
      filterList[colIndex] = [];
273✔
399
    });
400

401
    return { columns: columnData, filterData, filterList };
57✔
402
  };
403

404
  transformData = (columns, data) => {
405
    const leaf = (obj, path) => path.split('.').reduce((value, el) => (value ? value[el] : undefined), obj);
56!
406

407
    return Array.isArray(data[0])
56✔
408
      ? data.map(row => {
409
          let i = -1;
242✔
410

411
          return columns.map(col => {
242✔
412
            if (!col.empty) i++;
1,162✔
413
            return col.empty ? undefined : row[i];
1,162✔
414
          });
415
        })
416
      : data.map(row => columns.map(col => leaf(row, col.name)));
20✔
417
  };
418

419
  setTableData(props, status, callback = () => {}) {
×
420
    const { options } = props;
57✔
421

422
    let tableData = [];
57✔
423
    let { columns, filterData, filterList } = this.buildColumns(props.columns);
57✔
424
    let sortIndex = null;
57✔
425
    let sortDirection = null;
57✔
426

427
    const data = status === TABLE_LOAD.INITIAL ? this.transformData(columns, props.data) : props.data;
57✔
428
    const searchText = status === TABLE_LOAD.INITIAL ? options.searchText : null;
57✔
429

430
    columns.forEach((column, colIndex) => {
57✔
431
      for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
273✔
432
        let value = status === TABLE_LOAD.INITIAL ? data[rowIndex][colIndex] : data[rowIndex].data[colIndex];
1,197✔
433

434
        if (typeof tableData[rowIndex] === 'undefined') {
1,197✔
435
          tableData.push({
249✔
436
            index: status === TABLE_LOAD.INITIAL ? rowIndex : data[rowIndex].index,
249✔
437
            data: status === TABLE_LOAD.INITIAL ? data[rowIndex] : data[rowIndex].data,
249✔
438
          });
439
        }
440

441
        if (typeof column.customBodyRender === 'function') {
1,197✔
442
          const tableMeta = this.getTableMeta(rowIndex, colIndex, value, column, [], this.state);
691✔
443
          const funcResult = column.customBodyRender(value, tableMeta);
691✔
444

445
          if (React.isValidElement(funcResult) && funcResult.props.value) {
691✔
446
            value = funcResult.props.value;
229✔
447
          } else if (typeof funcResult === 'string') {
462✔
448
            value = funcResult;
412✔
449
          }
450
        }
451

452
        if (filterData[colIndex].indexOf(value) < 0 && !Array.isArray(value)) {
1,197✔
453
          filterData[colIndex].push(value);
743✔
454
        } else if (Array.isArray(value)) {
454✔
455
          value.forEach(element => {
8✔
456
            if (filterData[colIndex].indexOf(element) < 0) {
12✔
457
              filterData[colIndex].push(element);
6✔
458
            }
459
          });
460
        }
461
      }
462

463
      if (column.filterOptions) {
273✔
464
        if (Array.isArray(column.filterOptions)) {
3!
465
          filterData[colIndex] = cloneDeep(column.filterOptions);
×
466
          console.error(
×
467
            'Deprecated: filterOptions must now be an object. see https://github.com/gregnb/mui-datatables/tree/master/examples/customize-filter example',
468
          );
469
        } else if (Array.isArray(column.filterOptions.names)) {
3!
470
          filterData[colIndex] = cloneDeep(column.filterOptions.names);
3✔
471
        }
472
      }
473

474
      if (column.filterList) {
273!
475
        filterList[colIndex] = cloneDeep(column.filterList);
×
476
      }
477

478
      if (this.options.sortFilterList) {
273!
479
        const comparator = getCollatorComparator();
273✔
480
        filterData[colIndex].sort(comparator);
273✔
481
      }
482

483
      if (column.sortDirection !== null) {
273!
484
        sortIndex = colIndex;
×
485
        sortDirection = column.sortDirection;
×
486
      }
487
    });
488

489
    let selectedRowsData = {
57✔
490
      data: [],
491
      lookup: {},
492
    };
493

494
    let expandedRowsData = {
57✔
495
      data: [],
496
      lookup: {},
497
    };
498

499
    if (TABLE_LOAD.INITIAL) {
57!
500
      // Multiple row selection customization
501
      if (options.rowsSelected && options.rowsSelected.length && options.selectableRows === 'multiple') {
57✔
502
        options.rowsSelected.forEach(row => {
1✔
503
          let rowPos = row;
2✔
504

505
          for (let cIndex = 0; cIndex < this.state.displayData.length; cIndex++) {
2✔
506
            if (this.state.displayData[cIndex].dataIndex === row) {
×
507
              rowPos = cIndex;
×
508
              break;
×
509
            }
510
          }
511

512
          selectedRowsData.data.push({ index: rowPos, dataIndex: row });
2✔
513
          selectedRowsData.lookup[row] = true;
2✔
514
        });
515
      }
516

517
      // Single row selection customization
518
      if (options.rowsSelected && options.rowsSelected.length === 1 && options.selectableRows === 'single') {
57!
519
        let rowPos = options.rowsSelected[0];
×
520

521
        for (let cIndex = 0; cIndex < this.state.displayData.length; cIndex++) {
×
522
          if (this.state.displayData[cIndex].dataIndex === options.rowsSelected[0]) {
×
523
            rowPos = cIndex;
×
524
            break;
×
525
          }
526
        }
527

528
        selectedRowsData.data.push({ index: rowPos, dataIndex: options.rowsSelected[0] });
×
529
        selectedRowsData.lookup[options.rowsSelected[0]] = true;
×
530
      } else if (options.rowsSelected && options.rowsSelected.length > 1 && options.selectableRows === 'single') {
57!
531
        console.error(
×
532
          'Multiple values provided for selectableRows, but selectableRows set to "single". Either supply only a single value or use "multiple".',
533
        );
534
      }
535

536
      if (options.rowsExpanded && options.rowsExpanded.length && options.expandableRows) {
57✔
537
        options.rowsExpanded.forEach(row => {
1✔
538
          let rowPos = row;
2✔
539

540
          for (let cIndex = 0; cIndex < this.state.displayData.length; cIndex++) {
2✔
541
            if (this.state.displayData[cIndex].dataIndex === row) {
×
542
              rowPos = cIndex;
×
543
              break;
×
544
            }
545
          }
546

547
          expandedRowsData.data.push({ index: rowPos, dataIndex: row });
2✔
548
          expandedRowsData.lookup[row] = true;
2✔
549
        });
550
      }
551
    }
552

553
    if (!options.serverSide && sortIndex !== null) {
57!
554
      const sortedData = this.sortTable(tableData, sortIndex, sortDirection);
×
555
      tableData = sortedData.data;
×
556
    }
557
    /* set source data and display Data set source set */
558
    this.setState(
57✔
559
      prevState => ({
57✔
560
        columns: columns,
561
        filterData: filterData,
562
        filterList: filterList,
563
        searchText: searchText,
564
        selectedRows: selectedRowsData,
565
        expandedRows: expandedRowsData,
566
        count: options.count,
567
        data: tableData,
568
        displayData: this.getDisplayData(columns, tableData, filterList, searchText),
569
      }),
570
      callback,
571
    );
572
  }
573

574
  /*
575
   *  Build the table data used to display to the user (ie: after filter/search applied)
576
   */
577
  computeDisplayRow(columns, row, rowIndex, filterList, searchText) {
578
    let isFiltered = false;
353✔
579
    let isSearchFound = false;
353✔
580
    let displayRow = [];
353✔
581

582
    for (let index = 0; index < row.length; index++) {
353✔
583
      let columnDisplay = row[index];
1,693✔
584
      let columnValue = row[index];
1,693✔
585
      let column = columns[index];
1,693✔
586

587
      if (column.customBodyRender) {
1,693✔
588
        const tableMeta = this.getTableMeta(rowIndex, index, row, column, this.state.data, {
967✔
589
          ...this.state,
590
          filterList: filterList,
591
          searchText: searchText,
592
        });
593

594
        const funcResult = column.customBodyRender(
967✔
595
          columnValue,
596
          tableMeta,
597
          this.updateDataCol.bind(null, rowIndex, index),
598
        );
599
        columnDisplay = funcResult;
967✔
600

601
        /* drill down to get the value of a cell */
602
        columnValue =
967✔
603
          typeof funcResult === 'string' || !funcResult
2,328✔
604
            ? funcResult
605
            : funcResult.props && funcResult.props.value
963!
606
            ? funcResult.props.value
607
            : columnValue;
608
      }
609

610
      displayRow.push(columnDisplay);
1,693✔
611

612
      const columnVal = columnValue === null || columnValue === undefined ? '' : columnValue.toString();
1,693✔
613

614
      const filterVal = filterList[index];
1,693✔
615
      const caseSensitive = this.options.caseSensitive;
1,693✔
616
      const filterType = column.filterType || this.options.filterType;
1,693✔
617
      if (filterVal.length || filterType === 'custom') {
1,693✔
618
        if (column.filterOptions && column.filterOptions.logic) {
68✔
619
          if (column.filterOptions.logic(columnValue, filterVal)) isFiltered = true;
12✔
620
        } else if (filterType === 'textField' && !this.hasSearchText(columnVal, filterVal, caseSensitive)) {
56✔
621
          isFiltered = true;
2✔
622
        } else if (
54✔
623
          filterType !== 'textField' &&
154✔
624
          Array.isArray(columnValue) === false &&
625
          filterVal.indexOf(columnValue) < 0
626
        ) {
627
          isFiltered = true;
45✔
628
        } else if (filterType !== 'textField' && Array.isArray(columnValue)) {
9✔
629
          //true if every filterVal exists in columnVal, false otherwise
630
          const isFullMatch = filterVal.every(el => {
4✔
631
            return columnValue.indexOf(el) >= 0;
4✔
632
          });
633
          //if it is not a fullMatch, filter row out
634
          if (!isFullMatch) {
4✔
635
            isFiltered = true;
2✔
636
          }
637
        }
638
      }
639

640
      if (
1,693✔
641
        searchText &&
1,737✔
642
        this.hasSearchText(columnVal, searchText, caseSensitive) &&
643
        column.display !== 'false' &&
644
        column.searchable
645
      ) {
646
        isSearchFound = true;
2✔
647
      }
648
    }
649

650
    const { customSearch } = this.props.options;
353✔
651

652
    if (searchText && customSearch) {
353!
653
      const customSearchResult = customSearch(searchText, row, columns);
×
654
      if (typeof customSearchResult !== 'boolean') {
×
655
        console.error('customSearch must return a boolean');
×
656
      } else {
657
        isSearchFound = customSearchResult;
×
658
      }
659
    }
660

661
    if (this.options.serverSide) {
353✔
662
      if (customSearch) {
8!
663
        console.warn('Server-side filtering is enabled, hence custom search will be ignored.');
×
664
      }
665

666
      return displayRow;
8✔
667
    }
668

669
    if (isFiltered || (searchText && !isSearchFound)) return null;
345✔
670
    else return displayRow;
285✔
671
  }
672

673
  hasSearchText = (toSearch, toFind, caseSensitive) => {
674
    let stack = toSearch.toString();
44✔
675
    let needle = toFind.toString();
44✔
676

677
    if (!caseSensitive) {
44!
678
      needle = needle.toLowerCase();
44✔
679
      stack = stack.toLowerCase();
44✔
680
    }
681

682
    return stack.indexOf(needle) >= 0;
44✔
683
  };
684

685
  updateDataCol = (row, index, value) => {
686
    this.setState(prevState => {
1✔
687
      let changedData = cloneDeep(prevState.data);
1✔
688
      let filterData = cloneDeep(prevState.filterData);
1✔
689

690
      const tableMeta = this.getTableMeta(row, index, row, prevState.columns[index], prevState.data, prevState);
1✔
691
      const funcResult = prevState.columns[index].customBodyRender(value, tableMeta);
1✔
692

693
      const filterValue =
694
        React.isValidElement(funcResult) && funcResult.props.value
1!
695
          ? funcResult.props.value
696
          : prevState['data'][row][index];
697

698
      const prevFilterIndex = filterData[index].indexOf(filterValue);
1✔
699
      filterData[index].splice(prevFilterIndex, 1, filterValue);
1✔
700

701
      changedData[row].data[index] = value;
1✔
702

703
      if (this.options.sortFilterList) {
1!
704
        const comparator = getCollatorComparator();
1✔
705
        filterData[index].sort(comparator);
1✔
706
      }
707

708
      return {
1✔
709
        data: changedData,
710
        filterData: filterData,
711
        displayData: this.getDisplayData(prevState.columns, changedData, prevState.filterList, prevState.searchText),
712
      };
713
    });
714
  };
715

716
  getTableMeta = (rowIndex, colIndex, rowData, columnData, tableData, curState) => {
717
    const { columns, data, displayData, filterData, ...tableState } = curState;
1,659✔
718

719
    return {
1,659✔
720
      rowIndex: rowIndex,
721
      columnIndex: colIndex,
722
      columnData: columnData,
723
      rowData: rowData,
724
      tableData: tableData,
725
      tableState: tableState,
726
    };
727
  };
728

729
  getDisplayData(columns, data, filterList, searchText) {
730
    let newRows = [];
83✔
731

732
    for (let index = 0; index < data.length; index++) {
83✔
733
      const value = data[index].data;
353✔
734
      const displayRow = this.computeDisplayRow(columns, value, index, filterList, searchText);
353✔
735

736
      if (displayRow) {
353✔
737
        newRows.push({
293✔
738
          data: displayRow,
739
          dataIndex: data[index].index,
740
        });
741
      }
742
    }
743
    return newRows;
83✔
744
  }
745

746
  toggleViewColumn = index => {
747
    this.setState(
1✔
748
      prevState => {
749
        const columns = cloneDeep(prevState.columns);
1✔
750
        columns[index].display = columns[index].display === 'true' ? 'false' : 'true';
1!
751
        return {
1✔
752
          columns: columns,
753
        };
754
      },
755
      () => {
756
        this.setTableAction('columnViewChange');
1✔
757
        if (this.options.onColumnViewChange) {
1!
758
          this.options.onColumnViewChange(
×
759
            this.state.columns[index].name,
760
            this.state.columns[index].display === 'true' ? 'add' : 'remove',
×
761
          );
762
        }
763
      },
764
    );
765
  };
766

767
  getSortDirection(column) {
768
    return column.sortDirection === 'asc' ? 'ascending' : 'descending';
3✔
769
  }
770

771
  toggleSortColumn = index => {
772
    this.setState(
3✔
773
      prevState => {
774
        let columns = cloneDeep(prevState.columns);
3✔
775
        let data = prevState.data;
3✔
776
        const newOrder = columns[index].sortDirection === 'desc' ? 'asc' : 'desc';
3✔
777

778
        for (let pos = 0; pos < columns.length; pos++) {
3✔
779
          if (index !== pos) {
15✔
780
            columns[pos].sortDirection = null;
12✔
781
          } else {
782
            columns[pos].sortDirection = newOrder;
3✔
783
          }
784
        }
785

786
        const orderLabel = this.getSortDirection(columns[index]);
3✔
787
        const announceText = `Table now sorted by ${columns[index].name} : ${orderLabel}`;
3✔
788

789
        let newState = {
3✔
790
          columns: columns,
791
          announceText: announceText,
792
          activeColumn: index,
793
        };
794

795
        if (this.options.serverSide) {
3!
796
          newState = {
×
797
            ...newState,
798
            data: prevState.data,
799
            displayData: prevState.displayData,
800
            selectedRows: prevState.selectedRows,
801
          };
802
        } else {
803
          const sortedData = this.sortTable(data, index, newOrder);
3✔
804

805
          newState = {
3✔
806
            ...newState,
807
            data: sortedData.data,
808
            displayData: this.getDisplayData(columns, sortedData.data, prevState.filterList, prevState.searchText),
809
            selectedRows: sortedData.selectedRows,
810
          };
811
        }
812

813
        return newState;
3✔
814
      },
815
      () => {
816
        this.setTableAction('sort');
3✔
817
        if (this.options.onColumnSortChange) {
3!
818
          this.options.onColumnSortChange(
×
819
            this.state.columns[index].name,
820
            this.getSortDirection(this.state.columns[index]),
821
          );
822
        }
823
      },
824
    );
825
  };
826

827
  changeRowsPerPage = rows => {
828
    /**
829
     * After changing rows per page recalculate totalPages and checks its if current page not higher.
830
     * Otherwise sets current page the value of nextTotalPages
831
     */
832
    const rowCount = this.options.count || this.state.displayData.length;
3✔
833
    const nextTotalPages = Math.floor(rowCount / rows);
3✔
834

835
    this.setState(
3✔
836
      () => ({
3✔
837
        rowsPerPage: rows,
838
        page: this.state.page > nextTotalPages ? nextTotalPages : this.state.page,
3✔
839
      }),
840
      () => {
841
        this.setTableAction('changeRowsPerPage');
3✔
842
        if (this.options.onChangeRowsPerPage) {
3!
843
          this.options.onChangeRowsPerPage(this.state.rowsPerPage);
×
844
        }
845
      },
846
    );
847
  };
848

849
  changePage = page => {
850
    this.setState(
4✔
851
      () => ({
4✔
852
        page: page,
853
      }),
854
      () => {
855
        this.setTableAction('changePage');
4✔
856
        if (this.options.onChangePage) {
4✔
857
          this.options.onChangePage(this.state.page);
2✔
858
        }
859
      },
860
    );
861
  };
862

863
  searchTextUpdate = text => {
864
    this.setState(
1✔
865
      prevState => ({
1✔
866
        searchText: text && text.length ? text : null,
3!
867
        page: 0,
868
        displayData: this.options.serverSide
1!
869
          ? prevState.displayData
870
          : this.getDisplayData(prevState.columns, prevState.data, prevState.filterList, text),
871
      }),
872
      () => {
873
        this.setTableAction('search');
1✔
874
      },
875
    );
876
  };
877

878
  resetFilters = () => {
879
    this.setState(
2✔
880
      prevState => {
881
        const filterList = prevState.columns.map((column, index) => []);
10✔
882

883
        return {
2✔
884
          filterList: filterList,
885
          displayData: this.options.serverSide
2!
886
            ? prevState.displayData
887
            : this.getDisplayData(prevState.columns, prevState.data, filterList, prevState.searchText),
888
        };
889
      },
890
      () => {
891
        this.setTableAction('resetFilters');
2✔
892
        if (this.options.onFilterChange) {
2✔
893
          this.options.onFilterChange(null, this.state.filterList);
1✔
894
        }
895
      },
896
    );
897
  };
898

899
  filterUpdate = (index, value, column, type) => {
900
    this.setState(
19✔
901
      prevState => {
902
        const filterList = prevState.filterList.slice(0);
19✔
903
        const filterPos = filterList[index].indexOf(value);
19✔
904

905
        switch (type) {
19!
906
          case 'checkbox':
907
            filterPos >= 0 ? filterList[index].splice(filterPos, 1) : filterList[index].push(value);
5!
908
            break;
5✔
909
          case 'multiselect':
910
            filterList[index] = value === '' ? [] : value;
1!
911
            break;
1✔
912
          case 'custom':
913
            filterList[index] = value;
×
914
            break;
×
915
          default:
916
            filterList[index] = filterPos >= 0 || value === '' ? [] : [value];
13✔
917
        }
918

919
        return {
19✔
920
          page: 0,
921
          filterList: filterList,
922
          displayData: this.options.serverSide
19✔
923
            ? prevState.displayData
924
            : this.getDisplayData(prevState.columns, prevState.data, filterList, prevState.searchText),
925
        };
926
      },
927
      () => {
928
        this.setTableAction('filterChange');
19✔
929
        if (this.options.onFilterChange) {
19✔
930
          this.options.onFilterChange(column, this.state.filterList);
4✔
931
        }
932
      },
933
    );
934
  };
935

936
  selectRowDelete = () => {
937
    const { selectedRows, data, filterList } = this.state;
2✔
938

939
    const selectedMap = buildMap(selectedRows.data);
2✔
940
    const cleanRows = data.filter(({ index }) => !selectedMap[index]);
8✔
941

942
    if (this.options.onRowsDelete) {
2✔
943
      if (this.options.onRowsDelete(selectedRows) === false) return;
1!
944
    }
945

946
    this.setTableData(
1✔
947
      {
948
        columns: this.props.columns,
949
        data: cleanRows,
950
        options: {
951
          filterList: filterList,
952
        },
953
      },
954
      TABLE_LOAD.UPDATE,
955
      () => {
956
        this.setTableAction('rowDelete');
1✔
957
      },
958
    );
959
  };
960

961
  toggleExpandRow = row => {
962
    const { dataIndex } = row;
×
963
    let expandedRows = [...this.state.expandedRows.data];
×
964
    let rowPos = -1;
×
965

966
    for (let cIndex = 0; cIndex < expandedRows.length; cIndex++) {
×
967
      if (expandedRows[cIndex].dataIndex === dataIndex) {
×
968
        rowPos = cIndex;
×
969
        break;
×
970
      }
971
    }
972

973
    if (rowPos >= 0) {
×
974
      expandedRows.splice(rowPos, 1);
×
975
    } else {
976
      expandedRows.push(row);
×
977
    }
978

979
    this.setState(
×
980
      {
981
        expandedRows: {
982
          lookup: buildMap(expandedRows),
983
          data: expandedRows,
984
        },
985
      },
986
      () => {
987
        this.setTableAction('expandRow');
×
988
      },
989
    );
990
  };
991

992
  selectRowUpdate = (type, value) => {
993
    // safety check
994
    const { selectableRows } = this.options;
12✔
995
    if (selectableRows === 'none') {
12!
996
      return;
×
997
    }
998

999
    if (type === 'head') {
12✔
1000
      const { isRowSelectable } = this.options;
3✔
1001
      this.setState(
3✔
1002
        prevState => {
1003
          const { displayData } = prevState;
3✔
1004
          const selectedRowsLen = prevState.selectedRows.data.length;
3✔
1005
          const isDeselect =
1006
            selectedRowsLen === displayData.length || (selectedRowsLen < displayData.length && selectedRowsLen > 0)
3!
1007
              ? true
1008
              : false;
1009

1010
          let selectedRows = displayData.reduce((arr, d, i) => {
3✔
1011
            const selected = isRowSelectable ? isRowSelectable(displayData[i].dataIndex) : true;
12!
1012
            selected && arr.push({ index: i, dataIndex: displayData[i].dataIndex });
12✔
1013
            return arr;
12✔
1014
          }, []);
1015

1016
          let newRows = [...prevState.selectedRows, ...selectedRows];
3✔
1017
          let selectedMap = buildMap(newRows);
3✔
1018

1019
          if (isDeselect) {
3!
1020
            newRows = prevState.selectedRows.data.filter(({ dataIndex }) => !selectedMap[dataIndex]);
×
1021
            selectedMap = buildMap(newRows);
×
1022
          }
1023

1024
          return {
3✔
1025
            curSelectedRows: newRows,
1026
            selectedRows: {
1027
              data: newRows,
1028
              lookup: selectedMap,
1029
            },
1030
          };
1031
        },
1032
        () => {
1033
          this.setTableAction('rowsSelect');
3✔
1034
          if (this.options.onRowsSelect) {
3!
1035
            this.options.onRowsSelect(this.state.curSelectedRows, this.state.selectedRows.data);
×
1036
          }
1037
        },
1038
      );
1039
    } else if (type === 'cell') {
9✔
1040
      this.setState(
7✔
1041
        prevState => {
1042
          const { index, dataIndex } = value;
7✔
1043
          let selectedRows = [...prevState.selectedRows.data];
7✔
1044
          let rowPos = -1;
7✔
1045

1046
          for (let cIndex = 0; cIndex < selectedRows.length; cIndex++) {
7✔
1047
            if (selectedRows[cIndex].dataIndex === dataIndex) {
2!
1048
              rowPos = cIndex;
×
1049
              break;
×
1050
            }
1051
          }
1052

1053
          if (rowPos >= 0) {
7!
1054
            selectedRows.splice(rowPos, 1);
×
1055
          } else if (selectableRows === 'single') {
7✔
1056
            selectedRows = [value];
2✔
1057
          } else {
1058
            // multiple
1059
            selectedRows.push(value);
5✔
1060
          }
1061

1062
          return {
7✔
1063
            selectedRows: {
1064
              lookup: buildMap(selectedRows),
1065
              data: selectedRows,
1066
            },
1067
          };
1068
        },
1069
        () => {
1070
          this.setTableAction('rowsSelect');
7✔
1071
          if (this.options.onRowsSelect) {
7!
1072
            this.options.onRowsSelect([value], this.state.selectedRows.data);
×
1073
          }
1074
        },
1075
      );
1076
    } else if (type === 'custom') {
2!
1077
      const { displayData } = this.state;
2✔
1078

1079
      const data = value.map(row => ({ index: row, dataIndex: displayData[row].dataIndex }));
4✔
1080
      const lookup = buildMap(data);
2✔
1081

1082
      this.setState(
2✔
1083
        {
1084
          selectedRows: { data, lookup },
1085
        },
1086
        () => {
1087
          this.setTableAction('rowsSelect');
2✔
1088
          if (this.options.onRowsSelect) {
2!
1089
            this.options.onRowsSelect(this.state.selectedRows.data, this.state.selectedRows.data);
×
1090
          }
1091
        },
1092
      );
1093
    }
1094
  };
1095

1096
  sortTable(data, col, order) {
1097
    let dataSrc = this.options.customSort ? this.options.customSort(data, col, order || 'desc') : data;
3!
1098

1099
    let sortedData = dataSrc.map((row, sIndex) => ({
12✔
1100
      data: row.data[col],
1101
      rowData: row.data,
1102
      position: sIndex,
1103
      rowSelected: this.state.selectedRows.lookup[row.index] ? true : false,
12!
1104
    }));
1105

1106
    if (!this.options.customSort) {
3!
1107
      sortedData.sort(sortCompare(order));
3✔
1108
    }
1109

1110
    let tableData = [];
3✔
1111
    let selectedRows = [];
3✔
1112

1113
    for (let i = 0; i < sortedData.length; i++) {
3✔
1114
      const row = sortedData[i];
12✔
1115
      tableData.push(dataSrc[row.position]);
12✔
1116
      if (row.rowSelected) {
12!
1117
        selectedRows.push({ index: i, dataIndex: dataSrc[row.position].index });
×
1118
      }
1119
    }
1120

1121
    return {
3✔
1122
      data: tableData,
1123
      selectedRows: {
1124
        lookup: buildMap(selectedRows),
1125
        data: selectedRows,
1126
      },
1127
    };
1128
  }
1129

1130
  // must be arrow function on local field to refer to the correct instance when passed around
1131
  // assigning it as arrow function in the JSX would cause hard to track re-render errors
1132
  getTableContentRef = () => {
1133
    return this.tableContent.current;
×
1134
  };
1135

1136
  render() {
1137
    const { classes, className, title } = this.props;
107✔
1138
    const {
1139
      announceText,
1140
      activeColumn,
1141
      data,
1142
      displayData,
1143
      columns,
1144
      page,
1145
      filterData,
1146
      filterList,
1147
      selectedRows,
1148
      expandedRows,
1149
      searchText,
1150
    } = this.state;
107✔
1151

1152
    const rowCount = this.state.count || displayData.length;
107✔
1153
    const rowsPerPage = this.options.pagination ? this.state.rowsPerPage : displayData.length;
107✔
1154
    const showToolbar = hasToolbarItem(this.options, title);
107✔
1155
    const columnNames = columns.map(column => ({ name: column.name, filterType: column.filterType }));
517✔
1156

1157
    return (
107✔
1158
      <Paper
1159
        elevation={this.options.elevation}
1160
        ref={this.tableContent}
1161
        className={classnames(classes.paper, className)}>
1162
        {selectedRows.data.length ? (
107✔
1163
          <TableToolbarSelect
1164
            options={this.options}
1165
            selectedRows={selectedRows}
1166
            onRowsDelete={this.selectRowDelete}
1167
            displayData={displayData}
1168
            selectRowUpdate={this.selectRowUpdate}
1169
          />
1170
        ) : (
1171
          showToolbar && (
187✔
1172
            <TableToolbar
1173
              columns={columns}
1174
              displayData={displayData}
1175
              data={data}
1176
              filterData={filterData}
1177
              filterList={filterList}
1178
              filterUpdate={this.filterUpdate}
1179
              options={this.options}
1180
              resetFilters={this.resetFilters}
1181
              searchText={searchText}
1182
              searchTextUpdate={this.searchTextUpdate}
1183
              tableRef={this.getTableContentRef}
1184
              title={title}
1185
              toggleViewColumn={this.toggleViewColumn}
1186
              setTableAction={this.setTableAction}
1187
            />
1188
          )
1189
        )}
1190
        <TableFilterList
1191
          options={this.options}
1192
          filterListRenderers={columns.map(c => {
1193
            return c.customFilterListRender ? c.customFilterListRender : f => f;
517✔
1194
          })}
1195
          filterList={filterList}
1196
          filterUpdate={this.filterUpdate}
1197
          columnNames={columnNames}
1198
        />
1199
        <div
1200
          style={{ position: 'relative' }}
1201
          className={this.options.responsive === 'scroll' ? classes.responsiveScroll : classes.responsiveStacked}>
107✔
1202
          {this.options.resizableColumns && (
107!
1203
            <TableResize
1204
              key={rowCount}
1205
              updateDividers={fn => (this.updateDividers = fn)}
×
1206
              setResizeable={fn => (this.setHeadResizeable = fn)}
×
1207
            />
1208
          )}
1209
          <MuiTable ref={el => (this.tableRef = el)} tabIndex={'0'} role={'grid'} className={classes.tableRoot}>
27✔
1210
            <caption className={classes.caption}>{title}</caption>
1211
            <TableHead
1212
              columns={columns}
1213
              activeColumn={activeColumn}
1214
              data={displayData}
1215
              count={rowCount}
1216
              columns={columns}
1217
              page={page}
1218
              rowsPerPage={rowsPerPage}
1219
              handleHeadUpdateRef={fn => (this.updateToolbarSelect = fn)}
7✔
1220
              selectedRows={selectedRows}
1221
              selectRowUpdate={this.selectRowUpdate}
1222
              toggleSort={this.toggleSortColumn}
1223
              setCellRef={this.setHeadCellRef}
1224
              options={this.options}
1225
            />
1226
            <TableBody
1227
              data={displayData}
1228
              count={rowCount}
1229
              columns={columns}
1230
              page={page}
1231
              rowsPerPage={rowsPerPage}
1232
              selectedRows={selectedRows}
1233
              selectRowUpdate={this.selectRowUpdate}
1234
              expandedRows={expandedRows}
1235
              toggleExpandRow={this.toggleExpandRow}
1236
              options={this.options}
1237
              filterList={filterList}
1238
            />
1239
          </MuiTable>
1240
        </div>
1241
        <TableFooter
1242
          options={this.options}
1243
          page={page}
1244
          rowCount={rowCount}
1245
          rowsPerPageOptions={this.options.rowsPerPageOptions}
1246
          rowsPerPage={rowsPerPage}
1247
          changeRowsPerPage={this.changeRowsPerPage}
1248
          changePage={this.changePage}
1249
        />
1250
        <div className={classes.liveAnnounce} aria-live={'polite'} ref={el => (this.announceRef = el)}>
27✔
1251
          {announceText}
1252
        </div>
1253
      </Paper>
1254
    );
1255
  }
1256
}
1257

1258
export default withStyles(defaultTableStyles, { name: 'MUIDataTable' })(MUIDataTable);
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc