├── .gitattributes ├── src ├── plugins │ ├── local │ │ ├── actions │ │ │ └── index.js │ │ ├── index.js │ │ ├── components │ │ │ ├── TableHeadingCellContainer.js │ │ │ ├── index.js │ │ │ ├── NextButtonContainer.js │ │ │ ├── PreviousButtonContainer.js │ │ │ ├── PageDropdownContainer.js │ │ │ ├── TableContainer.js │ │ │ ├── TableBodyContainer.js │ │ │ ├── TableHeadingContainer.js │ │ │ └── RowContainer.js │ │ ├── reducers │ │ │ ├── index.js │ │ │ └── __tests__ │ │ │ │ └── localReducerTests.js │ │ └── selectors │ │ │ └── localSelectors.js │ ├── position │ │ ├── constants │ │ │ └── index.js │ │ ├── components │ │ │ ├── Pagination.js │ │ │ ├── index.js │ │ │ ├── TableBody.js │ │ │ ├── SpacerRow.js │ │ │ └── TableEnhancer.js │ │ ├── actions │ │ │ └── index.js │ │ ├── index.js │ │ ├── selectors │ │ │ ├── __tests__ │ │ │ │ └── indexTest.js │ │ │ └── index.js │ │ ├── initial-state.js │ │ ├── reducers │ │ │ ├── index.js │ │ │ └── __tests__ │ │ │ │ └── indexTest.js │ │ └── utils.js │ └── legacyStyle │ │ └── index.js ├── components │ ├── TableHeadingCellEnhancer.js │ ├── Test.js │ ├── Loading.js │ ├── NoResults.js │ ├── NextButton.js │ ├── PreviousButton.js │ ├── TableBody.js │ ├── SettingsToggle.js │ ├── Layout.js │ ├── Cell.js │ ├── Pagination.js │ ├── TableHeadingCell.js │ ├── Settings.js │ ├── Table.js │ ├── TableHeading.js │ ├── SettingsWrapper.js │ ├── NextButtonContainer.js │ ├── Row.js │ ├── NextButtonEnhancer.js │ ├── PageDropdownEnhancer.js │ ├── PreviousButtonEnhancer.js │ ├── FilterEnhancer.js │ ├── PreviousButtonContainer.js │ ├── FilterContainer.js │ ├── Filter.js │ ├── PageDropdownContainer.js │ ├── SettingsToggleContainer.js │ ├── LoadingContainer.js │ ├── NoResultsContainer.js │ ├── __tests__ │ │ ├── SettingsToggleTest.js │ │ ├── FilterTest.js │ │ ├── TableBodyTest.js │ │ ├── RowTest.js │ │ ├── NextButtonTest.js │ │ ├── CellTest.js │ │ ├── SettingsTest.js │ │ ├── PaginationTest.js │ │ ├── PreviousButtonTest.js │ │ ├── TableTest.js │ │ ├── TableHeadingTest.js │ │ ├── TableHeadingCellTest.js │ │ ├── SettingsWrapperTest.js │ │ └── PageDropdownTest.js │ ├── RowDefinition.js │ ├── PaginationContainer.js │ ├── SettingsWrapperContainer.js │ ├── LayoutContainer.js │ ├── TableHeadingContainer.js │ ├── TableBodyContainer.js │ ├── PageDropdown.js │ ├── TableContainer.js │ ├── RowContainer.js │ ├── SettingsContainer.js │ ├── ColumnDefinition.js │ ├── CellContainer.js │ ├── TableHeadingCellContainer.js │ └── index.js ├── utils │ ├── valueUtils.js │ ├── index.js │ ├── rowUtils.js │ ├── __tests__ │ │ ├── griddleConnectTest.js │ │ ├── sortUtilsTests.js │ │ ├── rowUtilsTests.js │ │ ├── dataUtilsTests.js │ │ ├── columnUtilsTests.js │ │ └── initilizerTests.js │ ├── griddleConnect.js │ ├── listenerUtils.js │ ├── columnUtils.js │ ├── sortUtils.js │ ├── initializer.js │ └── dataUtils.js ├── settingsComponentObjects │ ├── index.js │ ├── PageSizeSettings.js │ └── ColumnChooser.js ├── core │ ├── __tests__ │ │ └── corePluginTests.js │ ├── index.js │ └── initialState.js ├── module.js ├── constants │ └── index.js ├── actions │ └── index.js ├── index.js ├── reducers │ ├── dataReducer.js │ └── __tests__ │ │ └── dataReducerTest.js └── selectors │ ├── __tests__ │ └── dataSelectorsTest.js │ └── dataSelectors.js ├── .vscode └── settings.json ├── .prettierrc ├── .eslintrc ├── .editorconfig ├── .jshintrc ├── test └── helpers │ └── setupTest.js ├── .github ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE.md ├── .storybook ├── config.js ├── webpack.config.js └── webpack-build.config.js ├── .travis.yml ├── stories ├── fakeData.d.ts ├── fakeData2.d.ts └── fakeData2.js ├── tsconfig.json ├── .babelrc ├── .npmignore ├── .gitignore ├── LICENSE ├── webpack.config.js ├── conduct.md ├── README.md ├── CHANGELOG.md └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /src/plugins/local/actions/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "arrowParens": "always" 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "comma-dangle": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/plugins/position/constants/index.js: -------------------------------------------------------------------------------- 1 | export const XY_POSITION_CHANGED = 'XY_POSITION_CHANGED'; 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | insert_final_newline = true 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "asi": true, 4 | "eqeqeq": "cantbeturnedoff", 5 | "eqnull": true, 6 | "sub":true 7 | } 8 | -------------------------------------------------------------------------------- /test/helpers/setupTest.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /.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? -------------------------------------------------------------------------------- /src/components/TableHeadingCellEnhancer.js: -------------------------------------------------------------------------------- 1 | // Obsolete 2 | const EnhancedHeadingCell = OriginalComponent => OriginalComponent; 3 | 4 | export default EnhancedHeadingCell; 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/components/Test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Test extends React.Component { 4 | render() { 5 | return
Hi from component
; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Loading = ({ className, style }) => ( 4 |
5 | Loading… 6 |
7 | ); 8 | 9 | export default Loading; 10 | -------------------------------------------------------------------------------- /src/components/NoResults.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const NoResults = ({ className, style }) => ( 4 |
5 | No results found. 6 |
7 | ); 8 | 9 | export default NoResults; 10 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/components/NextButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const NextButton = ({ hasNext, onClick, style, className, text }) => hasNext ? ( 4 | 5 | ) : 6 | null; 7 | 8 | export default NextButton; 9 | -------------------------------------------------------------------------------- /src/components/PreviousButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const PreviousButton = ({ hasPrevious, onClick, style, className, text }) => hasPrevious ? ( 4 | 5 | ) : 6 | null; 7 | 8 | export default PreviousButton; 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/components/TableBody.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const TableBody = ({ rowIds, Row, style, className }) => ( 4 | 5 | { rowIds && rowIds.map((k, i) => ) } 6 | 7 | ); 8 | 9 | export default TableBody; 10 | -------------------------------------------------------------------------------- /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/components/SettingsToggle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SettingsToggle = ({onClick, text, style, className}) => ( 4 | 12 | ); 13 | 14 | export default SettingsToggle; 15 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/components/TableHeadingCellContainer.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 withHandlers from 'recompose/withHandlers'; 8 | import { sortPropertyByIdSelector, iconsForComponentSelector, customHeadingComponentSelector, stylesForComponentSelector, classNamesForComponentSelector, cellPropertiesSelector } from '../selectors/dataSelectors'; 9 | import { setSortColumn } from '../actions'; 10 | import { combineHandlers } from '../utils/compositionUtils'; 11 | import { getSortIconProps, setSortProperties } from '../utils/sortUtils'; 12 | import { valueOrResult } from '../utils/valueUtils'; 13 | 14 | const DefaultTableHeadingCellContent = ({title, icon, iconClassName}) => ( 15 | 16 | { title } 17 | { icon && {icon} } 18 | 19 | ) 20 | 21 | const EnhancedHeadingCell = OriginalComponent => compose( 22 | getContext({ 23 | events: PropTypes.object, 24 | selectors: PropTypes.object, 25 | }), 26 | connect( 27 | (state, props) => ({ 28 | sortProperty: sortPropertyByIdSelector(state, props), 29 | customHeadingComponent: customHeadingComponentSelector(state, props), 30 | cellProperties: cellPropertiesSelector(state, props), 31 | className: classNamesForComponentSelector(state, 'TableHeadingCell'), 32 | sortAscendingClassName: classNamesForComponentSelector(state, 'TableHeadingCellAscending'), 33 | sortDescendingClassName: classNamesForComponentSelector(state, 'TableHeadingCellDescending'), 34 | style: stylesForComponentSelector(state, 'TableHeadingCell'), 35 | ...iconsForComponentSelector(state, 'TableHeadingCell'), 36 | }), 37 | (dispatch, { events: { onSort } }) => ({ 38 | setSortColumn: combineHandlers([ 39 | onSort, 40 | compose(dispatch, setSortColumn), 41 | ]), 42 | }) 43 | ), 44 | withHandlers(props => ({ 45 | onClick: props.cellProperties.sortable === false ? (() => () => {}) : 46 | props.events.setSortProperties || setSortProperties, 47 | })), 48 | //TODO: use with props on change or something more performant here 49 | mapProps(props => { 50 | const iconProps = getSortIconProps(props); 51 | const title = props.customHeadingComponent ? 52 | : 53 | ; 54 | const className = valueOrResult(props.cellProperties.headerCssClassName, props) || props.className; 55 | const style = { 56 | ...(props.cellProperties.sortable === false || { cursor: 'pointer' }), 57 | ...props.style, 58 | }; 59 | 60 | return { 61 | ...props.cellProperties.extraData, 62 | ...props, 63 | ...iconProps, 64 | title, 65 | style, 66 | className 67 | }; 68 | }) 69 | )(props => 70 | 73 | ); 74 | 75 | export default EnhancedHeadingCell; 76 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | createStore, 3 | combineReducers, 4 | bindActionCreators, 5 | applyMiddleware, 6 | compose 7 | } from 'redux'; 8 | import { createProvider } from 'react-redux'; 9 | import React, { Component } from 'react'; 10 | import PropTypes from 'prop-types'; 11 | import forIn from 'lodash.forin'; 12 | import pickBy from 'lodash.pickby'; 13 | 14 | import corePlugin from './core'; 15 | import init from './utils/initializer'; 16 | import { StoreListener } from './utils/listenerUtils'; 17 | import * as actions from './actions'; 18 | 19 | class Griddle extends Component { 20 | static childContextTypes = { 21 | components: PropTypes.object.isRequired, 22 | settingsComponentObjects: PropTypes.object, 23 | events: PropTypes.object, 24 | selectors: PropTypes.object, 25 | storeKey: PropTypes.string, 26 | storeListener: PropTypes.object 27 | }; 28 | 29 | constructor(props) { 30 | super(props); 31 | 32 | const { core = corePlugin, storeKey = Griddle.storeKey || 'store' } = props; 33 | 34 | const { initialState, reducer, reduxMiddleware } = init.call(this, core); 35 | 36 | const composeEnhancers = 37 | (typeof window !== 'undefined' && 38 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || 39 | compose; 40 | this.store = createStore( 41 | reducer, 42 | initialState, 43 | composeEnhancers(applyMiddleware(...reduxMiddleware)) 44 | ); 45 | 46 | this.provider = createProvider(storeKey); 47 | 48 | this.storeListener = new StoreListener(this.store); 49 | forIn(this.listeners, (listener, name) => { 50 | this.storeListener.addListener(listener, name, { 51 | events: this.events, 52 | selectors: this.selectors 53 | }); 54 | }); 55 | } 56 | 57 | componentWillReceiveProps(nextProps) { 58 | const newState = pickBy(nextProps, (value, key) => { 59 | return this.props[key] !== value; 60 | }); 61 | 62 | // Only update the state if something has changed. 63 | if (Object.keys(newState).length > 0) { 64 | this.store.dispatch(actions.updateState(newState)); 65 | } 66 | } 67 | 68 | shouldComponentUpdate() { 69 | // As relevant property updates are captured in `componentWillReceiveProps`. 70 | // return false to prevent the the entire root node from being deleted. 71 | return false; 72 | } 73 | 74 | getStoreKey = () => { 75 | return this.props.storeKey || Griddle.storeKey || 'store'; 76 | }; 77 | 78 | getChildContext() { 79 | return { 80 | components: this.components, 81 | settingsComponentObjects: this.settingsComponentObjects, 82 | events: this.events, 83 | selectors: this.selectors, 84 | storeKey: this.getStoreKey(), 85 | storeListener: this.storeListener 86 | }; 87 | } 88 | 89 | render() { 90 | if (!this.components.Layout) { 91 | return null; 92 | } 93 | 94 | return ( 95 | 96 | 97 | 98 | ); 99 | } 100 | } 101 | 102 | Griddle.storeKey = 'store'; 103 | 104 | export default Griddle; 105 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import Cell from './Cell'; 2 | import CellContainer from './CellContainer'; 3 | import ColumnDefinition from './ColumnDefinition'; 4 | import Row from './Row'; 5 | import RowContainer from './RowContainer'; 6 | import RowDefinition from './RowDefinition'; 7 | import Table from './Table'; 8 | import TableBody from './TableBody'; 9 | import TableBodyContainer from './TableBodyContainer'; 10 | import TableHeading from './TableHeading'; 11 | import TableHeadingContainer from './TableHeadingContainer'; 12 | import TableHeadingCell from './TableHeadingCell'; 13 | import TableHeadingCellContainer from './TableHeadingCellContainer'; 14 | import TableHeadingCellEnhancer from './TableHeadingCellEnhancer'; 15 | import TableContainer from './TableContainer'; 16 | import Layout from './Layout'; 17 | import LayoutContainer from './LayoutContainer'; 18 | import Pagination from './Pagination'; 19 | import PaginationContainer from './PaginationContainer'; 20 | import Filter from './Filter'; 21 | import FilterEnhancer from './FilterEnhancer'; 22 | import FilterContainer from './FilterContainer'; 23 | import SettingsToggle from './SettingsToggle'; 24 | import SettingsToggleContainer from './SettingsToggleContainer'; 25 | import SettingsWrapper from './SettingsWrapper'; 26 | import SettingsWrapperContainer from './SettingsWrapperContainer'; 27 | import Settings from './Settings'; 28 | import SettingsContainer from './SettingsContainer'; 29 | import { components as SettingsComponents } from '../settingsComponentObjects'; 30 | import NextButton from './NextButton'; 31 | import NextButtonEnhancer from './NextButtonEnhancer'; 32 | import NextButtonContainer from './NextButtonContainer'; 33 | import Loading from './Loading'; 34 | import LoadingContainer from './LoadingContainer'; 35 | import NoResults from './NoResults'; 36 | import NoResultsContainer from './NoResultsContainer'; 37 | import PreviousButton from './PreviousButton'; 38 | import PreviousButtonEnhancer from './PreviousButtonEnhancer'; 39 | import PreviousButtonContainer from './PreviousButtonContainer'; 40 | import PageDropdown from './PageDropdown'; 41 | import PageDropdownContainer from './PageDropdownContainer'; 42 | import PageDropdownEnhancer from './PageDropdownEnhancer'; 43 | 44 | const components = { 45 | Cell, 46 | CellContainer, 47 | ColumnDefinition, 48 | Row, 49 | RowContainer, 50 | RowDefinition, 51 | Table, 52 | TableBody, 53 | TableBodyContainer, 54 | TableHeading, 55 | TableHeadingContainer, 56 | TableHeadingCell, 57 | TableHeadingCellContainer, 58 | TableHeadingCellEnhancer, 59 | TableContainer, 60 | Layout, 61 | LayoutContainer, 62 | NextButton, 63 | NextButtonEnhancer, 64 | NextButtonContainer, 65 | Loading, 66 | LoadingContainer, 67 | NoResults, 68 | NoResultsContainer, 69 | PageDropdown, 70 | PageDropdownContainer, 71 | PageDropdownEnhancer, 72 | Pagination, 73 | PaginationContainer, 74 | PreviousButton, 75 | PreviousButtonEnhancer, 76 | PreviousButtonContainer, 77 | Filter, 78 | FilterEnhancer, 79 | FilterContainer, 80 | SettingsToggle, 81 | SettingsToggleContainer, 82 | SettingsWrapper, 83 | SettingsWrapperContainer, 84 | Settings, 85 | SettingsContainer, 86 | SettingsComponents, 87 | Style: () => null, 88 | }; 89 | 90 | export default components; 91 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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 | --------------------------------------------------------------------------------