mui-table-2026 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,630 @@
1
+ import React, { useState } from 'react';
2
+ import { Typography } from '@mui/material';
3
+ import { Toolbar } from '@mui/material';
4
+ import { IconButton } from '@mui/material';
5
+ import { Menu } from '@mui/material';
6
+ import { MenuItem } from '@mui/material';
7
+ import { ListItemIcon } from '@mui/material';
8
+ import { ListItemText } from '@mui/material';
9
+ import Popover from './Popover';
10
+ import TableFilter from './TableFilter';
11
+ import TableViewCol from './TableViewCol';
12
+ import TableSearch from './TableSearch';
13
+ import { Search as SearchIcon } from '@mui/icons-material';
14
+ import DownloadIcon from '@mui/icons-material/CloudDownload';
15
+ import { Print as PrintIcon } from '@mui/icons-material';
16
+ import { ViewColumn as ViewColumnIcon } from '@mui/icons-material';
17
+ import FilterIcon from '@mui/icons-material/FilterList';
18
+ import { FileDownload as FileDownloadIcon } from '@mui/icons-material';
19
+ import { PictureAsPdf as PictureAsPdfIcon } from '@mui/icons-material';
20
+ import { TableChart as TableChartIcon } from '@mui/icons-material';
21
+ import html2canvas from 'html2canvas';
22
+ import jsPDF from 'jspdf';
23
+ import find from 'lodash.find';
24
+ import { withStyles } from 'tss-react/mui';
25
+ import { createCSVDownload, downloadCSV } from '../utils';
26
+ import MuiTooltip from '@mui/material/Tooltip';
27
+
28
+ export const defaultToolbarStyles = (theme) => ({
29
+ root: {
30
+ '@media print': {
31
+ display: 'none',
32
+ },
33
+ },
34
+ fullWidthRoot: {},
35
+ left: {
36
+ flex: '1 1 auto',
37
+ },
38
+ fullWidthLeft: {
39
+ flex: '1 1 auto',
40
+ },
41
+ actions: {
42
+ flex: '1 1 auto',
43
+ textAlign: 'right',
44
+ },
45
+ fullWidthActions: {
46
+ flex: '1 1 auto',
47
+ textAlign: 'right',
48
+ },
49
+ titleRoot: {},
50
+ titleText: {},
51
+ fullWidthTitleText: {
52
+ textAlign: 'left',
53
+ },
54
+ icon: {
55
+ '&:hover': {
56
+ color: theme.palette.primary.main,
57
+ },
58
+ },
59
+ iconActive: {
60
+ color: theme.palette.primary.main,
61
+ },
62
+ filterPaper: {
63
+ maxWidth: '50%',
64
+ },
65
+ filterCloseIcon: {
66
+ position: 'absolute',
67
+ right: 0,
68
+ top: 0,
69
+ zIndex: 100,
70
+ },
71
+ searchIcon: {
72
+ display: 'inline-flex',
73
+ marginTop: '10px',
74
+ marginRight: '8px',
75
+ },
76
+ exportMenu: {
77
+ '& .MuiPaper-root': {
78
+ borderRadius: '8px',
79
+ boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
80
+ border: '1px solid #e5e7eb',
81
+ },
82
+ },
83
+ exportMenuItem: {
84
+ '&:hover': {
85
+ backgroundColor: '#f3f4f6',
86
+ },
87
+ },
88
+ [theme.breakpoints.down('md')]: {
89
+ titleRoot: {},
90
+ titleText: {
91
+ fontSize: '16px',
92
+ },
93
+ spacer: {
94
+ display: 'none',
95
+ },
96
+ left: {
97
+ // flex: "1 1 40%",
98
+ padding: '8px 0px',
99
+ },
100
+ actions: {
101
+ // flex: "1 1 60%",
102
+ textAlign: 'right',
103
+ },
104
+ },
105
+ [theme.breakpoints.down('sm')]: {
106
+ root: {
107
+ display: 'block',
108
+ '@media print': {
109
+ display: 'none !important',
110
+ },
111
+ },
112
+ left: {
113
+ padding: '8px 0px 0px 0px',
114
+ },
115
+ titleText: {
116
+ textAlign: 'center',
117
+ },
118
+ actions: {
119
+ textAlign: 'center',
120
+ },
121
+ },
122
+ '@media screen and (max-width: 480px)': {},
123
+ });
124
+
125
+ const RESPONSIVE_FULL_WIDTH_NAME = 'scrollFullHeightFullWidth';
126
+
127
+ class TableToolbar extends React.Component {
128
+ state = {
129
+ iconActive: null,
130
+ showSearch: Boolean(
131
+ this.props.searchText ||
132
+ this.props.options.searchText ||
133
+ this.props.options.searchOpen ||
134
+ this.props.options.searchAlwaysOpen,
135
+ ),
136
+ searchText: this.props.searchText || null,
137
+ anchorEl: null,
138
+ };
139
+
140
+ componentDidUpdate(prevProps) {
141
+ if (this.props.searchText !== prevProps.searchText) {
142
+ this.setState({ searchText: this.props.searchText });
143
+ }
144
+ }
145
+
146
+ handleCSVDownload = () => {
147
+ this.handleExportMenuClose();
148
+ const { data, displayData, columns, options, columnOrder } = this.props;
149
+ let dataToDownload = []; //cloneDeep(data);
150
+ let columnsToDownload = [];
151
+ let columnOrderCopy = Array.isArray(columnOrder) ? columnOrder.slice(0) : [];
152
+
153
+ if (columnOrderCopy.length === 0) {
154
+ columnOrderCopy = columns.map((item, idx) => idx);
155
+ }
156
+
157
+ data.forEach((row) => {
158
+ let newRow = { index: row.index, data: [] };
159
+ columnOrderCopy.forEach((idx) => {
160
+ newRow.data.push(row.data[idx]);
161
+ });
162
+ dataToDownload.push(newRow);
163
+ });
164
+
165
+ columnOrderCopy.forEach((idx) => {
166
+ columnsToDownload.push(columns[idx]);
167
+ });
168
+
169
+ if (options.downloadOptions && options.downloadOptions.filterOptions) {
170
+ // check rows first:
171
+ if (options.downloadOptions.filterOptions.useDisplayedRowsOnly) {
172
+ let filteredDataToDownload = displayData.map((row, index) => {
173
+ let i = -1;
174
+
175
+ // Help to preserve sort order in custom render columns
176
+ row.index = index;
177
+
178
+ return {
179
+ data: row.data.map((column) => {
180
+ i += 1;
181
+
182
+ // if we have a custom render, which will appear as a react element, we must grab the actual value from data
183
+ // that matches the dataIndex and column
184
+ // TODO: Create a utility function for checking whether or not something is a react object
185
+ let val =
186
+ typeof column === 'object' && column !== null && !Array.isArray(column)
187
+ ? find(data, (d) => d.index === row.dataIndex).data[i]
188
+ : column;
189
+ val = typeof val === 'function' ? find(data, (d) => d.index === row.dataIndex).data[i] : val;
190
+ return val;
191
+ }),
192
+ };
193
+ });
194
+
195
+ dataToDownload = [];
196
+ filteredDataToDownload.forEach((row) => {
197
+ let newRow = { index: row.index, data: [] };
198
+ columnOrderCopy.forEach((idx) => {
199
+ newRow.data.push(row.data[idx]);
200
+ });
201
+ dataToDownload.push(newRow);
202
+ });
203
+ }
204
+
205
+ // now, check columns:
206
+ if (options.downloadOptions.filterOptions.useDisplayedColumnsOnly) {
207
+ columnsToDownload = columnsToDownload.filter((_) => _.display === 'true');
208
+
209
+ dataToDownload = dataToDownload.map((row) => {
210
+ row.data = row.data.filter((_, index) => columns[columnOrderCopy[index]].display === 'true');
211
+ return row;
212
+ });
213
+ }
214
+ }
215
+ createCSVDownload(columnsToDownload, dataToDownload, options, downloadCSV);
216
+ };
217
+
218
+ handleExportMenuOpen = (event) => {
219
+ this.setState({ anchorEl: event.currentTarget });
220
+ };
221
+
222
+ handleExportMenuClose = () => {
223
+ this.setState({ anchorEl: null });
224
+ };
225
+
226
+ exportToCSV = () => {
227
+ this.handleCSVDownload();
228
+ };
229
+
230
+ exportToExcel = () => {
231
+ this.handleCSVDownload(); // For now, same as CSV
232
+ };
233
+
234
+ exportToPDF = async () => {
235
+ this.handleExportMenuClose();
236
+ const { tableRef } = this.props;
237
+ if (!tableRef()) return;
238
+
239
+ try {
240
+ const canvas = await html2canvas(tableRef(), {
241
+ scale: 2,
242
+ useCORS: true,
243
+ allowTaint: true,
244
+ backgroundColor: '#ffffff',
245
+ });
246
+
247
+ const imgData = canvas.toDataURL('image/png');
248
+ const pdf = new jsPDF('p', 'mm', 'a4');
249
+ const imgWidth = 210;
250
+ const pageHeight = 295;
251
+ const imgHeight = (canvas.height * imgWidth) / canvas.width;
252
+ let heightLeft = imgHeight;
253
+ let position = 0;
254
+
255
+ pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
256
+ heightLeft -= pageHeight;
257
+
258
+ while (heightLeft >= 0) {
259
+ position = heightLeft - imgHeight;
260
+ pdf.addPage();
261
+ pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
262
+ heightLeft -= pageHeight;
263
+ }
264
+
265
+ pdf.save('table-export.pdf');
266
+ } catch (error) {
267
+ console.error('Print error:', error);
268
+ }
269
+ };
270
+
271
+ handleCustomExport = (type) => {
272
+ this.handleExportMenuClose();
273
+ const { onExport } = this.props.options || {};
274
+ if (onExport && typeof onExport === 'function') {
275
+ onExport(type);
276
+ }
277
+ };
278
+
279
+ setActiveIcon = (iconName) => {
280
+ this.setState(
281
+ (prevState) => ({
282
+ showSearch: this.isSearchShown(iconName),
283
+ iconActive: iconName,
284
+ prevIconActive: prevState.iconActive,
285
+ }),
286
+ () => {
287
+ const { iconActive, prevIconActive } = this.state;
288
+
289
+ if (iconActive === 'filter') {
290
+ this.props.setTableAction('onFilterDialogOpen');
291
+ if (this.props.options.onFilterDialogOpen) {
292
+ this.props.options.onFilterDialogOpen();
293
+ }
294
+ }
295
+ if (iconActive === undefined && prevIconActive === 'filter') {
296
+ this.props.setTableAction('onFilterDialogClose');
297
+ if (this.props.options.onFilterDialogClose) {
298
+ this.props.options.onFilterDialogClose();
299
+ }
300
+ }
301
+ },
302
+ );
303
+ };
304
+
305
+ isSearchShown = (iconName) => {
306
+ if (this.props.options.searchAlwaysOpen) {
307
+ return true;
308
+ }
309
+
310
+ let nextVal = false;
311
+ if (this.state.showSearch) {
312
+ if (this.state.searchText) {
313
+ nextVal = true;
314
+ } else {
315
+ const { onSearchClose } = this.props.options;
316
+ this.props.setTableAction('onSearchClose');
317
+ if (onSearchClose) onSearchClose();
318
+ nextVal = false;
319
+ }
320
+ } else if (iconName === 'search') {
321
+ nextVal = this.showSearch();
322
+ }
323
+ return nextVal;
324
+ };
325
+
326
+ getActiveIcon = (styles, iconName) => {
327
+ let isActive = this.state.iconActive === iconName;
328
+ if (iconName === 'search') {
329
+ const { showSearch, searchText } = this.state;
330
+ isActive = isActive || showSearch || searchText;
331
+ }
332
+ return isActive ? styles.iconActive : styles.icon;
333
+ };
334
+
335
+ showSearch = () => {
336
+ this.props.setTableAction('onSearchOpen');
337
+ !!this.props.options.onSearchOpen && this.props.options.onSearchOpen();
338
+ return true;
339
+ };
340
+
341
+ hideSearch = () => {
342
+ const { onSearchClose } = this.props.options;
343
+
344
+ this.props.setTableAction('onSearchClose');
345
+ if (onSearchClose) onSearchClose();
346
+ this.props.searchClose();
347
+
348
+ this.setState(() => ({
349
+ iconActive: null,
350
+ showSearch: false,
351
+ searchText: null,
352
+ }));
353
+ };
354
+
355
+ handleSearch = (value) => {
356
+ this.setState({ searchText: value });
357
+ this.props.searchTextUpdate(value);
358
+ };
359
+
360
+ handleSearchIconClick = () => {
361
+ const { showSearch, searchText } = this.state;
362
+ if (showSearch && !searchText) {
363
+ this.hideSearch();
364
+ } else {
365
+ this.setActiveIcon('search');
366
+ }
367
+ };
368
+
369
+ handlePrint = async () => {
370
+ try {
371
+ const tableElement = this.props.tableRef();
372
+ if (!tableElement) return;
373
+
374
+ const canvas = await html2canvas(tableElement, {
375
+ scale: 2,
376
+ useCORS: true,
377
+ allowTaint: true,
378
+ backgroundColor: '#ffffff',
379
+ });
380
+
381
+ const imgData = canvas.toDataURL('image/png');
382
+ const pdf = new jsPDF('p', 'mm', 'a4');
383
+ const imgWidth = 210;
384
+ const pageHeight = 295;
385
+ const imgHeight = (canvas.height * imgWidth) / canvas.width;
386
+ let heightLeft = imgHeight;
387
+
388
+ let position = 0;
389
+
390
+ pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
391
+ heightLeft -= pageHeight;
392
+
393
+ while (heightLeft >= 0) {
394
+ position = heightLeft - imgHeight;
395
+ pdf.addPage();
396
+ pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
397
+ heightLeft -= pageHeight;
398
+ }
399
+
400
+ pdf.save('table-export.pdf');
401
+ } catch (error) {
402
+ console.error('Print error:', error);
403
+ }
404
+ };
405
+
406
+ render() {
407
+ const {
408
+ data,
409
+ options,
410
+ classes,
411
+ columns,
412
+ filterData,
413
+ filterList,
414
+ filterUpdate,
415
+ resetFilters,
416
+ toggleViewColumn,
417
+ updateColumns,
418
+ title,
419
+ components = {},
420
+ updateFilterByType,
421
+ } = this.props;
422
+ const { icons = {} } = components;
423
+
424
+ const Tooltip = components.Tooltip || MuiTooltip;
425
+ const TableViewColComponent = components.TableViewCol || TableViewCol;
426
+ const TableFilterComponent = components.TableFilter || TableFilter;
427
+ const SearchIconComponent = icons.SearchIcon || SearchIcon;
428
+ const DownloadIconComponent = icons.DownloadIcon || DownloadIcon;
429
+ const PrintIconComponent = icons.PrintIcon || PrintIcon;
430
+ const ViewColumnIconComponent = icons.ViewColumnIcon || ViewColumnIcon;
431
+ const FilterIconComponent = icons.FilterIcon || FilterIcon;
432
+ const { search, downloadCsv, print, viewColumns, filterTable } = options.textLabels.toolbar;
433
+ const { showSearch, searchText } = this.state;
434
+
435
+ const filterPopoverExit = () => {
436
+ this.setState({ hideFilterPopover: false });
437
+ this.setActiveIcon();
438
+ };
439
+
440
+ const closeFilterPopover = () => {
441
+ this.setState({ hideFilterPopover: true });
442
+ };
443
+
444
+ return (
445
+ <Toolbar
446
+ className={options.responsive !== RESPONSIVE_FULL_WIDTH_NAME ? classes.root : classes.fullWidthRoot}
447
+ role={'toolbar'}
448
+ aria-label={'Table Toolbar'}>
449
+ <div className={options.responsive !== RESPONSIVE_FULL_WIDTH_NAME ? classes.left : classes.fullWidthLeft}>
450
+ {showSearch === true ? (
451
+ options.customSearchRender ? (
452
+ options.customSearchRender(searchText, this.handleSearch, this.hideSearch, options)
453
+ ) : (
454
+ <TableSearch
455
+ searchText={searchText}
456
+ onSearch={this.handleSearch}
457
+ onHide={this.hideSearch}
458
+ options={options}
459
+ />
460
+ )
461
+ ) : typeof title !== 'string' ? (
462
+ title
463
+ ) : (
464
+ <div className={classes.titleRoot} aria-hidden={'true'}>
465
+ <Typography
466
+ variant="h6"
467
+ className={
468
+ options.responsive !== RESPONSIVE_FULL_WIDTH_NAME ? classes.titleText : classes.fullWidthTitleText
469
+ }>
470
+ {title}
471
+ </Typography>
472
+ </div>
473
+ )}
474
+ </div>
475
+ <div className={options.responsive !== RESPONSIVE_FULL_WIDTH_NAME ? classes.actions : classes.fullWidthActions}>
476
+ {!(options.search === false || options.search === 'false' || options.searchAlwaysOpen === true) && (
477
+ <Tooltip title={search} disableFocusListener>
478
+ <IconButton
479
+ aria-label={search}
480
+ data-testid={search + '-iconButton'}
481
+ ref={(el) => (this.searchButton = el)}
482
+ classes={{ root: this.getActiveIcon(classes, 'search') }}
483
+ disabled={options.search === 'disabled'}
484
+ onClick={this.handleSearchIconClick}>
485
+ <SearchIconComponent />
486
+ </IconButton>
487
+ </Tooltip>
488
+ )}
489
+ {!(options.download === false || options.download === 'false') && (
490
+ <>
491
+ <Tooltip title="Export Options" disableFocusListener>
492
+ <IconButton
493
+ data-testid="export-menu-iconButton"
494
+ aria-label="Export Options"
495
+ classes={{ root: classes.icon }}
496
+ disabled={options.download === 'disabled'}
497
+ onClick={this.handleExportMenuOpen}>
498
+ <FileDownloadIcon />
499
+ </IconButton>
500
+ </Tooltip>
501
+ <Menu
502
+ anchorEl={this.state.anchorEl}
503
+ open={Boolean(this.state.anchorEl)}
504
+ onClose={this.handleExportMenuClose}
505
+ classes={{ paper: classes.exportMenu }}
506
+ anchorOrigin={{
507
+ vertical: 'bottom',
508
+ horizontal: 'right',
509
+ }}
510
+ transformOrigin={{
511
+ vertical: 'top',
512
+ horizontal: 'right',
513
+ }}>
514
+ <MenuItem onClick={this.exportToCSV} className={classes.exportMenuItem}>
515
+ <ListItemIcon>
516
+ <TableChartIcon fontSize="small" />
517
+ </ListItemIcon>
518
+ <ListItemText>Export to CSV</ListItemText>
519
+ </MenuItem>
520
+ <MenuItem onClick={this.exportToExcel} className={classes.exportMenuItem}>
521
+ <ListItemIcon>
522
+ <DownloadIcon fontSize="small" />
523
+ </ListItemIcon>
524
+ <ListItemText>Export to Excel</ListItemText>
525
+ </MenuItem>
526
+ <MenuItem onClick={this.exportToPDF} className={classes.exportMenuItem}>
527
+ <ListItemIcon>
528
+ <PictureAsPdfIcon fontSize="small" />
529
+ </ListItemIcon>
530
+ <ListItemText>Export to PDF</ListItemText>
531
+ </MenuItem>
532
+ {this.props.options?.onExport && (
533
+ <MenuItem onClick={() => this.handleCustomExport('custom')} className={classes.exportMenuItem}>
534
+ <ListItemIcon>
535
+ <FileDownloadIcon fontSize="small" />
536
+ </ListItemIcon>
537
+ <ListItemText>Custom Export</ListItemText>
538
+ </MenuItem>
539
+ )}
540
+ </Menu>
541
+ </>
542
+ )}
543
+ {!(options.print === false || options.print === 'false') && (
544
+ <Tooltip title={print}>
545
+ <span>
546
+ <IconButton
547
+ data-testid={print + '-iconButton'}
548
+ aria-label={print}
549
+ disabled={options.print === 'disabled'}
550
+ onClick={this.handlePrint}
551
+ classes={{ root: classes.icon }}>
552
+ <PrintIconComponent />
553
+ </IconButton>
554
+ </span>
555
+ </Tooltip>
556
+ )}
557
+ {!(options.viewColumns === false || options.viewColumns === 'false') && (
558
+ <Popover
559
+ refExit={this.setActiveIcon.bind(null)}
560
+ classes={{ closeIcon: classes.filterCloseIcon }}
561
+ hide={options.viewColumns === 'disabled'}
562
+ trigger={
563
+ <Tooltip title={viewColumns} disableFocusListener>
564
+ <span>
565
+ <IconButton
566
+ data-testid={viewColumns + '-iconButton'}
567
+ aria-label={viewColumns}
568
+ classes={{ root: this.getActiveIcon(classes, 'viewcolumns') }}
569
+ disabled={options.viewColumns === 'disabled'}
570
+ onClick={this.setActiveIcon.bind(null, 'viewcolumns')}>
571
+ <ViewColumnIconComponent />
572
+ </IconButton>
573
+ </span>
574
+ </Tooltip>
575
+ }
576
+ content={
577
+ <TableViewColComponent
578
+ data={data}
579
+ columns={columns}
580
+ options={options}
581
+ onColumnUpdate={toggleViewColumn}
582
+ updateColumns={updateColumns}
583
+ components={components}
584
+ />
585
+ }
586
+ />
587
+ )}
588
+ {!(options.filter === false || options.filter === 'false') && (
589
+ <Popover
590
+ refExit={filterPopoverExit}
591
+ hide={this.state.hideFilterPopover || options.filter === 'disabled'}
592
+ classes={{ paper: classes.filterPaper, closeIcon: classes.filterCloseIcon }}
593
+ trigger={
594
+ <Tooltip title={filterTable} disableFocusListener>
595
+ <span>
596
+ <IconButton
597
+ data-testid={filterTable + '-iconButton'}
598
+ aria-label={filterTable}
599
+ classes={{ root: this.getActiveIcon(classes, 'filter') }}
600
+ disabled={options.filter === 'disabled'}
601
+ onClick={this.setActiveIcon.bind(null, 'filter')}>
602
+ <FilterIconComponent />
603
+ </IconButton>
604
+ </span>
605
+ </Tooltip>
606
+ }
607
+ content={
608
+ <TableFilterComponent
609
+ customFooter={options.customFilterDialogFooter}
610
+ columns={columns}
611
+ options={options}
612
+ filterList={filterList}
613
+ filterData={filterData}
614
+ onFilterUpdate={filterUpdate}
615
+ onFilterReset={resetFilters}
616
+ handleClose={closeFilterPopover}
617
+ updateFilterByType={updateFilterByType}
618
+ components={components}
619
+ />
620
+ }
621
+ />
622
+ )}
623
+ {options.customToolbar && options.customToolbar({ displayData: this.props.displayData })}
624
+ </div>
625
+ </Toolbar>
626
+ );
627
+ }
628
+ }
629
+
630
+ export default withStyles(TableToolbar, defaultToolbarStyles, { name: 'MUIDataTableToolbar' });