mirror of
https://github.com/gosticks/react-table.git
synced 2025-10-16 11:55:36 +00:00
docs(examples/material-ui-enhanced-table): Add more Material UI table (#1847)
* docs(examples/material-ui-enhanced-table): add more Material UI table This enhanced Material UI table demonstrates client side pagination, sorting, global search, add row, and delete row. * Update EnhancedTable.js Co-authored-by: Tanner Linsley <tannerlinsley@gmail.com>
This commit is contained in:
parent
164b3d6b7e
commit
2bf99aaea0
@ -1,21 +1,13 @@
|
|||||||
{
|
{
|
||||||
"dist/index.js": {
|
"dist/index.js": {
|
||||||
"bundled": 113198,
|
"bundled": 113244,
|
||||||
"minified": 52513,
|
"minified": 52531,
|
||||||
"gzipped": 13835
|
"gzipped": 13840
|
||||||
},
|
},
|
||||||
"dist/index.es.js": {
|
"dist/index.es.js": {
|
||||||
"bundled": 112261,
|
"bundled": 112307,
|
||||||
"minified": 51677,
|
"minified": 51695,
|
||||||
"gzipped": 13669,
|
"gzipped": 13674,
|
||||||
"bundled": 126701,
|
|
||||||
"minified": 59700,
|
|
||||||
"gzipped": 15373
|
|
||||||
},
|
|
||||||
"dist/index.es.js": {
|
|
||||||
"bundled": 125788,
|
|
||||||
"minified": 58888,
|
|
||||||
"gzipped": 15205,
|
|
||||||
"treeshaked": {
|
"treeshaked": {
|
||||||
"rollup": {
|
"rollup": {
|
||||||
"code": 80,
|
"code": 80,
|
||||||
|
|||||||
@ -73,6 +73,9 @@
|
|||||||
- Material-UI
|
- Material-UI
|
||||||
- [Source](https://github.com/tannerlinsley/react-table/tree/master/examples/material-UI-components)
|
- [Source](https://github.com/tannerlinsley/react-table/tree/master/examples/material-UI-components)
|
||||||
- [Open in CodeSandbox](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/material-UI-components)
|
- [Open in CodeSandbox](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/material-UI-components)
|
||||||
|
- Material-UI Enhanced Table
|
||||||
|
- [Source](https://github.com/tannerlinsley/react-table/tree/master/examples/material-UI-enhanced-table)
|
||||||
|
- [Open in CodeSandbox](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/material-UI-enhanced-table)
|
||||||
- [ ] Styled-Components
|
- [ ] Styled-Components
|
||||||
- [ ] CSS
|
- [ ] CSS
|
||||||
- [ ] Bootstrap
|
- [ ] Bootstrap
|
||||||
|
|||||||
3
examples/material-UI-enhanced-table/.babelrc
Normal file
3
examples/material-UI-enhanced-table/.babelrc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"presets": ["react-app"]
|
||||||
|
}
|
||||||
1
examples/material-UI-enhanced-table/.env
Normal file
1
examples/material-UI-enhanced-table/.env
Normal file
@ -0,0 +1 @@
|
|||||||
|
SKIP_PREFLIGHT_CHECK=true
|
||||||
7
examples/material-UI-enhanced-table/.eslintrc
Normal file
7
examples/material-UI-enhanced-table/.eslintrc
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": ["react-app", "prettier"],
|
||||||
|
"rules": {
|
||||||
|
// "eqeqeq": 0,
|
||||||
|
// "jsx-a11y/anchor-is-valid": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
23
examples/material-UI-enhanced-table/.gitignore
vendored
Normal file
23
examples/material-UI-enhanced-table/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
29
examples/material-UI-enhanced-table/.rescriptsrc.js
Normal file
29
examples/material-UI-enhanced-table/.rescriptsrc.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
const path = require('path')
|
||||||
|
const resolveFrom = require('resolve-from')
|
||||||
|
|
||||||
|
const fixLinkedDependencies = config => {
|
||||||
|
config.resolve = {
|
||||||
|
...config.resolve,
|
||||||
|
alias: {
|
||||||
|
...config.resolve.alias,
|
||||||
|
react$: resolveFrom(path.resolve('node_modules'), 'react'),
|
||||||
|
'react-dom$': resolveFrom(path.resolve('node_modules'), 'react-dom'),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
const includeSrcDirectory = config => {
|
||||||
|
config.resolve = {
|
||||||
|
...config.resolve,
|
||||||
|
modules: [path.resolve('src'), ...config.resolve.modules],
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
['use-babel-config', '.babelrc'],
|
||||||
|
['use-eslint-config', '.eslintrc'],
|
||||||
|
fixLinkedDependencies,
|
||||||
|
// includeSrcDirectory,
|
||||||
|
]
|
||||||
8
examples/material-UI-enhanced-table/README.md
Normal file
8
examples/material-UI-enhanced-table/README.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app) and Rescripts.
|
||||||
|
|
||||||
|
You can:
|
||||||
|
|
||||||
|
- [Open this example in a new CodeSandbox](https://codesandbox.io/s/github/tannerlinsley/react-table/tree/master/examples/material-UI-enhanced-table)
|
||||||
|
- `yarn` and `yarn start` to run and edit the example
|
||||||
|
|
||||||
|
It demonstrates client side pagination, sorting, global search, add row, and delete row.
|
||||||
37
examples/material-UI-enhanced-table/package.json
Normal file
37
examples/material-UI-enhanced-table/package.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "rescripts start",
|
||||||
|
"build": "rescripts build",
|
||||||
|
"test": "rescripts test",
|
||||||
|
"eject": "rescripts eject"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@material-ui/core": "^4.8.3",
|
||||||
|
"@material-ui/icons": "^4.5.1",
|
||||||
|
"namor": "^1.1.2",
|
||||||
|
"react": "^16.8.6",
|
||||||
|
"react-dom": "^16.8.6",
|
||||||
|
"react-scripts": "3.0.1",
|
||||||
|
"react-table": "latest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rescripts/cli": "^0.0.11",
|
||||||
|
"@rescripts/rescript-use-babel-config": "^0.0.8",
|
||||||
|
"@rescripts/rescript-use-eslint-config": "^0.0.9",
|
||||||
|
"babel-eslint": "10.0.1",
|
||||||
|
"eslint-config-prettier": "^6.3.0"
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
examples/material-UI-enhanced-table/public/favicon.ico
Normal file
BIN
examples/material-UI-enhanced-table/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
38
examples/material-UI-enhanced-table/public/index.html
Normal file
38
examples/material-UI-enhanced-table/public/index.html
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
15
examples/material-UI-enhanced-table/public/manifest.json
Normal file
15
examples/material-UI-enhanced-table/public/manifest.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
||||||
77
examples/material-UI-enhanced-table/src/App.js
Normal file
77
examples/material-UI-enhanced-table/src/App.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import CssBaseline from '@material-ui/core/CssBaseline'
|
||||||
|
import EnhancedTable from './components/EnhancedTable'
|
||||||
|
import makeData from './makeData'
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
const columns = React.useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
Header: 'First Name',
|
||||||
|
accessor: 'firstName',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Last Name',
|
||||||
|
accessor: 'lastName',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Age',
|
||||||
|
accessor: 'age',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Visits',
|
||||||
|
accessor: 'visits',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Status',
|
||||||
|
accessor: 'status',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Profile Progress',
|
||||||
|
accessor: 'progress',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const [data, setData] = React.useState(React.useMemo(() => makeData(20), []))
|
||||||
|
const [skipPageReset, setSkipPageReset] = React.useState(false)
|
||||||
|
|
||||||
|
// We need to keep the table from resetting the pageIndex when we
|
||||||
|
// Update data. So we can keep track of that flag with a ref.
|
||||||
|
|
||||||
|
// When our cell renderer calls updateMyData, we'll use
|
||||||
|
// the rowIndex, columnId and new value to update the
|
||||||
|
// original data
|
||||||
|
const updateMyData = (rowIndex, columnId, value) => {
|
||||||
|
// We also turn on the flag to not reset the page
|
||||||
|
setSkipPageReset(true)
|
||||||
|
setData(old =>
|
||||||
|
old.map((row, index) => {
|
||||||
|
if (index === rowIndex) {
|
||||||
|
return {
|
||||||
|
...old[rowIndex],
|
||||||
|
[columnId]: value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return row
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CssBaseline />
|
||||||
|
<EnhancedTable
|
||||||
|
columns={columns}
|
||||||
|
data={data}
|
||||||
|
setData={setData}
|
||||||
|
updateMyData={updateMyData}
|
||||||
|
skipPageReset={skipPageReset}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
9
examples/material-UI-enhanced-table/src/App.test.js
Normal file
9
examples/material-UI-enhanced-table/src/App.test.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import App from './App'
|
||||||
|
|
||||||
|
it('renders without crashing', () => {
|
||||||
|
const div = document.createElement('div')
|
||||||
|
ReactDOM.render(<App />, div)
|
||||||
|
ReactDOM.unmountComponentAtNode(div)
|
||||||
|
})
|
||||||
@ -0,0 +1,152 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
import AddIcon from '@material-ui/icons/Add'
|
||||||
|
import Button from '@material-ui/core/Button'
|
||||||
|
import Dialog from '@material-ui/core/Dialog'
|
||||||
|
import DialogActions from '@material-ui/core/DialogActions'
|
||||||
|
import DialogContent from '@material-ui/core/DialogContent'
|
||||||
|
import DialogContentText from '@material-ui/core/DialogContentText'
|
||||||
|
import DialogTitle from '@material-ui/core/DialogTitle'
|
||||||
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import Switch from '@material-ui/core/Switch'
|
||||||
|
import TextField from '@material-ui/core/TextField'
|
||||||
|
import Tooltip from '@material-ui/core/Tooltip'
|
||||||
|
|
||||||
|
const initialUser = {
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
age: 0,
|
||||||
|
visits: 0,
|
||||||
|
status: 'single',
|
||||||
|
progress: 0,
|
||||||
|
subRows: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddUserDialog = props => {
|
||||||
|
const [user, setUser] = useState(initialUser)
|
||||||
|
const { addUserHandler } = props
|
||||||
|
const [open, setOpen] = React.useState(false)
|
||||||
|
|
||||||
|
const [switchState, setSwitchState] = React.useState({
|
||||||
|
addMultiple: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSwitchChange = name => event => {
|
||||||
|
setSwitchState({ ...switchState, [name]: event.target.checked })
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetSwitch = () => {
|
||||||
|
setSwitchState({ addMultiple: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClickOpen = () => {
|
||||||
|
setOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false)
|
||||||
|
resetSwitch()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = event => {
|
||||||
|
addUserHandler(user)
|
||||||
|
setUser(initialUser)
|
||||||
|
switchState.addMultiple ? setOpen(true) : setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = name => ({ target: { value } }) => {
|
||||||
|
setUser({ ...user, [name]: value })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Tooltip title="Add">
|
||||||
|
<IconButton aria-label="add" onClick={handleClickOpen}>
|
||||||
|
<AddIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
aria-labelledby="form-dialog-title"
|
||||||
|
>
|
||||||
|
<DialogTitle id="form-dialog-title">Add User</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>Demo add item to react table.</DialogContentText>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
label="First Name"
|
||||||
|
type="text"
|
||||||
|
fullWidth
|
||||||
|
value={user.firstName}
|
||||||
|
onChange={handleChange('firstName')}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="dense"
|
||||||
|
label="Last Name"
|
||||||
|
type="text"
|
||||||
|
fullWidth
|
||||||
|
value={user.lastName}
|
||||||
|
onChange={handleChange('lastName')}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="dense"
|
||||||
|
label="Age"
|
||||||
|
type="number"
|
||||||
|
fullWidth
|
||||||
|
value={user.age}
|
||||||
|
onChange={handleChange('age')}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="dense"
|
||||||
|
label="Visits"
|
||||||
|
type="number"
|
||||||
|
fullWidth
|
||||||
|
value={user.visits}
|
||||||
|
onChange={handleChange('visits')}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="dense"
|
||||||
|
label="Status"
|
||||||
|
type="text"
|
||||||
|
fullWidth
|
||||||
|
value={user.status}
|
||||||
|
onChange={handleChange('status')}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="dense"
|
||||||
|
label="Profile Progress"
|
||||||
|
type="number"
|
||||||
|
fullWidth
|
||||||
|
value={user.progress}
|
||||||
|
onChange={handleChange('progress')}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Tooltip title="Add multiple">
|
||||||
|
<Switch
|
||||||
|
checked={switchState.addMultiple}
|
||||||
|
onChange={handleSwitchChange('addMultiple')}
|
||||||
|
value="addMultiple"
|
||||||
|
inputProps={{ 'aria-label': 'secondary checkbox' }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Button onClick={handleClose} color="primary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleAdd} color="primary">
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AddUserDialog.propTypes = {
|
||||||
|
addUserHandler: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddUserDialog
|
||||||
@ -0,0 +1,274 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import Checkbox from '@material-ui/core/Checkbox'
|
||||||
|
import MaUTable from '@material-ui/core/Table'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import TableBody from '@material-ui/core/TableBody'
|
||||||
|
import TableCell from '@material-ui/core/TableCell'
|
||||||
|
import TableContainer from '@material-ui/core/TableContainer'
|
||||||
|
import TableFooter from '@material-ui/core/TableFooter'
|
||||||
|
import TableHead from '@material-ui/core/TableHead'
|
||||||
|
import TablePagination from '@material-ui/core/TablePagination'
|
||||||
|
import TablePaginationActions from './TablePaginationActions'
|
||||||
|
import TableRow from '@material-ui/core/TableRow'
|
||||||
|
import TableSortLabel from '@material-ui/core/TableSortLabel'
|
||||||
|
import TableToolbar from './TableToolbar'
|
||||||
|
import {
|
||||||
|
useGlobalFilter,
|
||||||
|
usePagination,
|
||||||
|
useRowSelect,
|
||||||
|
useSortBy,
|
||||||
|
useTable,
|
||||||
|
} from 'react-table'
|
||||||
|
|
||||||
|
const IndeterminateCheckbox = React.forwardRef(
|
||||||
|
({ indeterminate, ...rest }, ref) => {
|
||||||
|
const defaultRef = React.useRef()
|
||||||
|
const resolvedRef = ref || defaultRef
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
resolvedRef.current.indeterminate = indeterminate
|
||||||
|
}, [resolvedRef, indeterminate])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Checkbox ref={resolvedRef} {...rest} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const inputStyle = {
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
border: 0,
|
||||||
|
background: 'transparent',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an editable cell renderer
|
||||||
|
const EditableCell = ({
|
||||||
|
cell: { value: initialValue },
|
||||||
|
row: { index },
|
||||||
|
column: { id },
|
||||||
|
updateMyData, // This is a custom function that we supplied to our table instance
|
||||||
|
}) => {
|
||||||
|
// We need to keep and update the state of the cell normally
|
||||||
|
const [value, setValue] = React.useState(initialValue)
|
||||||
|
|
||||||
|
const onChange = e => {
|
||||||
|
setValue(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll only update the external data when the input is blurred
|
||||||
|
const onBlur = () => {
|
||||||
|
updateMyData(index, id, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the initialValue is changed externall, sync it up with our state
|
||||||
|
React.useEffect(() => {
|
||||||
|
setValue(initialValue)
|
||||||
|
}, [initialValue])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
style={inputStyle}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
onBlur={onBlur}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
EditableCell.propTypes = {
|
||||||
|
cell: PropTypes.shape({
|
||||||
|
value: PropTypes.any.isRequired,
|
||||||
|
}),
|
||||||
|
row: PropTypes.shape({
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
|
}),
|
||||||
|
column: PropTypes.shape({
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
}),
|
||||||
|
updateMyData: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set our editable cell renderer as the default Cell renderer
|
||||||
|
const defaultColumn = {
|
||||||
|
Cell: EditableCell,
|
||||||
|
}
|
||||||
|
|
||||||
|
const EnhancedTable = ({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
setData,
|
||||||
|
updateMyData,
|
||||||
|
skipPageReset,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
getTableProps,
|
||||||
|
headerGroups,
|
||||||
|
prepareRow,
|
||||||
|
page,
|
||||||
|
gotoPage,
|
||||||
|
setPageSize,
|
||||||
|
preGlobalFilteredRows,
|
||||||
|
setGlobalFilter,
|
||||||
|
state: { pageIndex, pageSize, selectedRowIds, globalFilter },
|
||||||
|
} = useTable(
|
||||||
|
{
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
defaultColumn,
|
||||||
|
autoResetPage: !skipPageReset,
|
||||||
|
// updateMyData isn't part of the API, but
|
||||||
|
// anything we put into these options will
|
||||||
|
// automatically be available on the instance.
|
||||||
|
// That way we can call this function from our
|
||||||
|
// cell renderer!
|
||||||
|
updateMyData,
|
||||||
|
},
|
||||||
|
useGlobalFilter,
|
||||||
|
useSortBy,
|
||||||
|
usePagination,
|
||||||
|
useRowSelect,
|
||||||
|
hooks => {
|
||||||
|
hooks.allColumns.push(columns => [
|
||||||
|
// Let's make a column for selection
|
||||||
|
{
|
||||||
|
id: 'selection',
|
||||||
|
// The header can use the table's getToggleAllRowsSelectedProps method
|
||||||
|
// to render a checkbox. Pagination is a problem since this will select all
|
||||||
|
// rows even though not all rows are on the current page. The solution should
|
||||||
|
// be server side pagination. For one, the clients should not download all
|
||||||
|
// rows in most cases. The client should only download data for the current page.
|
||||||
|
// In that case, getToggleAllRowsSelectedProps works fine.
|
||||||
|
Header: ({ getToggleAllRowsSelectedProps }) => (
|
||||||
|
<div>
|
||||||
|
<IndeterminateCheckbox {...getToggleAllRowsSelectedProps()} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
// The cell can use the individual row's getToggleRowSelectedProps method
|
||||||
|
// to the render a checkbox
|
||||||
|
Cell: ({ row }) => (
|
||||||
|
<div>
|
||||||
|
<IndeterminateCheckbox {...row.getToggleRowSelectedProps()} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
...columns,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleChangePage = (event, newPage) => {
|
||||||
|
gotoPage(newPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChangeRowsPerPage = event => {
|
||||||
|
setPageSize(Number(event.target.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeByIndexs = (array, indexs) =>
|
||||||
|
array.filter((_, i) => !indexs.includes(i))
|
||||||
|
|
||||||
|
const deleteUserHandler = event => {
|
||||||
|
const newData = removeByIndexs(
|
||||||
|
data,
|
||||||
|
Object.keys(selectedRowIds).map(x => parseInt(x, 10))
|
||||||
|
)
|
||||||
|
setData(newData)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addUserHandler = user => {
|
||||||
|
const newData = data.concat([user])
|
||||||
|
setData(newData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the UI for your table
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<TableToolbar
|
||||||
|
numSelected={Object.keys(selectedRowIds).length}
|
||||||
|
deleteUserHandler={deleteUserHandler}
|
||||||
|
addUserHandler={addUserHandler}
|
||||||
|
preGlobalFilteredRows={preGlobalFilteredRows}
|
||||||
|
setGlobalFilter={setGlobalFilter}
|
||||||
|
globalFilter={globalFilter}
|
||||||
|
/>
|
||||||
|
<MaUTable {...getTableProps()}>
|
||||||
|
<TableHead>
|
||||||
|
{headerGroups.map(headerGroup => (
|
||||||
|
<TableRow {...headerGroup.getHeaderGroupProps()}>
|
||||||
|
{headerGroup.headers.map(column => (
|
||||||
|
<TableCell
|
||||||
|
{...(column.id === 'selection'
|
||||||
|
? column.getHeaderProps()
|
||||||
|
: column.getHeaderProps(column.getSortByToggleProps()))}
|
||||||
|
>
|
||||||
|
{column.render('Header')}
|
||||||
|
{column.id !== 'selection' ? (
|
||||||
|
<TableSortLabel
|
||||||
|
active={column.isSorted}
|
||||||
|
// react-table has a unsorted state which is not treated here
|
||||||
|
direction={column.isSortedDesc ? 'desc' : 'asc'}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{page.map((row, i) => {
|
||||||
|
prepareRow(row)
|
||||||
|
return (
|
||||||
|
<TableRow {...row.getRowProps()}>
|
||||||
|
{row.cells.map(cell => {
|
||||||
|
return (
|
||||||
|
<TableCell {...cell.getCellProps()}>
|
||||||
|
{cell.render('Cell')}
|
||||||
|
</TableCell>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
|
||||||
|
<TableFooter>
|
||||||
|
<TableRow>
|
||||||
|
<TablePagination
|
||||||
|
rowsPerPageOptions={[
|
||||||
|
5,
|
||||||
|
10,
|
||||||
|
25,
|
||||||
|
{ label: 'All', value: data.length },
|
||||||
|
]}
|
||||||
|
colSpan={3}
|
||||||
|
count={data.length}
|
||||||
|
rowsPerPage={pageSize}
|
||||||
|
page={pageIndex}
|
||||||
|
SelectProps={{
|
||||||
|
inputProps: { 'aria-label': 'rows per page' },
|
||||||
|
native: true,
|
||||||
|
}}
|
||||||
|
onChangePage={handleChangePage}
|
||||||
|
onChangeRowsPerPage={handleChangeRowsPerPage}
|
||||||
|
ActionsComponent={TablePaginationActions}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
</TableFooter>
|
||||||
|
</MaUTable>
|
||||||
|
</TableContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
EnhancedTable.propTypes = {
|
||||||
|
columns: PropTypes.array.isRequired,
|
||||||
|
data: PropTypes.array.isRequired,
|
||||||
|
updateMyData: PropTypes.func.isRequired,
|
||||||
|
setData: PropTypes.func.isRequired,
|
||||||
|
skipPageReset: PropTypes.bool.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EnhancedTable
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import InputBase from '@material-ui/core/InputBase'
|
||||||
|
import { fade, makeStyles } from '@material-ui/core/styles'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import SearchIcon from '@material-ui/icons/Search'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(theme => ({
|
||||||
|
search: {
|
||||||
|
position: 'relative',
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
backgroundColor: fade(theme.palette.common.white, 0.15),
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: fade(theme.palette.common.white, 0.25),
|
||||||
|
},
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
marginLeft: 0,
|
||||||
|
width: '100%',
|
||||||
|
[theme.breakpoints.up('sm')]: {
|
||||||
|
marginLeft: theme.spacing(3),
|
||||||
|
width: 'auto',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
searchIcon: {
|
||||||
|
width: theme.spacing(7),
|
||||||
|
height: '100%',
|
||||||
|
position: 'absolute',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
inputRoot: {
|
||||||
|
color: 'inherit',
|
||||||
|
},
|
||||||
|
inputInput: {
|
||||||
|
padding: theme.spacing(1, 1, 1, 7),
|
||||||
|
transition: theme.transitions.create('width'),
|
||||||
|
width: '100%',
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const GlobalFilter = ({
|
||||||
|
preGlobalFilteredRows,
|
||||||
|
globalFilter,
|
||||||
|
setGlobalFilter,
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
const count = preGlobalFilteredRows.length
|
||||||
|
|
||||||
|
// Global filter only works with pagination from the first page.
|
||||||
|
// This may not be a problem for server side pagination when
|
||||||
|
// only the current page is downloaded.
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.search}>
|
||||||
|
<div className={classes.searchIcon}>
|
||||||
|
<SearchIcon />
|
||||||
|
</div>
|
||||||
|
<InputBase
|
||||||
|
value={globalFilter || ''}
|
||||||
|
onChange={e => {
|
||||||
|
setGlobalFilter(e.target.value || undefined) // Set undefined to remove the filter entirely
|
||||||
|
}}
|
||||||
|
placeholder={`${count} records...`}
|
||||||
|
classes={{
|
||||||
|
root: classes.inputRoot,
|
||||||
|
input: classes.inputInput,
|
||||||
|
}}
|
||||||
|
inputProps={{ 'aria-label': 'search' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalFilter.propTypes = {
|
||||||
|
preGlobalFilteredRows: PropTypes.array.isRequired,
|
||||||
|
globalFilter: PropTypes.string.isRequired,
|
||||||
|
setGlobalFilter: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GlobalFilter
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import FirstPageIcon from '@material-ui/icons/FirstPage'
|
||||||
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft'
|
||||||
|
import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight'
|
||||||
|
import LastPageIcon from '@material-ui/icons/LastPage'
|
||||||
|
import { makeStyles, useTheme } from '@material-ui/core/styles'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(theme => ({
|
||||||
|
root: {
|
||||||
|
flexShrink: 0,
|
||||||
|
marginLeft: theme.spacing(2.5),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const TablePaginationActions = props => {
|
||||||
|
const classes = useStyles()
|
||||||
|
const theme = useTheme()
|
||||||
|
const { count, page, rowsPerPage, onChangePage } = props
|
||||||
|
|
||||||
|
const handleFirstPageButtonClick = event => {
|
||||||
|
onChangePage(event, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBackButtonClick = event => {
|
||||||
|
onChangePage(event, page - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNextButtonClick = event => {
|
||||||
|
onChangePage(event, page + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLastPageButtonClick = event => {
|
||||||
|
onChangePage(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleFirstPageButtonClick}
|
||||||
|
disabled={page === 0}
|
||||||
|
aria-label="first page"
|
||||||
|
>
|
||||||
|
{theme.direction === 'rtl' ? <LastPageIcon /> : <FirstPageIcon />}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleBackButtonClick}
|
||||||
|
disabled={page === 0}
|
||||||
|
aria-label="previous page"
|
||||||
|
>
|
||||||
|
{theme.direction === 'rtl' ? (
|
||||||
|
<KeyboardArrowRight />
|
||||||
|
) : (
|
||||||
|
<KeyboardArrowLeft />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleNextButtonClick}
|
||||||
|
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||||
|
aria-label="next page"
|
||||||
|
>
|
||||||
|
{theme.direction === 'rtl' ? (
|
||||||
|
<KeyboardArrowLeft />
|
||||||
|
) : (
|
||||||
|
<KeyboardArrowRight />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleLastPageButtonClick}
|
||||||
|
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||||
|
aria-label="last page"
|
||||||
|
>
|
||||||
|
{theme.direction === 'rtl' ? <FirstPageIcon /> : <LastPageIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TablePaginationActions.propTypes = {
|
||||||
|
count: PropTypes.number.isRequired,
|
||||||
|
onChangePage: PropTypes.func.isRequired,
|
||||||
|
page: PropTypes.number.isRequired,
|
||||||
|
rowsPerPage: PropTypes.number.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TablePaginationActions
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import AddUserDialog from './AddUserDialog'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import DeleteIcon from '@material-ui/icons/Delete'
|
||||||
|
import GlobalFilter from './GlobalFilter'
|
||||||
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
|
import { lighten, makeStyles } from '@material-ui/core/styles'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import Toolbar from '@material-ui/core/Toolbar'
|
||||||
|
import Typography from '@material-ui/core/Typography'
|
||||||
|
import Tooltip from '@material-ui/core/Tooltip'
|
||||||
|
|
||||||
|
const useToolbarStyles = makeStyles(theme => ({
|
||||||
|
root: {
|
||||||
|
paddingLeft: theme.spacing(2),
|
||||||
|
paddingRight: theme.spacing(1),
|
||||||
|
},
|
||||||
|
highlight:
|
||||||
|
theme.palette.type === 'light'
|
||||||
|
? {
|
||||||
|
color: theme.palette.secondary.main,
|
||||||
|
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
backgroundColor: theme.palette.secondary.dark,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
flex: '1 1 100%',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const TableToolbar = props => {
|
||||||
|
const classes = useToolbarStyles()
|
||||||
|
const {
|
||||||
|
numSelected,
|
||||||
|
addUserHandler,
|
||||||
|
deleteUserHandler,
|
||||||
|
preGlobalFilteredRows,
|
||||||
|
setGlobalFilter,
|
||||||
|
globalFilter,
|
||||||
|
} = props
|
||||||
|
return (
|
||||||
|
<Toolbar
|
||||||
|
className={clsx(classes.root, {
|
||||||
|
[classes.highlight]: numSelected > 0,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<AddUserDialog addUserHandler={addUserHandler} />
|
||||||
|
{numSelected > 0 ? (
|
||||||
|
<Typography
|
||||||
|
className={classes.title}
|
||||||
|
color="inherit"
|
||||||
|
variant="subtitle1"
|
||||||
|
>
|
||||||
|
{numSelected} selected
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Typography className={classes.title} variant="h6" id="tableTitle">
|
||||||
|
Users
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{numSelected > 0 ? (
|
||||||
|
<Tooltip title="Delete">
|
||||||
|
<IconButton aria-label="delete" onClick={deleteUserHandler}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<GlobalFilter
|
||||||
|
preGlobalFilteredRows={preGlobalFilteredRows}
|
||||||
|
globalFilter={globalFilter}
|
||||||
|
setGlobalFilter={setGlobalFilter}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Toolbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TableToolbar.propTypes = {
|
||||||
|
numSelected: PropTypes.number.isRequired,
|
||||||
|
addUserHandler: PropTypes.func.isRequired,
|
||||||
|
deleteUserHandler: PropTypes.func.isRequired,
|
||||||
|
setGlobalFilter: PropTypes.func.isRequired,
|
||||||
|
preGlobalFilteredRows: PropTypes.array.isRequired,
|
||||||
|
globalFilter: PropTypes.string.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TableToolbar
|
||||||
13
examples/material-UI-enhanced-table/src/index.css
Normal file
13
examples/material-UI-enhanced-table/src/index.css
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
6
examples/material-UI-enhanced-table/src/index.js
Normal file
6
examples/material-UI-enhanced-table/src/index.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App'
|
||||||
|
|
||||||
|
ReactDOM.render(<App />, document.getElementById('root'))
|
||||||
40
examples/material-UI-enhanced-table/src/makeData.js
Normal file
40
examples/material-UI-enhanced-table/src/makeData.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import namor from 'namor'
|
||||||
|
|
||||||
|
const range = len => {
|
||||||
|
const arr = []
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
arr.push(i)
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPerson = () => {
|
||||||
|
const statusChance = Math.random()
|
||||||
|
return {
|
||||||
|
firstName: namor.generate({ words: 1, numbers: 0 }),
|
||||||
|
lastName: namor.generate({ words: 1, numbers: 0 }),
|
||||||
|
age: Math.floor(Math.random() * 30),
|
||||||
|
visits: Math.floor(Math.random() * 100),
|
||||||
|
progress: Math.floor(Math.random() * 100),
|
||||||
|
status:
|
||||||
|
statusChance > 0.66
|
||||||
|
? 'relationship'
|
||||||
|
: statusChance > 0.33
|
||||||
|
? 'complicated'
|
||||||
|
: 'single',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function makeData(...lens) {
|
||||||
|
const makeDataLevel = (depth = 0) => {
|
||||||
|
const len = lens[depth]
|
||||||
|
return range(len).map(d => {
|
||||||
|
return {
|
||||||
|
...newPerson(),
|
||||||
|
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeDataLevel()
|
||||||
|
}
|
||||||
10448
examples/material-UI-enhanced-table/yarn.lock
Normal file
10448
examples/material-UI-enhanced-table/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user