├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── stale.yml └── workflows │ ├── build.yml │ ├── codeql-analysis.yml │ ├── lint-pr.yml │ └── publish.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── sample ├── app │ └── index.html └── src │ ├── ColumnFeatures │ ├── ColumnFeatures.tsx │ └── ModifyingColumns.tsx │ ├── CustomHttpClient.ts │ ├── CustomLayoutDataGrid.tsx │ ├── ErrorBoundary.tsx │ ├── RemoteGridList.tsx │ ├── TbListExample.tsx │ ├── app.tsx │ ├── data │ ├── columns.ts │ └── localData.ts │ ├── localDataGrid.tsx │ ├── main.tsx │ ├── masterDetailRowSample.tsx │ └── remoteDataGrid.tsx ├── src ├── BareBones │ ├── DetailComponentProps.ts │ ├── TbMobileRow.tsx │ ├── TbRow.tsx │ └── index.ts ├── DataGrid │ ├── DataGrid.tsx │ ├── DataGridCard.tsx │ ├── DataGridTable.tsx │ ├── FeaturesDrawer.tsx │ ├── GridBody.tsx │ ├── GridHeader.tsx │ ├── GridHeaderCell.tsx │ ├── MobileDataGridTable.tsx │ ├── NoDataRow.tsx │ └── index.ts ├── Filtering │ ├── BooleanFilterEditor.tsx │ ├── ChipBar.tsx │ ├── ChipFilter.tsx │ ├── DateFilter.tsx │ ├── FilterControl.tsx │ ├── FiltersContainer.tsx │ ├── NumericFilter.tsx │ ├── SearchTextInput.tsx │ ├── StandardFilterEditor.tsx │ ├── StringFilter.tsx │ ├── index.ts │ └── utils.tsx ├── Pagination │ ├── AdvancePaginationActions.tsx │ ├── Paginator.tsx │ └── index.ts ├── SvgIcons │ ├── TbBetweenIcon.tsx │ ├── TbContainsIcon.tsx │ ├── TbEndsWithIcon.tsx │ ├── TbEqualsIcon.tsx │ ├── TbGreaterOrEqualsToIcon.tsx │ ├── TbGreaterThanIcon.tsx │ ├── TbLessOrEqualsToIcon.tsx │ ├── TbLessThanIcon.tsx │ ├── TbNotContainsIcon.tsx │ ├── TbNotEndsWithIcon.tsx │ ├── TbNotEqualsIcon.tsx │ ├── TbNotStartsWithIcon.tsx │ └── TbStartsWithIcon.tsx ├── TbList │ ├── TbList.tsx │ ├── TbListItem.tsx │ └── index.ts ├── Toolbar │ ├── ExportButton.tsx │ ├── GridToolbar.tsx │ ├── SelectionToolbar.tsx │ ├── ToolbarOptions.ts │ └── index.ts ├── hooks │ └── useTbSelection.ts ├── index.ts └── utils │ ├── Lang.ts │ ├── Selection.ts │ ├── index.ts │ ├── languages │ ├── English.ts │ └── TubularLangDef.ts │ └── renders.tsx ├── test ├── DataGrid.spec.tsx ├── LanguageService.spec.ts └── utils │ ├── columns.ts │ ├── localData.ts │ ├── responses.ts │ └── utils.tsx ├── tsconfig.json └── typings.d.ts /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", // Specifies the ESLint parser 3 | extends: [ 4 | "plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react 5 | "plugin:@typescript-eslint/recommended", // Uses the recommended rules from @typescript-eslint/eslint-plugin 6 | "prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 7 | "plugin:prettier/recommended" // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 8 | 9 | ], 10 | parserOptions: { 11 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 12 | sourceType: "module", // Allows for the use of imports 13 | ecmaFeatures: { 14 | jsx: true // Allows for the parsing of JSX 15 | } 16 | }, 17 | rules: { 18 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 19 | "@typescript-eslint/explicit-function-return-type": "off", 20 | "@typescript-eslint/interface-name-prefix": "off", 21 | "@typescript-eslint/prop-types": "off", 22 | "react/display-name": "off", 23 | "no-restricted-imports": [ 24 | "error", 25 | { 26 | "patterns": ["@material-ui/*/*/*", "!@material-ui/core/test-utils/*"] 27 | } 28 | ] 29 | }, 30 | settings: { 31 | react: { 32 | version: "detect" // Tells eslint-plugin-react to automatically detect the version of React to use 33 | } 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 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. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: [ubuntu-latest] 8 | 9 | steps: 10 | - uses: actions/checkout@v1 11 | - uses: actions/cache@v1 12 | with: 13 | path: ~/.npm 14 | key: node-${{ hashFiles('**/package-lock.json') }} 15 | restore-keys: | 16 | node- 17 | - name: npm build and coverage (pending) 18 | run: | 19 | npm ci 20 | npm test 21 | npm run build 22 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 17 * * 3' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | # Override automatic language detection by changing the below list 21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 22 | language: ['javascript'] 23 | # Learn more... 24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | with: 30 | # We must fetch at least the immediate parents so that if this is 31 | # a pull request then we can checkout the head. 32 | fetch-depth: 2 33 | 34 | # If this run was triggered by a pull request event, then checkout 35 | # the head of the pull request instead of the merge commit. 36 | - run: git checkout HEAD^2 37 | if: ${{ github.event_name == 'pull_request' }} 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v1 42 | with: 43 | languages: ${{ matrix.language }} 44 | # If you wish to specify custom queries, you can do so here or in a config file. 45 | # By default, queries listed here will override any specified in a config file. 46 | # Prefix the list here with "+" to use these queries and those in the config file. 47 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 48 | 49 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 50 | # If this step fails, then you should remove it and run the build manually (see below) 51 | - name: Autobuild 52 | uses: github/codeql-action/autobuild@v1 53 | 54 | # ℹ️ Command-line programs to run using the OS shell. 55 | # 📚 https://git.io/JvXDl 56 | 57 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 58 | # and modify them (or add more) to build your code if your project 59 | # uses a compiled language 60 | 61 | #- run: | 62 | # make bootstrap 63 | # make release 64 | 65 | - name: Perform CodeQL Analysis 66 | uses: github/codeql-action/analyze@v1 67 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr.yml: -------------------------------------------------------------------------------- 1 | name: Lint PR 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | eslint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: actions/cache@v1 11 | with: 12 | path: ~/.npm 13 | key: node-${{ hashFiles('**/package-lock.json') }} 14 | restore-keys: | 15 | node- 16 | - run: npm ci 17 | - name: Lint 18 | uses: a-b-r-o-w-n/eslint-action@v1 19 | with: 20 | repo-token: ${{ secrets.GITHUB_TOKEN }} 21 | files: 'src/**/*' 22 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: npm Publish 2 | 3 | on: 4 | push: 5 | branches: master 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/cache@v1 13 | with: 14 | path: ~/.npm 15 | key: node-${{ hashFiles('**/package-lock.json') }} 16 | restore-keys: | 17 | node- 18 | - name: npm build 19 | run: | 20 | npm ci 21 | npm run build 22 | - name: Publish 23 | uses: mikeal/merge-release@master 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | .cache/** 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # Typescript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | #Build directory 62 | build 63 | 64 | # Examples bundle 65 | sample/app/bundle.js 66 | 67 | package-lock.json 68 | 69 | .vscode/* 70 | !.vscode/settings.json 71 | !.vscode/tasks.json 72 | !.vscode/launch.json 73 | !.vscode/extensions.json 74 | 75 | .next 76 | /sample/app/*.map 77 | dist 78 | 79 | yarn\.lock 80 | distsample -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | .github 3 | src 4 | .nyc_output 5 | .vscode 6 | *.json 7 | .eslintrc.json 8 | .prettierrc.js 9 | *.yml 10 | srcdocs 11 | static 12 | docs -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Unosquare Labs 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 | [![npm version](https://badge.fury.io/js/tubular-react.svg)](https://badge.fury.io/js/tubular-react) 2 | ![Buils status](https://github.com/unosquare/tubular-react/workflows/Node.js%20Package/badge.svg) 3 | 4 | ![Tubular-React](https://unosquare.github.io/assets/tubular.png) 5 | 6 | ** THIS REPO HAS BEEN ARCHIVED** 7 | 8 | **Tubular-React** is a [Material-UI](https://material-ui.com/) table (or data grid) with local or remote data-source. Featuring: 9 | 10 | - Define a custom layout for columns and cells using `render` methods. 11 | - Use a remote or local datasource. Remote datasource use a specific Request and Response format. 12 | - Sort and filter multiple columns. 13 | - Free-text search of string columns. 14 | - Page data. Remote data is paged in the server side. 15 | - Export data to a CSV file. 16 | - Print data. 17 | 18 | You can try a [CodeSandbox demo](https://codesandbox.io/s/818mwv72ll). 19 | 20 | Please visit the [Tubular GitHub Page](http://unosquare.github.io/tubular) to learn how quickly you can start coding. See [Related projects](#related-projects) below to discover more Tubular libraries and backend solutions. 21 | 22 | # Table of contents 23 | 24 | - [Table of contents](#table-of-contents) 25 | - [Installation](#installation) 26 | - [Usages](#usages) 27 | - [`DataGrid`](#datagrid) 28 | - [DataGrid with a remote data source](#datagrid-with-a-remote-data-source) 29 | - [DataGrid with a local data source](#datagrid-with-a-local-data-source) 30 | - [Tubular react in a grid list](#tubular-react-in-a-grid-list) 31 | - [Run integrated sample](#run-integrated-sample) 32 | - [i18n Support](#i18n-support) 33 | - [Related Projects](#related-projects) 34 | 35 | ## Installation 36 | 37 | ```sh 38 | $ npm install tubular-react --save 39 | ``` 40 | 41 | ## Usages 42 | 43 | You can check the documentation of the components at [https://unosquare.github.io/tubular/tubular-react](https://unosquare.github.io/tubular/tubular-react) 44 | 45 | ### `DataGrid` 46 | 47 | You can start using `DataGrid` with this sample code. The grid will connect to a remote datasource or have a local datasource depending on what it's passed in the dataSource property. 48 | 49 | To create Column you have to use **createColumn** function and have to pass the desired name of column as string. 50 | ```js 51 | import React from 'react'; 52 | import ReactDOM from 'react-dom'; 53 | 54 | import { DataGrid } from 'tubular-react'; 55 | import {createColumn} from "tubular-common"; 56 | 57 | const columns = [createColumn('OrderID'), createColumn('CustomerName'), createColumn('ShipperCity')]; 58 | 59 | const SampleGrid = () => ( 60 | 61 | ); 62 | 63 | ReactDOM.render(, document.getElementById('root')); 64 | ``` 65 | 66 | This is a preview of the previous code: 67 | 68 | ![DataGrid](https://user-images.githubusercontent.com/25437790/57318742-a7a2b200-70c0-11e9-8d5b-aaf2107bd059.gif) 69 | 70 | ### DataGrid with a remote data source 71 | It is possible to display data from a remote source. 72 | 73 | ![Remote](https://user-images.githubusercontent.com/36867256/85425475-d71fb280-b53e-11ea-9aee-33308b6f79d4.gif) 74 | 75 | [![Edit RemoteDataGrid -Example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/remotedatagrid-example-forked-lnt7h?fontsize=14&hidenavigation=1&theme=dark) 76 | 77 | ### DataGrid with a local data source 78 | It is possible to display data from a local data source. 79 | 80 | ![Local](https://user-images.githubusercontent.com/36867256/85425715-24038900-b53f-11ea-9248-e03ca1c43d8a.gif) 81 | 82 | [![Edit LocalDataGrid -Example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/localdatagrid-example-forked-sif6w?file=/src/LocalDataGrid.tsx&fontsize=14&hidenavigation=1&theme=dark) 83 | 84 | ### Tubular react in a grid list 85 | Tubular can also be used to render data in a different layout. 86 | 87 | ![Grid](https://user-images.githubusercontent.com/36867256/85425888-6331da00-b53f-11ea-9359-88f83689da3a.gif) 88 | 89 | [![Edit GridList -Example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/gridlist-example-forked-vfniq?file=/src/RemoteList.tsx&fontsize=14&hidenavigation=1&theme=dark) 90 | 91 | ### Run integrated sample 92 | 93 | There is a sample included in this project, you can run it just by doing the following. 94 | 95 | ```shell 96 | // Install all the dependencies 97 | npm install 98 | // Run the sample project 99 | npm start 100 | ``` 101 | 102 | ## i18n Support 103 | 104 | Tubular React now includes a brand new Language Service that will translate the content of the grid to a preferred language. 105 | Devs can also implement content on their language and import it to use this language. 106 | By default, Tubular React comes with implementations in **English** and **Spanish**. 107 | If any key content needs parameters to include in the translation, devs can pass the parameters in the `translate` function. 108 | 109 | ```ts 110 | import { Lang } from 'tubular-react'; 111 | 112 | Lang.translate('PageNum', 16); 113 | // => 'Page 16' 114 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tubular-react", 3 | "version": "4.0.0", 4 | "description": "Unosquare Tubular React", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "start": "parcel ./sample/app/index.html --open --port 9000", 8 | "build": "tsc", 9 | "check": "npm run lint && npm test", 10 | "lint": "tsc --noEmit && eslint ./src/**/*.{ts,tsx} --quiet", 11 | "lint:fix": "eslint ./src/**/*.{ts,tsx} --quiet --fix", 12 | "test": "jest", 13 | "cest": "jest --collect-coverage", 14 | "preparepackage": "npm run build && npm run copypackage && cd dist && npm pack", 15 | "copypackage": "npx shx cp package.json dist", 16 | "prettier": "prettier --check src/**/*.ts*", 17 | "prettier:fix": "prettier --write src/**/*.ts*", 18 | "codestyle:fix": "npm run lint:fix && npm run prettier:fix" 19 | }, 20 | "prettier": { 21 | "endOfLine": "auto", 22 | "semi": true, 23 | "trailingComma": "all", 24 | "singleQuote": true, 25 | "printWidth": 120, 26 | "tabWidth": 4 27 | }, 28 | "jest": { 29 | "automock": false, 30 | "collectCoverageFrom": [ 31 | "./src/**/*.{ts,tsx}", 32 | "!**/node_modules/**", 33 | "!./test/**" 34 | ], 35 | "moduleFileExtensions": [ 36 | "ts", 37 | "tsx", 38 | "js" 39 | ], 40 | "transform": { 41 | "^.+\\.(ts|tsx)$": "ts-jest" 42 | }, 43 | "testMatch": [ 44 | "/test/**/*.spec.(ts|tsx)" 45 | ], 46 | "globals": { 47 | "ts-jest": { 48 | "diagnostics": false, 49 | "tsConfig": "tsconfig.json" 50 | } 51 | } 52 | }, 53 | "repository": { 54 | "type": "git", 55 | "url": "git+https://github.com/unosquare/tubular-react.git" 56 | }, 57 | "author": "Unosquare", 58 | "license": "MIT", 59 | "bugs": { 60 | "url": "https://github.com/unosquare/tubular-react/issues" 61 | }, 62 | "homepage": "https://github.com/unosquare/tubular-react#readme", 63 | "peerDependencies": { 64 | "@material-ui/core": "^4.11.3", 65 | "@material-ui/icons": "^4.11.2" 66 | }, 67 | "dependencies": { 68 | "@date-io/date-fns": "^2.10.8", 69 | "@material-ui/pickers": "^4.0.0-alpha.8", 70 | "@svgr/parcel-plugin-svgr": "^5.5.0", 71 | "clsx": "^1.1.1", 72 | "date-fns": "^2.17.0", 73 | "react-virtualized": "^9.22.2", 74 | "tubular-common": "^5.0.11", 75 | "tubular-react-common": "3.0.9", 76 | "uno-material-ui": "^2.0.10", 77 | "uno-react": "1.0.12" 78 | }, 79 | "devDependencies": { 80 | "@testing-library/react": "^11.2.5", 81 | "@testing-library/react-hooks": "^5.0.3", 82 | "@types/jest": "^26.0.20", 83 | "@types/react": "^17.0.1", 84 | "@types/react-dom": "^17.0.0", 85 | "@types/react-virtualized": "^9.21.11", 86 | "@typescript-eslint/eslint-plugin": "^4.15.0", 87 | "@typescript-eslint/parser": "^4.15.0", 88 | "eslint": "^7.19.0", 89 | "eslint-config-prettier": "^7.2.0", 90 | "eslint-plugin-prettier": "^3.3.1", 91 | "eslint-plugin-react": "^7.22.0", 92 | "jest": "^26.6.3", 93 | "jest-fetch-mock": "^3.0.3", 94 | "parcel-bundler": "^1.12.4", 95 | "parcel-plugin-clean-easy": "^1.0.2", 96 | "prettier": "^2.2.1", 97 | "react": "^17.0.1", 98 | "react-dom": "17.0.1", 99 | "ts-jest": "^26.5.1", 100 | "typescript": "^4.1.5" 101 | }, 102 | "parcelCleanPaths": [ 103 | "dist/*.*" 104 | ] 105 | } 106 | -------------------------------------------------------------------------------- /sample/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tubular React - Sample 6 | 7 | 8 | 9 | 10 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /sample/src/ColumnFeatures/ColumnFeatures.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import ModifyingColumns from './ModifyingColumns'; 3 | 4 | const ColumnFeatures: React.FunctionComponent = () => ( 5 |
6 | 7 |
8 | ); 9 | 10 | export default ColumnFeatures; 11 | -------------------------------------------------------------------------------- /sample/src/ColumnFeatures/ModifyingColumns.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import Button from '@material-ui/core/Button'; 4 | import Snackbar from '@material-ui/core/Snackbar'; 5 | import { LocalStorage, createColumn } from 'tubular-common'; 6 | import { DataGrid } from '../../../src'; 7 | import { ToolbarOptions } from '../../../src/Toolbar/ToolbarOptions'; 8 | import columns from '../data/columns'; 9 | import localData from '../data/localData'; 10 | 11 | const ModifyingColumns: React.FunctionComponent = () => { 12 | const [getErrorMessage, setErrorMessage] = React.useState(null as string); 13 | const [gridColumns, setGridColumns] = React.useState(columns); 14 | const [columnCounter, setColumnCounter] = React.useState(0); 15 | 16 | const handleAddColumn = () => { 17 | setGridColumns([ 18 | ...gridColumns, 19 | createColumn(`Column ${columnCounter}`, { 20 | filterable: true, 21 | searchable: true, 22 | sortable: true, 23 | }), 24 | ]); 25 | 26 | setColumnCounter(columnCounter + 1); 27 | }; 28 | 29 | const handleDeleteLastColumn = () => { 30 | setGridColumns([...gridColumns.filter(c => c.name !== gridColumns[gridColumns.length - 1].name)]); 31 | }; 32 | 33 | const toolbarOptions = new ToolbarOptions({ 34 | customItems: ( 35 |
36 | 37 | 38 |
39 | ), 40 | }); 41 | 42 | return ( 43 |
44 | {getErrorMessage && ( 45 | {getErrorMessage}} 51 | /> 52 | )} 53 | 61 |
62 | ); 63 | }; 64 | 65 | export default ModifyingColumns; 66 | -------------------------------------------------------------------------------- /sample/src/CustomHttpClient.ts: -------------------------------------------------------------------------------- 1 | import { GridRequest, TubularHttpClientAbstract, TubularHttpClient } from 'tubular-common'; 2 | 3 | export default class CustomHttpClient implements TubularHttpClientAbstract { 4 | public request: string | Request; 5 | 6 | public constructor(request: string | Request | TubularHttpClientAbstract) { 7 | this.request = TubularHttpClient.resolveRequest(request); 8 | } 9 | 10 | public async fetch(gridRequest: GridRequest): Promise<{}> { 11 | const response = await fetch(TubularHttpClient.getRequest(this.request, gridRequest)); 12 | const data = await response.json(); 13 | data.payload = data.Payload; 14 | delete data.Payload; 15 | data.payload = data.counter; 16 | delete data.counter; 17 | data.payload = data.CurrentPage; 18 | delete data.currentPage; 19 | // We simulate always one page 20 | data.totalPages = 1; 21 | data.totalRecordCount = 10; 22 | data.filteredRecordCount = 10; 23 | 24 | return data; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sample/src/CustomLayoutDataGrid.tsx: -------------------------------------------------------------------------------- 1 | import Card from '@material-ui/core/Card'; 2 | import CardContent from '@material-ui/core/CardContent'; 3 | import Paper from '@material-ui/core/Paper'; 4 | import Snackbar from '@material-ui/core/Snackbar'; 5 | import Table from '@material-ui/core/Table'; 6 | import TableCell from '@material-ui/core/TableCell'; 7 | import TableHead from '@material-ui/core/TableHead'; 8 | import TableRow from '@material-ui/core/TableRow'; 9 | import Typography from '@material-ui/core/Typography'; 10 | import CheckBox from '@material-ui/icons/CheckBox'; 11 | import CheckBoxOutlineBlank from '@material-ui/icons/CheckBoxOutlineBlank'; 12 | import * as React from 'react'; 13 | import { ColumnModel, parseDateColumnValue } from 'tubular-common'; 14 | import { FixedLinearProgress } from 'uno-material-ui/dist/FixedLinearProgress'; 15 | import { useResolutionSwitch } from 'uno-react/lib/hooks/useResolutionSwitch'; 16 | import { TbRowProps } from '../../src/BareBones/TbRow'; 17 | import { DataGridTable } from '../../src/DataGrid'; 18 | import { MobileDataGridTable } from '../../src/DataGrid/MobileDataGridTable'; 19 | import { Paginator } from '../../src/Pagination'; 20 | import sampleColumns from './data/columns'; 21 | import localData from './data/localData'; 22 | import { useTbTable } from 'tubular-react-common'; 23 | 24 | const CustomTbRow: React.FunctionComponent = ({ row, onRowClick }: TbRowProps) => ( 25 | 26 | {row.OrderID} 27 | {row.CustomerName} 28 | {row.ShippedDate} 29 | {row.ShipperCity} 30 | 31 | {row.Amount || 0} 32 | 33 | {row.IsShipped ? : } 34 | 35 | ); 36 | 37 | const CustomTbMobileRow = ({ columns, row, onRowClick }: TbRowProps) => ( 38 | 39 | 40 | {columns.map((column: ColumnModel, index: number) => ( 41 |
42 | 43 | {column.name}: 44 | 45 | 46 | {row[column.name]} 47 | 48 |
49 | ))} 50 |
51 |
52 | ); 53 | 54 | const tbFooter = ({ aggregates }: any) => ( 55 | 56 | Total: 57 | {aggregates && aggregates.CustomerName} 58 | 59 | 60 | 61 | 62 | 63 | ); 64 | 65 | const CustomLayoutDataGrid: React.FunctionComponent = () => { 66 | const [getErrorMessage] = React.useState(null as string); 67 | const tbTableInstance = useTbTable(sampleColumns, localData); 68 | const onRowClick = row => console.log(row); 69 | const [isMobileResolution] = useResolutionSwitch(800, 400); 70 | if (isMobileResolution) { 71 | return ( 72 | 73 | 74 | 79 | 80 | ); 81 | } 82 | 83 | return ( 84 | <> 85 | {getErrorMessage && ( 86 | {getErrorMessage}} 92 | /> 93 | )} 94 | 95 | No card grid! 96 | 97 | 98 | 99 | 100 | 105 | 106 | 107 |
108 | 109 | 115 | 116 | ); 117 | }; 118 | 119 | export default CustomLayoutDataGrid; 120 | -------------------------------------------------------------------------------- /sample/src/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import Paper from '@material-ui/core/Paper'; 2 | import Typography from '@material-ui/core/Typography'; 3 | import * as React from 'react'; 4 | 5 | const message = { 6 | color: 'red', 7 | }; 8 | 9 | class ErrorBoundary extends React.Component { 10 | public state = { 11 | error: null as any, 12 | errorInfo: null as any, 13 | }; 14 | 15 | public componentDidCatch(error: any, errorInfo: any) { 16 | this.setState({ error, errorInfo }); 17 | } 18 | 19 | public render() { 20 | const { errorInfo } = this.state; 21 | if (!errorInfo) { 22 | return this.props.children; 23 | } 24 | 25 | return ( 26 |
27 | 28 | Something went wrong. 29 | 30 |
31 | 32 |
33 | {this.state.error && this.state.error.toString()} 34 | {this.state.errorInfo.componentStack} 35 |
36 |
37 |
38 | ); 39 | } 40 | } 41 | export default ErrorBoundary; 42 | -------------------------------------------------------------------------------- /sample/src/RemoteGridList.tsx: -------------------------------------------------------------------------------- 1 | import Button from '@material-ui/core/Button'; 2 | import Card from '@material-ui/core/Card'; 3 | import CardActions from '@material-ui/core/CardActions'; 4 | import CardContent from '@material-ui/core/CardContent'; 5 | import GridList from '@material-ui/core/GridList'; 6 | import GridListTile from '@material-ui/core/GridListTile'; 7 | import LinearProgress from '@material-ui/core/LinearProgress'; 8 | import Paper from '@material-ui/core/Paper'; 9 | import Table from '@material-ui/core/Table'; 10 | import TableBody from '@material-ui/core/TableBody'; 11 | import TableCell from '@material-ui/core/TableCell'; 12 | import TableFooter from '@material-ui/core/TableFooter'; 13 | import TableRow from '@material-ui/core/TableRow'; 14 | import Typography from '@material-ui/core/Typography'; 15 | import * as React from 'react'; 16 | import { formatDate, LocalStorage } from 'tubular-common'; 17 | import { Paginator, SearchTextInput } from '../../src'; 18 | import CustomHttpClient from './CustomHttpClient'; 19 | import columns from './data/columns'; 20 | import { useTbTable } from 'tubular-react-common'; 21 | 22 | const styles: any = { 23 | progress: { 24 | height: '20px', 25 | }, 26 | search: { 27 | margin: '15px 10px 10px 10px', 28 | textAlign: 'right', 29 | }, 30 | }; 31 | 32 | const RemoteGridList: React.FunctionComponent = () => { 33 | const [getErrorMessage, setErrorMessage] = React.useState(null as string); 34 | 35 | const tbTableInstance = useTbTable(columns, 'https://tubular.azurewebsites.net/api/orders/paged', { 36 | storage: new LocalStorage(), 37 | componentName: 'RemoteGridList', 38 | }); 39 | 40 | console.log(tbTableInstance); 41 | return ( 42 | 43 |
44 | 48 |
49 |
{tbTableInstance.state.isLoading && }
50 | 51 | 52 | 53 | 54 | 55 | {tbTableInstance.state.data && 56 | tbTableInstance.state.data.map(row => ( 57 | 58 | 59 | 60 | 61 | {row.OrderID} - {row.CustomerName} 62 | 63 | {row.ShipperCity} 64 | 65 | {formatDate(row.ShippedDate, 'M/d/yyyy h:mm a')} 66 | 67 | 68 | 69 | 72 | 73 | 74 | 75 | ))} 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
86 |
87 | ); 88 | }; 89 | 90 | export default RemoteGridList; 91 | -------------------------------------------------------------------------------- /sample/src/TbListExample.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import Button from '@material-ui/core/Button'; 4 | import ListItem from '@material-ui/core/ListItem'; 5 | import ListItemText from '@material-ui/core/ListItemText'; 6 | import Menu from '@material-ui/core/Menu'; 7 | import MenuItem from '@material-ui/core/MenuItem'; 8 | import TextField from '@material-ui/core/TextField'; 9 | import { useTbList } from 'tubular-react-common'; 10 | import { TbList } from '../../src/TbList/TbList'; 11 | import columns from './data/columns'; 12 | import localData from './data/localData'; 13 | 14 | const MyListItem: React.FunctionComponent = ({ rowStyle, selectedIndex, onItemClick, row }: any) => { 15 | return ( 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | const TbListExample: React.FunctionComponent = () => { 23 | const [data, setData] = React.useState(localData); 24 | const tbList = useTbList(columns, 'https://tubular.azurewebsites.net/api/orders/paged'); 25 | 26 | const rowClick = (row: any) => { 27 | console.log('You clicked on a row: ', row); 28 | }; 29 | 30 | const handleAddRow = () => { 31 | setData([ 32 | ...data, 33 | { 34 | Amount: 150.0, 35 | CustomerName: 'Tiempo Development', 36 | OrderID: 23, 37 | ShippedDate: '2016-01-04T18:00:00', 38 | ShipperCity: 'Monterrey, NL, Mexico', 39 | }, 40 | ]); 41 | }; 42 | 43 | const [anchorEl, setAnchorEl] = React.useState(null); 44 | const [searchText, setSearchText] = React.useState(tbList.state.searchText); 45 | 46 | const handleChangeSearch = (event: any) => { 47 | setSearchText(event.target.value); 48 | tbList.api.search(event.target.value); 49 | }; 50 | 51 | const handleClick = (event: React.MouseEvent) => setAnchorEl(event.currentTarget); 52 | 53 | const handleClose = () => setAnchorEl(null); 54 | 55 | const sortEvent = columnName => tbList.api.sortByColumn(columnName); 56 | 57 | const handleColumnSelect = (colName: string) => (event: any) => { 58 | sortEvent(colName); 59 | handleClose(); 60 | }; 61 | 62 | return ( 63 |
64 |
65 | 68 |
69 | 77 |
78 | 85 | OrderID 86 | CustomerName 87 | ShipperCity 88 | 89 |
90 |
91 | 92 |
93 |
94 | ); 95 | }; 96 | 97 | export default TbListExample; 98 | -------------------------------------------------------------------------------- /sample/src/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import createMuiTheme from '@material-ui/core/styles/createMuiTheme'; 5 | import responsiveFontSizes from '@material-ui/core/styles/responsiveFontSizes'; 6 | import ThemeProvider from '@material-ui/styles/ThemeProvider'; 7 | 8 | import Main from './main'; 9 | 10 | let theme = createMuiTheme(); 11 | theme = responsiveFontSizes(theme); 12 | 13 | render( 14 | 15 |
16 | , 17 | document.getElementById('root'), 18 | ); 19 | -------------------------------------------------------------------------------- /sample/src/data/columns.ts: -------------------------------------------------------------------------------- 1 | import { AggregateFunctions, ColumnDataType, ColumnSortDirection, createColumn } from 'tubular-common'; 2 | 3 | const columns = [ 4 | createColumn('OrderID', { 5 | dataType: ColumnDataType.Numeric, 6 | filterable: true, 7 | isKey: true, 8 | label: 'Id', 9 | sortDirection: ColumnSortDirection.Ascending, 10 | sortOrder: 1, 11 | sortable: true, 12 | }), 13 | createColumn('CustomerName', { 14 | aggregate: AggregateFunctions.Count, 15 | filterable: true, 16 | searchable: true, 17 | sortable: true, 18 | }), 19 | createColumn('ShippedDate', { 20 | dataType: ColumnDataType.DateTime, 21 | filterable: true, 22 | sortable: true, 23 | }), 24 | createColumn('ShipperCity'), 25 | createColumn('Amount', { 26 | dataType: ColumnDataType.Numeric, 27 | sortable: true, 28 | }), 29 | createColumn('IsShipped', { 30 | dataType: ColumnDataType.Boolean, 31 | filterable: true, 32 | sortable: true, 33 | }), 34 | ]; 35 | export default columns; 36 | -------------------------------------------------------------------------------- /sample/src/data/localData.ts: -------------------------------------------------------------------------------- 1 | const localData = [ 2 | { 3 | OrderID: 1, 4 | CustomerName: 'Microsoft', 5 | ShippedDate: '2016-03-19T19:00:00', 6 | ShipperCity: 'Guadalajara, JAL, Mexico', 7 | Amount: 300.0, 8 | }, 9 | { 10 | OrderID: 2, 11 | CustomerName: 'Microsoft', 12 | ShippedDate: '2016-11-08T18:00:00', 13 | ShipperCity: 'Los Angeles, CA, USA', 14 | Amount: 9.0, 15 | }, 16 | { 17 | OrderID: 3, 18 | CustomerName: 'Unosquare LLC', 19 | ShippedDate: '2016-11-08T18:00:00', 20 | ShipperCity: 'Guadalajara, JAL, Mexico', 21 | Amount: 92.0, 22 | }, 23 | { 24 | OrderID: 4, 25 | CustomerName: 'Vesta', 26 | ShippedDate: '2016-03-19T19:00:00', 27 | ShipperCity: 'Portland, OR, USA', 28 | Amount: 300.0, 29 | }, 30 | { 31 | OrderID: 5, 32 | CustomerName: 'Super La Playa', 33 | ShippedDate: null, 34 | ShipperCity: 'Leon, GTO, Mexico', 35 | Amount: 174.0, 36 | }, 37 | { 38 | OrderID: 6, 39 | CustomerName: 'OXXO', 40 | ShippedDate: '', 41 | ShipperCity: 'Guadalajara, JAL, Mexico', 42 | Amount: 92.0, 43 | }, 44 | { 45 | OrderID: 7, 46 | CustomerName: 'Super La Playa', 47 | ShippedDate: '2016-03-19T19:00:00', 48 | ShipperCity: 'Portland, OR, USA', 49 | Amount: 300.0, 50 | }, 51 | { 52 | OrderID: 8, 53 | CustomerName: 'Super La Playa', 54 | ShippedDate: '2016-04-23T10:00:00', 55 | ShipperCity: 'Leon, GTO, Mexico', 56 | Amount: 15.0, 57 | }, 58 | { 59 | OrderID: 9, 60 | CustomerName: 'OXXO', 61 | ShippedDate: '2016-12-22T08:00:00', 62 | ShipperCity: 'Guadalajara, JAL, Mexico', 63 | Amount: 92.0, 64 | }, 65 | { 66 | OrderID: 10, 67 | CustomerName: 'Vesta', 68 | ShippedDate: '2016-03-19T19:00:00', 69 | ShipperCity: 'Portland, OR, USA', 70 | Amount: 300.0, 71 | }, 72 | { 73 | OrderID: 11, 74 | CustomerName: 'Microsoft', 75 | ShippedDate: '2016-04-23T10:00:00', 76 | ShipperCity: 'Leon, GTO, Mexico', 77 | Amount: 16.0, 78 | }, 79 | { 80 | OrderID: 12, 81 | CustomerName: 'OXXO', 82 | ShippedDate: '2016-11-08T18:00:00', 83 | ShipperCity: 'Guadalajara, JAL, Mexico', 84 | Amount: 92.0, 85 | }, 86 | { 87 | OrderID: 13, 88 | CustomerName: 'Unosquare LLC', 89 | ShippedDate: '2016-03-19T19:00:00', 90 | ShipperCity: 'Portland, OR, USA', 91 | Amount: 300.0, 92 | }, 93 | { 94 | OrderID: 14, 95 | CustomerName: 'Vesta', 96 | ShippedDate: '2016-04-23T10:00:00', 97 | ShipperCity: 'Guadalajara, JAL, Mexico', 98 | Amount: 60.0, 99 | }, 100 | { 101 | OrderID: 15, 102 | CustomerName: 'Super La Playa', 103 | ShippedDate: '2016-12-22T08:00:00', 104 | ShipperCity: 'Portland, OR, US', 105 | Amount: 192.0, 106 | }, 107 | { 108 | OrderID: 16, 109 | CustomerName: 'Microsoft', 110 | ShippedDate: '2016-03-19T19:00:00', 111 | ShipperCity: 'Leon, GTO, Mexico', 112 | Amount: 300.0, 113 | }, 114 | { 115 | OrderID: 17, 116 | CustomerName: 'Unosquare LLC', 117 | ShippedDate: '2016-04-23T10:00:00', 118 | ShipperCity: 'Leon, GTO, Mexico', 119 | Amount: 108.0, 120 | }, 121 | { 122 | OrderID: 18, 123 | CustomerName: 'Microsoft', 124 | ShippedDate: '2016-12-22T08:00:00', 125 | ShipperCity: 'Los Angeles, CA, USA', 126 | Amount: 92.0, 127 | }, 128 | { 129 | OrderID: 19, 130 | CustomerName: 'Vesta', 131 | ShippedDate: '2016-11-08T18:00:00', 132 | ShipperCity: 'Guadalajara, JAL, Mexico', 133 | Amount: 300.0, 134 | }, 135 | { 136 | OrderID: 20, 137 | CustomerName: 'OXXO', 138 | ShippedDate: '2016-11-04T18:00:00', 139 | ShipperCity: 'Portland, OR, USA', 140 | Amount: 78.0, 141 | }, 142 | { 143 | OrderID: 21, 144 | CustomerName: 'Wizeline', 145 | ShippedDate: '2015-11-04T18:00:00', 146 | ShipperCity: 'Guadalajara, JAL, Mexico', 147 | Amount: 100.0, 148 | }, 149 | { 150 | OrderID: 22, 151 | CustomerName: 'Tiempo Development', 152 | ShippedDate: '2016-01-04T18:00:00', 153 | ShipperCity: 'Monterrey, NL, Mexico', 154 | Amount: 150.0, 155 | }, 156 | ]; 157 | 158 | export default localData; 159 | -------------------------------------------------------------------------------- /sample/src/localDataGrid.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import Button from '@material-ui/core/Button'; 4 | import Snackbar from '@material-ui/core/Snackbar'; 5 | import { LocalStorage } from 'tubular-common'; 6 | import { DataGrid } from '../../src'; 7 | import { ToolbarOptions } from '../../src/Toolbar/ToolbarOptions'; 8 | import columns from './data/columns'; 9 | import localData from './data/localData'; 10 | 11 | const LocalDataGrid: React.FunctionComponent = () => { 12 | const [getErrorMessage, setErrorMessage] = React.useState(null as string); 13 | const [data, setData] = React.useState(localData); 14 | 15 | const rowClick = (row: {}) => { 16 | console.log('You clicked on a row: ', row); 17 | }; 18 | 19 | const handleAddRow = () => { 20 | setData([ 21 | ...data, 22 | { 23 | Amount: 150.0, 24 | CustomerName: 'Tiempo Development', 25 | OrderID: 23, 26 | ShippedDate: '2016-01-04T18:00:00', 27 | ShipperCity: 'Monterrey, NL, Mexico', 28 | }, 29 | ]); 30 | }; 31 | 32 | const toolbarOptions = new ToolbarOptions({ 33 | customItems: , 34 | }); 35 | 36 | return ( 37 |
38 | {getErrorMessage && ( 39 | {getErrorMessage}} 45 | /> 46 | )} 47 | 57 |
58 | ); 59 | }; 60 | 61 | export default LocalDataGrid; 62 | -------------------------------------------------------------------------------- /sample/src/main.tsx: -------------------------------------------------------------------------------- 1 | import AppBar from '@material-ui/core/AppBar'; 2 | import Tab from '@material-ui/core/Tab'; 3 | import Tabs from '@material-ui/core/Tabs'; 4 | import makeStyles from '@material-ui/styles/makeStyles'; 5 | 6 | import * as React from 'react'; 7 | 8 | import ColumnFeatures from './ColumnFeatures/ColumnFeatures'; 9 | import CustomLayoutDataGrid from './CustomLayoutDataGrid'; 10 | import ErrorBoundary from './ErrorBoundary'; 11 | import LocalDataGrid from './localDataGrid'; 12 | import MasterDetailRowSample from './masterDetailRowSample'; 13 | import RemoteDataGrid from './remoteDataGrid'; 14 | import RemoteGridList from './remoteGridList'; 15 | import TbListExample from './TbListExample'; 16 | 17 | const useStyles = makeStyles(({ palette }: any) => ({ 18 | logo: { 19 | color: 'rgb(255, 255, 255)', 20 | display: 'block', 21 | height: 50, 22 | maxWidth: 150, 23 | width: 150, 24 | }, 25 | root: { 26 | backgroundColor: palette.background.paper, 27 | display: 'flex', 28 | flexDirection: 'column', 29 | flexGrow: 1, 30 | height: '100%', 31 | }, 32 | })); 33 | 34 | const Main: React.FunctionComponent = () => { 35 | const classes = useStyles({}); 36 | const [currentValue, setValue] = React.useState(0); 37 | 38 | const handleChange = (event: any, value: any) => setValue(value); 39 | 40 | return ( 41 | 42 |
43 | 44 | Tubular React 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {currentValue === 0 && } 60 | {currentValue === 1 && } 61 | {currentValue === 2 && } 62 | {currentValue === 3 && } 63 | {currentValue === 4 && } 64 | {currentValue === 5 && } 65 | {currentValue === 6 && } 66 |
67 |
68 | ); 69 | }; 70 | 71 | export default Main; 72 | -------------------------------------------------------------------------------- /sample/src/masterDetailRowSample.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import Button from '@material-ui/core/Button'; 4 | import Snackbar from '@material-ui/core/Snackbar'; 5 | import { LocalStorage } from 'tubular-common'; 6 | import { DataGrid } from '../../src'; 7 | import { ToolbarOptions } from '../../src/Toolbar/ToolbarOptions'; 8 | import columns from './data/columns'; 9 | import localData from './data/localData'; 10 | import DetailComponentProps from '../../src/BareBones/DetailComponentProps'; 11 | 12 | const DetailComponent: React.FunctionComponent = ({ row }: DetailComponentProps) => ( 13 |
This is a test with the row #{row.OrderID}
14 | ); 15 | 16 | const MasterDetailRowSample: React.FunctionComponent = () => { 17 | const [getErrorMessage, setErrorMessage] = React.useState(null as string); 18 | const [data, setData] = React.useState(localData); 19 | 20 | const rowClick = (row: {}) => { 21 | console.log('You clicked on a row: ', row); 22 | }; 23 | 24 | const handleAddRow = () => { 25 | setData([ 26 | ...data, 27 | { 28 | Amount: 150.0, 29 | CustomerName: 'Tiempo Development', 30 | OrderID: 23, 31 | ShippedDate: '2016-01-04T18:00:00', 32 | ShipperCity: 'Monterrey, NL, Mexico', 33 | }, 34 | ]); 35 | }; 36 | 37 | const toolbarOptions = new ToolbarOptions({ 38 | customItems: , 39 | }); 40 | 41 | return ( 42 |
43 | {getErrorMessage && ( 44 | {getErrorMessage}} 50 | /> 51 | )} 52 | 63 |
64 | ); 65 | }; 66 | 67 | export default MasterDetailRowSample; 68 | -------------------------------------------------------------------------------- /sample/src/remoteDataGrid.tsx: -------------------------------------------------------------------------------- 1 | import Button from '@material-ui/core/Button'; 2 | import * as React from 'react'; 3 | import { LocalStorage } from 'tubular-common'; 4 | import { useGridRefresh } from 'tubular-react-common'; 5 | import { DataGrid, ToolbarOptions } from '../../src'; 6 | import columns from './data/columns'; 7 | import Tooltip from '@material-ui/core/Tooltip'; 8 | import IconButton from '@material-ui/core/IconButton'; 9 | import DeleteIcon from '@material-ui/icons/Delete'; 10 | 11 | const RemoteDataGrid: React.FunctionComponent = () => { 12 | const [refresh, forceRefresh] = useGridRefresh(); 13 | const forceGridRefresh = () => forceRefresh(); 14 | 15 | const rowClick = (row: any) => console.log('You clicked on a row: ', row); 16 | 17 | const toolbarButton = new ToolbarOptions({ 18 | customItems: , 19 | actionsArea: ({ selection }: any) => { 20 | return ( 21 | 22 | console.log(selection.getSelectedRows())}> 23 | 24 | 25 | 26 | ); 27 | }, 28 | }); 29 | 30 | return ( 31 | 41 | ); 42 | }; 43 | 44 | export default RemoteDataGrid; 45 | -------------------------------------------------------------------------------- /src/BareBones/DetailComponentProps.ts: -------------------------------------------------------------------------------- 1 | export default interface DetailComponentProps { 2 | row?: any; 3 | } 4 | -------------------------------------------------------------------------------- /src/BareBones/TbMobileRow.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ColumnModel } from 'tubular-common'; 3 | import { DataGridCard } from '../DataGrid'; 4 | 5 | export interface TbMobileRowProps { 6 | columns: ColumnModel[]; 7 | onRowClick(row: any): void; 8 | row: any; 9 | } 10 | 11 | export const TbMobileRow: React.FunctionComponent = ({ 12 | columns, 13 | onRowClick, 14 | row, 15 | }: TbMobileRowProps) => { 16 | return ; 17 | }; 18 | -------------------------------------------------------------------------------- /src/BareBones/TbRow.tsx: -------------------------------------------------------------------------------- 1 | import TableRow from '@material-ui/core/TableRow'; 2 | import * as React from 'react'; 3 | import { ColumnModel } from 'tubular-common'; 4 | import { renderCells } from '../utils/renders'; 5 | import TableCell from '@material-ui/core/TableCell'; 6 | import Checkbox from '@material-ui/core/Checkbox'; 7 | import { TbSelection } from '../utils/Selection'; 8 | import { Collapse, IconButton } from '@material-ui/core'; 9 | import KeyboardArrowRightIcon from '@material-ui/icons/KeyboardArrowRight'; 10 | import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; 11 | import { useMasterDetails } from 'tubular-react-common'; 12 | import DetailComponentProps from './DetailComponentProps'; 13 | 14 | export interface TbRowProps { 15 | row: any; 16 | rowIndex: number; 17 | columns: ColumnModel[]; 18 | onRowClick?(): void; 19 | rowSelectionEnabled?: boolean; 20 | selection?: TbSelection; 21 | detailComponent?: React.FunctionComponent; 22 | } 23 | 24 | export const TbRow: React.FunctionComponent = ({ 25 | row, 26 | columns, 27 | onRowClick, 28 | rowSelectionEnabled, 29 | selection, 30 | detailComponent, 31 | }: TbRowProps) => { 32 | const [open, openDetails] = useMasterDetails(); 33 | const openMasterDetails = () => { 34 | openDetails(); 35 | }; 36 | 37 | const DetailComponent = detailComponent ? detailComponent : null; 38 | return ( 39 | <> 40 | 41 | {detailComponent && ( 42 | 43 | 44 | {open ? : } 45 | 46 | 47 | )} 48 | {rowSelectionEnabled && selection.rowSelection[row[columns.find((c) => c.isKey).name]] !== undefined && ( 49 | 50 | c.isKey).name]]} 52 | onChange={() => { 53 | selection.toggleRowSelection(row[columns.find((c) => c.isKey).name]); 54 | }} 55 | value={selection.rowSelection[row[columns.find((c) => c.isKey).name]]} 56 | inputProps={{ 'aria-label': 'select all desserts' }} 57 | /> 58 | 59 | )} 60 | {renderCells(columns, row)} 61 | 62 | {detailComponent && open && ( 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | )} 71 | 72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /src/BareBones/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TbMobileRow'; 2 | export * from './TbRow'; 3 | export * from './DetailComponentProps'; 4 | -------------------------------------------------------------------------------- /src/DataGrid/DataGrid.tsx: -------------------------------------------------------------------------------- 1 | import LinearProgress from '@material-ui/core/LinearProgress'; 2 | import Paper from '@material-ui/core/Paper'; 3 | import Table from '@material-ui/core/Table'; 4 | import TableHead from '@material-ui/core/TableHead'; 5 | import TableRow from '@material-ui/core/TableRow'; 6 | import makeStyles from '@material-ui/styles/makeStyles'; 7 | import * as React from 'react'; 8 | import { ColumnModel, DataGridStorage, TubularHttpClientAbstract, CompareOperators } from 'tubular-common'; 9 | import { useTbTable } from 'tubular-react-common'; 10 | import { useResolutionSwitch } from 'uno-react'; 11 | import { TbRowProps } from '../BareBones/TbRow'; 12 | import DetailComponentProps from '../BareBones/DetailComponentProps'; 13 | import { Paginator } from '../Pagination'; 14 | import { GridToolbar } from '../Toolbar/GridToolbar'; 15 | import { ToolbarOptions } from '../Toolbar/ToolbarOptions'; 16 | import { DataGridTable } from './'; 17 | import { MobileDataGridTable } from './MobileDataGridTable'; 18 | import { ChipBar } from '../Filtering/ChipBar'; 19 | import { useTbSelection } from '../hooks/useTbSelection'; 20 | import { SelectionToolbar } from '../Toolbar/SelectionToolbar'; 21 | 22 | const useStyles = makeStyles({ 23 | linearProgress: { 24 | marginTop: '-10px', 25 | height: '20px', 26 | }, 27 | root: { 28 | overflowX: 'auto', 29 | width: '100%', 30 | }, 31 | }); 32 | 33 | const timeout = 400; 34 | 35 | export interface DataGridProps { 36 | columns: ColumnModel[]; 37 | dataSource: any[] | string | Request | TubularHttpClientAbstract; 38 | deps?: any[]; 39 | detailComponent?: React.FunctionComponent; 40 | gridName: string; 41 | storage?: DataGridStorage; 42 | toolbarOptions?: ToolbarOptions; 43 | 44 | // ToDo: new ones: 45 | mobileBreakpointWidth?: number; 46 | rowComponent?: React.FunctionComponent; 47 | rowMobileComponent?: React.FunctionComponent; 48 | footerComponent?: React.FunctionComponent; 49 | onError?(err: string): void; 50 | onRowClick?(row: any): void; 51 | rowSelectionEnabled?: boolean; 52 | } 53 | 54 | export const DataGrid: React.FunctionComponent = (props: DataGridProps) => { 55 | const { 56 | columns, 57 | dataSource, 58 | deps, 59 | footerComponent, 60 | gridName, 61 | mobileBreakpointWidth = props.mobileBreakpointWidth || 800, 62 | onError, 63 | onRowClick, 64 | rowComponent, 65 | rowMobileComponent, 66 | storage, 67 | toolbarOptions = props.toolbarOptions || new ToolbarOptions(), 68 | detailComponent, 69 | rowSelectionEnabled, 70 | } = props; 71 | 72 | const classes = useStyles({}); 73 | 74 | const tbTableInstance = useTbTable(columns, dataSource, { 75 | callbacks: { onError }, 76 | componentName: gridName, 77 | deps, 78 | pagination: { 79 | itemsPerPage: toolbarOptions.itemsPerPage, 80 | }, 81 | storage, 82 | }); 83 | 84 | const [isMobileResolution] = useResolutionSwitch(mobileBreakpointWidth, timeout); 85 | const selection = useTbSelection(tbTableInstance, rowSelectionEnabled); 86 | 87 | const showSelectionToolbar = rowSelectionEnabled && selection.getSelectedCount() > 0; 88 | if (isMobileResolution) { 89 | toolbarOptions.SetMobileMode(); 90 | 91 | return ( 92 | 93 | {!showSelectionToolbar && ( 94 | 99 | )} 100 | {showSelectionToolbar && } 101 |
102 | {tbTableInstance.state.isLoading && } 103 |
104 | 109 | 114 |
115 | ); 116 | } 117 | 118 | const paginator = ( 119 | 120 | 121 | 122 | 127 | 128 | 129 |
130 | ); 131 | 132 | const applyOrResetFilter = (columnName: string, value?: string) => { 133 | const newColumns = tbTableInstance.state.columns.map((column) => { 134 | if (column.name === columnName) { 135 | return { 136 | ...column, 137 | filterText: value, 138 | filterOperator: !!value ? CompareOperators.Equals : CompareOperators.None, 139 | filterArgument: !!value ? [] : null, 140 | }; 141 | } 142 | 143 | return column; 144 | }); 145 | 146 | tbTableInstance.api.setColumns(newColumns); 147 | }; 148 | 149 | return ( 150 | 151 | {!showSelectionToolbar && ( 152 | 153 | )} 154 | {showSelectionToolbar && ( 155 | 156 | )} 157 |
158 | {tbTableInstance.state.isLoading && } 159 |
160 | 161 | 170 | {toolbarOptions.enablePagination && paginator} 171 |
172 | ); 173 | }; 174 | -------------------------------------------------------------------------------- /src/DataGrid/DataGridCard.tsx: -------------------------------------------------------------------------------- 1 | import Card from '@material-ui/core/Card'; 2 | import CardActions from '@material-ui/core/CardActions'; 3 | import CardContent from '@material-ui/core/CardContent'; 4 | import IconButton from '@material-ui/core/IconButton'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import LabelImportant from '@material-ui/icons/LabelImportant'; 7 | import makeStyles from '@material-ui/styles/makeStyles'; 8 | import * as React from 'react'; 9 | import { ColumnDataType, ColumnModel } from 'tubular-common'; 10 | import { humanize } from 'uno-js'; 11 | 12 | const useStyles = makeStyles(() => ({ 13 | cardActions: { 14 | justifyContent: 'flex-end', 15 | paddingTop: 0, 16 | }, 17 | cardBtn: { 18 | color: 'none', 19 | textDecoration: 'none', 20 | }, 21 | cardMobile: { 22 | marginBottom: '2px', 23 | marginLeft: '10px', 24 | marginRight: '10px', 25 | maxHeight: '400px', 26 | minHeight: '200px', 27 | minWidth: '95%', 28 | }, 29 | dataLabel: { 30 | flexDirection: 'column', 31 | flexGrow: 1, 32 | fontWeight: 'bold', 33 | margin: '4px', 34 | textAlign: 'right', 35 | width: '50%', 36 | }, 37 | dataRow: { 38 | display: 'flex', 39 | flexDirection: 'row', 40 | justifyContent: 'flex-start ', 41 | }, 42 | dataValue: { 43 | flexDirection: 'column', 44 | flexGrow: 1, 45 | margin: '2px', 46 | textAlign: 'left', 47 | width: '50%', 48 | }, 49 | })); 50 | 51 | const renderGeneral = (column: ColumnModel, item: any) => item[column.name]; 52 | 53 | const renderBoolean = (column: ColumnModel, item: any) => ( 54 | 55 | ); 56 | 57 | const renderString = (column: ColumnModel, item: any) => 58 | item[column.name] && item[column.name].length > 50 59 | ? item[column.name].substring(0, 50) + '...' 60 | : renderGeneral(column, item); 61 | 62 | const columnRender = (column: ColumnModel, item: any) => { 63 | switch (column.dataType) { 64 | case ColumnDataType.Boolean: 65 | return renderBoolean(column, item); 66 | case ColumnDataType.String: 67 | return renderString(column, item); 68 | default: 69 | return renderGeneral(column, item); 70 | } 71 | }; 72 | 73 | export interface DataGridCardProps { 74 | columns: ColumnModel[]; 75 | item: any; 76 | onClickCallback: (row: any) => void; 77 | } 78 | 79 | export const DataGridCard = ({ columns, item, onClickCallback }: DataGridCardProps) => { 80 | const classes = useStyles({}); 81 | 82 | return ( 83 | 84 | 85 | {columns.map((column: ColumnModel, index: number) => ( 86 |
87 | 88 | {humanize(column.name)}: 89 | 90 | 91 | {columnRender(column, item)} 92 | 93 |
94 | ))} 95 | 96 | {onClickCallback && ( 97 | 98 | 99 | 100 | )} 101 | 102 |
103 |
104 | ); 105 | }; 106 | -------------------------------------------------------------------------------- /src/DataGrid/DataGridTable.tsx: -------------------------------------------------------------------------------- 1 | import Table from '@material-ui/core/Table'; 2 | import TableFooter from '@material-ui/core/TableFooter'; 3 | import TableHead from '@material-ui/core/TableHead'; 4 | 5 | import * as React from 'react'; 6 | import { ITbTableInstance } from 'tubular-react-common'; 7 | import { TbRowProps } from '../BareBones/TbRow'; 8 | import { GridBody } from './GridBody'; 9 | import { GridHeader } from './GridHeader'; 10 | import { TbSelection } from '../utils/Selection'; 11 | import DetailComponentProps from '../BareBones/DetailComponentProps'; 12 | 13 | export interface DataGridTableProps { 14 | tbTableInstance: ITbTableInstance; 15 | rowComponent: React.FunctionComponent; 16 | footerComponent: React.FunctionComponent; 17 | detailComponent?: React.FunctionComponent; 18 | rowSelectionEnabled?: boolean; 19 | selection?: TbSelection; 20 | onRowClick?(row: any): void; 21 | } 22 | 23 | export const DataGridTable: React.FunctionComponent = (props: DataGridTableProps) => { 24 | const Footer = props.footerComponent; 25 | 26 | return ( 27 | 28 | 29 | 35 | 36 | 44 | {props.footerComponent && ( 45 | 46 |
47 | 48 | )} 49 |
50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/DataGrid/FeaturesDrawer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Tabs from '@material-ui/core/Tabs'; 3 | import Tab from '@material-ui/core/Tab'; 4 | import Typography from '@material-ui/core/Typography'; 5 | import Box from '@material-ui/core/Box'; 6 | import Drawer from '@material-ui/core/Drawer'; 7 | import { FiltersContainer } from '../Filtering/FiltersContainer'; 8 | import FilterListIcon from '@material-ui/icons/FilterList'; 9 | 10 | import { ColumnModel, CompareOperators, ColumnDataType } from 'tubular-common'; 11 | import { Button, Grid, AppBar, makeStyles } from '@material-ui/core'; 12 | 13 | interface TabPanelProps { 14 | children?: React.ReactNode; 15 | index: string; 16 | value: string; 17 | } 18 | 19 | const useStyles = makeStyles({ 20 | tabPanel: { 21 | // It seems the appbar for tabs will always be 72px 22 | height: 'calc(100% - 72px)', 23 | overflow: 'auto', 24 | }, 25 | mainWrapper: { 26 | height: '100%', 27 | width: 400, 28 | padding: 20, 29 | position: 'relative', 30 | overflow: 'hidden', 31 | }, 32 | featureContainer: { 33 | overflow: 'hidden', 34 | }, 35 | actionsArea: { 36 | paddingTop: 20, 37 | }, 38 | }); 39 | 40 | const TabPanel = (props: TabPanelProps) => { 41 | const { children, value, index, ...other } = props; 42 | const classes = useStyles(); 43 | 44 | return ( 45 | 56 | ); 57 | }; 58 | 59 | export interface FeaturesDrawerProps { 60 | columns: ColumnModel[]; 61 | onApplyFeatures: (columns: ColumnModel[]) => void; 62 | togglePanel: () => void; 63 | } 64 | 65 | const resolveFilterOperator = (column: ColumnModel): CompareOperators => 66 | (column.filterOperator = 67 | column.filterOperator === CompareOperators.None 68 | ? column.dataType === ColumnDataType.String 69 | ? CompareOperators.Contains 70 | : CompareOperators.Equals 71 | : column.filterOperator); 72 | 73 | const copyColumns = (columns: ColumnModel[]): ColumnModel[] => 74 | columns.map((column) => ({ 75 | ...column, 76 | filterOperator: resolveFilterOperator(column), 77 | })); 78 | 79 | export const FeaturesDrawer: React.FunctionComponent = ({ 80 | columns, 81 | onApplyFeatures, 82 | togglePanel, 83 | }: FeaturesDrawerProps) => { 84 | const tempColumns = copyColumns(columns); 85 | const classes = useStyles(); 86 | 87 | const [value, setValue] = React.useState('filters'); 88 | 89 | const handleChange = (_event: React.ChangeEvent, newValue: string) => { 90 | setValue(newValue); 91 | }; 92 | 93 | const onApplyClick = () => { 94 | onApplyFeatures(tempColumns); 95 | togglePanel(); 96 | }; 97 | 98 | return ( 99 | 100 | 109 | 110 | 111 | 112 | } label="Filters" /> 113 | {/* } label="Columns" /> */} 114 | 115 | 116 | 117 | c.filterable)} onApply={onApplyClick} /> 118 | 119 | {/* 120 | Toggle Columns 121 | */} 122 | 123 | 124 | 127 | 130 | 131 | 132 | 133 | ); 134 | }; 135 | -------------------------------------------------------------------------------- /src/DataGrid/GridBody.tsx: -------------------------------------------------------------------------------- 1 | import TableBody from '@material-ui/core/TableBody'; 2 | import * as React from 'react'; 3 | import { ITbTableInstance } from 'tubular-react-common'; 4 | import { TbRowProps, TbRow } from '../BareBones/TbRow'; 5 | import { NoDataRow } from './NoDataRow'; 6 | import { TbSelection } from '../utils/Selection'; 7 | import DetailComponentProps from '../BareBones/DetailComponentProps'; 8 | 9 | interface GridBodyProps { 10 | detailComponent?: React.FunctionComponent; 11 | tbTableInstance: ITbTableInstance; 12 | rowComponent: React.FunctionComponent; 13 | rowSelectionEnabled?: boolean; 14 | onRowClick?(row: any): void; 15 | selection?: TbSelection; 16 | } 17 | 18 | const getStyles = (isPointer: boolean) => ({ 19 | row: { cursor: isPointer ? 'pointer' : 'auto' }, 20 | title: { paddingLeft: '15px' }, 21 | }); 22 | 23 | const generateOnRowClickProxy = (onRowClick: any) => { 24 | return (row: any) => () => { 25 | if (onRowClick) { 26 | onRowClick(row); 27 | } 28 | }; 29 | }; 30 | 31 | export const GridBody: React.FunctionComponent = ({ 32 | tbTableInstance, 33 | rowComponent, 34 | onRowClick, 35 | detailComponent, 36 | rowSelectionEnabled, 37 | selection, 38 | }: GridBodyProps) => { 39 | const styles = getStyles(Boolean(onRowClick)); 40 | const RowComponent = rowComponent ? rowComponent : TbRow; 41 | const onRowClickProxy = onRowClick ? generateOnRowClickProxy(onRowClick) : (_row: any): (() => void) => void 0; 42 | const { state } = tbTableInstance; 43 | const columnKey = tbTableInstance.state.columns.find((c) => c.isKey); 44 | 45 | let content = null; 46 | 47 | if (state.filteredRecordCount === 0 && !state.isLoading) { 48 | content = ; 49 | } else { 50 | content = state.data.map((row: any, rowIndex: number) => { 51 | return ( 52 | 62 | ); 63 | }); 64 | } 65 | 66 | return {content}; 67 | }; 68 | -------------------------------------------------------------------------------- /src/DataGrid/GridHeader.tsx: -------------------------------------------------------------------------------- 1 | import TableCell from '@material-ui/core/TableCell'; 2 | import TableRow from '@material-ui/core/TableRow'; 3 | import * as React from 'react'; 4 | import { ColumnModel } from 'tubular-common'; 5 | import { ITbTableInstance } from 'tubular-react-common'; 6 | import { GridHeaderCell } from './GridHeaderCell'; 7 | import Checkbox from '@material-ui/core/Checkbox'; 8 | import { TbSelection } from '../utils/Selection'; 9 | import DetailComponentProps from '../BareBones/DetailComponentProps'; 10 | 11 | export interface GridHeaderProps { 12 | tbTableInstance: ITbTableInstance; 13 | detailComponent?: React.FunctionComponent; 14 | rowSelectionEnabled: boolean; 15 | selection?: TbSelection; 16 | } 17 | 18 | export const GridHeader: React.FunctionComponent = ({ 19 | tbTableInstance, 20 | detailComponent, 21 | rowSelectionEnabled, 22 | selection, 23 | }: GridHeaderProps) => { 24 | const { api, state } = tbTableInstance; 25 | 26 | return ( 27 | 28 | {detailComponent && } 29 | {rowSelectionEnabled && ( 30 | 31 | 0} 34 | onChange={selection.toggleAllRowsSelection} 35 | inputProps={{ 'aria-label': 'select all desserts' }} 36 | /> 37 | 38 | )} 39 | {state.columns 40 | .filter((col: ColumnModel) => col.visible) 41 | .map((column: ColumnModel) => ( 42 | 43 | ))} 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/DataGrid/GridHeaderCell.tsx: -------------------------------------------------------------------------------- 1 | import TableCell from '@material-ui/core/TableCell'; 2 | import TableSortLabel from '@material-ui/core/TableSortLabel'; 3 | import Tooltip from '@material-ui/core/Tooltip'; 4 | import * as React from 'react'; 5 | import { ColumnModel, ColumnSortDirection } from 'tubular-common'; 6 | import Lang from '../utils/Lang'; 7 | 8 | export interface GridHeaderCellProps { 9 | column: ColumnModel; 10 | key: string; 11 | sortColumn: (property: string) => void; 12 | } 13 | 14 | export const GridHeaderCell: React.FunctionComponent = ({ 15 | column, 16 | sortColumn, 17 | }: GridHeaderCellProps) => { 18 | const sort = () => sortColumn(column.name); 19 | const direction = 20 | column.sortDirection === ColumnSortDirection.Ascending || column.sortDirection === ColumnSortDirection.None 21 | ? 'asc' 22 | : 'desc'; 23 | 24 | const render = column.sortable ? ( 25 | 26 | 31 | {column.label} 32 | 33 | 34 | ) : ( 35 | column.label 36 | ); 37 | 38 | return ( 39 | 40 | {render} 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/DataGrid/MobileDataGridTable.tsx: -------------------------------------------------------------------------------- 1 | import GridList from '@material-ui/core/GridList'; 2 | import * as React from 'react'; 3 | 4 | import { ITbTableInstance } from 'tubular-react-common'; 5 | import { TbMobileRow } from '../BareBones/TbMobileRow'; 6 | import { TbRowProps } from '../BareBones/TbRow'; 7 | 8 | export interface MobileDataGridTableProps { 9 | tbTableInstance: ITbTableInstance; 10 | rowComponent?: React.FunctionComponent; 11 | onRowClick?(row: any): void; 12 | } 13 | 14 | const generateOnRowClickProxy = (onRowClick: any) => { 15 | return (row: any) => { 16 | return () => { 17 | if (onRowClick) { 18 | onRowClick(row); 19 | } 20 | }; 21 | }; 22 | }; 23 | 24 | export const MobileDataGridTable: React.FunctionComponent = ({ 25 | tbTableInstance, 26 | rowComponent, 27 | onRowClick, 28 | }: MobileDataGridTableProps) => { 29 | const RowComponent = rowComponent ? rowComponent : TbMobileRow; 30 | const onRowClickProxy = onRowClick ? generateOnRowClickProxy(onRowClick) : (_row: any): (() => void) => void 0; 31 | 32 | return ( 33 | 34 | {tbTableInstance.state.data.map((row: any, index: number) => ( 35 | 42 | ))} 43 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/DataGrid/NoDataRow.tsx: -------------------------------------------------------------------------------- 1 | import TableCell from '@material-ui/core/TableCell'; 2 | import TableRow from '@material-ui/core/TableRow'; 3 | import Typography from '@material-ui/core/Typography'; 4 | import Warning from '@material-ui/icons/Warning'; 5 | import * as React from 'react'; 6 | import { ColumnModel } from 'tubular-common'; 7 | import Lang from '../utils/Lang'; 8 | 9 | export interface NoDataRowProps { 10 | columns: ColumnModel[]; 11 | styles: any; 12 | } 13 | 14 | export const NoDataRow: React.FunctionComponent = ({ columns, styles }: NoDataRowProps) => ( 15 | 16 | col.visible).length}> 17 | 18 | {Lang.translate('NoRecords')} 19 | 20 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /src/DataGrid/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DataGridTable'; 2 | export * from './DataGridCard'; 3 | export * from './DataGrid'; 4 | -------------------------------------------------------------------------------- /src/Filtering/BooleanFilterEditor.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { FilterEditorProps } from './utils'; 3 | import { CompareOperators } from 'tubular-common'; 4 | import RadioGroup from '@material-ui/core/RadioGroup'; 5 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 6 | import Radio from '@material-ui/core/Radio'; 7 | import CheckBoxIcon from '@material-ui/icons/CheckBox'; 8 | import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; 9 | import { makeStyles } from '@material-ui/core/styles'; 10 | 11 | const useStyles = makeStyles({ 12 | label: { 13 | paddingTop: 5, 14 | }, 15 | }); 16 | 17 | export const BooleanFilterEditor = ({ column }: FilterEditorProps) => { 18 | const [selectedOption, setSelectedOption] = React.useState(column.filterText || 'all'); 19 | const classes = useStyles(); 20 | const onChoiceChange = (value: string) => { 21 | setSelectedOption(value); 22 | if (value === 'all') { 23 | column.filterOperator = CompareOperators.None; 24 | column.filterText = null; 25 | return; 26 | } 27 | 28 | column.filterOperator = CompareOperators.Equals; 29 | column.filterText = value; 30 | }; 31 | 32 | return ( 33 |
34 | onChoiceChange(value)} 39 | > 40 | } label={} /> 41 | } 45 | label={} 46 | /> 47 | } label="All" /> 48 | 49 |
50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/Filtering/ChipBar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ColumnModel, columnHasFilter } from 'tubular-common'; 3 | import { ChipFilter } from './ChipFilter'; 4 | 5 | export interface ChipBarProps { 6 | columns: ColumnModel[]; 7 | onClearFilter: (columnName: string) => void; 8 | } 9 | 10 | export const ChipBar: React.FunctionComponent = ({ columns, onClearFilter }: ChipBarProps) => { 11 | const filteredColumns = columns.filter((c) => columnHasFilter(c) && c.filterable); 12 | const onRemove = (columnName: string) => () => onClearFilter(columnName); 13 | 14 | return ( 15 |
16 | {filteredColumns.map((column) => ( 17 | 18 | ))} 19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/Filtering/ChipFilter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ColumnModel, ColumnDataType, CompareOperators } from 'tubular-common'; 3 | import Chip from '@material-ui/core/Chip'; 4 | import { getOperatorIcon } from './utils'; 5 | import { makeStyles } from '@material-ui/core/styles'; 6 | import CheckBoxIcon from '@material-ui/icons/CheckBox'; 7 | import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; 8 | 9 | export interface IChipFilterProps { 10 | column: ColumnModel; 11 | onRemoveFilter: (columnName: string) => void; 12 | } 13 | 14 | const convertToFriendlyDateString = (date: string | number) => new Date(date).toDateString(); 15 | 16 | const getFilterText = (column: ColumnModel) => { 17 | const isDate = 18 | column.dataType === ColumnDataType.Date || 19 | column.dataType === ColumnDataType.DateTime || 20 | column.dataType === ColumnDataType.DateTimeUtc; 21 | 22 | const filterText = isDate ? convertToFriendlyDateString(column.filterText) : column.filterText; 23 | 24 | if (column.filterOperator === CompareOperators.Between) { 25 | let argument = column.filterArgument[0]; 26 | if (isDate) { 27 | argument = convertToFriendlyDateString(argument); 28 | } 29 | return `${filterText} - ${argument}`; 30 | } 31 | 32 | if (column.dataType === ColumnDataType.Boolean) { 33 | return filterText === 'true' ? : ; 34 | } 35 | 36 | return filterText; 37 | }; 38 | 39 | const useStyles = makeStyles({ 40 | root: { 41 | marginRight: 6, 42 | }, 43 | label: { 44 | display: 'flex', 45 | alignItems: 'center', 46 | }, 47 | }); 48 | 49 | export const ChipFilter: React.FunctionComponent = ({ column, onRemoveFilter }: IChipFilterProps) => { 50 | const filterValue = getFilterText(column); 51 | const classes = useStyles(); 52 | const labelNode = ( 53 | <> 54 | {column.label} 55 | {getOperatorIcon(column.filterOperator)} 56 | {filterValue} 57 | 58 | ); 59 | 60 | return onRemoveFilter(column.name)} />; 61 | }; 62 | -------------------------------------------------------------------------------- /src/Filtering/DateFilter.tsx: -------------------------------------------------------------------------------- 1 | import 'date-fns'; 2 | import * as React from 'react'; 3 | import Grid from '@material-ui/core/Grid'; 4 | import DateFnsUtils from '@date-io/date-fns'; 5 | import { LocalizationProvider, DatePicker } from '@material-ui/pickers'; 6 | import { ColumnModel, CompareOperators } from 'tubular-common'; 7 | import { TextField } from '@material-ui/core'; 8 | 9 | export interface DateFilterProps { 10 | column: ColumnModel; 11 | onApply: () => void; 12 | } 13 | 14 | const getInitialDates = (column: ColumnModel) => { 15 | const dates: [Date, Date] = [null, null]; 16 | 17 | const startDate = Date.parse(column.filterText); 18 | 19 | if (!isNaN(startDate)) { 20 | dates[0] = new Date(startDate); 21 | } 22 | 23 | const toDate = Date.parse( 24 | column.filterArgument && column.filterArgument[0] ? column.filterArgument[0].toString() : null, 25 | ); 26 | 27 | if (!isNaN(startDate)) { 28 | dates[1] = new Date(toDate); 29 | } 30 | 31 | return dates; 32 | }; 33 | 34 | export const DateFilter: React.FunctionComponent = ({ column }: DateFilterProps) => { 35 | const [dates, setDates] = React.useState(getInitialDates(column)); 36 | 37 | const handleDateChange = (isSecondInput?: boolean) => (date: Date | null | undefined) => { 38 | const normalizedDate = !!date ? date : null; 39 | if (isSecondInput) { 40 | column.filterArgument = []; 41 | setDates([dates[0], normalizedDate]); 42 | column.filterArgument[0] = normalizedDate ? normalizedDate.toISOString() : null; 43 | } else { 44 | setDates([normalizedDate, dates[1]]); 45 | column.filterText = normalizedDate ? normalizedDate.toISOString() : null; 46 | } 47 | }; 48 | 49 | const isBetween = column.filterOperator === CompareOperators.Between; 50 | 51 | return ( 52 | 53 | 54 | 55 | } 60 | /> 61 | 62 | {column.filterOperator === CompareOperators.Between && ( 63 | 64 | } 69 | /> 70 | 71 | )} 72 | 73 | 74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /src/Filtering/FilterControl.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ColumnModel, columnHasFilter, ColumnDataType } from 'tubular-common'; 3 | import { StandardFilterEditor } from './StandardFilterEditor'; 4 | import ExpansionPanel from '@material-ui/core/ExpansionPanel'; 5 | import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary'; 6 | import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails'; 7 | import Typography from '@material-ui/core/Typography'; 8 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; 9 | import { Theme, makeStyles } from '@material-ui/core/styles'; 10 | import { BooleanFilterEditor } from './BooleanFilterEditor'; 11 | 12 | export interface FilterControlProps { 13 | column: ColumnModel; 14 | onApply: () => void; 15 | } 16 | 17 | const useStyles = makeStyles((theme: Theme) => ({ 18 | root: { 19 | backgroundColor: theme.palette.primary.main, 20 | color: theme.palette.primary.contrastText, 21 | }, 22 | expandIcon: { 23 | color: theme.palette.primary.contrastText, 24 | }, 25 | })); 26 | 27 | export const FilterControl: React.FunctionComponent = ({ column, onApply }: FilterControlProps) => { 28 | const hasFilter = columnHasFilter(column); 29 | const classes = useStyles(); 30 | const FilterEditor = column.dataType === ColumnDataType.Boolean ? BooleanFilterEditor : StandardFilterEditor; 31 | 32 | return ( 33 | 34 | } 37 | aria-controls="panel1a-content" 38 | > 39 | {column.label} 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/Filtering/FiltersContainer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import List from '@material-ui/core/List'; 3 | import { ColumnModel } from 'tubular-common'; 4 | import { FilterControl } from './FilterControl'; 5 | 6 | export interface FiltersContainerProps { 7 | columns: ColumnModel[]; 8 | onApply: () => void; 9 | } 10 | 11 | export const FiltersContainer: React.FunctionComponent = ({ 12 | columns, 13 | onApply, 14 | }: FiltersContainerProps) => { 15 | return ( 16 | 17 | {columns.map((column) => ( 18 | 19 | ))} 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/Filtering/NumericFilter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { onKeyDown, FilterEditorProps } from './utils'; 3 | import { CompareOperators } from 'tubular-common'; 4 | // import { ITextStyles } from 'office-ui-fabric-react'; 5 | import TextField from '@material-ui/core/TextField'; 6 | import Grid from '@material-ui/core/Grid'; 7 | 8 | // const secondInputStyle: ITextStyles = { 9 | // root: { 10 | // marginTop: 5, 11 | // }, 12 | // }; 13 | 14 | export const NumericFilter = ({ column, onApply }: FilterEditorProps) => { 15 | const handleFilterChange = (isSecondInput?: boolean) => ( 16 | event: React.ChangeEvent, 17 | ) => { 18 | const newValue = event.target.value; 19 | if (isSecondInput) { 20 | column.filterArgument = []; 21 | column.filterArgument[0] = newValue; 22 | } else { 23 | column.filterText = newValue; 24 | } 25 | }; 26 | 27 | const isBetween = column.filterOperator === CompareOperators.Between; 28 | 29 | return ( 30 | 31 | 32 | 40 | 41 | {isBetween && ( 42 | 43 | 51 | 52 | )} 53 | 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /src/Filtering/SearchTextInput.tsx: -------------------------------------------------------------------------------- 1 | import FormControl from '@material-ui/core/FormControl'; 2 | import IconButton from '@material-ui/core/IconButton'; 3 | import Input from '@material-ui/core/Input'; 4 | import InputAdornment from '@material-ui/core/InputAdornment'; 5 | import Close from '@material-ui/icons/Close'; 6 | import Search from '@material-ui/icons/Search'; 7 | 8 | import * as React from 'react'; 9 | 10 | const styles = { 11 | formControl: { 12 | margin: '10px', 13 | width: 250, 14 | }, 15 | }; 16 | 17 | export interface SearchTextInputProps { 18 | searchText: string; 19 | updateSearchText: (value: string) => void; 20 | } 21 | 22 | export const SearchTextInput: React.FunctionComponent = ({ 23 | searchText, 24 | updateSearchText, 25 | }: SearchTextInputProps) => { 26 | const onChange = (e: any) => updateSearchText(e.target.value); 27 | const onClear = () => updateSearchText(''); 28 | 29 | const adorment = ( 30 | 31 | 32 | 33 | ); 34 | 35 | return ( 36 | 37 | 46 | 47 | 48 | 49 | 50 | ) 51 | } 52 | /> 53 | 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /src/Filtering/StandardFilterEditor.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ColumnModel, getOperators, ColumnDataType, CompareOperators } from 'tubular-common'; 3 | import { getOperatorIcon, getOperatorText, FilterEditorProps } from './utils'; 4 | import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; 5 | 6 | import Grid from '@material-ui/core/Grid'; 7 | import Button from '@material-ui/core/Button'; 8 | import MenuItem from '@material-ui/core/MenuItem'; 9 | import Menu from '@material-ui/core/Menu'; 10 | import { NumericFilter } from './NumericFilter'; 11 | import { StringFilter } from './StringFilter'; 12 | import { DateFilter } from './DateFilter'; 13 | 14 | const getFilterControl = (column: ColumnModel, onApply: () => void) => { 15 | switch (column.dataType) { 16 | case ColumnDataType.Numeric: 17 | return ; 18 | 19 | case ColumnDataType.String: 20 | return ; 21 | case ColumnDataType.Date: 22 | case ColumnDataType.DateTime: 23 | case ColumnDataType.DateTimeUtc: 24 | return ; 25 | 26 | default: 27 | return null; 28 | } 29 | }; 30 | 31 | export const StandardFilterEditor: React.FunctionComponent = ({ 32 | column, 33 | onApply, 34 | }: FilterEditorProps) => { 35 | const [currentOperator, setCurrentOperator] = React.useState(column.filterOperator); 36 | const [anchorEl, setAnchorEl] = React.useState(null); 37 | 38 | const options = getOperators(column).map((row: any) => ({ 39 | key: row.value, 40 | icon: getOperatorIcon(column.filterOperator), 41 | text: getOperatorText(row.value, row.title), 42 | })); 43 | 44 | const handleClose = () => { 45 | setAnchorEl(null); 46 | }; 47 | const handleMenuClick = (event: React.MouseEvent) => { 48 | setAnchorEl(event.currentTarget); 49 | }; 50 | 51 | const handleOperatorClick = (operator: CompareOperators) => { 52 | setCurrentOperator(operator); 53 | column.filterOperator = operator; 54 | handleClose(); 55 | }; 56 | 57 | return ( 58 | <> 59 | 60 | 61 | 74 | 75 | 76 | {getFilterControl(column, onApply)} 77 | 78 | 79 | 80 | {options.map((option) => ( 81 | handleOperatorClick(option.key)} 85 | > 86 | {getOperatorIcon(option.key)} 87 | {option.text} 88 | 89 | ))} 90 | 91 | 92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /src/Filtering/StringFilter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ColumnModel } from 'tubular-common'; 3 | import TextField from '@material-ui/core/TextField'; 4 | import { handleFilterChange, onKeyDown } from './utils'; 5 | 6 | export interface StringFilterProps { 7 | column: ColumnModel; 8 | onApply: () => void; 9 | } 10 | 11 | export const StringFilter: React.FunctionComponent = ({ column, onApply }: StringFilterProps) => { 12 | return ( 13 | <> 14 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/Filtering/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SearchTextInput'; 2 | -------------------------------------------------------------------------------- /src/Filtering/utils.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ColumnModel, CompareOperators } from 'tubular-common'; 3 | import { TbNotContainsIcon } from '../SvgIcons/TbNotContainsIcon'; 4 | import { TbContainsIcon } from '../SvgIcons/TbContainsIcon'; 5 | import { TbStartsWithIcon } from '../SvgIcons/TbStartsWithIcon'; 6 | import { TbNotStartsWithIcon } from '../SvgIcons/TbNotStartsWithIcon'; 7 | import { TbEndsWithIcon } from '../SvgIcons/TbEndsWithIcon'; 8 | import { TbNotEndsWithIcon } from '../SvgIcons/TbNotEndsWithIcon'; 9 | import { TbEqualsIcon } from '../SvgIcons/TbEqualsIcon'; 10 | import { TbGreaterThanIcon } from '../SvgIcons/TbGreaterThanIcon'; 11 | import { TbNotEqualsIcon } from '../SvgIcons/TbNotEqualsIcon'; 12 | import { TbGreaterOrEqualsToIcon } from '../SvgIcons/TbGreaterOrEqualsToIcon'; 13 | import { TbLessThanIcon } from '../SvgIcons/TbLessThanIcon'; 14 | import { TbLessOrEqualsToIcon } from '../SvgIcons/TbLessOrEqualsToIcon'; 15 | import { TbBetweenIcon } from '../SvgIcons/TbBetweenIcon'; 16 | import FilterListIcon from '@material-ui/icons/FilterList'; 17 | 18 | export const handleFilterChange = (column: ColumnModel) => ( 19 | event: React.ChangeEvent, 20 | ) => { 21 | column.filterText = event.target.value; 22 | }; 23 | 24 | export const onKeyDown = (onEnter: () => void) => (ev: React.KeyboardEvent) => { 25 | if (ev.keyCode === 13 && onEnter) { 26 | ev.preventDefault(); 27 | ev.stopPropagation(); 28 | onEnter(); 29 | } 30 | }; 31 | 32 | export interface FilterEditorProps { 33 | column: ColumnModel; 34 | onApply: () => void; 35 | } 36 | 37 | export const getOperatorText = (value: CompareOperators, title: string) => { 38 | switch (value) { 39 | case CompareOperators.NotContains: 40 | case CompareOperators.Contains: 41 | case CompareOperators.StartsWith: 42 | case CompareOperators.NotStartsWith: 43 | case CompareOperators.EndsWith: 44 | case CompareOperators.NotEndsWith: 45 | case CompareOperators.Equals: 46 | case CompareOperators.NotEquals: 47 | case CompareOperators.Between: 48 | return title; 49 | case CompareOperators.Gt: 50 | return 'Greater than'; 51 | case CompareOperators.Gte: 52 | return 'Greater than or equals to'; 53 | case CompareOperators.Lt: 54 | return 'Less than'; 55 | case CompareOperators.Lte: 56 | return 'Less than or equals to'; 57 | default: 58 | return 'None'; 59 | } 60 | }; 61 | 62 | export const getOperatorIcon = (operator: CompareOperators): JSX.Element => { 63 | switch (operator) { 64 | case CompareOperators.NotContains: 65 | return ; 66 | case CompareOperators.Contains: 67 | return ; 68 | case CompareOperators.StartsWith: 69 | return ; 70 | case CompareOperators.NotStartsWith: 71 | return ; 72 | case CompareOperators.EndsWith: 73 | return ; 74 | case CompareOperators.NotEndsWith: 75 | return ; 76 | case CompareOperators.Equals: 77 | return ; 78 | case CompareOperators.NotEquals: 79 | return ; 80 | case CompareOperators.Gt: 81 | return ; 82 | case CompareOperators.Gte: 83 | return ; 84 | case CompareOperators.Lt: 85 | return ; 86 | case CompareOperators.Lte: 87 | return ; 88 | case CompareOperators.Between: 89 | return ; 90 | default: 91 | return ; 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /src/Pagination/AdvancePaginationActions.tsx: -------------------------------------------------------------------------------- 1 | import IconButton from '@material-ui/core/IconButton'; 2 | import FirstPage from '@material-ui/icons/FirstPage'; 3 | import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft'; 4 | import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight'; 5 | import LastPage from '@material-ui/icons/LastPage'; 6 | import makeStyles from '@material-ui/styles/makeStyles'; 7 | import * as React from 'react'; 8 | import { getPages } from 'tubular-common'; 9 | import Lang from '../utils/Lang'; 10 | 11 | const useStyles = makeStyles({ 12 | root: { 13 | flexShrink: 0, 14 | }, 15 | }); 16 | 17 | export interface AdvancePaginationActionsProps { 18 | count: number; 19 | isAdvanced: boolean; 20 | isLoading: boolean; 21 | page: number; 22 | rowsPerPage: number; 23 | onChangePage(event: React.MouseEvent, page: number): void; 24 | } 25 | 26 | export const AdvancePaginationActions: React.FunctionComponent = ({ 27 | count, 28 | isAdvanced, 29 | isLoading, 30 | page, 31 | rowsPerPage, 32 | onChangePage, 33 | }: AdvancePaginationActionsProps) => { 34 | const classes = useStyles({}); 35 | const pages = getPages(page, count, rowsPerPage); 36 | const lastPage = Math.ceil(count / rowsPerPage) - 1; 37 | const gotoPage = (value: number) => (e: any) => onChangePage(e, value); 38 | 39 | const gotoFirstPage = gotoPage(0); 40 | const gotoPrevPage = gotoPage(page - 1); 41 | const gotoNextPage = gotoPage(page + 1); 42 | const gotoLastPage = gotoPage(Math.max(0, lastPage)); 43 | 44 | const canNotBack = page === 0 || isLoading; 45 | const canNotFwd = page >= lastPage || isLoading; 46 | 47 | return ( 48 |
49 | {isAdvanced && ( 50 | 51 | 52 | 53 | )} 54 | 55 | 56 | 57 | 58 | {isAdvanced && 59 | pages.map((value) => ( 60 | = Math.ceil(count / rowsPerPage) || isLoading} 64 | aria-label={Lang.translate('PageNum', value + 1)} 65 | color={value === page ? 'primary' : 'default'} 66 | > 67 | {value + 1} 68 | 69 | ))} 70 | 71 | 72 | 73 | 74 | {isAdvanced && ( 75 | 76 | 77 | 78 | )} 79 |
80 | ); 81 | }; 82 | -------------------------------------------------------------------------------- /src/Pagination/Paginator.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@material-ui/core/styles'; 2 | import TablePagination from '@material-ui/core/TablePagination'; 3 | import * as React from 'react'; 4 | import { ITbTableInstance } from 'tubular-react-common'; 5 | import { useResolutionSwitch } from 'uno-react'; 6 | import { AdvancePaginationActions } from './AdvancePaginationActions'; 7 | import Lang from '../utils/Lang'; 8 | 9 | const useStyles = makeStyles({ 10 | caption: { 11 | flexShrink: 1, 12 | height: '55px', 13 | }, 14 | root: { 15 | height: '75px', 16 | maxWidth: '95%', 17 | }, 18 | }); 19 | 20 | const outerWidth = 800; 21 | const timeout = 400; 22 | 23 | const message = (totalRecordCount: number, filteredRecordCount: number) => ({ from, to, count }: any) => 24 | totalRecordCount === filteredRecordCount 25 | ? Lang.translate('Pages', from, to, count) 26 | : filteredRecordCount === 0 27 | ? Lang.translate('NoRecords') 28 | : Lang.translate('TotalRecords', from, to, count, totalRecordCount); 29 | 30 | export interface PaginatorProps { 31 | tbTableInstance: ITbTableInstance; 32 | rowsPerPageOptions: number[]; 33 | advancePagination: boolean; 34 | } 35 | 36 | export const Paginator: React.FunctionComponent = ({ 37 | tbTableInstance, 38 | rowsPerPageOptions, 39 | advancePagination, 40 | }: PaginatorProps) => { 41 | const [isMobileResolution] = useResolutionSwitch(outerWidth, timeout); 42 | const classes = useStyles({}); 43 | const { state, api } = tbTableInstance; 44 | 45 | if (!state.itemsPerPage) { 46 | return null; 47 | } 48 | 49 | const newProps = { 50 | count: state.filteredRecordCount, 51 | labelDisplayedRows: message(state.totalRecordCount, state.filteredRecordCount), 52 | onChangePage: (_e: any, page: number) => api.goToPage(page), 53 | onChangeRowsPerPage: (e: any) => api.updateItemsPerPage(Number(e.target.value)), 54 | page: state.filteredRecordCount > 0 ? state.page : 0, 55 | rowsPerPage: state.itemsPerPage, 56 | rowsPerPageOptions: rowsPerPageOptions || [10, 20, 50], 57 | } as any; 58 | 59 | // eslint-disable-next-line react/display-name 60 | newProps.ActionsComponent = () => ( 61 | 69 | ); 70 | 71 | return ( 72 | 79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /src/Pagination/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Paginator'; 2 | -------------------------------------------------------------------------------- /src/SvgIcons/TbBetweenIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import SvgIcon from '@material-ui/core/SvgIcon'; 3 | 4 | export const TbBetweenIcon = () => { 5 | return ( 6 | 7 | 8 | 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/SvgIcons/TbContainsIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import SvgIcon from '@material-ui/core/SvgIcon'; 3 | 4 | export const TbContainsIcon = () => { 5 | return ( 6 | 7 | 8 | 9 | 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/SvgIcons/TbEndsWithIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import SvgIcon from '@material-ui/core/SvgIcon'; 3 | 4 | export const TbEndsWithIcon = () => { 5 | return ( 6 | 7 | 8 | 9 | 13 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/SvgIcons/TbEqualsIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import SvgIcon from '@material-ui/core/SvgIcon'; 3 | 4 | export const TbEqualsIcon = () => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/SvgIcons/TbGreaterOrEqualsToIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import SvgIcon from '@material-ui/core/SvgIcon'; 3 | 4 | export const TbGreaterOrEqualsToIcon = () => { 5 | return ( 6 | 7 | 8 | 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/SvgIcons/TbGreaterThanIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import SvgIcon from '@material-ui/core/SvgIcon'; 3 | 4 | export const TbGreaterThanIcon = () => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/SvgIcons/TbLessOrEqualsToIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import SvgIcon from '@material-ui/core/SvgIcon'; 3 | 4 | export const TbLessOrEqualsToIcon = () => { 5 | return ( 6 | 7 | 8 | 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/SvgIcons/TbLessThanIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import SvgIcon from '@material-ui/core/SvgIcon'; 3 | 4 | export const TbLessThanIcon = () => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/SvgIcons/TbNotContainsIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import SvgIcon from '@material-ui/core/SvgIcon'; 3 | 4 | export const TbNotContainsIcon = () => { 5 | return ( 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/SvgIcons/TbNotEndsWithIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import SvgIcon from '@material-ui/core/SvgIcon'; 3 | 4 | export const TbNotEndsWithIcon = () => { 5 | return ( 6 | 7 | 8 | 9 | 13 | 17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/SvgIcons/TbNotEqualsIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import SvgIcon from '@material-ui/core/SvgIcon'; 3 | 4 | export const TbNotEqualsIcon = () => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/SvgIcons/TbNotStartsWithIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import SvgIcon from '@material-ui/core/SvgIcon'; 3 | 4 | export const TbNotStartsWithIcon = () => { 5 | return ( 6 | 7 | 8 | 12 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/SvgIcons/TbStartsWithIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import SvgIcon from '@material-ui/core/SvgIcon'; 3 | 4 | export const TbStartsWithIcon = () => { 5 | return ( 6 | 7 | 8 | 12 | 16 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/TbList/TbList.tsx: -------------------------------------------------------------------------------- 1 | import ListItem from '@material-ui/core/ListItem'; 2 | import ListItemText from '@material-ui/core/ListItemText'; 3 | import * as React from 'react'; 4 | import { AutoSizer, CellMeasurer, CellMeasurerCache, InfiniteLoader, List, Index, IndexRange } from 'react-virtualized'; 5 | import { ITbListInstance, generateOnRowClickProxy } from 'tubular-react-common'; 6 | import { TbListItem } from './TbListItem'; 7 | import Lang from '../utils/Lang'; 8 | 9 | export interface TbListProps { 10 | tbInstance: ITbListInstance; 11 | listItemComponent?: React.FunctionComponent; 12 | onItemClick?(row: any): void; 13 | } 14 | 15 | export const TbList: React.FunctionComponent = (tbProps) => { 16 | const { tbInstance, onItemClick, listItemComponent } = tbProps; 17 | 18 | const { items, hasNextPage } = tbInstance.state.list; 19 | 20 | const loadNextPage = (args: IndexRange) => { 21 | const pageToLoad = Math.ceil(args.stopIndex / (tbInstance.state.itemsPerPage - 1)) - 1; 22 | if (tbInstance.state.isLoading || pageToLoad <= tbInstance.state.page) { 23 | return; 24 | } 25 | 26 | tbInstance.api.loadPage(pageToLoad); 27 | 28 | // We're resolving immediately because tubular will take care of 29 | // updating the values once the request is complete. 30 | return Promise.resolve(); 31 | }; 32 | 33 | // This cache is enabling better performance when it comes to reload 34 | // previously loaded items. 35 | const cache = new CellMeasurerCache({ defaultHeight: 85, fixedWidth: true }); 36 | const noRecordsFound = !hasNextPage && !tbInstance.state.isLoading && items.length === 0; 37 | 38 | // We need a place holder to give user some feedback on what's happening 39 | const itemCount = tbInstance.state.isLoading || noRecordsFound || hasNextPage ? items.length + 1 : items.length; 40 | 41 | const loadMoreItems = loadNextPage; 42 | 43 | // Every row is loaded except for our Loading/NoRecordsFound indicator. 44 | const isItemLoaded = (index: Index) => !hasNextPage || index.index < items.length; 45 | 46 | const ListItemComponent = listItemComponent ? listItemComponent : TbListItem; 47 | 48 | const rowRenderer = (props: any) => { 49 | const { index, key, style } = props; 50 | const row = items[index]; 51 | const itemClickProxy = generateOnRowClickProxy(onItemClick)(row); 52 | const isPlaceholder = !isItemLoaded({ index }) || !items[index]; 53 | const itemToRender = ( 54 | 60 | ); 61 | 62 | const placeholderItem = (placeholderStyle: any) => { 63 | const placeholderMessage = noRecordsFound ? Lang.translate('NoRecords') : Lang.translate('Loading'); 64 | return ( 65 | 66 | 67 | 68 | ); 69 | }; 70 | 71 | const content = isPlaceholder ? placeholderItem(style) : itemToRender; 72 | 73 | return ( 74 | 75 | {content} 76 | 77 | ); 78 | }; 79 | 80 | return ( 81 | 88 | {({ onRowsRendered }: any) => ( 89 | 90 | {({ width, height }: any) => { 91 | return ( 92 | 102 | ); 103 | }} 104 | 105 | )} 106 | 107 | ); 108 | }; 109 | -------------------------------------------------------------------------------- /src/TbList/TbListItem.tsx: -------------------------------------------------------------------------------- 1 | import ListItem from '@material-ui/core/ListItem'; 2 | import * as React from 'react'; 3 | import { renderDefaultListItem } from '../utils/renders'; 4 | import { ColumnModel } from 'tubular-common'; 5 | 6 | export interface TbListItemProps { 7 | selectedIndex: number; 8 | onItemClick(row: any): void; 9 | row: any; 10 | rowStyle: any; 11 | columns: ColumnModel[]; 12 | } 13 | 14 | export const TbListItem: React.FunctionComponent = ({ 15 | selectedIndex, 16 | onItemClick, 17 | row, 18 | rowStyle, 19 | columns, 20 | }: TbListItemProps) => { 21 | return ( 22 | 28 | {renderDefaultListItem(columns, row)} 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/TbList/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TbList'; 2 | export * from './TbListItem'; 3 | -------------------------------------------------------------------------------- /src/Toolbar/ExportButton.tsx: -------------------------------------------------------------------------------- 1 | import IconButton from '@material-ui/core/IconButton'; 2 | import Menu from '@material-ui/core/Menu'; 3 | import MenuItem from '@material-ui/core/MenuItem'; 4 | import Tooltip from '@material-ui/core/Tooltip'; 5 | import CloudDownload from '@material-ui/icons/CloudDownload'; 6 | import Print from '@material-ui/icons/Print'; 7 | import * as React from 'react'; 8 | import { ColumnModel } from 'tubular-common'; 9 | import { exportGrid } from 'tubular-react-common'; 10 | import Lang from '../utils/Lang'; 11 | 12 | export interface ExportButtonProps { 13 | type: string; 14 | gridName: string; 15 | filteredRecordCount: number; 16 | toolTip?: string; 17 | exportTo: (allRows: boolean, exportFunc: (payload: any[], columns: ColumnModel[]) => void) => void; 18 | } 19 | 20 | export const ExportButton: React.FunctionComponent = ({ 21 | type, 22 | gridName, 23 | toolTip, 24 | exportTo, 25 | filteredRecordCount, 26 | }: ExportButtonProps) => { 27 | const [anchorPrint, setAnchorPrint] = React.useState(null); 28 | 29 | const handlePrintMenu = (event: React.MouseEvent): void => 30 | setAnchorPrint(event ? event.currentTarget : null); 31 | 32 | const closePrint = () => setAnchorPrint(null); 33 | 34 | const partialExport = (data: [], columns: ColumnModel[]) => { 35 | exportGrid(type, data, columns, gridName); 36 | closePrint(); 37 | }; 38 | 39 | const printCurrent = () => exportTo(false, partialExport); 40 | const printAll = () => exportTo(true, partialExport); 41 | 42 | return ( 43 | 44 | 45 | {type === 'print' ? ( 46 | 47 | 48 | 49 | ) : ( 50 | 51 | 52 | 53 | )} 54 | 55 | 56 | {Lang.translate('CurrentRows')} 57 | {Lang.translate('AllRows')} 58 | 59 | 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /src/Toolbar/GridToolbar.tsx: -------------------------------------------------------------------------------- 1 | import Toolbar from '@material-ui/core/Toolbar'; 2 | import * as React from 'react'; 3 | import { ITbTableInstance } from 'tubular-react-common'; 4 | import { useResolutionSwitch, useToggle } from 'uno-react'; 5 | import { SearchTextInput } from '../Filtering/SearchTextInput'; 6 | import { ExportButton } from './ExportButton'; 7 | import { ToolbarOptions } from './ToolbarOptions'; 8 | import IconButton from '@material-ui/core/IconButton'; 9 | import Tooltip from '@material-ui/core/Tooltip'; 10 | import TuneIcon from '@material-ui/icons/Tune'; 11 | import { FeaturesDrawer } from '../DataGrid/FeaturesDrawer'; 12 | import { ColumnModel, CompareOperators, columnHasFilter } from 'tubular-common'; 13 | 14 | const mobileSpacer: React.CSSProperties = { flexShrink: 1 }; 15 | const spacer: React.CSSProperties = { flex: '1 0' }; 16 | 17 | const outerWidth = 800; 18 | const timeout = 400; 19 | 20 | export interface GridToolbarProps { 21 | toolbarOptions: ToolbarOptions; 22 | gridName: string; 23 | tbTableInstance: ITbTableInstance; 24 | } 25 | 26 | export const GridToolbar: React.FunctionComponent = ({ 27 | toolbarOptions, 28 | gridName, 29 | tbTableInstance, 30 | }: GridToolbarProps) => { 31 | const [isMobileResolution] = useResolutionSwitch(outerWidth, timeout); 32 | 33 | const applyFilters = (columns: ColumnModel[]): ColumnModel[] => { 34 | columns.forEach((fColumn) => { 35 | const column = columns.find((c: ColumnModel) => c.name === fColumn.name); 36 | 37 | if (columnHasFilter(fColumn)) { 38 | column.filterText = fColumn.filterText; 39 | column.filterOperator = fColumn.filterOperator; 40 | column.filterArgument = fColumn.filterArgument; 41 | 42 | if ( 43 | column.filterOperator === CompareOperators.Between && 44 | (!column.filterArgument || !column.filterArgument[0]) 45 | ) { 46 | column.filterOperator = CompareOperators.Gte; 47 | column.filterArgument = null; 48 | } 49 | } else { 50 | column.filterText = null; 51 | column.filterOperator = CompareOperators.None; 52 | column.filterArgument = null; 53 | } 54 | }); 55 | 56 | return columns; 57 | }; 58 | 59 | const onApplyFeatures = (columns: ColumnModel[]) => { 60 | const newColumns = applyFilters(columns); 61 | tbTableInstance.api.setColumns(newColumns); 62 | }; 63 | 64 | const [isPanelOpen, togglePanel] = useToggle(false); 65 | const enableFeaturesDrawer = tbTableInstance.state.columns.find((c) => c.filterable); 66 | 67 | return ( 68 | <> 69 | 70 | {toolbarOptions.title &&

{toolbarOptions.title}

} 71 |
72 | {toolbarOptions.customItems} 73 | {toolbarOptions.exportButton && ( 74 | 81 | )} 82 | {toolbarOptions.printButton && ( 83 | 90 | )} 91 | {toolbarOptions.searchText && ( 92 | 97 | )} 98 | 99 | {enableFeaturesDrawer && ( 100 | 101 | 102 | 103 | 104 | 105 | )} 106 | 107 | {enableFeaturesDrawer && isPanelOpen && ( 108 | 113 | )} 114 | 115 | ); 116 | }; 117 | -------------------------------------------------------------------------------- /src/Toolbar/SelectionToolbar.tsx: -------------------------------------------------------------------------------- 1 | import Toolbar from '@material-ui/core/Toolbar'; 2 | import * as React from 'react'; 3 | import clsx from 'clsx'; 4 | import Typography from '@material-ui/core/Typography'; 5 | import { TbSelection } from '../utils/Selection'; 6 | import { createStyles, makeStyles, lighten, Theme } from '@material-ui/core/styles'; 7 | 8 | export interface SelectionToolbarProps { 9 | selection: TbSelection; 10 | actionsArea?: React.ComponentType; 11 | } 12 | 13 | const useToolbarStyles = makeStyles((theme: Theme) => 14 | createStyles({ 15 | root: { 16 | paddingLeft: theme.spacing(2), 17 | paddingRight: theme.spacing(1), 18 | }, 19 | highlight: 20 | theme.palette.type === 'light' 21 | ? { 22 | color: theme.palette.secondary.main, 23 | backgroundColor: lighten(theme.palette.secondary.light, 0.85), 24 | } 25 | : { 26 | color: theme.palette.text.primary, 27 | backgroundColor: theme.palette.secondary.dark, 28 | }, 29 | title: { 30 | flex: '1 1 100%', 31 | }, 32 | }), 33 | ); 34 | 35 | const spacer: React.CSSProperties = { flex: '1 0' }; 36 | 37 | export const SelectionToolbar: React.FunctionComponent = ({ 38 | selection, 39 | actionsArea, 40 | }: SelectionToolbarProps) => { 41 | const classes = useToolbarStyles(); 42 | const ActionsArea = actionsArea; 43 | 44 | return ( 45 | 0, 49 | })} 50 | > 51 | 52 | {selection.getSelectedCount()} selected 53 | 54 |
55 | {ActionsArea && } 56 | 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /src/Toolbar/ToolbarOptions.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | export class ToolbarOptions { 4 | public advancePagination = true; 5 | public enablePagination = true; 6 | public customItems: ReactNode; 7 | public actionsArea: React.ComponentType; 8 | public exportButton = true; 9 | public printButton = true; 10 | public searchText = true; 11 | public rowsPerPageOptions: number[] = [10, 20, 50, 100]; 12 | public itemsPerPage = 10; 13 | public title = ''; 14 | 15 | constructor(options?: Partial) { 16 | Object.assign(this, options); 17 | } 18 | 19 | public SetMobileMode() { 20 | this.advancePagination = false; 21 | this.enablePagination = false; 22 | this.exportButton = false; 23 | this.printButton = false; 24 | this.rowsPerPageOptions = [5, 10]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Toolbar/index.ts: -------------------------------------------------------------------------------- 1 | import { GridToolbar } from './GridToolbar'; 2 | import { ToolbarOptions } from './ToolbarOptions'; 3 | 4 | export { GridToolbar, ToolbarOptions }; 5 | -------------------------------------------------------------------------------- /src/hooks/useTbSelection.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ITbTableInstance } from 'tubular-react-common'; 3 | import { TbSelection } from '../utils/Selection'; 4 | import { ColumnModel } from 'tubular-common'; 5 | 6 | const createRowSelectionFromData = (data: any[], columns: ColumnModel[]) => { 7 | const keyColumn = columns.find((c) => c.isKey).name; 8 | const newSelection: any = {}; 9 | data.forEach((row: any) => { 10 | if (newSelection[row[keyColumn]] === undefined) { 11 | newSelection[row[keyColumn]] = false; 12 | } 13 | }); 14 | 15 | return newSelection; 16 | }; 17 | 18 | export const useTbSelection = (tbInstance: ITbTableInstance, rowSelectionEnabled: boolean): TbSelection => { 19 | const [rowSelection, setRowSelection] = React.useState({} as any); 20 | const keyColumn = tbInstance.state.columns.find((c) => c.isKey); 21 | const toggleRowSelection = (id: string) => setRowSelection({ ...rowSelection, [id]: !rowSelection[id] }); 22 | const getSelectedCount = () => Object.keys(rowSelection).filter((k) => rowSelection[k]).length; 23 | const getUnSelectedCount = () => Object.keys(rowSelection).filter((k) => !rowSelection[k]).length; 24 | const getSelectedRows = () => { 25 | const selectedKeys = Object.keys(rowSelection).filter((k) => rowSelection[k]); 26 | return tbInstance.state.data.filter((row) => selectedKeys.includes(`${row[keyColumn.name]}`)); 27 | }; 28 | 29 | const isIndeterminateSelection = () => 30 | Object.keys(rowSelection).length > 0 && getSelectedCount() > 0 && getUnSelectedCount() > 0; 31 | 32 | const toggleAllRowsSelection = () => { 33 | const newRowSelection = createRowSelectionFromData(tbInstance.state.data, tbInstance.state.columns); 34 | const unSelectedCount = Object.keys(rowSelection).filter((k) => !rowSelection[k]).length; 35 | 36 | // all rows are selected 37 | if (unSelectedCount === 0) { 38 | Object.keys(rowSelection).forEach((f) => (newRowSelection[f] = false)); 39 | setRowSelection(newRowSelection); 40 | return; 41 | } 42 | 43 | // Indeterminate | non-selected 44 | Object.keys(rowSelection).forEach((f) => (newRowSelection[f] = true)); 45 | setRowSelection(newRowSelection); 46 | }; 47 | 48 | React.useEffect(() => { 49 | if (rowSelectionEnabled) { 50 | const newSelection = createRowSelectionFromData(tbInstance.state.data, tbInstance.state.columns); 51 | setRowSelection(newSelection); 52 | } 53 | }, [tbInstance.state.data]); 54 | 55 | return { 56 | rowSelection, 57 | toggleRowSelection, 58 | toggleAllRowsSelection, 59 | getSelectedCount, 60 | getSelectedRows, 61 | getUnSelectedCount, 62 | isIndeterminateSelection, 63 | }; 64 | }; 65 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DataGrid'; 2 | export * from './utils'; 3 | export * from './Pagination'; 4 | export * from './Filtering'; 5 | export * from './Toolbar'; 6 | export * from './BareBones'; 7 | export * from './TbList'; 8 | -------------------------------------------------------------------------------- /src/utils/Lang.ts: -------------------------------------------------------------------------------- 1 | import TubularLangDef from './languages/TubularLangDef'; 2 | import English from './languages/English'; 3 | 4 | const formatTranslation = (val: string, args: any[]) => 5 | val.replace(new RegExp(`{([0-${args.length - 1}])}`, 'gi'), (_, index) => args[index]); 6 | 7 | export class LangBase { 8 | private data: any = {}; 9 | private currentLanguage: string; 10 | 11 | constructor(langKey: string, initialLanguage: T) { 12 | this.addLanguage(langKey, initialLanguage); 13 | this.changeLanguage(langKey); 14 | } 15 | 16 | public translate(key: string, ...args: any[]): string { 17 | const text = this.data[this.currentLanguage][key]; 18 | 19 | if (!text) return key; 20 | 21 | return args.length > 0 ? formatTranslation(text, args) : text; 22 | } 23 | 24 | public changeLanguage(langKey: string) { 25 | this.currentLanguage = langKey; 26 | } 27 | 28 | public addLanguage(langKey: string, data: T) { 29 | this.data[langKey] = data; 30 | } 31 | } 32 | 33 | export class LangDefault extends LangBase { 34 | constructor() { 35 | super('en', English); 36 | } 37 | } 38 | 39 | const Lang = new LangDefault(); 40 | 41 | export default Lang; 42 | -------------------------------------------------------------------------------- /src/utils/Selection.ts: -------------------------------------------------------------------------------- 1 | export interface TbSelection { 2 | rowSelection: any; 3 | toggleRowSelection: (id: string) => void; 4 | toggleAllRowsSelection: () => void; 5 | getSelectedCount: () => number; 6 | getUnSelectedCount: () => number; 7 | isIndeterminateSelection: () => boolean; 8 | getSelectedRows: () => any[]; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './renders'; 2 | export * from './Lang'; 3 | export * from './languages/TubularLangDef'; 4 | -------------------------------------------------------------------------------- /src/utils/languages/English.ts: -------------------------------------------------------------------------------- 1 | import TubularLangDef from './TubularLangDef'; 2 | 3 | const English: TubularLangDef = { 4 | ClickSort: 'Click to sort. Press Ctrl to sort by multiple columns', 5 | NoRecords: 'No records found', 6 | Value: 'Value', 7 | FirstValue: 'First Value', 8 | SecondValue: 'Second Value', 9 | None: 'None', 10 | FirstPage: 'First Page', 11 | PrevPage: 'Previous Page', 12 | PageNum: 'Page {0}', 13 | NextPage: 'Next Page', 14 | LastPage: 'Last Page', 15 | Pages: '{0} - {1} of {2}', 16 | TotalRecords: '{0} to {1} of {2} from {3} records', 17 | Loading: 'Loading...', 18 | Download: 'Download', 19 | Print: 'Print', 20 | CurrentRows: 'Current rows', 21 | AllRows: 'All rows', 22 | Operator: 'Operator', 23 | }; 24 | 25 | export default English; 26 | -------------------------------------------------------------------------------- /src/utils/languages/TubularLangDef.ts: -------------------------------------------------------------------------------- 1 | export default interface TubularLangDef { 2 | ClickSort: string; 3 | NoRecords: string; 4 | Value: string; 5 | FirstValue: string; 6 | SecondValue: string; 7 | None: string; 8 | FirstPage: string; 9 | PrevPage: string; 10 | PageNum: string; 11 | NextPage: string; 12 | LastPage: string; 13 | Pages: string; 14 | TotalRecords: string; 15 | Loading: string; 16 | Download: string; 17 | Print: string; 18 | CurrentRows: string; 19 | AllRows: string; 20 | Operator: string; 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/renders.tsx: -------------------------------------------------------------------------------- 1 | import TableCell from '@material-ui/core/TableCell'; 2 | import CheckBox from '@material-ui/icons/CheckBox'; 3 | import CheckBoxOutlineBlank from '@material-ui/icons/CheckBoxOutlineBlank'; 4 | import * as React from 'react'; 5 | import { ColumnDataType, ColumnModel, getColumnAlign, parseDateColumnValue } from 'tubular-common'; 6 | 7 | export const formatDate = (date: Date) => 8 | date.toLocaleString('en-us', { 9 | day: '2-digit', 10 | month: 'long', 11 | year: 'numeric', 12 | }); 13 | 14 | export const formatDateTime = (date: Date) => 15 | date.toLocaleString('en-us', { 16 | day: '2-digit', 17 | month: 'long', 18 | year: 'numeric', 19 | hour: '2-digit', 20 | minute: '2-digit', 21 | }); 22 | 23 | export const renderCellContent: any = (column: ColumnModel, row: any) => { 24 | const value = row[column.name]; 25 | switch (column.dataType) { 26 | case ColumnDataType.Numeric: 27 | return value || 0; 28 | case ColumnDataType.Date: 29 | case ColumnDataType.DateTime: 30 | case ColumnDataType.DateTimeUtc: 31 | const dateAsString = !value ? '' : parseDateColumnValue(column, value); 32 | return dateAsString; 33 | case ColumnDataType.Boolean: 34 | return value === true ? : ; 35 | default: 36 | return value; 37 | } 38 | }; 39 | 40 | export const renderDefaultListItem: any = (columns: ColumnModel[], row: any) => 41 | columns 42 | .filter((col: ColumnModel) => col.visible) 43 | .map((column: ColumnModel) =>
{renderCellContent(column, row)}
); 44 | 45 | export const renderCells: any = (columns: ColumnModel[], row: any) => 46 | columns 47 | .filter((col: ColumnModel) => col.visible) 48 | .map((column: ColumnModel) => ( 49 | 54 | {renderCellContent(column, row)} 55 | 56 | )); 57 | -------------------------------------------------------------------------------- /test/DataGrid.spec.tsx: -------------------------------------------------------------------------------- 1 | describe('DataGrid Component', () => { 2 | test('render', () => { 3 | expect(true).toBe(true); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /test/LanguageService.spec.ts: -------------------------------------------------------------------------------- 1 | import Lang from '../src/utils/Lang'; 2 | import TubularLangDef from '../src/utils/languages/TubularLangDef'; 3 | 4 | const BadEnglish: TubularLangDef = { 5 | ClickSort: 'Click to sort. Press Ctrl to sort by multiple columns', 6 | NoRecords: 'No recordz found', 7 | Value: 'Value', 8 | FirstValue: 'First Value', 9 | SecondValue: 'Second Value', 10 | None: 'None', 11 | FirstPage: 'First Page', 12 | PrevPage: 'Previous Page', 13 | PageNum: 'Page {0}', 14 | NextPage: 'Next Page', 15 | LastPage: 'Last Page', 16 | Pages: '{0} - {1} of {2}', 17 | TotalRecords: '{0} to {1} of {2} from {3} records', 18 | Loading: 'Loading...', 19 | Download: 'Download', 20 | Print: 'Print', 21 | CurrentRows: 'Current rows', 22 | AllRows: 'All rows', 23 | Operator: 'Operator', 24 | }; 25 | 26 | describe('Language service', () => { 27 | test('Translate text', () => { 28 | const translation = Lang.translate('NoRecords'); 29 | expect(translation).toEqual('No records found'); 30 | }); 31 | 32 | test('Invalid key returns the key', () => { 33 | const translation = Lang.translate('SomeKey'); 34 | expect(translation).toEqual('SomeKey'); 35 | }); 36 | 37 | test('Add language', () => { 38 | Lang.addLanguage('bad', BadEnglish); 39 | Lang.changeLanguage('bad'); 40 | 41 | const translation = Lang.translate('NoRecords'); 42 | expect(translation).toEqual('No recordz found'); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/utils/columns.ts: -------------------------------------------------------------------------------- 1 | import { AggregateFunctions, ColumnDataType, ColumnSortDirection, createColumn } from 'tubular-common'; 2 | 3 | const regularOrderIdCol = createColumn('OrderID', { 4 | dataType: ColumnDataType.Numeric, 5 | filterable: true, 6 | isKey: true, 7 | sortDirection: ColumnSortDirection.Ascending, 8 | sortOrder: 1, 9 | sortable: true, 10 | }); 11 | 12 | // Column samples 13 | const validColumnsSample = [ 14 | regularOrderIdCol, 15 | createColumn('CustomerName', { 16 | aggregate: AggregateFunctions.Count, 17 | filterable: true, 18 | searchable: true, 19 | }), 20 | createColumn('ShippedDate', { 21 | dataType: ColumnDataType.DateTime, 22 | filterable: true, 23 | }), 24 | createColumn('ShipperCity'), 25 | createColumn('Amount', { dataType: ColumnDataType.Numeric }), 26 | ]; 27 | 28 | export { validColumnsSample }; 29 | -------------------------------------------------------------------------------- /test/utils/localData.ts: -------------------------------------------------------------------------------- 1 | export const expectedLocalData = [ 2 | { 3 | OrderID: 1, 4 | CustomerName: 'Microsoft', 5 | ShippedDate: '2016-03-19T19:00:00', 6 | ShipperCity: 'Guadalajara, JAL, Mexico', 7 | Amount: 300.00, 8 | }, 9 | { 10 | OrderID: 2, 11 | CustomerName: 'Microsoft', 12 | ShippedDate: '2016-11-08T18:00:00', 13 | ShipperCity: 'Los Angeles, CA, USA', 14 | Amount: 9.00, 15 | }, 16 | { 17 | OrderID: 3, 18 | CustomerName: 'Unosquare LLC', 19 | ShippedDate: '2016-11-08T18:00:00', 20 | ShipperCity: 'Guadalajara, JAL, Mexico', 21 | Amount: 92.00, 22 | }, 23 | { 24 | OrderID: 4, 25 | CustomerName: 'Vesta', 26 | ShippedDate: '2016-03-19T19:00:00', 27 | ShipperCity: 'Portland, OR, USA', 28 | Amount: 300.00, 29 | }, 30 | { 31 | OrderID: 5, 32 | CustomerName: 'Super La Playa', 33 | ShippedDate: '2016-04-23T10:00:00', 34 | ShipperCity: 'Leon, GTO, Mexico', 35 | Amount: 174.00, 36 | }, 37 | { 38 | OrderID: 6, 39 | CustomerName: 'OXXO', 40 | ShippedDate: '2016-12-22T08:00:00', 41 | ShipperCity: 'Guadalajara, JAL, Mexico', 42 | Amount: 92.00, 43 | }, 44 | { 45 | OrderID: 7, 46 | CustomerName: 'Super La Playa', 47 | ShippedDate: '2016-03-19T19:00:00', 48 | ShipperCity: 'Portland, OR, USA', 49 | Amount: 300.00, 50 | }, 51 | { 52 | OrderID: 8, 53 | CustomerName: 'Super La Playa', 54 | ShippedDate: '2016-04-23T10:00:00', 55 | ShipperCity: 'Leon, GTO, Mexico', 56 | Amount: 15.00, 57 | }, 58 | { 59 | OrderID: 9, 60 | CustomerName: 'OXXO', 61 | ShippedDate: '2016-12-22T08:00:00', 62 | ShipperCity: 'Guadalajara, JAL, Mexico', 63 | Amount: 92.00, 64 | }, 65 | { 66 | OrderID: 10, 67 | CustomerName: 'Vesta', 68 | ShippedDate: '2016-03-19T19:00:00', 69 | ShipperCity: 'Portland, OR, USA', 70 | Amount: 300.00, 71 | }, 72 | ]; 73 | 74 | export const localData = [ 75 | ...expectedLocalData, 76 | { 77 | OrderID: 11, 78 | CustomerName: 'Microsoft', 79 | ShippedDate: '2016-04-23T10:00:00', 80 | ShipperCity: 'Leon, GTO, Mexico', 81 | Amount: 16.00, 82 | }, 83 | { 84 | OrderID: 12, 85 | CustomerName: 'OXXO', 86 | ShippedDate: '2016-11-08T18:00:00', 87 | ShipperCity: 'Guadalajara, JAL, Mexico', 88 | Amount: 92.00, 89 | }, 90 | { 91 | OrderID: 13, 92 | CustomerName: 'Unosquare LLC', 93 | ShippedDate: '2016-03-19T19:00:00', 94 | ShipperCity: 'Portland, OR, USA', 95 | Amount: 300.00, 96 | }, 97 | { 98 | OrderID: 14, 99 | CustomerName: 'Vesta', 100 | ShippedDate: '2016-04-23T10:00:00', 101 | ShipperCity: 'Guadalajara, JAL, Mexico', 102 | Amount: 60.00, 103 | }, 104 | { 105 | OrderID: 15, 106 | CustomerName: 'Super La Playa', 107 | ShippedDate: '2016-12-22T08:00:00', 108 | ShipperCity: 'Portland, OR, US', 109 | Amount: 192.00, 110 | }, 111 | { 112 | OrderID: 16, 113 | CustomerName: 'Microsoft', 114 | ShippedDate: '2016-03-19T19:00:00', 115 | ShipperCity: 'Leon, GTO, Mexico', 116 | Amount: 300.00, 117 | }, 118 | { 119 | OrderID: 17, 120 | CustomerName: 'Unosquare LLC', 121 | ShippedDate: '2016-04-23T10:00:00', 122 | ShipperCity: 'Leon, GTO, Mexico', 123 | Amount: 108.00, 124 | }, 125 | { 126 | OrderID: 18, 127 | CustomerName: 'Microsoft', 128 | ShippedDate: '2016-12-22T08:00:00', 129 | ShipperCity: 'Los Angeles, CA, USA', 130 | Amount: 92.00, 131 | }, 132 | { 133 | OrderID: 19, 134 | CustomerName: 'Vesta', 135 | ShippedDate: '2016-11-08T18:00:00', 136 | ShipperCity: 'Guadalajara, JAL, Mexico', 137 | Amount: 300.00, 138 | }, 139 | { 140 | OrderID: 20, 141 | CustomerName: 'OXXO', 142 | ShippedDate: '2016-11-04T18:00:00', 143 | ShipperCity: 'Portland, OR, USA', 144 | Amount: 78.00, 145 | }, 146 | { 147 | OrderID: 21, 148 | CustomerName: 'Wizeline', 149 | ShippedDate: '2015-11-04T18:00:00', 150 | ShipperCity: 'Guadalajara, JAL, Mexico', 151 | Amount: 100.00, 152 | }, 153 | { 154 | OrderID: 22, 155 | CustomerName: 'Tiempo Development', 156 | ShippedDate: '2016-01-04T18:00:00', 157 | ShipperCity: 'Monterrey, NL, Mexico', 158 | Amount: 150.00, 159 | }, 160 | ]; 161 | -------------------------------------------------------------------------------- /test/utils/responses.ts: -------------------------------------------------------------------------------- 1 | import { GridResponse } from 'tubular-common'; 2 | 3 | const simpleRecordsExpected = new GridResponse(0); 4 | simpleRecordsExpected.AggregationPayload = { CustomerName: 500 }; 5 | simpleRecordsExpected.CurrentPage = 1; 6 | simpleRecordsExpected.FilteredRecordCount = 500; 7 | simpleRecordsExpected.Payload = [ 8 | { 9 | Amount: 300, 10 | CustomerName: 'Microsoft', 11 | OrderID: 1, 12 | ShippedDate: '2016-03-19T19:00:00', 13 | ShipperCity: 'Guadalajara, JAL, Mexico', 14 | }, 15 | { 16 | Amount: '', 17 | CustomerName: 'Microsoft', 18 | OrderID: 2, 19 | ShippedDate: '2016-04-23T10:00:00', 20 | ShipperCity: 'Guadalajara, JAL, Mexico', 21 | }, 22 | { 23 | Amount: 300, 24 | CustomerName: 'Microsoft', 25 | OrderID: 3, 26 | ShippedDate: '2016-12-22T08:00:00', 27 | ShipperCity: 'Guadalajara, JAL, Mexico', 28 | }, 29 | { 30 | Amount: '', 31 | CustomerName: 'Unosquare LLC', 32 | OrderID: 4, 33 | ShippedDate: '2016-02-01T18:00:00', 34 | ShipperCity: 'Los Angeles, CA, USA', 35 | }, 36 | { 37 | Amount: 92, 38 | CustomerName: 'Microsoft', 39 | OrderID: 5, 40 | ShippedDate: '2016-11-10T18:00:00', 41 | ShipperCity: 'Guadalajara, JAL, Mexico', 42 | }, 43 | { 44 | Amount: 18, 45 | CustomerName: 'Unosquare LLC', 46 | OrderID: 6, 47 | ShippedDate: '2016-11-06T18:00:00', 48 | ShipperCity: 'Los Angeles, CA, USA', 49 | }, 50 | { 51 | Amount: 50, 52 | CustomerName: 'Unosquare LLC', 53 | OrderID: 7, 54 | ShippedDate: '2016-11-11T18:00:00', 55 | ShipperCity: 'Leon, GTO, Mexico', 56 | }, 57 | { 58 | Amount: 9, 59 | CustomerName: 'Unosquare LLC', 60 | OrderID: 8, 61 | ShippedDate: '2016-11-08T18:00:00', 62 | ShipperCity: 'Portland, OR, USA', 63 | }, 64 | { 65 | Amount: 108, 66 | CustomerName: 'Vesta', 67 | OrderID: 9, 68 | ShippedDate: '2016-11-07T18:00:00', 69 | ShipperCity: 'Guadalajara, JAL, Mexico', 70 | }, 71 | { 72 | Amount: 15, 73 | CustomerName: 'Unosquare LLC', 74 | OrderID: 10, 75 | ShippedDate: '2016-11-05T18:00:00', 76 | ShipperCity: 'Portland, OR, USA', 77 | }, 78 | ]; 79 | simpleRecordsExpected.TotalPages = 50; 80 | simpleRecordsExpected.TotalRecordCount = 500; 81 | 82 | export { simpleRecordsExpected }; 83 | -------------------------------------------------------------------------------- /test/utils/utils.tsx: -------------------------------------------------------------------------------- 1 | import { act as reactAct } from 'react-dom/test-utils'; 2 | 3 | const SUPPRESSED_PREFIXES = [ 4 | 'Warning: Do not await the result of calling ReactTestUtils.act(...)', 5 | 'Warning: An update to %s inside a test was not wrapped in act(...)', 6 | ]; 7 | 8 | function isSuppressedErrorMessage(message: string): boolean { 9 | return SUPPRESSED_PREFIXES.some((sp) => message.startsWith(sp)); 10 | } 11 | 12 | export async function act(f: () => void): Promise { 13 | const oldError = window.console.error; 14 | window.console.error = (...args: any[]) => { 15 | if (!isSuppressedErrorMessage(args[0])) { 16 | oldError(...args); 17 | } 18 | }; 19 | await Promise.race([reactAct(f), new Promise((res) => setTimeout(res))]); 20 | window.console.error = oldError; 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "noUnusedLocals": true, 5 | "noUnusedParameters": true, 6 | "allowJs": false, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "preserveConstEnums": true, 10 | "outDir": "./dist", 11 | "jsx": "react", 12 | "lib": ["es5", "es6", "dom"], 13 | "declaration": true, 14 | "sourceMap": false, 15 | "target": "es5", 16 | "skipLibCheck": true, 17 | "resolveJsonModule": true 18 | }, 19 | "include": [ "./src/**/*.ts", "./src/**/*.tsx" ], 20 | "exclude": [ "./test/**/*.*", "./node_modules" ] 21 | } -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.json" { 2 | const value: any; 3 | export default value; 4 | } --------------------------------------------------------------------------------