├── .babelrc ├── .coveralls.yml ├── .eslintrc ├── .firebaserc ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── icons │ └── GitHub.js ├── pages │ ├── _document.js │ └── index.js ├── static │ ├── favicon.ico │ ├── header.png │ └── mui-datatables-main.jpg ├── utils │ ├── CodeSnippet.js │ ├── Menu.js │ ├── getPageContext.js │ ├── layout.js │ └── withRoot.js └── v2_to_v3_guide.md ├── examples ├── Router │ ├── ExamplesGrid.js │ └── index.js ├── array-value-columns │ └── index.js ├── column-filters │ └── index.js ├── column-options-update │ └── index.js ├── column-sort │ └── index.js ├── component │ ├── cities.js │ └── index.js ├── csv-export │ └── index.js ├── custom-action-columns │ └── index.js ├── custom-components │ ├── TableViewCol.js │ └── index.js ├── customize-columns │ └── index.js ├── customize-filter │ └── index.js ├── customize-footer │ ├── CustomFooter.js │ └── index.js ├── customize-rows │ └── index.js ├── customize-search-render │ ├── CustomSearchRender.js │ └── index.js ├── customize-search │ └── index.js ├── customize-sorting │ └── index.js ├── customize-styling │ └── index.js ├── customize-toolbar-icons │ └── index.js ├── customize-toolbar │ ├── CustomToolbar.js │ └── index.js ├── customize-toolbarselect │ ├── CustomToolbarSelect.js │ └── index.js ├── data-as-objects │ └── index.js ├── draggable-columns │ └── index.js ├── examples.js ├── expandable-rows │ └── index.js ├── fixed-header │ └── index.js ├── hide-columns-print │ └── index.js ├── infinite-scrolling │ └── index.js ├── large-data-set │ └── index.js ├── on-download │ └── index.js ├── on-table-init │ └── index.js ├── resizable-columns │ └── index.js ├── selectable-rows │ └── index.js ├── serverside-filters │ └── index.js ├── serverside-pagination │ └── index.js ├── serverside-sorting │ ├── cities.js │ └── index.js ├── simple-no-toolbar │ └── index.js ├── simple │ └── index.js ├── text-localization │ └── index.js └── themes │ └── index.js ├── firebase.json ├── index.html ├── next.config.js ├── package-lock.json ├── package.json ├── prettier.config.js ├── rollup.config.js ├── src ├── MUIDataTable.js ├── components │ ├── ExpandButton.js │ ├── JumpToPage.js │ ├── Popover.js │ ├── TableBody.js │ ├── TableBodyCell.js │ ├── TableBodyRow.js │ ├── TableFilter.js │ ├── TableFilterList.js │ ├── TableFilterListItem.js │ ├── TableFooter.js │ ├── TableHead.js │ ├── TableHeadCell.js │ ├── TableHeadRow.js │ ├── TablePagination.js │ ├── TableResize.js │ ├── TableSearch.js │ ├── TableSelectCell.js │ ├── TableToolbar.js │ ├── TableToolbarSelect.js │ └── TableViewCol.js ├── hooks │ └── useColumnDrop.js ├── index.js ├── localStorage │ ├── index.js │ ├── load.js │ └── save.js ├── plug-ins │ └── DebounceSearchRender.js ├── textLabels.js └── utils.js ├── test ├── MUIDataTable.test.js ├── MUIDataTableBody.test.js ├── MUIDataTableBodyCell.test.js ├── MUIDataTableCustomComponents.test.js ├── MUIDataTableFilter.test.js ├── MUIDataTableFilterList.test.js ├── MUIDataTableFooter.test.js ├── MUIDataTableHead.test.js ├── MUIDataTableHeadCell.test.js ├── MUIDataTablePagination.test.js ├── MUIDataTableSearch.test.js ├── MUIDataTableSelectCell.test.js ├── MUIDataTableToolbar.test.js ├── MUIDataTableToolbarCustomIcons.test.js ├── MUIDataTableToolbarSelect.test.js ├── MUIDataTableViewCol.test.js ├── TableResize.test.js ├── UseColumnDrop.test.js ├── mocha.opts ├── setup-mocha-env.js └── utils.test.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "plugins": [ 5 | "istanbul" 6 | ] 7 | } 8 | }, 9 | "presets": [ 10 | [ 11 | "@babel/preset-env", 12 | { 13 | "targets": { 14 | "browsers": [ 15 | "ie >= 11", 16 | "> 1%", 17 | "iOS >= 8", 18 | "Android >= 4" 19 | ], 20 | "node": "6.10" 21 | }, 22 | "useBuiltIns": "entry", 23 | "debug": false, 24 | "modules": false, 25 | "corejs": { 26 | "version": 3, 27 | "proposals": true 28 | } 29 | } 30 | ], 31 | [ 32 | "@babel/preset-react" 33 | ] 34 | ], 35 | "plugins": [ 36 | [ 37 | "@babel/plugin-proposal-class-properties" 38 | ], 39 | [ 40 | "@babel/plugin-proposal-object-rest-spread" 41 | ], 42 | [ 43 | "@babel/plugin-transform-async-to-generator" 44 | ], 45 | [ 46 | "@babel/plugin-transform-runtime", 47 | { 48 | "corejs": 3, 49 | "regenerator": true 50 | } 51 | ] 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: mUu8CcBb7ZPxJvlzfjykPQjABLh52Gxob 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "settings": { 4 | "react": { 5 | "version": "latest" 6 | }, 7 | "import/extensions": [ 8 | ".js" 9 | ], 10 | "import/parser": "babel-eslint", 11 | "import/resolver": { 12 | "node": { 13 | "extensions": [ 14 | ".js" 15 | ] 16 | }, 17 | "webpack": { 18 | "config": "webpack.config.js" 19 | } 20 | } 21 | }, 22 | "parserOptions": { 23 | "ecmaVersion": 6, 24 | "sourceType": "module", 25 | "allowImportExportEverywhere": true, 26 | "ecmaFeatures": { 27 | "jsx": true, 28 | "experimentalObjectRestSpread": true 29 | } 30 | }, 31 | "env": { 32 | "es6": true, 33 | "browser": true, 34 | "mocha": true, 35 | "node": true 36 | }, 37 | "extends": [ 38 | "plugin:jsx-a11y/recommended" 39 | ], 40 | "rules": { 41 | "no-console": "off", 42 | "semi": 2, 43 | "no-undef": 2, 44 | "no-undef-init": 2, 45 | "no-tabs": 2, 46 | "react/self-closing-comp": 2, 47 | "react/no-typos": 2, 48 | "react/jsx-no-duplicate-props": "warn", 49 | "react-hooks/rules-of-hooks": "error", 50 | "react-hooks/exhaustive-deps": "warn", 51 | "jsx-a11y/no-autofocus": [ 52 | 2, 53 | { 54 | "ignoreNonDOM": true 55 | } 56 | ] 57 | }, 58 | "plugins": [ 59 | "import", 60 | "react", 61 | "jsx-a11y", 62 | "filenames", 63 | "react-hooks" 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "materialui-datatables" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | ## Expected Behavior 8 | 12 | 13 | ## Current Behavior 14 | 18 | 19 | ## Steps to Reproduce (for bugs) 20 | 24 | 25 | 1. 26 | 2. 27 | 3. 28 | 4. 29 | 30 | ## Your Environment 31 | 32 | 33 | | Tech | Version | 34 | |--------------|---------| 35 | | Material-UI | | 36 | | MUI-datatables | | 37 | | React | | 38 | | browser | | 39 | | etc | | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Osx 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # sublime project 15 | *.sublime-project 16 | *.sublime-workspace 17 | 18 | # vscode project 19 | .vscode 20 | 21 | # idea project 22 | .idea 23 | 24 | # Runtime data 25 | pids 26 | *.pid 27 | *.seed 28 | *.pid.lock 29 | 30 | # Directory for instrumented libs generated by jscoverage/JSCover 31 | lib-cov 32 | 33 | # Coverage directory used by tools like istanbul 34 | coverage 35 | .nyc_output/ 36 | *.lcov 37 | 38 | # Compiled binary addons (http://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directory 42 | node_modules 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Build Data 51 | dist/* 52 | es/* 53 | lib/* 54 | 55 | # Docs 56 | docs/.next/* 57 | docs/export/* 58 | .firebase/ 59 | 60 | # Optional REPL history 61 | .node_repl_history 62 | 63 | ### VisualStudioCode Patch ### 64 | # Ignore all local history of files 65 | .history 66 | 67 | # Yarn Integrity file 68 | .yarn-integrity 69 | 70 | # Yarn lock 71 | # Note: 72 | # Remove the line below when we switched to Yarn 73 | yarn.lock 74 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | 5 | script: 6 | - npm run -s lint 7 | - npm run -s coverage 8 | - npm run -s build 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 gregn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/icons/GitHub.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | 3 | import React from 'react'; 4 | import SvgIcon from '@mui/material/SvgIcon'; 5 | 6 | function GitHub(props) { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | GitHub.muiName = 'SvgIcon'; 15 | 16 | export default GitHub; 17 | -------------------------------------------------------------------------------- /docs/pages/_document.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Document, { Head, Main, NextScript } from 'next/document'; 3 | import JssProvider from 'react-jss/lib/JssProvider'; 4 | import getPageContext from '../utils/getPageContext'; 5 | 6 | class MyDocument extends Document { 7 | render() { 8 | return ( 9 | 10 | 11 | Material-UI DataTables 12 | 13 | 14 | 15 | 16 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 54 | 55 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | // next.config.js 2 | module.exports = { 3 | exportPathMap: function() { 4 | return { 5 | '/': { page: '/' }, 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | bracketSpacing: true, 6 | jsxBracketSameLine: true, 7 | parser: 'babel', 8 | semi: true, 9 | }; 10 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import replace from '@rollup/plugin-replace'; 4 | import uglify from '@lopatnov/rollup-plugin-uglify'; 5 | 6 | export default { 7 | input: 'src/index.js', 8 | plugins: [ 9 | replace({ 10 | 'process.env.NODE_ENV': JSON.stringify('production'), 11 | }), 12 | commonjs({ 13 | include: ['node_modules/**'], 14 | }), 15 | babel({ 16 | babelHelpers: 'runtime', 17 | babelrc: true, 18 | }), 19 | uglify({ 20 | compress: { 21 | conditionals: true, 22 | unused: true, 23 | comparisons: true, 24 | sequences: true, 25 | dead_code: true, 26 | evaluate: true, 27 | if_return: true, 28 | join_vars: true, 29 | }, 30 | output: { 31 | comments: false, 32 | }, 33 | }), 34 | ], 35 | output: { 36 | file: 'dist/index.js', 37 | format: 'cjs', 38 | exports: 'named', 39 | sourcemap: true, 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/ExpandButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import IconButton from '@mui/material/IconButton'; 3 | import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; 4 | import RemoveIcon from '@mui/icons-material/Remove'; 5 | 6 | const ExpandButton = ({ 7 | areAllRowsExpanded, 8 | buttonClass, 9 | expandableRowsHeader, 10 | expandedRows, 11 | iconClass, 12 | iconIndeterminateClass, 13 | isHeaderCell, 14 | onExpand, 15 | }) => { 16 | return ( 17 | <> 18 | {isHeaderCell && !areAllRowsExpanded() && areAllRowsExpanded && expandedRows.data.length > 0 ? ( 19 | 24 | 25 | 26 | ) : ( 27 | 32 | 33 | 34 | )} 35 | 36 | ); 37 | }; 38 | 39 | export default ExpandButton; 40 | -------------------------------------------------------------------------------- /src/components/JumpToPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import InputBase from '@mui/material/InputBase'; 4 | import MenuItem from '@mui/material/MenuItem'; 5 | import Select from '@mui/material/Select'; 6 | import Toolbar from '@mui/material/Toolbar'; 7 | import Typography from '@mui/material/Typography'; 8 | import { makeStyles } from 'tss-react/mui'; 9 | import { getPageValue } from '../utils.js'; 10 | import clsx from 'clsx'; 11 | 12 | const useStyles = makeStyles({ name: 'MUIDataTableJumpToPage' })(theme => ({ 13 | root: { 14 | color: theme.palette.text.primary, 15 | }, 16 | caption: { 17 | flexShrink: 0, 18 | }, 19 | /*  Styles applied to the Select component root element */ 20 | selectRoot: { 21 | marginRight: 32, 22 | marginLeft: 8, 23 | }, 24 | select: { 25 | paddingTop: 6, 26 | paddingBottom: 7, 27 | paddingLeft: 8, 28 | paddingRight: 24, 29 | textAlign: 'right', 30 | textAlignLast: 'right', 31 | fontSize: theme.typography.pxToRem(14), 32 | }, 33 | /* Styles applied to Select component icon class */ 34 | selectIcon: {}, 35 | /* Styles applied to InputBase component */ 36 | input: { 37 | color: 'inhert', 38 | fontSize: 'inhert', 39 | flexShrink: 0, 40 | }, 41 | })); 42 | 43 | function JumpToPage(props) { 44 | const { classes } = useStyles(); 45 | 46 | const handlePageChange = event => { 47 | props.changePage(parseInt(event.target.value, 10)); 48 | }; 49 | 50 | const { count, textLabels, rowsPerPage, page, changePage } = props; 51 | 52 | const textLabel = textLabels.pagination.jumpToPage; 53 | 54 | let pages = []; 55 | let lastPage = Math.min(1000, getPageValue(count, rowsPerPage, 1000000)); 56 | 57 | for (let ii = 0; ii <= lastPage; ii++) { 58 | pages.push(ii); 59 | } 60 | const MenuItemComponent = MenuItem; 61 | 62 | let myStyle = { 63 | display: 'flex', 64 | minHeight: '52px', 65 | alignItems: 'center', 66 | }; 67 | 68 | return ( 69 | 70 | 71 | {textLabel} 72 | 73 | 85 | 86 | ); 87 | } 88 | 89 | JumpToPage.propTypes = { 90 | count: PropTypes.number.isRequired, 91 | page: PropTypes.number.isRequired, 92 | rowsPerPage: PropTypes.number.isRequired, 93 | textLabels: PropTypes.object.isRequired, 94 | }; 95 | 96 | export default JumpToPage; 97 | -------------------------------------------------------------------------------- /src/components/Popover.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import MuiPopover from '@mui/material/Popover'; 4 | import IconButton from '@mui/material/IconButton'; 5 | import CloseIcon from '@mui/icons-material/Close'; 6 | 7 | const Popover = ({ className, trigger, refExit, hide, content, ...providedProps }) => { 8 | const [isOpen, open] = useState(false); 9 | const anchorEl = useRef(null); 10 | 11 | useEffect(() => { 12 | if (isOpen) { 13 | const shouldHide = typeof hide === 'boolean' ? hide : false; 14 | if (shouldHide) { 15 | open(false); 16 | } 17 | } 18 | }, [hide, isOpen, open]); 19 | 20 | const handleClick = event => { 21 | anchorEl.current = event.currentTarget; 22 | open(true); 23 | }; 24 | 25 | const handleRequestClose = () => { 26 | open(false); 27 | }; 28 | 29 | const closeIconClass = providedProps.classes.closeIcon; 30 | delete providedProps.classes.closeIcon; // remove non-standard class from being passed to the popover component 31 | 32 | const transformOriginSpecs = { 33 | vertical: 'top', 34 | horizontal: 'center', 35 | }; 36 | 37 | const anchorOriginSpecs = { 38 | vertical: 'bottom', 39 | horizontal: 'center', 40 | }; 41 | 42 | const handleOnExit = () => { 43 | if (refExit) { 44 | refExit(); 45 | } 46 | }; 47 | 48 | const triggerProps = { 49 | key: 'content', 50 | onClick: event => { 51 | if (trigger.props.onClick) trigger.props.onClick(); 52 | handleClick(event); 53 | }, 54 | }; 55 | 56 | return ( 57 | <> 58 | {trigger} 59 | 68 | 73 | 74 | 75 | {content} 76 | 77 | 78 | ); 79 | }; 80 | 81 | Popover.propTypes = { 82 | refExit: PropTypes.func, 83 | trigger: PropTypes.node.isRequired, 84 | content: PropTypes.node.isRequired, 85 | hide: PropTypes.bool, 86 | }; 87 | 88 | export default Popover; 89 | -------------------------------------------------------------------------------- /src/components/TableBodyRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import clsx from 'clsx'; 4 | import TableRow from '@mui/material/TableRow'; 5 | import { withStyles } from 'tss-react/mui'; 6 | 7 | const defaultBodyRowStyles = theme => ({ 8 | root: { 9 | // material v4 10 | '&.Mui-selected': { 11 | backgroundColor: theme.palette.action.selected, 12 | }, 13 | 14 | // material v3 workaround 15 | '&.mui-row-selected': { 16 | backgroundColor: theme.palette.action.selected, 17 | }, 18 | }, 19 | hoverCursor: { cursor: 'pointer' }, 20 | responsiveStacked: { 21 | [theme.breakpoints.down('md')]: { 22 | borderTop: 'solid 2px rgba(0, 0, 0, 0.15)', 23 | borderBottom: 'solid 2px rgba(0, 0, 0, 0.15)', 24 | padding: 0, 25 | margin: 0, 26 | }, 27 | }, 28 | responsiveSimple: { 29 | [theme.breakpoints.down('sm')]: { 30 | borderTop: 'solid 2px rgba(0, 0, 0, 0.15)', 31 | borderBottom: 'solid 2px rgba(0, 0, 0, 0.15)', 32 | padding: 0, 33 | margin: 0, 34 | }, 35 | }, 36 | }); 37 | 38 | class TableBodyRow extends React.Component { 39 | static propTypes = { 40 | /** Options used to describe table */ 41 | options: PropTypes.object.isRequired, 42 | /** Callback to execute when row is clicked */ 43 | onClick: PropTypes.func, 44 | /** Current row selected or not */ 45 | rowSelected: PropTypes.bool, 46 | /** Extend the style applied to components */ 47 | classes: PropTypes.object, 48 | }; 49 | 50 | render() { 51 | const { classes, options, rowSelected, onClick, className, isRowSelectable, ...rest } = this.props; 52 | 53 | var methods = {}; 54 | if (onClick) { 55 | methods.onClick = onClick; 56 | } 57 | 58 | return ( 59 | 78 | {this.props.children} 79 | 80 | ); 81 | } 82 | } 83 | 84 | export default withStyles(TableBodyRow, defaultBodyRowStyles, { name: 'MUIDataTableBodyRow' }); 85 | -------------------------------------------------------------------------------- /src/components/TableFilterList.js: -------------------------------------------------------------------------------- 1 | import { makeStyles } from 'tss-react/mui'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import TableFilterListItem from './TableFilterListItem'; 5 | 6 | const useStyles = makeStyles({ name: 'MUIDataTableFilterList' })(() => ({ 7 | root: { 8 | display: 'flex', 9 | justifyContent: 'left', 10 | flexWrap: 'wrap', 11 | margin: '0px 16px 0px 16px', 12 | }, 13 | chip: { 14 | margin: '8px 8px 0px 0px', 15 | }, 16 | })); 17 | 18 | const TableFilterList = ({ 19 | options, 20 | filterList, 21 | filterUpdate, 22 | filterListRenderers, 23 | columnNames, 24 | serverSideFilterList, 25 | customFilterListUpdate, 26 | ItemComponent = TableFilterListItem, 27 | }) => { 28 | const { classes } = useStyles(); 29 | const { serverSide } = options; 30 | 31 | const removeFilter = (index, filterValue, columnName, filterType, customFilterListUpdate = null) => { 32 | let removedFilter = filterValue; 33 | if (Array.isArray(removedFilter) && removedFilter.length === 0) { 34 | removedFilter = filterList[index]; 35 | } 36 | 37 | filterUpdate(index, filterValue, columnName, filterType, customFilterListUpdate, filterList => { 38 | if (options.onFilterChipClose) { 39 | options.onFilterChipClose(index, removedFilter, filterList); 40 | } 41 | }); 42 | }; 43 | const customFilterChip = (customFilterItem, index, customFilterItemIndex, item, isArray) => { 44 | let type; 45 | // If our custom filter list is an array, we need to check for custom update functions to determine 46 | // default type. Otherwise we use the supplied type in options. 47 | if (isArray) { 48 | type = customFilterListUpdate[index] ? 'custom' : 'chip'; 49 | } else { 50 | type = columnNames[index].filterType; 51 | } 52 | 53 | return ( 54 | 58 | removeFilter( 59 | index, 60 | item[customFilterItemIndex] || [], 61 | columnNames[index].name, 62 | type, 63 | customFilterListUpdate[index], 64 | ) 65 | } 66 | className={classes.chip} 67 | itemKey={customFilterItemIndex} 68 | index={index} 69 | data={item} 70 | columnNames={columnNames} 71 | filterProps={ 72 | options.setFilterChipProps 73 | ? options.setFilterChipProps(index, columnNames[index].name, item[customFilterItemIndex] || []) 74 | : {} 75 | } 76 | /> 77 | ); 78 | }; 79 | 80 | const filterChip = (index, data, colIndex) => ( 81 | removeFilter(index, data, columnNames[index].name, 'chip')} 85 | className={classes.chip} 86 | itemKey={colIndex} 87 | index={index} 88 | data={data} 89 | columnNames={columnNames} 90 | filterProps={options.setFilterChipProps ? options.setFilterChipProps(index, columnNames[index].name, data) : {}} 91 | /> 92 | ); 93 | 94 | const getFilterList = filterList => { 95 | return filterList.map((item, index) => { 96 | if (columnNames[index].filterType === 'custom' && filterList[index].length) { 97 | const filterListRenderersValue = filterListRenderers[index](item); 98 | 99 | if (Array.isArray(filterListRenderersValue)) { 100 | return filterListRenderersValue.map((customFilterItem, customFilterItemIndex) => 101 | customFilterChip(customFilterItem, index, customFilterItemIndex, item, true), 102 | ); 103 | } else { 104 | return customFilterChip(filterListRenderersValue, index, index, item, false); 105 | } 106 | } 107 | 108 | return item.map((data, colIndex) => filterChip(index, data, colIndex)); 109 | }); 110 | }; 111 | 112 | return ( 113 |
114 | {serverSide && serverSideFilterList ? getFilterList(serverSideFilterList) : getFilterList(filterList)} 115 |
116 | ); 117 | }; 118 | 119 | TableFilterList.propTypes = { 120 | /** Data used to filter table against */ 121 | filterList: PropTypes.array.isRequired, 122 | /** Filter List value renderers */ 123 | filterListRenderers: PropTypes.array.isRequired, 124 | /** Columns used to describe table */ 125 | columnNames: PropTypes.arrayOf( 126 | PropTypes.oneOfType([ 127 | PropTypes.string, 128 | PropTypes.shape({ name: PropTypes.string.isRequired, filterType: PropTypes.string }), 129 | ]), 130 | ).isRequired, 131 | /** Callback to trigger filter update */ 132 | onFilterUpdate: PropTypes.func, 133 | ItemComponent: PropTypes.any, 134 | }; 135 | 136 | export default TableFilterList; 137 | -------------------------------------------------------------------------------- /src/components/TableFilterListItem.js: -------------------------------------------------------------------------------- 1 | import Chip from '@mui/material/Chip'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import clsx from 'clsx'; 5 | 6 | const TableFilterListItem = ({ label, onDelete, className, filterProps }) => { 7 | filterProps = filterProps || {}; 8 | if (filterProps.className) { 9 | className = clsx(className, filterProps.className); 10 | } 11 | return ; 12 | }; 13 | 14 | TableFilterListItem.propTypes = { 15 | label: PropTypes.node, 16 | onDelete: PropTypes.func.isRequired, 17 | className: PropTypes.string.isRequired, 18 | }; 19 | 20 | export default TableFilterListItem; 21 | -------------------------------------------------------------------------------- /src/components/TableFooter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MuiTable from '@mui/material/Table'; 3 | import TablePagination from './TablePagination'; 4 | import { makeStyles } from 'tss-react/mui'; 5 | import PropTypes from 'prop-types'; 6 | 7 | const useStyles = makeStyles({ name: 'MUIDataTableFooter' })(() => ({ 8 | root: { 9 | '@media print': { 10 | display: 'none', 11 | }, 12 | }, 13 | })); 14 | 15 | const TableFooter = ({ options, rowCount, page, rowsPerPage, changeRowsPerPage, changePage }) => { 16 | const { classes } = useStyles(); 17 | const { customFooter, pagination = true } = options; 18 | 19 | if (customFooter) { 20 | return ( 21 | 22 | {options.customFooter( 23 | rowCount, 24 | page, 25 | rowsPerPage, 26 | changeRowsPerPage, 27 | changePage, 28 | options.textLabels.pagination, 29 | )} 30 | 31 | ); 32 | } 33 | 34 | if (pagination) { 35 | return ( 36 | 37 | 46 | 47 | ); 48 | } 49 | 50 | return null; 51 | }; 52 | 53 | TableFooter.propTypes = { 54 | /** Total number of table rows */ 55 | rowCount: PropTypes.number.isRequired, 56 | /** Options used to describe table */ 57 | options: PropTypes.shape({ 58 | customFooter: PropTypes.func, 59 | pagination: PropTypes.bool, 60 | textLabels: PropTypes.shape({ 61 | pagination: PropTypes.object, 62 | }), 63 | }), 64 | /** Current page index */ 65 | page: PropTypes.number.isRequired, 66 | /** Total number allowed of rows per page */ 67 | rowsPerPage: PropTypes.number.isRequired, 68 | /** Callback to trigger rows per page change */ 69 | changeRowsPerPage: PropTypes.func.isRequired, 70 | /** Callback to trigger page change */ 71 | changePage: PropTypes.func.isRequired, 72 | }; 73 | 74 | export default TableFooter; 75 | -------------------------------------------------------------------------------- /src/components/TableHeadRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import clsx from 'clsx'; 4 | import TableRow from '@mui/material/TableRow'; 5 | import { makeStyles } from 'tss-react/mui'; 6 | 7 | const useStyles = makeStyles({ name: 'MUIDataTableHeadRow' })(() => ({ 8 | root: {}, 9 | })); 10 | 11 | const TableHeadRow = ({ children }) => { 12 | const { classes } = useStyles(); 13 | 14 | return ( 15 | 19 | {children} 20 | 21 | ); 22 | }; 23 | 24 | TableHeadRow.propTypes = { 25 | children: PropTypes.node, 26 | }; 27 | 28 | export default TableHeadRow; 29 | -------------------------------------------------------------------------------- /src/components/TablePagination.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import MuiTableCell from '@mui/material/TableCell'; 4 | import MuiTableRow from '@mui/material/TableRow'; 5 | import MuiTableFooter from '@mui/material/TableFooter'; 6 | import MuiTablePagination from '@mui/material/TablePagination'; 7 | import JumpToPage from './JumpToPage'; 8 | import { makeStyles } from 'tss-react/mui'; 9 | import { getPageValue } from '../utils'; 10 | 11 | const useStyles = makeStyles({ name: 'MUIDataTablePagination' })(theme => ({ 12 | root: {}, 13 | tableCellContainer: { 14 | padding: '0px 24px 0px 24px', 15 | }, 16 | navContainer: { 17 | display: 'flex', 18 | justifyContent: 'flex-end', 19 | }, 20 | toolbar: {}, 21 | selectRoot: {}, 22 | '@media screen and (max-width: 400px)': { 23 | toolbar: { 24 | '& span:nth-of-type(2)': { 25 | display: 'none', 26 | }, 27 | }, 28 | selectRoot: { 29 | marginRight: '8px', 30 | }, 31 | }, 32 | })); 33 | 34 | function TablePagination(props) { 35 | const { classes } = useStyles(); 36 | 37 | const handleRowChange = event => { 38 | props.changeRowsPerPage(event.target.value); 39 | }; 40 | 41 | const handlePageChange = (_, page) => { 42 | props.changePage(page); 43 | }; 44 | 45 | const { count, options, rowsPerPage, page } = props; 46 | const textLabels = options.textLabels.pagination; 47 | 48 | return ( 49 | 50 | 51 | 52 |
53 | {options.jumpToPage ? ( 54 | 62 | ) : null} 63 | `${from}-${to} ${textLabels.displayRows} ${count}`} 76 | backIconButtonProps={{ 77 | id: 'pagination-back', 78 | 'data-testid': 'pagination-back', 79 | 'aria-label': textLabels.previous, 80 | title: textLabels.previous || '', 81 | }} 82 | nextIconButtonProps={{ 83 | id: 'pagination-next', 84 | 'data-testid': 'pagination-next', 85 | 'aria-label': textLabels.next, 86 | title: textLabels.next || '', 87 | }} 88 | SelectProps={{ 89 | id: 'pagination-input', 90 | SelectDisplayProps: { id: 'pagination-rows', 'data-testid': 'pagination-rows' }, 91 | MenuProps: { 92 | id: 'pagination-menu', 93 | 'data-testid': 'pagination-menu', 94 | MenuListProps: { id: 'pagination-menu-list', 'data-testid': 'pagination-menu-list' }, 95 | }, 96 | }} 97 | rowsPerPageOptions={options.rowsPerPageOptions} 98 | onPageChange={handlePageChange} 99 | onRowsPerPageChange={handleRowChange} 100 | /> 101 |
102 |
103 |
104 |
105 | ); 106 | } 107 | 108 | TablePagination.propTypes = { 109 | /** Total number of table rows */ 110 | count: PropTypes.number.isRequired, 111 | /** Options used to describe table */ 112 | options: PropTypes.object.isRequired, 113 | /** Current page index */ 114 | page: PropTypes.number.isRequired, 115 | /** Total number allowed of rows per page */ 116 | rowsPerPage: PropTypes.number.isRequired, 117 | /** Callback to trigger rows per page change */ 118 | changeRowsPerPage: PropTypes.func.isRequired, 119 | }; 120 | 121 | export default TablePagination; 122 | -------------------------------------------------------------------------------- /src/components/TableSearch.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Grow from '@mui/material/Grow'; 3 | import TextField from '@mui/material/TextField'; 4 | import SearchIcon from '@mui/icons-material/Search'; 5 | import IconButton from '@mui/material/IconButton'; 6 | import ClearIcon from '@mui/icons-material/Clear'; 7 | import { makeStyles } from 'tss-react/mui'; 8 | 9 | const useStyles = makeStyles({ name: 'MUIDataTableSearch' })(theme => ({ 10 | main: { 11 | display: 'flex', 12 | flex: '1 0 auto', 13 | alignItems: 'center', 14 | }, 15 | searchIcon: { 16 | color: theme.palette.text.secondary, 17 | marginRight: '8px', 18 | }, 19 | searchText: { 20 | flex: '0.8 0', 21 | }, 22 | clearIcon: { 23 | '&:hover': { 24 | color: theme.palette.error.main, 25 | }, 26 | }, 27 | })); 28 | 29 | const TableSearch = ({ options, searchText, onSearch, onHide }) => { 30 | const { classes } = useStyles(); 31 | 32 | const handleTextChange = event => { 33 | onSearch(event.target.value); 34 | }; 35 | 36 | const onKeyDown = event => { 37 | if (event.key === 'Escape') { 38 | onHide(); 39 | } 40 | }; 41 | 42 | const clearIconVisibility = options.searchAlwaysOpen ? 'hidden' : 'visible'; 43 | 44 | return ( 45 | 46 |
47 | 48 | 65 | 66 | 67 | 68 |
69 |
70 | ); 71 | }; 72 | 73 | export default TableSearch; 74 | -------------------------------------------------------------------------------- /src/components/TableSelectCell.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import clsx from 'clsx'; 4 | import Checkbox from '@mui/material/Checkbox'; 5 | import TableCell from '@mui/material/TableCell'; 6 | import { makeStyles } from 'tss-react/mui'; 7 | import ExpandButton from './ExpandButton'; 8 | 9 | const useStyles = makeStyles({ name: 'MUIDataTableSelectCell' })(theme => ({ 10 | root: { 11 | '@media print': { 12 | display: 'none', 13 | }, 14 | }, 15 | fixedHeader: { 16 | position: 'sticky', 17 | top: '0px', 18 | zIndex: 100, 19 | }, 20 | fixedLeft: { 21 | position: 'sticky', 22 | left: '0px', 23 | zIndex: 100, 24 | }, 25 | icon: { 26 | cursor: 'pointer', 27 | transition: 'transform 0.25s', 28 | }, 29 | expanded: { 30 | transform: 'rotate(90deg)', 31 | }, 32 | hide: { 33 | visibility: 'hidden', 34 | }, 35 | headerCell: { 36 | zIndex: 110, 37 | backgroundColor: theme.palette.background.paper, 38 | }, 39 | expandDisabled: {}, 40 | checkboxRoot: {}, 41 | checked: {}, 42 | disabled: {}, 43 | })); 44 | 45 | const TableSelectCell = ({ 46 | fixedHeader, 47 | fixedSelectColumn, 48 | isHeaderCell = false, 49 | expandableOn = false, 50 | selectableOn = 'none', 51 | isRowExpanded = false, 52 | onExpand, 53 | isRowSelectable, 54 | selectableRowsHeader, 55 | hideExpandButton, 56 | expandableRowsHeader, 57 | expandedRows, 58 | areAllRowsExpanded = () => false, 59 | selectableRowsHideCheckboxes, 60 | setHeadCellRef, 61 | dataIndex, 62 | components = {}, 63 | ...otherProps 64 | }) => { 65 | const { classes } = useStyles(); 66 | const CheckboxComponent = components.Checkbox || Checkbox; 67 | const ExpandButtonComponent = components.ExpandButton || ExpandButton; 68 | 69 | if (expandableOn === false && (selectableOn === 'none' || selectableRowsHideCheckboxes === true)) { 70 | return null; 71 | } 72 | 73 | const cellClass = clsx({ 74 | [classes.root]: true, 75 | [classes.fixedHeader]: fixedHeader && isHeaderCell, 76 | [classes.fixedLeft]: fixedSelectColumn, 77 | [classes.headerCell]: isHeaderCell, 78 | }); 79 | 80 | const buttonClass = clsx({ 81 | [classes.expandDisabled]: hideExpandButton, 82 | }); 83 | 84 | const iconClass = clsx({ 85 | [classes.icon]: true, 86 | [classes.hide]: isHeaderCell && !expandableRowsHeader, 87 | [classes.expanded]: isRowExpanded || (isHeaderCell && areAllRowsExpanded()), 88 | }); 89 | const iconIndeterminateClass = clsx({ 90 | [classes.icon]: true, 91 | [classes.hide]: isHeaderCell && !expandableRowsHeader, 92 | }); 93 | 94 | let refProp = {}; 95 | if (setHeadCellRef) { 96 | refProp.ref = el => { 97 | setHeadCellRef(0, 0, el); 98 | }; 99 | } 100 | 101 | const renderCheckBox = () => { 102 | if (isHeaderCell && (selectableOn !== 'multiple' || selectableRowsHeader === false)) { 103 | // only display the header checkbox for multiple selection. 104 | return null; 105 | } 106 | return ( 107 | 119 | ); 120 | }; 121 | 122 | return ( 123 | 124 |
125 | {expandableOn && ( 126 | 137 | )} 138 | {selectableOn !== 'none' && selectableRowsHideCheckboxes !== true && renderCheckBox()} 139 |
140 |
141 | ); 142 | }; 143 | 144 | TableSelectCell.propTypes = { 145 | /** Select cell checked on/off */ 146 | checked: PropTypes.bool.isRequired, 147 | /** Select cell part of fixed header */ 148 | fixedHeader: PropTypes.bool, 149 | /** Callback to trigger cell update */ 150 | onChange: PropTypes.func, 151 | /** Extend the style applied to components */ 152 | classes: PropTypes.object, 153 | /** Is expandable option enabled */ 154 | expandableOn: PropTypes.bool, 155 | /** Adds extra class, `expandDisabled` when the row is not expandable. */ 156 | hideExpandButton: PropTypes.bool, 157 | /** Is selectable option enabled */ 158 | selectableOn: PropTypes.string, 159 | /** Select cell disabled on/off */ 160 | isRowSelectable: PropTypes.bool, 161 | }; 162 | 163 | export default TableSelectCell; 164 | -------------------------------------------------------------------------------- /src/components/TableToolbarSelect.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Paper from '@mui/material/Paper'; 4 | import IconButton from '@mui/material/IconButton'; 5 | import Typography from '@mui/material/Typography'; 6 | import DeleteIcon from '@mui/icons-material/Delete'; 7 | import { withStyles } from 'tss-react/mui'; 8 | import MuiTooltip from '@mui/material/Tooltip'; 9 | 10 | const defaultToolbarSelectStyles = theme => ({ 11 | root: { 12 | backgroundColor: theme.palette.background.default, 13 | flex: '1 1 100%', 14 | display: 'flex', 15 | position: 'relative', 16 | zIndex: 120, 17 | justifyContent: 'space-between', 18 | alignItems: 'center', 19 | paddingTop: typeof theme.spacing === 'function' ? theme.spacing(1) : theme.spacing.unit, 20 | paddingBottom: typeof theme.spacing === 'function' ? theme.spacing(1) : theme.spacing.unit, 21 | '@media print': { 22 | display: 'none', 23 | }, 24 | }, 25 | title: { 26 | paddingLeft: '26px', 27 | }, 28 | iconButton: { 29 | marginRight: '24px', 30 | }, 31 | deleteIcon: {}, 32 | }); 33 | 34 | class TableToolbarSelect extends React.Component { 35 | static propTypes = { 36 | /** Options used to describe table */ 37 | options: PropTypes.object.isRequired, 38 | /** Current row selected or not */ 39 | rowSelected: PropTypes.bool, 40 | /** Callback to trigger selected rows delete */ 41 | onRowsDelete: PropTypes.func, 42 | /** Extend the style applied to components */ 43 | classes: PropTypes.object, 44 | }; 45 | 46 | /** 47 | * @param {number[]} selectedRows Array of rows indexes that are selected, e.g. [0, 2] will select first and third rows in table 48 | */ 49 | handleCustomSelectedRows = selectedRows => { 50 | if (!Array.isArray(selectedRows)) { 51 | throw new TypeError(`"selectedRows" must be an "array", but it's "${typeof selectedRows}"`); 52 | } 53 | 54 | if (selectedRows.some(row => typeof row !== 'number')) { 55 | throw new TypeError(`Array "selectedRows" must contain only numbers`); 56 | } 57 | 58 | const { options } = this.props; 59 | if (selectedRows.length > 1 && options.selectableRows === 'single') { 60 | throw new Error('Can not select more than one row when "selectableRows" is "single"'); 61 | } 62 | this.props.selectRowUpdate('custom', selectedRows); 63 | }; 64 | 65 | render() { 66 | const { classes, onRowsDelete, selectedRows, options, displayData, components = {} } = this.props; 67 | const textLabels = options.textLabels.selectedRows; 68 | const Tooltip = components.Tooltip || MuiTooltip; 69 | 70 | return ( 71 | 72 |
73 | 74 | {selectedRows.data.length} {textLabels.text} 75 | 76 |
77 | {options.customToolbarSelect ? ( 78 | options.customToolbarSelect(selectedRows, displayData, this.handleCustomSelectedRows) 79 | ) : ( 80 | 81 | 82 | 83 | 84 | 85 | )} 86 |
87 | ); 88 | } 89 | } 90 | 91 | export default withStyles(TableToolbarSelect, defaultToolbarSelectStyles, { name: 'MUIDataTableToolbarSelect' }); 92 | -------------------------------------------------------------------------------- /src/components/TableViewCol.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Checkbox from '@mui/material/Checkbox'; 4 | import Typography from '@mui/material/Typography'; 5 | import FormControl from '@mui/material/FormControl'; 6 | import FormGroup from '@mui/material/FormGroup'; 7 | import FormControlLabel from '@mui/material/FormControlLabel'; 8 | import { makeStyles } from 'tss-react/mui'; 9 | 10 | const useStyles = makeStyles({ name: 'MUIDataTableViewCol' })(theme => ({ 11 | root: { 12 | padding: '16px 24px 16px 24px', 13 | fontFamily: 'Roboto', 14 | }, 15 | title: { 16 | marginLeft: '-7px', 17 | marginRight: '24px', 18 | fontSize: '14px', 19 | color: theme.palette.text.secondary, 20 | textAlign: 'left', 21 | fontWeight: 500, 22 | }, 23 | formGroup: { 24 | marginTop: '8px', 25 | }, 26 | formControl: {}, 27 | checkbox: { 28 | padding: '0px', 29 | width: '32px', 30 | height: '32px', 31 | }, 32 | checkboxRoot: {}, 33 | checked: {}, 34 | label: { 35 | fontSize: '15px', 36 | marginLeft: '8px', 37 | color: theme.palette.text.primary, 38 | }, 39 | })); 40 | 41 | const TableViewCol = ({ columns, options, components = {}, onColumnUpdate, updateColumns }) => { 42 | const { classes } = useStyles(); 43 | const textLabels = options.textLabels.viewColumns; 44 | const CheckboxComponent = components.Checkbox || Checkbox; 45 | 46 | const handleColChange = index => { 47 | onColumnUpdate(index); 48 | }; 49 | 50 | return ( 51 | 52 | 53 | {textLabels.title} 54 | 55 | 56 | {columns.map((column, index) => { 57 | return ( 58 | column.display !== 'excluded' && 59 | column.viewColumns !== false && ( 60 | handleColChange(index)} 76 | checked={column.display === 'true'} 77 | value={column.name} 78 | /> 79 | } 80 | label={column.label} 81 | /> 82 | ) 83 | ); 84 | })} 85 | 86 | 87 | ); 88 | }; 89 | 90 | TableViewCol.propTypes = { 91 | /** Columns used to describe table */ 92 | columns: PropTypes.array.isRequired, 93 | /** Options used to describe table */ 94 | options: PropTypes.object.isRequired, 95 | /** Callback to trigger View column update */ 96 | onColumnUpdate: PropTypes.func, 97 | /** Extend the style applied to components */ 98 | classes: PropTypes.object, 99 | }; 100 | 101 | export default TableViewCol; 102 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './MUIDataTable'; 2 | export { default as Popover } from './components/Popover'; 3 | export { default as TableBodyCell } from './components/TableBodyCell'; 4 | export { default as TableBody } from './components/TableBody'; 5 | export { default as TableBodyRow } from './components/TableBodyRow'; 6 | export { default as TableFilter } from './components/TableFilter'; 7 | export { default as TableFilterList } from './components/TableFilterList'; 8 | export { default as TableFooter } from './components/TableFooter'; 9 | export { default as TableHeadCell } from './components/TableHeadCell'; 10 | export { default as TableHead } from './components/TableHead'; 11 | export { default as TableHeadRow } from './components/TableHeadRow'; 12 | export { default as TablePagination } from './components/TablePagination'; 13 | export { default as TableResize } from './components/TableResize'; 14 | export { default as TableSearch } from './components/TableSearch'; 15 | export { default as TableSelectCell } from './components/TableSelectCell'; 16 | export { default as TableToolbar } from './components/TableToolbar'; 17 | export { default as TableToolbarSelect } from './components/TableToolbarSelect'; 18 | export { default as TableViewCol } from './components/TableViewCol'; 19 | export { default as ExpandButton } from './components/ExpandButton'; 20 | export { debounceSearchRender, DebounceTableSearch } from './plug-ins/DebounceSearchRender'; 21 | -------------------------------------------------------------------------------- /src/localStorage/index.js: -------------------------------------------------------------------------------- 1 | export { load } from './load'; 2 | export { save } from './save'; 3 | -------------------------------------------------------------------------------- /src/localStorage/load.js: -------------------------------------------------------------------------------- 1 | const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; 2 | 3 | export const load = storageKey => { 4 | if (isBrowser) { 5 | return JSON.parse(window.localStorage.getItem(storageKey)); 6 | } else if (storageKey !== undefined) { 7 | console.warn('storageKey support only on browser'); 8 | return undefined; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/localStorage/save.js: -------------------------------------------------------------------------------- 1 | export const save = (storageKey, state) => { 2 | const { selectedRows, data, displayData, ...savedState } = state; 3 | 4 | window.localStorage.setItem(storageKey, JSON.stringify(savedState)); 5 | }; 6 | -------------------------------------------------------------------------------- /src/plug-ins/DebounceSearchRender.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Grow from '@mui/material/Grow'; 3 | import TextField from '@mui/material/TextField'; 4 | import SearchIcon from '@mui/icons-material/Search'; 5 | import IconButton from '@mui/material/IconButton'; 6 | import ClearIcon from '@mui/icons-material/Clear'; 7 | import { withStyles } from 'tss-react/mui'; 8 | 9 | function debounce(func, wait, immediate) { 10 | var timeout; 11 | return function() { 12 | var context = this, 13 | args = arguments; 14 | var later = function() { 15 | timeout = null; 16 | if (!immediate) func.apply(context, args); 17 | }; 18 | var callNow = immediate && !timeout; 19 | clearTimeout(timeout); 20 | timeout = setTimeout(later, wait); 21 | if (callNow) func.apply(context, args); 22 | }; 23 | } 24 | 25 | const defaultStyles = theme => ({ 26 | main: { 27 | display: 'flex', 28 | flex: '1 0 auto', 29 | alignItems: 'center', 30 | }, 31 | searchIcon: { 32 | color: theme.palette.text.secondary, 33 | marginRight: '8px', 34 | }, 35 | searchText: { 36 | flex: '0.8 0', 37 | }, 38 | clearIcon: { 39 | '&:hover': { 40 | color: theme.palette.error.main, 41 | }, 42 | }, 43 | }); 44 | 45 | class _DebounceTableSearch extends React.Component { 46 | handleTextChangeWrapper = debouncedSearch => { 47 | return function(event) { 48 | debouncedSearch(event.target.value); 49 | }; 50 | }; 51 | 52 | componentDidMount() { 53 | document.addEventListener('keydown', this.onKeyDown, false); 54 | } 55 | 56 | componentWillUnmount() { 57 | document.removeEventListener('keydown', this.onKeyDown, false); 58 | } 59 | 60 | onKeyDown = event => { 61 | if (event.keyCode === 27) { 62 | this.props.onHide(); 63 | } 64 | }; 65 | 66 | render() { 67 | const { classes, options, onHide, searchText, debounceWait } = this.props; 68 | 69 | const debouncedSearch = debounce(value => { 70 | this.props.onSearch(value); 71 | }, debounceWait); 72 | 73 | const clearIconVisibility = options.searchAlwaysOpen ? 'hidden' : 'visible'; 74 | 75 | return ( 76 | 77 |
78 | 79 | (this.searchField = el)} 91 | placeholder={options.searchPlaceholder} 92 | {...(options.searchProps ? options.searchProps : {})} 93 | /> 94 | 95 | 96 | 97 |
98 |
99 | ); 100 | } 101 | } 102 | 103 | var DebounceTableSearch = withStyles(_DebounceTableSearch, defaultStyles, { name: 'MUIDataTableSearch' }); 104 | export { DebounceTableSearch }; 105 | 106 | export function debounceSearchRender(debounceWait = 200) { 107 | return (searchText, handleSearch, hideSearch, options) => { 108 | return ( 109 | 116 | ); 117 | }; 118 | } 119 | -------------------------------------------------------------------------------- /src/textLabels.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Default text labels. 3 | */ 4 | const getTextLabels = () => ({ 5 | body: { 6 | noMatch: 'Sorry, no matching records found', 7 | toolTip: 'Sort', 8 | }, 9 | pagination: { 10 | next: 'Next Page', 11 | previous: 'Previous Page', 12 | rowsPerPage: 'Rows per page:', 13 | displayRows: 'of', 14 | jumpToPage: 'Jump to Page:', 15 | }, 16 | toolbar: { 17 | search: 'Search', 18 | downloadCsv: 'Download CSV', 19 | print: 'Print', 20 | viewColumns: 'View Columns', 21 | filterTable: 'Filter Table', 22 | }, 23 | filter: { 24 | all: 'All', 25 | title: 'FILTERS', 26 | reset: 'RESET', 27 | }, 28 | viewColumns: { 29 | title: 'Show Columns', 30 | titleAria: 'Show/Hide Table Columns', 31 | }, 32 | selectedRows: { 33 | text: 'row(s) selected', 34 | delete: 'Delete', 35 | deleteAria: 'Delete Selected Rows', 36 | }, 37 | }); 38 | 39 | export default getTextLabels; 40 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | function buildMap(rows) { 2 | return rows.reduce((accum, { dataIndex }) => { 3 | accum[dataIndex] = true; 4 | return accum; 5 | }, {}); 6 | } 7 | 8 | function escapeDangerousCSVCharacters(data) { 9 | if (typeof data === 'string') { 10 | // Places single quote before the appearance of dangerous characters if they 11 | // are the first in the data string. 12 | return data.replace(/^\+|^\-|^\=|^\@/g, "'$&"); 13 | } 14 | 15 | return data; 16 | } 17 | 18 | function warnDeprecated(warning, consoleWarnings = true) { 19 | let consoleWarn = typeof consoleWarnings === 'function' ? consoleWarnings : console.warn; 20 | if (consoleWarnings) { 21 | consoleWarn(`Deprecation Notice: ${warning}`); 22 | } 23 | } 24 | 25 | function warnInfo(warning, consoleWarnings = true) { 26 | let consoleWarn = typeof consoleWarnings === 'function' ? consoleWarnings : console.warn; 27 | if (consoleWarnings) { 28 | consoleWarn(`${warning}`); 29 | } 30 | } 31 | 32 | function getPageValue(count, rowsPerPage, page) { 33 | const totalPages = count <= rowsPerPage ? 1 : Math.ceil(count / rowsPerPage); 34 | 35 | // `page` is 0-indexed 36 | return page >= totalPages ? totalPages - 1 : page; 37 | } 38 | 39 | function getCollatorComparator() { 40 | if (!!Intl) { 41 | const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }); 42 | return collator.compare; 43 | } 44 | 45 | const fallbackComparator = (a, b) => a.localeCompare(b); 46 | return fallbackComparator; 47 | } 48 | 49 | function sortCompare(order) { 50 | return (a, b) => { 51 | var aData = a.data === null || typeof a.data === 'undefined' ? '' : a.data; 52 | var bData = b.data === null || typeof b.data === 'undefined' ? '' : b.data; 53 | return ( 54 | (typeof aData.localeCompare === 'function' ? aData.localeCompare(bData) : aData - bData) * 55 | (order === 'asc' ? 1 : -1) 56 | ); 57 | }; 58 | } 59 | 60 | function buildCSV(columns, data, options) { 61 | const replaceDoubleQuoteInString = columnData => 62 | typeof columnData === 'string' ? columnData.replace(/\"/g, '""') : columnData; 63 | 64 | const buildHead = columns => { 65 | return ( 66 | columns 67 | .reduce( 68 | (soFar, column) => 69 | column.download 70 | ? soFar + 71 | '"' + 72 | escapeDangerousCSVCharacters(replaceDoubleQuoteInString(column.label || column.name)) + 73 | '"' + 74 | options.downloadOptions.separator 75 | : soFar, 76 | '', 77 | ) 78 | .slice(0, -1) + '\r\n' 79 | ); 80 | }; 81 | const CSVHead = buildHead(columns); 82 | 83 | const buildBody = data => { 84 | if (!data.length) return ''; 85 | return data 86 | .reduce( 87 | (soFar, row) => 88 | soFar + 89 | '"' + 90 | row.data 91 | .filter((_, index) => columns[index].download) 92 | .map(columnData => escapeDangerousCSVCharacters(replaceDoubleQuoteInString(columnData))) 93 | .join('"' + options.downloadOptions.separator + '"') + 94 | '"\r\n', 95 | '', 96 | ) 97 | .trim(); 98 | }; 99 | const CSVBody = buildBody(data); 100 | 101 | const csv = options.onDownload 102 | ? options.onDownload(buildHead, buildBody, columns, data) 103 | : `${CSVHead}${CSVBody}`.trim(); 104 | 105 | return csv; 106 | } 107 | 108 | function downloadCSV(csv, filename) { 109 | const blob = new Blob([csv], { type: 'text/csv' }); 110 | 111 | /* taken from react-csv */ 112 | if (navigator && navigator.msSaveOrOpenBlob) { 113 | navigator.msSaveOrOpenBlob(blob, filename); 114 | } else { 115 | const dataURI = `data:text/csv;charset=utf-8,${csv}`; 116 | 117 | const URL = window.URL || window.webkitURL; 118 | const downloadURI = typeof URL.createObjectURL === 'undefined' ? dataURI : URL.createObjectURL(blob); 119 | 120 | let link = document.createElement('a'); 121 | link.setAttribute('href', downloadURI); 122 | link.setAttribute('download', filename); 123 | document.body.appendChild(link); 124 | link.click(); 125 | document.body.removeChild(link); 126 | } 127 | } 128 | 129 | function createCSVDownload(columns, data, options, downloadCSV) { 130 | const csv = buildCSV(columns, data, options); 131 | 132 | if (options.onDownload && csv === false) { 133 | return; 134 | } 135 | 136 | downloadCSV(csv, options.downloadOptions.filename); 137 | } 138 | 139 | export { 140 | buildMap, 141 | getPageValue, 142 | getCollatorComparator, 143 | sortCompare, 144 | createCSVDownload, 145 | buildCSV, 146 | downloadCSV, 147 | warnDeprecated, 148 | warnInfo, 149 | escapeDangerousCSVCharacters, 150 | }; 151 | -------------------------------------------------------------------------------- /test/MUIDataTableBodyCell.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { spy, stub } from 'sinon'; 3 | import { mount, shallow } from 'enzyme'; 4 | import { assert, expect, should } from 'chai'; 5 | import MUIDataTable from '../src/MUIDataTable'; 6 | import TableBodyCell from '../src/components/TableBodyCell'; 7 | 8 | describe('', function() { 9 | let data; 10 | let columns; 11 | 12 | before(() => { 13 | columns = [ 14 | { 15 | name: 'Name', 16 | }, 17 | 'Company', 18 | { name: 'City', label: 'City Label', options: { filterType: 'textField' } }, 19 | { 20 | name: 'State', 21 | options: { filterType: 'multiselect' }, 22 | }, 23 | { name: 'Empty', options: { empty: true, filterType: 'checkbox' } }, 24 | ]; 25 | data = [ 26 | ['Joe James', 'Test Corp', 'Yonkers', 'NY'], 27 | ['John Walsh', 'Test Corp', 'Hartford', null], 28 | ['Bob Herm', 'Test Corp X', 'Tampa', 'FL'], 29 | ['James Houston', 'Test Corp', 'Dallas', 'TX'], 30 | ]; 31 | }); 32 | 33 | it('should execute "onCellClick" prop when clicked if provided', () => { 34 | var clickCount = 0; 35 | var colIndex, rowIndex, colData; 36 | const options = { 37 | onCellClick: (val, colMeta) => { 38 | clickCount++; 39 | colIndex = colMeta.colIndex; 40 | rowIndex = colMeta.rowIndex; 41 | colData = val; 42 | }, 43 | }; 44 | 45 | const fullWrapper = mount(); 46 | 47 | fullWrapper 48 | .find('[data-testid="MuiDataTableBodyCell-0-0"]') 49 | .at(0) 50 | .simulate('click'); 51 | assert.strictEqual(clickCount, 1); 52 | assert.strictEqual(colIndex, 0); 53 | assert.strictEqual(rowIndex, 0); 54 | assert.strictEqual(colData, 'Joe James'); 55 | 56 | fullWrapper 57 | .find('[data-testid="MuiDataTableBodyCell-2-3"]') 58 | .at(0) 59 | .simulate('click'); 60 | assert.strictEqual(clickCount, 2); 61 | assert.strictEqual(colIndex, 2); 62 | assert.strictEqual(rowIndex, 3); 63 | assert.strictEqual(colData, 'Dallas'); 64 | 65 | fullWrapper 66 | .find('[data-testid="MuiDataTableBodyCell-1-2"]') 67 | .at(0) 68 | .simulate('click'); 69 | assert.strictEqual(clickCount, 3); 70 | assert.strictEqual(colIndex, 1); 71 | assert.strictEqual(rowIndex, 2); 72 | assert.strictEqual(colData, 'Test Corp X'); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/MUIDataTableCustomComponents.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount, shallow } from 'enzyme'; 3 | import { assert } from 'chai'; 4 | import MUIDataTable from '../src/MUIDataTable'; 5 | import Chip from '@mui/material/Chip'; 6 | import TableFilterList from '../src/components/TableFilterList'; 7 | 8 | const CustomChip = props => { 9 | return ; 10 | }; 11 | 12 | const CustomFilterList = props => { 13 | return ; 14 | }; 15 | 16 | describe(' with custom components', function() { 17 | let data; 18 | let columns; 19 | 20 | before(() => { 21 | columns = [ 22 | { name: 'Name' }, 23 | { 24 | name: 'Company', 25 | options: { 26 | filter: true, 27 | filterType: 'custom', 28 | filterList: ['Test Corp'], 29 | }, 30 | }, 31 | { name: 'City', label: 'City Label' }, 32 | { name: 'State' }, 33 | { name: 'Empty', options: { empty: true, filterType: 'checkbox' } }, 34 | ]; 35 | data = [ 36 | ['Joe James', 'Test Corp', 'Yonkers', 'NY'], 37 | ['John Walsh', 'Test Corp', 'Hartford', null], 38 | ['Bob Herm', 'Test Corp', 'Tampa', 'FL'], 39 | ['James Houston', 'Test Corp', 'Dallas', 'TX'], 40 | ]; 41 | }); 42 | 43 | it('should render a table with custom Chip in TableFilterList', () => { 44 | const wrapper = mount( 45 | , 52 | ); 53 | const customFilterList = wrapper.find(CustomFilterList); 54 | assert.lengthOf(customFilterList, 1); 55 | const customChip = customFilterList.find(CustomChip); 56 | assert.lengthOf(customChip, 1); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/MUIDataTableFooter.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { spy } from 'sinon'; 3 | import { mount } from 'enzyme'; 4 | import { assert } from 'chai'; 5 | import MuiTableFooter from '@mui/material/TableFooter'; 6 | import getTextLabels from '../src/textLabels'; 7 | import TableFooter from '../src/components/TableFooter'; 8 | import JumpToPage from '../src/components/JumpToPage'; 9 | 10 | describe('', function() { 11 | let options; 12 | const changeRowsPerPage = spy(); 13 | const changePage = spy(); 14 | before(() => { 15 | options = { 16 | rowsPerPageOptions: [5, 10, 15], 17 | textLabels: getTextLabels(), 18 | }; 19 | }); 20 | 21 | it('should render a table footer', () => { 22 | const mountWrapper = mount( 23 | , 31 | ); 32 | 33 | const actualResult = mountWrapper.find(MuiTableFooter); 34 | assert.strictEqual(actualResult.length, 1); 35 | }); 36 | 37 | it('should render a table footer with customFooter', () => { 38 | const customOptions = { 39 | rowsPerPageOptions: [5, 10, 15], 40 | textLabels: getTextLabels(), 41 | customFooter: (rowCount, page, rowsPerPage, changeRowsPerPage, changePage, textLabels) => { 42 | return ( 43 | 51 | ); 52 | }, 53 | }; 54 | 55 | const mountWrapper = mount( 56 | , 64 | ); 65 | 66 | const actualResult = mountWrapper.find(MuiTableFooter); 67 | assert.strictEqual(actualResult.length, 1); 68 | }); 69 | 70 | it('should not render a table footer', () => { 71 | const nonPageOption = { 72 | rowsPerPageOptions: [5, 10, 15], 73 | textLabels: getTextLabels(), 74 | pagination: false, 75 | }; 76 | 77 | const mountWrapper = mount( 78 | , 86 | ); 87 | 88 | const actualResult = mountWrapper.find(MuiTableFooter); 89 | assert.strictEqual(actualResult.length, 0); 90 | }); 91 | 92 | it('should render a JumpToPage component', () => { 93 | const options = { 94 | rowsPerPageOptions: [5, 10, 15], 95 | textLabels: getTextLabels(), 96 | jumpToPage: true, 97 | }; 98 | 99 | const mountWrapper = mount( 100 | , 108 | ); 109 | 110 | const actualResult = mountWrapper.find(JumpToPage); 111 | assert.strictEqual(actualResult.length, 1); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /test/MUIDataTableHeadCell.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { spy, stub } from 'sinon'; 3 | import { mount, shallow } from 'enzyme'; 4 | import { assert } from 'chai'; 5 | import getTextLabels from '../src/textLabels'; 6 | import TableHeadCell from '../src/components/TableHeadCell'; 7 | import TableCell from '@mui/material/TableCell'; 8 | import TableSortLabel from '@mui/material/TableSortLabel'; 9 | import HelpIcon from '@mui/icons-material/Help'; 10 | import { DndProvider } from 'react-dnd'; 11 | import { HTML5Backend } from 'react-dnd-html5-backend'; 12 | 13 | describe('', function() { 14 | let classes; 15 | 16 | before(() => { 17 | classes = { 18 | root: {}, 19 | }; 20 | }); 21 | 22 | it('should add custom props to header cell if "setCellHeaderProps" provided', () => { 23 | const options = { sort: true, textLabels: getTextLabels() }; 24 | const toggleSort = () => {}; 25 | const setCellHeaderProps = { myProp: 'test', className: 'testClass' }; 26 | const selectRowUpdate = stub(); 27 | const toggleExpandRow = () => {}; 28 | 29 | const mountWrapper = mount( 30 | 31 | 38 | some content 39 | 40 | , 41 | ); 42 | 43 | const props = mountWrapper.find(TableCell).props(); 44 | const classNames = props.className.split(' '); 45 | const finalClass = classNames[classNames.length - 1]; 46 | 47 | assert.strictEqual(props.myProp, 'test'); 48 | assert.strictEqual(finalClass, 'testClass'); 49 | }); 50 | 51 | it('should render a table head cell with sort label when options.sort = true provided', () => { 52 | const options = { sort: true, textLabels: getTextLabels() }; 53 | const toggleSort = () => {}; 54 | 55 | const wrapper = mount( 56 | 57 | 58 | some content 59 | 60 | , 61 | ); 62 | 63 | const actualResult = wrapper.find(TableSortLabel); 64 | assert.strictEqual(actualResult.length, 1); 65 | }); 66 | 67 | it('should render a table head cell without sort label when options.sort = false provided', () => { 68 | const options = { sort: false, textLabels: getTextLabels() }; 69 | const toggleSort = () => {}; 70 | 71 | const shallowWrapper = shallow( 72 | 73 | 74 | some content 75 | 76 | , 77 | ); 78 | 79 | const actualResult = shallowWrapper.find(TableSortLabel); 80 | assert.strictEqual(actualResult.length, 0); 81 | }); 82 | 83 | it('should render a table help icon when hint provided', () => { 84 | const options = { sort: true, textLabels: getTextLabels() }; 85 | 86 | const wrapper = mount( 87 | 88 | 89 | some content 90 | 91 | , 92 | ); 93 | 94 | const actualResult = wrapper.find(HelpIcon); 95 | assert.strictEqual(actualResult.length, 1); 96 | }); 97 | 98 | it('should render a table head cell without custom tooltip when hint provided', () => { 99 | const options = { sort: true, textLabels: getTextLabels() }; 100 | 101 | const shallowWrapper = shallow( 102 | 103 | 104 | some content 105 | 106 | , 107 | ).dive(); 108 | 109 | const actualResult = shallowWrapper.find(HelpIcon); 110 | assert.strictEqual(actualResult.length, 0); 111 | }); 112 | 113 | it('should trigger toggleSort prop callback when calling method handleSortClick', () => { 114 | const options = { sort: true, textLabels: getTextLabels() }; 115 | const toggleSort = spy(); 116 | 117 | const wrapper = mount( 118 | 119 | 126 | some content 127 | 128 | , 129 | ); 130 | 131 | const instance = wrapper 132 | .find('td') 133 | .at(0) 134 | .childAt(0); 135 | const event = { target: { value: 'All' } }; 136 | instance.simulate('click'); 137 | assert.strictEqual(toggleSort.callCount, 1); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /test/MUIDataTablePagination.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { spy } from 'sinon'; 3 | import { mount, shallow } from 'enzyme'; 4 | import { assert } from 'chai'; 5 | import MuiTablePagination from '@mui/material/TablePagination'; 6 | import getTextLabels from '../src/textLabels'; 7 | import TablePagination from '../src/components/TablePagination'; 8 | 9 | describe('', function() { 10 | let options; 11 | 12 | before(() => { 13 | options = { 14 | rowsPerPageOptions: [5, 10, 15], 15 | textLabels: getTextLabels(), 16 | }; 17 | }); 18 | 19 | it('should render a table footer with pagination', () => { 20 | const mountWrapper = mount(); 21 | 22 | const actualResult = mountWrapper.find(MuiTablePagination); 23 | assert.strictEqual(actualResult.length, 1); 24 | }); 25 | 26 | it('should trigger changePage prop callback when page is changed', () => { 27 | const changePage = spy(); 28 | const wrapper = mount( 29 | , 30 | ); 31 | 32 | wrapper 33 | .find('#pagination-next') 34 | .at(0) 35 | .simulate('click'); 36 | wrapper.unmount(); 37 | 38 | assert.strictEqual(changePage.callCount, 1); 39 | }); 40 | 41 | it('should correctly change page to be in bounds if out of bounds page was set', () => { 42 | // Set a page that is too high for the count and rowsPerPage 43 | const mountWrapper = mount(); 44 | const actualResult = mountWrapper.find(MuiTablePagination).props().page; 45 | 46 | // material-ui v3 does some internal calculations to protect against out of bounds pages, but material v4 does not 47 | assert.strictEqual(actualResult, 0); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/MUIDataTableSearch.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import simulant from 'simulant'; 3 | import { spy, stub } from 'sinon'; 4 | import { mount, shallow } from 'enzyme'; 5 | import { assert, expect, should } from 'chai'; 6 | import TextField from '@mui/material/TextField'; 7 | import TableSearch from '../src/components/TableSearch'; 8 | import getTextLabels from '../src/textLabels'; 9 | 10 | describe('', function() { 11 | it('should render a search bar', () => { 12 | const options = { textLabels: getTextLabels() }; 13 | const onSearch = () => {}; 14 | const onHide = () => {}; 15 | 16 | const mountWrapper = mount(); 17 | 18 | const actualResult = mountWrapper.find(TextField); 19 | assert.strictEqual(actualResult.length, 1); 20 | }); 21 | 22 | it('should render a search bar with text initialized', () => { 23 | const options = { textLabels: getTextLabels() }; 24 | const onSearch = () => {}; 25 | const onHide = () => {}; 26 | 27 | const mountWrapper = mount( 28 | , 29 | ); 30 | const actualResult = mountWrapper.find(TextField); 31 | assert.strictEqual(actualResult.length, 1); 32 | assert.strictEqual(actualResult.props().value, 'searchText'); 33 | }); 34 | 35 | it('should change search bar text when searchText changes', () => { 36 | const options = { textLabels: getTextLabels() }; 37 | const onSearch = () => {}; 38 | const onHide = () => {}; 39 | 40 | const mountWrapper = mount( 41 | , 42 | ); 43 | const actualResult = mountWrapper.setProps({ searchText: 'nextText' }).update(); 44 | assert.strictEqual(actualResult.length, 1); 45 | assert.strictEqual(actualResult.find(TextField).props().value, 'nextText'); 46 | }); 47 | 48 | it('should render a search bar with placeholder when searchPlaceholder is set', () => { 49 | const options = { textLabels: getTextLabels(), searchPlaceholder: 'TestingPlaceholder' }; 50 | const onSearch = () => {}; 51 | const onHide = () => {}; 52 | 53 | const mountWrapper = mount(); 54 | const actualResult = mountWrapper.find(TextField); 55 | assert.strictEqual(actualResult.length, 1); 56 | assert.strictEqual(actualResult.props().placeholder, 'TestingPlaceholder'); 57 | }); 58 | 59 | it('should trigger handleTextChange prop callback when calling method handleTextChange', () => { 60 | const options = { onSearchChange: () => true, textLabels: getTextLabels() }; 61 | const onSearch = spy(); 62 | const onHide = () => {}; 63 | 64 | const wrapper = mount(); 65 | 66 | wrapper 67 | .find('input') 68 | .at(0) 69 | .simulate('change', { target: { value: '' } }); 70 | wrapper.unmount(); 71 | 72 | assert.strictEqual(onSearch.callCount, 1); 73 | }); 74 | 75 | it('should hide the search bar when hitting the ESCAPE key', () => { 76 | const options = { textLabels: getTextLabels() }; 77 | const onHide = spy(); 78 | 79 | const mountWrapper = mount(, { attachTo: document.body }); 80 | 81 | simulant.fire(document.body.querySelector('input'), 'keydown', { keyCode: 27 }); 82 | assert.strictEqual(onHide.callCount, 1); 83 | }); 84 | 85 | it('should hide not hide search bar when entering anything but the ESCAPE key', () => { 86 | const options = { textLabels: getTextLabels() }; 87 | const onHide = spy(); 88 | 89 | const mountWrapper = mount(, { attachTo: document.body }); 90 | 91 | simulant.fire(document.body.querySelector('input'), 'keydown', { keyCode: 25 }); 92 | assert.strictEqual(onHide.callCount, 0); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /test/MUIDataTableSelectCell.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { spy, stub } from 'sinon'; 3 | import { mount, shallow } from 'enzyme'; 4 | import { assert, expect, should } from 'chai'; 5 | import Checkbox from '@mui/material/Checkbox'; 6 | import TableSelectCell from '../src/components/TableSelectCell'; 7 | 8 | describe('', function() { 9 | before(() => {}); 10 | 11 | it('should render table select cell', () => { 12 | const mountWrapper = mount(); 13 | 14 | const actualResult = mountWrapper.find(Checkbox); 15 | assert.strictEqual(actualResult.length, 1); 16 | }); 17 | 18 | it('should render table select cell checked', () => { 19 | const mountWrapper = mount(); 20 | 21 | const actualResult = mountWrapper.find(Checkbox); 22 | assert.strictEqual(actualResult.props().checked, true); 23 | }); 24 | 25 | it('should render table select cell unchecked', () => { 26 | const mountWrapper = mount(); 27 | 28 | const actualResult = mountWrapper.find(Checkbox); 29 | assert.strictEqual(actualResult.props().checked, false); 30 | }); 31 | 32 | // it("should trigger onColumnUpdate prop callback when calling method handleColChange", () => { 33 | // const options = {}; 34 | // const onColumnUpdate = spy(); 35 | 36 | // const shallowWrapper = shallow( 37 | // , 43 | // ).dive(); 44 | 45 | // const instance = shallowWrapper.instance(); 46 | 47 | // instance.handleColChange(0); 48 | // assert.strictEqual(onColumnUpdate.callCount, 1); 49 | // }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/MUIDataTableToolbarCustomIcons.test.js: -------------------------------------------------------------------------------- 1 | import IconButton from '@mui/material/IconButton'; 2 | import DownloadIcon from '@mui/icons-material/CloudDownload'; 3 | import FilterIcon from '@mui/icons-material/FilterList'; 4 | import PrintIcon from '@mui/icons-material/Print'; 5 | import SearchIcon from '@mui/icons-material/Search'; 6 | import ViewColumnIcon from '@mui/icons-material/ViewColumn'; 7 | import Chip from '@mui/material/Chip'; 8 | import { assert } from 'chai'; 9 | import { mount } from 'enzyme'; 10 | import React from 'react'; 11 | import TableToolbar from '../src/components/TableToolbar'; 12 | import getTextLabels from '../src/textLabels'; 13 | 14 | const CustomChip = props => { 15 | return ; 16 | }; 17 | 18 | const icons = { 19 | SearchIcon, 20 | DownloadIcon, 21 | PrintIcon, 22 | ViewColumnIcon, 23 | FilterIcon, 24 | }; 25 | let setTableAction = () => {}; 26 | const options = { 27 | print: true, 28 | download: true, 29 | search: true, 30 | filter: true, 31 | viewColumns: true, 32 | textLabels: getTextLabels(), 33 | downloadOptions: { 34 | separator: ',', 35 | filename: 'tableDownload.csv', 36 | filterOptions: { 37 | useDisplayedRowsOnly: true, 38 | useDisplayedColumnsOnly: true, 39 | }, 40 | }, 41 | }; 42 | const columns = ['First Name', 'Company', 'City', 'State']; 43 | const data = [ 44 | { 45 | data: ['Joe James', 'Test Corp', 'Yonkers', 'NY'], 46 | dataIndex: 0, 47 | }, 48 | { 49 | data: ['John Walsh', 'Test Corp', 'Hartford', 'CT'], 50 | dataIndex: 1, 51 | }, 52 | { 53 | data: ['Bob Herm', 'Test Corp', 'Tampa', 'FL'], 54 | dataIndex: 2, 55 | }, 56 | { 57 | data: ['James Houston', 'Test Corp', 'Dallas', 'TX'], 58 | dataIndex: 3, 59 | }, 60 | ]; 61 | 62 | const testCustomIcon = iconName => { 63 | const components = { icons: { [iconName]: CustomChip } }; 64 | const wrapper = mount(); 65 | assert.strictEqual(wrapper.find(IconButton).length, 5); // All icons show 66 | assert.strictEqual(wrapper.find(CustomChip).length, 1); // Custom chip shows once 67 | Object.keys(icons).forEach(icon => { 68 | // The original default for the custom icon should be gone, the rest should remain 69 | assert.strictEqual(wrapper.find(icons[icon]).length, iconName === icon ? 0 : 1); 70 | }); 71 | }; 72 | 73 | describe(' with custom icons', function() { 74 | it('should render a toolbar with a custom chip in place of the search icon', () => { 75 | testCustomIcon('SearchIcon'); 76 | }); 77 | 78 | it('should render a toolbar with a custom chip in place of the download icon', () => { 79 | testCustomIcon('DownloadIcon'); 80 | }); 81 | 82 | it('should render a toolbar with a custom chip in place of the print icon', () => { 83 | testCustomIcon('PrintIcon'); 84 | }); 85 | 86 | it('should render a toolbar with a custom chip in place of the view columns icon', () => { 87 | testCustomIcon('ViewColumnIcon'); 88 | }); 89 | 90 | it('should render a toolbar with a custom chip in place of the filter icon', () => { 91 | testCustomIcon('FilterIcon'); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /test/MUIDataTableToolbarSelect.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { match, spy, stub } from 'sinon'; 3 | import { mount, shallow } from 'enzyme'; 4 | import { assert, expect, should } from 'chai'; 5 | import DeleteIcon from '@mui/icons-material/Delete'; 6 | import TableToolbarSelect from '../src/components/TableToolbarSelect'; 7 | import getTextLabels from '../src/textLabels'; 8 | 9 | describe('', function() { 10 | before(() => {}); 11 | 12 | it('should render table toolbar select', () => { 13 | const onRowsDelete = () => {}; 14 | const mountWrapper = mount( 15 | , 20 | ); 21 | 22 | const actualResult = mountWrapper.find(DeleteIcon); 23 | assert.strictEqual(actualResult.length, 1); 24 | }); 25 | 26 | it('should call customToolbarSelect with 3 arguments', () => { 27 | const onRowsDelete = () => {}; 28 | const customToolbarSelect = spy(); 29 | const selectedRows = { data: [1] }; 30 | const displayData = [1]; 31 | 32 | const mountWrapper = mount( 33 | , 39 | ); 40 | 41 | assert.strictEqual(customToolbarSelect.calledWith(selectedRows, displayData, match.typeOf('function')), true); 42 | }); 43 | 44 | it('should throw TypeError if selectedRows is not an array of numbers', done => { 45 | const onRowsDelete = () => {}; 46 | const selectRowUpdate = () => {}; 47 | const customToolbarSelect = (_, __, setSelectedRows) => { 48 | const spySetSelectedRows = spy(setSelectedRows); 49 | try { 50 | spySetSelectedRows(''); 51 | } catch (error) { 52 | //do nothing 53 | } 54 | try { 55 | spySetSelectedRows(['1']); 56 | } catch (error) { 57 | //do nothing 58 | } 59 | 60 | spySetSelectedRows.exceptions.forEach(error => assert.strictEqual(error instanceof TypeError, true)); 61 | 62 | done(); 63 | }; 64 | const selectedRows = { data: [1] }; 65 | const displayData = [1]; 66 | 67 | const mountWrapper = mount( 68 | , 75 | ); 76 | }); 77 | 78 | it('should call selectRowUpdate when customToolbarSelect passed and setSelectedRows was called', () => { 79 | const onRowsDelete = () => {}; 80 | const selectRowUpdate = spy(); 81 | const customToolbarSelect = (_, __, setSelectedRows) => { 82 | setSelectedRows([1]); 83 | }; 84 | const selectedRows = { data: [1] }; 85 | const displayData = [1]; 86 | 87 | const mountWrapper = mount( 88 | , 95 | ); 96 | 97 | assert.strictEqual(selectRowUpdate.calledOnce, true); 98 | }); 99 | 100 | it('should throw an error when multiple rows are selected and selectableRows="single"', () => { 101 | const onRowsDelete = () => {}; 102 | const selectRowUpdate = spy(); 103 | const selectedRows = { data: [1] }; 104 | const displayData = [1]; 105 | const catchErr = spy(); 106 | 107 | const wrapper = shallow( 108 | , 115 | ); 116 | const instance = wrapper.dive().instance(); 117 | 118 | try { 119 | instance.handleCustomSelectedRow([1, 2]); 120 | } catch (err) { 121 | catchErr(); 122 | } 123 | 124 | assert.strictEqual(catchErr.calledOnce, true); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /test/MUIDataTableViewCol.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { spy, stub } from 'sinon'; 3 | import { mount, shallow } from 'enzyme'; 4 | import { assert, expect, should } from 'chai'; 5 | import Checkbox from '@mui/material/Checkbox'; 6 | import TableViewCol from '../src/components/TableViewCol'; 7 | import getTextLabels from '../src/textLabels'; 8 | import { FormControlLabel } from '@mui/material'; 9 | 10 | describe('', function() { 11 | let columns; 12 | let options; 13 | 14 | before(() => { 15 | columns = [ 16 | { name: 'a', label: 'A', display: 'true' }, 17 | { name: 'b', label: 'B', display: 'true' }, 18 | { name: 'c', label: 'C', display: 'true' }, 19 | { name: 'd', label: 'D', display: 'true' }, 20 | ]; 21 | options = { 22 | textLabels: getTextLabels(), 23 | }; 24 | }); 25 | 26 | it('should render view columns', () => { 27 | const mountWrapper = mount(); 28 | 29 | const actualResult = mountWrapper.find(Checkbox); 30 | assert.strictEqual(actualResult.length, 4); 31 | }); 32 | 33 | it('should labels as view column names when present', () => { 34 | const mountWrapper = mount(); 35 | const labels = mountWrapper.find(FormControlLabel).map(n => n.text()); 36 | assert.deepEqual(labels, ['A', 'B', 'C', 'D']); 37 | }); 38 | 39 | it('should trigger onColumnUpdate prop callback when calling method handleColChange', () => { 40 | const onColumnUpdate = spy(); 41 | 42 | const wrapper = mount(); 43 | 44 | wrapper 45 | .find('input[type="checkbox"]') 46 | .at(0) 47 | .simulate('change', { target: { checked: false, value: false } }); 48 | wrapper.unmount(); 49 | 50 | assert.strictEqual(onColumnUpdate.callCount, 1); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/TableResize.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { spy, stub } from 'sinon'; 3 | import { mount, shallow } from 'enzyme'; 4 | import { assert, expect, should } from 'chai'; 5 | import TableResize from '../src/components/TableResize'; 6 | import MUIDataTable from '../src/MUIDataTable'; 7 | 8 | describe('', function() { 9 | let options; 10 | 11 | before(() => { 12 | options = { 13 | resizableColumns: true, 14 | tableBodyHeight: '500px', 15 | }; 16 | }); 17 | 18 | it('should render a table resize component', () => { 19 | const updateDividers = spy(); 20 | const setResizeable = spy(); 21 | 22 | const mountWrapper = mount( 23 | , 24 | ); 25 | 26 | const actualResult = mountWrapper.find(TableResize); 27 | assert.strictEqual(actualResult.length, 1); 28 | 29 | assert.strictEqual(updateDividers.callCount, 1); 30 | assert.strictEqual(setResizeable.callCount, 1); 31 | }); 32 | 33 | it('should create a coordinate map for each column', () => { 34 | const columns = ['Name', 'Age', 'Location', 'Phone']; 35 | const data = [['Joe', 26, 'Chile', '555-5555']]; 36 | 37 | const shallowWrapper = mount(); 38 | 39 | var state = shallowWrapper 40 | .find(TableResize) 41 | .childAt(0) 42 | .state(); 43 | 44 | var colCoordCount = 0; 45 | for (let prop in state.resizeCoords) { 46 | colCoordCount++; 47 | } 48 | 49 | shallowWrapper.unmount(); 50 | 51 | assert.strictEqual(colCoordCount, 5); 52 | }); 53 | 54 | it('should execute resize methods correctly', () => { 55 | const updateDividers = spy(); 56 | let cellsRef = { 57 | 0: { 58 | left: 0, 59 | width: 50, 60 | getBoundingClientRect: () => ({ 61 | left: 0, 62 | width: 50, 63 | height: 100, 64 | }), 65 | style: {}, 66 | }, 67 | 1: { 68 | left: 50, 69 | width: 50, 70 | getBoundingClientRect: () => ({ 71 | left: 50, 72 | width: 50, 73 | height: 100, 74 | }), 75 | style: {}, 76 | }, 77 | }; 78 | let tableRef = { 79 | style: { 80 | width: '100px', 81 | }, 82 | getBoundingClientRect: () => ({ 83 | width: 100, 84 | height: 100, 85 | }), 86 | offsetParent: { 87 | offsetLeft: 0, 88 | }, 89 | }; 90 | 91 | const setResizeable = next => { 92 | next(cellsRef, tableRef); 93 | }; 94 | 95 | const shallowWrapper = shallow( 96 | , 97 | ); 98 | const instance = shallowWrapper.dive().instance(); 99 | 100 | instance.handleResize(); 101 | 102 | let evt = { 103 | clientX: 48, 104 | }; 105 | instance.onResizeStart(0, evt); 106 | instance.onResizeMove(0, evt); 107 | instance.onResizeEnd(0, evt); 108 | 109 | evt = { 110 | clientX: 52, 111 | }; 112 | instance.onResizeStart(0, evt); 113 | instance.onResizeMove(0, evt); 114 | instance.onResizeEnd(0, evt); 115 | 116 | let endState = shallowWrapper.dive().state(); 117 | //console.dir(endState); 118 | 119 | assert.strictEqual(endState.tableWidth, 100); 120 | assert.strictEqual(endState.tableHeight, 100); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /test/UseColumnDrop.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { spy, stub } from 'sinon'; 3 | import { mount, shallow } from 'enzyme'; 4 | import { assert, expect, should } from 'chai'; 5 | import { getColModel, reorderColumns, handleHover } from '../src/hooks/useColumnDrop'; 6 | 7 | describe('useColumnDrop', function() { 8 | before(() => {}); 9 | 10 | it('should reorder columns when reorderColumns is called', () => { 11 | let prevColumnOrder = [1, 2, 3, 4]; 12 | let newOrder = reorderColumns(prevColumnOrder, 1, 4); 13 | 14 | expect(newOrder).to.eql([2, 3, 4, 1]); 15 | }); 16 | 17 | it('should build a column model object when getColModel is called', () => { 18 | let offsetParent = { 19 | offsetLeft: 10, 20 | offsetParent: null, 21 | }; 22 | let headCellRefs = { 23 | 0: { 24 | offsetLeft: 0, 25 | offsetParent: 0, 26 | offsetWidth: 0, 27 | offsetParent: offsetParent, 28 | }, 29 | 1: { 30 | offsetLeft: 0, 31 | offsetParent: 0, 32 | offsetWidth: 0, 33 | offsetParent: null, 34 | }, 35 | 2: { 36 | offsetLeft: 0, 37 | offsetParent: 0, 38 | offsetWidth: 0, 39 | offsetParent: null, 40 | }, 41 | }; 42 | let columnOrder = [0, 1]; 43 | let columns = [ 44 | { 45 | display: 'true', 46 | }, 47 | { 48 | display: 'true', 49 | }, 50 | ]; 51 | 52 | let newModel = getColModel(headCellRefs, columnOrder, columns); 53 | 54 | expect(newModel.length).to.equal(3); 55 | expect(newModel[0].left).to.equal(10); 56 | expect(newModel[0].ref.offsetParent).to.equal(offsetParent); 57 | expect(newModel[1].columnIndex).to.equal(0); 58 | }); 59 | 60 | it('should build a column model object when getColModel is called and no select cell exists', () => { 61 | let headCellRefs = { 62 | 0: null, 63 | 1: { 64 | offsetLeft: 0, 65 | offsetParent: 0, 66 | offsetWidth: 0, 67 | offsetParent: null, 68 | }, 69 | 2: { 70 | offsetLeft: 0, 71 | offsetParent: 0, 72 | offsetWidth: 0, 73 | offsetParent: null, 74 | }, 75 | }; 76 | let columnOrder = [0, 1]; 77 | let columns = [ 78 | { 79 | display: 'true', 80 | }, 81 | { 82 | display: 'true', 83 | }, 84 | ]; 85 | 86 | let newModel = getColModel(headCellRefs, columnOrder, columns); 87 | 88 | expect(newModel.length).to.equal(2); 89 | expect(newModel[0].left).to.equal(0); 90 | expect(newModel[1].columnIndex).to.equal(1); 91 | }); 92 | 93 | it('should set columnShift on timers when handleHover is called', () => { 94 | let offsetParent = { 95 | offsetLeft: 10, 96 | offsetParent: null, 97 | }; 98 | let headCellRefs = { 99 | 0: { 100 | offsetLeft: 0, 101 | offsetParent: 0, 102 | offsetWidth: 0, 103 | offsetParent: offsetParent, 104 | style: {}, 105 | }, 106 | 1: { 107 | offsetLeft: 0, 108 | offsetParent: 0, 109 | offsetWidth: 10, 110 | offsetParent: null, 111 | style: {}, 112 | }, 113 | 2: { 114 | offsetLeft: 0, 115 | offsetParent: 0, 116 | offsetWidth: 10, 117 | offsetParent: null, 118 | style: {}, 119 | }, 120 | }; 121 | let columnOrder = [0, 1]; 122 | let columns = [ 123 | { 124 | display: 'true', 125 | }, 126 | { 127 | display: 'true', 128 | }, 129 | ]; 130 | let timers = { 131 | columnShift: null, 132 | }; 133 | 134 | handleHover({ 135 | item: { 136 | columnIndex: 0, 137 | left: 0, 138 | style: {}, 139 | }, 140 | mon: { 141 | getItem: () => ({ 142 | colIndex: 1, 143 | headCellRefs: headCellRefs, 144 | }), 145 | getClientOffset: () => ({ 146 | x: 15, 147 | }), 148 | }, 149 | index: 0, 150 | headCellRefs, 151 | updateColumnOrder: spy(), 152 | columnOrder: [0, 1], 153 | transitionTime: 0, 154 | tableRef: { 155 | querySelectorAll: () => [ 156 | { 157 | style: {}, 158 | }, 159 | ], 160 | }, 161 | tableId: '123', 162 | timers, 163 | columns, 164 | }); 165 | 166 | expect(timers.columnShift).to.not.equal(null); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ./test/setup-mocha-env.js 2 | --extensions js,jsx 3 | -------------------------------------------------------------------------------- /test/setup-mocha-env.js: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme'; 2 | import React from 'react'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | 5 | /* required when running >= 16.0 */ 6 | Enzyme.configure({ adapter: new Adapter() }); 7 | 8 | function setupDom() { 9 | const { JSDOM } = require('jsdom'); 10 | const Node = require('jsdom/lib/jsdom/living/node-document-position'); 11 | 12 | const dom = new JSDOM(''); 13 | 14 | global.window = dom.window; 15 | global.document = window.document; 16 | global.Node = Node; 17 | 18 | global.navigator = { 19 | userAgent: 'node.js', 20 | appVersion: '', 21 | }; 22 | 23 | function copyProps(src, target) { 24 | const props = Object.getOwnPropertyNames(src) 25 | .filter(prop => typeof target[prop] === 'undefined') 26 | .map(prop => Object.getOwnPropertyDescriptor(src, prop)); 27 | Object.defineProperties(target, props); 28 | } 29 | 30 | copyProps(dom.window, global); 31 | 32 | const KEYS = ['HTMLElement']; 33 | KEYS.forEach(key => { 34 | global[key] = window[key]; 35 | }); 36 | 37 | global.document.createRange = () => ({ 38 | setStart: () => {}, 39 | setEnd: () => {}, 40 | commonAncestorContainer: { 41 | nodeName: 'BODY', 42 | ownerDocument: { 43 | documentElement: window.document.body, 44 | parent: { 45 | nodeName: 'BODY', 46 | }, 47 | }, 48 | }, 49 | }); 50 | 51 | global.requestAnimationFrame = callback => { 52 | setTimeout(callback, 0); 53 | }; 54 | 55 | global.window.cancelAnimationFrame = () => {}; 56 | global.getComputedStyle = global.window.getComputedStyle; 57 | global.HTMLInputElement = global.window.HTMLInputElement; 58 | global.Element = global.window.Element; 59 | global.Event = global.window.Event; 60 | global.dispatchEvent = global.window.dispatchEvent; 61 | global.window.getComputedStyle = () => ({}); 62 | 63 | Object.defineProperty(global.window.URL, 'createObjectURL', { value: () => {} }); 64 | global.Blob = () => ''; 65 | } 66 | 67 | setupDom(); 68 | console.error = function() {}; 69 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | import { getPageValue, buildCSV, createCSVDownload, escapeDangerousCSVCharacters } from '../src/utils'; 2 | import { spy } from 'sinon'; 3 | import { assert } from 'chai'; 4 | 5 | describe('utils.js', () => { 6 | describe('escapeDangerousCSVCharacters', () => { 7 | it('properly escapes the first character in a string if it can be used for injection', () => { 8 | assert.strictEqual(escapeDangerousCSVCharacters('+SUM(1+1)'), "'+SUM(1+1)"); 9 | assert.strictEqual(escapeDangerousCSVCharacters('-SUM(1+1)'), "'-SUM(1+1)"); 10 | assert.strictEqual(escapeDangerousCSVCharacters('=SUM(1+1)'), "'=SUM(1+1)"); 11 | assert.strictEqual(escapeDangerousCSVCharacters('@SUM(1+1)'), "'@SUM(1+1)"); 12 | assert.equal(escapeDangerousCSVCharacters(123), 123); 13 | }); 14 | }); 15 | 16 | describe('getPageValue', () => { 17 | it('returns the highest in bounds page value when page is out of bounds and count is greater than rowsPerPage', () => { 18 | const count = 30; 19 | const rowsPerPage = 10; 20 | const page = 5; 21 | 22 | const actualResult = getPageValue(count, rowsPerPage, page); 23 | assert.strictEqual(actualResult, 2); 24 | }); 25 | 26 | it('returns the highest in bounds page value when page is in bounds and count is greater than rowsPerPage', () => { 27 | const count = 30; 28 | const rowsPerPage = 10; 29 | const page = 1; 30 | 31 | const actualResult = getPageValue(count, rowsPerPage, page); 32 | assert.strictEqual(actualResult, 1); 33 | }); 34 | 35 | it('returns the highest in bounds page value when page is out of bounds and count is less than rowsPerPage', () => { 36 | const count = 3; 37 | const rowsPerPage = 10; 38 | const page = 1; 39 | 40 | const actualResult = getPageValue(count, rowsPerPage, page); 41 | assert.strictEqual(actualResult, 0); 42 | }); 43 | 44 | it('returns the highest in bounds page value when page is in bounds and count is less than rowsPerPage', () => { 45 | const count = 3; 46 | const rowsPerPage = 10; 47 | const page = 0; 48 | 49 | const actualResult = getPageValue(count, rowsPerPage, page); 50 | assert.strictEqual(actualResult, 0); 51 | }); 52 | 53 | it('returns the highest in bounds page value when page is out of bounds and count is equal to rowsPerPage', () => { 54 | const count = 10; 55 | const rowsPerPage = 10; 56 | const page = 1; 57 | 58 | const actualResult = getPageValue(count, rowsPerPage, page); 59 | assert.strictEqual(actualResult, 0); 60 | }); 61 | 62 | it('returns the highest in bounds page value when page is in bounds and count is equal to rowsPerPage', () => { 63 | const count = 10; 64 | const rowsPerPage = 10; 65 | const page = 0; 66 | 67 | const actualResult = getPageValue(count, rowsPerPage, page); 68 | assert.strictEqual(actualResult, 0); 69 | }); 70 | }); 71 | 72 | describe('buildCSV', () => { 73 | const options = { 74 | downloadOptions: { 75 | separator: ';', 76 | }, 77 | onDownload: null, 78 | }; 79 | const columns = [ 80 | { 81 | name: 'firstname', 82 | download: true, 83 | }, 84 | { 85 | name: 'lastname', 86 | download: true, 87 | }, 88 | ]; 89 | 90 | it('properly builds a csv when given a non-empty dataset', () => { 91 | const data = [{ data: ['anton', 'abraham'] }, { data: ['berta', 'buchel'] }]; 92 | const csv = buildCSV(columns, data, options); 93 | 94 | assert.strictEqual(csv, '"firstname";"lastname"\r\n' + '"anton";"abraham"\r\n' + '"berta";"buchel"'); 95 | }); 96 | 97 | it('returns an empty csv with header when given an empty dataset', () => { 98 | const data = []; 99 | const csv = buildCSV(columns, data, options); 100 | 101 | assert.strictEqual(csv, '"firstname";"lastname"'); 102 | }); 103 | }); 104 | 105 | describe('createCSVDownload', () => { 106 | const columns = [ 107 | { 108 | name: 'firstname', 109 | download: true, 110 | }, 111 | { 112 | name: 'lastname', 113 | download: true, 114 | }, 115 | ]; 116 | const data = [{ data: ['anton', 'abraham'] }, { data: ['berta', 'buchel'] }]; 117 | 118 | it('does not call download function if download callback returns `false`', () => { 119 | const options = { 120 | downloadOptions: { 121 | separator: ';', 122 | }, 123 | onDownload: () => false, 124 | }; 125 | const downloadCSV = spy(); 126 | 127 | createCSVDownload(columns, data, options, downloadCSV); 128 | 129 | assert.strictEqual(downloadCSV.callCount, 0); 130 | }); 131 | 132 | it('calls download function if download callback returns truthy', () => { 133 | const options = { 134 | downloadOptions: { 135 | separator: ';', 136 | }, 137 | onDownload: () => true, 138 | }; 139 | const downloadCSV = spy(); 140 | 141 | createCSVDownload(columns, data, options, downloadCSV); 142 | 143 | assert.strictEqual(downloadCSV.callCount, 1); 144 | }); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = { 4 | entry: { 5 | app: ['core-js/stable', 'regenerator-runtime/runtime', './examples/Router/index.js'], 6 | }, 7 | stats: 'verbose', 8 | context: __dirname, 9 | output: { 10 | filename: 'bundle.js', 11 | }, 12 | devtool: 'source-map', 13 | devServer: { 14 | disableHostCheck: true, 15 | host: 'localhost', 16 | hot: true, 17 | inline: true, 18 | port: 5050, 19 | stats: 'errors-warnings', 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.(js|jsx)$/, 25 | exclude: /(node_modules)/, 26 | use: ['babel-loader', 'eslint-loader'], 27 | }, 28 | { 29 | test: /\.css$/i, 30 | use: ['style-loader', 'css-loader'], 31 | }, 32 | ], 33 | }, 34 | plugins: [ 35 | new webpack.HotModuleReplacementPlugin(), 36 | new webpack.NamedModulesPlugin(), 37 | new webpack.DefinePlugin({ 38 | 'process.env': { 39 | NODE_ENV: JSON.stringify('development'), 40 | }, 41 | }), 42 | ], 43 | }; 44 | --------------------------------------------------------------------------------