├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .jshintrc ├── .npmignore ├── .prettierrc ├── .storybook ├── config.js ├── webpack-build.config.js └── webpack.config.js ├── .travis.yml ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── conduct.md ├── package-lock.json ├── package.json ├── src ├── actions │ └── index.js ├── components │ ├── Cell.js │ ├── CellContainer.js │ ├── ColumnDefinition.js │ ├── Filter.js │ ├── FilterContainer.js │ ├── FilterEnhancer.js │ ├── Layout.js │ ├── LayoutContainer.js │ ├── Loading.js │ ├── LoadingContainer.js │ ├── NextButton.js │ ├── NextButtonContainer.js │ ├── NextButtonEnhancer.js │ ├── NoResults.js │ ├── NoResultsContainer.js │ ├── PageDropdown.js │ ├── PageDropdownContainer.js │ ├── PageDropdownEnhancer.js │ ├── Pagination.js │ ├── PaginationContainer.js │ ├── PreviousButton.js │ ├── PreviousButtonContainer.js │ ├── PreviousButtonEnhancer.js │ ├── Row.js │ ├── RowContainer.js │ ├── RowDefinition.js │ ├── Settings.js │ ├── SettingsContainer.js │ ├── SettingsToggle.js │ ├── SettingsToggleContainer.js │ ├── SettingsWrapper.js │ ├── SettingsWrapperContainer.js │ ├── Table.js │ ├── TableBody.js │ ├── TableBodyContainer.js │ ├── TableContainer.js │ ├── TableHeading.js │ ├── TableHeadingCell.js │ ├── TableHeadingCellContainer.js │ ├── TableHeadingCellEnhancer.js │ ├── TableHeadingContainer.js │ ├── Test.js │ ├── __tests__ │ │ ├── CellTest.js │ │ ├── FilterTest.js │ │ ├── NextButtonTest.js │ │ ├── PageDropdownTest.js │ │ ├── PaginationTest.js │ │ ├── PreviousButtonTest.js │ │ ├── RowTest.js │ │ ├── SettingsTest.js │ │ ├── SettingsToggleTest.js │ │ ├── SettingsWrapperTest.js │ │ ├── TableBodyTest.js │ │ ├── TableHeadingCellTest.js │ │ ├── TableHeadingTest.js │ │ └── TableTest.js │ └── index.js ├── constants │ └── index.js ├── core │ ├── __tests__ │ │ └── corePluginTests.js │ ├── index.js │ └── initialState.js ├── index.js ├── module.d.ts ├── module.js ├── plugins │ ├── legacyStyle │ │ └── index.js │ ├── local │ │ ├── actions │ │ │ └── index.js │ │ ├── components │ │ │ ├── NextButtonContainer.js │ │ │ ├── PageDropdownContainer.js │ │ │ ├── PreviousButtonContainer.js │ │ │ ├── RowContainer.js │ │ │ ├── TableBodyContainer.js │ │ │ ├── TableContainer.js │ │ │ ├── TableHeadingCellContainer.js │ │ │ ├── TableHeadingContainer.js │ │ │ └── index.js │ │ ├── index.js │ │ ├── reducers │ │ │ ├── __tests__ │ │ │ │ └── localReducerTests.js │ │ │ └── index.js │ │ └── selectors │ │ │ ├── __tests__ │ │ │ └── localSelectorsTest.js │ │ │ └── localSelectors.js │ └── position │ │ ├── actions │ │ └── index.js │ │ ├── components │ │ ├── Pagination.js │ │ ├── SpacerRow.js │ │ ├── TableBody.js │ │ ├── TableEnhancer.js │ │ └── index.js │ │ ├── constants │ │ └── index.js │ │ ├── index.js │ │ ├── initial-state.js │ │ ├── reducers │ │ ├── __tests__ │ │ │ └── indexTest.js │ │ └── index.js │ │ ├── selectors │ │ ├── __tests__ │ │ │ └── indexTest.js │ │ └── index.js │ │ └── utils.js ├── reducers │ ├── __tests__ │ │ └── dataReducerTest.js │ └── dataReducer.js ├── selectors │ ├── __tests__ │ │ └── dataSelectorsTest.js │ └── dataSelectors.js ├── settingsComponentObjects │ ├── ColumnChooser.js │ ├── PageSizeSettings.js │ └── index.js └── utils │ ├── __tests__ │ ├── columnUtilsTests.js │ ├── compositionUtilsTest.js │ ├── dataUtilsTests.js │ ├── griddleConnectTest.js │ ├── initilizerTests.js │ ├── rowUtilsTests.js │ └── sortUtilsTests.js │ ├── columnUtils.js │ ├── compositionUtils.js │ ├── dataUtils.js │ ├── griddleConnect.js │ ├── index.js │ ├── initializer.js │ ├── listenerUtils.js │ ├── rowUtils.js │ ├── sortUtils.js │ └── valueUtils.js ├── stories ├── fakeData.d.ts ├── fakeData.js ├── fakeData2.d.ts ├── fakeData2.js └── index.tsx ├── test └── helpers │ └── setupTest.js ├── tsconfig.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "presets": ["@babel/preset-env", "@babel/preset-react"], 5 | "plugins": ["@babel/plugin-proposal-class-properties"] 6 | }, 7 | "build": { 8 | "plugins": ["lodash", "@babel/plugin-proposal-class-properties"], 9 | "presets": ["@babel/preset-env", "@babel/preset-react"], 10 | "ignore": ["**/__tests__/*.js", "**/fake-*"] 11 | }, 12 | "test": { 13 | "presets": ["@babel/preset-env", "@babel/preset-react"], 14 | "plugins": ["@babel/plugin-proposal-class-properties"], 15 | "only": ["./**/*.js", "node_modules/jest-runtime"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | insert_final_newline = true 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "comma-dangle": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Griddle version 2 | 3 | ## Expected Behavior 4 | 5 | ## Actual Behavior 6 | 7 | ## Steps to reproduce 8 | 9 | ## Pull request with failing test or storybook story with issue 10 | 11 | While this step is not necessary, a failing test(s) and/or a [storybook story](https://github.com/storybooks/react-storybook) will help us resolve the issue much more easily. Please see the README for more information. 12 | 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Griddle major version 2 | 3 | ## Changes proposed in this pull request 4 | 5 | ## Why these changes are made 6 | 7 | ## Are there tests? -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Thank you : https://github.com/github/gitignore/blob/master/Node.gitignore 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # Compiled binary addons (http://nodejs.org/api/addons.html) 22 | build/Release 23 | 24 | # Dependency directory 25 | # Commenting this out is preferred by some people, see 26 | # https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 27 | node_modules 28 | modules 29 | docs/html 30 | 31 | # Users Environment Variables 32 | .lock-wscript 33 | 34 | .DS_Store 35 | 36 | build/ 37 | lib/ 38 | dist/ 39 | storybook-static/ 40 | 41 | *.tgz -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "asi": true, 4 | "eqeqeq": "cantbeturnedoff", 5 | "eqnull": true, 6 | "sub":true 7 | } 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Thank you : https://github.com/github/gitignore/blob/master/Node.gitignore 2 | 3 | # source 4 | src 5 | 6 | # Storybook 7 | stories 8 | storybook-static 9 | 10 | 11 | # Logs 12 | logs 13 | *.log 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Dependency directory 30 | # Commenting this out is preferred by some people, see 31 | # https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 32 | node_modules 33 | modules 34 | docs/html 35 | 36 | # Users Environment Variables 37 | .lock-wscript 38 | 39 | .DS_Store 40 | *.tgz -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "arrowParens": "always" 4 | } 5 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | function loadStories() { 4 | require('../stories/index.tsx'); 5 | } 6 | 7 | configure(loadStories, module); 8 | -------------------------------------------------------------------------------- /.storybook/webpack-build.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const include = path.resolve(__dirname, '../'); 3 | 4 | // you can use this file to add your custom webpack plugins, loaders and anything you like. 5 | // This is just the basic way to add addional webpack configurations. 6 | // For more information refer the docs: https://goo.gl/qPbSyX 7 | 8 | // IMPORTANT 9 | // When you add this file, we won't add the default configurations which is similar 10 | // to "React Create App". This only has babel loader to load JavaScript. 11 | 12 | module.exports = { 13 | devtool: 'source-map', 14 | entry: './stories/index.tsx', 15 | output: { 16 | path: path.join(__dirname, '/dist/examples/'), 17 | filename: 'storybook.js' 18 | }, 19 | resolve: { 20 | extensions: ['.webpack.js', '.web.js', '.ts', '.tsx', '.js'] 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.(ts|tsx)$/, 26 | use: [ 27 | { 28 | loader: 'babel-loader' 29 | }, 30 | { 31 | loader: 'awesome-typescript-loader' 32 | }, 33 | { 34 | loader: 'react-docgen-typescript-loader' 35 | } 36 | ], 37 | exclude: ['/node_modules/'] 38 | }, 39 | { 40 | test: /\.(js|jsx)$/, 41 | use: [ 42 | { 43 | loader: 'babel-loader', 44 | options: { 45 | presets: ['@babel/preset-env', '@babel/preset-react'] 46 | } 47 | } 48 | ], 49 | exclude: ['/node_modules/'] 50 | } 51 | ] 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports = (baseConfig, env, config) => { 3 | config.module.rules.push({ 4 | test: /\.(ts|tsx)$/, 5 | use: [ 6 | { loader: 'babel-loader' }, 7 | { 8 | loader: 'awesome-typescript-loader' 9 | }, 10 | { 11 | loader: 'react-docgen-typescript-loader' 12 | } 13 | ] 14 | }); 15 | config.resolve.extensions.push('.ts', '.tsx'); 16 | return config; 17 | }; 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '9' 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | install: 10 | - 'npm install' 11 | script: 12 | - npm run build 13 | - npm run check-ts 14 | - npm run build-examples 15 | - npm test 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project _now_ adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [1.12.0] - 2018-03-16 8 | - Additional propTypes fixes 9 | - Fix for filter matching invisible properties (thanks @mbland) 10 | - Add ability to change placeholder text (thanks @miguelsaldivar) 11 | 12 | ## [1.11.2] - 2018-02-15 13 | - Fixes for propTypes typo 14 | 15 | ## [1.11.1] - 2017-12-20 16 | - Fixes for initializers 17 | 18 | ## [1.11.0] - 2017-12-20 19 | - TypeScript updates 20 | - Filter updates 21 | - other enhancements 22 | 23 | ## [1.9.0] - 2017-09-15 24 | - Performance improvements 25 | - Store listeners 26 | - Thanks @andreme, @shorja! 27 | 28 | ## [1.8.1] - 2017-08-31 29 | - Fixes for TypeScript definitions 30 | - Add redux middleware to Griddle through plugins 31 | - Thanks Jesse Farebrother, Short, James, and @Errorific 32 | 33 | ## [1.8.0] - 2017-08-20 34 | - Add custom store 35 | - Fix for table styles 36 | - Updates to pull more information from textProperties 37 | - Better sorting, filtering 38 | - Add components.Style for better plugins 39 | - Various bug fixes and improvements 40 | - Thanks @JesseFarebro, @dahlbyk, @Jermorin, @andreme 41 | 42 | ## [1.5.0] - 2017-05-08 43 | - Update to PropTypes library instead of using deprecated React version 44 | - Respect sortable columns 45 | - Add lodash babel plugin (for smaller builds) 46 | - Column ordering 47 | - Conditional columns 48 | - Thanks @followbl, @dahlbyk, @bpugh, @andreme! 49 | 50 | ## [1.4.0] - 2017-04-21 51 | - CSS class name can be a function (to generate the name) 52 | 53 | ## [1.3.1] - 2017-04-18 54 | - Fix for cssClassName and headerCssClassName 55 | - Thanks @zeusi83! 56 | 57 | ## [1.3.0] - 2017-04-04 58 | - Add type definitions to Griddle 59 | - Settings Component customization [See this PR for more info](https://github.com/GriddleGriddle/Griddle/pull/628) 60 | - Table / No result improvements [See this PR for more info](https://github.com/GriddleGriddle/Griddle/pull/624) 61 | - Thanks a ton @dahlbyk for these! 62 | 63 | ## [1.2.0] - 2017-03-21 64 | - Fix for dates in data 65 | 66 | ## [1.1.0] - 2017-03-04 67 | - Add rowKey option to RowDefinition 68 | 69 | ## [1.0.3] - 2017-03-03 70 | ### Fixed 71 | - Fix a problem where columns could not have the same title 72 | 73 | ## [1.0.2] - 2017-03-03 74 | ### Fixed 75 | - Fix a problem toggling columns that don't have related data 76 | 77 | ## [1.0.1] - 2017-02-28 78 | ### Added 79 | - Fixed performance problem with cell selectors -- anecdotal but ~500k rows on my computer is pretty fast as opposed to previous lag 80 | 81 | ## [1.0.0] - 2017-02-19 82 | ### Added 83 | - New version of Griddle. [See the docs](http://griddlegriddle.github.io/Griddle/) for more information. 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 DynamicTyped 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Griddle 2 | ======= 3 | 4 | [Needs more maintainers - Please see this issue](https://github.com/GriddleGriddle/Griddle/issues/848) 5 | 6 | An ultra customizable datagrid component for React 7 | ---------- 8 | 9 | [![Gitter](https://badges.gitter.im/JoinChat.svg)](https://gitter.im/DynamicTyped/Griddle?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/GriddleGriddle/Griddle.svg?branch=master)](https://travis-ci.org/GriddleGriddle/Griddle) 10 | 11 | ---------- 12 | 13 | Please check out the [documentation and examples](http://griddlegriddle.github.io/Griddle/). 14 | 15 | TLDR: Griddle now has a customizable architecture that allows for one-off customizations or reusable plugins. These customization options allow for overriding everything from components, to internal datagrid state management, and more. 16 | 17 | ---------- 18 | 19 | To use Griddle: 20 | 21 | `npm install griddle-react` 22 | 23 | ---------- 24 | 25 | To run from source: 26 | 27 | 1. `npm install` 28 | 2. `npm run storybook` 29 | 30 | ---------- 31 | 32 | ### Issues: ### 33 | 34 | If you run into an issue in Griddle please let us know through the issue tracker. It is incredibly helpful if you create a failing test(s) and/or a storybook story that demonstrates the issue as a pull request and reference this pull request in the issue. To add a storybook story, navigate to `/stories/index.js` and add a story to the `storiesOf('Bug fixes' ...)` section. 35 | 36 | ### Contributing: ### 37 | 38 | Please feel free submit any bugs. Any questions should go in the [Gitter chat](https://gitter.im/DynamicTyped/Griddle) channel or [stackoverflow](http://stackoverflow.com/). Pull requests are welcome but if you have an idea please post as an issue first to make sure everyone is on the same-page (and to help avoid duplicate work). If you are looking to help out but don't know where to start, please take a look at [approved issues that don't have anyone assigned](https://github.com/GriddleGriddle/Griddle/issues?q=is%3Aopen+label%3Aapproved+no%3Aassignee). 39 | 40 | We do most of our initial feature development in the [Storybook](https://github.com/storybooks/react-storybook) stories contained in this project. When you run `npm run storybook`, a web server is setup that quickly provides access to Griddle in various states. All storybook stories are currently in `/stories/index.js` 41 | 42 | We would love any help at all but want to call out the following things: 43 | * Help maintaining 0.x 44 | * More tests - we have a number of tests in version 1.0 but not quite where we'd like it to be 45 | * More plugins 46 | -------------------------------------------------------------------------------- /conduct.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## All contributors and maintainers of this project are subject to this code of conduct. 4 | 5 | As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 6 | 7 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. 8 | 9 | Examples of unacceptable behavior by participants include: 10 | 11 | * The use of sexualized language or imagery 12 | * Personal attacks 13 | * Trolling or insulting/derogatory comments 14 | * Public or private harassment 15 | * Publishing other's private information, such as physical or electronic addresses, without explicit permission 16 | * Other unethical or unprofessional conduct 17 | 18 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 19 | 20 | By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. 21 | 22 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. 23 | 24 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project maintainer at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident. 25 | 26 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.3.0, 27 | available at [http://contributor-covenant.org/version/1/3/0/][version] 28 | 29 | [homepage]: http://contributor-covenant.org 30 | [version]: http://contributor-covenant.org/version/1/3/0/ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "griddle-react", 3 | "version": "1.14.0", 4 | "description": "A fast and flexible grid component for React", 5 | "keywords": [ 6 | "react-component", 7 | "grid", 8 | "react", 9 | "pagination", 10 | "sort" 11 | ], 12 | "main": "dist/module/module.js", 13 | "types": "dist/module/module.d.ts", 14 | "scripts": { 15 | "start": "start-storybook -p 6006", 16 | "test": "ava", 17 | "check-ts": "tsc --version && tsc --strict src/module.d.ts", 18 | "watch-test": "ava --watch", 19 | "storybook": "start-storybook -p 6006", 20 | "build-storybook": "build-storybook", 21 | "build": "npm run clean-dist && npm run build-modules && npm run build-umd && npm run build-ts", 22 | "clean-dist": "rimraf dist", 23 | "build-examples": "webpack --config .storybook/webpack-build.config.js", 24 | "build-ts": "cp src/module.d.ts dist/module/", 25 | "build-umd": "webpack --config webpack.config.js", 26 | "build-modules": "cross-env BABEL_ENV=build babel src --out-dir dist/module ", 27 | "postpublish": "git push --tags", 28 | "prepare": "npm run build", 29 | "preversion": "npm test", 30 | "ship-it": "npm publish --tag next" 31 | }, 32 | "peerDependencies": { 33 | "react": ">=16" 34 | }, 35 | "devDependencies": { 36 | "@babel/cli": "^7.2.3", 37 | "@babel/core": "^7.3.3", 38 | "@babel/plugin-proposal-class-properties": "^7.3.3", 39 | "@babel/preset-env": "^7.3.1", 40 | "@babel/preset-react": "^7.0.0", 41 | "@babel/register": "^7.0.0", 42 | "@storybook/addon-info": "^4.1.13", 43 | "@storybook/react": "^4.1.13", 44 | "@types/jest": "^24.0.6", 45 | "@types/node": "^11.9.5", 46 | "@types/prop-types": "15.5.9", 47 | "@types/react": "^16.8.4", 48 | "@types/react-redux": "^5.0.21", 49 | "@types/recompose": "^0.30.4", 50 | "@types/storybook__react": "^4.0.1", 51 | "ava": "^1.2.1", 52 | "awesome-typescript-loader": "^5.2.1", 53 | "babel-loader": "^8.0.5", 54 | "babel-plugin-lodash": "^3.3.4", 55 | "core-js": "^2.6.5", 56 | "cross-env": "^5.2.0", 57 | "enzyme": "^3.9.0", 58 | "enzyme-adapter-react-16": "^1.9.1", 59 | "eslint": "^5.14.1", 60 | "eslint-config-airbnb": "^17.1.0", 61 | "eslint-plugin-import": "^2.16.0", 62 | "eslint-plugin-jsx-a11y": "^6.2.1", 63 | "eslint-plugin-react": "^7.12.4", 64 | "jest": "^24.1.0", 65 | "jsdom": "^13.2.0", 66 | "jsdom-global": "^3.0.2", 67 | "lodash-webpack-plugin": "^0.11.5", 68 | "node-libs-browser": "^2.2.0", 69 | "react": "^16.8.3", 70 | "react-docgen-typescript-loader": "^3.0.1", 71 | "react-docgen-typescript-webpack-plugin": "^1.1.0", 72 | "react-dom": "^16.8.3", 73 | "rimraf": "^2.6.3", 74 | "ts-jest": "^24.0.0", 75 | "ts-loader": "^5.3.3", 76 | "typescript": "^3.3.3333", 77 | "uglifyjs-webpack-plugin": "^2.1.1", 78 | "webpack": "^4.29.5", 79 | "webpack-cli": "^3.2.3", 80 | "webpack-dev-server": "^3.2.0" 81 | }, 82 | "dependencies": { 83 | "immutable": "^3.8.2", 84 | "lodash.assignin": "^4.2.0", 85 | "lodash.compact": "^3.0.1", 86 | "lodash.flatten": "^4.4.0", 87 | "lodash.flattendeep": "^4.4.0", 88 | "lodash.flow": "^3.5.0", 89 | "lodash.flowright": "^3.5.0", 90 | "lodash.forin": "^4.4.0", 91 | "lodash.isequal": "^4.5.0", 92 | "lodash.isfinite": "^3.3.2", 93 | "lodash.isstring": "^4.0.1", 94 | "lodash.merge": "^4.6.1", 95 | "lodash.pick": "^4.4.0", 96 | "lodash.pickby": "^4.6.0", 97 | "lodash.range": "^3.2.0", 98 | "lodash.union": "^4.6.0", 99 | "lodash.uniq": "^4.5.0", 100 | "max-safe-integer": "^2.0.0", 101 | "prop-types": "^15.7.2", 102 | "react-redux": "^5.1.1", 103 | "recompose": "^0.30.0", 104 | "redux": "^4.0.1", 105 | "reselect": "^4.0.0" 106 | }, 107 | "ava": { 108 | "require": [ 109 | "@babel/register", 110 | "./test/helpers/setupTest.js" 111 | ] 112 | }, 113 | "author": "Ryan Lanciaux & Joel Lanciaux", 114 | "license": "MIT" 115 | } 116 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | GRIDDLE_NEXT_PAGE, 3 | GRIDDLE_PREVIOUS_PAGE, 4 | GRIDDLE_SET_PAGE, 5 | GRIDDLE_SET_SORT, 6 | GRIDDLE_SET_FILTER, 7 | GRIDDLE_TOGGLE_SETTINGS, 8 | GRIDDLE_TOGGLE_COLUMN, 9 | GRIDDLE_SET_PAGE_SIZE, 10 | GRIDDLE_UPDATE_STATE, 11 | } from '../constants'; 12 | 13 | export function getNext() { 14 | return { 15 | type: GRIDDLE_NEXT_PAGE 16 | } 17 | } 18 | 19 | export function getPrevious() { 20 | return { 21 | type: GRIDDLE_PREVIOUS_PAGE 22 | } 23 | } 24 | 25 | export function setPage(pageNumber) { 26 | return { 27 | type: GRIDDLE_SET_PAGE, 28 | pageNumber 29 | }; 30 | } 31 | 32 | export function setFilter(filter) { 33 | return { 34 | type: GRIDDLE_SET_FILTER, 35 | filter 36 | } 37 | } 38 | 39 | export function setSortColumn(sortProperties) { 40 | return { 41 | type: GRIDDLE_SET_SORT, 42 | sortProperties 43 | } 44 | } 45 | 46 | export function toggleSettings() { 47 | return { 48 | type: GRIDDLE_TOGGLE_SETTINGS 49 | } 50 | } 51 | 52 | export function toggleColumn(columnId) { 53 | return { 54 | type: GRIDDLE_TOGGLE_COLUMN, 55 | columnId 56 | } 57 | } 58 | 59 | export function setPageSize(pageSize) { 60 | return { 61 | type: GRIDDLE_SET_PAGE_SIZE, 62 | pageSize 63 | } 64 | } 65 | 66 | export function updateState(newState) { 67 | return { 68 | type: GRIDDLE_UPDATE_STATE, 69 | newState 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/components/Cell.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Cell = ({ value, onClick, onMouseEnter, onMouseLeave, style, className }) => ( 4 | 11 | {value} 12 | 13 | ); 14 | 15 | export default Cell; 16 | -------------------------------------------------------------------------------- /src/components/CellContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from '../utils/griddleConnect'; 4 | import getContext from 'recompose/getContext'; 5 | import mapProps from 'recompose/mapProps'; 6 | import compose from 'recompose/compose'; 7 | 8 | import { 9 | customComponentSelector, 10 | cellValueSelector, 11 | cellPropertiesSelectorFactory, 12 | classNamesForComponentSelector, 13 | stylesForComponentSelector 14 | } from '../selectors/dataSelectors'; 15 | import { valueOrResult } from '../utils/valueUtils'; 16 | 17 | function hasWidthOrStyles(cellProperties) { 18 | return cellProperties.hasOwnProperty('width') || cellProperties.hasOwnProperty('styles'); 19 | } 20 | 21 | function getCellStyles(cellProperties, originalStyles) { 22 | if (!hasWidthOrStyles(cellProperties)) { return originalStyles; } 23 | 24 | let styles = originalStyles; 25 | 26 | // we want to take griddle style object styles, cell specific styles 27 | if (cellProperties.hasOwnProperty('style')) { 28 | styles = Object.assign({}, styles, originalStyles, cellProperties.style); 29 | } 30 | 31 | if (cellProperties.hasOwnProperty('width')) { 32 | styles = Object.assign({}, styles, { width: cellProperties.width }); 33 | } 34 | 35 | return styles; 36 | } 37 | 38 | const mapStateToProps = () => { 39 | const cellPropertiesSelector = cellPropertiesSelectorFactory(); 40 | return (state, props) => { 41 | return { 42 | value: cellValueSelector(state, props), 43 | customComponent: customComponentSelector(state, props), 44 | cellProperties: cellPropertiesSelector(state, props), 45 | className: classNamesForComponentSelector(state, 'Cell'), 46 | style: stylesForComponentSelector(state, 'Cell'), 47 | }; 48 | }; 49 | } 50 | 51 | const ComposedCellContainer = OriginalComponent => compose( 52 | connect(mapStateToProps), 53 | mapProps(props => { 54 | return ({ 55 | ...props.cellProperties.extraData, 56 | ...props, 57 | className: valueOrResult(props.cellProperties.cssClassName, props) || props.className, 58 | style: getCellStyles(props.cellProperties, props.style), 59 | value: props.customComponent ? 60 | : 61 | props.value 62 | })}), 63 | )(props => 64 | 67 | ); 68 | 69 | export default ComposedCellContainer; 70 | -------------------------------------------------------------------------------- /src/components/ColumnDefinition.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class ColumnDefinition extends Component { 5 | static propTypes = { 6 | //The name of the column that this definition applies to. 7 | id: PropTypes.string.isRequired, 8 | 9 | //The order that this column appears in. If not specified will just use the order that they are defined 10 | order: PropTypes.number, 11 | 12 | //Determines whether or not the user can disable this column from the settings. 13 | locked: PropTypes.bool, 14 | 15 | //The css class name, or a function to generate a class name from props, to apply to the header for the column. 16 | headerCssClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), 17 | 18 | //The css class name, or a function to generate a class name from props, to apply to this column. 19 | cssClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), 20 | 21 | //The display name for the column. This is used when the name in the column heading and settings should be different from the data passed in to the Griddle component. 22 | title: PropTypes.string, 23 | 24 | //The component that should be rendered instead of the standard column data. This component will still be rendered inside of a TD element. 25 | customComponent: PropTypes.func, 26 | 27 | //The component that should be used instead of the normal title 28 | customHeadingComponent: PropTypes.func, 29 | 30 | //Can this column be filtered 31 | filterable: PropTypes.bool, 32 | 33 | //Can this column be sorted 34 | sortable: PropTypes.bool, 35 | 36 | //What sort type this column uses - magic string :shame: 37 | sortType: PropTypes.string, 38 | 39 | //Any extra data that should be passed to each instance of this column 40 | extraData: PropTypes.object, 41 | 42 | //The width of this column -- this is string so things like % can be specified 43 | width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 44 | 45 | //The number of cells this column should extend. Default is 1. 46 | colSpan: PropTypes.number, 47 | 48 | // Is this column visible 49 | visible: PropTypes.bool, 50 | 51 | // Is this column metadta 52 | isMetadata: PropTypes.bool, 53 | }; 54 | 55 | render() { 56 | return null; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/components/Filter.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class Filter extends Component { 5 | static propTypes = { 6 | setFilter: PropTypes.func, 7 | style: PropTypes.object, 8 | className: PropTypes.string, 9 | placeholder: PropTypes.string, 10 | } 11 | 12 | setFilter = (e) => { 13 | this.props.setFilter(e.target.value); 14 | } 15 | 16 | render() { 17 | return ( 18 | 26 | ) 27 | } 28 | } 29 | 30 | export default Filter; 31 | -------------------------------------------------------------------------------- /src/components/FilterContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from '../utils/griddleConnect'; 4 | 5 | import { classNamesForComponentSelector, stylesForComponentSelector, textSelector } from '../selectors/dataSelectors'; 6 | import { setFilter } from '../actions'; 7 | 8 | const EnhancedFilter = OriginalComponent => connect((state, props) => ({ 9 | placeholder: textSelector(state, { key: 'filterPlaceholder' }), 10 | className: classNamesForComponentSelector(state, 'Filter'), 11 | style: stylesForComponentSelector(state, 'Filter'), 12 | }), { setFilter })(props => ); 13 | 14 | export default EnhancedFilter; 15 | -------------------------------------------------------------------------------- /src/components/FilterEnhancer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import compose from 'recompose/compose'; 4 | import mapProps from 'recompose/mapProps'; 5 | import getContext from 'recompose/getContext'; 6 | import { combineHandlers } from '../utils/compositionUtils'; 7 | 8 | const EnhancedFilter = OriginalComponent => compose( 9 | getContext({ 10 | events: PropTypes.object 11 | }), 12 | mapProps(({ events: { onFilter }, ...props }) => ({ 13 | ...props, 14 | setFilter: combineHandlers([onFilter, props.setFilter]), 15 | })) 16 | )(props => ); 17 | 18 | export default EnhancedFilter; 19 | -------------------------------------------------------------------------------- /src/components/Layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const component = ({Table, Pagination, Filter, SettingsWrapper, Style, className, style}) => ( 4 |
5 | {Style && 100 | ), 101 | } 102 | }; 103 | -------------------------------------------------------------------------------- /src/plugins/local/actions/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GriddleGriddle/Griddle/184fc32276274631125b8f0c4e50096f385f7f6a/src/plugins/local/actions/index.js -------------------------------------------------------------------------------- /src/plugins/local/components/NextButtonContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from '../../../utils/griddleConnect'; 3 | 4 | import { textSelector, hasNextSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/localSelectors'; 5 | import { getNext } from '../../../actions'; 6 | 7 | const enhance = OriginalComponent => connect(state => ({ 8 | text: textSelector(state, { key: 'next' }), 9 | hasNext: hasNextSelector(state), 10 | className: classNamesForComponentSelector(state, 'NextButton'), 11 | style: stylesForComponentSelector(state, 'NextButton'), 12 | }), 13 | { 14 | getNext 15 | } 16 | )(props => ); 17 | 18 | export default enhance; 19 | -------------------------------------------------------------------------------- /src/plugins/local/components/PageDropdownContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from '../../../utils/griddleConnect'; 3 | import { createStructuredSelector } from 'reselect'; 4 | 5 | import { currentPageSelector, maxPageSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/localSelectors'; 6 | import { setPage } from '../../../actions'; 7 | 8 | const enhance = OriginalComponent => connect(state => ({ 9 | maxPages: maxPageSelector(state), 10 | currentPage: currentPageSelector(state), 11 | className: classNamesForComponentSelector(state, 'PageDropdown'), 12 | style: stylesForComponentSelector(state, 'PageDropdown'), 13 | }), 14 | { 15 | setPage 16 | } 17 | )(props => ); 18 | 19 | export default enhance; 20 | 21 | -------------------------------------------------------------------------------- /src/plugins/local/components/PreviousButtonContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from '../../../utils/griddleConnect'; 3 | 4 | import { textSelector, hasPreviousSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/localSelectors'; 5 | import { getPrevious } from '../../../actions'; 6 | 7 | const enhance = OriginalComponent => connect(state => ({ 8 | text: textSelector(state, { key: 'previous' }), 9 | hasPrevious: hasPreviousSelector(state), 10 | className: classNamesForComponentSelector(state, 'PreviousButton'), 11 | style: stylesForComponentSelector(state, 'PreviousButton'), 12 | }), 13 | { 14 | getPrevious 15 | } 16 | )(props => ); 17 | 18 | export default enhance; 19 | -------------------------------------------------------------------------------- /src/plugins/local/components/RowContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from '../../../utils/griddleConnect'; 4 | import compose from 'recompose/compose'; 5 | import mapProps from 'recompose/mapProps'; 6 | import getContext from 'recompose/getContext'; 7 | 8 | import { 9 | columnIdsSelector, 10 | rowDataSelector, 11 | rowPropertiesSelector, 12 | classNamesForComponentSelector, 13 | stylesForComponentSelector, 14 | } from '../selectors/localSelectors'; 15 | import { valueOrResult } from '../../../utils/valueUtils'; 16 | 17 | const ComposedRowContainer = OriginalComponent => compose( 18 | getContext({ 19 | components: PropTypes.object 20 | }), 21 | connect((state, props) => ({ 22 | columnIds: columnIdsSelector(state), 23 | rowProperties: rowPropertiesSelector(state), 24 | rowData: rowDataSelector(state, props), 25 | className: classNamesForComponentSelector(state, 'Row'), 26 | style: stylesForComponentSelector(state, 'Row'), 27 | })), 28 | mapProps((props) => { 29 | const { components, rowProperties, className, ...otherProps } = props; 30 | return { 31 | Cell: components.Cell, 32 | className: valueOrResult(rowProperties.cssClassName, props) || props.className, 33 | ...otherProps, 34 | }; 35 | }), 36 | )(({Cell, columnIds, griddleKey, style, className }) => ( 37 | 44 | )); 45 | 46 | export default ComposedRowContainer; 47 | -------------------------------------------------------------------------------- /src/plugins/local/components/TableBodyContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from '../../../utils/griddleConnect'; 4 | import compose from 'recompose/compose'; 5 | import mapProps from 'recompose/mapProps'; 6 | import getContext from 'recompose/getContext'; 7 | 8 | import { classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/localSelectors'; 9 | 10 | const ComposedTableBodyContainer = OriginalComponent => compose( 11 | getContext({ 12 | components: PropTypes.object, 13 | selectors: PropTypes.object, 14 | }), 15 | mapProps(props => ({ 16 | Row: props.components.Row, 17 | visibleRowIdsSelector: props.selectors.visibleRowIdsSelector, 18 | ...props 19 | })), 20 | connect((state, props) => ({ 21 | visibleRowIds: props.visibleRowIdsSelector(state), 22 | className: classNamesForComponentSelector(state, 'TableBody'), 23 | style: stylesForComponentSelector(state, 'TableBody'), 24 | })), 25 | // withHandlers({ 26 | // Row: props => props.components.Row 27 | // }) 28 | )(({ Row, visibleRowIds, style, className }) => ( 29 | 35 | )); 36 | 37 | export default ComposedTableBodyContainer; 38 | -------------------------------------------------------------------------------- /src/plugins/local/components/TableContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from '../../../utils/griddleConnect'; 4 | import compose from 'recompose/compose'; 5 | import mapProps from 'recompose/mapProps'; 6 | import getContext from 'recompose/getContext'; 7 | 8 | import { classNamesForComponentSelector, stylesForComponentSelector, dataLoadingSelector, visibleRowCountSelector } from '../selectors/localSelectors'; 9 | 10 | const ComposedContainerComponent = OriginalComponent => compose( 11 | getContext({ 12 | components: PropTypes.object 13 | }), 14 | mapProps(props => ({ 15 | TableHeading: props.components.TableHeading, 16 | TableBody: props.components.TableBody, 17 | Loading: props.components.Loading, 18 | NoResults: props.components.NoResults, 19 | })), 20 | connect( 21 | (state, props) => ({ 22 | dataLoading: dataLoadingSelector(state), 23 | visibleRows: visibleRowCountSelector(state), 24 | className: classNamesForComponentSelector(state, 'Table'), 25 | style: stylesForComponentSelector(state, 'Table'), 26 | }) 27 | ), 28 | )(props => ); 29 | 30 | export default ComposedContainerComponent; 31 | -------------------------------------------------------------------------------- /src/plugins/local/components/TableHeadingCellContainer.js: -------------------------------------------------------------------------------- 1 | import TableHeadingCellContainer from '../../../components/TableHeadingCellContainer'; 2 | 3 | // Obsolete 4 | const EnhancedHeadingCell = TableHeadingCellContainer; 5 | 6 | export default EnhancedHeadingCell; 7 | -------------------------------------------------------------------------------- /src/plugins/local/components/TableHeadingContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from '../../../utils/griddleConnect'; 4 | import compose from 'recompose/compose'; 5 | import mapProps from 'recompose/mapProps'; 6 | import getContext from 'recompose/getContext'; 7 | import { columnTitlesSelector, columnIdsSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/localSelectors'; 8 | 9 | const ComposedContainerComponent = OriginalComponent => compose( 10 | getContext({ 11 | components: PropTypes.object 12 | }), 13 | connect((state) => ({ 14 | columnTitles: columnTitlesSelector(state), 15 | columnIds: columnIdsSelector(state), 16 | className: classNamesForComponentSelector(state, 'TableHeading'), 17 | style: stylesForComponentSelector(state, 'TableHeading'), 18 | })), 19 | mapProps(props => ({ 20 | TableHeadingCell: props.components.TableHeadingCell, 21 | ...props 22 | })) 23 | // withHandlers({ 24 | // TableHeadingCell: props => props.components.TableHeadingCell 25 | // }) 26 | )(({TableHeadingCell, columnTitles, columnIds, className, style }) => ( 27 | 34 | )); 35 | 36 | export default ComposedContainerComponent; 37 | -------------------------------------------------------------------------------- /src/plugins/local/components/index.js: -------------------------------------------------------------------------------- 1 | import TableBodyContainer from './TableBodyContainer'; 2 | import RowContainer from './RowContainer'; 3 | import NextButtonContainer from './NextButtonContainer'; 4 | import PreviousButtonContainer from './PreviousButtonContainer'; 5 | import PageDropdownContainer from './PageDropdownContainer'; 6 | import TableContainer from './TableContainer'; 7 | import TableHeadingCellContainer from './TableHeadingCellContainer'; 8 | 9 | export default { 10 | TableBodyContainer, 11 | RowContainer, 12 | NextButtonContainer, 13 | PreviousButtonContainer, 14 | PageDropdownContainer, 15 | TableContainer, 16 | TableHeadingCellContainer, // TODO: Obsolete; remove 17 | }; 18 | -------------------------------------------------------------------------------- /src/plugins/local/index.js: -------------------------------------------------------------------------------- 1 | import components from './components'; 2 | import * as reducer from './reducers'; 3 | import * as selectors from './selectors/localSelectors'; 4 | 5 | export default { 6 | components, 7 | reducer, 8 | selectors 9 | }; -------------------------------------------------------------------------------- /src/plugins/local/reducers/__tests__/localReducerTests.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import Immutable from 'immutable'; 3 | 4 | import * as reducers from '../index'; 5 | import constants from '../../../../constants'; 6 | 7 | test('it loads data', test => { 8 | const state = reducers.GRIDDLE_LOADED_DATA(Immutable.fromJS({ renderProperties: { } }), { 9 | data: [ 10 | {name: "one"}, 11 | {name: "two"} 12 | ]} 13 | ); 14 | 15 | test.deepEqual(state.toJSON(), { 16 | data: [ 17 | {name: "one", griddleKey: 0}, 18 | {name: "two", griddleKey: 1} 19 | ], 20 | lookup: { 0: 0, 1: 1 }, 21 | renderProperties: {}, 22 | loading: false 23 | }); 24 | }); 25 | 26 | test('sets the correct page number', test => { 27 | const state = reducers.GRIDDLE_SET_PAGE(new Immutable.Map(), { 28 | pageNumber: 2 29 | }); 30 | 31 | test.is(state.getIn(['pageProperties', 'currentPage']), 2); 32 | }); 33 | 34 | 35 | test('sets page size', test => { 36 | const state = reducers.GRIDDLE_SET_PAGE_SIZE( new Immutable.Map(), { 37 | pageSize: 11 38 | }); 39 | 40 | test.is(state.getIn(['pageProperties', 'pageSize']), 11); 41 | }); 42 | 43 | test('sets filter null', test => { 44 | const state = reducers.GRIDDLE_SET_FILTER(new Immutable.Map(), { 45 | filter: null, 46 | }); 47 | 48 | test.is(state.get('filter'), null); 49 | test.is(state.getIn(['pageProperties', 'currentPage']), 1) 50 | }); 51 | 52 | test('sets filter string', test => { 53 | const filter = 'onetwothree'; 54 | const state = reducers.GRIDDLE_SET_FILTER(new Immutable.Map(), { 55 | filter 56 | }); 57 | 58 | test.is(state.get('filter'), filter); 59 | test.is(state.getIn(['pageProperties', 'currentPage']), 1) 60 | }); 61 | 62 | test('sets filter function', test => { 63 | const filter = (v, i) => i % 2; 64 | const state = reducers.GRIDDLE_SET_FILTER(new Immutable.Map(), { 65 | filter, 66 | }); 67 | 68 | test.is(state.get('filter'), filter); 69 | test.is(state.getIn(['pageProperties', 'currentPage']), 1) 70 | }); 71 | 72 | test('sets filter object', test => { 73 | const filter = { 74 | id: (v, i) => i % 2, 75 | name: 'ben', 76 | }; 77 | const state = reducers.GRIDDLE_SET_FILTER(new Immutable.Map(), { 78 | filter, 79 | }); 80 | 81 | test.not(state.get('filter'), filter); 82 | test.deepEqual(state.get('filter').toJS(), filter); 83 | test.is(state.getIn(['pageProperties', 'currentPage']), 1) 84 | }); 85 | 86 | test('sets sort columns', test => { 87 | const state = reducers.GRIDDLE_SET_SORT(new Immutable.Map(), { 88 | sortProperties: [ 89 | { id: 'one', sortAscending: true }, 90 | { id: 'two', sortAscending: false } 91 | ] 92 | }); 93 | 94 | test.deepEqual(state.get('sortProperties').toJSON(), [ 95 | { id: 'one', sortAscending: true }, 96 | { id: 'two', sortAscending: false } 97 | ]); 98 | }); 99 | /* 100 | describe('sorting', () => { 101 | const reducer = (options, method) => { 102 | return getMethod(extend(options, { method })); 103 | } 104 | 105 | it('sets sort column', () => { 106 | const state = reducer({payload: { sortColumns: ['one']}}, GRIDDLE_SORT); 107 | 108 | expect(state.get('sortColumns')).toEqual(['one']); 109 | }); 110 | }); 111 | }); 112 | 113 | */ 114 | -------------------------------------------------------------------------------- /src/plugins/local/reducers/index.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import { maxPageSelector, currentPageSelector } from '../selectors/localSelectors'; 3 | 4 | import * as dataReducers from '../../../reducers//dataReducer'; 5 | 6 | export function GRIDDLE_INITIALIZED(state) { 7 | return dataReducers.GRIDDLE_INITIALIZED(state); 8 | } 9 | /** Sets the Griddle data 10 | * @param {Immutable} state - Immutable state object 11 | * @param {Object} action - the action object to work with 12 | * 13 | * This simply wraps dataReducer 14 | */ 15 | export function GRIDDLE_LOADED_DATA(state, action) { 16 | return dataReducers.GRIDDLE_LOADED_DATA(state, action); 17 | } 18 | 19 | /** Sets the page size 20 | * @param {Immutable} state - Immutable state object 21 | * @param {Object} action - the action object to work with 22 | * 23 | * This simply wraps dataReducer 24 | */ 25 | export function GRIDDLE_SET_PAGE_SIZE(state, action) { 26 | return dataReducers.GRIDDLE_SET_PAGE_SIZE(state, action); 27 | } 28 | 29 | /** Sets the current page 30 | * @param {Immutable} state - Immutable state object 31 | * @param {Object} action - the action object to work with 32 | * 33 | * This simply wraps dataReducer 34 | */ 35 | export function GRIDDLE_SET_PAGE(state, action) { 36 | return dataReducers.GRIDDLE_SET_PAGE(state, action); 37 | } 38 | 39 | export function GRIDDLE_NEXT_PAGE(state, action) { 40 | const maxPage = maxPageSelector(state); 41 | const currentPage = currentPageSelector(state); 42 | 43 | if(currentPage < maxPage) { 44 | return state.setIn(['pageProperties', 'currentPage'], currentPage + 1); 45 | } 46 | 47 | return state; 48 | } 49 | 50 | export function GRIDDLE_PREVIOUS_PAGE(state, action) { 51 | const currentPage = currentPageSelector(state); 52 | 53 | if(currentPage > 0) { 54 | return state.setIn(['pageProperties', 'currentPage'], currentPage - 1); 55 | } 56 | 57 | return state; 58 | } 59 | 60 | /** Sets the current filter 61 | * @param {Immutable} state - Immutable state object 62 | * @param {Object} action - the action object to work with 63 | * 64 | */ 65 | export function GRIDDLE_SET_FILTER(state, action) { 66 | return state 67 | .set('filter', action.filter && Immutable.fromJS(action.filter)) 68 | .setIn(['pageProperties', 'currentPage'], 1); 69 | }; 70 | 71 | /** Sets the sort options 72 | * @param {Immutable} state - Immutable state object 73 | * @param {Object} action - the action object to work with 74 | * 75 | * This simply wraps dataReducer 76 | */ 77 | export function GRIDDLE_SET_SORT(state, action) { 78 | return dataReducers.GRIDDLE_SET_SORT(state, action); 79 | }; 80 | -------------------------------------------------------------------------------- /src/plugins/local/selectors/localSelectors.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import { createSelector } from 'reselect'; 3 | import isFinite from 'lodash.isfinite'; 4 | 5 | import { defaultSort } from '../../../utils/sortUtils'; 6 | import { getVisibleDataForColumns } from '../../../utils/dataUtils'; 7 | import * as dataSelectors from '../../../selectors/dataSelectors'; 8 | 9 | /** Gets the entire data set 10 | * @param {Immutable} state - state object 11 | */ 12 | export const dataSelector = state => state.get('data'); 13 | 14 | export const dataLoadingSelector = dataSelectors.dataLoadingSelector; 15 | 16 | /** Gets the current page from pageProperties 17 | * @param {Immutable} state - state object 18 | */ 19 | export const currentPageSelector = state => state.getIn(['pageProperties', 'currentPage']); 20 | 21 | /** Gets the currently set page size 22 | * @param {Immutable} state - state object 23 | */ 24 | export const pageSizeSelector = state => state.getIn(['pageProperties', 'pageSize']); 25 | 26 | /** Gets the currently set filter 27 | */ 28 | export const filterSelector = state => (state.get('filter') || ''); 29 | 30 | export const sortPropertiesSelector = state => (state.get('sortProperties')); 31 | 32 | export const sortMethodSelector = state => state.get('sortMethod'); 33 | 34 | export const renderPropertiesSelector = state => (state.get('renderProperties')); 35 | 36 | export const metaDataColumnsSelector = dataSelectors.metaDataColumnsSelector; 37 | 38 | const columnPropertiesSelector = state => state.getIn(['renderProperties', 'columnProperties']); 39 | 40 | const substringSearch = (value, filter) => { 41 | if (!filter) { 42 | return true; 43 | } 44 | 45 | const filterToLower = filter.toLowerCase(); 46 | return value && value.toString().toLowerCase().indexOf(filterToLower) > -1; 47 | }; 48 | 49 | const filterable = (columnProperties, key) => { 50 | if (key === 'griddleKey') { 51 | return false; 52 | } 53 | if (columnProperties) { 54 | if (columnProperties.get(key) === undefined || 55 | columnProperties.getIn([key, 'filterable']) === false) { 56 | return false; 57 | } 58 | } 59 | return true; 60 | }; 61 | 62 | const textFilterRowSearch = (columnProperties, filter) => (row) => { 63 | return row.keySeq() 64 | .some((key) => { 65 | if (!filterable(columnProperties, key)) { 66 | return false; 67 | } 68 | return substringSearch(row.get(key), filter); 69 | }); 70 | }; 71 | 72 | const objectFilterRowSearch = (columnProperties, filter) => (row, i, data) => { 73 | return row.keySeq().every((key) => { 74 | if (!filterable(columnProperties, key)) { 75 | return true; 76 | } 77 | const keyFilter = filter.get(key); 78 | switch (typeof (keyFilter)) { 79 | case 'string': 80 | return substringSearch(row.get(key), keyFilter); 81 | break; 82 | case 'function': 83 | return keyFilter(row.get(key), i, data); 84 | break; 85 | default: 86 | return true; 87 | break; 88 | } 89 | }); 90 | }; 91 | 92 | /** Gets the data filtered by the current filter 93 | */ 94 | export const filteredDataSelector = createSelector( 95 | dataSelector, 96 | filterSelector, 97 | columnPropertiesSelector, 98 | (data, filter, columnProperties) => { 99 | if (!filter || !data) { 100 | return data; 101 | } 102 | 103 | switch (typeof (filter)) { 104 | case 'string': 105 | return data.filter(textFilterRowSearch(columnProperties, filter)); 106 | case 'object': 107 | return data.filter(objectFilterRowSearch(columnProperties, filter)); 108 | case 'function': 109 | return data.filter(filter); 110 | default: 111 | return data; 112 | } 113 | } 114 | ); 115 | 116 | 117 | /** Gets the max page size 118 | */ 119 | export const maxPageSelector = createSelector( 120 | pageSizeSelector, 121 | filteredDataSelector, 122 | (pageSize, data) => { 123 | const total = data ? data.size : 0; 124 | const calc = total / pageSize; 125 | 126 | const result = calc > Math.floor(calc) ? Math.floor(calc) + 1 : Math.floor(calc); 127 | 128 | return isFinite(result) ? result : 1; 129 | } 130 | ) 131 | 132 | export const allColumnsSelector = createSelector( 133 | dataSelector, 134 | data => (!data || data.size === 0 ? [] : data.get(0).keySeq().toJSON()) 135 | ); 136 | 137 | /** Gets the column properties objects sorted by order 138 | */ 139 | export const sortedColumnPropertiesSelector = dataSelectors.sortedColumnPropertiesSelector; 140 | 141 | /** Gets the visible columns either obtaining the sorted column properties or all columns 142 | */ 143 | export const visibleColumnsSelector = dataSelectors.visibleColumnsSelector; 144 | 145 | /** Returns whether or not this result set has more pages 146 | */ 147 | export const hasNextSelector = createSelector( 148 | currentPageSelector, 149 | maxPageSelector, 150 | (currentPage, maxPage) => (currentPage < maxPage) 151 | ); 152 | 153 | /** Returns whether or not there is a previous page to the current data 154 | */ 155 | export const hasPreviousSelector = state => (state.getIn(['pageProperties', 'currentPage']) > 1); 156 | 157 | /** Gets the data sorted by the sort function specified in render properties 158 | * if no sort method is supplied, it will use the default sort defined in griddle 159 | */ 160 | export const sortedDataSelector = createSelector( 161 | filteredDataSelector, 162 | sortPropertiesSelector, 163 | renderPropertiesSelector, 164 | sortMethodSelector, 165 | (filteredData, sortProperties, renderProperties, sortMethod = defaultSort) => { 166 | if (!sortProperties) { return filteredData; } 167 | 168 | return sortProperties.reverse().reduce((data, sortColumnOptions) => { 169 | const columnProperties = renderProperties && renderProperties.get('columnProperties').get(sortColumnOptions.get('id')); 170 | 171 | const sortFunction = (columnProperties && columnProperties.get('sortMethod')) || sortMethod; 172 | 173 | return sortFunction(data, sortColumnOptions.get('id'), sortColumnOptions.get('sortAscending')); 174 | }, filteredData); 175 | } 176 | ); 177 | 178 | /** Gets the current page of data 179 | */ 180 | export const currentPageDataSelector = createSelector( 181 | sortedDataSelector, 182 | pageSizeSelector, 183 | currentPageSelector, 184 | (sortedData, pageSize, currentPage) => { 185 | if (!sortedData) { 186 | return []; 187 | } 188 | 189 | return sortedData 190 | .skip(pageSize * (currentPage - 1)) 191 | .take(pageSize); 192 | } 193 | ) 194 | 195 | /** Get the visible data (and only the columns that are visible) 196 | */ 197 | export const visibleDataSelector = createSelector( 198 | currentPageDataSelector, 199 | visibleColumnsSelector, 200 | (currentPageData, visibleColumns) => getVisibleDataForColumns(currentPageData, visibleColumns) 201 | ); 202 | 203 | /** Gets the griddleIds for the visible rows */ 204 | export const visibleRowIdsSelector = createSelector( 205 | currentPageDataSelector, 206 | currentPageData => (currentPageData ? currentPageData.map(c => c.get('griddleKey')) : new Immutable.List()) 207 | ); 208 | 209 | /** Gets the count of visible rows */ 210 | export const visibleRowCountSelector = createSelector( 211 | visibleRowIdsSelector, 212 | (visibleRowIds) => visibleRowIds.size 213 | ); 214 | 215 | /** Gets the columns that are not currently visible 216 | */ 217 | export const hiddenColumnsSelector = createSelector( 218 | visibleColumnsSelector, 219 | allColumnsSelector, 220 | metaDataColumnsSelector, 221 | (visibleColumns, allColumns, metaDataColumns) => { 222 | const removeColumns = [...visibleColumns, ...metaDataColumns]; 223 | 224 | return allColumns.filter(c => removeColumns.indexOf(c) === -1); 225 | } 226 | ); 227 | 228 | /** Gets the column ids for the visible columns 229 | */ 230 | export const columnIdsSelector = createSelector( 231 | visibleDataSelector, 232 | renderPropertiesSelector, 233 | (visibleData, renderProperties) => { 234 | if (visibleData.size > 0) { 235 | return Object.keys(visibleData.get(0).toJSON()).map(k => 236 | renderProperties.getIn(['columnProperties', k, 'id']) || k 237 | ) 238 | } 239 | } 240 | ) 241 | 242 | /** Gets the column titles for the visible columns 243 | */ 244 | export const columnTitlesSelector = dataSelectors.columnTitlesSelector; 245 | export const cellValueSelector = dataSelectors.cellValueSelector; 246 | export const rowDataSelector = dataSelectors.rowDataSelector; 247 | export const iconsForComponentSelector = dataSelectors.iconsForComponentSelector; 248 | export const iconsByNameSelector = dataSelectors.iconsForComponentSelector; 249 | export const stylesForComponentSelector = dataSelectors.stylesForComponentSelector; 250 | export const classNamesForComponentSelector = dataSelectors.classNamesForComponentSelector; 251 | 252 | export const rowPropertiesSelector = dataSelectors.rowPropertiesSelector; 253 | export const cellPropertiesSelector = dataSelectors.cellPropertiesSelector; 254 | export const textSelector = dataSelectors.textSelector; 255 | -------------------------------------------------------------------------------- /src/plugins/position/actions/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | XY_POSITION_CHANGED, 3 | } from '../constants'; 4 | 5 | export function setScrollPosition(xScrollPosition, xScrollMax, xVisible, yScrollPosition, yScrollMax, yVisible) { 6 | return { 7 | type: XY_POSITION_CHANGED, 8 | xScrollPosition, 9 | xScrollMax, 10 | xVisible, 11 | yScrollPosition, 12 | yScrollMax, 13 | yVisible 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/plugins/position/components/Pagination.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // We're not going to be displaying a pagination bar for infinite scrolling. 4 | const PaginationComponent = (props) => ; 5 | 6 | export default PaginationComponent; 7 | -------------------------------------------------------------------------------- /src/plugins/position/components/SpacerRow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from '../../../utils/griddleConnect'; 4 | import compose from 'recompose/compose'; 5 | import mapProps from 'recompose/mapProps'; 6 | import getContext from 'recompose/getContext'; 7 | import withHandlers from 'recompose/withHandlers'; 8 | 9 | const spacerRow = compose( 10 | getContext({ 11 | selectors: PropTypes.object, 12 | }), 13 | connect((state, props) => { 14 | const { topSpacerSelector, bottomSpacerSelector } = props.selectors; 15 | const { placement } = props; 16 | 17 | return { 18 | spacerHeight: placement === 'top' ? topSpacerSelector(state, props) : bottomSpacerSelector(state, props), 19 | }; 20 | }), 21 | mapProps(props => ({ 22 | placement: props.placement, 23 | spacerHeight: props.spacerHeight, 24 | })) 25 | )(class extends Component { 26 | static propTypes = { 27 | placement: PropTypes.string, 28 | spacerHeight: PropTypes.number, 29 | } 30 | static defaultProps = { 31 | placement: 'top' 32 | } 33 | 34 | // shouldComponentUpdate(nextProps) { 35 | // const { currentPosition: oldPosition, placement: oldPlacement } = this.props; 36 | // const { currentPosition, placement } = nextProps; 37 | // 38 | // return oldPosition !== currentPosition || oldPlacement !== placement; 39 | // } 40 | 41 | render() { 42 | const { placement, spacerHeight } = this.props; 43 | let spacerRowStyle = { 44 | height: `${spacerHeight}px`, 45 | }; 46 | 47 | return ( 48 | 49 | ); 50 | } 51 | }); 52 | 53 | export default spacerRow; 54 | -------------------------------------------------------------------------------- /src/plugins/position/components/TableBody.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SpacerRow from './SpacerRow'; 3 | 4 | const TableBody = ({ rowIds, Row }) => ( 5 | 6 | 7 | { rowIds && rowIds.map(r => ) } 8 | 9 | 10 | ); 11 | 12 | export default TableBody; 13 | -------------------------------------------------------------------------------- /src/plugins/position/components/TableEnhancer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from '../../../utils/griddleConnect'; 4 | import compose from 'recompose/compose'; 5 | import mapProps from 'recompose/mapProps'; 6 | import getContext from 'recompose/getContext'; 7 | 8 | import { setScrollPosition } from '../actions'; 9 | 10 | const Table = OriginalComponent => compose( 11 | getContext({ 12 | selectors: PropTypes.object, 13 | }), 14 | connect((state, props) => { 15 | const { tableHeightSelector, tableWidthSelector, rowHeightSelector } = props.selectors; 16 | return { 17 | TableHeight: tableHeightSelector(state), 18 | TableWidth: tableWidthSelector(state), 19 | RowHeight: rowHeightSelector(state), 20 | }; 21 | }, 22 | { 23 | setScrollPosition, 24 | } 25 | ), 26 | mapProps((props) => { 27 | const { selectors, ...restProps } = props; 28 | return restProps; 29 | }) 30 | )(class extends Component { 31 | constructor(props, context) { 32 | super(props, context); 33 | 34 | this.state = { scrollTop: 0 }; 35 | } 36 | render() { 37 | const { TableHeight, TableWidth } = this.props; 38 | const scrollStyle = { 39 | 'overflow': TableHeight && TableWidth ? 'scroll' : null, 40 | 'overflowY' : TableHeight && !TableWidth ? 'scroll' : null, 41 | 'overflowX' : !TableHeight && TableWidth ? 'scroll' : null, 42 | 'height': TableHeight ? TableHeight : null, 43 | 'width': TableWidth ? TableWidth : null, 44 | 'display': 'inline-block' 45 | }; 46 | 47 | return ( 48 |
this._scrollable = ref} style={scrollStyle} onScroll={this._scroll}> 49 | 50 |
51 | ); 52 | } 53 | 54 | _scroll = () => { 55 | const { setScrollPosition, RowHeight } = this.props; 56 | const { scrollTop } = this.state; 57 | 58 | if (this._scrollable && Math.abs(this._scrollable.scrollTop - scrollTop) >= RowHeight) { 59 | setScrollPosition( 60 | this._scrollable.scrollLeft, 61 | this._scrollable.scrollWidth, 62 | this._scrollable.clientWidth, 63 | this._scrollable.scrollTop, 64 | this._scrollable.scrollHeight, 65 | this._scrollable.clientHeight 66 | ); 67 | this.setState({ scrollTop: this._scrollable.scrollTop }); 68 | } 69 | } 70 | }); 71 | 72 | export default Table; 73 | -------------------------------------------------------------------------------- /src/plugins/position/components/index.js: -------------------------------------------------------------------------------- 1 | import Pagination from './Pagination'; 2 | import SpacerRow from './SpacerRow'; 3 | import TableBody from './TableBody'; 4 | import TableEnhancer from './TableEnhancer'; 5 | 6 | export default { 7 | Pagination, 8 | SpacerRow, 9 | TableBody, 10 | TableEnhancer, 11 | } 12 | -------------------------------------------------------------------------------- /src/plugins/position/constants/index.js: -------------------------------------------------------------------------------- 1 | export const XY_POSITION_CHANGED = 'XY_POSITION_CHANGED'; 2 | -------------------------------------------------------------------------------- /src/plugins/position/index.js: -------------------------------------------------------------------------------- 1 | import components from './components'; 2 | import * as reducer from './reducers'; 3 | import initialState from './initial-state'; 4 | import * as selectors from './selectors'; 5 | 6 | const PositionPlugin = (config) => { 7 | return { 8 | initialState: { 9 | ...initialState, 10 | positionSettings: Object.assign({}, initialState.positionSettings, config), 11 | }, 12 | components, 13 | reducer, 14 | selectors, 15 | }; 16 | } 17 | 18 | export default PositionPlugin; 19 | -------------------------------------------------------------------------------- /src/plugins/position/initial-state.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | renderedData: [], 3 | currentPosition: { 4 | height: 500, 5 | width: 500, 6 | xScrollChangePosition: 0, 7 | yScrollChangePosition: 0, 8 | renderedStartDisplayIndex: 0, 9 | renderedEndDisplayIndex: 16, 10 | visibleDataLength: 16 11 | }, 12 | positionSettings: { 13 | // The height of the table 14 | tableHeight: '70%', 15 | // The width of the table 16 | tableWidth: null, 17 | // The minimum row height 18 | rowHeight: 30, 19 | // The minimum column width 20 | defaultColumnWidth: null, 21 | // Whether or not the header should be fixed 22 | fixedHeader: true, 23 | // Disable pointer events while scrolling to improve performance 24 | disablePointerEvents: false, 25 | }, 26 | }; 27 | 28 | export default initialState; 29 | -------------------------------------------------------------------------------- /src/plugins/position/reducers/__tests__/indexTest.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import Immutable from 'immutable'; 3 | 4 | import { 5 | XY_POSITION_CHANGED 6 | } from '../index'; 7 | 8 | test('xy_position_changed sets position to 0 when not available', t => { 9 | const state = new Immutable.Map(); 10 | const outputState = XY_POSITION_CHANGED(state, {}); 11 | 12 | t.deepEqual(outputState.toJSON(), { 13 | currentPosition: { 14 | xScrollChangePosition: 0, 15 | yScrollChangePosition: 0, 16 | height: 0, 17 | width: 0 18 | } 19 | }); 20 | }); 21 | 22 | test('xy_position_changed sets position to action information', t => { 23 | const state = new Immutable.Map(); 24 | const outputState = XY_POSITION_CHANGED(state, { 25 | yScrollPosition: 10, 26 | xScrollPosition: 20, 27 | height: 30, 28 | width: 40 29 | }); 30 | 31 | t.deepEqual(outputState.toJSON(), { 32 | currentPosition: { 33 | xScrollChangePosition: 20, 34 | yScrollChangePosition: 10, 35 | height: 30, 36 | width: 40 37 | } 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/plugins/position/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { setCurrentPosition, updatePositionProperties } from '../utils'; 2 | 3 | export function XY_POSITION_CHANGED(state, action) { 4 | const height = state.getIn(['currentPosition', 'height']) || 0; 5 | const width = state.getIn(['currentPosition', 'width']) || 0; 6 | 7 | return state 8 | .setIn(['currentPosition', 'xScrollChangePosition'], action.xScrollPosition || 0) 9 | .setIn(['currentPosition', 'yScrollChangePosition'], action.yScrollPosition || 0) 10 | .setIn(['currentPosition', 'height'], action.height || height) 11 | .setIn(['currentPosition', 'width'], action.width || width); 12 | } 13 | 14 | export function GRIDDLE_SET_FILTER_AFTER(state, action, helpers) { 15 | return state.setIn(['currentPosition', 'xScrollChangePosition'], 0) 16 | .setIn(['currentPosition', 'yScrollChangePosition'], 0); 17 | } 18 | -------------------------------------------------------------------------------- /src/plugins/position/selectors/__tests__/indexTest.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import Immutable from 'immutable'; 3 | 4 | import { 5 | visibleRecordCountSelector 6 | } from '../index'; 7 | 8 | test('visible record count selector', test => { 9 | const state = new Immutable.fromJS({ 10 | positionSettings: { 11 | rowHeight: 50, 12 | height: 600 13 | }, 14 | currentPosition: { 15 | height: 600 16 | }, 17 | }); 18 | 19 | test.is(visibleRecordCountSelector(state), 12); 20 | }); 21 | 22 | -------------------------------------------------------------------------------- /src/plugins/position/selectors/index.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | import { sortedDataSelector, visibleColumnsSelector } from '../../local/selectors/localSelectors'; 4 | 5 | export const positionSettingsSelector = state => state.get('positionSettings'); 6 | export const rowHeightSelector = state => state.getIn(['positionSettings', 'rowHeight']); 7 | export const currentHeightSelector = state => state.getIn(['currentPosition', 'height']); 8 | 9 | export const tableHeightSelector = state => state.getIn(['positionSettings', 'tableHeight']); 10 | export const tableWidthSelector = state => state.getIn(['positionSettings', 'tableWidth']); 11 | 12 | // From what i can tell from the original virtual scrolling plugin... 13 | // 1. We want to get the visible record count 14 | // 2. Get the size of the dataset we're working with (whether thats local or remote) 15 | // 3. Figure out the renderedStart and End display index 16 | // 4. Show only the records that'd fall in the render indexes 17 | 18 | /** Gets the number of viisble rows based on the height of the container and the rowHeight 19 | */ 20 | export const visibleRecordCountSelector = createSelector( 21 | rowHeightSelector, 22 | currentHeightSelector, 23 | (rowHeight, currentHeight) => { 24 | return Math.ceil(currentHeight / rowHeight); 25 | } 26 | ); 27 | 28 | export const visibleDataLengthSelector = createSelector( 29 | sortedDataSelector, 30 | (sortedData) => { 31 | return sortedData.size; 32 | } 33 | ); 34 | 35 | export const hoizontalScrollChangeSelector = state => state.getIn(['currentPosition', 'xScrollChangePosition']) || 0; 36 | export const verticalScrollChangeSelector = state => state.getIn(['currentPosition', 'yScrollChangePosition']) || 0; 37 | 38 | export const startIndexSelector = createSelector( 39 | verticalScrollChangeSelector, 40 | rowHeightSelector, 41 | visibleRecordCountSelector, 42 | (verticalScrollPosition, rowHeight, visibleRecordCount) => { 43 | // Inspired by : http://jsfiddle.net/vjeux/KbWJ2/9/ 44 | return Math.max(0, Math.floor(Math.floor(verticalScrollPosition / rowHeight) - visibleRecordCount * 0.25)); 45 | } 46 | ); 47 | 48 | export const endIndexSelector = createSelector( 49 | startIndexSelector, 50 | visibleRecordCountSelector, 51 | visibleDataLengthSelector, 52 | (startDisplayIndex, visibleRecordCount, visibleDataLength) => { 53 | // Inspired by : http://jsfiddle.net/vjeux/KbWJ2/9/ 54 | return Math.min(Math.floor(startDisplayIndex + visibleRecordCount * 2), visibleDataLength - 1) + 1; 55 | } 56 | ); 57 | 58 | export const topSpacerSelector = createSelector( 59 | rowHeightSelector, 60 | startIndexSelector, 61 | (rowHeight, startIndex) => { 62 | return rowHeight * startIndex; 63 | } 64 | ); 65 | 66 | export const bottomSpacerSelector = createSelector( 67 | rowHeightSelector, 68 | visibleDataLengthSelector, 69 | endIndexSelector, 70 | (rowHeight, visibleDataLength, endIndex) => { 71 | return rowHeight * (visibleDataLength - endIndex); 72 | } 73 | ); 74 | 75 | /** Gets the current page of data 76 | * Won't be memoized :cry: 77 | */ 78 | export const currentPageDataSelector = (...args) => { 79 | return createSelector( 80 | sortedDataSelector, 81 | startIndexSelector, 82 | endIndexSelector, 83 | (sortedData, startDisplayIndex, endDisplayIndex) => { 84 | return sortedData 85 | .skip(startDisplayIndex) 86 | .take(endDisplayIndex - startDisplayIndex); 87 | } 88 | )(...args); 89 | }; 90 | 91 | /** Get the visible data (and only the columns that are visible) 92 | */ 93 | export const visibleDataSelector = createSelector( 94 | currentPageDataSelector, 95 | visibleColumnsSelector, 96 | (currentPageData, visibleColumns) => getVisibleDataForColumns(currentPageData, visibleColumns) 97 | ); 98 | 99 | /** Gets the griddleIds for the visible rows */ 100 | export const visibleRowIdsSelector = createSelector( 101 | currentPageDataSelector, 102 | (currentPageData) => currentPageData.map(c => c.get('griddleKey')) 103 | ); 104 | -------------------------------------------------------------------------------- /src/plugins/position/utils.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import initialState from './initial-state'; 3 | 4 | export function shouldUpdateDrawnRows(action, state) { 5 | const height = state.getIn(['currentPosition', 'height']); 6 | const width = state.getIn(['currentPosition', 'width']); 7 | 8 | // If the containers have changed size, update drawn rows. 9 | if (height != action.yVisible || width != action.xVisible) 10 | return true; 11 | 12 | const yScrollChangePosition = state.getIn(['currentPosition', 'yScrollChangePosition']); 13 | const rowHeight = state.getIn(['positionConfig', 'rowHeight']); 14 | 15 | // Get the current visible record count. 16 | const visibleRecordCount = getVisibleRecordCount(state); 17 | 18 | // Get the count of rendered rows. 19 | const startDisplayIndex = state.getIn(['currentPosition', 'renderedStartDisplayIndex']); 20 | const endDisplayIndex = state.getIn(['currentPosition', 'renderedEndDisplayIndex']); 21 | const renderedRecordCount = endDisplayIndex - startDisplayIndex; 22 | 23 | // Calculate the height of a third of the difference. 24 | const rowDifferenceHeight = rowHeight * (renderedRecordCount - visibleRecordCount) / 3; 25 | 26 | return Math.abs(action.yScrollPosition - yScrollChangePosition) >= rowDifferenceHeight; 27 | } 28 | 29 | export function setCurrentPosition(state, yScrollPosition, xScrollPosition) { 30 | return state 31 | .setIn(['currentPosition', 'yScrollChangePosition'], yScrollPosition) 32 | .setIn(['currentPosition', 'xScrollChangePosition'], xScrollPosition); 33 | } 34 | 35 | export function updatePositionProperties(action, state, force) { 36 | if (!action.force && !shouldUpdateDrawnRows(action, state) && !Immutable.is(state.get('currentPosition'), initialState().get('currentPosition'))) { 37 | return state; // Indicate that this shouldn't result in an emit. 38 | } 39 | 40 | const sizeUpdatedState = state.setIn(['currentPosition', 'height'], action.yVisible ? 41 | action.yVisible * 1.2 : 42 | state.getIn(['currentPosition', 'height']) 43 | ) 44 | .setIn(['currentPosition', 'width'], action.xVisible || state.getIn(['currentPosition', 'width'])); 45 | 46 | const visibleRecordCount = getVisibleRecordCount(sizeUpdatedState); 47 | const visibleDataLength = helpers.getDataSetSize(sizeUpdatedState); 48 | 49 | const rowHeight = sizeUpdatedState.getIn(['positionConfig', 'rowHeight']); 50 | 51 | const verticalScrollPosition = action.yScrollPosition || 0; 52 | const horizontalScrollPosition = action.xScrollPosition || 0; 53 | 54 | // Inspired by : http://jsfiddle.net/vjeux/KbWJ2/9/ 55 | let renderedStartDisplayIndex = Math.max(0, Math.floor(Math.floor(verticalScrollPosition / rowHeight) - visibleRecordCount * 0.25)); 56 | let renderedEndDisplayIndex = Math.min(Math.floor(renderedStartDisplayIndex + visibleRecordCount * 2), visibleDataLength - 1) + 1; 57 | 58 | return setCurrentPosition(sizeUpdatedState, verticalScrollPosition, horizontalScrollPosition) 59 | .setIn(['currentPosition', 'renderedStartDisplayIndex'], renderedStartDisplayIndex) 60 | .setIn(['currentPosition', 'renderedEndDisplayIndex'], renderedEndDisplayIndex) 61 | .setIn(['currentPosition', 'visibleDataLength'], visibleDataLength); 62 | } 63 | 64 | export function updateRenderedData(state) { 65 | const startDisplayIndex = state.getIn(['currentPosition', 'renderedStartDisplayIndex']); 66 | const columns = helpers.getDataColumns(state, data); 67 | const data = helpers.getDataSet(state); 68 | 69 | return state 70 | .set('renderedData', helpers.getVisibleDataColumns(data 71 | .skip(startDisplayIndex) 72 | .take(state.getIn(['currentPosition', 'renderedEndDisplayIndex']) - startDisplayIndex), columns)); 73 | } 74 | -------------------------------------------------------------------------------- /src/reducers/__tests__/dataReducerTest.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import Immutable from 'immutable'; 3 | 4 | import * as reducers from '../dataReducer'; 5 | import constants from '../../constants'; 6 | 7 | test('initializes data', test => { 8 | const initializedState = reducers.GRIDDLE_INITIALIZED({ 9 | renderProperties: { 10 | one: 'one', 11 | two: 'two' 12 | } 13 | }); 14 | 15 | test.deepEqual(initializedState.get('renderProperties').toJSON(), { 16 | one: 'one', 17 | two: 'two' 18 | }); 19 | }); 20 | 21 | test('creates column properties if none exist for data', test => { 22 | const state = reducers.GRIDDLE_INITIALIZED({ 23 | data: [ 24 | {one: 1, two: 2, three: 3}, 25 | {one: 11, two: 22, three: 33} 26 | ], 27 | renderProperties: {}, 28 | }); 29 | 30 | test.deepEqual(state.getIn(['renderProperties', 'columnProperties']).toJSON(), { 31 | one: { id: 'one', visible: true }, 32 | two: { id: 'two', visible: true }, 33 | three: { id: 'three', visible: true } 34 | }); 35 | }); 36 | 37 | test('does not adjust column properties if exists already', test => { 38 | const state = reducers.GRIDDLE_INITIALIZED({ 39 | data: [ 40 | { one: 1, two: 2, three: 3}, 41 | { one: 11, two: 22, three: 33 } 42 | ], 43 | renderProperties: { 44 | columnProperties: { 45 | one: { id: 'one', visible: true } 46 | } 47 | } 48 | }); 49 | 50 | test.deepEqual(state.getIn(['renderProperties', 'columnProperties']).toJSON(), { 51 | one: { id: 'one', visible: true } 52 | }); 53 | }); 54 | 55 | [undefined, null].map(data => 56 | test(`does not adjust column properties if data is ${data}`, (assert) => { 57 | const state = reducers.GRIDDLE_INITIALIZED({ 58 | data, 59 | renderProperties: { 60 | columnProperties: { 61 | one: { id: 'one', visible: true } 62 | } 63 | } 64 | }); 65 | 66 | assert.deepEqual(state.getIn(['renderProperties', 'columnProperties']).toJSON(), { 67 | one: { id: 'one', visible: true } 68 | }); 69 | }) 70 | ); 71 | 72 | test('sets data', test => { 73 | const reducedState = reducers.GRIDDLE_LOADED_DATA(Immutable.fromJS({ renderProperties: {} }), 74 | { type: 'GRIDDLE_LOADED_DATA', data: [ 75 | {name: "one"}, 76 | {name: "two"} 77 | ]} 78 | ); 79 | 80 | test.deepEqual(reducedState.toJSON(), { 81 | data: [ 82 | {name: "one", griddleKey: 0}, 83 | {name: "two", griddleKey: 1} 84 | ], 85 | renderProperties: {}, 86 | lookup: { 0: 0, 1: 1 }, 87 | loading: false 88 | }); 89 | }); 90 | 91 | test('sets the correct page number', test => { 92 | const state = reducers.GRIDDLE_SET_PAGE(new Immutable.Map(), { 93 | pageNumber: 2 94 | }); 95 | 96 | test.is(state.getIn(['pageProperties', 'currentPage']), 2); 97 | }); 98 | 99 | test('sets page size', test => { 100 | const state = reducers.GRIDDLE_SET_PAGE_SIZE( new Immutable.Map(), { 101 | pageSize: 11 102 | }); 103 | 104 | test.is(state.getIn(['pageProperties', 'pageSize']), 11); 105 | }); 106 | 107 | test('sets filter', test => { 108 | const state = reducers.GRIDDLE_SET_FILTER(new Immutable.Map(), { 109 | filter: 'onetwothree' 110 | }); 111 | 112 | test.is(state.get('filter'), 'onetwothree'); 113 | }); 114 | 115 | test('sets sort columns', test => { 116 | const state = reducers.GRIDDLE_SET_SORT(new Immutable.Map(), { 117 | sortProperties: [ 118 | { id: 'one', sortAscending: true }, 119 | { id: 'two', sortAscending: false } 120 | ] 121 | }); 122 | 123 | test.deepEqual(state.get('sortProperties').toJSON(), [ 124 | { id: 'one', sortAscending: true }, 125 | { id: 'two', sortAscending: false } 126 | ]); 127 | }); 128 | 129 | test('sets settings visibility', test => { 130 | const initialState = Immutable.fromJS({ 131 | }); 132 | 133 | // should be true when showSettings isn't in state 134 | const trueState = reducers.GRIDDLE_TOGGLE_SETTINGS(initialState); 135 | test.is(trueState.get('showSettings'), true); 136 | 137 | const falseState = reducers.GRIDDLE_TOGGLE_SETTINGS(trueState); 138 | test.is(falseState.get('showSettings'), false); 139 | }) 140 | 141 | test('toggle column changes column properties visibility', test => { 142 | const initialState = Immutable.fromJS({ 143 | renderProperties: { 144 | columnProperties: { 145 | name: { id: 'name', visible: false } 146 | } 147 | } 148 | }); 149 | 150 | const state = reducers.GRIDDLE_TOGGLE_COLUMN(initialState, { columnId: 'name' }); 151 | test.deepEqual(state.getIn(['renderProperties', 'columnProperties', 'name']).toJSON(), { id: 'name', visible: true }); 152 | }) 153 | 154 | test('toggle column sets true when no columnProperty for column but other columnProperties exist', test => { 155 | const initialState = Immutable.fromJS({ 156 | renderProperties: { 157 | columnProperties: { 158 | name: { id: 'name', visible: false } 159 | } 160 | } 161 | }); 162 | 163 | const state = reducers.GRIDDLE_TOGGLE_COLUMN(initialState, { columnId: 'state' }); 164 | test.deepEqual(state.getIn(['renderProperties', 'columnProperties', 'state']).toJSON(), { id: 'state', visible: true }); 165 | }); 166 | 167 | test('toggle column works when there is no visible property', (t) => { 168 | const initialState = Immutable.fromJS({ 169 | renderProperties: { 170 | columnProperties: { 171 | name: { id: 'name' } 172 | } 173 | } 174 | }); 175 | 176 | // if column isn't in renderProperties->column properties, we should set visible to true 177 | const state = reducers.GRIDDLE_TOGGLE_COLUMN(initialState, { columnId: 'state' }); 178 | t.deepEqual(state.getIn(['renderProperties', 'columnProperties', 'state']).toJSON(), { id: 'state', visible: true }); 179 | 180 | // if column is in reducerProperties but has no visible property should set to false 181 | const otherState = reducers.GRIDDLE_TOGGLE_COLUMN(initialState, { columnId: 'name' }); 182 | t.deepEqual(otherState.getIn(['renderProperties', 'columnProperties', 'name']).toJSON(), { id: 'name', visible: false }); 183 | 184 | 185 | }); 186 | 187 | test('update state merges non-data', (t) => { 188 | const initialState = Immutable.fromJS({ 189 | changed: 1, 190 | unchanged: 2, 191 | nested: { 192 | changed: 3, 193 | unchanged: 4, 194 | }, 195 | data: [], 196 | lookup: {}, 197 | }); 198 | const newState = { 199 | changed: -1, 200 | nested: { 201 | changed: -3, 202 | }, 203 | }; 204 | 205 | const state = reducers.GRIDDLE_UPDATE_STATE(initialState, { newState }); 206 | 207 | t.deepEqual(state.toJSON(), { 208 | changed: -1, 209 | unchanged: 2, 210 | nested: { 211 | changed: -3, 212 | unchanged: 4, 213 | }, 214 | data: [], 215 | lookup: {}, 216 | }); 217 | }); 218 | 219 | test('update state transforms data', (t) => { 220 | const initialState = Immutable.fromJS({ 221 | unchanged: 2, 222 | nested: { 223 | unchanged: 4, 224 | }, 225 | data: [ 226 | {name: "one", griddleKey: 0}, 227 | {name: "two", griddleKey: 1}, 228 | ], 229 | lookup: { 0: 0, 1: 1 }, 230 | }); 231 | const newState = { 232 | data: [ 233 | { name: 'uno' }, 234 | { name: 'dos' }, 235 | { name: 'tre' }, 236 | ] 237 | }; 238 | 239 | const state = reducers.GRIDDLE_UPDATE_STATE(initialState, { newState }); 240 | 241 | t.deepEqual(state.toJSON(), { 242 | unchanged: 2, 243 | nested: { 244 | unchanged: 4, 245 | }, 246 | data: [ 247 | {name: "uno", griddleKey: 0}, 248 | {name: "dos", griddleKey: 1}, 249 | {name: "tre", griddleKey: 2}, 250 | ], 251 | lookup: { 0: 0, 1: 1, 2: 2 }, 252 | }); 253 | }); 254 | -------------------------------------------------------------------------------- /src/reducers/dataReducer.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | 3 | /* 4 | * State 5 | * ------------------ 6 | * data {Immutable.List} - the data that the grid is displaying 7 | * loading {boolean} - is the data currently loading 8 | * renderProperties {Immutable.Map} - the properties that determine how the grid should be displayed 9 | * pageProperties {Immutable.Map} - the metadata for paging information 10 | * .-- currentPage {int} - The current, visible page 11 | * .-- pageSize {int} - The number of records to display 12 | * sortProperties {Immutable.List} - the metadata surrounding sort 13 | * .-- id {string} - the column id 14 | * .-- sortAscending {boolean} - the direction of the sort. Index matches that of sortColumns 15 | **/ 16 | import { 17 | addColumnPropertiesWhenNoneExist, 18 | transformData, 19 | } from '../utils/dataUtils'; 20 | 21 | function isColumnVisible(state, columnId) { 22 | const hasRenderProperty = state.getIn(['renderProperties', 'columnProperties', columnId]); 23 | const currentlyVisibleProperty = state.getIn(['renderProperties', 'columnProperties', columnId, 'visible']); 24 | 25 | // if there is a render property and visible is not set, visible is true 26 | if (hasRenderProperty && currentlyVisibleProperty === undefined) { 27 | return true; 28 | } 29 | 30 | // if there is no render property currently and visible is not set 31 | if (!hasRenderProperty && currentlyVisibleProperty === undefined) { 32 | return false; 33 | } 34 | 35 | return currentlyVisibleProperty; 36 | } 37 | 38 | 39 | /** Sets the default render properties 40 | * @param {Immutable} state- Immutable previous state object 41 | * @param {Object} action - The action object to work with 42 | * 43 | * TODO: Consider renaming this to be more in line with what it's actually doing (setting render properties) 44 | */ 45 | export function GRIDDLE_INITIALIZED(initialState) { 46 | let tempState = Object.assign({}, initialState); 47 | tempState = addColumnPropertiesWhenNoneExist(tempState); 48 | //TODO: could probably make this more efficient by removing data 49 | // making the rest of the properties initial state and 50 | // setting the mapped data on the new initial state immutable object 51 | if (initialState.data && 52 | initialState.data.length > 0) { 53 | const transformedData = transformData(initialState.data, initialState.renderProperties); 54 | tempState.data = transformedData.data; 55 | tempState.lookup = transformedData.lookup; 56 | } 57 | 58 | return Immutable.fromJS(tempState); 59 | } 60 | 61 | /** Sets the griddle data 62 | * @param {Immutable} state- Immutable previous state object 63 | * @param {Object} action - The action object to work with 64 | */ 65 | export function GRIDDLE_LOADED_DATA(state, action) { 66 | const transformedData = transformData(action.data, state.get('renderProperties').toJSON()); 67 | 68 | return state 69 | .set('data', transformedData.data) 70 | .set('lookup', transformedData.lookup) 71 | .set('loading', false); 72 | } 73 | 74 | /** Sets the current page size 75 | * @param {Immutable} state- Immutable previous state object 76 | * @param {Object} action - The action object to work with 77 | */ 78 | export function GRIDDLE_SET_PAGE_SIZE(state, action) { 79 | return state 80 | .setIn(['pageProperties', 'currentPage'], 1) 81 | .setIn(['pageProperties', 'pageSize'], action.pageSize); 82 | } 83 | 84 | /** Sets the current page 85 | * @param {Immutable} state- Immutable previous state object 86 | * @param {Object} action - The action object to work with 87 | */ 88 | export function GRIDDLE_SET_PAGE(state, action) { 89 | return state.setIn(['pageProperties', 'currentPage'], action.pageNumber); 90 | } 91 | 92 | /** Sets the filter 93 | * @param {Immutable} state- Immutable previous state object 94 | * @param {Object} action - The action object to work with 95 | */ 96 | export function GRIDDLE_SET_FILTER(state, action) { 97 | return state.set('filter', action.filter); 98 | } 99 | 100 | /** Sets sort properties 101 | * @param {Immutable} state- Immutable previous state object 102 | * @param {Object} action - The action object to work with 103 | */ 104 | export function GRIDDLE_SET_SORT(state, action) { 105 | // turn this into an array if it's not already 106 | const sortProperties = action.sortProperties.hasOwnProperty('length') ? 107 | action.sortProperties : 108 | [action.sortProperties]; 109 | 110 | return state.set('sortProperties', new Immutable.fromJS(sortProperties)); 111 | } 112 | 113 | /** Sets the settings visibility to true / false depending on the current property 114 | */ 115 | export function GRIDDLE_TOGGLE_SETTINGS(state, action) { 116 | // if undefined treat as if it's false 117 | const showSettings = state.get('showSettings') || false; 118 | 119 | return state.set('showSettings', !showSettings); 120 | } 121 | 122 | export function GRIDDLE_TOGGLE_COLUMN(state, action) { 123 | // flips the visible state if the column property exists 124 | const currentlyVisible = isColumnVisible(state, action.columnId); 125 | 126 | return state.getIn(['renderProperties', 'columnProperties', action.columnId]) ? 127 | state.setIn(['renderProperties', 'columnProperties', action.columnId, 'visible'], 128 | !currentlyVisible) : 129 | 130 | // if the columnProperty doesn't exist, create a new one and set the property to true 131 | state.setIn(['renderProperties', 'columnProperties', action.columnId], 132 | new Immutable.Map({ id: action.columnId, visible: true })); 133 | } 134 | 135 | const defaultRenderProperties = Immutable.fromJS({}); 136 | export function GRIDDLE_UPDATE_STATE(state, action) { 137 | const { data, ...newState } = action.newState; 138 | 139 | var mergedState = state.mergeDeep(Immutable.fromJS(newState)); 140 | if (!data) { 141 | return mergedState; 142 | } 143 | 144 | const renderProperties = state.get('renderProperties', defaultRenderProperties).toJSON(); 145 | const transformedData = transformData(data, renderProperties); 146 | 147 | return mergedState 148 | .set('data', transformedData.data) 149 | .set('lookup', transformedData.lookup); 150 | } 151 | -------------------------------------------------------------------------------- /src/selectors/__tests__/dataSelectorsTest.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import Immutable from 'immutable'; 3 | 4 | import * as selectors from '../dataSelectors'; 5 | 6 | test('gets data', (test) => { 7 | const state = new Immutable.Map().set('data', 'hi'); 8 | test.is(selectors.dataSelector(state), 'hi'); 9 | }); 10 | 11 | test('gets pageSize', (test) => { 12 | const state = new Immutable.Map().setIn(['pageProperties', 'pageSize'], 7); 13 | test.is(selectors.pageSizeSelector(state), 7); 14 | }); 15 | 16 | /* currentPageSelector */ 17 | test('gets current page', (test) => { 18 | const state = new Immutable.Map().setIn(['pageProperties', 'currentPage'], 3); 19 | test.is(selectors.currentPageSelector(state), 3); 20 | }); 21 | 22 | /* recordCountSelector */ 23 | test('gets record count', (test) => { 24 | const state = new Immutable.Map().setIn( 25 | ['pageProperties', 'recordCount'], 26 | 10 27 | ); 28 | test.is(selectors.recordCountSelector(state), 10); 29 | }); 30 | 31 | /* hasNextSelector */ 32 | test('hasNext gets true when there are more pages', (test) => { 33 | const state = Immutable.fromJS({ 34 | pageProperties: { 35 | recordCount: 20, 36 | pageSize: 7, 37 | currentPage: 2 38 | } 39 | }); 40 | 41 | test.true(selectors.hasNextSelector(state)); 42 | }); 43 | 44 | test('hasNext gets false when there are not more pages', (test) => { 45 | const state = Immutable.fromJS({ 46 | pageProperties: { 47 | recordCount: 20, 48 | pageSize: 11, 49 | currentPage: 2 50 | } 51 | }); 52 | 53 | test.false(selectors.hasNextSelector(state)); 54 | }); 55 | 56 | /* this is just double checking that we're not showing next when on record 11-20 of 20 */ 57 | test('hasNext gets false when on the last page', (test) => { 58 | const state = Immutable.fromJS({ 59 | pageProperties: { 60 | recordCount: 20, 61 | pageSize: 10, 62 | currentPage: 2 63 | } 64 | }); 65 | 66 | test.false(selectors.hasNextSelector(state)); 67 | }); 68 | 69 | /* hasPreviousSelector */ 70 | test('has previous gets true when there are prior pages', (test) => { 71 | const state = new Immutable.Map().setIn(['pageProperties', 'currentPage'], 2); 72 | test.true(selectors.hasPreviousSelector(state)); 73 | }); 74 | 75 | test.skip('has previous gets false when there are not prior pages', (test) => { 76 | const state = new Immutable.Map().setIn(['pageProperties', 'currentPage'], 2); 77 | test.true(selectors.hasPreviousSelector(state)); 78 | }); 79 | 80 | /* currentPageSelector */ 81 | test('gets default current page', (test) => { 82 | const state = new Immutable.Map().setIn(['pageProperties', 'currentPage'], 1); 83 | test.false(selectors.hasPreviousSelector(state)); 84 | }); 85 | 86 | /* maxPageSelector */ 87 | test('gets max page', (test) => { 88 | const state = Immutable.fromJS({ 89 | pageProperties: { 90 | recordCount: 20, 91 | pageSize: 10, 92 | currentPage: 2 93 | } 94 | }); 95 | 96 | test.is(selectors.maxPageSelector(state), 2); 97 | 98 | //ensure that we get 2 pages when full pageSize would not be displayed on next page 99 | const otherState = state.setIn(['pageProperties', 'pageSize'], 11); 100 | test.is(selectors.maxPageSelector(otherState), 2); 101 | 102 | //when pageSize === recordCount should have 1 page 103 | const onePageState = state.setIn(['pageProperties', 'pageSize'], 20); 104 | test.is(selectors.maxPageSelector(onePageState), 1); 105 | 106 | //when there are no records, there should be 0 pages 107 | const noDataState = state.setIn(['pageProperties', 'recordCount'], 0); 108 | test.is(selectors.maxPageSelector(noDataState), 0); 109 | }); 110 | 111 | /* filterSelector */ 112 | test('gets filter when present', (test) => { 113 | const state = new Immutable.Map().set('filter', 'some awesome filter'); 114 | test.is(selectors.filterSelector(state), 'some awesome filter'); 115 | }); 116 | 117 | test('gets empty string when no filter present', (test) => { 118 | const state = new Immutable.Map(); 119 | test.is(selectors.filterSelector(state), ''); 120 | }); 121 | 122 | /* sortColumnsSelector */ 123 | test('gets empty array for sortColumns when none specified', (test) => { 124 | const state = new Immutable.Map(); 125 | test.deepEqual(selectors.sortColumnsSelector(state), []); 126 | }); 127 | 128 | test('gets sort column array when specified', (test) => { 129 | const state = new Immutable.Map().set('sortColumns', [ 130 | { column: 'one', sortAscending: true }, 131 | { column: 'two', sortAscending: true }, 132 | { column: 'three', sortAscending: true } 133 | ]); 134 | 135 | test.deepEqual(selectors.sortColumnsSelector(state), [ 136 | { column: 'one', sortAscending: true }, 137 | { column: 'two', sortAscending: true }, 138 | { column: 'three', sortAscending: true } 139 | ]); 140 | }); 141 | 142 | /* allColumnsSelector */ 143 | test('allColumnsSelector: gets all columns', (test) => { 144 | const data = Immutable.fromJS([ 145 | { one: 'one', two: 'two', three: 'three', four: 'four' } 146 | ]); 147 | 148 | const state = new Immutable.Map().set('data', data); 149 | 150 | test.deepEqual(selectors.allColumnsSelector(state), [ 151 | 'one', 152 | 'two', 153 | 'three', 154 | 'four' 155 | ]); 156 | }); 157 | 158 | test('allColumnsSelector: gets empty array when no data present', (test) => { 159 | const state = new Immutable.Map(); 160 | 161 | test.deepEqual(selectors.allColumnsSelector(state), []); 162 | }); 163 | 164 | test('allColumnsSelector: gets empty array when data is empty', (test) => { 165 | const state = new Immutable.Map().set('data', new Immutable.List()); 166 | test.deepEqual(selectors.allColumnsSelector(state), []); 167 | }); 168 | 169 | test('allColumnsSelector accounts for made up columns', (test) => { 170 | // this is to catch the case where someone has a column that they added through column 171 | // definitions and something that's not in the data 172 | const state = new Immutable.fromJS({ 173 | data: [{ one: 'one', two: 'two', three: 'three' }], 174 | renderProperties: { 175 | columnProperties: { 176 | something: { id: 'one', title: 'One' } 177 | } 178 | } 179 | }); 180 | 181 | test.deepEqual(selectors.allColumnsSelector(state), [ 182 | 'one', 183 | 'two', 184 | 'three', 185 | 'something' 186 | ]); 187 | }); 188 | 189 | test('iconByNameSelector gets given icon', (test) => { 190 | const state = new Immutable.fromJS({ 191 | styleConfig: { 192 | icons: { 193 | one: 'yo' 194 | } 195 | } 196 | }); 197 | 198 | test.is(selectors.iconByNameSelector(state, { name: 'one' }), 'yo'); 199 | }); 200 | 201 | test('iconByNameSelector gets undefined when icon not present in collection', (test) => { 202 | const state = new Immutable.fromJS({ 203 | styles: { 204 | icons: { 205 | one: 'yo' 206 | } 207 | } 208 | }); 209 | 210 | test.is(selectors.iconByNameSelector(state, { name: 'two' }), undefined); 211 | }); 212 | 213 | test('classNamesForComponentSelector gets given class', (test) => { 214 | const state = new Immutable.fromJS({ 215 | styleConfig: { 216 | classNames: { 217 | one: 'yo' 218 | } 219 | } 220 | }); 221 | 222 | test.is(selectors.classNamesForComponentSelector(state, 'one'), 'yo'); 223 | }); 224 | 225 | test('classNameForComponentSelector gets undefined when icon not present in collection', (test) => { 226 | const state = new Immutable.fromJS({ 227 | styleConfig: { 228 | classNames: { 229 | one: 'yo' 230 | } 231 | } 232 | }); 233 | 234 | test.is(selectors.classNamesForComponentSelector(state, 'two'), undefined); 235 | }); 236 | 237 | test('isSettingsEnabled returns true when not set', (test) => { 238 | const state = new Immutable.fromJS({}); 239 | 240 | test.is(selectors.isSettingsEnabledSelector(state), true); 241 | }); 242 | 243 | test('isSettingsEnabled returns the value that was set', (test) => { 244 | const enabledState = new Immutable.fromJS({ enableSettings: true }); 245 | const disabledState = new Immutable.fromJS({ enableSettings: false }); 246 | 247 | test.is(selectors.isSettingsEnabledSelector(enabledState), true); 248 | test.is(selectors.isSettingsEnabledSelector(disabledState), false); 249 | }); 250 | 251 | test('gets text from state', (test) => { 252 | const state = new Immutable.fromJS({ 253 | textProperties: { 254 | one: 'one two three' 255 | } 256 | }); 257 | 258 | test.is(selectors.textSelector(state, { key: 'one' }), 'one two three'); 259 | }); 260 | 261 | test('gets metadata columns', (test) => { 262 | const state = new Immutable.fromJS({ 263 | data: [{ one: 'hi', two: 'hello', three: 'this should not show up' }], 264 | renderProperties: { 265 | columnProperties: { 266 | one: { id: 'one', title: 'One' }, 267 | two: { id: 'two', title: 'Two', isMetadata: true } 268 | } 269 | } 270 | }); 271 | 272 | test.deepEqual(selectors.metaDataColumnsSelector(state), ['two']); 273 | }); 274 | 275 | test('it gets columnTitles in the correct order', (test) => { 276 | const state = new Immutable.fromJS({ 277 | data: [{ one: 'hi', two: 'hello', three: 'this should not show up' }], 278 | renderProperties: { 279 | columnProperties: { 280 | one: { id: 'one', title: 'One', order: 2 }, 281 | two: { id: 'two', title: 'Two', order: 1 } 282 | } 283 | } 284 | }); 285 | 286 | test.deepEqual(selectors.columnTitlesSelector(state), ['Two', 'One']); 287 | }); 288 | 289 | [undefined, null].map((data) => 290 | test(`visibleRowIds is empty if data is ${data}`, (assert) => { 291 | const state = new Immutable.fromJS({ 292 | data 293 | }); 294 | 295 | assert.deepEqual( 296 | selectors.visibleRowIdsSelector(state), 297 | new Immutable.List() 298 | ); 299 | }) 300 | ); 301 | 302 | test('visibleRowIds gets griddleKey from data', (assert) => { 303 | const state = new Immutable.fromJS({ 304 | data: [{ griddleKey: 2 }, { griddleKey: 4 }, { griddleKey: 6 }] 305 | }); 306 | 307 | assert.deepEqual( 308 | selectors.visibleRowIdsSelector(state), 309 | new Immutable.List([2, 4, 6]) 310 | ); 311 | }); 312 | 313 | test('rowDataSelector gets row data', (assert) => { 314 | const state = new Immutable.fromJS({ 315 | data: [{ griddleKey: 2, id: 2 }, { griddleKey: 6, id: 1 }], 316 | lookup: { 317 | '2': 0, 318 | '6': 1 319 | } 320 | }); 321 | 322 | assert.deepEqual(selectors.rowDataSelector(state, { griddleKey: 6 }), { 323 | griddleKey: 6, 324 | id: 1 325 | }); 326 | }); 327 | -------------------------------------------------------------------------------- /src/selectors/dataSelectors.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect'; 3 | import isEqual from 'lodash.isequal'; 4 | import isFinite from 'lodash.isfinite'; 5 | import union from 'lodash.union'; 6 | 7 | const createDeepEqualSelector = createSelectorCreator( 8 | defaultMemoize, 9 | isEqual, 10 | ) 11 | 12 | import MAX_SAFE_INTEGER from 'max-safe-integer' 13 | //import { createSelector } from 'reselect'; 14 | 15 | /** Gets the full dataset currently tracked by Griddle */ 16 | export const dataSelector = state => state.get('data'); 17 | 18 | export const dataLoadingSelector = createSelector(dataSelector, data => !data); 19 | 20 | /** Gets the page size */ 21 | export const pageSizeSelector = state => state.getIn(['pageProperties', 'pageSize']); 22 | 23 | /** Gets the current page */ 24 | export const currentPageSelector = state => state.getIn(['pageProperties', 'currentPage']); 25 | 26 | /** Gets the record count */ 27 | export const recordCountSelector = state => state.getIn(['pageProperties', 'recordCount']); 28 | 29 | /** Gets the render properties */ 30 | export const renderPropertiesSelector = state => (state.get('renderProperties')); 31 | 32 | /** Determines if there are previous pages */ 33 | export const hasPreviousSelector = createSelector( 34 | currentPageSelector, 35 | (currentPage) => (currentPage > 1) 36 | ); 37 | 38 | /** Gets the max page size 39 | */ 40 | export const maxPageSelector = createSelector( 41 | pageSizeSelector, 42 | recordCountSelector, 43 | (pageSize, recordCount) => { 44 | const calc = recordCount / pageSize; 45 | 46 | const result = calc > Math.floor(calc) ? Math.floor(calc) + 1 : Math.floor(calc); 47 | 48 | return isFinite(result) ? result : 1; 49 | } 50 | ); 51 | 52 | /** Determines if there are more pages available. Assumes pageProperties.maxPage is set by the container */ 53 | export const hasNextSelector = createSelector( 54 | currentPageSelector, 55 | maxPageSelector, 56 | (currentPage, maxPage) => { 57 | return currentPage < maxPage; 58 | } 59 | ); 60 | 61 | /** Gets current filter */ 62 | export const filterSelector = state => state.get('filter') || ''; 63 | 64 | /** Gets the current sortColumns */ 65 | export const sortColumnsSelector = state => state.get('sortColumns') || []; 66 | 67 | /** Gets all the columns */ 68 | export const allColumnsSelector = createSelector( 69 | dataSelector, 70 | renderPropertiesSelector, 71 | (data, renderProperties) => { 72 | const dataColumns = !data || data.size === 0 ? 73 | [] : 74 | data.get(0).keySeq().toJSON(); 75 | 76 | const columnPropertyColumns = (renderProperties && renderProperties.size > 0) ? 77 | // TODO: Make this not so ugly 78 | Object.keys(renderProperties.get('columnProperties').toJSON()) : 79 | []; 80 | 81 | return union(dataColumns, columnPropertyColumns); 82 | } 83 | ); 84 | 85 | /** Gets the column properties objects sorted by order 86 | */ 87 | export const sortedColumnPropertiesSelector = createSelector( 88 | renderPropertiesSelector, 89 | (renderProperties) => ( 90 | renderProperties && renderProperties.get('columnProperties') && renderProperties.get('columnProperties').size !== 0 ? 91 | renderProperties.get('columnProperties') 92 | .sortBy(col => (col && col.get('order'))||MAX_SAFE_INTEGER) : 93 | null 94 | ) 95 | ); 96 | 97 | /** Gets metadata column ids 98 | */ 99 | export const metaDataColumnsSelector = createSelector( 100 | sortedColumnPropertiesSelector, 101 | (sortedColumnProperties) => ( 102 | sortedColumnProperties ? sortedColumnProperties 103 | .filter(c => c.get('isMetadata')) 104 | .keySeq() 105 | .toJSON() : 106 | [] 107 | ) 108 | ); 109 | 110 | /** Gets the visible columns either obtaining the sorted column properties or all columns 111 | */ 112 | export const visibleColumnsSelector = createSelector( 113 | sortedColumnPropertiesSelector, 114 | allColumnsSelector, 115 | (sortedColumnProperties, allColumns) => ( 116 | sortedColumnProperties ? sortedColumnProperties 117 | .filter(c => { 118 | const isVisible = c.get('visible') || c.get('visible') === undefined; 119 | const isMetadata = c.get('isMetadata'); 120 | return isVisible && !isMetadata; 121 | }) 122 | .keySeq() 123 | .toJSON() : 124 | allColumns 125 | ) 126 | ); 127 | 128 | /** TODO: add tests and docs 129 | */ 130 | export const visibleColumnPropertiesSelector = createSelector( 131 | visibleColumnsSelector, 132 | renderPropertiesSelector, 133 | (visibleColumns=[], renderProperties) => ( 134 | visibleColumns.map(c => { 135 | const columnProperty = renderProperties.getIn(['columnProperties', c]); 136 | return (columnProperty && columnProperty.toJSON()) || { id: c } 137 | }) 138 | ) 139 | ) 140 | 141 | /** Gets the possible columns that are currently hidden */ 142 | export const hiddenColumnsSelector = createSelector( 143 | visibleColumnsSelector, 144 | allColumnsSelector, 145 | metaDataColumnsSelector, 146 | (visibleColumns, allColumns, metaDataColumns) => { 147 | const removeColumns = [...visibleColumns, ...metaDataColumns]; 148 | 149 | return allColumns.filter(c => removeColumns.indexOf(c) === -1); 150 | } 151 | ); 152 | 153 | /** TODO: add tests and docs 154 | */ 155 | export const hiddenColumnPropertiesSelector = createSelector( 156 | hiddenColumnsSelector, 157 | renderPropertiesSelector, 158 | (hiddenColumns=[], renderProperties) => ( 159 | hiddenColumns.map(c => { 160 | const columnProperty = renderProperties.getIn(['columnProperties', c]); 161 | 162 | return (columnProperty && columnProperty.toJSON()) || { id: c } 163 | }) 164 | ) 165 | ) 166 | 167 | /** Gets the sort property for a given column */ 168 | export const sortPropertyByIdSelector = (state, { columnId }) => { 169 | const sortProperties = state.get('sortProperties'); 170 | const individualProperty = sortProperties && sortProperties.size > 0 && sortProperties.find(r => r.get('id') === columnId); 171 | 172 | return (individualProperty && individualProperty.toJSON()) || null; 173 | } 174 | 175 | /** Gets the icons property from styles */ 176 | export const iconByNameSelector = (state, { name }) => { 177 | return state.getIn(['styleConfig', 'icons', name]); 178 | } 179 | 180 | /** Gets the icons for a component */ 181 | export const iconsForComponentSelector = (state, componentName) => { 182 | const icons = state.getIn(['styleConfig', 'icons', componentName]); 183 | return icons && icons.toJS ? icons.toJS() : icons; 184 | } 185 | 186 | /** Gets a style for a component */ 187 | export const stylesForComponentSelector = (state, componentName) => { 188 | const style = state.getIn(['styleConfig', 'styles', componentName]); 189 | return style && style.toJS ? style.toJS() : style; 190 | } 191 | 192 | /** Gets a classname for a component */ 193 | export const classNamesForComponentSelector = (state, componentName) => { 194 | const classNames = state.getIn(['styleConfig', 'classNames', componentName]); 195 | return classNames && classNames.toJS ? classNames.toJS() : classNames; 196 | } 197 | 198 | /** Gets a custom component for a given column 199 | * TODO: Needs tests 200 | */ 201 | export const customComponentSelector = (state, { columnId }) => { 202 | return state.getIn(['renderProperties', 'columnProperties', columnId, 'customComponent']); 203 | } 204 | 205 | /** Gets a custom heading component for a given column 206 | * TODO: Needs tests 207 | */ 208 | export const customHeadingComponentSelector = (state, { columnId}) => { 209 | return state.getIn(['renderProperties', 'columnProperties', columnId, 'customHeadingComponent']); 210 | } 211 | 212 | export const isSettingsEnabledSelector = (state) => { 213 | const enableSettings = state.get('enableSettings'); 214 | 215 | return enableSettings === undefined ? true : enableSettings; 216 | } 217 | 218 | export const isSettingsVisibleSelector = (state) => state.get('showSettings'); 219 | 220 | export const textSelector = (state, { key}) => { 221 | return state.getIn(['textProperties', key]); 222 | } 223 | 224 | /** Gets the column ids for the visible columns 225 | */ 226 | export const columnIdsSelector = createSelector( 227 | renderPropertiesSelector, 228 | visibleColumnsSelector, 229 | (renderProperties, visibleColumns) => { 230 | const offset = 1000; 231 | // TODO: Make this better -- This is pretty inefficient 232 | return visibleColumns 233 | .map((k, index) => ({ 234 | id: renderProperties.getIn(['columnProperties', k, 'id']) || k, 235 | order: renderProperties.getIn(['columnProperties', k, 'order']) || offset + index 236 | })) 237 | .sort((first, second) => first.order - second.order) 238 | .map(item => item.id); 239 | } 240 | ); 241 | 242 | /** Gets the column titles for the visible columns 243 | */ 244 | export const columnTitlesSelector = createSelector( 245 | columnIdsSelector, 246 | renderPropertiesSelector, 247 | (columnIds, renderProperties) => columnIds.map(k => renderProperties.getIn(['columnProperties', k, 'title']) || k) 248 | ); 249 | 250 | /** Gets the griddleIds for the visible rows */ 251 | export const visibleRowIdsSelector = createSelector( 252 | dataSelector, 253 | currentPageData => currentPageData ? currentPageData.map(c => c.get('griddleKey')) : new Immutable.List() 254 | ); 255 | 256 | /** Gets the count of visible rows */ 257 | export const visibleRowCountSelector = createSelector( 258 | visibleRowIdsSelector, 259 | (visibleRowIds) => visibleRowIds.size 260 | ); 261 | 262 | // TODO: Needs tests and jsdoc 263 | export const cellValueSelector = (state, props) => { 264 | const { griddleKey, columnId } = props; 265 | const cellProperties = cellPropertiesSelector(state, props); 266 | 267 | //TODO: Make Griddle key a string in data utils 268 | const lookup = state.getIn(['lookup', griddleKey.toString()]); 269 | 270 | const value = state 271 | .get('data').get(lookup) 272 | .getIn(columnId.split('.')); 273 | const type = !!cellProperties ? cellProperties.type : 'string'; 274 | switch (type) { 275 | case 'date': 276 | return value.toLocaleDateString(); 277 | case 'string': 278 | default: 279 | return value; 280 | } 281 | }; 282 | 283 | // TODO: Needs jsdoc 284 | export const rowDataSelector = (state, { griddleKey }) => { 285 | const rowIndex = state.getIn(['lookup', griddleKey.toString()]); 286 | return state.get('data').get(rowIndex).toJSON(); 287 | }; 288 | 289 | /** Gets the row render properties 290 | */ 291 | export const rowPropertiesSelector = (state) => { 292 | const row = state.getIn(['renderProperties', 'rowProperties']); 293 | 294 | return (row && row.toJSON()) || {}; 295 | }; 296 | 297 | /** Gets the column render properties for the specified columnId 298 | */ 299 | export const cellPropertiesSelectorFactory = () => { 300 | const immutableCellPropertiesSelector = (state, { columnId }) => { 301 | const item = state.getIn(['renderProperties', 'columnProperties', columnId]); 302 | 303 | return (item && item.toJSON()) || {}; 304 | }; 305 | 306 | return createDeepEqualSelector( 307 | immutableCellPropertiesSelector, 308 | item => item, 309 | ); 310 | }; 311 | 312 | export const cellPropertiesSelector = cellPropertiesSelectorFactory(); 313 | -------------------------------------------------------------------------------- /src/settingsComponentObjects/ColumnChooser.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from '../utils/griddleConnect'; 3 | import compose from 'recompose/compose'; 4 | import withHandlers from 'recompose/withHandlers'; 5 | 6 | import { visibleColumnPropertiesSelector, hiddenColumnPropertiesSelector } from '../selectors/dataSelectors'; 7 | import { toggleColumn as toggleColumnAction } from '../actions'; 8 | 9 | const style = { 10 | label: { clear: 'both' } 11 | } 12 | 13 | const ComposedColumnSettings = compose( 14 | connect( 15 | (state) => ({ 16 | visibleColumns: visibleColumnPropertiesSelector(state), 17 | hiddenColumns: hiddenColumnPropertiesSelector(state) 18 | }), 19 | { 20 | toggleColumn: toggleColumnAction 21 | } 22 | ), 23 | withHandlers({ 24 | onToggle: ({toggleColumn}) => event => { 25 | toggleColumn(event.target.name) 26 | } 27 | }) 28 | )(({ visibleColumns, hiddenColumns, onToggle }) => { 29 | return ( 30 |
31 |
32 |

Visible Columns

33 | { Object.keys(visibleColumns).map(v => 34 | 47 | )} 48 |
49 |
50 |

Hidden Columns

51 | { Object.keys(hiddenColumns).map(v => 52 | 65 | )} 66 |
67 |
68 | )}); 69 | 70 | export default ComposedColumnSettings; 71 | -------------------------------------------------------------------------------- /src/settingsComponentObjects/PageSizeSettings.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from '../utils/griddleConnect'; 3 | import compose from 'recompose/compose'; 4 | import withState from 'recompose/withState'; 5 | import withHandlers from 'recompose/withHandlers'; 6 | 7 | import { pageSizeSelector } from '../selectors/dataSelectors'; 8 | 9 | import { setPageSize as setPageSizeAction } from '../actions'; 10 | 11 | const ComposedPageSizeSettings = compose( 12 | connect( 13 | (state) => ({ 14 | pageSize: pageSizeSelector(state), 15 | }), 16 | { 17 | setPageSize: setPageSizeAction 18 | } 19 | ), 20 | withState('value', 'updateValue', ''), 21 | withHandlers({ 22 | onChange: props => e => { 23 | props.updateValue(e.target.value) 24 | }, 25 | onSave: props => e => { 26 | props.setPageSize(props.value) 27 | } 28 | }), 29 | )(({ pageSize, onChange, onSave }) => ( 30 |
31 | 32 | 33 |
34 | )) 35 | 36 | export default ComposedPageSizeSettings; 37 | -------------------------------------------------------------------------------- /src/settingsComponentObjects/index.js: -------------------------------------------------------------------------------- 1 | import PageSizeSettings from './PageSizeSettings'; 2 | import ColumnChooser from './ColumnChooser'; 3 | 4 | export const components = { 5 | pageSizeSettings: PageSizeSettings, 6 | columnChooser: ColumnChooser, 7 | }; 8 | 9 | export default { 10 | pageSizeSettings: { order: 1 }, 11 | columnChooser: { order: 2 }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/__tests__/columnUtilsTests.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import { getColumnProperties } from '../columnUtils'; 4 | 5 | test('get column properties works with array', test => { 6 | const rowProperties = { 7 | props: { 8 | children: [ 9 | { props: { id: 1, name: "one"}}, 10 | { props: { id: 2, name: "two", order: 5 }} 11 | ] 12 | } 13 | }; 14 | 15 | const columnProperties = getColumnProperties(rowProperties); 16 | 17 | test.deepEqual(columnProperties, { 18 | 1: { id: 1, name: "one", order: 1000 }, 19 | 2: { id: 2, name: "two", order: 5 }, 20 | }); 21 | }); 22 | 23 | test('get column properties works with single column property object', test => { 24 | const rowProperties = { 25 | props: { 26 | children: { props: { id: 1, name: 'one' }}, 27 | } 28 | }; 29 | 30 | const columnProperties = getColumnProperties(rowProperties); 31 | 32 | test.deepEqual(columnProperties, { 33 | 1: { id: 1, name: 'one', order: 1000 }, 34 | }); 35 | }); 36 | 37 | test('get column properties returns all columns when no property columns specified', test => { 38 | const rowProperties = { 39 | props: {} 40 | }; 41 | 42 | const allColumns = ['one', 'two', 'three']; 43 | 44 | const columnProperties = getColumnProperties(rowProperties, allColumns); 45 | 46 | test.deepEqual(columnProperties, { 47 | one: { id: 'one', order: 1000 }, 48 | two: { id: 'two', order: 1001 }, 49 | three: { id: 'three', order: 1002 } 50 | }); 51 | }); 52 | 53 | test('get column properties ignores falsy values in array', test => { 54 | const rowProperties = { 55 | props: { 56 | children: [ 57 | { props: { id: 1, name: "one"}}, 58 | 0, 59 | undefined, 60 | null, 61 | false, 62 | ] 63 | } 64 | }; 65 | 66 | const columnProperties = getColumnProperties(rowProperties); 67 | 68 | test.is(Object.keys(columnProperties).length, 1); 69 | }); 70 | -------------------------------------------------------------------------------- /src/utils/__tests__/dataUtilsTests.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import Immutable from 'immutable'; 4 | 5 | import { 6 | hasData, 7 | transformData, 8 | } from '../dataUtils'; 9 | 10 | const collection = Immutable.fromJS([ 11 | { name: 'one' }, 12 | { name: 'two' }, 13 | { name: 'three' } 14 | ]); 15 | 16 | test('hasData is false when data does not exist', (assert) => { 17 | const res = hasData({}); 18 | assert.is(res, false); 19 | }); 20 | 21 | [undefined, null].map(data => 22 | test(`hasData is false when data is ${data}`, (assert) => { 23 | const res = hasData({ data }); 24 | assert.is(res, false); 25 | }) 26 | ); 27 | 28 | test('hasData is false when data is empty', (assert) => { 29 | const res = hasData({ data: [] }); 30 | assert.is(res, false); 31 | }); 32 | 33 | test('hasData is false when data is not empty', (assert) => { 34 | const res = hasData({ data: [{}] }); 35 | assert.is(res, true); 36 | }); 37 | 38 | test('transforms data', test => { 39 | const data = [ 40 | { first: 'Luke', last: 'Skywalker' }, 41 | { first: 'Darth', last: 'Vader' } 42 | ]; 43 | 44 | const transformedData = transformData(data, {}); 45 | 46 | test.deepEqual(Object.keys(transformedData), ['data', 'lookup']); 47 | 48 | test.deepEqual(transformedData.data.toJSON(), [ 49 | { first: 'Luke', last: 'Skywalker', griddleKey: 0 }, 50 | { first: 'Darth', last: 'Vader', griddleKey: 1 } 51 | ]); 52 | 53 | test.deepEqual(transformedData.lookup.toJSON(), { 0: 0, 1: 1 }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/utils/__tests__/griddleConnectTest.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import { mergeConnectParametersWithOptions } from '../griddleConnect'; 4 | 5 | test('makes options the fourth parameter even if connectOptions contains only one parameter', assert => { 6 | const mapStateToProps = (state, props) => {}; 7 | const connectParams = [mapStateToProps]; 8 | 9 | const output = mergeConnectParametersWithOptions(connectParams, { 'test': 'hi' }); 10 | assert.deepEqual( 11 | output, 12 | [mapStateToProps, undefined, undefined, { 'test': 'hi' }] 13 | ); 14 | }); 15 | 16 | test('merges with existing options', assert => { 17 | const mapStateToProps = (state, props) => {}; 18 | const action = () => { }; 19 | const connectParams = [mapStateToProps, { someAction: action }, undefined, { 'one': 'two'}]; 20 | 21 | const output = mergeConnectParametersWithOptions(connectParams, { 'test': 'hi' }); 22 | assert.deepEqual( 23 | output, 24 | [mapStateToProps, { someAction: action }, undefined, { 'test': 'hi', 'one': 'two' }] 25 | ); 26 | }) 27 | -------------------------------------------------------------------------------- /src/utils/__tests__/initilizerTests.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import range from 'lodash.range'; 3 | 4 | import init from '../initializer'; 5 | 6 | import { getColumnProperties } from '../columnUtils'; 7 | import { getRowProperties } from '../rowUtils'; 8 | 9 | const expectedDefaultInitialState = { 10 | data: [], 11 | renderProperties: { 12 | rowProperties: null, 13 | columnProperties: {}, 14 | }, 15 | styleConfig: {}, 16 | }; 17 | 18 | test('init succeeds given null defaults and empty props', (assert) => { 19 | const ctx = { props: {} }; 20 | const defaults = null; 21 | 22 | const res = init.call(ctx, defaults); 23 | assert.truthy(res); 24 | 25 | assert.deepEqual(res.initialState, expectedDefaultInitialState); 26 | 27 | assert.is(typeof res.reducer, 'function'); 28 | assert.deepEqual(res.reducer({}, { type: 'REDUCE' }), {}); 29 | 30 | assert.deepEqual(res.reduxMiddleware, []); 31 | 32 | assert.deepEqual(ctx.components, {}); 33 | assert.deepEqual(ctx.settingsComponentObjects, {}); 34 | assert.deepEqual(ctx.events, {}); 35 | assert.deepEqual(ctx.selectors, {}); 36 | assert.deepEqual(ctx.listeners, {}); 37 | }); 38 | 39 | test('init succeeds given empty defaults and props', (assert) => { 40 | const ctx = { props: {} }; 41 | const defaults = {}; 42 | 43 | const res = init.call(ctx, defaults); 44 | assert.truthy(res); 45 | 46 | assert.deepEqual(res.initialState, expectedDefaultInitialState); 47 | 48 | assert.is(typeof res.reducer, 'function'); 49 | assert.deepEqual(res.reducer({}, { type: 'REDUCE' }), {}); 50 | 51 | assert.deepEqual(res.reduxMiddleware, []); 52 | 53 | assert.deepEqual(ctx.components, {}); 54 | assert.deepEqual(ctx.settingsComponentObjects, {}); 55 | assert.deepEqual(ctx.events, {}); 56 | assert.deepEqual(ctx.selectors, {}); 57 | assert.deepEqual(ctx.listeners, {}); 58 | }); 59 | 60 | test('init returns defaults given minimum props', (assert) => { 61 | const ctx = { props: { data: [] } }; 62 | const defaults = { 63 | reducer: { REDUCE: () => ({ reduced: true }) }, 64 | components: { Layout: () => null }, 65 | settingsComponentObjects: { mySettings: { order: 10 } }, 66 | selectors: { aSelector: () => null }, 67 | styleConfig: { classNames: {} }, 68 | pageProperties: { pageSize: 100 }, 69 | init: true, 70 | }; 71 | 72 | const res = init.call(ctx, defaults); 73 | assert.truthy(res); 74 | 75 | assert.deepEqual(res.initialState, { 76 | ...expectedDefaultInitialState, 77 | 78 | init: true, 79 | data: ctx.props.data, 80 | pageProperties: defaults.pageProperties, 81 | styleConfig: defaults.styleConfig, 82 | }); 83 | 84 | assert.is(typeof res.reducer, 'function'); 85 | assert.deepEqual(Object.keys(res.reducer), Object.keys(defaults.reducer)); 86 | assert.deepEqual(res.reducer({}, { type: 'REDUCE' }), { reduced: true }); 87 | 88 | assert.deepEqual(res.reduxMiddleware, []); 89 | 90 | assert.deepEqual(ctx.components, defaults.components); 91 | assert.deepEqual(ctx.settingsComponentObjects, defaults.settingsComponentObjects); 92 | assert.deepEqual(ctx.events, {}); 93 | assert.deepEqual(ctx.selectors, defaults.selectors); 94 | assert.deepEqual(ctx.listeners, {}); 95 | }); 96 | 97 | test('init returns expected initialState.data given props.data', (assert) => { 98 | const ctx = { 99 | props: { 100 | data: [{ foo: 'bar' }], 101 | }, 102 | }; 103 | const defaults = {}; 104 | 105 | const res = init.call(ctx, defaults); 106 | assert.truthy(res); 107 | 108 | assert.deepEqual(res.initialState.data, ctx.props.data); 109 | }); 110 | 111 | test('init returns expected initialState.pageProperties given props (user)', (assert) => { 112 | const ctx = { 113 | props: { 114 | pageProperties: { user: true }, 115 | }, 116 | }; 117 | const defaults = { 118 | pageProperties: { 119 | defaults: true, 120 | user: false, 121 | }, 122 | }; 123 | 124 | const res = init.call(ctx, defaults); 125 | assert.truthy(res); 126 | 127 | assert.deepEqual(res.initialState.pageProperties, { 128 | defaults: true, 129 | user: true, 130 | }); 131 | }); 132 | 133 | test('init returns expected initialState.renderProperties given props (children, plugins, user)', (assert) => { 134 | const ctx = { 135 | props: { 136 | children: { 137 | props: { 138 | children: [{ props: { id: 'foo', order: 1 } }], 139 | } 140 | }, 141 | plugins: [ 142 | { renderProperties: { plugin: 0, user: false } }, 143 | { renderProperties: { plugin: 1 } }, 144 | ], 145 | renderProperties: { user: true }, 146 | }, 147 | }; 148 | const defaults = {}; 149 | 150 | const res = init.call(ctx, defaults); 151 | assert.truthy(res); 152 | 153 | assert.deepEqual(res.initialState.renderProperties, { 154 | rowProperties: getRowProperties(ctx.props.children), 155 | columnProperties: getColumnProperties(ctx.props.children), 156 | plugin: 1, 157 | user: true, 158 | }); 159 | }); 160 | 161 | test('init returns expected initialState.sortProperties given props (user)', (assert) => { 162 | const ctx = { 163 | props: { 164 | sortProperties: { user: true }, 165 | }, 166 | }; 167 | const defaults = {}; 168 | 169 | const res = init.call(ctx, defaults); 170 | assert.truthy(res); 171 | 172 | assert.deepEqual(res.initialState.sortProperties, { 173 | user: true, 174 | }); 175 | }); 176 | 177 | test('init returns merged initialState.styleConfig given props (plugins, user)', (assert) => { 178 | const ctx = { 179 | props: { 180 | plugins: [ 181 | { styleConfig: { styles: { plugin: 0, user: false } } }, 182 | { styleConfig: { styles: { plugin: 1, defaults: false } } }, 183 | ], 184 | styleConfig: { 185 | styles: { user: true }, 186 | }, 187 | }, 188 | }; 189 | const defaults = { 190 | styleConfig: { 191 | classNames: { defaults: true }, 192 | styles: { defaults: true, plugin: false, user: false }, 193 | }, 194 | }; 195 | 196 | const res = init.call(ctx, defaults); 197 | assert.truthy(res); 198 | 199 | assert.deepEqual(res.initialState.styleConfig, { 200 | classNames: { defaults: true }, 201 | styles: { 202 | defaults: false, 203 | plugin: 1, 204 | user: true, 205 | }, 206 | }); 207 | }); 208 | 209 | test('init returns expected extra initialState given props (plugins, user)', (assert) => { 210 | const ctx = { 211 | props: { 212 | plugins: [ 213 | { initialState: { plugin: 0, user: false } }, 214 | { initialState: { plugin: 1 } }, 215 | ], 216 | user: true, 217 | }, 218 | }; 219 | const defaults = { 220 | defaults: true, 221 | user: false, 222 | plugin: false, 223 | }; 224 | 225 | const res = init.call(ctx, defaults); 226 | assert.truthy(res); 227 | 228 | assert.deepEqual(res.initialState, { 229 | ...expectedDefaultInitialState, 230 | 231 | defaults: true, 232 | user: true, 233 | plugin: 1, 234 | }); 235 | }); 236 | 237 | test('init returns composed reducer given plugins', (assert) => { 238 | const ctx = { 239 | props: { 240 | plugins: [ 241 | { reducer: { PLUGIN: () => ({ plugin: 0 }) } }, 242 | { reducer: { PLUGIN: () => ({ plugin: 1 }) } }, 243 | ], 244 | }, 245 | }; 246 | const defaults = { 247 | reducer: { 248 | DEFAULTS: () => ({ defaults: true }), 249 | PLUGIN: () => ({ plugin: false }), 250 | }, 251 | }; 252 | 253 | const res = init.call(ctx, defaults); 254 | assert.truthy(res); 255 | 256 | assert.is(typeof res.reducer, 'function'); 257 | assert.deepEqual(Object.keys(res.reducer), ['DEFAULTS', 'PLUGIN']); 258 | assert.deepEqual(res.reducer({}, { type: 'DEFAULTS' }), { defaults: true }); 259 | assert.deepEqual(res.reducer({}, { type: 'PLUGIN' }), { plugin: 1 }); 260 | }); 261 | 262 | test('init returns flattened/compacted reduxMiddleware given plugins', (assert) => { 263 | const mw = range(0, 4).map(i => () => i); 264 | const ctx = { 265 | props: { 266 | plugins: [ 267 | {}, 268 | { reduxMiddleware: [mw[0]] }, 269 | {}, 270 | { reduxMiddleware: [null, mw[1], undefined, mw[2], null] }, 271 | {}, 272 | ], 273 | reduxMiddleware: [null, mw[3], undefined], 274 | }, 275 | }; 276 | const defaults = {}; 277 | 278 | const res = init.call(ctx, defaults); 279 | assert.truthy(res); 280 | 281 | assert.deepEqual(res.reduxMiddleware, mw); 282 | }); 283 | 284 | test('init sets context.components as expected given plugins', (assert) => { 285 | const ctx = { 286 | props: { 287 | plugins: [ 288 | { components: { Plugin: 0, User: false } }, 289 | { components: { Plugin: 1 } }, 290 | ], 291 | components: { User: true }, 292 | }, 293 | }; 294 | const defaults = { 295 | components: { 296 | Defaults: true, 297 | Plugin: false, 298 | }, 299 | }; 300 | 301 | const res = init.call(ctx, defaults); 302 | assert.truthy(res); 303 | 304 | assert.deepEqual(ctx.components, { 305 | Defaults: true, 306 | Plugin: 1, 307 | User: true, 308 | }); 309 | }); 310 | 311 | test('init sets context.settingsComponentObjects as expected given plugins', (assert) => { 312 | const ctx = { 313 | props: { 314 | plugins: [ 315 | { settingsComponentObjects: { Plugin: 0, User: false } }, 316 | { settingsComponentObjects: { Plugin: 1 } }, 317 | ], 318 | settingsComponentObjects: { User: true }, 319 | }, 320 | }; 321 | const defaults = { 322 | settingsComponentObjects: { 323 | Defaults: true, 324 | Plugin: false, 325 | }, 326 | }; 327 | 328 | const res = init.call(ctx, defaults); 329 | assert.truthy(res); 330 | 331 | assert.deepEqual(ctx.settingsComponentObjects, { 332 | Defaults: true, 333 | Plugin: 1, 334 | User: true, 335 | }); 336 | }); 337 | 338 | test('init sets context.events as expected given plugins', (assert) => { 339 | const ctx = { 340 | props: { 341 | plugins: [ 342 | { events: { Plugin: 0, User: false } }, 343 | { events: { Plugin: 1 } }, 344 | ], 345 | events: { User: true, User2: true }, 346 | }, 347 | }; 348 | const defaults = {}; 349 | 350 | const res = init.call(ctx, defaults); 351 | assert.truthy(res); 352 | 353 | assert.deepEqual(ctx.events, { 354 | Plugin: 1, 355 | User: false, // TODO: bug that plugins overwrite user events? 356 | User2: true, 357 | }); 358 | }); 359 | 360 | test('init sets context.selectors as expected given plugins', (assert) => { 361 | const ctx = { 362 | props: { 363 | plugins: [ 364 | { selectors: { Plugin: 0 } }, 365 | { selectors: { Plugin: 1 } }, 366 | ], 367 | }, 368 | }; 369 | const defaults = { 370 | selectors: { 371 | Defaults: true, 372 | Plugin: false, 373 | }, 374 | }; 375 | 376 | const res = init.call(ctx, defaults); 377 | assert.truthy(res); 378 | 379 | assert.deepEqual(ctx.selectors, { 380 | Defaults: true, 381 | Plugin: 1, 382 | }); 383 | }); 384 | 385 | 386 | test('init sets context.listeners as expected given props (plugins, user)', (assert) => { 387 | const ctx = { 388 | props: { 389 | plugins: [ 390 | { listeners: { plugin: () => 0, user: () => false } }, 391 | { listeners: { plugin: () => 1 } }, 392 | ], 393 | listeners: { 394 | user: () => true, 395 | user2: () => true, 396 | }, 397 | }, 398 | }; 399 | const defaults = {}; 400 | 401 | const res = init.call(ctx, defaults); 402 | assert.truthy(res); 403 | assert.truthy(res); 404 | 405 | assert.false('defaults' in ctx.listeners); 406 | assert.deepEqual(ctx.listeners.plugin(), 1); 407 | assert.deepEqual(ctx.listeners.user(), false); // TODO: bug that plugins overwrite user listeners? 408 | assert.deepEqual(ctx.listeners.user2(), true); 409 | }); 410 | -------------------------------------------------------------------------------- /src/utils/__tests__/rowUtilsTests.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import { getRowProperties } from '../rowUtils'; 4 | 5 | test('row utils gets object from react component', test => { 6 | const rowProperties = { 7 | props: { 8 | onHover: 'hi', 9 | onClick: 'hello', 10 | somethingElse: 'nothing', 11 | children: [ 12 | { props: { id: 1, name: "one"}}, 13 | { props: { id: 2, name: "two"}} 14 | ] 15 | } 16 | }; 17 | 18 | const transformedRowProperties = getRowProperties(rowProperties); 19 | test.deepEqual(transformedRowProperties, { 20 | onHover: 'hi', 21 | onClick: 'hello', 22 | somethingElse: 'nothing', 23 | childColumnName: 'children' 24 | }) 25 | }); 26 | 27 | test('row utils uses provided childColumnName instead of default', test => { 28 | const rowProperties = { 29 | props: { 30 | onHover: 'hi', 31 | onClick: 'hello', 32 | somethingElse: 'nothing', 33 | childColumnName: 'somethingElse', 34 | children: [ 35 | { props: { id: 1, name: "one"}}, 36 | { props: { id: 2, name: "two"}} 37 | ] 38 | } 39 | }; 40 | 41 | const transformedRowProperties = getRowProperties(rowProperties); 42 | test.deepEqual(transformedRowProperties, { 43 | onHover: 'hi', 44 | onClick: 'hello', 45 | somethingElse: 'nothing', 46 | childColumnName: 'somethingElse' 47 | }) 48 | 49 | }) -------------------------------------------------------------------------------- /src/utils/__tests__/sortUtilsTests.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { fromJS } from 'immutable'; 3 | 4 | import { defaultSort } from '../sortUtils'; 5 | 6 | const testData = fromJS([ 7 | { 8 | name: 'cool2', 9 | location: { 10 | city: 'city2', 11 | } 12 | }, 13 | { 14 | name: 'cool1', 15 | location: { 16 | city: 'city1', 17 | } 18 | }, 19 | { 20 | name: 'cool3', 21 | location: { 22 | city: 'city0', 23 | } 24 | } 25 | ]); 26 | 27 | test('defaultSort sorts on column value', test => { 28 | const sortedData = defaultSort(testData, 'name'); 29 | test.is(sortedData.get('0').get('name'), 'cool1'); 30 | }); 31 | 32 | test('defaultSort sorts in descending order', test => { 33 | const sortedData = defaultSort(testData, 'name', false); 34 | test.is(sortedData.get('0').get('name'), 'cool3'); 35 | }); 36 | 37 | test('defaultSort sorts in by nested data', test => { 38 | const sortedData = defaultSort(testData, 'location.city', true); 39 | test.is(sortedData.get('0').get('name'), 'cool3'); 40 | }); 41 | -------------------------------------------------------------------------------- /src/utils/columnUtils.js: -------------------------------------------------------------------------------- 1 | const offset = 1000; 2 | 3 | /** Gets a column properties object from an array of columnNames 4 | * @param {Array} columns - array of column names 5 | */ 6 | function getColumnPropertiesFromColumnArray(columnProperties, columns) { 7 | return columns.reduce((previous, current, i) => { 8 | previous[current] = { id: current, order: offset + i }; 9 | return previous; 10 | }, 11 | columnProperties); 12 | } 13 | 14 | /** Gets the column properties object from a react component (rowProperties) that contains child component(s) for columnProperties. 15 | * If no properties are found, it will work return a column properties object based on the all columns array 16 | * @param {Object} rowProperties - An React component that contains the rowProperties and child columnProperties components 17 | * @param {Array optional} allColumns - An optional array of colummn names. This will be used to generate the columnProperties when they are not defined in rowProperties 18 | */ 19 | export function getColumnProperties(rowProperties, allColumns=[]) { 20 | const children = rowProperties && rowProperties.props && rowProperties.props.children; 21 | const columnProperties = {}; 22 | 23 | // Working against an array of columnProperties 24 | if (Array.isArray(children)) { 25 | // build one object that contains all of the column properties keyed by id 26 | children.reduce((previous, current, i) => { 27 | if (current) { 28 | previous[current.props.id] = {order: offset + i, ...current.props}; 29 | } 30 | return previous; 31 | }, columnProperties); 32 | 33 | // Working against a lone, columnProperties object 34 | } else if (children && children.props) { 35 | columnProperties[children.props.id] = { order: offset, ...children.props }; 36 | } 37 | 38 | if(Object.keys(columnProperties).length === 0 && allColumns) { 39 | getColumnPropertiesFromColumnArray(columnProperties, allColumns); 40 | } 41 | 42 | return columnProperties; 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/dataUtils.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | 3 | //From Immutable docs - https://github.com/facebook/immutable-js/wiki/Predicates 4 | function keyInArray(keys) { 5 | var keySet = Immutable.Set(keys); 6 | return function (v, k) { 7 | 8 | return keySet.has(k); 9 | } 10 | } 11 | 12 | export function getIncrementer(startIndex) { 13 | let key = startIndex; 14 | return () => key++; 15 | } 16 | 17 | function isImmutableConvertibleValue(value) { 18 | return typeof value !== 'object' || value === null || value instanceof Date; 19 | } 20 | 21 | // From https://github.com/facebook/immutable-js/wiki/Converting-from-JS-objects#custom-conversion 22 | function fromJSGreedy(js) { 23 | return isImmutableConvertibleValue(js) ? js : 24 | Array.isArray(js) ? 25 | Immutable.Seq(js).map(fromJSGreedy).toList() : 26 | Immutable.Seq(js).map(fromJSGreedy).toMap(); 27 | } 28 | 29 | export function transformData(data, renderProperties) { 30 | if (!data) { 31 | return {}; 32 | } 33 | 34 | const hasCustomRowId = renderProperties.rowProperties && renderProperties.rowProperties.rowKey; 35 | 36 | // Validate that the first item in our data has the custom Griddle key 37 | if (hasCustomRowId && data.length > 0 && !data[0].hasOwnProperty(renderProperties.rowProperties.rowKey)) { 38 | throw new Error(`Griddle: Property '${renderProperties.rowProperties.rowKey}' doesn't exist in row data. Please specify a rowKey that exists in `); 39 | } 40 | 41 | const list = []; 42 | const lookup = {}; 43 | 44 | data.forEach((rowData, index) => { 45 | const map = fromJSGreedy(rowData); 46 | 47 | // if this has a row key use that -- otherwise use Griddle key 48 | const key = hasCustomRowId ? rowData[renderProperties.rowProperties.rowKey] : index; 49 | 50 | // if our map object already has griddleKey use that -- otherwise add key as griddleKey 51 | const keyedData = map.has('griddleKey') ? map : map.set('griddleKey', key); 52 | 53 | list.push(keyedData); 54 | lookup[key] = index; 55 | }); 56 | 57 | return { 58 | data: new Immutable.List(list), 59 | lookup: new Immutable.Map(lookup), 60 | }; 61 | } 62 | 63 | /** Gets the visible data based on columns 64 | * @param (List) data - data collection 65 | * @param (array) columns - list of columns to display 66 | * 67 | * TODO: Needs tests 68 | */ 69 | export function getVisibleDataForColumns(data, columns) { 70 | if (data.size < 1) { 71 | return data; 72 | } 73 | 74 | const dataColumns = data.get(0).keySeq().toArray(); 75 | 76 | const metadataColumns = dataColumns.filter(item => columns.indexOf(item) < 0); 77 | 78 | //if columns are specified but aren't in the data 79 | //make it up (as null). We will append this column 80 | //to the resultant data 81 | const magicColumns = columns 82 | .filter(item => dataColumns.indexOf(item) < 0) 83 | .reduce((original, item) => { original[item] = null; return original}, {}) 84 | //combine the metadata and the "magic" columns 85 | const extra = data.map((d, i) => new Immutable.Map( 86 | Object.assign(magicColumns) 87 | )); 88 | 89 | const result = data.map(d => d.filter(keyInArray(columns))); 90 | 91 | return result.mergeDeep(extra) 92 | .map(item => item.sortBy((val, key) => columns.indexOf(key) > -1 ? columns.indexOf(key) : MAX_SAFE_INTEGER )); 93 | } 94 | 95 | /* TODO: Add documentation and tests for this whole section!*/ 96 | 97 | /** Does this initial state object have column properties? 98 | * @param (object) stateObject - a non-immutable state object for initialization 99 | * 100 | * TODO: Needs tests 101 | */ 102 | export function hasColumnProperties(stateObject) { 103 | return stateObject.hasOwnProperty('renderProperties') && 104 | stateObject.renderProperties.hasOwnProperty('columnProperties') && 105 | Object.keys(stateObject.renderProperties.columnProperties).length > 0 106 | } 107 | 108 | /** Does this initial state object have data? 109 | * @param (object) stateObject - a non-immutable state object for initialization 110 | */ 111 | export function hasData(stateObject) { 112 | return !!stateObject.data && stateObject.data.length > 0; 113 | } 114 | 115 | /** Gets a new state object (not immutable) that has columnProperties if none exist 116 | * @param (object) stateObject - a non-immutable state object for initialization 117 | * 118 | * TODO: Needs tests 119 | */ 120 | export function addColumnPropertiesWhenNoneExist(stateObject) { 121 | if(!hasData(stateObject) || hasColumnProperties(stateObject)) { 122 | return stateObject; 123 | } 124 | 125 | return { 126 | ...stateObject, 127 | renderProperties: { 128 | columnProperties: Object.keys(stateObject.data[0]).reduce(((previous, current) => { 129 | previous[current] = { id: current, visible: true } 130 | 131 | return previous; 132 | }), {}) 133 | } 134 | }; 135 | } 136 | -------------------------------------------------------------------------------- /src/utils/griddleConnect.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | /// This method appends options onto existing connect parameters 6 | export const mergeConnectParametersWithOptions = ( 7 | originalConnect, 8 | newOptions 9 | ) => { 10 | const [ 11 | mapStateFromProps, 12 | mapDispatchFromProps, 13 | mergeProps, 14 | options 15 | ] = originalConnect; 16 | 17 | return [ 18 | mapStateFromProps, 19 | mapDispatchFromProps, 20 | mergeProps, 21 | { ...options, ...newOptions } 22 | ]; 23 | }; 24 | 25 | const griddleConnect = (...connectOptions) => OriginalComponent => 26 | class extends React.Component { 27 | static contextTypes = { 28 | storeKey: PropTypes.string 29 | }; 30 | 31 | constructor(props, context) { 32 | super(props, context); 33 | const newOptions = mergeConnectParametersWithOptions(connectOptions, { 34 | storeKey: context.storeKey 35 | }); 36 | this.ConnectedComponent = connect(...newOptions)(OriginalComponent); 37 | } 38 | 39 | render() { 40 | return ; 41 | } 42 | }; 43 | 44 | export { griddleConnect as connect }; 45 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import * as columnUtils from './columnUtils'; 2 | import * as compositionUtils from './compositionUtils'; 3 | import * as dataUtils from './dataUtils'; 4 | import * as rowUtils from './rowUtils'; 5 | import * as sortUtils from './sortUtils'; 6 | import { connect } from './griddleConnect'; 7 | 8 | export default { 9 | columnUtils, 10 | compositionUtils, 11 | dataUtils, 12 | rowUtils, 13 | sortUtils, 14 | connect 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/initializer.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash.merge'; 2 | import pickBy from 'lodash.pickby'; 3 | import compact from 'lodash.compact'; 4 | import flatten from 'lodash.flatten'; 5 | import { 6 | buildGriddleReducer, 7 | buildGriddleComponents 8 | } from './compositionUtils'; 9 | import { getColumnProperties } from './columnUtils'; 10 | import { getRowProperties } from './rowUtils'; 11 | 12 | function initializer(defaults) { 13 | if (!this) throw new Error('this missing!'); 14 | 15 | const { 16 | reducer: defaultReducer, 17 | components, 18 | settingsComponentObjects, 19 | selectors, 20 | styleConfig: defaultStyleConfig, 21 | ...defaultInitialState 22 | } = defaults || {}; 23 | 24 | const { 25 | plugins = [], 26 | data = [], 27 | children: rowPropertiesComponent, 28 | events: userEvents = {}, 29 | styleConfig: userStyleConfig = {}, 30 | components: userComponents, 31 | renderProperties: userRenderProperties = {}, 32 | settingsComponentObjects: userSettingsComponentObjects, 33 | reduxMiddleware = [], 34 | listeners = {}, 35 | ...userInitialState 36 | } = this.props; 37 | 38 | const rowProperties = getRowProperties(rowPropertiesComponent); 39 | const columnProperties = getColumnProperties(rowPropertiesComponent); 40 | 41 | // Combine / compose the reducers to make a single, unified reducer 42 | const reducer = buildGriddleReducer([ 43 | defaultReducer, 44 | ...plugins.map(p => p.reducer) 45 | ]); 46 | 47 | // Combine / Compose the components to make a single component for each component type 48 | this.components = buildGriddleComponents([ 49 | components, 50 | ...plugins.map(p => p.components), 51 | userComponents 52 | ]); 53 | 54 | this.settingsComponentObjects = Object.assign( 55 | { ...settingsComponentObjects }, 56 | ...plugins.map(p => p.settingsComponentObjects), 57 | userSettingsComponentObjects 58 | ); 59 | 60 | this.events = Object.assign({}, userEvents, ...plugins.map(p => p.events)); 61 | 62 | this.selectors = plugins.reduce( 63 | (combined, plugin) => ({ ...combined, ...plugin.selectors }), 64 | { ...selectors } 65 | ); 66 | 67 | const styleConfig = merge( 68 | { ...defaultStyleConfig }, 69 | ...plugins.map(p => p.styleConfig), 70 | userStyleConfig 71 | ); 72 | 73 | // TODO: This should also look at the default and plugin initial state objects 74 | const renderProperties = Object.assign( 75 | { 76 | rowProperties, 77 | columnProperties 78 | }, 79 | ...plugins.map(p => p.renderProperties), 80 | userRenderProperties 81 | ); 82 | 83 | // TODO: Make this its own method 84 | const initialState = merge( 85 | defaultInitialState, 86 | ...plugins.map(p => p.initialState), 87 | userInitialState, 88 | { 89 | data, 90 | renderProperties, 91 | styleConfig 92 | } 93 | ); 94 | 95 | const sanitizedListeners = pickBy( 96 | listeners, 97 | value => typeof value === 'function' 98 | ); 99 | this.listeners = plugins.reduce( 100 | (combined, plugin) => ({ 101 | ...combined, 102 | ...pickBy(plugin.listeners, value => typeof value === 'function') 103 | }), 104 | sanitizedListeners 105 | ); 106 | 107 | return { 108 | initialState, 109 | reducer, 110 | reduxMiddleware: compact([ 111 | ...flatten(plugins.map(p => p.reduxMiddleware)), 112 | ...reduxMiddleware 113 | ]) 114 | }; 115 | } 116 | 117 | export default initializer; 118 | -------------------------------------------------------------------------------- /src/utils/listenerUtils.js: -------------------------------------------------------------------------------- 1 | export const StoreListener = class StoreListener { 2 | constructor(store) { 3 | this.store = store; 4 | this.unsubscribers = {}; 5 | } 6 | 7 | removeListener = (name) => { 8 | if (this.unsubscribers.hasOwnProperty(name)) { 9 | this.unsubscribers[name](); 10 | delete this.unsubscribers[name]; 11 | return true; 12 | } else { 13 | return false; 14 | } 15 | } 16 | 17 | // Adds a listener to the store. 18 | // Will attempt to remove an existing listener if the name 19 | // matches that of an existing listener. 20 | // If no name is provided this is an anonymous lister, it 21 | // is not registered in the list of unsubscribe functions, 22 | // returns the unsubscribe function so it can still be handled 23 | // manually if desired. 24 | addListener = (listener, name, otherArgs) => { 25 | // attempt to unsubscribe an existing listener if the new 26 | // listener name matches 27 | // if no name is provided, do nothing 28 | name && this.removeListener(name); 29 | const unsubscribe = (() => { 30 | let oldState; 31 | return this.store.subscribe(() => { 32 | const newState = this.store.getState(); 33 | listener(oldState, newState, {...otherArgs}); 34 | oldState = newState; 35 | }); 36 | })(); 37 | // if name was provided, add the unsubscribe 38 | // otherwise this is an "anonymous" listener 39 | name && (this.unsubscribers[name] = unsubscribe); 40 | return unsubscribe; 41 | } 42 | 43 | hasListener = (name) => { 44 | return this.unsubscribers.hasOwnProperty(name); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/utils/rowUtils.js: -------------------------------------------------------------------------------- 1 | /** Gets a row properties object from a rowProperties component 2 | * @param {Object} rowPropertiesComponent - A react component that contains rowProperties as props 3 | */ 4 | export function getRowProperties(rowPropertiesComponent) { 5 | if (!rowPropertiesComponent) return null; 6 | 7 | let rowProps = Object.assign({}, rowPropertiesComponent.props); 8 | delete rowProps.children; 9 | 10 | if (!rowProps.hasOwnProperty('childColumnName')) { 11 | rowProps.childColumnName = 'children'; 12 | } 13 | 14 | return rowProps; 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/sortUtils.js: -------------------------------------------------------------------------------- 1 | /* Sorts the given data by the specified column 2 | * @parameter {array} data - The data to sort 3 | * @parameter {string} column - the name of the column to sort 4 | * @parameter {boolean optional} sortAscending - whether or not to sort this column in ascending order 5 | * 6 | * TODO: Needs tests! 7 | */ 8 | export function defaultSort(data, column, sortAscending = true) { 9 | return data.sort( 10 | (original, newRecord) => { 11 | const columnKey = column.split('.'); 12 | const originalValue = (original.hasIn(columnKey) && original.getIn(columnKey)) || ''; 13 | const newRecordValue = (newRecord.hasIn(columnKey) && newRecord.getIn(columnKey)) || ''; 14 | 15 | //TODO: This is about the most cheezy sorting check ever. 16 | //Make it better 17 | if(originalValue === newRecordValue) { 18 | return 0; 19 | } else if (originalValue > newRecordValue) { 20 | return sortAscending ? 1 : -1; 21 | } 22 | else { 23 | return sortAscending ? -1 : 1; 24 | } 25 | }); 26 | } 27 | 28 | export function setSortProperties({ setSortColumn, sortProperty, columnId }) { 29 | return () => { 30 | if (sortProperty === null) { 31 | setSortColumn({ id: columnId, sortAscending: true }); 32 | return; 33 | } 34 | 35 | const newSortProperty = { 36 | ...sortProperty, 37 | sortAscending: !sortProperty.sortAscending 38 | }; 39 | 40 | setSortColumn(newSortProperty); 41 | }; 42 | } 43 | 44 | export function getSortIconProps(props) { 45 | const { sortProperty, sortAscendingIcon, sortDescendingIcon } = props; 46 | const { sortAscendingClassName, sortDescendingClassName } = props; 47 | 48 | if (sortProperty) { 49 | return sortProperty.sortAscending ? 50 | { 51 | icon: sortAscendingIcon, 52 | iconClassName: sortAscendingClassName, 53 | } : 54 | { 55 | icon: sortDescendingIcon, 56 | iconClassName: sortDescendingClassName, 57 | }; 58 | } 59 | 60 | // return null so we don't render anything if no sortProperty 61 | return null; 62 | } 63 | -------------------------------------------------------------------------------- /src/utils/valueUtils.js: -------------------------------------------------------------------------------- 1 | export function valueOrResult(arg, ...args) { 2 | if (typeof arg === 'function') { 3 | return arg.apply(null, args); 4 | } 5 | return arg; 6 | } 7 | -------------------------------------------------------------------------------- /stories/fakeData.d.ts: -------------------------------------------------------------------------------- 1 | export interface FakeData { 2 | id: number; 3 | name: string; 4 | city: string; 5 | state: string; 6 | country: string; 7 | company: string; 8 | favoriteNumber: number; 9 | } 10 | 11 | declare const fakeData: FakeData[]; 12 | 13 | export default fakeData; 14 | -------------------------------------------------------------------------------- /stories/fakeData2.d.ts: -------------------------------------------------------------------------------- 1 | import { FakeData } from "./fakeData"; 2 | 3 | declare class person { 4 | constructor(data: FakeData); 5 | } 6 | 7 | declare class personClass { 8 | constructor(data: FakeData); 9 | } 10 | 11 | declare const fakeData2: person[]; 12 | declare const fakeData3: personClass[]; 13 | -------------------------------------------------------------------------------- /stories/fakeData2.js: -------------------------------------------------------------------------------- 1 | import fakeData from './fakeData'; 2 | 3 | export function person({ 4 | id, 5 | name, 6 | city, 7 | state, 8 | country, 9 | company, 10 | favoriteNumber 11 | }) { 12 | const personObject = {}; 13 | 14 | personObject.id = id; 15 | personObject.name = name; 16 | personObject.city = city; 17 | personObject.state = state; 18 | personObject.country = country; 19 | personObject.company = company; 20 | personObject.favoriteNumber = favoriteNumber; 21 | 22 | return personObject; 23 | } 24 | 25 | export class personClass { 26 | constructor({ id, name, city, state, country, company, favoriteNumber }) { 27 | this.id = id; 28 | this.name = name; 29 | this.city = city; 30 | this.state = state; 31 | this.country = country; 32 | this.company = company; 33 | this.favoriteNumber = favoriteNumber; 34 | } 35 | } 36 | 37 | export const fakeData2 = fakeData.map(x => new person(x)); 38 | export const fakeData3 = fakeData.map(x => new personClass(x)); 39 | -------------------------------------------------------------------------------- /test/helpers/setupTest.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "lib": ["es5", "es6", "es7", "es2017", "dom"], 6 | "allowJs": true, 7 | "jsx": "preserve", 8 | "noImplicitAny": false, 9 | "noImplicitThis": false, 10 | "strictNullChecks": false, 11 | "types": [], 12 | "noEmit": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "allowSyntheticDefaultImports": true 15 | }, 16 | "files": ["stories/index.tsx"], 17 | "exclude": ["node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const LodashModuleReplacementPlugin = require('lodash-webpack-plugin'); 2 | const webpack = require('webpack'); 3 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 4 | const path = require('path'); 5 | 6 | module.exports = { 7 | devtool: 'source-map', 8 | entry: './src/module.js', 9 | output: { 10 | path: path.join(__dirname, '/dist/umd/'), 11 | filename: 'griddle.js', 12 | publicPath: '/build/', 13 | library: 'Griddle', 14 | libraryTarget: 'umd' 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.jsx?$/, 20 | use: { 21 | loader: 'babel-loader?cacheDirectory', 22 | options: { 23 | presets: ['@babel/preset-env', '@babel/preset-react'] 24 | } 25 | }, 26 | exclude: ['/node_modules/', '/stories/', '/storybook-static/'] 27 | } 28 | ] 29 | }, 30 | plugins: [ 31 | new LodashModuleReplacementPlugin(), 32 | new webpack.optimize.OccurrenceOrderPlugin(), 33 | new UglifyJsPlugin() 34 | ], 35 | externals: [ 36 | { 37 | react: { 38 | root: 'React', 39 | commonjs2: 'react', 40 | commonjs: 'react', 41 | amd: 'react' 42 | } 43 | } 44 | ] 45 | }; 46 | --------------------------------------------------------------------------------