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

gregnb / mui-datatables / #1869

17 Dec 2018 07:23PM UTC coverage: 78.18% (+0.06%) from 78.118%
#1869

push

gregnb
Fix issue with sort not taking into account on re-render (#316)

271 of 377 branches covered (71.88%)

Branch coverage included in aggregate %.

5 of 9 new or added lines in 1 file covered. (55.56%)

42 existing lines in 3 files now uncovered.

442 of 535 relevant lines covered (82.62%)

42.95 hits per line

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

75.94
/src/MUIDataTable.js
1
import React from "react";
2
import PropTypes from "prop-types";
3
import Paper from "@material-ui/core/Paper";
4
import Table from "@material-ui/core/Table";
5
import MUIDataTableToolbar from "./MUIDataTableToolbar";
6
import MUIDataTableToolbarSelect from "./MUIDataTableToolbarSelect";
7
import MUIDataTableFilterList from "./MUIDataTableFilterList";
8
import MUIDataTableBody from "./MUIDataTableBody";
9
import MUIDataTableResize from "./MUIDataTableResize";
10
import MUIDataTableHead from "./MUIDataTableHead";
11
import MUIDataTablePagination from "./MUIDataTablePagination";
12
import cloneDeep from "lodash.clonedeep";
13
import merge from "lodash.merge";
14
import isEqual from "lodash.isequal";
15
import textLabels from "./textLabels";
16
import { withStyles } from "@material-ui/core/styles";
17

18
const defaultTableStyles = {
1✔
19
  root: {},
20
  responsiveScroll: {
21
    overflowX: "auto",
22
  },
23
  caption: {
24
    position: "absolute",
25
    left: "-3000px",
26
  },
27
  liveAnnounce: {
28
    border: "0",
29
    clip: "rect(0 0 0 0)",
30
    height: "1px",
31
    margin: "-1px",
32
    overflow: "hidden",
33
    padding: "0",
34
    position: "absolute",
35
    width: "1px",
36
  },
37
};
38

39
const TABLE_LOAD = {
1✔
40
  INITIAL: 1,
41
  UPDATE: 2,
42
};
43

44
class MUIDataTable extends React.Component {
45
  static propTypes = {
46
    /** Title of the table */
47
    title: PropTypes.string.isRequired,
48
    /** Data used to describe table */
49
    data: PropTypes.array.isRequired,
50
    /** Columns used to describe table */
51
    columns: PropTypes.PropTypes.arrayOf(
52
      PropTypes.oneOfType([
53
        PropTypes.string,
54
        PropTypes.shape({
55
          name: PropTypes.string.isRequired,
56
          options: PropTypes.shape({
57
            display: PropTypes.string, // enum('true', 'false', 'excluded')
58
            filter: PropTypes.bool,
59
            sort: PropTypes.bool,
60
            download: PropTypes.bool,
61
            customHeadRender: PropTypes.func,
62
            customBodyRender: PropTypes.func,
63
          }),
64
        }),
65
      ]),
66
    ).isRequired,
67
    /** Options used to describe table */
68
    options: PropTypes.shape({
69
      responsive: PropTypes.oneOf(["stacked", "scroll"]),
70
      filterType: PropTypes.oneOf(["dropdown", "checkbox", "multiselect"]),
71
      textLabels: PropTypes.object,
72
      pagination: PropTypes.bool,
73
      customToolbar: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
74
      customToolbarSelect: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
75
      customFooter: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
76
      onRowClick: PropTypes.func,
77
      resizableColumns: PropTypes.bool,
78
      selectableRows: PropTypes.bool,
79
      serverSide: PropTypes.bool,
80
      onTableChange: PropTypes.func,
81
      caseSensitive: PropTypes.bool,
82
      rowHover: PropTypes.bool,
83
      fixedHeader: PropTypes.bool,
84
      page: PropTypes.number,
85
      count: PropTypes.number,
86
      filterList: PropTypes.array,
87
      rowsSelected: PropTypes.array,
88
      rowsPerPage: PropTypes.number,
89
      rowsPerPageOptions: PropTypes.array,
90
      filter: PropTypes.bool,
91
      sort: PropTypes.bool,
92
      customSort: PropTypes.func,
93
      search: PropTypes.bool,
94
      print: PropTypes.bool,
95
      viewColumns: PropTypes.bool,
96
      download: PropTypes.bool,
97
      downloadOptions: PropTypes.shape({
98
        filename: PropTypes.string,
99
        separator: PropTypes.string,
100
      }),
101
    }),
102
    /** Pass and use className to style MUIDataTable as desired */
103
    className: PropTypes.string,
104
  };
105

106
  static defaultProps = {
107
    title: "",
108
    options: {},
109
    data: [],
110
    columns: [],
111
  };
112

113
  state = {
114
    announceText: null,
115
    activeColumn: null,
116
    data: [],
117
    displayData: [],
118
    page: 0,
119
    rowsPerPage: 0,
120
    columns: [],
121
    filterData: [],
122
    filterList: [],
123
    selectedRows: {
124
      data: [],
125
      lookup: {},
126
    },
127
    showResponsive: false,
128
    searchText: null,
129
  };
130

131
  constructor() {
132
    super();
133
    this.tableRef = false;
31✔
134
    this.tableContent = React.createRef();
31✔
135
    this.headCellRefs = {};
31✔
136
    this.setHeadResizeable = () => {};
31✔
137
  }
31✔
138

139
  componentWillMount() {
140
    this.initializeTable(this.props);
141
  }
31✔
142

143
  componentDidMount() {
144
    this.setHeadResizeable(this.headCellRefs, this.tableRef);
145
  }
31✔
146

147
  componentWillReceiveProps(nextProps) {
148
    if (this.props.data !== nextProps.data || this.props.columns !== nextProps.columns) {
149
      this.initializeTable(nextProps);
1!
UNCOV
150
    }
×
151
  }
152

153
  initializeTable(props) {
154
    this.getDefaultOptions(props);
155
    this.setTableOptions(props);
31✔
156
    this.setTableData(props, TABLE_LOAD.INITIAL);
31✔
157
  }
31✔
158

159
  static fallbackComparator = (a, b) => a.localeCompare(b);
160

2✔
161
  static getCollatzComparator = () => {
162
    if (!!Intl) {
163
      const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
127✔
164
      return collator.compare;
126✔
165
    }
126✔
166

167
    return MUIDataTable.fallbackComparator;
168
  };
1✔
169

170
  /*
171
   * React currently does not support deep merge for defaultProps. Objects are overwritten
172
   */
173
  getDefaultOptions(props) {
174
    const defaultOptions = {
175
      responsive: "stacked",
31✔
176
      filterType: "checkbox",
177
      pagination: true,
178
      textLabels,
179
      resizableColumns: false,
180
      selectableRows: true,
181
      caseSensitive: false,
182
      serverSide: false,
183
      rowHover: true,
184
      fixedHeader: true,
185
      rowsPerPage: 10,
186
      rowsPerPageOptions: [10, 15, 100],
187
      filter: true,
188
      sortFilterList: true,
189
      sort: true,
190
      search: true,
191
      print: true,
192
      viewColumns: true,
193
      download: true,
194
      downloadOptions: {
195
        filename: "tableDownload.csv",
196
        separator: ",",
197
      },
198
    };
199

200
    this.options = merge(defaultOptions, props.options);
201
  }
31✔
202

203
  validateOptions(options) {
204
    if (options.serverSide && options.onTableChange === undefined) {
205
      throw Error("onTableChange callback must be provided when using serverSide option");
31!
UNCOV
206
    }
×
207
  }
208

209
  setTableAction = action => {
210
    if (typeof this.options.onTableChange === "function") {
211
      this.options.onTableChange(action, this.state);
21✔
212
    }
3✔
213
  };
214

215
  setTableOptions(props) {
216
    const optionNames = ["rowsPerPage", "page", "rowsSelected", "filterList", "rowsPerPageOptions"];
217
    const optState = optionNames.reduce((acc, cur) => {
31✔
218
      if (this.options[cur] !== undefined) {
31✔
219
        acc[cur] = this.options[cur];
155✔
220
      }
62✔
221
      return acc;
222
    }, {});
155✔
223

224
    this.validateOptions(optState);
225
    this.setState(optState);
31✔
226
  }
31✔
227

228
  setHeadCellRef = (index, el) => {
229
    this.headCellRefs[index] = el;
230
  };
50✔
231

232
  rawColumns = cols => {
233
    return cols.map(item => {
UNCOV
234
      if (typeof item !== "object") return item;
×
UNCOV
235

×
236
      const { options, ...otherOpts } = item;
UNCOV
237
      return otherOpts;
×
UNCOV
238
    });
×
239
  };
240

241
  /*
242
   *  Build the source table data
243
   */
244

245
  buildColumns = newColumns => {
246
    let columnData = [];
247
    let filterData = [];
31✔
248
    let filterList = [];
31✔
249

31✔
250
    if (this.state.columns.length && isEqual(this.rawColumns(newColumns), this.rawColumns(this.props.columns))) {
251
      const { columns, filterList, filterData } = this.state;
31!
UNCOV
252
      return { columns, filterList, filterData };
×
UNCOV
253
    }
×
254

255
    newColumns.forEach((column, colIndex) => {
256
      let columnOptions = {
31✔
257
        display: "true",
124✔
258
        filter: true,
259
        sort: true,
260
        download: true,
261
        sortDirection: null,
262
      };
263

264
      if (typeof column === "object") {
265
        if (column.options && column.options.display !== undefined) {
124✔
266
          column.options.display = column.options.display.toString();
93!
UNCOV
267
        }
×
268

269
        columnOptions = {
270
          name: column.name,
93✔
271
          ...columnOptions,
272
          ...(column.options ? column.options : {}),
273
        };
93✔
274
      } else {
275
        columnOptions = { ...columnOptions, name: column };
276
      }
31✔
277

278
      columnData.push(columnOptions);
279

124✔
280
      filterData[colIndex] = [];
281
      filterList[colIndex] = [];
124✔
282
    });
124✔
283

284
    return { columns: columnData, filterData, filterList };
285
  };
31✔
286

287
  setTableData(props, status, callback = () => {}) {
288
    const { data, options } = props;
31✔
289

31✔
290
    let tableData = [];
291
    let { columns, filterData, filterList } = this.buildColumns(props.columns);
31✔
292
    let sortIndex = null;
31✔
293
    let sortDirection = null;
31✔
294

31✔
295
    columns.forEach((column, colIndex) => {
296
      for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
31✔
297
        let value = status === TABLE_LOAD.INITIAL ? data[rowIndex][colIndex] : data[rowIndex].data[colIndex];
124✔
298

596!
299
        if (typeof tableData[rowIndex] === "undefined") {
300
          tableData.push({
596✔
301
            index: status === TABLE_LOAD.INITIAL ? rowIndex : data[rowIndex].index,
149✔
302
            data: status === TABLE_LOAD.INITIAL ? data[rowIndex] : data[rowIndex].data,
149!
303
          });
149!
304
        }
305

306
        if (typeof column.customBodyRender === "function") {
307
          const tableMeta = this.getTableMeta(rowIndex, colIndex, value, [], column, this.state);
596✔
308
          const funcResult = column.customBodyRender(value, tableMeta);
298✔
309

298✔
310
          if (React.isValidElement(funcResult) && funcResult.props.value) {
311
            value = funcResult.props.value;
298✔
312
          } else if (typeof funcResult === "string") {
149✔
313
            value = funcResult;
149!
314
          }
149✔
315
        }
316

317
        if (filterData[colIndex].indexOf(value) < 0) filterData[colIndex].push(value);
318
      }
596✔
319

320
      if (this.options.sortFilterList) {
321
        const comparator = MUIDataTable.getCollatzComparator();
124!
322
        filterData[colIndex].sort(comparator);
124✔
323
      }
124✔
324

325
      if (column.sortDirection !== null) {
326
        sortIndex = colIndex;
124!
NEW
327
        sortDirection = column.sortDirection === "asc" ? "desc" : "asc";
×
NEW
328
      }
×
329
    });
330

331
    if (options.filterList) filterList = options.filterList;
332

31!
333
    if (filterList.length !== columns.length) {
334
      throw new Error("Provided options.filterList does not match the column length");
31!
UNCOV
335
    }
×
336

337
    let selectedRowsData = {
338
      data: [],
31✔
339
      lookup: {},
340
    };
341

342
    if (TABLE_LOAD.INITIAL) {
343
      if (options.rowsSelected && options.rowsSelected.length) {
31!
344
        options.rowsSelected.forEach(row => {
31!
UNCOV
345
          selectedRowsData.data.push({ index: row, dataIndex: row });
×
UNCOV
346
          selectedRowsData.lookup[row] = true;
×
UNCOV
347
        });
×
348
      }
349
    }
350

351
    if (sortIndex !== null) {
352
      const sortedData = this.sortTable(tableData, sortIndex, sortDirection);
31!
NEW
353
      tableData = sortedData.data;
×
NEW
354
    }
×
355

356
    /* set source data and display Data set source set */
357
    this.setState(
358
      prevState => ({
31✔
359
        columns: columns,
31✔
360
        filterData: filterData,
361
        filterList: filterList,
362
        selectedRows: selectedRowsData,
363
        data: tableData,
364
        displayData: this.getDisplayData(columns, tableData, filterList, prevState.searchText),
365
      }),
366
      callback,
367
    );
368
  }
369

370
  /*
371
   *  Build the table data used to display to the user (ie: after filter/search applied)
372
   */
373
  computeDisplayRow(columns, row, rowIndex, filterList, searchText) {
374
    let isFiltered = false;
375
    let isSearchFound = false;
197✔
376
    let displayRow = [];
197✔
377

197✔
378
    for (let index = 0; index < row.length; index++) {
379
      let columnDisplay = row[index];
197✔
380
      let columnValue = row[index];
788✔
381

788✔
382
      if (columns[index].customBodyRender) {
383
        const tableMeta = this.getTableMeta(rowIndex, index, row, columns[index], this.state.data, {
788✔
384
          ...this.state,
386✔
385
          filterList: filterList,
386
          searchText: searchText,
387
        });
388

389
        const funcResult = columns[index].customBodyRender(
390
          columnValue,
386✔
391
          tableMeta,
392
          this.updateDataCol.bind(null, rowIndex, index),
393
        );
394
        columnDisplay = funcResult;
395

386✔
396
        /* drill down to get the value of a cell */
397
        columnValue =
398
          typeof funcResult === "string"
386✔
399
            ? funcResult
386✔
400
            : funcResult.props && funcResult.props.value
401
            ? funcResult.props.value
579!
402
            : columnValue;
403
      }
404

405
      displayRow.push(columnDisplay);
406

788✔
407
      if (filterList[index].length && filterList[index].indexOf(columnValue) < 0) {
408
        isFiltered = true;
788✔
409
      }
20✔
410

411
      const columnVal = columnValue === null ? "" : columnValue.toString();
412

788!
413
      if (searchText) {
414
        let searchNeedle = searchText.toString();
788✔
415
        let searchStack = columnVal.toString();
16✔
416

16✔
417
        if (!this.options.caseSensitive) {
418
          searchNeedle = searchNeedle.toLowerCase();
16!
419
          searchStack = searchStack.toLowerCase();
16✔
420
        }
16✔
421

422
        if (searchStack.indexOf(searchNeedle) >= 0) {
423
          isSearchFound = true;
16✔
424
        }
1✔
425
      }
426
    }
427

428
    if (isFiltered || (!this.options.serverSide && searchText && !isSearchFound)) return null;
429
    else return displayRow;
197✔
430
  }
174✔
431

432
  updateDataCol = (row, index, value) => {
433
    this.setState(prevState => {
434
      let changedData = cloneDeep(prevState.data);
1✔
435
      let filterData = cloneDeep(prevState.filterData);
1✔
436

1✔
437
      const tableMeta = this.getTableMeta(row, index, row, prevState.columns[index], prevState.data, prevState);
438
      const funcResult = prevState.columns[index].customBodyRender(value, tableMeta);
1✔
439

1✔
440
      const filterValue =
441
        React.isValidElement(funcResult) && funcResult.props.value
442
          ? funcResult.props.value
1!
443
          : prevState["data"][row][index];
444

445
      const prevFilterIndex = filterData[index].indexOf(filterValue);
446
      filterData[index].splice(prevFilterIndex, 1, filterValue);
1✔
447

1✔
448
      changedData[row].data[index] = value;
449

1✔
450
      if (this.options.sortFilterList) {
451
        const comparator = MUIDataTable.getCollatzComparator();
1!
452
        filterData[index].sort(comparator);
1✔
453
      }
1✔
454

455
      return {
456
        data: changedData,
1✔
457
        filterData: filterData,
458
        displayData: this.getDisplayData(prevState.columns, changedData, prevState.filterList, prevState.searchText),
459
      };
460
    });
461
  };
462

463
  getTableMeta = (rowIndex, colIndex, rowData, columnData, tableData, curState) => {
464
    const { columns, data, displayData, filterData, ...tableState } = curState;
465

685✔
466
    return {
467
      rowIndex: rowIndex,
685✔
468
      columnIndex: colIndex,
469
      columnData: columnData,
470
      rowData: rowData,
471
      tableData: tableData,
472
      tableState: tableState,
473
    };
474
  };
475

476
  getDisplayData(columns, data, filterList, searchText) {
477
    let newRows = [];
478

43✔
479
    for (let index = 0; index < data.length; index++) {
480
      const value = data[index].data;
43✔
481
      const displayRow = this.computeDisplayRow(columns, value, index, filterList, searchText);
197✔
482

197✔
483
      if (displayRow) {
484
        newRows.push({
197✔
485
          data: displayRow,
174✔
486
          dataIndex: data[index].index,
487
        });
488
      }
489
    }
490

491
    return newRows;
492
  }
43✔
493

494
  toggleViewColumn = index => {
495
    this.setState(
496
      prevState => {
1✔
497
        const columns = cloneDeep(prevState.columns);
498
        columns[index].display = columns[index].display === "true" ? "false" : "true";
1✔
499
        return {
1!
500
          columns: columns,
1✔
501
        };
502
      },
503
      () => {
504
        this.setTableAction("columnViewChange");
505
        if (this.options.onColumnViewChange) {
1✔
506
          this.options.onColumnViewChange(
1!
UNCOV
507
            this.state.columns[index].name,
×
508
            this.state.columns[index].display === "true" ? "add" : "remove",
509
          );
×
510
        }
511
      },
512
    );
513
  };
514

515
  getSortDirection(column) {
516
    return column.sortDirection === "asc" ? "ascending" : "descending";
517
  }
1!
518

519
  toggleSortColumn = index => {
520
    this.setState(
521
      prevState => {
1✔
522
        let columns = cloneDeep(prevState.columns);
523
        let data = prevState.data;
1✔
524
        const order = prevState.columns[index].sortDirection;
1✔
525

1✔
526
        for (let pos = 0; pos < columns.length; pos++) {
527
          if (index !== pos) {
1✔
528
            columns[pos].sortDirection = null;
4✔
529
          } else {
3✔
530
            columns[pos].sortDirection = columns[pos].sortDirection === "asc" ? "desc" : "asc";
531
          }
1!
532
        }
533

534
        const orderLabel = this.getSortDirection(columns[index]);
535
        const announceText = `Table now sorted by ${columns[index].name} : ${orderLabel}`;
1✔
536

1✔
537
        let newState = {
538
          columns: columns,
1✔
539
          announceText: announceText,
540
          activeColumn: index,
541
        };
542

543
        if (this.options.serverSide) {
544
          newState = {
1!
545
            ...newState,
×
546
            data: prevState.data,
547
            displayData: prevState.displayData,
548
            selectedRows: prevState.selectedRows,
549
          };
550
        } else {
551
          const sortedData = this.sortTable(data, index, order);
552

1✔
553
          newState = {
554
            ...newState,
1✔
555
            data: sortedData.data,
556
            displayData: this.getDisplayData(columns, sortedData.data, prevState.filterList, prevState.searchText),
557
            selectedRows: sortedData.selectedRows,
558
          };
559
        }
560

561
        return newState;
562
      },
1✔
563
      () => {
564
        this.setTableAction("sort");
565
        if (this.options.onColumnSortChange) {
1✔
566
          this.options.onColumnSortChange(
1!
UNCOV
567
            this.state.columns[index].name,
×
568
            this.getSortDirection(this.state.columns[index]),
569
          );
570
        }
571
      },
572
    );
573
  };
574

575
  changeRowsPerPage = rows => {
576
    /**
577
     * After changing rows per page recalculate totalPages and checks its if current page not higher.
578
     * Otherwise sets current page the value of nextTotalPages
579
     */
580
    const rowCount = this.options.count || this.state.displayData.length;
581
    const nextTotalPages = Math.floor(rowCount / rows);
2✔
582

2✔
583
    this.setState(
584
      () => ({
2✔
585
        rowsPerPage: rows,
2✔
586
        page: this.state.page > nextTotalPages ? nextTotalPages : this.state.page,
587
      }),
2✔
588
      () => {
589
        this.setTableAction("changeRowsPerPage");
590
        if (this.options.onChangeRowsPerPage) {
2✔
591
          this.options.onChangeRowsPerPage(this.state.rowsPerPage);
2!
UNCOV
592
        }
×
593
      },
594
    );
595
  };
596

597
  changePage = page => {
598
    this.setState(
599
      () => ({
2✔
600
        page: page,
2✔
601
      }),
602
      () => {
603
        this.setTableAction("changePage");
604
        if (this.options.onChangePage) {
2✔
605
          this.options.onChangePage(this.state.page);
2!
UNCOV
606
        }
×
607
      },
608
    );
609
  };
610

611
  searchTextUpdate = text => {
612
    this.setState(
613
      prevState => ({
1✔
614
        searchText: text && text.length ? text : null,
1✔
615
        displayData: this.options.serverSide
3!
616
          ? prevState.displayData
1!
617
          : this.getDisplayData(prevState.columns, prevState.data, prevState.filterList, text),
618
      }),
619
      () => {
620
        this.setTableAction("search");
621
      },
1✔
622
    );
623
  };
624

625
  resetFilters = () => {
626
    this.setState(
627
      prevState => {
1✔
628
        const filterList = prevState.columns.map((column, index) => []);
629

4✔
630
        return {
631
          filterList: filterList,
1✔
632
          displayData: this.options.serverSide
633
            ? prevState.displayData
1!
634
            : this.getDisplayData(prevState.columns, prevState.data, filterList, prevState.searchText),
635
        };
636
      },
637
      () => {
638
        this.setTableAction("resetFilters");
639
        if (this.options.onFilterChange) {
1✔
640
          this.options.onFilterChange(null, this.state.filterList);
1!
UNCOV
641
        }
×
642
      },
643
    );
644
  };
645

646
  filterUpdate = (index, column, type) => {
647
    this.setState(
648
      prevState => {
7✔
649
        const filterList = cloneDeep(prevState.filterList);
650
        const filterPos = filterList[index].indexOf(column);
7✔
651

7✔
652
        switch (type) {
653
          case "checkbox":
7!
654
            filterPos >= 0 ? filterList[index].splice(filterPos, 1) : filterList[index].push(column);
655
            break;
4✔
656
          case "multiselect":
4✔
657
            filterList[index] = column === "" ? [] : column;
658
            break;
×
UNCOV
659
          default:
×
660
            filterList[index] = filterPos >= 0 || column === "" ? [] : [column];
661
        }
3✔
662

663
        return {
664
          filterList: filterList,
7✔
665
          displayData: this.options.serverSide
666
            ? prevState.displayData
7!
667
            : this.getDisplayData(prevState.columns, prevState.data, filterList, prevState.searchText),
668
        };
669
      },
670
      () => {
671
        this.setTableAction("filterChange");
672
        if (this.options.onFilterChange) {
7✔
673
          this.options.onFilterChange(column, this.state.filterList);
7!
UNCOV
674
        }
×
675
      },
676
    );
677
  };
678

679
  selectRowDelete = () => {
680
    const { selectedRows, data, filterList } = this.state;
UNCOV
681

×
682
    const selectedMap = this.buildSelectedMap(selectedRows.data);
UNCOV
683
    const cleanRows = data.filter(({ index }) => !selectedMap[index]);
×
UNCOV
684

×
685
    if (this.options.onRowsDelete) {
UNCOV
686
      this.options.onRowsDelete(selectedRows);
×
UNCOV
687
    }
×
688

689
    this.setTableData(
UNCOV
690
      {
×
691
        columns: this.props.columns,
692
        data: cleanRows,
693
        options: {
694
          filterList: filterList,
695
        },
696
      },
697
      TABLE_LOAD.UPDATE,
698
      () => {
699
        this.setTableAction("rowDelete");
UNCOV
700
      },
×
701
    );
702
  };
703

704
  buildSelectedMap = rows => {
705
    return rows.reduce((accum, { dataIndex }) => {
706
      accum[dataIndex] = true;
7✔
707
      return accum;
14✔
708
    }, {});
14✔
709
  };
710

711
  selectRowUpdate = (type, value) => {
712
    if (type === "head") {
713
      this.setState(
6✔
714
        prevState => {
2✔
715
          const { displayData } = prevState;
716
          const selectedRowsLen = prevState.selectedRows.data.length;
2✔
717
          const isDeselect =
2✔
718
            selectedRowsLen === displayData.length || (selectedRowsLen < displayData.length && selectedRowsLen > 0)
719
              ? true
2!
720
              : false;
721

722
          let selectedRows = Array(displayData.length)
723
            .fill()
2✔
724
            .map((d, i) => ({ index: i, dataIndex: displayData[i].dataIndex }));
725

8✔
726
          let newRows = [...prevState.selectedRows, ...selectedRows];
727
          let selectedMap = this.buildSelectedMap(newRows);
2✔
728

2✔
729
          if (isDeselect) {
730
            newRows = prevState.selectedRows.data.filter(({ dataIndex }) => !selectedMap[dataIndex]);
2!
UNCOV
731
            selectedMap = this.buildSelectedMap(newRows);
×
UNCOV
732
          }
×
733

734
          return {
735
            curSelectedRows: newRows,
2✔
736
            selectedRows: {
737
              data: newRows,
738
              lookup: selectedMap,
739
            },
740
          };
741
        },
742
        () => {
743
          this.setTableAction("rowsSelect");
744
          if (this.options.onRowsSelect) {
2✔
745
            this.options.onRowsSelect(this.state.curSelectedRows, this.state.selectedRows.data);
2!
UNCOV
746
          }
×
747
        },
748
      );
749
    } else if (type === "cell") {
750
      this.setState(
4✔
751
        prevState => {
2✔
752
          const { index, dataIndex } = value;
753
          let selectedRows = [...prevState.selectedRows.data];
2✔
754
          let rowPos = -1;
2✔
755

2✔
756
          for (let cIndex = 0; cIndex < selectedRows.length; cIndex++) {
757
            if (selectedRows[cIndex].index === index) {
2✔
UNCOV
758
              rowPos = cIndex;
×
UNCOV
759
              break;
×
UNCOV
760
            }
×
761
          }
762

763
          if (rowPos >= 0) {
764
            selectedRows.splice(rowPos, 1);
2!
UNCOV
765
          } else {
×
766
            selectedRows.push(value);
767
          }
2✔
768

769
          return {
770
            selectedRows: {
2✔
771
              lookup: this.buildSelectedMap(selectedRows),
772
              data: selectedRows,
773
            },
774
          };
775
        },
776
        () => {
777
          this.setTableAction("rowsSelect");
778
          if (this.options.onRowsSelect) {
2✔
779
            this.options.onRowsSelect([value], this.state.selectedRows.data);
2!
UNCOV
780
          }
×
781
        },
782
      );
783
    } else if (type === "custom") {
784
      const { displayData } = this.state;
2!
785

2✔
786
      const data = value.map(row => ({ index: row, dataIndex: displayData[row].dataIndex }));
787
      const lookup = this.buildSelectedMap(data);
4✔
788

2✔
789
      this.setState(
790
        {
2✔
791
          selectedRows: { data, lookup },
792
        },
793
        () => {
794
          this.setTableAction("rowsSelect");
795
          if (this.options.onRowsSelect) {
2✔
796
            this.options.onRowsSelect(this.state.selectedRows.data, this.state.selectedRows.data);
2!
UNCOV
797
          }
×
798
        },
799
      );
800
    }
801
  };
802

803
  sortCompare(order) {
804
    return (a, b) => {
805
      if (a.data === null) a.data = "";
1✔
806
      if (b.data === null) b.data = "";
6!
807
      return (
6!
808
        (typeof a.data.localeCompare === "function" ? a.data.localeCompare(b.data) : a.data - b.data) *
6✔
809
        (order === "asc" ? -1 : 1)
6!
810
      );
6!
811
    };
812
  }
813

814
  sortTable(data, col, order) {
815
    let dataSrc = this.options.customSort ? this.options.customSort(data, col, order || "desc") : data;
816

1!
817
    let sortedData = dataSrc.map((row, sIndex) => ({
818
      data: row.data[col],
4✔
819
      position: sIndex,
820
      rowSelected: this.state.selectedRows.lookup[sIndex] ? true : false,
821
    }));
4!
822

823
    if (!this.options.customSort) {
824
      sortedData.sort(this.sortCompare(order));
1!
825
    }
1✔
826

827
    let tableData = [];
828
    let selectedRows = [];
1✔
829

1✔
830
    for (let i = 0; i < sortedData.length; i++) {
831
      const row = sortedData[i];
1✔
832
      tableData.push(data[row.position]);
4✔
833
      if (row.rowSelected) {
4✔
834
        selectedRows.push({ index: i, dataIndex: data[row.position].index });
4!
UNCOV
835
      }
×
836
    }
837

838
    return {
839
      data: tableData,
1✔
840
      selectedRows: {
841
        lookup: this.buildSelectedMap(selectedRows),
842
        data: selectedRows,
843
      },
844
    };
845
  }
846

847
  // must be arrow function on local field to refer to the correct instance when passed around
848
  // assigning it as arrow function in the JSX would cause hard to track re-render errors
849
  getTableContentRef = () => {
850
    return this.tableContent.current;
UNCOV
851
  };
×
852

853
  render() {
854
    const { classes, title } = this.props;
855
    const {
54✔
856
      announceText,
857
      activeColumn,
858
      data,
859
      displayData,
860
      columns,
861
      page,
862
      filterData,
863
      filterList,
864
      rowsPerPage,
865
      selectedRows,
866
      searchText,
867
    } = this.state;
868

54✔
869
    const rowCount = this.options.count || displayData.length;
870

54✔
871
    return (
872
      <Paper elevation={4} ref={this.tableContent} className={classes.paper}>
54✔
873
        {selectedRows.data.length ? (
874
          <MUIDataTableToolbarSelect
54✔
875
            options={this.options}
876
            selectedRows={selectedRows}
877
            onRowsDelete={this.selectRowDelete}
878
            displayData={displayData}
879
            selectRowUpdate={this.selectRowUpdate}
880
          />
881
        ) : (
882
          <MUIDataTableToolbar
883
            columns={columns}
884
            displayData={displayData}
885
            data={data}
886
            filterData={filterData}
887
            filterList={filterList}
888
            filterUpdate={this.filterUpdate}
889
            options={this.options}
890
            resetFilters={this.resetFilters}
891
            searchTextUpdate={this.searchTextUpdate}
892
            tableRef={this.getTableContentRef}
893
            title={title}
894
            toggleViewColumn={this.toggleViewColumn}
895
            setTableAction={this.setTableAction}
896
          />
897
        )}
898
        <MUIDataTableFilterList options={this.options} filterList={filterList} filterUpdate={this.filterUpdate} />
899
        <div
900
          style={{ position: "relative" }}
901
          className={this.options.responsive === "scroll" ? classes.responsiveScroll : null}>
902
          {this.options.resizableColumns && (
54!
903
            <MUIDataTableResize key={rowCount} setResizeable={fn => (this.setHeadResizeable = fn)} />
54!
UNCOV
904
          )}
×
905
          <Table ref={el => (this.tableRef = el)} tabIndex={"0"} role={"grid"}>
906
            <caption className={classes.caption}>{title}</caption>
10✔
907
            <MUIDataTableHead
908
              columns={columns}
909
              activeColumn={activeColumn}
910
              data={displayData}
911
              count={rowCount}
912
              columns={columns}
913
              page={page}
914
              rowsPerPage={rowsPerPage}
915
              handleHeadUpdateRef={fn => (this.updateToolbarSelect = fn)}
916
              selectedRows={selectedRows}
4✔
917
              selectRowUpdate={this.selectRowUpdate}
918
              toggleSort={this.toggleSortColumn}
919
              setCellRef={this.setHeadCellRef}
920
              options={this.options}
921
            />
922
            <MUIDataTableBody
923
              data={displayData}
924
              count={rowCount}
925
              columns={columns}
926
              page={page}
927
              rowsPerPage={rowsPerPage}
928
              selectedRows={selectedRows}
929
              selectRowUpdate={this.selectRowUpdate}
930
              options={this.options}
931
              searchText={searchText}
932
              filterList={filterList}
933
            />
934
          </Table>
935
        </div>
936
        <Table>
937
          {this.options.customFooter
938
            ? this.options.customFooter(rowCount, page, rowsPerPage, this.changeRowsPerPage, this.changePage)
54!
939
            : this.options.pagination && (
940
                <MUIDataTablePagination
107✔
941
                  count={rowCount}
942
                  page={page}
943
                  rowsPerPage={rowsPerPage}
944
                  changeRowsPerPage={this.changeRowsPerPage}
945
                  changePage={this.changePage}
946
                  component={"div"}
947
                  options={this.options}
948
                />
949
              )}
950
        </Table>
951
        <div className={classes.liveAnnounce} aria-live={"polite"} ref={el => (this.announceRef = el)}>
952
          {announceText}
10✔
953
        </div>
954
      </Paper>
955
    );
956
  }
957
}
958

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

© 2026 Coveralls, Inc