├── .eslintignore
├── .eslintrc
├── .github
├── CODEOWNERS
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── feature_request.md
│ └── help.md
├── PUBLISHING.md
├── README_ORIGINAL.md
├── ROADMAP.md
├── actions
│ └── setup-node
├── pull_request_template.md
├── stale.yml
└── workflows
│ ├── build.yml
│ ├── codeql-analysis.yml
│ ├── publish-next.yml
│ └── publish.yml
├── .gitignore
├── .prettierrc
├── .slugignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── __tests__
├── demo
│ ├── demo-components
│ │ ├── EditableRowDateColumnIssue
│ │ │ └── index.js
│ │ ├── RemoteData
│ │ │ └── index.js
│ │ └── index.js
│ ├── demo.js
│ ├── demo.original.js
│ ├── errorBoundary.js
│ ├── index.html
│ └── webpack.config.js
├── detailPanel.test.js
├── disableSort.test.js
├── editRow.test.js
├── localization.test.js
├── multiColumnSort.test.js
├── post.build.test.js
├── pre.build.test.js
├── resizeColumn.test.js
├── selectByObject.test.js
├── selection.test.js
├── showPaginationButtons.test.js
├── summaryRow.test.js
├── test.helper.js
└── useDoubleClick.test.js
├── babel.config.js
├── esbuild.config.js
├── jest.config.js
├── package-lock.json
├── package.json
├── src
├── components
│ ├── Container
│ │ └── index.js
│ ├── MTableAction
│ │ └── index.js
│ ├── MTableActions
│ │ └── index.js
│ ├── MTableBodyRow
│ │ └── index.js
│ ├── MTableCell
│ │ ├── cellUtils.js
│ │ └── index.js
│ ├── MTableCustomIcon
│ │ └── index.js
│ ├── MTableEditCell
│ │ └── index.js
│ ├── MTableEditField
│ │ ├── BooleanField.js
│ │ ├── CurrencyField.js
│ │ ├── DateField.js
│ │ ├── DateTimeField.js
│ │ ├── LookupField.js
│ │ ├── TextField.js
│ │ ├── TimeField.js
│ │ └── index.js
│ ├── MTableEditRow
│ │ ├── index.js
│ │ └── m-table-edit-row.js
│ ├── MTableFilterRow
│ │ ├── BooleanFilter.js
│ │ ├── DateFilter.js
│ │ ├── DefaultFilter.js
│ │ ├── Filter.js
│ │ ├── LookupFilter.js
│ │ ├── index.js
│ │ └── utils.js
│ ├── MTableGroupRow
│ │ └── index.js
│ ├── MTableGroupbar
│ │ └── index.js
│ ├── MTableHeader
│ │ └── index.js
│ ├── MTablePagination
│ │ └── index.js
│ ├── MTableScrollbar
│ │ └── index.js
│ ├── MTableSteppedPaginationInner
│ │ └── index.js
│ ├── MTableSummaryRow
│ │ └── index.js
│ ├── MTableToolbar
│ │ └── index.js
│ ├── Overlay
│ │ ├── OverlayError.js
│ │ └── OverlayLoading.js
│ ├── index.js
│ ├── m-table-body.js
│ ├── m-table-detailpanel.js
│ ├── m-table-edit-cell.js
│ └── m-table-edit-field.js
├── defaults
│ ├── index.js
│ ├── props.components.js
│ ├── props.icons.js
│ ├── props.localization.js
│ └── props.options.js
├── index.js
├── material-table.js
├── prop-types.js
├── store
│ ├── LocalizationStore.js
│ └── index.js
└── utils
│ ├── common-values.js
│ ├── constants.js
│ ├── data-manager.js
│ ├── hooks
│ └── useDoubleClick.js
│ ├── index.js
│ └── validate.js
└── types
├── helper.d.ts
└── index.d.ts
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | types
3 | .github
4 | demo
5 | dist
6 | __tests__
7 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["standard", "eslint:recommended", "plugin:react/recommended"],
3 | "env": {
4 | "browser": true
5 | },
6 | "rules": {
7 | "multiline-ternary": "off",
8 | "react/prop-types": "off",
9 | "indent": "off",
10 | "one-var": "off",
11 | "semi": [
12 | "error",
13 | "always",
14 | {
15 | "omitLastInOneLineBlock": true
16 | }
17 | ],
18 | "space-before-function-paren": "off"
19 | },
20 | "parser": "babel-eslint",
21 | "plugins": [
22 | /**
23 | *
24 | * Only warn for EVERYTHING (for now)
25 | *
26 | */
27 | "only-warn"
28 | ],
29 | "settings": {
30 | "react": {
31 | "version": "detect"
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @domino051
2 | * @oze4
3 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at mehmetbaran@mehmetbaran.net. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # [Publishing](https://github.com/material-table-core/core/blob/master/.github/PUBLISHING.md)
2 |
3 | Please click the link above to see more about how to publish to NPM.
4 |
5 | # Contributing
6 |
7 | 1. Fork the repo
8 | 2. Create your feature or bug branch : `git checkout -b feature/my-new-feature`
9 | 3. Commit your changes: `git commit -m 'Add some feature'`
10 | 4. Push to the branch: `git push origin feature/my-new-feature`
11 |
12 | **After your pull request is merged**, you can safely delete your branch.
13 |
14 | ### [<- Back](https://github.com/material-table-core/core/)
15 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: material-table-core
2 | open_collective: material-table-core
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | labels: 'bug'
5 | ---
6 |
7 | # Guidelines
8 |
9 | - #### Please include a demo of the issue/behavior/question you have
10 |
11 | - #### Please try to be as detailed as possible
12 |
13 | - #### You may fork one of the following starter templates if you would like:
14 |
15 | - #### [CodeSandbox mui v4](https://codesandbox.io/s/material-table-starter-template-forked-q85qi?file=/src/index.js)
16 | - #### [CodeSandbox mui v5](https://codesandbox.io/s/material-table-starter-template-forked-jlrfld)
17 | - #### [StackBlitz](https://stackblitz.com/edit/material-table-starter-template)
18 |
19 | **Describe the bug**
20 | A clear and concise description of what the bug is.
21 |
22 | **To Reproduce**
23 | Steps to reproduce the behavior:
24 |
25 | 1. Go to '...'
26 | 2. Click on '....'
27 | 3. Scroll down to '....'
28 | 4. See error
29 |
30 | **Expected behavior**
31 | A clear and concise description of what you expected to happen.
32 |
33 | **Screenshots**
34 | If applicable, add screenshots to help explain your problem.
35 |
36 | **Desktop (please complete the following information):**
37 |
38 | - OS: [e.g. iOS]
39 | - Browser [e.g. chrome, safari]
40 | - Version [e.g. 22]
41 |
42 | **Smartphone (please complete the following information):**
43 |
44 | - Device: [e.g. iPhone6]
45 | - OS: [e.g. iOS8.1]
46 | - Browser [e.g. stock browser, safari]
47 | - Version [e.g. 22]
48 |
49 | **Additional context**
50 | Add any other context about the problem here.
51 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | labels: "feature"
5 | ---
6 |
7 | # Guidelines
8 |
9 | - #### Please include a demo of the issue/behavior/question you have
10 |
11 | - #### Please try to be as detailed as possible
12 |
13 | - #### You may fork one of the following starter templates if you would like:
14 |
15 | - #### [CodeSandbox](https://codesandbox.io/s/material-table-starter-template-xnfpo)
16 | - #### [StackBlitz](https://stackblitz.com/edit/material-table-starter-template)
17 |
18 | **Is your feature request related to a problem? Please describe.**
19 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
20 |
21 | **Describe the solution you'd like**
22 | A clear and concise description of what you want to happen.
23 |
24 | **Describe alternatives you've considered**
25 | A clear and concise description of any alternative solutions or features you've considered.
26 |
27 | **Additional context**
28 | Add any other context or screenshots about the feature request here.
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/help.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Help
3 | about: Help request about material-table
4 | labels: "question"
5 | ---
6 |
7 | # Guidelines
8 |
9 | - #### Please include a demo of the issue/behavior/question you have
10 |
11 | - #### Please try to be as detailed as possible
12 |
13 | - #### You may fork one of the following starter templates if you would like:
14 |
15 | - #### [CodeSandbox](https://codesandbox.io/s/material-table-starter-template-xnfpo)
16 | - #### [StackBlitz](https://stackblitz.com/edit/material-table-starter-template)
17 |
--------------------------------------------------------------------------------
/.github/PUBLISHING.md:
--------------------------------------------------------------------------------
1 | ### Important Info On Setup
2 |
3 | We have 2 GitHub actions that run whenever your commit message starts with `Release v` (as in `Release v9.34.83`). _It is important to note that the commit message must start with `Release v` to trigger these GitHub Actions!_
4 |
5 | - GH Actions use stored NPM API key/secret within [Secrets](https://github.com/material-table-core/core/settings/secrets/actions)
6 | - The 2 GH actions do the following:
7 | 1. [Publishes to NPM](https://github.com/material-table-core/core/blob/master/.github/workflows/publish.yml#L21)
8 | 2. [Creates a GitHub release](https://github.com/material-table-core/core/blob/master/.github/workflows/publish.yml#L36)
9 |
10 | # Publishing
11 |
12 | - Publish new **major** version
13 | - `npm run release:major`
14 | - Publish new **minor** version
15 | - `npm run release:minor`
16 | - Publish new **patch** version
17 | - `npm run release:patch`
18 |
--------------------------------------------------------------------------------
/.github/ROADMAP.md:
--------------------------------------------------------------------------------
1 |
5 |
10 |
15 |
16 |
17 |
18 |
19 | 🛣️ Material Table Roadmap 🛣️
20 |
21 |
22 |
23 | last updated: June 6, 2020
24 |
25 |
26 |
27 |
28 |
29 | Currently, we would like to gain control of open issues and smaller pull requests. Once we have control of issues and PR's we will begin to entertain new features and enhancements.
30 |
31 |
32 |
33 |
34 | ---
35 |
36 |
37 | 🚧 Help Wanted 🚧
38 |
39 |
40 |
41 |
42 | If you have some free time and would like to contribute, we always welcome help with open issues and/or pull requests to help resolve open issues.
43 |
44 |
45 |
--------------------------------------------------------------------------------
/.github/actions/setup-node:
--------------------------------------------------------------------------------
1 | strategy:
2 | matrix:
3 | node-version: [8.16.2, 10.17.0]
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Related Issue
2 |
3 | Relate the Github issue with this PR using `#`
4 |
5 | ## Description
6 |
7 | Simple words to describe the overall goals of the pull request's commits.
8 |
9 | ## Related PRs
10 |
11 | List related PRs against other branches:
12 |
13 | | branch | PR |
14 | | ------------------- | -------- |
15 | | other_pr_production | [link]() |
16 | | other_pr_master | [link]() |
17 |
18 | ## Impacted Areas in Application
19 |
20 | List general components of the application that this PR will affect:
21 |
22 | \*
23 |
24 | ## Additional Notes
25 |
26 | This is optional, feel free to follow your heart and write here :)
27 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 90
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 7
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - pinned
8 | - security
9 | # Label to use when marking an issue as stale
10 | staleLabel: wontfix
11 | # Comment to post when marking an issue as stale. Set to `false` to disable
12 | markComment: >
13 | This issue has been automatically marked as stale because it has not had
14 | recent activity. It will be closed if no further activity occurs. Thank you
15 | for your contributions. You can reopen it if it required.
16 | # Comment to post when closing a stale issue. Set to `false` to disable
17 | closeComment: false
18 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | #
2 | # Should run on each push, this should include pull requests
3 | #
4 | # If the commit message starts with "skip:build" we will skip this action
5 | #
6 | # If the commit message starts with "Release " we skip build
7 | #
8 |
9 | name: Build
10 |
11 | on:
12 | push:
13 | tags:
14 | - '*'
15 |
16 | jobs:
17 | build:
18 | if: |
19 | (startsWith(github.event.head_commit.message, 'skip:build') != true) &&
20 | (startsWith(github.event.head_commit.message, 'Release ') != true)
21 | runs-on: ubuntu-latest
22 | strategy:
23 | matrix:
24 | node-version: [16.x, 18.x]
25 | steps:
26 | - uses: actions/checkout@v3
27 | - name: Build on Node.js ${{ matrix.node-version }}
28 | uses: actions/setup-node@v3
29 | with:
30 | node-version: ${{ matrix.node-version }}
31 | - run: npm install
32 | - run: npm run build
33 | - run: npm run test
34 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: 'CodeQL'
13 |
14 | on:
15 | push:
16 | branches: [master]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [master]
20 | schedule:
21 | - cron: '15 6 * * 3'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: ['javascript']
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v3
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v1
46 | with:
47 | languages: ${{ matrix.language }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
52 |
53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
54 | # If this step fails, then you should remove it and run the build manually (see below)
55 | - name: Autobuild
56 | uses: github/codeql-action/autobuild@v1
57 |
58 | # ℹ️ Command-line programs to run using the OS shell.
59 | # 📚 https://git.io/JvXDl
60 |
61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
62 | # and modify them (or add more) to build your code if your project
63 | # uses a compiled language
64 |
65 | #- run: |
66 | # make bootstrap
67 | # make release
68 |
69 | - name: Perform CodeQL Analysis
70 | uses: github/codeql-action/analyze@v1
71 |
--------------------------------------------------------------------------------
/.github/workflows/publish-next.yml:
--------------------------------------------------------------------------------
1 | name: Publish Next
2 |
3 | #
4 | #
5 | # ~ IMPORTANT ~
6 | # If your commit summary starts with `Release`
7 | # - We will attempt to automatically publish your commit (although, your commit will not be automatically approved)
8 | # - *NOTE*
9 | # - We **DO NOT** increase the package.json version please do this yourself!!
10 | # - We will attempt to create a new release using the commit description as release notes
11 | #
12 | #
13 |
14 | on:
15 | push:
16 | branches:
17 | - next
18 |
19 | jobs:
20 | publish:
21 | if: startsWith(github.event.head_commit.message, 'Release ')
22 | runs-on: ubuntu-latest
23 | steps:
24 | - uses: actions/checkout@v4
25 | - uses: actions/setup-node@v3
26 | with:
27 | node-version: 18
28 | - name: Publish
29 | run: |
30 | npm install
31 | npm run build
32 | npm config set @material-table:registry https://registry.npmjs.org/
33 | npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_AUTH_TOKEN }}
34 | npm publish --ignore-scripts --tag next
35 | create-release:
36 | needs: publish
37 | runs-on: ubuntu-latest
38 | steps:
39 | - name: Checkout code
40 | uses: actions/checkout@v4
41 | - name: Set env
42 | run: |
43 | RV=$(echo $${{ github.event.head_commit.message }} | awk '{print $NF}')
44 | echo "RELEASE_VERSION=$RV" >> $GITHUB_ENV
45 | echo $RV
46 | - name: Create Release
47 | id: create_release
48 | uses: ncipollo/release-action@v1
49 | env:
50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
51 | with:
52 | name: ${{ env.RELEASE_VERSION }}
53 | body: ${{ github.event.head_commit.message }}
54 | draft: false
55 | prerelease: false
56 | tag: ${{ env.RELEASE_VERSION }}
57 | generateReleaseNotes: true
58 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | #
4 | #
5 | # ~ IMPORTANT ~
6 | # If your commit summary starts with `Release`
7 | # - We will attempt to automatically publish your commit (although, your commit will not be automatically approved)
8 | # - *NOTE*
9 | # - We will attempt to create a new release using the commit description as release notes
10 | #
11 | #
12 |
13 | on:
14 | push:
15 | branches:
16 | - main
17 | - master
18 | - experimental
19 |
20 | jobs:
21 | publish:
22 | if: startsWith(github.event.head_commit.message, 'Release ')
23 | runs-on: ubuntu-latest
24 | steps:
25 | - uses: actions/checkout@v4
26 | - uses: actions/setup-node@v3
27 | with:
28 | node-version: 18
29 | - name: Publish
30 | run: |
31 | npm install
32 | npm run build
33 | npm run test
34 | npm config set @material-table:registry https://registry.npmjs.org/
35 | npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_AUTH_TOKEN }}
36 | npm publish --ignore-scripts
37 | create-release:
38 | needs: publish
39 | runs-on: ubuntu-latest
40 | steps:
41 | - name: Checkout code
42 | uses: actions/checkout@v3
43 | - name: Set env
44 | run: |
45 | RV=$(echo $${{ github.event.head_commit.message }} | awk '{print $NF}')
46 | echo "RELEASE_VERSION=$RV" >> $GITHUB_ENV
47 | echo $RV
48 | - name: Create Release
49 | id: create_release
50 | uses: ncipollo/release-action@v1
51 | env:
52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
53 | with:
54 | name: ${{ env.RELEASE_VERSION }}
55 | body: ${{ github.event.head_commit.message }}
56 | draft: false
57 | prerelease: false
58 | tag: ${{ env.RELEASE_VERSION }}
59 | generateReleaseNotes: true
60 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .docz
2 | .vscode
3 | dist
4 | dist_esbuild
5 | node_modules
6 | yarn.lock
7 | .idea
8 | .DS_Store
9 | *.iml
10 | cypress/videos
11 | __tests__/coverage
12 | /exporters
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "none"
4 | }
5 |
--------------------------------------------------------------------------------
/.slugignore:
--------------------------------------------------------------------------------
1 | esbuild.config.js
2 | jest.config.js
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Mehmet Baran
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # @material-table/core
4 |
5 | **A highly customizable table library built on Material UI, forked from [`mbrn/material-table`](https://material-table.com)**
6 |
7 | [](https://github.com/material-table-core/core/actions?query=workflow%3ABuild)
8 | [](https://github.com/material-table-core/core/actions?query=workflow%3APublish)
9 | [](https://www.npmjs.com/package/@material-table/core)
10 | [](https://discord.gg/uMr8pKDu8n)
11 |
12 | ---
13 |
14 | Check out our [**roadmap**](https://github.com/material-table-core/core/wiki/Roadmap) for upcoming features and improvements.
15 |
16 | 💾 [**Installation**](https://material-table-core.github.io/docs/#installation) • 🎉 [**Basic Usage**](https://material-table-core.github.io/docs/#basic-usage)
17 | ✅ [**Why this repo exists?**](https://material-table-core.github.io/docs/about) • 🚧 [**Documentation**](https://material-table-core.github.io/docs) • ⚙️ [**Demos**](https://material-table-core.github.io/demos/)
18 |
19 |
20 |
21 | ---
22 |
23 | ## 🚧 Mui V6 Support is in Progress
24 |
25 | The team is working on migrating the library to be fully compatible with Material UI V6. Stay tuned!
26 |
27 | ---
28 |
29 | ## 🛠️ Installation
30 |
31 | Install `@material-table/core` using npm or yarn:
32 |
33 | ```bash
34 | npm install @material-table/core
35 | or
36 |
37 | bash
38 | Code kopieren
39 | yarn add @material-table/core
40 | Refer to the installation guide for more information and advanced usage.
41 |
42 | 💡 Basic Usage
43 | javascript
44 | Code kopieren
45 | import MaterialTable from '@material-table/core';
46 |
47 | function MyTable() {
48 | return (
49 |
64 | );
65 | }
66 | ```
67 | # Explore more features and advanced usage in our documentation.
68 |
69 | ## 🙌 Sponsorship
70 | We appreciate contributions and sponsorships! You can support this project through:
71 |
72 | ## GitHub Sponsors
73 | Open Collective
74 | Your support helps us maintain and improve the project.
75 |
76 | ## 🚀 Contributing
77 | Thank you for considering contributing to the project! The following items are in urgent need of attention:
78 |
79 | Refactor: Replace data-manager.js with React Context.
80 | Documentation: Help us improve the docs.
81 | Tests: Implement unit tests using Jest to improve stability.
82 | We appreciate all contributions, big or small. Check out our contributing guide for more details.
83 |
--------------------------------------------------------------------------------
/__tests__/demo/demo-components/EditableRowDateColumnIssue/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MaterialTable from '../../../../src';
3 |
4 | export default function EditableTable() {
5 | const { useState } = React;
6 |
7 | const [columns, setColumns] = useState([
8 | {
9 | title: 'Date',
10 | field: 'date',
11 | type: 'date',
12 | dateSetting: { locale: 'en-US', format: 'MM/dd/yyyy' }
13 | }
14 | ]);
15 |
16 | const [data, setData] = useState([{ date: new Date(), id: 0 }]);
17 |
18 | return (
19 |
25 | new Promise((resolve, reject) => {
26 | setTimeout(() => {
27 | setData([...data, newData]);
28 |
29 | resolve();
30 | }, 1000);
31 | }),
32 | onRowUpdate: (newData, oldData) =>
33 | new Promise((resolve, reject) => {
34 | setTimeout(() => {
35 | const dataUpdate = [...data];
36 | const index = oldData.tableData.id;
37 | dataUpdate[index] = newData;
38 | setData([...dataUpdate]);
39 |
40 | resolve();
41 | }, 1000);
42 | }),
43 | onRowDelete: (oldData) =>
44 | new Promise((resolve, reject) => {
45 | setTimeout(() => {
46 | const dataDelete = [...data];
47 | const index = oldData.tableData.id;
48 | dataDelete.splice(index, 1);
49 | setData([...dataDelete]);
50 |
51 | resolve();
52 | }, 1000);
53 | })
54 | }}
55 | />
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/__tests__/demo/demo-components/RemoteData/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, Component } from 'react';
2 | import MaterialTable, { MTableBodyRow, MTableEditRow } from '../../../../src';
3 | // check if removing this.isRemoteData()@https://github.com/material-table-core/core/blob/0e953441fd9f9912d8cf97db103a8e0cb4f43912/src/material-table.js#L119-L120
4 | // is any good
5 |
6 | // https://github.com/mbrn/material-table/issues/1353 not remote data but it's where the condition was added
7 | // excerpt from https://github.com/orestes22/material-table/commit/6d708f37fa6814c749c69ed6c6e4171c79e624df
8 | export function I1353() {
9 | const tableRef = useRef();
10 | const columns = [
11 | { title: 'Adı', field: 'name', filterPlaceholder: 'Adı filter' },
12 | { title: 'Soyadı', field: 'surname', initialEditValue: 'test' },
13 | { title: 'Evli', field: 'isMarried', type: 'boolean' },
14 | { title: 'Cinsiyet', field: 'sex', disableClick: true, editable: 'onAdd' },
15 | { title: 'Tipi', field: 'type', removable: false, editable: 'never' },
16 | { title: 'Doğum Yılı', field: 'birthDate', type: 'date' },
17 | {
18 | title: 'Doğum Yeri',
19 | field: 'birthCity',
20 | lookup: { 34: 'İstanbul', 0: 'Şanlıurfa' }
21 | },
22 | { title: 'Kayıt Tarihi', field: 'insertDateTime', type: 'datetime' },
23 | { title: 'Zaman', field: 'time', type: 'time' }
24 | ];
25 | const data = [
26 | {
27 | id: 1,
28 | name: 'A1',
29 | surname: 'B',
30 | isMarried: true,
31 | birthDate: new Date(1987, 1, 1),
32 | birthCity: 0,
33 | sex: 'Male',
34 | type: 'adult',
35 | insertDateTime: new Date(2018, 1, 1, 12, 23, 44),
36 | time: new Date(1900, 1, 1, 14, 23, 35)
37 | },
38 | {
39 | id: 2,
40 | name: 'A2',
41 | surname: 'B',
42 | isMarried: false,
43 | birthDate: new Date(1987, 1, 1),
44 | birthCity: 34,
45 | sex: 'Female',
46 | type: 'adult',
47 | insertDateTime: new Date(2018, 1, 1, 12, 23, 44),
48 | time: new Date(1900, 1, 1, 14, 23, 35),
49 | parentId: 1
50 | },
51 | {
52 | id: 3,
53 | name: 'A3',
54 | surname: 'B',
55 | isMarried: true,
56 | birthDate: new Date(1987, 1, 1),
57 | birthCity: 34,
58 | sex: 'Female',
59 | type: 'child',
60 | insertDateTime: new Date(2018, 1, 1, 12, 23, 44),
61 | time: new Date(1900, 1, 1, 14, 23, 35),
62 | parentId: 1
63 | },
64 | {
65 | id: 4,
66 | name: 'A4',
67 | surname: 'Dede',
68 | isMarried: true,
69 | birthDate: new Date(1987, 1, 1),
70 | birthCity: 34,
71 | sex: 'Female',
72 | type: 'child',
73 | insertDateTime: new Date(2018, 1, 1, 12, 23, 44),
74 | time: new Date(1900, 1, 1, 14, 23, 35),
75 | parentId: 3
76 | },
77 | {
78 | id: 5,
79 | name: 'A5',
80 | surname: 'C',
81 | isMarried: false,
82 | birthDate: new Date(1987, 1, 1),
83 | birthCity: 34,
84 | sex: 'Female',
85 | type: 'child',
86 | insertDateTime: new Date(2018, 1, 1, 12, 23, 44),
87 | time: new Date(1900, 1, 1, 14, 23, 35)
88 | },
89 | {
90 | id: 6,
91 | name: 'A6',
92 | surname: 'C',
93 | isMarried: true,
94 | birthDate: new Date(1989, 1, 1),
95 | birthCity: 34,
96 | sex: 'Female',
97 | type: 'child',
98 | insertDateTime: new Date(2018, 1, 1, 12, 23, 44),
99 | time: new Date(1900, 1, 1, 14, 23, 35),
100 | parentId: 5
101 | }
102 | ];
103 | const [selectedRow, setSelectedRow] = useState(null);
104 | return (
105 | <>
106 | setSelectedRow(selectedRow)}
112 | options={{
113 | rowStyle: (rowData) => ({
114 | backgroundColor:
115 | selectedRow && selectedRow.tableData.id === rowData.tableData.id
116 | ? '#EEE'
117 | : '#FFF'
118 | })
119 | }}
120 | />
121 | tableRef.current.onAllSelected(true)}
123 | style={{ margin: 10 }}
124 | >
125 | Select
126 |
127 | >
128 | );
129 | }
130 |
131 | // https://github.com/mbrn/material-table/issues/1941
132 | export function I1941() {
133 | const [test, setTest] = useState('');
134 | const [counter, setCounter] = useState(0);
135 |
136 | const buttonClick = () => {
137 | setTest('test ' + counter);
138 | setCounter(counter + 1);
139 | };
140 |
141 | return (
142 |
143 |
(
150 |
154 | )
155 | },
156 | { title: 'Id', field: 'id' },
157 | { title: 'First Name', field: 'first_name' },
158 | { title: 'Last Name', field: 'last_name' }
159 | ]}
160 | data={(query) =>
161 | new Promise((resolve, reject) => {
162 | let url = 'https://reqres.in/api/users?';
163 | url += 'per_page=' + query.pageSize;
164 | url += '&page=' + (query.page + 1);
165 | fetch(url)
166 | .then((response) => response.json())
167 | .then((result) => {
168 | resolve({
169 | data: result.data.map((d, i) => ({ ...d, id: i })),
170 | page: result.page - 1,
171 | totalCount: result.total
172 | });
173 | });
174 | })
175 | }
176 | />
177 | buttonClick()}> click Me!
178 | {test}
179 |
180 | );
181 | }
182 |
183 | // https://github.com/material-table-core/core/issues/122
184 | // basically same as I1941 except that hook change happens after data promise resolves
185 | export function I122() {
186 | const [count, setCount] = useState(0);
187 | return (
188 | (
195 |
199 | )
200 | },
201 | { title: 'Id', field: 'id' },
202 | { title: 'First Name', field: 'first_name' },
203 | { title: 'Last Name', field: 'last_name' }
204 | ]}
205 | data={(query) =>
206 | new Promise((resolve, reject) => {
207 | let url = 'https://reqres.in/api/users?';
208 | url += 'per_page=' + query.pageSize;
209 | url += '&page=' + (query.page + 1);
210 | fetch(url)
211 | .then((response) => response.json())
212 | .then((result) => {
213 | resolve({
214 | data: result.data.map((d, i) => ({ ...d, id: i })),
215 | page: result.page - 1,
216 | totalCount: result.total
217 | });
218 | setCount((prev) => prev + 1);
219 | });
220 | })
221 | }
222 | options={{
223 | pageSize: 5,
224 | paginationType: 'stepped'
225 | }}
226 | />
227 | );
228 | }
229 |
--------------------------------------------------------------------------------
/__tests__/demo/demo.js:
--------------------------------------------------------------------------------
1 | /**
2 | * --- IMPORTANT NOTE FOR CONTRIBUTORS ---
3 | * Please try to keep this file as
4 | * clean and clutter free as possible
5 | * by placing demo components/code at:
6 | * `./demo-components/index.js`, or if
7 | * you need a place to store many demo
8 | * files, `./demo-components/MyComponent/foo.js`
9 | *
10 | *
11 | */
12 |
13 | /**
14 | * --- README ---
15 | * This file is the entrypoint to the
16 | * built-in dev server (run `npm start`)
17 | */
18 |
19 | import {
20 | ThemeProvider,
21 | StyledEngineProvider,
22 | createTheme
23 | } from '@mui/material/styles';
24 | import React, { StrictMode } from 'react';
25 |
26 | import {
27 | Basic,
28 | CustomExport,
29 | OneDetailPanel,
30 | MultipleDetailPanels,
31 | DefaultOrderIssue,
32 | TestingNewActionHandlersProp,
33 | BulkEdit,
34 | BasicRef,
35 | BulkEditWithDetailPanel,
36 | EditableRow,
37 | EditableCells,
38 | FrankensteinDemo,
39 | HidingColumns,
40 | Resizable,
41 | PersistentGroupings,
42 | DataSwitcher,
43 | DetailPanelIssuesProgrammaticallyHidingWhenOpen,
44 | EventTargetErrorOnRowClick,
45 | SelectionOnRowClick,
46 | DetailPanelRemounting,
47 | TreeData,
48 | TableWithSummary,
49 | TableWithNumberOfPagesAround,
50 | FixedColumnWithEdit,
51 | FilterWithOperatorSelection,
52 | TableMultiSorting,
53 | LocalizationWithCustomComponents
54 | } from './demo-components';
55 | import { createRoot } from 'react-dom/client';
56 | import { I1353, I1941, I122 } from './demo-components/RemoteData';
57 | import { Switch, FormControlLabel, CssBaseline } from '@mui/material';
58 |
59 | module.hot.accept();
60 |
61 | const container = document.querySelector('#app');
62 | const root = createRoot(container); // createRoot(container!) if you use TypeScript
63 |
64 | function Demo() {
65 | const [darkMode, setMode] = React.useState(false);
66 | return (
67 |
68 |
69 |
74 |
75 | {
80 | setMode(e.target.checked);
81 | }}
82 | />
83 | }
84 | label="Dark Mode"
85 | />
86 |
87 | DetailPanelRemounting
88 |
89 | {/* Switcher
90 |
91 |
92 | SelectionOnRowClick
93 |
94 |
95 | EventTargetErrorOnRowClick
96 | console.log('onSelectionChange', d)}
98 | />
99 |
100 | DetailPanelIssuesProgrammaticallyHidingWhenOpen
101 |
102 |
103 | Basic
104 |
105 |
106 | Basic Ref
107 |
108 |
109 | {/*
110 | Export Data
111 |
112 | */}
113 | {/*
114 | Custom Export
115 |
116 | */}
117 | Bulk Edit
118 |
119 | Default Order Issue
120 |
121 | Bulk Edit With Detail Panel
122 |
123 | Hiding Columns
124 |
125 | TestingNewActionHandlersProp
126 |
127 | Editable Rows
128 |
129 | One Detail Panel
130 |
131 | Multiple Detail Panels
132 |
133 | Editable
134 |
135 | Frankenstein
136 |
137 | Resizable Columns
138 |
139 | Persistent Groupings
140 |
141 | Persistent Groupings Same ID
142 |
143 | Persistent Groupings unshared
144 |
145 | Tree data
146 |
147 | Table with Summary Row
148 |
149 |
150 | Table with custom numbers of pages around current page in stepped
151 | navigation
152 |
153 |
154 | Fixed Column with Row Edits
155 |
156 | Localization with Custom Components
157 |
158 | Filter with operator selection
159 |
160 | Filter with operator selection and default operator
161 |
162 | Remote Data Related
163 |
164 |
165 |
171 |
172 |
173 |
174 |
180 |
181 |
182 |
183 |
188 |
189 |
190 |
191 |
192 |
193 |
194 | );
195 | }
196 | root.render( );
197 |
--------------------------------------------------------------------------------
/__tests__/demo/errorBoundary.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default class ErrorBoundary extends React.Component {
4 | constructor(props) {
5 | super(props);
6 | this.state = { error: null, errorInfo: null };
7 | }
8 |
9 | componentDidCatch(error, errorInfo) {
10 | // Catch errors in any components below and re-render with error message
11 | this.setState({
12 | error: error,
13 | errorInfo: errorInfo
14 | });
15 | // You can also log error messages to an error reporting service here
16 | }
17 |
18 | render() {
19 | if (this.state.errorInfo) {
20 | // Error path
21 | return (
22 |
23 |
Something went wrong.
24 |
25 | {this.state.error && this.state.error.toString()}
26 |
27 | {this.state.errorInfo.componentStack}
28 |
29 |
30 | );
31 | }
32 | // Normally, just render children
33 | return this.props.children;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/__tests__/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The Minimal React Webpack Babel Setup
5 |
9 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/__tests__/demo/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const BundleAnalyzerPlugin =
4 | require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
5 |
6 | module.exports = {
7 | entry: [path.resolve(__dirname, './demo.js')],
8 | module: {
9 | rules: [
10 | {
11 | test: /\.(js|jsx)$/,
12 | exclude: /node_modules/,
13 | use: ['babel-loader']
14 | }
15 | ]
16 | },
17 | resolve: {
18 | extensions: ['*', '.js', '.jsx']
19 | },
20 | output: {
21 | path: '/dist',
22 | publicPath: '/',
23 | filename: 'bundle.js'
24 | },
25 | plugins: [
26 | new webpack.HotModuleReplacementPlugin()
27 | // new BundleAnalyzerPlugin()
28 | ],
29 | devServer: {
30 | contentBase: '__tests__/demo',
31 | hot: true,
32 | disableHostCheck: true,
33 | port: 8080,
34 | open: true
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/__tests__/detailPanel.test.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {
3 | act,
4 | screen,
5 | render,
6 | waitForElementToBeRemoved,
7 | within,
8 | waitFor,
9 | fireEvent
10 | } from '@testing-library/react';
11 | import MaterialTable from '../src';
12 |
13 | const lookup = { 1: 'One', 2: 'Two' };
14 |
15 | const columns = [
16 | { title: 'Enum', field: 'enum', lookup },
17 | { title: 'Name', field: 'id' }
18 | ];
19 |
20 | const data = [{ id: 1, enum: 1 }];
21 |
22 | describe('Detailpanel render', () => {
23 | test('It displays and hides the detail with function', async () => {
24 | render(
25 | {
29 | return Detail Panel Test
;
30 | }}
31 | />
32 | );
33 | screen.getByRole('cell', {
34 | name: /one/i
35 | });
36 |
37 | const panelIsHidden = screen.queryByText(/Detail Panel Test/i);
38 |
39 | expect(panelIsHidden).toBeNull();
40 |
41 | const toggleButton = screen.getByRole('button', {
42 | name: /detail panel visibility toggle/i
43 | });
44 |
45 | fireEvent.click(toggleButton);
46 |
47 | screen.findByText(/Detail Panel Test/i);
48 |
49 | fireEvent.click(toggleButton);
50 |
51 | expect(screen.queryByText(/Detail Panel Test/i)).toBeNull();
52 | });
53 |
54 | test.skip('It displays the detail as is array', async () => {
55 | render(
56 | {
63 | return Detail Panel Test
;
64 | }
65 | }
66 | ]}
67 | />
68 | );
69 | screen.getByRole('cell', {
70 | name: /one/i
71 | });
72 | const toggleButton = await screen.findByRole('button', {
73 | name: /detail panel visibility toggle/i
74 | });
75 |
76 | fireEvent.click(toggleButton);
77 |
78 | await waitFor(() => screen.findByText(/Detail Panel Test/i));
79 |
80 | fireEvent.click(toggleButton);
81 |
82 | await waitFor(() =>
83 | expect(screen.queryByText(/Detail Panel Test/i)).toBeNull()
84 | );
85 | });
86 | });
87 |
--------------------------------------------------------------------------------
/__tests__/disableSort.test.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import { fireEvent, render } from '@testing-library/react';
3 | import * as React from 'react';
4 | import MaterialTable from '../src';
5 |
6 | const columns = [
7 | {
8 | title: 'Number',
9 | field: 'number',
10 | minWidth: 140,
11 | maxWidth: 400,
12 | sorting: true
13 | }
14 | ];
15 |
16 | const data = [
17 | {
18 | number: 9
19 | },
20 | {
21 | number: 22
22 | },
23 | {
24 | number: 25
25 | },
26 | {
27 | number: 3
28 | }
29 | ];
30 |
31 | describe('Disabled Client Sorting', () => {
32 | let initialOrderCollection = [];
33 | let onOrderCollectionChangeSpy;
34 |
35 | beforeEach(() => {
36 | jest.clearAllMocks();
37 | onOrderCollectionChangeSpy = jest.fn();
38 | initialOrderCollection = [
39 | {
40 | orderBy: 0,
41 | orderDirection: 'asc',
42 | sortOrder: 0,
43 | orderByField: 'number'
44 | }
45 | ];
46 | });
47 |
48 | test('should not update order of rows when clientSorting false', () => {
49 | const { queryAllByTestId } = render(
50 |
60 | );
61 |
62 | const numberColumn = queryAllByTestId('mtableheader-sortlabel')[0];
63 | fireEvent.click(numberColumn);
64 |
65 | expect(onOrderCollectionChangeSpy).toHaveBeenCalledWith([
66 | { sortOrder: 1, orderBy: 0, orderDirection: 'asc', orderByField: 'number' }
67 | ]);
68 |
69 | const cells = queryAllByTestId('mtablebodyrow').map((row) =>
70 | row.querySelectorAll('[data-testid=mtablecell]')
71 | );
72 | expect(cells.length).toBe(4);
73 | expect(cells[0][0].innerHTML).toBe('9');
74 | expect(cells[1][0].innerHTML).toBe('22');
75 | expect(cells[2][0].innerHTML).toBe('25');
76 | expect(cells[3][0].innerHTML).toBe('3');
77 | });
78 |
79 | test('should update order of rows when clientSorting true', () => {
80 | const { queryAllByTestId } = render(
81 |
91 | );
92 |
93 | const numberColumn = queryAllByTestId('mtableheader-sortlabel')[0];
94 | fireEvent.click(numberColumn);
95 |
96 | expect(onOrderCollectionChangeSpy).toHaveBeenCalledWith([
97 | { sortOrder: 1, orderBy: 0, orderDirection: 'asc', orderByField: 'number' }
98 | ]);
99 |
100 | const cells = queryAllByTestId('mtablebodyrow').map((row) =>
101 | row.querySelectorAll('[data-testid=mtablecell]')
102 | );
103 | expect(cells.length).toBe(4);
104 | expect(cells[0][0].innerHTML).toBe('3');
105 | expect(cells[1][0].innerHTML).toBe('9');
106 | expect(cells[2][0].innerHTML).toBe('22');
107 | expect(cells[3][0].innerHTML).toBe('25');
108 | });
109 | });
110 |
--------------------------------------------------------------------------------
/__tests__/localization.test.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { screen, render } from '@testing-library/react';
3 | import MaterialTable from '../src';
4 |
5 | const lookup = { 1: 'One', 2: 'Two' };
6 |
7 | const columns = [
8 | { title: 'Enum', field: 'enum', lookup },
9 | { title: 'Name', field: 'id' }
10 | ];
11 |
12 | const data = [{ id: 1, enum: 1 }];
13 |
14 | describe('Localization', () => {
15 | test('Renders the pagination', () => {
16 | render(
17 |
28 | );
29 | screen.getByText(/test_labeldisplayedrows/i);
30 | screen.getByText(/test_labelrowsperpage/i);
31 | screen.getByText(/5 Test_labelRows/i);
32 | expect(screen.queryByText('1–5 of 1')).toEqual(null); // Hides the normal display
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/__tests__/multiColumnSort.test.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import { fireEvent, render } from '@testing-library/react';
3 | import * as React from 'react';
4 | import MaterialTable from '../src';
5 |
6 | const columns = [
7 | {
8 | title: 'Number',
9 | field: 'number',
10 | minWidth: 140,
11 | maxWidth: 400
12 | },
13 | {
14 | title: 'Title',
15 | field: 'title',
16 | minWidth: 140,
17 | maxWidth: 400,
18 | sorting: true
19 | },
20 | {
21 | title: 'Name',
22 | field: 'name',
23 | minWidth: 140,
24 | maxWidth: 400,
25 | sorting: true
26 | },
27 | {
28 | title: 'Last Name',
29 | field: 'lastName',
30 | minWidth: 140,
31 | maxWidth: 400,
32 | sorting: true
33 | }
34 | ];
35 |
36 | const data = [
37 | {
38 | number: 1,
39 | title: 'Developer',
40 | name: 'Mehmet',
41 | lastName: 'Baran',
42 | id: '1231'
43 | },
44 | {
45 | number: 22,
46 | title: 'Developer',
47 | name: 'Pratik',
48 | lastName: 'N',
49 | id: '1234'
50 | },
51 | {
52 | number: 25,
53 | title: 'Human Resources',
54 | name: 'Juan',
55 | lastName: 'Lopez',
56 | id: '1235'
57 | },
58 | {
59 | number: 3,
60 | title: 'Consultant',
61 | name: 'Raul',
62 | lastName: 'Barak',
63 | id: '1236'
64 | }
65 | ];
66 |
67 | describe('Multi Column Sort', () => {
68 | let initialOrderCollection = [];
69 | let onOrderCollectionChangeSpy;
70 |
71 | beforeEach(() => {
72 | jest.clearAllMocks();
73 | onOrderCollectionChangeSpy = jest.fn();
74 | initialOrderCollection = [
75 | {
76 | orderBy: 1,
77 | orderDirection: 'asc',
78 | sortOrder: 1,
79 | orderByField: 'title'
80 | },
81 | {
82 | orderBy: 2,
83 | orderDirection: 'desc',
84 | sortOrder: 2,
85 | orderByField: 'name'
86 | }
87 | ];
88 | });
89 |
90 | test('should update table by multi column', () => {
91 | const { queryAllByTestId } = render(
92 |
101 | );
102 |
103 | const numberColumn = queryAllByTestId('mtableheader-sortlabel')[0];
104 | fireEvent.click(numberColumn);
105 |
106 | expect(onOrderCollectionChangeSpy).toHaveBeenCalledWith([
107 | { sortOrder: 1, orderBy: 0, orderDirection: 'asc', orderByField: 'number' }
108 | ]);
109 |
110 | const titleColumn = queryAllByTestId('mtableheader-sortlabel')[1];
111 | fireEvent.click(titleColumn);
112 |
113 | expect(onOrderCollectionChangeSpy).toHaveBeenCalledWith([
114 | { sortOrder: 1, orderBy: 0, orderDirection: 'asc', orderByField: 'number' },
115 | { sortOrder: 2, orderBy: 1, orderDirection: 'asc', orderByField: 'title' }
116 | ]);
117 | });
118 |
119 | test('should update table by multi column and replace first if reach the maximum order columns', () => {
120 | const { queryAllByTestId } = render(
121 |
130 | );
131 |
132 | const numberColumn = queryAllByTestId('mtableheader-sortlabel')[0];
133 | fireEvent.click(numberColumn);
134 |
135 | expect(onOrderCollectionChangeSpy).toHaveBeenCalledWith([
136 | { sortOrder: 1, orderBy: 0, orderDirection: 'asc', orderByField: 'number' }
137 | ]);
138 |
139 | fireEvent.click(queryAllByTestId('mtableheader-sortlabel')[1]);
140 | fireEvent.click(queryAllByTestId('mtableheader-sortlabel')[2]);
141 | fireEvent.click(queryAllByTestId('mtableheader-sortlabel')[3]);
142 |
143 | expect(onOrderCollectionChangeSpy).toHaveBeenCalledWith([
144 | { sortOrder: 1, orderBy: 1, orderDirection: 'asc', orderByField: 'title' },
145 | { sortOrder: 2, orderBy: 2, orderDirection: 'asc', orderByField: 'name' },
146 | { sortOrder: 3, orderBy: 3, orderDirection: 'asc', orderByField: 'lastName' }
147 | ]);
148 | });
149 |
150 | test('should order desc when secon click', () => {
151 | const { queryAllByTestId } = render(
152 |
161 | );
162 |
163 | const numberColumn = queryAllByTestId('mtableheader-sortlabel')[0];
164 | fireEvent.click(numberColumn);
165 | fireEvent.click(numberColumn);
166 |
167 | expect(onOrderCollectionChangeSpy).toHaveBeenCalledWith([
168 | { sortOrder: 1, orderBy: 0, orderDirection: 'desc', orderByField: 'number' }
169 | ]);
170 | });
171 |
172 | test('should have being initialized by defaultOrderByCollection', () => {
173 | const { queryAllByTestId } = render(
174 |
184 | );
185 |
186 | const numberColumn = queryAllByTestId('mtableheader-sortlabel')[0];
187 | fireEvent.click(numberColumn);
188 |
189 | expect(onOrderCollectionChangeSpy).toHaveBeenCalledWith([
190 | { sortOrder: 1, orderBy: 1, orderDirection: 'asc', orderByField: 'title' },
191 | { sortOrder: 2, orderBy: 2, orderDirection: 'desc', orderByField: 'name' },
192 | { sortOrder: 3, orderBy: 0, orderDirection: 'asc', orderByField: 'number' }
193 | ]);
194 | });
195 | });
196 |
--------------------------------------------------------------------------------
/__tests__/post.build.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import MaterialTable from '../dist';
4 | import { render } from '@testing-library/react';
5 |
6 | import { makeData, columns } from './test.helper';
7 |
8 | /**
9 | * Uses '../dist' for MaterialTable
10 | */
11 | describe('Render Table : Post Build', () => {
12 | // Render empty table
13 | describe('when attempting to render an empty table', () => {
14 | it('renders without crashing', () => {
15 | render( );
16 | });
17 | });
18 | // Render table with random data
19 | describe('when attempting to render a table with data', () => {
20 | it('renders without crashing', () => {
21 | const data = makeData();
22 | render( );
23 | });
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/__tests__/pre.build.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | render,
4 | screen,
5 | fireEvent,
6 | waitForElementToBeRemoved,
7 | within
8 | } from '@testing-library/react';
9 |
10 | import MaterialTable from '../src';
11 |
12 | import { makeData, columns } from './test.helper';
13 |
14 | /**
15 | * Uses '../src' for MaterialTable
16 | */
17 | describe.skip('Render Table : Pre Build', () => {
18 | // Render empty table
19 | describe('when attempting to render an empty table', () => {
20 | it('renders without crashing', () => {
21 | render( );
22 | screen.getByRole('heading', {
23 | name: /table title/i
24 | });
25 | screen.getByTestId(/search/i);
26 | screen.getByRole('textbox', {
27 | name: /search/i
28 | });
29 | screen.getByRole('button', {
30 | name: /clear search/i
31 | });
32 | screen.getByRole('row', {
33 | name: /no records to display/i
34 | });
35 | screen.getByRole('button', { name: /rows per page: 5 rows/i });
36 | screen.getByRole('button', { name: /first page/i });
37 | screen.getByRole('button', { name: /previous page/i });
38 | screen.getByRole('button', { name: /next page/i });
39 | screen.getByRole('button', { name: /last page/i });
40 | expect(screen.getAllByRole('table')).toHaveLength(2);
41 | });
42 | });
43 |
44 | // Render table with data
45 | describe('when attempting to render a table with data', () => {
46 | it('renders without crashing', () => {
47 | const data = makeData();
48 | render( );
49 |
50 | screen.getAllByRole('columnheader', { name: /first name/i });
51 | screen.getAllByRole('columnheader', { name: /last name/i });
52 | screen.getAllByRole('columnheader', { name: /age/i });
53 | expect(
54 | screen.getAllByRole('button', { name: /first name/i })
55 | ).toHaveLength(1);
56 | expect(
57 | screen.getAllByRole('button', { name: /last name/i })
58 | ).toHaveLength(1);
59 |
60 | expect(screen.getAllByTestId('mtableheader-sortlabel')).toHaveLength(3);
61 | expect(screen.getAllByRole('button', { name: 'Age' })).toHaveLength(1);
62 | expect(screen.getAllByRole('row')).toHaveLength(7);
63 | screen.getByRole('row', {
64 | name: /first name last name age/i
65 | });
66 |
67 | screen.getByRole('row', {
68 | name: /oliver smith 0/i
69 | });
70 | screen.getByRole('row', {
71 | name: /elijah johnson 1/i
72 | });
73 | screen.getByRole('row', {
74 | name: /william williams 2/i
75 | });
76 | screen.getByRole('row', {
77 | name: /james brown 3/i
78 | });
79 | screen.getByText(/1-5 of 99/i);
80 | });
81 |
82 | it('navigates between the pages', () => {
83 | const data = makeData();
84 | render( );
85 |
86 | screen.getByRole('row', {
87 | name: /oliver smith 0/i
88 | });
89 | screen.getByRole('row', {
90 | name: /elijah johnson 1/i
91 | });
92 | screen.getByRole('row', {
93 | name: /william williams 2/i
94 | });
95 | screen.getByRole('row', {
96 | name: /james brown 3/i
97 | });
98 | screen.getByText(/1-5 of 99/i);
99 |
100 | fireEvent.click(screen.getByTestId(/chevron_right/i));
101 | screen.getByRole('row', {
102 | name: /lucas miller 5/i
103 | });
104 | screen.getByRole('row', {
105 | name: /henry davis 6/i
106 | });
107 | screen.getByRole('row', {
108 | name: /michael wilson 9/i
109 | });
110 | screen.getByText(/6-10 of 99/i);
111 | fireEvent.click(screen.getByTestId(/last_page/i));
112 | screen.getByRole('row', {
113 | name: /Daniel Martinez 95/i
114 | });
115 | screen.getByRole('row', {
116 | name: /Oliver Anderson 96/i
117 | });
118 | screen.getByRole('row', {
119 | name: /William Thomas 98/i
120 | });
121 | screen.getByText(/96-99 of 99/i);
122 | screen.getByText(/5 rows/i);
123 | screen.getByRole('button', {
124 | name: /next page/i
125 | });
126 | screen.getByRole('button', {
127 | name: /last page/i
128 | });
129 | screen.getByRole('button', {
130 | name: /previous page/i
131 | });
132 | screen.getByRole('button', {
133 | name: /first page/i
134 | });
135 | expect(screen.getAllByRole('row')).toHaveLength(8);
136 | });
137 |
138 | it('filters data by search input', async () => {
139 | const data = makeData();
140 | render( );
141 | screen.getByRole('row', {
142 | name: /oliver smith 0/i
143 | });
144 | fireEvent.input(
145 | screen.getByRole('textbox', {
146 | name: /search/i
147 | }),
148 | { target: { value: 'test' } }
149 | );
150 | screen.getByDisplayValue(/test/i);
151 | await waitForElementToBeRemoved(
152 | screen.getByRole('row', {
153 | name: /oliver smith 0/i
154 | })
155 | );
156 | screen.getByRole('row', {
157 | name: /no records to display/i
158 | });
159 | fireEvent.input(
160 | screen.getByRole('textbox', {
161 | name: /search/i
162 | }),
163 | { target: { value: 'john' } }
164 | );
165 | screen.getByDisplayValue(/john/i);
166 | await waitForElementToBeRemoved(
167 | screen.getByRole('row', {
168 | name: /no records to display/i
169 | })
170 | );
171 | screen.getByRole('row', {
172 | name: /elijah johnson 1/i
173 | });
174 | screen.getByRole('row', {
175 | name: /henry johnson 18/i
176 | });
177 | screen.getByRole('row', {
178 | name: /daniel johnson 35/i
179 | });
180 | screen.getByRole('row', {
181 | name: /benjamin johnson 52/i
182 | });
183 | screen.getByRole('row', {
184 | name: /michael johnson 69/i
185 | });
186 |
187 | screen.getByText(/1-5 of 6/i);
188 | screen.getByText(/5 rows/i);
189 | screen.getByRole('button', {
190 | name: /next page/i
191 | });
192 | screen.getByRole('button', {
193 | name: /last page/i
194 | });
195 | screen.getByRole('button', {
196 | name: /previous page/i
197 | });
198 | screen.getByRole('button', {
199 | name: /first page/i
200 | });
201 | fireEvent.click(
202 | screen.getByRole('button', {
203 | name: /clear search/i
204 | })
205 | );
206 | expect(screen.getByRole('textbox')).toHaveDisplayValue('');
207 | expect(
208 | screen.getByRole('button', {
209 | name: /clear search/i
210 | })
211 | ).toBeDisabled();
212 | });
213 | });
214 | // Render table with column render function
215 | it('renders the render function in column', () => {
216 | const data = makeData();
217 | render(
218 | ({
221 | ...col,
222 | field: '', // Removes the field to force the render to show
223 | render: (val) => val[col.field]
224 | }))}
225 | />
226 | );
227 |
228 | screen.getByRole('row', {
229 | name: /oliver smith 0/i
230 | });
231 | screen.getByRole('row', {
232 | name: /elijah johnson 1/i
233 | });
234 | screen.getByRole('row', {
235 | name: /william williams 2/i
236 | });
237 | screen.getByRole('row', {
238 | name: /james brown 3/i
239 | });
240 | screen.getByText(/1-5 of 99/i);
241 | });
242 | });
243 |
244 | describe.skip('Test event loop and flows', () => {
245 | it('calls onRowChange and onPageSizeChange during the same event loop', async () => {
246 | const apiCall = jest.fn(() => null);
247 | const data = makeData();
248 | const Component = () => {
249 | const [{ page, pageSize }, setPage] = React.useState({
250 | page: 0,
251 | pageSize: 5
252 | });
253 |
254 | React.useEffect(() => {
255 | apiCall(page, pageSize);
256 | }, [page, pageSize]);
257 |
258 | return (
259 | {
263 | setPage((prev) => ({ ...prev, pageSize: size }));
264 | }}
265 | onPageChange={(page) => {
266 | setPage((prev) => ({ ...prev, page }));
267 | }}
268 | />
269 | );
270 | };
271 | render( );
272 | expect(apiCall.mock.calls).toHaveLength(1);
273 | expect(apiCall.mock.calls[0]).toEqual([0, 5]);
274 | fireEvent.click(
275 | screen.getByRole('button', {
276 | name: /next page/i
277 | })
278 | );
279 |
280 | expect(apiCall.mock.calls).toHaveLength(2);
281 | expect(apiCall.mock.calls[1]).toEqual([1, 5]);
282 | fireEvent.mouseDown(
283 | screen.getByRole('button', {
284 | name: 'Rows per page: 5 rows'
285 | })
286 | );
287 | const listbox = within(screen.getByRole('presentation')).getByRole(
288 | 'listbox'
289 | );
290 | const options = within(listbox).getAllByRole('option');
291 | const optionValues = options.map((li) => li.getAttribute('data-value'));
292 |
293 | expect(optionValues).toEqual(['5', '10', '20']);
294 |
295 | fireEvent.click(options[1]);
296 |
297 | expect(apiCall.mock.calls).toHaveLength(3);
298 | expect(apiCall.mock.calls[2]).toEqual([0, 10]);
299 | });
300 | });
301 |
--------------------------------------------------------------------------------
/__tests__/selectByObject.test.js:
--------------------------------------------------------------------------------
1 | import { selectFromObject, setObjectByKey } from '../src/utils/';
2 |
3 | describe('selectFromObject', () => {
4 | describe('by string', () => {
5 | test('Select valid value', () => {
6 | const obj = { a: { b: { c: 'value' } } };
7 | const res = selectFromObject(obj, 'a.b.c');
8 | expect(res).toEqual('value');
9 | });
10 | test('Return undefined when path does not exist', () => {
11 | const obj = { a: { b: { c: 'value' } } };
12 | const res = selectFromObject(obj, 'b.a.c');
13 | expect(res).toEqual(undefined);
14 | });
15 | test('Allow selecting by number index', () => {
16 | const obj = { a: [{ b: { c: 'value' } }] };
17 | const res = selectFromObject(obj, 'a[0].b.c');
18 | expect(res).toEqual(obj.a[0].b.c);
19 | });
20 | });
21 |
22 | describe('by array', () => {
23 | test('Select valid value', () => {
24 | const obj = { a: { b: { c: 'value' } } };
25 | const res = selectFromObject(obj, ['a', 'b', 'c']);
26 | expect(res).toEqual('value');
27 | });
28 | test('Select valid value with number', () => {
29 | const obj = { a: [{ b: { c: 'value' } }] };
30 | const res = selectFromObject(obj, ['a', 0, 'b', 'c']);
31 | expect(res).toEqual('value');
32 | });
33 | test('Return undefined when path does not exist', () => {
34 | const obj = { a: { b: { c: 'value' } } };
35 | const res = selectFromObject(obj, ['x', 'y', 'z']);
36 | expect(res).toEqual(undefined);
37 | });
38 | test('Select root with empty array', () => {
39 | const obj = { a: { b: { c: 'value' } } };
40 | const res = selectFromObject(obj, []);
41 | expect(res).toEqual(obj);
42 | });
43 | });
44 | });
45 |
46 | describe('setObjectByKey', () => {
47 | describe('by string', () => {
48 | test('Select valid value', () => {
49 | const obj = { a: { b: { c: 'value' } } };
50 | setObjectByKey(obj, 'a.b.c', 'newValue');
51 | expect(obj).toEqual({ a: { b: { c: 'newValue' } } });
52 | });
53 | });
54 |
55 | describe('by array', () => {
56 | test('Select valid value', () => {
57 | const obj = { a: { b: { c: 'value' } } };
58 | setObjectByKey(obj, ['a', 'b', 'c'], 'newValue');
59 | expect(obj).toEqual({ a: { b: { c: 'newValue' } } });
60 | });
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/__tests__/selection.test.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {
3 | screen,
4 | render,
5 | fireEvent,
6 | within,
7 | waitFor
8 | } from '@testing-library/react';
9 | import MaterialTable from '../src';
10 | import '@testing-library/jest-dom';
11 |
12 | describe('Selection tests', () => {
13 | test('Basic selection', async () => {
14 | render(
15 |
29 | );
30 | const boxes = screen.getAllByRole('checkbox');
31 | expect(boxes).toHaveLength(4);
32 | boxes.forEach((box) => expect(box).not.toBeChecked());
33 | const [all, first, second, third] = screen.getAllByRole('checkbox');
34 | fireEvent.click(first);
35 | expect(first).toBeChecked();
36 | screen.getByRole('heading', {
37 | name: /1 row\(s\) selected/i
38 | });
39 | expect(all).toHaveAttribute('data-indeterminate', 'true');
40 |
41 | fireEvent.click(second);
42 | expect(second).toBeChecked();
43 |
44 | fireEvent.click(third);
45 | expect(third).toBeChecked();
46 | expect(all).toBeChecked();
47 | expect(all).not.toHaveAttribute('data-indeterminate', 'true');
48 | screen.getByRole('heading', {
49 | name: /3 row\(s\) selected/i
50 | });
51 | boxes.forEach((box) => expect(box).toBeChecked());
52 |
53 | fireEvent.click(all);
54 | boxes.forEach((box) => expect(box).not.toBeChecked());
55 |
56 | expect(
57 | screen.queryByRole('heading', {
58 | name: /3 row\(s\) selected/i
59 | })
60 | ).toBeNull();
61 | });
62 | test('Parent Child Selection', async () => {
63 | const words = ['Paper', 'Rock', 'Scissors'];
64 |
65 | const rawData = [];
66 | for (let i = 0; i < 5; i++) {
67 | rawData.push({ id: i, word: words[i % words.length] });
68 | }
69 |
70 | const columns = [
71 | { title: 'Id', field: 'id' },
72 | { title: 'Word', field: 'word' }
73 | ];
74 | render(
75 | rows.find((a) => a.id === row.parentId)}
77 | data={[
78 | ...rawData,
79 | { id: 11, word: 'test', parentId: 0 },
80 | { id: 12, word: 'test', parentId: 1 }
81 | ]}
82 | columns={columns}
83 | options={{
84 | selection: true
85 | }}
86 | />
87 | );
88 | expect(screen.getAllByRole('checkbox')).toHaveLength(6);
89 | screen
90 | .getAllByRole('checkbox')
91 | .forEach((box) => expect(box).not.toBeChecked());
92 | const [all, first, second, third, fourth, fifth] =
93 | screen.getAllByRole('checkbox');
94 |
95 | fireEvent.click(first);
96 | screen.getByRole('heading', {
97 | name: /2 row\(s\) selected/i
98 | });
99 | expect(first).toBeChecked();
100 | expect(all).toHaveAttribute('data-indeterminate', 'true');
101 | const row = screen.getByRole('row', {
102 | name: /0 paper/i
103 | });
104 |
105 | const firstToggle = within(row).getByRole('button', {
106 | name: /detail panel visibility toggle/i
107 | });
108 | expect(
109 | screen.queryByRole('cell', {
110 | name: /11/i
111 | })
112 | ).toBeNull;
113 |
114 | fireEvent.click(firstToggle);
115 |
116 | screen.getByRole('cell', {
117 | name: /11/i
118 | });
119 | expect(screen.getAllByRole('checkbox')).toHaveLength(7);
120 | fireEvent.click(second);
121 | screen.getByRole('heading', {
122 | name: /4 row\(s\) selected/i
123 | });
124 | fireEvent.click(third);
125 | fireEvent.click(fourth);
126 | fireEvent.click(fifth);
127 | screen.getByRole('heading', {
128 | name: /7 row\(s\) selected/i
129 | });
130 | expect(screen.getAllByRole('checkbox')[0]).toBeChecked();
131 | });
132 | test('Parent Child Selection with search', async () => {
133 | const words = ['Paper', 'Rock', 'Scissors'];
134 |
135 | const rawData = [];
136 | for (let i = 0; i < 5; i++) {
137 | rawData.push({ id: i, word: words[i % words.length] });
138 | }
139 |
140 | const columns = [
141 | { title: 'Id', field: 'id' },
142 | { title: 'Word', field: 'word' }
143 | ];
144 | const data = [
145 | ...rawData,
146 | { id: 11, word: 'test', parentId: 0 },
147 | { id: 12, word: 'test', parentId: 1 }
148 | ];
149 | render(
150 | rows.find((a) => a.id === row.parentId)}
152 | data={data}
153 | columns={columns}
154 | options={{
155 | selection: true
156 | }}
157 | />
158 | );
159 |
160 | await waitFor(async () => {
161 | const checkboxes = await screen.findAllByRole('checkbox');
162 | expect(checkboxes).toHaveLength(6);
163 | checkboxes.forEach((box) => expect(box).not.toBeChecked());
164 | });
165 |
166 | const search = screen.getByRole('textbox', {
167 | name: /search/i
168 | });
169 | fireEvent.change(search, { target: { value: '1' } });
170 | expect(search.value).toBe('1');
171 | await waitFor(() =>
172 | expect(screen.getAllByRole('checkbox')).toHaveLength(5)
173 | );
174 | const [all] = screen.getAllByRole('checkbox');
175 | fireEvent.click(all);
176 | screen
177 | .getAllByRole('checkbox')
178 | .forEach((box, i) => (i !== 1 ? expect(box).toBeChecked() : null));
179 |
180 | fireEvent.click(all);
181 | screen
182 | .getAllByRole('checkbox')
183 | .forEach((box) => expect(box).not.toBeChecked());
184 |
185 | const [a, b, third, d, fifth] = screen.getAllByRole('checkbox');
186 | fireEvent.click(third);
187 | fireEvent.click(fifth);
188 | expect(all).not.toBeChecked();
189 | });
190 | });
191 |
--------------------------------------------------------------------------------
/__tests__/showPaginationButtons.test.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {
3 | screen,
4 | render,
5 | waitForElementToBeRemoved
6 | } from '@testing-library/react';
7 | import MaterialTable from '../src';
8 | import '@testing-library/jest-dom';
9 |
10 | describe('Show Pagination Buttons', () => {
11 | test('Show no buttons', async () => {
12 | render(
13 |
35 | );
36 | screen.getByRole('button', { name: /previous page/i });
37 | screen.getByRole('button', { name: /next page/i });
38 | expect(screen.queryByRole('button', { name: /first page/i })).toBeNull();
39 | expect(screen.queryByRole('button', { name: /last page/i })).toBeNull();
40 | });
41 | test('Show first buttons', async () => {
42 | render(
43 |
65 | );
66 | screen.getByRole('button', { name: /previous page/i });
67 | screen.getByRole('button', { name: /next page/i });
68 | screen.getByRole('button', { name: /first page/i });
69 | expect(screen.queryByRole('button', { name: /last page/i })).toBeNull();
70 | });
71 | test('Show last buttons', async () => {
72 | render(
73 |
95 | );
96 | screen.getByRole('button', { name: /previous page/i });
97 | screen.getByRole('button', { name: /next page/i });
98 | screen.getByRole('button', { name: /last page/i });
99 | expect(screen.queryByRole('button', { name: /first page/i })).toBeNull();
100 | });
101 | test('Show all buttons', async () => {
102 | render(
103 |
123 | );
124 |
125 | screen.getByRole('button', { name: /previous page/i });
126 | screen.getByRole('button', { name: /next page/i });
127 | screen.getByRole('button', { name: /first page/i });
128 | screen.getByRole('button', { name: /last page/i });
129 | });
130 | });
131 |
--------------------------------------------------------------------------------
/__tests__/summaryRow.test.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { screen, render } from '@testing-library/react';
3 | import MaterialTable from '../src';
4 | import '@testing-library/jest-dom';
5 |
6 | const columns = [
7 | { title: 'Enum', field: 'enum' },
8 | { title: 'Name', field: 'id' }
9 | ];
10 |
11 | const data = [
12 | { id: 1, enum: 1 },
13 | { id: 2, enum: 2 }
14 | ];
15 |
16 | describe('Summary row of table', () => {
17 | test('renders summary row if renderSummaryRow prop is present', () => {
18 | render(
19 | `Summary_${column.title}`}
23 | />
24 | );
25 |
26 | expect(
27 | screen.getByRole('row', { name: 'Summary_Enum Summary_Name' })
28 | ).toBeTruthy();
29 | });
30 |
31 | test('calls renderSummaryRow function for each column of table', () => {
32 | const renderSummaryMock = jest.fn();
33 | render(
34 |
39 | );
40 |
41 | columns.forEach((column, index) => {
42 | expect(renderSummaryMock).toHaveBeenCalledWith(
43 | expect.objectContaining({
44 | index: index,
45 | column: column,
46 | data: data.map((rowData) => expect.objectContaining(rowData))
47 | })
48 | );
49 | });
50 | });
51 |
52 | test('renders summary cells given value and style', () => {
53 | render(
54 | {
58 | return { value: `Summary_${column.title}`, style: { color: 'red' } };
59 | }}
60 | />
61 | );
62 |
63 | expect(screen.getByText('Summary_Enum')).toHaveStyle({ color: 'red' });
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/__tests__/test.helper.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 |
3 | export const columns = [
4 | { field: 'firstName', title: 'First Name' },
5 | { field: 'lastName', title: 'Last Name' },
6 | { field: 'age', title: 'Age' }
7 | ];
8 |
9 | export function makeData() {
10 | const runtime = 99;
11 | const datas = [];
12 | for (let i = 0; i < runtime; i++) {
13 | datas.push({
14 | firstName: makeFirstName(i),
15 | lastName: makeLastName(i),
16 | age: makeAge(i)
17 | });
18 | }
19 | return datas;
20 | }
21 |
22 | export function makeFirstName(runtime) {
23 | const names = [
24 | 'Oliver',
25 | 'Elijah',
26 | 'William',
27 | 'James',
28 | 'Benjamin',
29 | 'Lucas',
30 | 'Henry',
31 | 'Alexander',
32 | 'Mason',
33 | 'Michael',
34 | 'Ethan',
35 | 'Daniel'
36 | ];
37 | return names[runtime % names.length];
38 | }
39 |
40 | export function makeLastName(runtime) {
41 | const lastnames = [
42 | 'Smith',
43 | 'Johnson',
44 | 'Williams',
45 | 'Brown',
46 | 'Jones',
47 | 'Miller',
48 | 'Davis',
49 | 'Garcia',
50 | 'Rodriguez',
51 | 'Wilson',
52 | 'Martinez',
53 | 'Anderson',
54 | 'Taylor',
55 | 'Thomas',
56 | 'Hernandez',
57 | 'Moore',
58 | 'Martin'
59 | ];
60 | return lastnames[runtime % lastnames.length];
61 | }
62 |
63 | export function makeAge(runtime) {
64 | return runtime;
65 | }
66 |
--------------------------------------------------------------------------------
/__tests__/useDoubleClick.test.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {
3 | act,
4 | fireEvent,
5 | render,
6 | screen,
7 | waitFor
8 | } from '@testing-library/react';
9 | import { useDoubleClick } from '../src/utils/hooks/useDoubleClick';
10 |
11 | jest.useFakeTimers();
12 |
13 | describe('useDouble click', () => {
14 | beforeEach(() => {
15 | jest.clearAllTimers();
16 | });
17 |
18 | test('handles single-click', async () => {
19 | const clickMock = jest.fn();
20 | const { getByTestId } = render( );
21 | expect(clickMock).not.toHaveBeenCalled();
22 | act(() => {
23 | fireEvent.click(getByTestId('test-component'));
24 | });
25 | // Uncomment this to make the test pass
26 | // await timeout();
27 | expect(clickMock).toHaveBeenCalled();
28 | });
29 | test('handles double-click when user clicks twice instantly', async () => {
30 | const clickMock = jest.fn();
31 | const doubleClickMock = jest.fn();
32 | const { getByTestId } = render(
33 |
37 | );
38 | expect(clickMock).not.toHaveBeenCalled();
39 | expect(doubleClickMock).not.toHaveBeenCalled();
40 |
41 | act(() => {
42 | fireEvent.click(getByTestId('test-component'));
43 | });
44 | act(() => {
45 | fireEvent.click(getByTestId('test-component'));
46 | });
47 | expect(doubleClickMock).toHaveBeenCalled();
48 | });
49 | test('handles double-click when user clicks twice within the interval', async () => {
50 | const clickMock = jest.fn();
51 | const doubleClickMock = jest.fn();
52 | const { getByTestId } = render(
53 |
57 | );
58 | expect(clickMock).not.toHaveBeenCalled();
59 | expect(doubleClickMock).not.toHaveBeenCalled();
60 | act(() => {
61 | fireEvent.click(getByTestId('test-component'));
62 | });
63 | jest.advanceTimersByTime(50);
64 | act(() => {
65 | fireEvent.click(getByTestId('test-component'));
66 | });
67 | expect(doubleClickMock).toHaveBeenCalled();
68 | });
69 |
70 | test('calls single-click twice if user clicks twice outside of the interval', async () => {
71 | const clickMock = jest.fn();
72 | const doubleClickMock = jest.fn();
73 | const { getByTestId } = render(
74 |
78 | );
79 | expect(clickMock).not.toHaveBeenCalled();
80 | expect(doubleClickMock).not.toHaveBeenCalled();
81 | act(() => {
82 | fireEvent.click(getByTestId('test-component'));
83 | });
84 | act(() => {
85 | jest.advanceTimersByTime(300);
86 | });
87 | act(() => {
88 | fireEvent.click(getByTestId('test-component'));
89 | });
90 | expect(doubleClickMock).not.toHaveBeenCalled();
91 | expect(clickMock).toHaveBeenCalledTimes(1);
92 | act(() => {
93 | jest.advanceTimersByTime(300);
94 | });
95 | expect(clickMock).toHaveBeenCalledTimes(2);
96 | });
97 | });
98 |
99 | function TestComponent({ onRowClick, onRowDoubleClick }) {
100 | const handleOnRowClick = useDoubleClick(onRowClick, onRowDoubleClick);
101 | return (
102 |
103 | click
104 |
105 | );
106 | }
107 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is needed for Jest
3 | */
4 | module.exports = {
5 | presets: ['@babel/preset-env', '@babel/react'],
6 | plugins: [
7 | '@babel/plugin-transform-runtime',
8 | '@babel/plugin-proposal-class-properties',
9 | '@babel/plugin-proposal-object-rest-spread',
10 | [
11 | 'module-resolver',
12 | {
13 | root: ['./'],
14 | alias: {
15 | '@components': './src/components',
16 | '@store': './src/store',
17 | '@utils': './src/utils'
18 | }
19 | }
20 | ],
21 | [
22 | 'import',
23 | {
24 | camel2DashComponentName: false,
25 | libraryDirectory: '',
26 | libraryName: '@mui/material'
27 | },
28 | '@mui/material'
29 | ],
30 | [
31 | 'import',
32 | {
33 | camel2DashComponentName: false,
34 | libraryDirectory: '',
35 | libraryName: '@mui/icons-material'
36 | },
37 | '@mui/icons-material'
38 | ]
39 | ]
40 | };
41 |
--------------------------------------------------------------------------------
/esbuild.config.js:
--------------------------------------------------------------------------------
1 | import esbuild from 'esbuild';
2 | import rimraf from 'rimraf';
3 | import path from 'path';
4 | import fs from 'fs';
5 |
6 | const { log, error } = console;
7 |
8 | /**
9 | * OUTPUT PATH IS SET HERE!!
10 | *
11 | * relative to root of project
12 | *
13 | * !! NO TRAILING SLASH !!
14 | */
15 | const BUILD_DIR = './dist';
16 |
17 | log(`-Cleaning build artifacts from : '${BUILD_DIR}' `);
18 |
19 | rimraf(path.resolve(BUILD_DIR), async (err) => {
20 | if (err) {
21 | error(`err cleaning '${BUILD_DIR}' : ${error.stderr}`);
22 | process.exit(1);
23 | }
24 |
25 | log('successfully cleaned build artifacts');
26 |
27 | const options = {
28 | entryPoints: getFilesRecursive('./src', '.js'),
29 | minifySyntax: true,
30 | minify: true,
31 | bundle: false,
32 | outdir: `${BUILD_DIR}`,
33 | target: 'es6',
34 | // format: 'cjs',
35 | loader: {
36 | '.js': 'jsx'
37 | }
38 | };
39 |
40 | log('-Begin building');
41 |
42 | try {
43 | await esbuild.build(options);
44 | log(
45 | `\nSuccessfully built to '${BUILD_DIR}''\n[note] : this path is relative to the root of this project)'`
46 | );
47 | } catch (err) {
48 | error(`\nERROR BUILDING : ${err}`);
49 | process.exit(1);
50 | } finally {
51 | process.exit(0);
52 | }
53 | });
54 |
55 | /**
56 | * Helper functions
57 | */
58 |
59 | function getFilesRecursive(dirPath, fileExtension = '') {
60 | const paths = traverseDir(dirPath);
61 | if (fileExtension === '') {
62 | return paths;
63 | }
64 | return paths.filter((p) => p.endsWith(fileExtension));
65 | }
66 |
67 | function traverseDir(dir, filePaths = []) {
68 | fs.readdirSync(dir).forEach((file) => {
69 | const fullPath = path.join(dir, file);
70 | if (fs.lstatSync(fullPath).isDirectory()) {
71 | traverseDir(fullPath, filePaths);
72 | } else {
73 | filePaths.push(fullPath);
74 | }
75 | });
76 | return filePaths;
77 | }
78 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /*
2 | * For a detailed explanation regarding each configuration property, visit:
3 | * https://jestjs.io/docs/en/configuration.html
4 | */
5 |
6 | module.exports = {
7 | // All imported modules in your tests should be mocked automatically
8 | // automock: false,
9 |
10 | // Stop running tests after `n` failures
11 | bail: 1,
12 |
13 | // The directory where Jest should store its cached dependency information
14 | // cacheDirectory: "/private/var/folders/62/dg4j741n0bb1jy2gs09h9_yh0000gn/T/jest_dx",
15 |
16 | // Automatically clear mock calls and instances between every test
17 | clearMocks: true,
18 |
19 | // Indicates whether the coverage information should be collected while executing the test
20 | collectCoverage: true,
21 |
22 | // An array of glob patterns indicating a set of files for which coverage information should be collected
23 | collectCoverageFrom: ['src/**/*'],
24 |
25 | // The directory where Jest should output its coverage files
26 | coverageDirectory: './__tests__/coverage',
27 |
28 | // An array of regexp pattern strings used to skip coverage collection
29 | // coveragePathIgnorePatterns: [
30 | // "/node_modules/"
31 | // ],
32 |
33 | // Indicates which provider should be used to instrument code for coverage
34 | // coverageProvider: "babel",
35 |
36 | // A list of reporter names that Jest uses when writing coverage reports
37 | coverageReporters: [
38 | 'json',
39 | 'text',
40 | 'lcov'
41 | // "clover"
42 | ],
43 |
44 | // An object that configures minimum threshold enforcement for coverage results
45 | coverageThreshold: {
46 | global: {
47 | /**
48 | * THE VALUES BELOW NEED TO BE CHANGED BACK
49 | * TO SOMETHING REASONABLE ONCE
50 | * TESTS ARE ACTUALLY ADDED
51 | */
52 | lines: 0, // 90, // <-- this value
53 | statements: 0 // 90 // <-- and this value
54 | /**
55 | * ^^ The values are above ^^
56 | * I am making an absurd amount of
57 | * comments so this
58 | * hopefully gets noticed
59 | * one day lol.
60 | */
61 | }
62 | },
63 |
64 | // A path to a custom dependency extractor
65 | // dependencyExtractor: undefined,
66 |
67 | // Make calling deprecated APIs throw helpful error messages
68 | // errorOnDeprecated: false,
69 |
70 | // Force coverage collection from ignored files using an array of glob patterns
71 | // forceCoverageMatch: [],
72 |
73 | // A path to a module which exports an async function that is triggered once before all test suites
74 | // globalSetup: undefined,
75 |
76 | // A path to a module which exports an async function that is triggered once after all test suites
77 | // globalTeardown: undefined,
78 |
79 | // A set of global variables that need to be available in all test environments
80 | // globals: {},
81 |
82 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
83 | // maxWorkers: "50%",
84 |
85 | // An array of directory names to be searched recursively up from the requiring module's location
86 | // moduleDirectories: [
87 | // "node_modules"
88 | // ],
89 |
90 | // An array of file extensions your modules use
91 | // moduleFileExtensions: [
92 | // "js",
93 | // "json",
94 | // "jsx",
95 | // "ts",
96 | // "tsx",
97 | // "node"
98 | // ],
99 |
100 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
101 | // moduleNameMapper: {},
102 |
103 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
104 | // modulePathIgnorePatterns: [],
105 |
106 | // Activates notifications for test results
107 | // notify: false,
108 |
109 | // An enum that specifies notification mode. Requires { notify: true }
110 | // notifyMode: "failure-change",
111 |
112 | // A preset that is used as a base for Jest's configuration
113 | // preset: undefined,
114 |
115 | // Run tests from one or more projects
116 | // projects: undefined,
117 |
118 | // Use this configuration option to add custom reporters to Jest
119 | // reporters: undefined,
120 |
121 | // Automatically reset mock state between every test
122 | // resetMocks: false,
123 |
124 | // Reset the module registry before running each individual test
125 | // resetModules: false,
126 |
127 | // A path to a custom resolver
128 | // resolver: undefined,
129 |
130 | // Automatically restore mock state between every test
131 | // restoreMocks: false,
132 |
133 | // The root directory that Jest should scan for tests and modules within
134 | // rootDir: undefined,
135 |
136 | // A list of paths to directories that Jest should use to search for files in
137 | roots: ['./__tests__/'],
138 |
139 | // Allows you to use a custom runner instead of Jest's default test runner
140 | // runner: "jest-runner",
141 |
142 | // The paths to modules that run some code to configure or set up the testing environment before each test
143 | setupFiles: ['jest-canvas-mock'], // https://stackoverflow.com/a/62768292/10431732
144 |
145 | // A list of paths to modules that run some code to configure or set up the testing framework before each test
146 | // setupFilesAfterEnv: [],
147 |
148 | // The number of seconds after which a test is considered as slow and reported as such in the results.
149 | // slowTestThreshold: 5,
150 |
151 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
152 | // snapshotSerializers: [],
153 |
154 | // The test environment that will be used for testing
155 | // testEnvironment: "jest-environment-jsdom",
156 |
157 | // Options that will be passed to the testEnvironment
158 | // testEnvironmentOptions: {},
159 |
160 | // Adds a location field to test results
161 | // testLocationInResults: false,
162 |
163 | // The glob patterns Jest uses to detect test files
164 | testMatch: [
165 | // "**/__tests__/**/*.[jt]s?(x)",
166 | '**/?(*.)+(spec|test).[tj]s?(x)'
167 | ],
168 |
169 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
170 | testPathIgnorePatterns: ['/node_modules/', '/__tests__/coverage/'],
171 |
172 | // The regexp pattern or array of patterns that Jest uses to detect test files
173 | // testRegex: [],
174 |
175 | // This option allows the use of a custom results processor
176 | // testResultsProcessor: undefined,
177 |
178 | // This option allows use of a custom test runner
179 | // testRunner: "jasmine2",
180 |
181 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
182 | // testURL: "http://localhost",
183 |
184 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
185 | // timers: "real",
186 |
187 | // A map from regular expressions to paths to transformers
188 | transform: {
189 | '\\.js$': ['babel-jest', { configFile: './babel.config.js' }]
190 | },
191 |
192 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
193 | // transformIgnorePatterns: [
194 | // "/node_modules/",
195 | // "\\.pnp\\.[^\\/]+$"
196 | // ],
197 |
198 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
199 | // unmockedModulePathPatterns: undefined,
200 |
201 | // Indicates whether each individual test should be reported during the run
202 | verbose: true
203 |
204 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
205 | // watchPathIgnorePatterns: [],
206 |
207 | // Whether to use watchman for file crawling
208 | // watchman: true,
209 | };
210 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@material-table/core",
3 | "publishConfig": {
4 | "access": "public"
5 | },
6 | "url": "https://material-table-core.github.io/",
7 | "version": "6.4.4",
8 | "description": "Datatable for React based on https://material-ui.com/api/table/ with additional features",
9 | "main": "dist/index.js",
10 | "types": "types/index.d.ts",
11 | "files": [
12 | "dist",
13 | "types"
14 | ],
15 | "scripts": {
16 | "start": "npx webpack serve --config ./__tests__/demo/webpack.config.js --mode development --progress",
17 | "build:esbuild": "ts-node esbuild.config.js",
18 | "build:babel": "npx babel src -d dist",
19 | "build": "npm run build:babel",
20 | "lint": "npm run eslint && npm run tsc",
21 | "eslint": "npx eslint src/** -c ./.eslintrc --ignore-path ./.eslintignore",
22 | "tsc": "npx tsc --noEmit --lib es6,dom --skipLibCheck types/index.d.ts",
23 | "lint:fix": "npx eslint src/** -c ./.eslintrc --ignore-path ./.eslintignore --fix",
24 | "prettify": "npx prettier -c ./.prettierrc --write **/*.js",
25 | "pretest": "npm run build",
26 | "test": "npx jest",
27 | "test:build": "npx jest __tests__/post.build.test.js",
28 | "prerelease:major": "npm run test",
29 | "prerelease:minor": "npm run test",
30 | "prerelease:patch": "npm run test",
31 | "release:major": "changelog -M && git add CHANGELOG.md && git commit -m 'updated CHANGELOG.md'",
32 | "release:minor": "changelog -m && git add CHANGELOG.md && git commit -m 'updated CHANGELOG.md' && npm version minor -m 'Release v%s' && npm run git:push:tags",
33 | "release:patch": "changelog -p && git add CHANGELOG.md && git commit -m 'updated CHANGELOG.md' && npm version patch -m 'Release v%s' && npm run git:push:tags",
34 | "git:push:tags": "git push origin && git push origin --tags"
35 | },
36 | "husky": {
37 | "hooks": {
38 | "pre-commit": "npm run lint && pretty-quick --staged"
39 | }
40 | },
41 | "repository": {
42 | "type": "git",
43 | "url": "git+https://github.com/material-table-core/core.git"
44 | },
45 | "keywords": [
46 | "react",
47 | "material-ui",
48 | "material",
49 | "mui",
50 | "datatable",
51 | "table"
52 | ],
53 | "author": "Mehmet Baran, @material-table/core contributors",
54 | "license": "MIT",
55 | "bugs": {
56 | "url": "https://material-table-core.github.io/"
57 | },
58 | "homepage": "https://material-table-core.github.io/",
59 | "devDependencies": {
60 | "@babel/cli": "^7.12.10",
61 | "@babel/core": "^7.12.10",
62 | "@babel/plugin-proposal-class-properties": "^7.12.1",
63 | "@babel/plugin-proposal-object-rest-spread": "^7.12.1",
64 | "@babel/plugin-transform-runtime": "^7.12.10",
65 | "@babel/preset-env": "^7.12.11",
66 | "@babel/preset-react": "^7.12.10",
67 | "@mui/system": ">=5.10.7",
68 | "@testing-library/jest-dom": "^5.16.5",
69 | "@testing-library/react": "^13.4.0",
70 | "@types/jest": "^26.0.20",
71 | "@types/react": "^18.2.14",
72 | "@webpack-cli/serve": "^1.2.1",
73 | "babel-eslint": "^10.1.0",
74 | "babel-jest": "^26.6.3",
75 | "babel-loader": "^8.2.2",
76 | "babel-plugin-import": "^1.13.3",
77 | "babel-plugin-module-resolver": "^5.0.0",
78 | "buble": "^0.20.0",
79 | "check-dts": "^0.6.7",
80 | "core-js": "^3.31.0",
81 | "esbuild": "^0.8.57",
82 | "eslint": "^7.16.0",
83 | "eslint-config-defaults": "^9.0.0",
84 | "eslint-config-standard": "^16.0.2",
85 | "eslint-plugin-import": "^2.22.1",
86 | "eslint-plugin-node": "^11.1.0",
87 | "eslint-plugin-only-warn": "^1.0.2",
88 | "eslint-plugin-promise": "^4.2.1",
89 | "eslint-plugin-react": "^7.21.5",
90 | "generate-changelog": "^1.8.0",
91 | "husky": "1.2.0",
92 | "jest": "^26.6.3",
93 | "jest-canvas-mock": "^2.3.0",
94 | "prettier": "^2.2.1",
95 | "pretty-quick": "2.0.1",
96 | "react": ">=18.2.0",
97 | "react-dom": ">=18.2.0",
98 | "react-test-renderer": ">=16.8.0",
99 | "ts-node": "^10.1.0",
100 | "typescript": "^4.1.3",
101 | "webpack": "^5.11.0",
102 | "webpack-bundle-analyzer": "^4.3.0",
103 | "webpack-cli": "^4.10.0",
104 | "webpack-dev-server": "^3.11.0"
105 | },
106 | "dependencies": {
107 | "@babel/runtime": "^7.19.0",
108 | "@date-io/core": "^3.0.0",
109 | "@date-io/date-fns": "^3.0.0",
110 | "@emotion/core": "^11.0.0",
111 | "@emotion/react": "^11.10.4",
112 | "@emotion/styled": "^11.10.4",
113 | "@hello-pangea/dnd": "^16.0.0",
114 | "@mui/icons-material": ">=5.10.6",
115 | "@mui/material": ">=5.11.12",
116 | "@mui/x-date-pickers": "^6.19.0",
117 | "classnames": "^2.3.2",
118 | "date-fns": "^3.2.0",
119 | "debounce": "^1.2.1",
120 | "deep-eql": "^4.1.1",
121 | "deepmerge": "^4.2.2",
122 | "prop-types": "^15.8.1",
123 | "uuid": "^9.0.0",
124 | "zustand": "^4.3.0"
125 | },
126 | "peerDependencies": {
127 | "@mui/system": ">=5.15.5",
128 | "react": ">=18.0.0",
129 | "react-dom": ">=18.0.0"
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/components/Container/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Paper } from '@mui/material';
3 |
4 | function Container({ forwardedRef, ...props }) {
5 | return ;
6 | }
7 |
8 | export default React.forwardRef(function ContainerRef(props, ref) {
9 | return ;
10 | });
11 |
--------------------------------------------------------------------------------
/src/components/MTableAction/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable multiline-ternary */
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 | import Icon from '@mui/material/Icon';
5 | import IconButton from '@mui/material/IconButton';
6 | import Tooltip from '@mui/material/Tooltip';
7 |
8 | function MTableAction({
9 | action: propsAction = defaultProps.action,
10 | data = defaultProps.data,
11 | size,
12 | forwardedRef,
13 | disabled
14 | }) {
15 | let action = propsAction;
16 |
17 | if (typeof action === 'function') {
18 | action = action(data);
19 | if (!action) {
20 | return null;
21 | }
22 | }
23 |
24 | if (action.action) {
25 | action = action.action(data);
26 | if (!action) {
27 | return null;
28 | }
29 | }
30 |
31 | if (action.hidden) {
32 | return null;
33 | }
34 |
35 | const isDisabled = action.disabled || disabled;
36 |
37 | const handleOnClick = (event) => {
38 | if (action.onClick) {
39 | action.onClick(event, data);
40 | event.stopPropagation();
41 | }
42 | };
43 |
44 | // You may provide events via the "action.handlers" prop. It is an object.
45 | // The event name is the key, and the value is the handler func.
46 | const handlers = action.handlers || {};
47 | const eventHandlers = Object.entries(handlers).reduce((o, [k, v]) => {
48 | o[k] = (e) => v(e, data);
49 | return o;
50 | }, {});
51 |
52 | let icon = null;
53 | switch (typeof action.icon) {
54 | case 'string':
55 | icon = {action.icon} ;
56 | break;
57 | case 'function':
58 | icon = action.icon({ ...action.iconProps, disabled: disabled });
59 | break;
60 | case 'undefined':
61 | icon = null;
62 | break;
63 | default:
64 | icon = ;
65 | }
66 |
67 | const button = (
68 |
76 | {icon}
77 |
78 | );
79 |
80 | if (action.tooltip) {
81 | // fix for issue #1049
82 | // https://github.com/mbrn/material-table/issues/1049
83 | return isDisabled ? (
84 |
85 | {button}
86 |
87 | ) : (
88 | {button}
89 | );
90 | } else {
91 | return button;
92 | }
93 | }
94 |
95 | const defaultProps = {
96 | action: {},
97 | data: {}
98 | };
99 |
100 | MTableAction.propTypes = {
101 | action: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired,
102 | columns: PropTypes.array,
103 | data: PropTypes.oneOfType([
104 | PropTypes.object,
105 | PropTypes.arrayOf(PropTypes.object)
106 | ]),
107 | disabled: PropTypes.bool,
108 | onColumnsChanged: PropTypes.func,
109 | size: PropTypes.string
110 | };
111 |
112 | export default React.forwardRef(function MTableActionRef(props, ref) {
113 | return ;
114 | });
115 |
--------------------------------------------------------------------------------
/src/components/MTableActions/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | function MTableActions({
5 | actions,
6 | columns,
7 | components,
8 | data = {},
9 | onColumnsChanged,
10 | size,
11 | disabled,
12 | forwardedRef
13 | }) {
14 | if (!actions) {
15 | return null;
16 | }
17 | return (
18 |
19 | {actions.map((action, index) => (
20 |
29 | ))}
30 |
31 | );
32 | }
33 |
34 | MTableActions.propTypes = {
35 | columns: PropTypes.array,
36 | components: PropTypes.object.isRequired,
37 | actions: PropTypes.array.isRequired,
38 | data: PropTypes.oneOfType([
39 | PropTypes.object,
40 | PropTypes.arrayOf(PropTypes.object)
41 | ]),
42 | disabled: PropTypes.bool,
43 | onColumnsChanged: PropTypes.func,
44 | size: PropTypes.string,
45 | forwardedRef: PropTypes.element
46 | };
47 |
48 | export default React.forwardRef(function MTableActionsRef(props, ref) {
49 | return ;
50 | });
51 |
--------------------------------------------------------------------------------
/src/components/MTableCell/cellUtils.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { parseISO } from 'date-fns';
3 |
4 | /* eslint-disable no-useless-escape */
5 | export const isoDateRegex = /^\d{4}-(0[1-9]|1[0-2])-([12]\d|0[1-9]|3[01])([T\s](([01]\d|2[0-3])\:[0-5]\d|24\:00)(\:[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3])\:?([0-5]\d)?)?)?$/;
6 | /* eslint-enable no-useless-escape */
7 |
8 | export function getEmptyValue(emptyValue = '', props = {}) {
9 | if (typeof emptyValue === 'function') {
10 | return props.columnDef.emptyValue(props.rowData);
11 | } else {
12 | return emptyValue;
13 | }
14 | }
15 |
16 | export function getCurrencyValue(currencySetting, value) {
17 | if (currencySetting !== undefined) {
18 | return new Intl.NumberFormat(
19 | currencySetting.locale !== undefined ? currencySetting.locale : 'en-US',
20 | {
21 | style: 'currency',
22 | currency:
23 | currencySetting.currencyCode !== undefined
24 | ? currencySetting.currencyCode
25 | : 'USD',
26 | minimumFractionDigits:
27 | currencySetting.minimumFractionDigits !== undefined
28 | ? currencySetting.minimumFractionDigits
29 | : 2,
30 | maximumFractionDigits:
31 | currencySetting.maximumFractionDigits !== undefined
32 | ? currencySetting.maximumFractionDigits
33 | : 2
34 | }
35 | ).format(value !== undefined ? value : 0);
36 | } else {
37 | return new Intl.NumberFormat('en-US', {
38 | style: 'currency',
39 | currency: 'USD'
40 | }).format(value !== undefined ? value : 0);
41 | }
42 | }
43 |
44 | export function getRenderValue(props, icons, type) {
45 | const dateLocale =
46 | props.columnDef.dateSetting && props.columnDef.dateSetting.locale
47 | ? props.columnDef.dateSetting.locale
48 | : undefined;
49 | if (
50 | props.columnDef.emptyValue !== undefined &&
51 | (props.value === undefined || props.value === null)
52 | ) {
53 | return getEmptyValue(props.columnDef.emptyValue, props);
54 | }
55 | if (
56 | props.rowData === undefined &&
57 | props.value &&
58 | props.columnDef.groupRender
59 | ) {
60 | return props.columnDef.groupRender(props.value);
61 | } else if (props.columnDef.render && props.rowData) {
62 | return props.columnDef.render(props.rowData);
63 | } else if (props.columnDef.type === 'boolean') {
64 | const style = { textAlign: 'left', verticalAlign: 'middle', width: 48 };
65 | if (props.value) {
66 | return ;
67 | } else {
68 | return ;
69 | }
70 | } else if (props.columnDef.type === 'date') {
71 | if (props.value instanceof Date) {
72 | return props.value.toLocaleDateString(dateLocale);
73 | } else if (isoDateRegex.exec(props.value)) {
74 | return parseISO(props.value).toLocaleDateString(dateLocale);
75 | } else {
76 | return props.value;
77 | }
78 | } else if (props.columnDef.type === 'time') {
79 | if (props.value instanceof Date) {
80 | return props.value.toLocaleTimeString();
81 | } else if (isoDateRegex.exec(props.value)) {
82 | return parseISO(props.value).toLocaleTimeString(dateLocale);
83 | } else {
84 | return props.value;
85 | }
86 | } else if (props.columnDef.type === 'datetime') {
87 | if (props.value instanceof Date) {
88 | return props.value.toLocaleString();
89 | } else if (isoDateRegex.exec(props.value)) {
90 | return parseISO(props.value).toLocaleString(dateLocale);
91 | } else {
92 | return props.value;
93 | }
94 | } else if (props.columnDef.type === 'currency') {
95 | return getCurrencyValue(props.columnDef.currencySetting, props.value);
96 | } else if (typeof props.value === 'boolean') {
97 | // To avoid forwardref boolean children.
98 | return props.value.toString();
99 | }
100 |
101 | return props.value;
102 | }
103 |
--------------------------------------------------------------------------------
/src/components/MTableCell/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TableCell from '@mui/material/TableCell';
3 | import PropTypes from 'prop-types';
4 | import { getRenderValue } from './cellUtils';
5 | import { getStyle } from '@utils';
6 | import { useIconStore } from '@store';
7 |
8 | function MTableCell(props) {
9 | const icons = useIconStore();
10 | const {
11 | forwardedRef,
12 | scrollWidth,
13 | rowData,
14 | onCellEditStarted,
15 | cellEditable,
16 | columnDef = {},
17 | errorState,
18 | ...spreadProps
19 | } = props;
20 | const handleClickCell = (e) => {
21 | if (props.columnDef.disableClick) {
22 | e.stopPropagation();
23 | }
24 | };
25 |
26 | /* eslint-disable indent */
27 | const cellAlignment =
28 | columnDef.align !== undefined
29 | ? columnDef.align
30 | : ['numeric', 'currency'].indexOf(columnDef.type) !== -1
31 | ? 'right'
32 | : 'left';
33 | /* eslint-enable indent */
34 |
35 | let renderValue = getRenderValue(props, icons);
36 |
37 | if (cellEditable) {
38 | renderValue = (
39 | {
47 | e.stopPropagation();
48 | onCellEditStarted(rowData, columnDef);
49 | }}
50 | >
51 | {renderValue}
52 |
53 | );
54 | }
55 |
56 | return (
57 |
68 | {props.children}
69 | {renderValue}
70 |
71 | );
72 | }
73 |
74 | MTableCell.propTypes = {
75 | columnDef: PropTypes.object.isRequired,
76 | value: PropTypes.any,
77 | rowData: PropTypes.object,
78 | errorState: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
79 | forwardedRef: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
80 | size: PropTypes.string,
81 | colSpan: PropTypes.number,
82 | children: PropTypes.element,
83 | cellEditable: PropTypes.bool,
84 | onCellEditStarted: PropTypes.func
85 | };
86 |
87 | export default React.forwardRef(function MTableCellRef(props, ref) {
88 | return ;
89 | });
90 |
--------------------------------------------------------------------------------
/src/components/MTableCustomIcon/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Icon } from '@mui/material';
4 |
5 | export default function MTableCustomIcon({ icon, iconProps = {} }) {
6 | if (!icon) {
7 | return;
8 | }
9 | if (typeof icon === 'string') {
10 | return {icon} ;
11 | }
12 | return React.createElement(icon, { ...iconProps });
13 | }
14 |
15 | MTableCustomIcon.propTypes = {
16 | icon: PropTypes.oneOfType([PropTypes.element, PropTypes.elementType])
17 | .isRequired,
18 | iconProps: PropTypes.object
19 | };
20 |
--------------------------------------------------------------------------------
/src/components/MTableEditCell/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * THIS FILE IS NOT IN USE RIGHT NOW DUE TO REFACTORING ISSUES!
4 | *
5 | *
6 | *
7 | *
8 | * PLEASE SEE THE FOLLOWING FILE, AS IT IS THE PROD VERSION OF `MTableEditCell`:
9 | *
10 | * https://github.com/material-table-core/core/blob/master/src/components/m-table-edit-cell.js
11 | *
12 | */
13 |
14 | import React, { useState, useEffect } from 'react';
15 | import PropTypes from 'prop-types';
16 | import { TableCell, CircularProgress } from '@mui/material';
17 |
18 | function MTableEditCell({
19 | columnDef = {},
20 | localization = defaultProps.localization,
21 | ...props
22 | }) {
23 | const [state, setState] = useState(() => ({
24 | isLoading: false,
25 | value: props.rowData[columnDef.field]
26 | }));
27 |
28 | useEffect(() => {
29 | props.cellEditable
30 | .onCellEditApproved(
31 | state.value, // newValue
32 | props.rowData[columnDef.field], // oldValue
33 | props.rowData, // rowData with old value
34 | columnDef // columnDef
35 | )
36 | .then(() => {
37 | setState({ ...state, isLoading: false });
38 | props.onCellEditFinished(props.rowData, columnDef);
39 | })
40 | .catch(() => {
41 | setState({ ...state, isLoading: false });
42 | });
43 | }, []);
44 |
45 | const getStyle = () => {
46 | let cellStyle = {
47 | boxShadow: '2px 0px 15px rgba(125,147,178,.25)',
48 | color: 'inherit',
49 | width: columnDef.tableData.width,
50 | boxSizing: 'border-box',
51 | fontSize: 'inherit',
52 | fontFamily: 'inherit',
53 | fontWeight: 'inherit',
54 | padding: '0 16px'
55 | };
56 |
57 | if (typeof columnDef.cellStyle === 'function') {
58 | cellStyle = {
59 | ...cellStyle,
60 | ...columnDef.cellStyle(state.value, props.rowData)
61 | };
62 | } else {
63 | cellStyle = { ...cellStyle, ...columnDef.cellStyle };
64 | }
65 |
66 | if (typeof props.cellEditable.cellStyle === 'function') {
67 | cellStyle = {
68 | ...cellStyle,
69 | ...props.cellEditable.cellStyle(state.value, props.rowData, columnDef)
70 | };
71 | } else {
72 | cellStyle = { ...cellStyle, ...props.cellEditable.cellStyle };
73 | }
74 |
75 | return cellStyle;
76 | };
77 |
78 | const handleKeyDown = (e) => {
79 | if (e.keyCode === 13) {
80 | onApprove();
81 | } else if (e.keyCode === 27) {
82 | onCancel();
83 | }
84 | };
85 |
86 | const onApprove = () => {
87 | setState({ ...state, isLoading: true });
88 | };
89 |
90 | const onCancel = () => {
91 | props.onCellEditFinished(props.rowData, columnDef);
92 | };
93 |
94 | function renderActions() {
95 | if (state.isLoading) {
96 | return (
97 |
98 |
99 |
100 | );
101 | }
102 |
103 | const actions = [
104 | {
105 | icon: props.icons.Check,
106 | tooltip: localization && localization.saveTooltip,
107 | onClick: onApprove,
108 | disabled: state.isLoading
109 | },
110 | {
111 | icon: props.icons.Clear,
112 | tooltip: localization && localization.cancelTooltip,
113 | onClick: onCancel,
114 | disabled: state.isLoading
115 | }
116 | ];
117 |
118 | return (
119 |
124 | );
125 | }
126 |
127 | return (
128 |
134 |
135 |
136 |
setState({ ...prevState, value })}
140 | onKeyDown={handleKeyDown}
141 | disabled={state.isLoading}
142 | rowData={props.rowData}
143 | autoFocus
144 | />
145 |
146 | {renderActions()}
147 |
148 |
149 | );
150 | }
151 |
152 | const defaultProps = {
153 | localization: {
154 | saveTooltip: 'Save',
155 | cancelTooltip: 'Cancel'
156 | }
157 | };
158 |
159 | MTableEditCell.propTypes = {
160 | cellEditable: PropTypes.object.isRequired,
161 | columnDef: PropTypes.object.isRequired,
162 | components: PropTypes.object.isRequired,
163 | errorState: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
164 | icons: PropTypes.object.isRequired,
165 | localization: PropTypes.object.isRequired,
166 | onCellEditFinished: PropTypes.func.isRequired,
167 | rowData: PropTypes.object.isRequired,
168 | size: PropTypes.string,
169 | forwardedRef: PropTypes.element
170 | };
171 |
172 | export default React.forwardRef(function MTableEditCellRef(props, ref) {
173 | return ;
174 | });
175 |
--------------------------------------------------------------------------------
/src/components/MTableEditField/BooleanField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | FormControl,
4 | FormGroup,
5 | FormControlLabel,
6 | Checkbox,
7 | FormHelperText
8 | } from '@mui/material';
9 |
10 | function BooleanField({ forwardedRef, ...props }) {
11 | return (
12 |
17 |
18 | props.onChange(event.target.checked)}
26 | style={{
27 | padding: 0,
28 | width: 24,
29 | marginLeft: 9
30 | }}
31 | inputProps={{
32 | 'aria-label': props.columnDef.title
33 | }}
34 | />
35 | }
36 | />
37 |
38 | {props.helperText}
39 |
40 | );
41 | }
42 |
43 | export default React.forwardRef(function BooleanFieldRef(props, ref) {
44 | return ;
45 | });
46 |
--------------------------------------------------------------------------------
/src/components/MTableEditField/CurrencyField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { TextField } from '@mui/material';
3 |
4 | function CurrencyField({ forwardedRef, ...props }) {
5 | return (
6 | {
13 | let value = event.target.valueAsNumber;
14 | if (!value && value !== 0) {
15 | value = undefined;
16 | }
17 | return props.onChange(value);
18 | }}
19 | InputProps={{
20 | style: {
21 | fontSize: 13,
22 | textAlign: 'right'
23 | }
24 | }}
25 | inputProps={{
26 | 'aria-label': props.columnDef.title,
27 | style: { textAlign: 'right' }
28 | }}
29 | onKeyDown={props.onKeyDown}
30 | autoFocus={props.autoFocus}
31 | />
32 | );
33 | }
34 |
35 | export default React.forwardRef(function CurrencyFieldRef(props, ref) {
36 | return ;
37 | });
38 |
--------------------------------------------------------------------------------
/src/components/MTableEditField/DateField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV3';
3 | import { TextField } from '@mui/material';
4 | import { LocalizationProvider, DatePicker } from '@mui/x-date-pickers';
5 |
6 | function DateField({
7 | columnDef,
8 | value,
9 | onChange,
10 | locale,
11 | forwardedRef,
12 | ...rest
13 | }) {
14 | const getProps = () => {
15 | const {
16 | columnDef,
17 | rowData,
18 | onRowDataChange,
19 | errorState,
20 | onBulkEditRowChanged,
21 | scrollWidth,
22 | ...remaining
23 | } = rest;
24 | return remaining;
25 | };
26 |
27 | const dateFormat =
28 | columnDef.dateSetting && columnDef.dateSetting.format
29 | ? columnDef.dateSetting.format
30 | : 'dd.MM.yyyy';
31 |
32 | const datePickerProps = getProps();
33 |
34 | return (
35 |
36 | }
49 | inputProps={{
50 | 'aria-label': `${columnDef.title}: press space to edit`
51 | }}
52 | />
53 |
54 | );
55 | }
56 |
57 | export default React.forwardRef(function DateFieldRef(props, ref) {
58 | return ;
59 | });
60 |
--------------------------------------------------------------------------------
/src/components/MTableEditField/DateTimeField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import DateFnsUtils from '@date-io/date-fns';
3 | import { LocalizationProvider, DateTimePicker } from '@mui/x-date-pickers';
4 |
5 | function DateTimeField({ forwardedRef, ...props }) {
6 | return (
7 |
8 |
24 |
25 | );
26 | }
27 |
28 | export default React.forwardRef(function DateTimeFieldRef(props, ref) {
29 | return ;
30 | });
31 |
--------------------------------------------------------------------------------
/src/components/MTableEditField/LookupField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FormControl, Select, MenuItem, FormHelperText } from '@mui/material';
3 |
4 | function LookupField({ forwardedRef, ...props }) {
5 | return (
6 |
7 | props.onChange(event.target.value)}
11 | style={{
12 | fontSize: 13
13 | }}
14 | SelectDisplayProps={{ 'aria-label': props.columnDef.title }}
15 | >
16 | {Object.keys(props.columnDef.lookup).map((key) => (
17 |
18 | {props.columnDef.lookup[key]}
19 |
20 | ))}
21 |
22 | {Boolean(props.helperText) && (
23 | {props.helperText}
24 | )}
25 |
26 | );
27 | }
28 |
29 | export default React.forwardRef(function LookupFieldRef(props, ref) {
30 | return ;
31 | });
32 |
--------------------------------------------------------------------------------
/src/components/MTableEditField/TextField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { TextField } from '@mui/material';
3 |
4 | function MTextField({ forwardedRef, ...props }) {
5 | return (
6 |
14 | props.onChange(
15 | props.columnDef.type === 'numeric'
16 | ? event.target.valueAsNumber
17 | : event.target.value
18 | )
19 | }
20 | InputProps={{
21 | style: {
22 | minWidth: 50,
23 | fontSize: 13
24 | }
25 | }}
26 | inputProps={{
27 | 'aria-label': props.columnDef.title,
28 | style: props.columnDef.type === 'numeric' ? { textAlign: 'right' } : {}
29 | }}
30 | />
31 | );
32 | }
33 |
34 | export default React.forwardRef(function MTextFieldRef(props, ref) {
35 | return ;
36 | });
37 |
--------------------------------------------------------------------------------
/src/components/MTableEditField/TimeField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import DateFnsUtils from '@date-io/date-fns';
3 | import { LocalizationProvider, TimePicker } from '@mui/x-date-pickers';
4 |
5 | function TimeField({ forwardedRef, ...props }) {
6 | return (
7 |
8 |
24 |
25 | );
26 | }
27 |
28 | export default React.forwardRef(function TimeFieldRef(props, ref) {
29 | return ;
30 | });
31 |
--------------------------------------------------------------------------------
/src/components/MTableEditField/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import LookupField from './LookupField';
4 | import BooleanField from './BooleanField';
5 | import DateField from './DateField';
6 | import TimeField from './TimeField';
7 | import TextField from './TextField';
8 | import DateTimeField from './DateTimeField';
9 | import CurrencyField from './CurrencyField';
10 |
11 | function MTableEditField({ forwardedRef, ...props }) {
12 | let component = 'ok';
13 | if (props.columnDef.editComponent) {
14 | component = props.columnDef.editComponent(props);
15 | } else if (props.columnDef.lookup) {
16 | component = ;
17 | } else if (props.columnDef.type === 'boolean') {
18 | component = ;
19 | } else if (props.columnDef.type === 'date') {
20 | component = ;
21 | } else if (props.columnDef.type === 'time') {
22 | component = ;
23 | } else if (props.columnDef.type === 'datetime') {
24 | component = ;
25 | } else if (props.columnDef.type === 'currency') {
26 | component = ;
27 | } else {
28 | component = ;
29 | }
30 | return component;
31 | }
32 |
33 | MTableEditField.propTypes = {
34 | value: PropTypes.any,
35 | onChange: PropTypes.func.isRequired,
36 | columnDef: PropTypes.object.isRequired,
37 | locale: PropTypes.object
38 | };
39 |
40 | export default React.forwardRef(function MTableEditFieldRef(props, ref) {
41 | return ;
42 | });
43 |
--------------------------------------------------------------------------------
/src/components/MTableFilterRow/BooleanFilter.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Checkbox } from '@mui/material';
3 |
4 | function BooleanFilter({ forwardedRef, columnDef, onFilterChanged }) {
5 | return (
6 | {
12 | let val;
13 | if (columnDef.tableData.filterValue === undefined) {
14 | val = 'checked';
15 | } else if (columnDef.tableData.filterValue === 'checked') {
16 | val = 'unchecked';
17 | }
18 | onFilterChanged(columnDef.tableData.id, val);
19 | }}
20 | />
21 | );
22 | }
23 |
24 | export default React.forwardRef(function BooleanFilterRef(props, ref) {
25 | return ;
26 | });
27 |
--------------------------------------------------------------------------------
/src/components/MTableFilterRow/DateFilter.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV3';
3 | import TextField from '@mui/material/TextField';
4 | import { getLocalizedFilterPlaceHolder } from './utils';
5 | import {
6 | DatePicker,
7 | DateTimePicker,
8 | TimePicker,
9 | LocalizationProvider
10 | } from '@mui/x-date-pickers';
11 |
12 | function DateFilter({
13 | columnDef,
14 | onFilterChanged,
15 | localization,
16 | forwardedRef
17 | }) {
18 | const onDateInputChange = (date) =>
19 | onFilterChanged(columnDef.tableData.id, date);
20 |
21 | const pickerProps = {
22 | value: columnDef.tableData.filterValue || null,
23 | onChange: onDateInputChange,
24 | placeholder: getLocalizedFilterPlaceHolder(columnDef, localization),
25 | clearable: true
26 | };
27 | let dateInputElement = null;
28 | if (columnDef.type === 'date') {
29 | dateInputElement = (
30 | }
34 | />
35 | );
36 | } else if (columnDef.type === 'datetime') {
37 | dateInputElement = (
38 | }
42 | />
43 | );
44 | } else if (columnDef.type === 'time') {
45 | dateInputElement = (
46 | }
50 | />
51 | );
52 | }
53 |
54 | return (
55 |
59 | {dateInputElement}
60 |
61 | );
62 | }
63 |
64 | export default React.forwardRef(function DateFilterRef(props, ref) {
65 | return ;
66 | });
67 |
--------------------------------------------------------------------------------
/src/components/MTableFilterRow/DefaultFilter.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { getLocalizedFilterPlaceHolder, getLocalizationData } from './utils';
3 | import { InputAdornment, TextField, Tooltip } from '@mui/material';
4 |
5 | function DefaultFilter({
6 | columnDef,
7 | icons,
8 | localization,
9 | hideFilterIcons,
10 | onFilterChanged,
11 | forwardedRef
12 | }) {
13 | const _localization = getLocalizationData(localization);
14 | const FilterIcon = icons.Filter;
15 |
16 | return (
17 | {
28 | onFilterChanged(columnDef.tableData.id, event.target.value);
29 | }}
30 | inputProps={{ 'aria-label': `filter data by ${columnDef.title}` }}
31 | InputProps={
32 | hideFilterIcons || columnDef.hideFilterIcon
33 | ? undefined
34 | : {
35 | startAdornment: (
36 |
37 |
38 |
39 |
40 |
41 | )
42 | }
43 | }
44 | />
45 | );
46 | }
47 |
48 | export default React.forwardRef(function DefaultFilterRef(props, ref) {
49 | return ;
50 | });
51 |
--------------------------------------------------------------------------------
/src/components/MTableFilterRow/Filter.js:
--------------------------------------------------------------------------------
1 | import React, { createElement } from 'react';
2 |
3 | function Filter({ columnDef, onFilterChanged, forwardedRef }) {
4 | return createElement(columnDef.filterComponent, {
5 | columnDef,
6 | onFilterChanged,
7 | forwardedRef
8 | });
9 | }
10 |
11 | export default React.forwardRef(function FilterRef(props, ref) {
12 | return ;
13 | });
14 |
--------------------------------------------------------------------------------
/src/components/MTableFilterRow/LookupFilter.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { getLocalizedFilterPlaceHolder } from './utils';
3 |
4 | import {
5 | Checkbox,
6 | FormControl,
7 | InputLabel,
8 | ListItemText,
9 | MenuItem,
10 | Select
11 | } from '@mui/material';
12 |
13 | const ITEM_HEIGHT = 48;
14 | const ITEM_PADDING_TOP = 8;
15 |
16 | const MenuProps = {
17 | PaperProps: {
18 | style: {
19 | maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
20 | width: 250
21 | }
22 | },
23 | variant: 'menu'
24 | };
25 |
26 | function LookupFilter({
27 | columnDef,
28 | onFilterChanged,
29 | localization,
30 | forwardedRef
31 | }) {
32 | const [selectedFilter, setSelectedFilter] = useState(
33 | columnDef.tableData.filterValue || []
34 | );
35 |
36 | useEffect(() => {
37 | setSelectedFilter(columnDef.tableData.filterValue || []);
38 | }, [columnDef.tableData.filterValue]);
39 |
40 | return (
41 |
42 |
46 | {getLocalizedFilterPlaceHolder(columnDef, localization)}
47 |
48 | {
52 | if (columnDef.filterOnItemSelect !== true) {
53 | onFilterChanged(columnDef.tableData.id, selectedFilter);
54 | }
55 | }}
56 | onChange={(event) => {
57 | setSelectedFilter(event.target.value);
58 | if (columnDef.filterOnItemSelect === true) {
59 | onFilterChanged(columnDef.tableData.id, event.target.value);
60 | }
61 | }}
62 | labelId={'select-multiple-checkbox' + columnDef.tableData.id}
63 | renderValue={(selectedArr) =>
64 | selectedArr.map((selected) => columnDef.lookup[selected]).join(', ')
65 | }
66 | MenuProps={MenuProps}
67 | style={{ marginTop: 0 }}
68 | >
69 | {Object.keys(columnDef.lookup).map((key) => (
70 |
71 | -1} />
72 |
73 |
74 | ))}
75 |
76 |
77 | );
78 | }
79 |
80 | export default React.forwardRef(function LookupFilterRef(props, ref) {
81 | return ;
82 | });
83 |
--------------------------------------------------------------------------------
/src/components/MTableFilterRow/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import DateFilter from './DateFilter';
4 | import LookupFilter from './LookupFilter';
5 | import DefaultFilter from './DefaultFilter';
6 | import BooleanFilter from './BooleanFilter';
7 | import Filter from './Filter';
8 | import { TableCell, TableRow } from '@mui/material';
9 | import { useOptionStore } from '@store/LocalizationStore';
10 |
11 | /**
12 | * MTableFilterRow is the row that is shown when `MaterialTable.options.filtering` is true.
13 | * This component allows you to provide a custom filtering algo or allow/disallow filtering for a column.
14 | *
15 | * THIS MUST BE EXPORTED (on top of the 'default' export)
16 | */
17 | export function MTableFilterRow({
18 | columns: propColumns = defaultProps.columns,
19 | hasActions = false,
20 | ...props
21 | }) {
22 | const options = useOptionStore();
23 | function getComponentForColumn(columnDef) {
24 | if (columnDef.filtering === false) {
25 | return null;
26 | }
27 | if (columnDef.field || columnDef.customFilterAndSearch) {
28 | if (columnDef.filterComponent) {
29 | return ;
30 | } else if (columnDef.lookup) {
31 | return ;
32 | } else if (columnDef.type === 'boolean') {
33 | return ;
34 | } else if (['date', 'datetime', 'time'].includes(columnDef.type)) {
35 | return ;
36 | } else {
37 | return ;
38 | }
39 | }
40 | }
41 |
42 | const columns = propColumns
43 | .filter(
44 | (columnDef) => !columnDef.hidden && !(columnDef.tableData.groupOrder > -1)
45 | )
46 | .sort((a, b) => a.tableData.columnOrder - b.tableData.columnOrder)
47 | .map((columnDef) => (
48 |
55 | {getComponentForColumn(columnDef)}
56 |
57 | ));
58 |
59 | if (options.selection) {
60 | columns.splice(
61 | 0,
62 | 0,
63 |
64 | );
65 | }
66 |
67 | if (hasActions) {
68 | if (options.actionsColumnIndex === -1) {
69 | columns.push( );
70 | } else {
71 | let endPos = 0;
72 | if (props.selection) {
73 | endPos = 1;
74 | }
75 | columns.splice(
76 | options.actionsColumnIndex + endPos,
77 | 0,
78 |
79 | );
80 | }
81 | }
82 |
83 | if (props.hasDetailPanel && options.showDetailPanelIcon) {
84 | const index =
85 | options.detailPanelColumnAlignment === 'left' ? 0 : columns.length;
86 | columns.splice(
87 | index,
88 | 0,
89 |
90 | );
91 | }
92 |
93 | if (props.isTreeData > 0) {
94 | columns.splice(
95 | 0,
96 | 0,
97 |
98 | );
99 | }
100 |
101 | propColumns
102 | .filter((columnDef) => columnDef.tableData.groupOrder > -1)
103 | .forEach((columnDef) => {
104 | columns.splice(
105 | 0,
106 | 0,
107 |
111 | );
112 | });
113 |
114 | return (
115 |
120 | {columns}
121 |
122 | );
123 | }
124 |
125 | const defaultProps = {
126 | columns: [],
127 | localization: {
128 | filterTooltip: 'Filter'
129 | }
130 | };
131 |
132 | MTableFilterRow.propTypes = {
133 | columns: PropTypes.array.isRequired,
134 | hasDetailPanel: PropTypes.bool.isRequired,
135 | isTreeData: PropTypes.bool.isRequired,
136 | onFilterChanged: PropTypes.func.isRequired,
137 | hasActions: PropTypes.bool,
138 | localization: PropTypes.object
139 | };
140 |
141 | export default React.forwardRef(function MTableFilterRowRef(props, ref) {
142 | return ;
143 | });
144 |
145 | export { defaultProps };
146 |
--------------------------------------------------------------------------------
/src/components/MTableFilterRow/utils.js:
--------------------------------------------------------------------------------
1 | import { defaultProps } from './';
2 |
3 | export const getLocalizationData = (localization) => ({
4 | ...defaultProps.localization,
5 | ...localization
6 | });
7 |
8 | export const getLocalizedFilterPlaceHolder = (columnDef, localization) => {
9 | return (
10 | columnDef.filterPlaceholder ||
11 | getLocalizationData(localization).filterPlaceHolder ||
12 | ''
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/src/components/MTableGroupRow/index.js:
--------------------------------------------------------------------------------
1 | import TableCell from '@mui/material/TableCell';
2 | import TableRow from '@mui/material/TableRow';
3 | import IconButton from '@mui/material/IconButton';
4 | import Checkbox from '@mui/material/Checkbox';
5 | import PropTypes from 'prop-types';
6 | import React from 'react';
7 | import { useOptionStore, useIconStore } from '@store';
8 |
9 | function MTableGroupRow({
10 | columns = defaultProps.columns,
11 | groups = defaultProps.groups,
12 | level = 0,
13 | ...props
14 | }) {
15 | const options = useOptionStore();
16 | const icons = useIconStore();
17 | const rotateIconStyle = (isOpen) => ({
18 | transform: isOpen ? 'rotate(90deg)' : 'none'
19 | });
20 |
21 | let colSpan = columns.filter((columnDef) => !columnDef.hidden).length;
22 | options.selection && colSpan++;
23 | props.detailPanel && colSpan++;
24 | props.actions && props.actions.length > 0 && colSpan++;
25 | const column = groups[level];
26 |
27 | let detail;
28 | if (props.groupData.isExpanded) {
29 | if (groups.length > level + 1) {
30 | // Is there another group
31 | detail = props.groupData.groups.map((groupData, index) => (
32 |
59 | ));
60 | } else {
61 | detail = props.groupData.data.map((rowData, index) => {
62 | if (rowData.tableData.editing) {
63 | return (
64 |
80 | );
81 | } else {
82 | return (
83 |
107 | );
108 | }
109 | });
110 | }
111 | }
112 |
113 | const freeCells = [];
114 | for (let i = 0; i < level; i++) {
115 | freeCells.push( );
116 | }
117 |
118 | let value = props.groupData.value;
119 | if (column.lookup) {
120 | value = column.lookup[value];
121 | }
122 |
123 | let title = column.title;
124 | if (typeof options.groupTitle === 'function') {
125 | title = options.groupTitle(props.groupData);
126 | } else if (typeof column.groupTitle === 'function') {
127 | title = column.groupTitle(props.groupData);
128 | } else if (typeof title !== 'string') {
129 | title = React.cloneElement(title);
130 | }
131 |
132 | const separator = options.groupRowSeparator || ': ';
133 |
134 | const showSelectGroupCheckbox =
135 | options.selection && options.showSelectGroupCheckbox;
136 |
137 | const mapSelectedRows = (groupData) => {
138 | let totalRows = 0;
139 | let selectedRows = 0;
140 |
141 | if (showSelectGroupCheckbox) {
142 | if (groupData.data.length) {
143 | totalRows += groupData.data.length;
144 | groupData.data.forEach(
145 | (row) => row.tableData.checked && selectedRows++
146 | );
147 | } else {
148 | groupData.groups.forEach((group) => {
149 | const [groupTotalRows, groupSelectedRows] = mapSelectedRows(group);
150 |
151 | totalRows += groupTotalRows;
152 | selectedRows += groupSelectedRows;
153 | });
154 | }
155 | }
156 |
157 | return [totalRows, selectedRows];
158 | };
159 |
160 | const [totalRows, selectedRows] = mapSelectedRows(props.groupData);
161 |
162 | if (options.showGroupingCount) {
163 | value += ` (${props.groupData.data?.length ?? 0})`;
164 | }
165 | return (
166 | <>
167 |
168 | {freeCells}
169 |
176 | <>
177 | {
183 | props.onGroupExpandChanged(props.path);
184 | }}
185 | size="large"
186 | >
187 |
188 |
189 | {showSelectGroupCheckbox && (
190 | 0 && totalRows !== selectedRows}
192 | checked={totalRows === selectedRows}
193 | onChange={(event, checked) =>
194 | props.onGroupSelected &&
195 | props.onGroupSelected(checked, props.groupData.path)
196 | }
197 | style={{ marginRight: 8 }}
198 | />
199 | )}
200 |
201 | {title}
202 | {separator}
203 |
204 | >
205 |
206 |
207 | {detail}
208 | >
209 | );
210 | }
211 |
212 | const defaultProps = {
213 | columns: [],
214 | groups: []
215 | };
216 |
217 | MTableGroupRow.propTypes = {
218 | actions: PropTypes.array,
219 | columns: PropTypes.arrayOf(PropTypes.object),
220 | components: PropTypes.object,
221 | cellEditable: PropTypes.object,
222 | detailPanel: PropTypes.oneOfType([
223 | PropTypes.func,
224 | PropTypes.arrayOf(PropTypes.object)
225 | ]),
226 | forwardedRef: PropTypes.element,
227 | getFieldValue: PropTypes.func,
228 | groupData: PropTypes.object,
229 | groups: PropTypes.arrayOf(PropTypes.object),
230 | hasAnyEditingRow: PropTypes.bool,
231 | icons: PropTypes.object,
232 | isTreeData: PropTypes.bool.isRequired,
233 | level: PropTypes.number,
234 | localization: PropTypes.object,
235 | onBulkEditRowChanged: PropTypes.func,
236 | onCellEditFinished: PropTypes.func,
237 | onCellEditStarted: PropTypes.func,
238 | onEditingApproved: PropTypes.func,
239 | onEditingCanceled: PropTypes.func,
240 | onGroupExpandChanged: PropTypes.func,
241 | onRowClick: PropTypes.func,
242 | onGroupSelected: PropTypes.func,
243 | onRowSelected: PropTypes.func,
244 | onToggleDetailPanel: PropTypes.func.isRequired,
245 | onTreeExpandChanged: PropTypes.func.isRequired,
246 | path: PropTypes.arrayOf(PropTypes.number),
247 | scrollWidth: PropTypes.number.isRequired,
248 | treeDataMaxLevel: PropTypes.number
249 | };
250 |
251 | export default React.forwardRef(function MTableGroupRowRef(props, ref) {
252 | return ;
253 | });
254 |
--------------------------------------------------------------------------------
/src/components/MTableGroupbar/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import Toolbar from '@mui/material/Toolbar';
3 | import Chip from '@mui/material/Chip';
4 | import Typography from '@mui/material/Typography';
5 | import PropTypes from 'prop-types';
6 | import React, { useEffect } from 'react';
7 | import { Droppable, Draggable } from '@hello-pangea/dnd';
8 | import { useLocalizationStore, useIconStore } from '@store';
9 | import { Box } from '@mui/material';
10 | import { useOptionStore } from '../../store/LocalizationStore';
11 | /* eslint-enable no-unused-vars */
12 |
13 | function MTableGroupbar(props) {
14 | const localization = useLocalizationStore().grouping;
15 | const icons = useIconStore();
16 | const options = useOptionStore();
17 | const getItemStyle = (isDragging, draggableStyle) => ({
18 | // some basic styles to make the items look a bit nicer
19 | userSelect: 'none',
20 | // padding: '8px 16px',
21 | margin: `0 ${8}px 0 0`,
22 |
23 | // change background colour if dragging
24 | // background: isDragging ? 'lightgreen' : 'grey',
25 |
26 | // styles we need to apply on draggables
27 | ...draggableStyle
28 | });
29 |
30 | const getListStyle = (isDraggingOver) => ({
31 | // background: isDraggingOver ? 'lightblue' : '#0000000a',
32 | background: '#0000000a',
33 | display: 'flex',
34 | width: '100%',
35 | padding: 1,
36 | overflow: 'auto',
37 | border: '1px solid #ccc',
38 | borderStyle: 'dashed'
39 | });
40 |
41 | useEffect(() => {
42 | if (props.persistentGroupingsId) {
43 | const persistentGroupings = props.groupColumns.map((column) => ({
44 | field: column.field,
45 | groupOrder: column.tableData.groupOrder,
46 | groupSort: column.tableData.groupSort,
47 | columnOrder: column.tableData.columnOrder
48 | }));
49 |
50 | let materialTableGroupings = localStorage.getItem(
51 | 'material-table-groupings'
52 | );
53 | if (materialTableGroupings) {
54 | materialTableGroupings = JSON.parse(materialTableGroupings);
55 | } else {
56 | materialTableGroupings = {};
57 | }
58 |
59 | if (persistentGroupings.length === 0) {
60 | delete materialTableGroupings[props.persistentGroupingsId];
61 |
62 | if (Object.keys(materialTableGroupings).length === 0) {
63 | localStorage.removeItem('material-table-groupings');
64 | } else {
65 | localStorage.setItem(
66 | 'material-table-groupings',
67 | JSON.stringify(materialTableGroupings)
68 | );
69 | }
70 | } else {
71 | materialTableGroupings[props.persistentGroupingsId] =
72 | persistentGroupings;
73 | localStorage.setItem(
74 | 'material-table-groupings',
75 | JSON.stringify(materialTableGroupings)
76 | );
77 | }
78 | }
79 | props.onGroupChange && props.onGroupChange(props.groupColumns);
80 | }, [props.groupColumns]);
81 |
82 | return (
83 |
88 |
93 | {(provided, snapshot) => (
94 |
98 | {props.groupColumns.length > 0 && (
99 |
100 | {localization.groupedBy}
101 |
102 | )}
103 | {props.groupColumns.map((columnDef, index) => {
104 | return (
105 |
110 | {(provided, snapshot) => (
111 |
120 | props.onSortChanged(columnDef)}
124 | label={
125 |
126 | {columnDef.title}
127 | {columnDef.tableData.groupSort && (
128 |
138 | )}
139 |
140 | }
141 | sx={{
142 | boxShadow: 'none',
143 | textTransform: 'none',
144 | ...(options.groupChipProps ?? {})
145 | }}
146 | onDelete={() => props.onGroupRemoved(columnDef, index)}
147 | />
148 |
149 | )}
150 |
151 | );
152 | })}
153 | {props.groupColumns.length === 0 && (
154 |
155 | {localization.placeholder}
156 |
157 | )}
158 | {provided.placeholder}
159 |
160 | )}
161 |
162 |
163 | );
164 | }
165 |
166 | MTableGroupbar.propTypes = {
167 | forwardedRef: PropTypes.element,
168 | className: PropTypes.string,
169 | onSortChanged: PropTypes.func,
170 | onGroupRemoved: PropTypes.func,
171 | onGroupChange: PropTypes.func,
172 | persistentGroupingsId: PropTypes.string
173 | };
174 |
175 | export default React.forwardRef(function MTableGroupbarRef(props, ref) {
176 | return ;
177 | });
178 |
--------------------------------------------------------------------------------
/src/components/MTablePagination/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import IconButton from '@mui/material/IconButton';
3 | import Tooltip from '@mui/material/Tooltip';
4 | import Typography from '@mui/material/Typography';
5 | import PropTypes from 'prop-types';
6 | import React from 'react';
7 | import { Box } from '@mui/material';
8 | import { useTheme } from '@mui/material/styles';
9 | import * as CommonValues from '../../utils/common-values';
10 | import { useLocalizationStore, useIconStore } from '@store/LocalizationStore';
11 | /* eslint-enable no-unused-vars */
12 |
13 | function MTablePagination(props) {
14 | const theme = useTheme();
15 | const icons = useIconStore();
16 | const localization = useLocalizationStore().pagination;
17 |
18 | if (process.env.NODE_ENV === 'development' && !props.onPageChange) {
19 | console.error(
20 | 'The prop `onPageChange` in pagination is undefined and paging does not work. ' +
21 | 'This is most likely caused by an old material-ui version <= 4.11.X.' +
22 | 'To fix this, install either material-ui >=4.12 or downgrade material-table-core to <=3.0.15.'
23 | );
24 | }
25 | if (process.env.NODE_ENV === 'development' && localization.labelRowsSelect) {
26 | console.warn(
27 | 'The prop `labelRowsSelect` was renamed to labelDisplayedRows. Please rename the prop accordingly: https://mui.com/material-ui/api/table-pagination/#main-content.'
28 | );
29 | }
30 | const handleFirstPageButtonClick = (event) => {
31 | props.onPageChange(event, 0);
32 | };
33 |
34 | const handleBackButtonClick = (event) => {
35 | props.onPageChange(event, props.page - 1);
36 | };
37 |
38 | const handleNextButtonClick = (event) => {
39 | props.onPageChange(event, props.page + 1);
40 | };
41 |
42 | const handleLastPageButtonClick = (event) => {
43 | props.onPageChange(
44 | event,
45 | Math.max(0, Math.ceil(props.count / props.rowsPerPage) - 1)
46 | );
47 | };
48 |
49 | const { count, page, rowsPerPage, showFirstLastPageButtons = true } = props;
50 |
51 | const { first, last } = CommonValues.parseFirstLastPageButtons(
52 | showFirstLastPageButtons,
53 | theme.direction === 'rtl'
54 | );
55 | return (
56 |
65 | {first && (
66 |
67 |
68 |
74 | {theme.direction === 'rtl' ? (
75 |
76 | ) : (
77 |
78 | )}
79 |
80 |
81 |
82 | )}
83 |
84 |
85 |
90 | {theme.direction === 'rtl' ? (
91 |
92 | ) : (
93 |
94 | )}
95 |
96 |
97 |
98 |
107 | {localization.labelDisplayedRows
108 | .replace(
109 | '{from}',
110 | props.count === 0 ? 0 : props.page * props.rowsPerPage + 1
111 | )
112 | .replace(
113 | '{to}',
114 | Math.min((props.page + 1) * props.rowsPerPage, props.count)
115 | )
116 | .replace('{count}', props.count)}
117 |
118 |
119 |
120 | = Math.ceil(count / rowsPerPage) - 1}
123 | aria-label={localization.nextAriaLabel}
124 | >
125 | {theme.direction === 'rtl' ? (
126 |
127 | ) : (
128 |
129 | )}
130 |
131 |
132 |
133 | {last && (
134 |
135 |
136 | = Math.ceil(count / rowsPerPage) - 1}
139 | aria-label={localization.lastAriaLabel}
140 | size="large"
141 | >
142 | {theme.direction === 'rtl' ? (
143 |
144 | ) : (
145 |
146 | )}
147 |
148 |
149 |
150 | )}
151 |
152 | );
153 | }
154 |
155 | MTablePagination.propTypes = {
156 | onPageChange: PropTypes.func,
157 | page: PropTypes.number,
158 | count: PropTypes.number,
159 | rowsPerPage: PropTypes.number,
160 | classes: PropTypes.object,
161 | localization: PropTypes.object,
162 | showFirstLastPageButtons: PropTypes.oneOfType([
163 | PropTypes.object,
164 | PropTypes.bool
165 | ]),
166 | forwardedRef: PropTypes.func
167 | };
168 |
169 | const MTableGroupRowRef = React.forwardRef(function MTablePaginationRef(
170 | props,
171 | ref
172 | ) {
173 | return ;
174 | });
175 |
176 | const MTablePaginationOuter = MTableGroupRowRef;
177 |
178 | export default MTablePaginationOuter;
179 |
--------------------------------------------------------------------------------
/src/components/MTableScrollbar/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box } from '@mui/material';
3 |
4 | const doubleStyle = {
5 | overflowX: 'auto',
6 | position: 'relative'
7 | };
8 |
9 | const singleStyle = {
10 | ...doubleStyle,
11 | '& ::-webkit-scrollbar': {
12 | WebkitAppearance: 'none'
13 | },
14 | '& ::-webkit-scrollbar:horizontal': {
15 | height: 8
16 | },
17 | '& ::-webkit-scrollbar-thumb': {
18 | backgroundColor: 'rgba(0, 0, 0, .3)',
19 | border: '2px solid white',
20 | borderRadius: 4
21 | }
22 | };
23 |
24 | const ScrollBar = ({ double, children }) => {
25 | return {children} ;
26 | };
27 |
28 | export default ScrollBar;
29 |
--------------------------------------------------------------------------------
/src/components/MTableSteppedPaginationInner/index.js:
--------------------------------------------------------------------------------
1 | import IconButton from '@mui/material/IconButton';
2 | import Tooltip from '@mui/material/Tooltip';
3 | import Box from '@mui/material/Box';
4 | import Button from '@mui/material/Button';
5 | import PropTypes from 'prop-types';
6 | import React from 'react';
7 | import { useTheme } from '@mui/material/styles';
8 | import * as CommonValues from '../../utils/common-values';
9 | import { useLocalizationStore, useIconStore } from '@store';
10 |
11 | function MTablePaginationInner(props) {
12 | const theme = useTheme();
13 | const localization = useLocalizationStore().pagination;
14 | const icons = useIconStore();
15 | const handleFirstPageButtonClick = (event) => {
16 | props.onPageChange(event, 0);
17 | };
18 |
19 | const handleBackButtonClick = (event) => {
20 | props.onPageChange(event, props.page - 1);
21 | };
22 |
23 | const handleNextButtonClick = (event) => {
24 | props.onPageChange(event, props.page + 1);
25 | };
26 |
27 | const handleNumberButtonClick = (number) => (event) => {
28 | props.onPageChange(event, number);
29 | };
30 |
31 | const handleLastPageButtonClick = (event) => {
32 | props.onPageChange(
33 | event,
34 | Math.max(0, Math.ceil(props.count / props.rowsPerPage) - 1)
35 | );
36 | };
37 |
38 | function renderPagesButton(start, end, maxPages, numberOfPagesAround) {
39 | const buttons = [];
40 |
41 | // normalize to 1 - 10
42 | numberOfPagesAround = Math.max(1, Math.min(10, numberOfPagesAround));
43 |
44 | for (
45 | let p = Math.max(start - numberOfPagesAround + 1, 0);
46 | p <= Math.min(end + numberOfPagesAround - 1, maxPages);
47 | p++
48 | ) {
49 | const buttonVariant = p === props.page ? 'contained' : 'text';
50 | buttons.push(
51 |
65 | {p + 1}
66 |
67 | );
68 | }
69 |
70 | return {buttons} ;
71 | }
72 |
73 | const {
74 | count,
75 | page,
76 | rowsPerPage,
77 | showFirstLastPageButtons = true,
78 | numberOfPagesAround
79 | } = props;
80 |
81 | const maxPages = Math.ceil(count / rowsPerPage) - 1;
82 |
83 | const pageStart = Math.max(page - 1, 0);
84 | const pageEnd = Math.min(maxPages, page + 1);
85 | const { first, last } = CommonValues.parseFirstLastPageButtons(
86 | showFirstLastPageButtons,
87 | theme.direction === 'rtl'
88 | );
89 | return (
90 |
100 | {first && (
101 |
102 |
103 |
109 | {theme.direction === 'rtl' ? (
110 |
111 | ) : (
112 |
113 | )}
114 |
115 |
116 |
117 | )}
118 |
119 |
120 |
125 |
126 |
127 |
128 |
129 |
130 | {renderPagesButton(pageStart, pageEnd, maxPages, numberOfPagesAround)}
131 |
132 |
133 |
134 | = maxPages}
137 | aria-label={localization.nextAriaLabel}
138 | size="large"
139 | >
140 | {theme.direction === 'rtl' ? (
141 |
142 | ) : (
143 |
144 | )}
145 |
146 |
147 |
148 | {last && (
149 |
150 |
151 | = Math.ceil(count / rowsPerPage) - 1}
154 | aria-label={localization.lastAriaLabel}
155 | size="large"
156 | >
157 | {theme.direction === 'rtl' ? (
158 |
159 | ) : (
160 |
161 | )}
162 |
163 |
164 |
165 | )}
166 |
167 | );
168 | }
169 |
170 | MTablePaginationInner.propTypes = {
171 | onPageChange: PropTypes.func,
172 | page: PropTypes.number,
173 | forwardedRef: PropTypes.func,
174 | count: PropTypes.number,
175 | rowsPerPage: PropTypes.number,
176 | numberOfPagesAround: PropTypes.number,
177 | classes: PropTypes.object,
178 | theme: PropTypes.any,
179 | showFirstLastPageButtons: PropTypes.oneOfType([
180 | PropTypes.object,
181 | PropTypes.bool
182 | ])
183 | };
184 |
185 | const MTableSteppedPaginationRef = React.forwardRef(
186 | function MTableSteppedPaginationRef(props, ref) {
187 | return ;
188 | }
189 | );
190 |
191 | const MTableSteppedPagination = MTableSteppedPaginationRef;
192 |
193 | export default MTableSteppedPagination;
194 |
--------------------------------------------------------------------------------
/src/components/MTableSummaryRow/index.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { TableRow, TableCell } from '@mui/material';
3 | import { getStyle } from '@utils';
4 | import * as CommonValues from '@utils/common-values';
5 | import { useOptionStore } from '@store';
6 | import PropTypes from 'prop-types';
7 |
8 | export function MTableSummaryRow({ columns, rowProps, renderSummaryRow }) {
9 | const options = useOptionStore();
10 | if (!renderSummaryRow) {
11 | return null;
12 | }
13 |
14 | function renderPlaceholderColumn(key, numIcons = 1) {
15 | const size = CommonValues.elementSize({ ...rowProps, options });
16 | const width =
17 | numIcons * CommonValues.baseIconSize({ ...rowProps, options });
18 | return (
19 |
29 | );
30 | }
31 | const placeholderLeftColumns = [];
32 | const placeholderRightColumns = [];
33 | let placeholderKey = 0;
34 |
35 | // Create empty columns corresponding to selection, actions, detail panel, and tree data icons
36 | if (options.selection) {
37 | placeholderLeftColumns.push(renderPlaceholderColumn(placeholderKey++));
38 | }
39 | if (
40 | rowProps.actions &&
41 | rowProps.actions.filter(
42 | (a) => a.position === 'row' || typeof a === 'function'
43 | ).length > 0
44 | ) {
45 | const numRowActions = CommonValues.rowActions(rowProps).length;
46 | if (options.actionsColumnIndex === -1) {
47 | placeholderRightColumns.push(
48 | renderPlaceholderColumn(placeholderKey++, numRowActions)
49 | );
50 | } else if (options.actionsColumnIndex >= 0) {
51 | placeholderLeftColumns.push(
52 | renderPlaceholderColumn(placeholderKey++, numRowActions)
53 | );
54 | }
55 | }
56 | if (rowProps.detailPanel && options.showDetailPanelIcon) {
57 | if (options.detailPanelColumnAlignment === 'right') {
58 | placeholderRightColumns.push(renderPlaceholderColumn(placeholderKey++));
59 | } else {
60 | placeholderLeftColumns.push(renderPlaceholderColumn(placeholderKey++));
61 | }
62 | }
63 | if (rowProps.isTreeData) {
64 | placeholderLeftColumns.push(renderPlaceholderColumn(placeholderKey++));
65 | }
66 | return (
67 |
68 | {placeholderLeftColumns}
69 | {[...columns]
70 | .sort((a, b) => a.tableData.columnOrder - b.tableData.columnOrder)
71 | .map((column, index) => {
72 | const summaryColumn = renderSummaryRow({
73 | index: column.tableData.columnOrder,
74 | column,
75 | columns
76 | });
77 | const cellAlignment =
78 | column.align !== undefined
79 | ? column.align
80 | : ['numeric', 'currency'].indexOf(column.type) !== -1
81 | ? 'right'
82 | : 'left';
83 |
84 | let value = '';
85 | let style = getStyle({ columnDef: column, scrollWidth: 0 });
86 |
87 | if (typeof summaryColumn === 'object' && summaryColumn !== null) {
88 | value = summaryColumn.value;
89 | style = summaryColumn.style;
90 | } else {
91 | value = summaryColumn;
92 | }
93 | return (
94 |
95 | {value}
96 |
97 | );
98 | })}
99 | {placeholderRightColumns}
100 |
101 | );
102 | }
103 |
104 | MTableSummaryRow.propTypes = {
105 | columns: PropTypes.array,
106 | renderSummaryRow: PropTypes.func
107 | };
108 |
109 | export default MTableSummaryRow;
110 |
--------------------------------------------------------------------------------
/src/components/Overlay/OverlayError.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { useTheme } from '@mui/material/styles';
4 | import { useIconStore } from '@store';
5 |
6 | function OverlayError(props) {
7 | const icons = useIconStore();
8 | const theme = useTheme();
9 | return (
10 |
20 |
29 | {props.error.message} {' '}
30 |
34 |
35 |
36 | );
37 | }
38 |
39 | OverlayError.propTypes = {
40 | error: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
41 | retry: PropTypes.func,
42 | theme: PropTypes.any
43 | };
44 |
45 | export default React.forwardRef(function OverlayErrorRef(props, ref) {
46 | return ;
47 | });
48 |
--------------------------------------------------------------------------------
/src/components/Overlay/OverlayLoading.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { CircularProgress } from '@mui/material';
4 | import { useTheme } from '@mui/material/styles';
5 |
6 | function OverlayLoading(props) {
7 | const theme = useTheme();
8 | return (
9 |
31 | );
32 | }
33 | OverlayLoading.propTypes = {
34 | theme: PropTypes.any
35 | };
36 |
37 | export default React.forwardRef(function OverlayLoadingRef(props, ref) {
38 | return ;
39 | });
40 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | /** -------------
2 | * Misc
3 | -------------- */
4 |
5 | export { default as OverlayLoading } from './Overlay/OverlayLoading.js';
6 | export { default as OverlayError } from './Overlay/OverlayError.js';
7 | export { default as Container } from './Container';
8 | export { default as MTableScrollbar } from './MTableScrollbar';
9 |
10 | /** ---------------------------
11 | * Class based components
12 | * (aka original)
13 | --------------------------- */
14 |
15 | /** Still needs to be refactored into functional */
16 | export { default as MTableBody } from './m-table-body';
17 | /** HAVING ISSUES WITH THE REFACTORED VERSIONS OF: */
18 | export { default as MTableEditField } from './m-table-edit-field';
19 | export { default as MTableEditCell } from './m-table-edit-cell';
20 |
21 | /** ---------------------------
22 | * Functional components
23 | * (aka refactor)
24 | --------------------------- */
25 |
26 | // Trying to keep these in alphabetical order
27 | export { default as MTableAction } from './MTableAction';
28 | export { default as MTableActions } from './MTableActions';
29 | export { default as MTableBodyRow } from './MTableBodyRow';
30 | export { default as MTableCell } from './MTableCell';
31 | export { default as MTableCustomIcon } from './MTableCustomIcon';
32 | export { default as MTableEditRow } from './MTableEditRow';
33 | export { default as MTableFilterRow } from './MTableFilterRow';
34 | export { default as MTableGroupbar } from './MTableGroupbar';
35 | export { default as MTableGroupRow } from './MTableGroupRow';
36 | export { default as MTableHeader } from './MTableHeader';
37 | export { default as MTableSteppedPagination } from './MTableSteppedPaginationInner';
38 | export { default as MTablePagination } from './MTablePagination';
39 | export { default as MTableSummaryRow } from './MTableSummaryRow';
40 | export { default as MTableToolbar } from './MTableToolbar';
41 | /** THESE REFACTORS ARE HAVING ISSUES */
42 | // export { default as MTableEditCell } from './MTableEditCell';
43 | // export { default as MTableEditField } from './MTableEditField';
44 |
--------------------------------------------------------------------------------
/src/components/m-table-detailpanel.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { TableCell, Collapse, TableRow } from '@mui/material';
3 |
4 | function MTableDetailPanel(props) {
5 | const shouldOpen = Boolean(
6 | props.data.tableData && props.data.tableData.showDetailPanel
7 | );
8 |
9 | const [isOpen, setOpen] = React.useState(shouldOpen);
10 | const [, rerender] = React.useReducer((s) => s + 1, 0);
11 | const renderRef = React.useRef();
12 |
13 | React.useEffect(() => {
14 | setTimeout(() => {
15 | setOpen(shouldOpen);
16 | }, 5);
17 | }, [shouldOpen]);
18 |
19 | let renderFunction;
20 |
21 | React.useEffect(() => {
22 | if (renderFunction && isOpen) {
23 | renderRef.current = renderFunction;
24 | }
25 | });
26 |
27 | // See issue #282 for more on why we have to check for the existence of props.detailPanel
28 | if (!props.detailPanel) {
29 | return ;
30 | } else {
31 | if (typeof props.detailPanel === 'function') {
32 | renderFunction = props.detailPanel;
33 | } else {
34 | renderFunction = props.detailPanel
35 | ? props.detailPanel
36 | .map((panel) =>
37 | typeof panel === 'function' ? panel(props.data) : panel
38 | )
39 | .find(
40 | (panel) =>
41 | panel.render.toString() ===
42 | (props.data.tableData.showDetailPanel || '').toString()
43 | )
44 | : undefined;
45 | renderFunction = renderFunction ? renderFunction.render : null;
46 | }
47 | }
48 |
49 | if (!renderRef.current && !props.data.tableData.showDetailPanel) {
50 | return null;
51 | }
52 | const Render = renderFunction || renderRef.current;
53 | return (
54 |
55 | {props.options.detailPanelOffset.left > 0 && (
56 |
57 | )}
58 |
67 | {
73 | renderRef.current = undefined;
74 | rerender();
75 | }}
76 | >
77 | {Render({ rowData: props.data })}
78 |
79 |
80 |
81 | );
82 | }
83 |
84 | export { MTableDetailPanel };
85 |
--------------------------------------------------------------------------------
/src/components/m-table-edit-cell.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import TableCell from '@mui/material/TableCell';
4 | import CircularProgress from '@mui/material/CircularProgress';
5 | import { validateInput } from '../utils/validate';
6 | class MTableEditCell extends React.Component {
7 | constructor(props) {
8 | super(props);
9 |
10 | this.state = {
11 | errorState: {
12 | isValid: true,
13 | helperText: ''
14 | },
15 | isLoading: false,
16 | value: props.getFieldValue(
17 | this.props.rowData,
18 | this.props.columnDef,
19 | false
20 | )
21 | };
22 | }
23 |
24 | getStyle = () => {
25 | let cellStyle = {
26 | boxShadow: '2px 0px 15px rgba(125,147,178,.25)',
27 | color: 'inherit',
28 | width: this.props.columnDef.tableData.width,
29 | boxSizing: 'border-box',
30 | fontSize: 'inherit',
31 | fontFamily: 'inherit',
32 | fontWeight: 'inherit',
33 | padding: '0 16px'
34 | };
35 |
36 | if (typeof this.props.columnDef.cellStyle === 'function') {
37 | cellStyle = {
38 | ...cellStyle,
39 | ...this.props.columnDef.cellStyle(this.state.value, this.props.rowData)
40 | };
41 | } else {
42 | cellStyle = { ...cellStyle, ...this.props.columnDef.cellStyle };
43 | }
44 |
45 | if (typeof this.props.cellEditable.cellStyle === 'function') {
46 | cellStyle = {
47 | ...cellStyle,
48 | ...this.props.cellEditable.cellStyle(
49 | this.state.value,
50 | this.props.rowData,
51 | this.props.columnDef
52 | )
53 | };
54 | } else {
55 | cellStyle = { ...cellStyle, ...this.props.cellEditable.cellStyle };
56 | }
57 |
58 | return cellStyle;
59 | };
60 |
61 | handleKeyDown = (e) => {
62 | if (e.keyCode === 13) {
63 | this.onApprove();
64 | } else if (e.keyCode === 27) {
65 | this.onCancel();
66 | }
67 | };
68 |
69 | onApprove = () => {
70 | const isValid = validateInput(this.props.columnDef, this.state.value)
71 | .isValid;
72 | if (!isValid) {
73 | return;
74 | }
75 | this.setState({ isLoading: true }, () => {
76 | this.props.cellEditable
77 | .onCellEditApproved(
78 | this.state.value, // newValue
79 | this.props.getFieldValue(this.props.rowData, this.props.columnDef), // oldValue
80 | this.props.rowData, // rowData with old value
81 | this.props.columnDef // columnDef
82 | )
83 | .then(() => {
84 | this.setState({ isLoading: false });
85 | this.props.onCellEditFinished(
86 | this.props.rowData,
87 | this.props.columnDef
88 | );
89 | })
90 | .catch((error) => {
91 | if (process.env.NODE_ENV === 'development') console.log(error);
92 | this.setState({ isLoading: false });
93 | });
94 | });
95 | };
96 |
97 | onCancel = () => {
98 | this.props.onCellEditFinished(this.props.rowData, this.props.columnDef);
99 | };
100 |
101 | renderActions() {
102 | if (this.state.isLoading) {
103 | return (
104 |
105 |
106 |
107 | );
108 | }
109 |
110 | const actions = [
111 | {
112 | icon: this.props.icons.Check,
113 | tooltip: this.props.localization.saveTooltip,
114 | onClick: this.onApprove,
115 | disabled: this.state.isLoading || !this.state.errorState.isValid
116 | },
117 | {
118 | icon: this.props.icons.Clear,
119 | tooltip: this.props.localization.cancelTooltip,
120 | onClick: this.onCancel,
121 | disabled: this.state.isLoading
122 | }
123 | ];
124 |
125 | return (
126 |
131 | );
132 | }
133 |
134 | handleChange(value) {
135 | const errorState = validateInput(this.props.columnDef, value);
136 | this.setState({ errorState, value });
137 | }
138 |
139 | render() {
140 | return (
141 |
142 |
143 |
144 | this.handleChange(value)}
150 | onKeyDown={this.handleKeyDown}
151 | disabled={this.state.isLoading}
152 | rowData={this.props.rowData}
153 | autoFocus
154 | />
155 |
156 | {this.renderActions()}
157 |
158 |
159 | );
160 | }
161 | }
162 |
163 | MTableEditCell.defaultProps = {
164 | columnDef: {},
165 | localization: {
166 | saveTooltip: 'Save',
167 | cancelTooltip: 'Cancel'
168 | }
169 | };
170 |
171 | MTableEditCell.propTypes = {
172 | cellEditable: PropTypes.object.isRequired,
173 | columnDef: PropTypes.object.isRequired,
174 | components: PropTypes.object.isRequired,
175 | errorState: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
176 | icons: PropTypes.object.isRequired,
177 | localization: PropTypes.object.isRequired,
178 | onCellEditFinished: PropTypes.func.isRequired,
179 | rowData: PropTypes.object.isRequired,
180 | size: PropTypes.string,
181 | getFieldValue: PropTypes.func.isRequired
182 | };
183 |
184 | export default MTableEditCell;
185 |
--------------------------------------------------------------------------------
/src/components/m-table-edit-field.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TextField from '@mui/material/TextField';
3 | import Checkbox from '@mui/material/Checkbox';
4 | import Select from '@mui/material/Select';
5 | import MenuItem from '@mui/material/MenuItem';
6 | import FormControl from '@mui/material/FormControl';
7 | import FormHelperText from '@mui/material/FormHelperText';
8 | import FormGroup from '@mui/material/FormGroup';
9 | import FormControlLabel from '@mui/material/FormControlLabel';
10 | import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV3';
11 | import {
12 | LocalizationProvider,
13 | TimePicker,
14 | DatePicker,
15 | DateTimePicker
16 | } from '@mui/x-date-pickers';
17 | import PropTypes from 'prop-types';
18 |
19 | class MTableEditField extends React.Component {
20 | getProps() {
21 | const {
22 | columnDef,
23 | rowData,
24 | onRowDataChange,
25 | errorState,
26 | autoFocus,
27 | onBulkEditRowChanged,
28 | scrollWidth,
29 | ...props
30 | } = this.props;
31 | return props;
32 | }
33 |
34 | renderLookupField() {
35 | const { helperText, error, ...props } = this.getProps();
36 | return (
37 |
38 | this.props.onChange(event.target.value)}
42 | style={{
43 | fontSize: 13
44 | }}
45 | inputProps={{
46 | autoFocus: this.props.autoFocus
47 | }}
48 | SelectDisplayProps={{ 'aria-label': this.props.columnDef.title }}
49 | >
50 | {Object.keys(this.props.columnDef.lookup).map((key) => (
51 |
52 | {this.props.columnDef.lookup[key]}
53 |
54 | ))}
55 |
56 | {Boolean(helperText) && {helperText} }
57 |
58 | );
59 | }
60 |
61 | renderBooleanField() {
62 | const { helperText, error, ...props } = this.getProps();
63 |
64 | return (
65 |
66 |
67 | this.props.onChange(event.target.checked)}
75 | style={{
76 | padding: 0,
77 | width: 24,
78 | marginLeft: 9
79 | }}
80 | inputProps={{
81 | autoFocus: this.props.autoFocus,
82 | 'aria-label': this.props.columnDef.title
83 | }}
84 | />
85 | }
86 | />
87 |
88 | {helperText}
89 |
90 | );
91 | }
92 |
93 | renderDateField() {
94 | const dateFormat =
95 | this.props.columnDef.dateSetting &&
96 | this.props.columnDef.dateSetting.format
97 | ? this.props.columnDef.dateSetting.format
98 | : 'dd.MM.yyyy';
99 | return (
100 |
104 | }
107 | format={dateFormat}
108 | value={this.props.value || null}
109 | onChange={this.props.onChange}
110 | clearable
111 | InputProps={{
112 | style: {
113 | fontSize: 13
114 | }
115 | }}
116 | inputProps={{
117 | autoFocus: this.props.autoFocus,
118 | 'aria-label': `${this.props.columnDef.title}: press space to edit`
119 | }}
120 | />
121 |
122 | );
123 | }
124 |
125 | renderTimeField() {
126 | return (
127 |
131 | }
134 | format="HH:mm:ss"
135 | value={this.props.value || null}
136 | onChange={this.props.onChange}
137 | clearable
138 | InputProps={{
139 | style: {
140 | fontSize: 13
141 | }
142 | }}
143 | inputProps={{
144 | autoFocus: this.props.autoFocus,
145 | 'aria-label': `${this.props.columnDef.title}: press space to edit`
146 | }}
147 | />
148 |
149 | );
150 | }
151 |
152 | renderDateTimeField() {
153 | return (
154 |
158 | }
161 | format="dd.MM.yyyy HH:mm:ss"
162 | value={this.props.value || null}
163 | onChange={this.props.onChange}
164 | clearable
165 | InputProps={{
166 | style: {
167 | fontSize: 13
168 | }
169 | }}
170 | inputProps={{
171 | autoFocus: this.props.autoFocus,
172 | 'aria-label': `${this.props.columnDef.title}: press space to edit`
173 | }}
174 | />
175 |
176 | );
177 | }
178 |
179 | renderTextField() {
180 | return (
181 |
191 | this.props.onChange(
192 | this.props.columnDef.type === 'numeric'
193 | ? event.target.valueAsNumber
194 | : event.target.value
195 | )
196 | }
197 | InputProps={{
198 | style: {
199 | minWidth: 50,
200 | fontSize: 13
201 | }
202 | }}
203 | inputProps={{
204 | autoFocus: this.props.autoFocus,
205 | 'aria-label': this.props.columnDef.title,
206 | style:
207 | this.props.columnDef.type === 'numeric'
208 | ? { textAlign: 'right' }
209 | : {}
210 | }}
211 | />
212 | );
213 | }
214 |
215 | renderCurrencyField() {
216 | return (
217 | {
226 | let value = event.target.valueAsNumber;
227 | if (!value && value !== 0) {
228 | value = undefined;
229 | }
230 | return this.props.onChange(value);
231 | }}
232 | InputProps={{
233 | style: {
234 | fontSize: 13,
235 | textAlign: 'right'
236 | }
237 | }}
238 | inputProps={{
239 | autoFocus: this.props.autoFocus,
240 | 'aria-label': this.props.columnDef.title,
241 | style: { textAlign: 'right' }
242 | }}
243 | onKeyDown={this.props.onKeyDown}
244 | />
245 | );
246 | }
247 |
248 | render() {
249 | let component = 'ok';
250 |
251 | if (this.props.columnDef.editComponent) {
252 | component = this.props.columnDef.editComponent(this.props);
253 | } else if (this.props.columnDef.lookup) {
254 | component = this.renderLookupField();
255 | } else if (this.props.columnDef.type === 'boolean') {
256 | component = this.renderBooleanField();
257 | } else if (this.props.columnDef.type === 'date') {
258 | component = this.renderDateField();
259 | } else if (this.props.columnDef.type === 'time') {
260 | component = this.renderTimeField();
261 | } else if (this.props.columnDef.type === 'datetime') {
262 | component = this.renderDateTimeField();
263 | } else if (this.props.columnDef.type === 'currency') {
264 | component = this.renderCurrencyField();
265 | } else {
266 | component = this.renderTextField();
267 | }
268 |
269 | return component;
270 | }
271 | }
272 |
273 | MTableEditField.propTypes = {
274 | value: PropTypes.any,
275 | onChange: PropTypes.func.isRequired,
276 | columnDef: PropTypes.object.isRequired,
277 | locale: PropTypes.object,
278 | rowData: PropTypes.object,
279 | onRowDataChange: PropTypes.func,
280 | errorState: PropTypes.func,
281 | autoFocus: PropTypes.bool,
282 | onBulkEditRowChanged: PropTypes.func,
283 | scrollWidth: PropTypes.number,
284 | onKeyDown: PropTypes.func
285 | };
286 |
287 | export default MTableEditField;
288 |
--------------------------------------------------------------------------------
/src/defaults/index.js:
--------------------------------------------------------------------------------
1 | import components from './props.components';
2 | import icons from './props.icons';
3 | import localization from './props.localization';
4 | import options from './props.options';
5 |
6 | export const defaultProps = {
7 | actions: [],
8 | classes: {},
9 | columns: [],
10 | components: components,
11 | data: [],
12 | icons: icons,
13 | isLoading: false,
14 | title: 'Table Title',
15 | options: options,
16 | localization: localization,
17 | style: {}
18 | };
19 |
--------------------------------------------------------------------------------
/src/defaults/props.components.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Default data for the `MaterialTable.components` attribute
3 | */
4 |
5 | import { TablePagination } from '@mui/material';
6 |
7 | import {
8 | Container,
9 | MTableAction,
10 | MTableActions,
11 | MTableBody,
12 | MTableCell,
13 | MTableEditCell,
14 | MTableEditField,
15 | MTableEditRow,
16 | MTableFilterRow,
17 | MTableGroupRow,
18 | MTableGroupbar,
19 | MTableHeader,
20 | MTableBodyRow,
21 | MTableSummaryRow,
22 | MTableToolbar,
23 | OverlayError,
24 | OverlayLoading
25 | } from '../components';
26 |
27 | export default {
28 | Action: MTableAction,
29 | Actions: MTableActions,
30 | Body: MTableBody,
31 | Cell: MTableCell,
32 | Container: Container,
33 | EditCell: MTableEditCell,
34 | EditField: MTableEditField,
35 | EditRow: MTableEditRow,
36 | FilterRow: MTableFilterRow,
37 | Groupbar: MTableGroupbar,
38 | GroupRow: MTableGroupRow,
39 | Header: MTableHeader,
40 | OverlayLoading: OverlayLoading,
41 | OverlayError: OverlayError,
42 | Pagination: TablePagination,
43 | Row: MTableBodyRow,
44 | SummaryRow: MTableSummaryRow,
45 | Toolbar: MTableToolbar
46 | };
47 |
--------------------------------------------------------------------------------
/src/defaults/props.icons.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Default data for `MaterialTable.icons` attribute
3 | */
4 |
5 | /* eslint-disable react/display-name */
6 | import React, { forwardRef } from 'react';
7 | import { Icon } from '@mui/material';
8 | import {
9 | AddBox,
10 | ArrowDownward,
11 | Check,
12 | ChevronLeft,
13 | ChevronRight,
14 | Clear,
15 | DeleteOutline,
16 | Edit,
17 | FilterList,
18 | FirstPage,
19 | LastPage,
20 | Remove,
21 | SaveAlt,
22 | Search,
23 | ViewColumn,
24 | Replay
25 | } from '@mui/icons-material';
26 |
27 | export default {
28 | Add: forwardRef((props, ref) => (
29 |
30 | )),
31 | Check: forwardRef((props, ref) => (
32 |
33 | )),
34 | Clear: forwardRef((props, ref) => (
35 |
36 | )),
37 | Delete: forwardRef((props, ref) => (
38 |
39 | )),
40 | DetailPanel: forwardRef((props, ref) => (
41 |
42 | )),
43 | Edit: forwardRef((props, ref) => (
44 |
45 | )),
46 | Export: forwardRef((props, ref) => (
47 |
48 | )),
49 | Filter: forwardRef((props, ref) => (
50 |
51 | )),
52 | FirstPage: forwardRef((props, ref) => (
53 |
54 | )),
55 | LastPage: forwardRef((props, ref) => (
56 |
57 | )),
58 | NextPage: forwardRef((props, ref) => (
59 |
60 | )),
61 | PreviousPage: forwardRef((props, ref) => (
62 |
63 | )),
64 | ResetSearch: forwardRef((props, ref) => (
65 |
66 | )),
67 | Resize: forwardRef((props, ref) => (
68 |
69 | |
70 |
71 | )),
72 | Retry: forwardRef((props, ref) => (
73 |
74 | )),
75 | Search: forwardRef((props, ref) => (
76 |
77 | )),
78 | SortArrow: forwardRef((props, ref) => (
79 |
80 | )),
81 | ThirdStateCheck: forwardRef((props, ref) => (
82 |
83 | )),
84 | ViewColumn: forwardRef((props, ref) => (
85 |
86 | ))
87 | };
88 |
--------------------------------------------------------------------------------
/src/defaults/props.localization.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Default data for the `MaterialTable.localization` attribute
3 | */
4 |
5 | export default {
6 | error: 'Data could not be retrieved',
7 | grouping: {
8 | groupedBy: 'Grouped By:',
9 | placeholder: 'Drag headers here to group by'
10 | },
11 | pagination: {
12 | labelDisplayedRows: '{from}-{to} of {count}',
13 | labelRowsPerPage: 'Rows per page:',
14 | labelRows: 'rows',
15 | firstAriaLabel: 'First Page',
16 | firstTooltip: 'First Page',
17 | previousAriaLabel: 'Previous Page',
18 | previousTooltip: 'Previous Page',
19 | nextAriaLabel: 'Next Page',
20 | nextTooltip: 'Next Page',
21 | lastAriaLabel: 'Last Page',
22 | lastTooltip: 'Last Page'
23 | },
24 | toolbar: {
25 | addRemoveColumns: 'Add or remove columns',
26 | nRowsSelected: '{0} row(s) selected',
27 | showColumnsTitle: 'Show Columns',
28 | showColumnsAriaLabel: 'Show Columns',
29 | exportTitle: 'Export',
30 | exportAriaLabel: 'Export',
31 | searchTooltip: 'Search',
32 | searchPlaceholder: 'Search',
33 | searchAriaLabel: 'Search',
34 | clearSearchAriaLabel: 'Clear Search'
35 | },
36 | header: { actions: 'Actions' },
37 | body: {
38 | emptyDataSourceMessage: 'No records to display',
39 | editRow: {
40 | saveTooltip: 'Save',
41 | cancelTooltip: 'Cancel',
42 | deleteText: 'Are you sure you want to delete this row?'
43 | },
44 | filterRow: {},
45 | dateTimePickerLocalization: 'Filter',
46 | addTooltip: 'Add',
47 | deleteTooltip: 'Delete',
48 | editTooltip: 'Edit',
49 | bulkEditTooltip: 'Edit All',
50 | bulkEditApprove: 'Save all changes',
51 | bulkEditCancel: 'Discard all changes'
52 | }
53 | };
54 |
--------------------------------------------------------------------------------
/src/defaults/props.options.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Default data for `MaterialTable.options` attribute
3 | */
4 |
5 | export default {
6 | idSynonym: 'id',
7 | actionsColumnIndex: 0,
8 | addRowPosition: 'last',
9 | columnsButton: false,
10 | detailPanelType: 'multiple',
11 | debounceInterval: 200,
12 | doubleHorizontalScroll: false,
13 | emptyRowsWhenPaging: true,
14 | exportAllData: false,
15 | exportMenu: [],
16 | filtering: false,
17 | groupTitle: false,
18 | header: true,
19 | headerSelectionProps: {},
20 | hideFilterIcons: false,
21 | loadingType: 'overlay',
22 | padding: 'normal',
23 | searchAutoFocus: false,
24 | paging: true,
25 | pageSize: 5,
26 | pageSizeOptions: [5, 10, 20],
27 | paginationType: 'normal',
28 | paginationPosition: 'bottom',
29 | showEmptyDataSourceMessage: true,
30 | showFirstLastPageButtons: true,
31 | showSelectAllCheckbox: true,
32 | showSelectGroupCheckbox: true,
33 | search: true,
34 | showTitle: true,
35 | showTextRowsSelected: true,
36 | showDetailPanelIcon: true,
37 | tableLayout: 'auto',
38 | tableWidth: 'full',
39 | toolbarButtonAlignment: 'right',
40 | searchFieldAlignment: 'right',
41 | searchFieldStyle: {},
42 | searchFieldVariant: 'standard',
43 | selection: false,
44 | selectionProps: {},
45 | // sorting: true,
46 | maxColumnSort: 1,
47 | clientSorting: true,
48 | groupChipProps: {},
49 | defaultOrderByCollection: [],
50 | showColumnSortOrder: false,
51 | keepSortDirectionOnColumnSwitch: true,
52 | toolbar: true,
53 | defaultExpanded: false,
54 | detailPanelColumnAlignment: 'left',
55 | detailPanelOffset: { left: 0, right: 0 },
56 | thirdSortClick: true,
57 | overflowY: 'auto',
58 | numberOfPagesAround: 1,
59 | actionsHeaderIndex: 0,
60 | draggable: true
61 | };
62 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { defaultProps } from './defaults';
3 | import { propTypes } from './prop-types';
4 | import MaterialTable from './material-table';
5 | import { useTheme } from '@mui/material/styles';
6 | import {
7 | useMergeProps,
8 | withContext,
9 | useLocalizationStore
10 | } from './store/LocalizationStore';
11 |
12 | MaterialTable.propTypes = propTypes;
13 |
14 | export default withContext((userProps) => {
15 | const props = { ...defaultProps, ...userProps };
16 | const theme = useTheme();
17 | const { localization, options, components } = useMergeProps(props);
18 | return (
19 |
27 | );
28 | });
29 |
30 | export { useLocalizationStore };
31 |
32 | export {
33 | MTableAction,
34 | MTableActions,
35 | MTableBody,
36 | MTableBodyRow,
37 | MTableCell,
38 | MTableEditCell,
39 | MTableEditField,
40 | MTableEditRow,
41 | MTableFilterRow,
42 | MTableGroupRow,
43 | MTableGroupbar,
44 | MTableHeader,
45 | MTablePagination,
46 | MTableSteppedPagination,
47 | MTableToolbar
48 | } from './components';
49 |
50 | export { ALL_COLUMNS } from './utils/constants';
51 |
--------------------------------------------------------------------------------
/src/store/LocalizationStore.js:
--------------------------------------------------------------------------------
1 | import { create, useStore } from 'zustand';
2 | import React from 'react';
3 | import deepEql from 'deep-eql';
4 | import defaultLocalization from '../defaults/props.localization';
5 | import defaultOptions from '../defaults/props.options';
6 | import defaultIcons from '../defaults/props.icons';
7 | import defaultComponents from '../defaults/props.components';
8 |
9 | const merge = require('deepmerge');
10 |
11 | const ZustandContext = React.createContext();
12 |
13 | const createStore = (props) =>
14 | create((set) => ({
15 | // Localization
16 | localization: merge(defaultLocalization, props.localization ?? {}),
17 | mergeLocalization: (nextLocalization) => {
18 | set(({ localization }) => {
19 | const mergedLocalization = merge(localization, nextLocalization ?? {});
20 | mergedLocalization.body.editRow.dateTimePickerLocalization =
21 | mergedLocalization.dateTimePickerLocalization;
22 | mergedLocalization.body.filterRow.dateTimePickerLocalization =
23 | mergedLocalization.dateTimePickerLocalization;
24 | if (!deepEql(mergedLocalization, nextLocalization)) {
25 | return { localization: mergedLocalization };
26 | } else {
27 | return { localization };
28 | }
29 | });
30 | },
31 | // Options
32 | options: { ...defaultOptions, ...props.options },
33 | mergeOptions: (nextOptions) => {
34 | set(() => {
35 | const mergedOptions = { ...defaultOptions, ...nextOptions };
36 | if (!deepEql(mergedOptions, nextOptions)) {
37 | return { options: mergedOptions };
38 | } else {
39 | return { options: defaultOptions };
40 | }
41 | });
42 | },
43 | // Icons
44 | icons: defaultIcons,
45 | mergeIcons: (nextIcons) => {
46 | set({
47 | icons: {
48 | ...defaultIcons,
49 | ...nextIcons
50 | }
51 | });
52 | },
53 | // Components
54 | components: defaultComponents,
55 | mergeComponents: (nextComponents) => {
56 | set(({ components }) => ({
57 | components: {
58 | ...components,
59 | ...nextComponents
60 | }
61 | }));
62 | }
63 | }));
64 |
65 | const useLocalizationStore = () => {
66 | const store = React.useContext(ZustandContext);
67 | const localization = useStore(store, (state) => state.localization);
68 | return localization;
69 | };
70 |
71 | const useOptionStore = () => {
72 | const store = React.useContext(ZustandContext);
73 | const options = useStore(store, (state) => state.options);
74 | return options;
75 | };
76 | const useIconStore = () => {
77 | const store = React.useContext(ZustandContext);
78 | const icons = useStore(store, (state) => state.icons);
79 | return icons;
80 | };
81 |
82 | function useMergeProps(props) {
83 | const store = React.useContext(ZustandContext);
84 | const {
85 | mergeLocalization,
86 | mergeOptions,
87 | mergeIcons,
88 | mergeComponents,
89 | localization,
90 | options,
91 | icons,
92 | components
93 | } = useStore(store, (state) => state);
94 | React.useEffect(() => {
95 | if (props.localization) {
96 | mergeLocalization(props.localization);
97 | }
98 | }, [props.localization]);
99 |
100 | React.useEffect(() => {
101 | if (props.options) {
102 | mergeOptions(props.options);
103 | }
104 | }, [props.options]);
105 | React.useEffect(() => {
106 | if (props.icons) {
107 | mergeIcons(props.icons);
108 | }
109 | }, [props.icons]);
110 | React.useEffect(() => {
111 | if (props.components) {
112 | mergeComponents(props.components);
113 | }
114 | }, [props.components]);
115 |
116 | return {
117 | localization,
118 | options,
119 | icons,
120 | components
121 | };
122 | }
123 |
124 | function withContext(WrappedComponent) {
125 | return function Wrapped(props) {
126 | const store = React.useRef(createStore(props)).current;
127 | return (
128 |
129 |
130 |
131 | );
132 | };
133 | }
134 |
135 | export {
136 | useLocalizationStore,
137 | useOptionStore,
138 | useMergeProps,
139 | withContext,
140 | useIconStore
141 | };
142 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | export {
2 | useLocalizationStore,
3 | useOptionStore,
4 | useMergeProps,
5 | useIconStore
6 | } from './LocalizationStore';
7 |
--------------------------------------------------------------------------------
/src/utils/common-values.js:
--------------------------------------------------------------------------------
1 | export const elementSize = ({ options = {} }) =>
2 | options.padding === 'normal' ? 'medium' : 'small';
3 |
4 | export const baseIconSize = (props) =>
5 | elementSize(props) === 'medium' ? 48 : 32;
6 |
7 | export const rowActions = (props) =>
8 | props.actions
9 | ? props.actions.filter(
10 | (a) => a.position === 'row' || typeof a === 'function'
11 | )
12 | : [];
13 | export const actionsColumnWidth = (props) =>
14 | rowActions(props).length * baseIconSize(props);
15 | export const selectionMaxWidth = (props, maxTreeLevel) =>
16 | baseIconSize(props) + 9 * maxTreeLevel;
17 |
18 | export const reducePercentsInCalc = (calc, fullValue) => {
19 | if (!calc) return `${fullValue}px`;
20 | const captureGroups = calc.match(/(\d*)%/);
21 | if (captureGroups && captureGroups.length > 1) {
22 | const percentage = captureGroups[1];
23 | return calc.replace(/\d*%/, `${fullValue * (percentage / 100)}px`);
24 | }
25 | return calc.replace(/\d*%/, `${fullValue}px`);
26 | };
27 |
28 | export const widthToNumber = (width) => {
29 | if (typeof width === 'number') return width;
30 | if (!width || !width.match(/^\s*\d+(px)?\s*$/)) return NaN;
31 | return Number(width.replace(/px$/, ''));
32 | };
33 |
34 | export const parseFirstLastPageButtons = (showFirstLastPageButtons, isRTL) => {
35 | let result = { first: true, last: true };
36 | if (typeof showFirstLastPageButtons === 'boolean') {
37 | result = {
38 | first: showFirstLastPageButtons,
39 | last: showFirstLastPageButtons
40 | };
41 | } else if (typeof showFirstLastPageButtons === 'object') {
42 | result = { ...result, ...showFirstLastPageButtons };
43 | }
44 | if (isRTL) {
45 | result = { first: result.last, last: result.first };
46 | }
47 | return result;
48 | };
49 |
--------------------------------------------------------------------------------
/src/utils/constants.js:
--------------------------------------------------------------------------------
1 | export const ALL_COLUMNS = 'all_columns';
2 |
--------------------------------------------------------------------------------
/src/utils/hooks/useDoubleClick.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function useDoubleClick(singleCallback, dbCallback) {
4 | const countRef = React.useRef(0);
5 | /** Refs for the timer **/
6 | const timerRef = React.useRef(null);
7 | const inputDoubleCallbackRef = React.useRef(null);
8 | const inputSingleCallbackRef = React.useRef(null);
9 |
10 | React.useEffect(() => {
11 | inputDoubleCallbackRef.current = dbCallback;
12 | inputSingleCallbackRef.current = singleCallback;
13 | });
14 |
15 | const reset = () => {
16 | clearTimeout(timerRef.current);
17 | timerRef.current = null;
18 | countRef.current = 0;
19 | };
20 |
21 | const onClick = React.useCallback((e) => {
22 | const isDoubleClick = countRef.current + 1 === 2;
23 | const timerIsPresent = timerRef.current;
24 | if (timerIsPresent && isDoubleClick) {
25 | reset();
26 | inputDoubleCallbackRef.current && inputDoubleCallbackRef.current(e);
27 | }
28 | if (!timerIsPresent) {
29 | countRef.current = countRef.current + 1;
30 | const singleClick = () => {
31 | reset();
32 | inputSingleCallbackRef.current && inputSingleCallbackRef.current(e);
33 | };
34 | if (inputDoubleCallbackRef.current) {
35 | const timer = setTimeout(singleClick, 250);
36 | timerRef.current = timer;
37 | } else {
38 | singleClick();
39 | }
40 | }
41 | }, []);
42 |
43 | return onClick;
44 | }
45 |
46 | export { useDoubleClick };
47 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import * as CommonValues from '@utils/common-values';
2 |
3 | export const selectFromObject = (o, s) => {
4 | if (!s) {
5 | return;
6 | }
7 | let a;
8 | if (!Array.isArray(s)) {
9 | s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
10 | s = s.replace(/^\./, ''); // strip a leading dot
11 | a = s.split('.');
12 | } else {
13 | a = s;
14 | }
15 | for (let i = 0, n = a.length; i < n; ++i) {
16 | const x = a[i];
17 | if (o && x in o) {
18 | o = o[x];
19 | } else {
20 | return;
21 | }
22 | }
23 | return o;
24 | };
25 |
26 | export const setObjectByKey = (obj, path, value) => {
27 | let schema = obj; // a moving reference to internal objects within obj
28 | let pList;
29 | if (!Array.isArray(path)) {
30 | path = path.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
31 | path = path.replace(/^\./, ''); // strip a leading dot
32 | pList = path.split('.');
33 | } else {
34 | pList = path;
35 | }
36 | const len = pList.length;
37 | for (let i = 0; i < len - 1; i++) {
38 | const elem = pList[i];
39 | if (!schema[elem]) schema[elem] = {};
40 | schema = schema[elem];
41 | }
42 | schema[pList[len - 1]] = value;
43 | };
44 |
45 | export function getStyle(props) {
46 | const width = CommonValues.reducePercentsInCalc(
47 | props.columnDef.tableData.width,
48 | props.scrollWidth
49 | );
50 | let cellStyle = {
51 | color: 'inherit',
52 | width,
53 | maxWidth: props.columnDef.maxWidth,
54 | minWidth: props.columnDef.minWidth,
55 | boxSizing: 'border-box',
56 | fontSize: 'inherit',
57 | fontFamily: 'inherit',
58 | fontWeight: 'inherit'
59 | };
60 | if (typeof props.columnDef.cellStyle === 'function') {
61 | cellStyle = {
62 | ...cellStyle,
63 | ...props.columnDef.cellStyle(props.value, props.rowData)
64 | };
65 | } else {
66 | cellStyle = { ...cellStyle, ...props.columnDef.cellStyle };
67 | }
68 | if (props.columnDef.disableClick) {
69 | cellStyle.cursor = 'default';
70 | }
71 | return { ...props.style, ...cellStyle };
72 | }
73 |
--------------------------------------------------------------------------------
/src/utils/validate.js:
--------------------------------------------------------------------------------
1 | function validateInput(columnDef, data) {
2 | if (columnDef.validate) {
3 | const validateResponse = columnDef.validate(data);
4 | switch (typeof validateResponse) {
5 | case 'object':
6 | return { ...validateResponse };
7 | case 'boolean':
8 | return { isValid: validateResponse, helperText: '' };
9 | case 'string':
10 | return { isValid: false, helperText: validateResponse };
11 | default:
12 | return { isValid: true, helperText: '' };
13 | }
14 | }
15 | return { isValid: true, helperText: '' };
16 | }
17 |
18 | export { validateInput };
19 |
--------------------------------------------------------------------------------
/types/helper.d.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type Concrete = {
4 | [Property in keyof Type]-?: Type[Property];
5 | };
6 |
7 | type Handlers = Concrete<
8 | Omit<
9 | React.DOMAttributes,
10 | 'children' | 'dangerouslySetInnerHTML'
11 | >
12 | >;
13 |
14 | type OnHandlers = Partial<
15 | {
16 | [Property in keyof Handlers]: (
17 | event: Parameters[0],
18 | rowData: Type
19 | ) => void;
20 | }
21 | >;
22 |
23 | export type { OnHandlers };
24 |
--------------------------------------------------------------------------------