├── .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 | material-table 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 | [![build](https://github.com/material-table-core/core/workflows/Build/badge.svg?branch=master)](https://github.com/material-table-core/core/actions?query=workflow%3ABuild) 8 | [![publish](https://github.com/material-table-core/core/actions/workflows/publish.yml/badge.svg)](https://github.com/material-table-core/core/actions?query=workflow%3APublish) 9 | [![npm version](https://badge.fury.io/js/@material-table%2Fcore.svg)](https://www.npmjs.com/package/@material-table/core) 10 | [![discord](https://img.shields.io/discord/796859493412765697)](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 | 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 | 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 |
  1. 165 |

    166 | mbrn{' '} 167 | 168 | #1353 169 | 170 |

    171 | 172 |
  2. 173 |
  3. 174 |

    175 | mbrn{' '} 176 | 177 | #1941 178 | 179 |

    180 | 181 |
  4. 182 |
  5. 183 |

    184 | 185 | #122 186 | 187 |

    188 | 189 |
  6. 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 | 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 | 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 | 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 | 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 |
19 |
28 | 29 |
30 |
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 | 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 | --------------------------------------------------------------------------------