diff --git a/package.json b/package.json index 7715a2e4..6bac5680 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,8 @@ "typescript": "^2.4.1", "webpack": "^1.14.0", "webpack-dev-server": "^1.16.2", - "webpack-fail-plugin": "^1.0.6" + "webpack-fail-plugin": "^1.0.6", + "redux-logger": "^3.0.6" }, "dependencies": { "immutable": "^3.8.1", diff --git a/src/components/FilterContainer.js b/src/components/FilterContainer.js deleted file mode 100644 index 7d46bc05..00000000 --- a/src/components/FilterContainer.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; - -import { classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; -import { setFilter } from '../actions'; - -const EnhancedFilter = OriginalComponent => connect((state, props) => ({ - className: classNamesForComponentSelector(state, 'Filter'), - style: stylesForComponentSelector(state, 'Filter'), -}), { setFilter })(props => ); - -export default EnhancedFilter; diff --git a/src/components/NextButtonContainer.js b/src/components/NextButtonContainer.js deleted file mode 100644 index 278d9eb6..00000000 --- a/src/components/NextButtonContainer.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import { connect } from '../utils/griddleConnect'; - -import { textSelector, hasNextSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; - -const enhance = OriginalComponent => connect((state, props) => ({ - text: textSelector(state, { key: 'next' }), - hasNext: hasNextSelector(state, props), - className: classNamesForComponentSelector(state, 'NextButton'), - style: stylesForComponentSelector(state, 'NextButton'), -}))((props) => ); - -export default enhance; diff --git a/src/components/PreviousButtonContainer.js b/src/components/PreviousButtonContainer.js deleted file mode 100644 index 628e227c..00000000 --- a/src/components/PreviousButtonContainer.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { connect } from '../utils/griddleConnect'; -import { textSelector, hasPreviousSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; - -const enhance = OriginalComponent => connect((state, props) => ({ - text: textSelector(state, { key: 'previous' }), - hasPrevious: hasPreviousSelector(state, props), - className: classNamesForComponentSelector(state, 'PreviousButton'), - style: stylesForComponentSelector(state, 'PreviousButton'), -}))((props) => ); - -export default enhance; diff --git a/src/components/SettingsToggleContainer.js b/src/components/SettingsToggleContainer.js deleted file mode 100644 index 16037ed3..00000000 --- a/src/components/SettingsToggleContainer.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import { connect } from '../utils/griddleConnect'; -import compose from 'recompose/compose'; -import { textSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; -import { toggleSettings as toggleSettingsAction } from '../actions'; - -const enhancedSettingsToggle = OriginalComponent => compose( - connect((state, props) => ({ - text: textSelector(state, { key: 'settingsToggle' }), - className: classNamesForComponentSelector(state, 'SettingsToggle'), - style: stylesForComponentSelector(state, 'SettingsToggle'), - }), - { - toggleSettings: toggleSettingsAction - } - ), -)(props => ); - -export default enhancedSettingsToggle; diff --git a/src/components/SettingsWrapperContainer.js b/src/components/SettingsWrapperContainer.js deleted file mode 100644 index 8475ad60..00000000 --- a/src/components/SettingsWrapperContainer.js +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; -import compose from 'recompose/compose'; -import mapProps from 'recompose/mapProps'; -import getContext from 'recompose/getContext'; - -import { isSettingsEnabledSelector, isSettingsVisibleSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; - -const EnhancedSettingsWrapper = OriginalComponent => compose( - getContext({ - components: PropTypes.object, - }), - mapProps(props => ({ - Settings: props.components.Settings, - SettingsToggle: props.components.SettingsToggle - })), - connect((state, props) => ({ - isEnabled: isSettingsEnabledSelector(state), - isVisible: isSettingsVisibleSelector(state), - className: classNamesForComponentSelector(state, 'SettingsWrapper'), - style: stylesForComponentSelector(state, 'SettingsWrapper'), - })) -)(props => ( - -)); - -export default EnhancedSettingsWrapper; diff --git a/src/components/TableContainer.js b/src/components/TableContainer.js deleted file mode 100644 index 7c754851..00000000 --- a/src/components/TableContainer.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; -import compose from 'recompose/compose'; -import mapProps from 'recompose/mapProps'; -import getContext from 'recompose/getContext'; - -import { classNamesForComponentSelector, stylesForComponentSelector, dataLoadingSelector, visibleRowCountSelector } from '../selectors/dataSelectors'; - -const ComposedContainerComponent = OriginalComponent => compose( - getContext( - { - components: PropTypes.object - }), - //TODO: Should we use withHandlers here instead? I realize that's not 100% the intent of that method - mapProps(props => ({ - TableHeading: props.components.TableHeading, - TableBody: props.components.TableBody, - Loading: props.components.Loading, - NoResults: props.components.NoResults, - })), - connect( - (state, props) => ({ - dataLoading: dataLoadingSelector(state), - visibleRows: visibleRowCountSelector(state), - className: classNamesForComponentSelector(state, 'Table'), - style: stylesForComponentSelector(state, 'Table'), - }) - ), -)(props => ); - -export default ComposedContainerComponent; diff --git a/src/index.js b/src/index.js index a898eead..cd8724e1 100644 --- a/src/index.js +++ b/src/index.js @@ -5,55 +5,26 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import _ from 'lodash'; -import * as dataReducers from './reducers/dataReducer'; -import components from './components'; +//import * as dataReducers from './reducers/dataReducer'; +//import components from './components'; import settingsComponentObjects from './settingsComponentObjects'; -import * as selectors from './selectors/dataSelectors'; +//import * as selectors from './selectors/dataSelectors'; import { buildGriddleReducer, buildGriddleComponents } from './utils/compositionUtils'; -import { getColumnProperties } from './utils/columnUtils'; -import { getRowProperties } from './utils/rowUtils'; +//import { getColumnProperties } from './utils/columnUtils'; +//import { getRowProperties } from './utils/rowUtils'; import { setSortProperties } from './utils/sortUtils'; import { StoreListener } from './utils/listenerUtils'; -import * as actions from './actions'; - -const defaultEvents = { - ...actions, - onFilter: actions.setFilter, - setSortProperties -}; - - -const defaultStyleConfig = { - icons: { - TableHeadingCell: { - sortDescendingIcon: '▼', - sortAscendingIcon: '▲' - }, - }, - classNames: { - Cell: 'griddle-cell', - Filter: 'griddle-filter', - Loading: 'griddle-loadingResults', - NextButton: 'griddle-next-button', - NoResults: 'griddle-noResults', - PageDropdown: 'griddle-page-select', - Pagination: 'griddle-pagination', - PreviousButton: 'griddle-previous-button', - Row: 'griddle-row', - RowDefinition: 'griddle-row-definition', - Settings: 'griddle-settings', - SettingsToggle: 'griddle-settings-toggle', - Table: 'griddle-table', - TableBody: 'griddle-table-body', - TableHeading: 'griddle-table-heading', - TableHeadingCell: 'griddle-table-heading-cell', - TableHeadingCellAscending: 'griddle-heading-ascending', - TableHeadingCellDescending: 'griddle-heading-descending', - }, - styles: { - } -}; +import { composeSelectors } from './utils/selectorUtils'; +//import * as actions from './actions'; + +import CorePlugin from './plugins/core'; + +//const defaultEvents = { +// ...actions, +// onFilter: actions.setFilter, +// setSortProperties +//}; class Griddle extends Component { static childContextTypes = { @@ -62,13 +33,15 @@ class Griddle extends Component { events: PropTypes.object, selectors: PropTypes.object, storeKey: PropTypes.string, - storeListener: PropTypes.object + storeListener: PropTypes.object, + actions: PropTypes.object, } constructor(props) { super(props); const { + baselinePlugin=CorePlugin, plugins=[], data, children:rowPropertiesComponent, @@ -85,23 +58,40 @@ class Griddle extends Component { ...userInitialState } = props; - const rowProperties = getRowProperties(rowPropertiesComponent); - const columnProperties = getColumnProperties(rowPropertiesComponent); + switch(typeof baselinePlugin) { + case 'function': + plugins.unshift(baselinePlugin(props)); + break; + case 'object': + plugins.unshift(baselinePlugin); + break; + }; + + this.plugins = plugins; //Combine / compose the reducers to make a single, unified reducer - const reducers = buildGriddleReducer([dataReducers, ...plugins.map(p => p.reducer)]); + //const reducers = buildGriddleReducer([dataReducers, ...plugins.map(p => p.reducer)]); + const reducers = buildGriddleReducer([...plugins.map(p => p.reducer)]); //Combine / Compose the components to make a single component for each component type - this.components = buildGriddleComponents([components, ...plugins.map(p => p.components), userComponents]); + //this.components = buildGriddleComponents([components, ...plugins.map(p => p.components), userComponents]); + this.components = buildGriddleComponents([...plugins.map(p => p.components), userComponents]); - this.settingsComponentObjects = Object.assign({}, settingsComponentObjects, ...plugins.map(p => p.settingsComponentObjects), userSettingsComponentObjects); + // NOTE this goes on the context which for the purposes of breaking out the + // 'core' code into a plugin is somewhat of a problem as it should + // be associated with the core code not general griddle code. + this.settingsComponentObjects = Object.assign({}, ...plugins.map(p => p.settingsComponentObjects), userSettingsComponentObjects); this.events = Object.assign({}, events, ...plugins.map(p => p.events)); - this.selectors = plugins.reduce((combined, plugin) => ({ ...combined, ...plugin.selectors }), {...selectors}); + this.selectors = composeSelectors(plugins); + + this.actions = plugins.reduce((combined, plugin) => ({ ...combined, ...plugin.actions }), {}); - const mergedStyleConfig = _.merge({}, defaultStyleConfig, ...plugins.map(p => p.styleConfig), styleConfig); + const mergedStyleConfig = _.merge({}, ...plugins.map(p => p.styleConfig), styleConfig); + // this would be good to move into the core plugin + // and namespace this state to the core plugin const pageProperties = Object.assign({}, { currentPage: 1, pageSize: 10 @@ -110,21 +100,15 @@ class Griddle extends Component { ); //TODO: This should also look at the default and plugin initial state objects - const renderProperties = Object.assign({ - rowProperties, - columnProperties - }, ...plugins.map(p => p.renderProperties), userRenderProperties); + const renderProperties = Object.assign(...plugins.map(p => p.renderProperties), userRenderProperties); // TODO: Make this its own method + // It would be nice if state was namespaced to the plugin + // it was associated with. For example pageProperties and + // sortProperties are specific to the core plugin. We could + // refactor the selectors to grab this data from a different + // place but would this affect other users? const initialState = _.merge( - { - enableSettings: true, - textProperties: { - next: 'Next', - previous: 'Previous', - settingsToggle: 'Settings' - }, - }, ...plugins.map(p => p.initialState), userInitialState, { @@ -161,8 +145,12 @@ class Griddle extends Component { }) // Only update the state if something has changed. + // + // NOTE the update state reducer in 'core' griddle is only + // concerned with the data, pageProperties, and sortProperties + // passing in only changed props breaks the contract it is expecting if (Object.keys(newState).length > 0) { - this.store.dispatch(actions.updateState(newState)); + this.store.dispatch(this.plugins[0].actions.updateState(newState)); } } @@ -183,7 +171,8 @@ class Griddle extends Component { events: this.events, selectors: this.selectors, storeKey: this.getStoreKey(), - storeListener: this.storeListener + storeListener: this.storeListener, + actions: this.actions, }; } diff --git a/src/module.d.ts b/src/module.d.ts index b25f4af4..019f6874 100644 --- a/src/module.d.ts +++ b/src/module.d.ts @@ -425,6 +425,7 @@ export namespace utils { const compositionUtils: PropertyBag; const dataUtils: PropertyBag; const rowUtils: PropertyBag; + const selectorUtils: PropertyBag; const connect : typeof originalConnect; diff --git a/src/module.js b/src/module.js index a5d36a0c..e429616d 100644 --- a/src/module.js +++ b/src/module.js @@ -1,9 +1,9 @@ import Griddle from './index'; -import * as actions from './actions'; -import components from './components'; -import * as constants from './constants'; -import * as selectors from './selectors/dataSelectors'; +import * as actions from './plugins/core/actions'; +import components from './plugins/core/components'; +import * as constants from './plugins/core/constants'; +import * as selectors from './plugins/core/selectors/dataSelectors'; import settingsComponentObjects from './settingsComponentObjects'; import utils from './utils'; @@ -14,7 +14,7 @@ import PositionPlugin from './plugins/position'; const plugins = { LegacyStylePlugin, LocalPlugin, - PositionPlugin, + PositionPlugin }; const ColumnDefinition = components.ColumnDefinition; diff --git a/src/actions/index.js b/src/plugins/core/actions/index.js similarity index 100% rename from src/actions/index.js rename to src/plugins/core/actions/index.js diff --git a/src/components/Cell.js b/src/plugins/core/components/Cell.js similarity index 100% rename from src/components/Cell.js rename to src/plugins/core/components/Cell.js diff --git a/src/components/CellContainer.js b/src/plugins/core/components/CellContainer.js similarity index 67% rename from src/components/CellContainer.js rename to src/plugins/core/components/CellContainer.js index b4eaf5ed..5ac8180a 100644 --- a/src/components/CellContainer.js +++ b/src/plugins/core/components/CellContainer.js @@ -1,18 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; import getContext from 'recompose/getContext'; import mapProps from 'recompose/mapProps'; import compose from 'recompose/compose'; -import { - customComponentSelector, - cellValueSelector, - cellPropertiesSelectorFactory, - classNamesForComponentSelector, - stylesForComponentSelector -} from '../selectors/dataSelectors'; -import { valueOrResult } from '../utils/valueUtils'; +// TODO: refactor this onto the context. We need to handle factories as well +import { cellPropertiesSelectorFactory } from '../selectors/dataSelectors'; +import { connect } from '../../../utils/griddleConnect'; +import { valueOrResult } from '../../../utils/valueUtils'; function hasWidthOrStyles(cellProperties) { return cellProperties.hasOwnProperty('width') || cellProperties.hasOwnProperty('styles'); @@ -36,19 +31,23 @@ function getCellStyles(cellProperties, originalStyles) { } const mapStateToProps = () => { + // TODO: selector factories on the context const cellPropertiesSelector = cellPropertiesSelectorFactory(); return (state, props) => { return { - value: cellValueSelector(state, props), - customComponent: customComponentSelector(state, props), - cellProperties: cellPropertiesSelector(state, props), - className: classNamesForComponentSelector(state, 'Cell'), - style: stylesForComponentSelector(state, 'Cell'), + value: props.selectors.cellValueSelector(state, props), + customComponent: props.selectors.customComponentSelector(state, props), + cellProperties: props.selectors.cellPropertiesSelector(state, props), + className: props.selectors.classNamesForComponentSelector(state, 'Cell'), + style: props.selectors.stylesForComponentSelector(state, 'Cell'), }; }; } const ComposedCellContainer = OriginalComponent => compose( + getContext({ + selectors: PropTypes.object + }), connect(mapStateToProps), mapProps(props => { return ({ diff --git a/src/components/ColumnDefinition.js b/src/plugins/core/components/ColumnDefinition.js similarity index 100% rename from src/components/ColumnDefinition.js rename to src/plugins/core/components/ColumnDefinition.js diff --git a/src/components/Filter.js b/src/plugins/core/components/Filter.js similarity index 100% rename from src/components/Filter.js rename to src/plugins/core/components/Filter.js diff --git a/src/plugins/core/components/FilterContainer.js b/src/plugins/core/components/FilterContainer.js new file mode 100644 index 00000000..297fd25a --- /dev/null +++ b/src/plugins/core/components/FilterContainer.js @@ -0,0 +1,24 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import compose from 'recompose/compose'; +import getContext from 'recompose/getContext'; + +import { connect } from '../../../utils/griddleConnect'; + +const EnhancedFilter = OriginalComponent => compose( + getContext({ + selectors: PropTypes.object, + actions: PropTypes.object, + }), + connect( + (state, props) => ({ + className: props.selectors.classNamesForComponentSelector(state, 'Filter'), + style: props.selectors.stylesForComponentSelector(state, 'Filter'), + }), + (dispatch, props) => ({ + setFilter: (filter) => dispatch(props.actions.setFilter(filter)), + }) + ) +)(props => ); + +export default EnhancedFilter; diff --git a/src/components/FilterEnhancer.js b/src/plugins/core/components/FilterEnhancer.js similarity index 88% rename from src/components/FilterEnhancer.js rename to src/plugins/core/components/FilterEnhancer.js index 7ca9dad0..1d297f12 100644 --- a/src/components/FilterEnhancer.js +++ b/src/plugins/core/components/FilterEnhancer.js @@ -4,7 +4,7 @@ import getContext from 'recompose/getContext'; import mapProps from 'recompose/mapProps'; import compose from 'recompose/compose'; -import { combineHandlers } from '../utils/compositionUtils'; +import { combineHandlers } from '../../../utils/compositionUtils'; const EnhancedFilter = OriginalComponent => compose( getContext({ diff --git a/src/components/Layout.js b/src/plugins/core/components/Layout.js similarity index 100% rename from src/components/Layout.js rename to src/plugins/core/components/Layout.js diff --git a/src/components/LayoutContainer.js b/src/plugins/core/components/LayoutContainer.js similarity index 72% rename from src/components/LayoutContainer.js rename to src/plugins/core/components/LayoutContainer.js index e44570b6..044d5ff5 100644 --- a/src/components/LayoutContainer.js +++ b/src/plugins/core/components/LayoutContainer.js @@ -1,20 +1,20 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; import getContext from 'recompose/getContext'; import mapProps from 'recompose/mapProps'; import compose from 'recompose/compose'; -import { classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; +import { connect } from '../../../utils/griddleConnect'; const EnhancedLayout = OriginalComponent => compose( getContext({ components: PropTypes.object, + selectors: PropTypes.object }), connect( (state, props) => ({ - className: classNamesForComponentSelector(state, 'Layout'), - style: stylesForComponentSelector(state, 'Layout'), + className: props.selectors.classNamesForComponentSelector(state, 'Layout'), + style: props.selectors.stylesForComponentSelector(state, 'Layout'), }) ), mapProps( props => ({ diff --git a/src/components/Loading.js b/src/plugins/core/components/Loading.js similarity index 100% rename from src/components/Loading.js rename to src/plugins/core/components/Loading.js diff --git a/src/components/LoadingContainer.js b/src/plugins/core/components/LoadingContainer.js similarity index 61% rename from src/components/LoadingContainer.js rename to src/plugins/core/components/LoadingContainer.js index 3c17b6d0..16f6b0c3 100644 --- a/src/components/LoadingContainer.js +++ b/src/plugins/core/components/LoadingContainer.js @@ -1,19 +1,19 @@ import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; -import { classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; +import { connect } from '../../../utils/griddleConnect'; const LoadingContainer = compose( getContext({ components: PropTypes.object, + selectors: PropTypes.object, }), connect( - state => ({ - className: classNamesForComponentSelector(state, 'Loading'), - style: stylesForComponentSelector(state, 'Loading'), + (state, props) => ({ + className: props.selectors.classNamesForComponentSelector(state, 'Loading'), + style: props.selectors.stylesForComponentSelector(state, 'Loading'), }) ), mapProps((props) => { diff --git a/src/components/NextButton.js b/src/plugins/core/components/NextButton.js similarity index 100% rename from src/components/NextButton.js rename to src/plugins/core/components/NextButton.js diff --git a/src/plugins/core/components/NextButtonContainer.js b/src/plugins/core/components/NextButtonContainer.js new file mode 100644 index 00000000..34bb6d29 --- /dev/null +++ b/src/plugins/core/components/NextButtonContainer.js @@ -0,0 +1,20 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import compose from 'recompose/compose'; +import getContext from 'recompose/getContext'; + +import { connect } from '../../../utils/griddleConnect'; + +const enhance = OriginalComponent => compose( + getContext({ + selectors: PropTypes.object + }), + connect((state, props) => ({ + text: props.selectors.textSelector(state, { key: 'next' }), + hasNext: props.selectors.hasNextSelector(state, props), + className: props.selectors.classNamesForComponentSelector(state, 'NextButton'), + style: props.selectors.stylesForComponentSelector(state, 'NextButton'), + })) +)((props) => ); + +export default enhance; diff --git a/src/components/NextButtonEnhancer.js b/src/plugins/core/components/NextButtonEnhancer.js similarity index 87% rename from src/components/NextButtonEnhancer.js rename to src/plugins/core/components/NextButtonEnhancer.js index 46ae444f..c0ef0ada 100644 --- a/src/components/NextButtonEnhancer.js +++ b/src/plugins/core/components/NextButtonEnhancer.js @@ -3,7 +3,8 @@ import PropTypes from 'prop-types'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; -import { combineHandlers } from '../utils/compositionUtils'; + +import { combineHandlers } from '../../../utils/compositionUtils'; const enhance = OriginalComponent => compose( getContext({ diff --git a/src/components/NoResults.js b/src/plugins/core/components/NoResults.js similarity index 100% rename from src/components/NoResults.js rename to src/plugins/core/components/NoResults.js diff --git a/src/components/NoResultsContainer.js b/src/plugins/core/components/NoResultsContainer.js similarity index 65% rename from src/components/NoResultsContainer.js rename to src/plugins/core/components/NoResultsContainer.js index 2496b0e5..a7517060 100644 --- a/src/components/NoResultsContainer.js +++ b/src/plugins/core/components/NoResultsContainer.js @@ -1,20 +1,20 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; -import { classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; +import { connect } from '../../../utils/griddleConnect'; const NoResultsContainer = OriginalComponent => compose( getContext({ components: PropTypes.object, + selectors: PropTypes.object }), connect( - state => ({ - className: classNamesForComponentSelector(state, 'NoResults'), - style: stylesForComponentSelector(state, 'NoResults'), + (state, props) => ({ + className: props.selectors.classNamesForComponentSelector(state, 'NoResults'), + style: props.selectors.stylesForComponentSelector(state, 'NoResults'), }) ), mapProps((props) => { diff --git a/src/components/PageDropdown.js b/src/plugins/core/components/PageDropdown.js similarity index 100% rename from src/components/PageDropdown.js rename to src/plugins/core/components/PageDropdown.js diff --git a/src/components/PageDropdownContainer.js b/src/plugins/core/components/PageDropdownContainer.js similarity index 50% rename from src/components/PageDropdownContainer.js rename to src/plugins/core/components/PageDropdownContainer.js index 88b2bd8c..cef5e830 100644 --- a/src/components/PageDropdownContainer.js +++ b/src/plugins/core/components/PageDropdownContainer.js @@ -1,21 +1,24 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; -import { currentPageSelector, maxPageSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; + +import { connect } from '../../../utils/griddleConnect'; const enhance = OriginalComponent => compose( getContext({ events: PropTypes.object, + selectors: PropTypes.object }), - connect((state, props) => ({ - maxPages: maxPageSelector(state, props), - currentPage: currentPageSelector(state, props), - className: classNamesForComponentSelector(state, 'PageDropdown'), - style: stylesForComponentSelector(state, 'PageDropdown'), - })), + connect( + (state, props) => ({ + maxPages: props.selectors.maxPageSelector(state, props), + currentPage: props.selectors.currentPageSelector(state, props), + className: props.selectors.classNamesForComponentSelector(state, 'PageDropdown'), + style: props.selectors.stylesForComponentSelector(state, 'PageDropdown'), + }) + ), mapProps(({ events: { onGetPage: setPage }, ...props }) => ({ ...props, setPage, diff --git a/src/components/Pagination.js b/src/plugins/core/components/Pagination.js similarity index 100% rename from src/components/Pagination.js rename to src/plugins/core/components/Pagination.js diff --git a/src/components/PaginationContainer.js b/src/plugins/core/components/PaginationContainer.js similarity index 70% rename from src/components/PaginationContainer.js rename to src/plugins/core/components/PaginationContainer.js index c82b570f..fc351754 100644 --- a/src/components/PaginationContainer.js +++ b/src/plugins/core/components/PaginationContainer.js @@ -1,20 +1,20 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; -import { classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; +import { connect } from '../../../utils/griddleConnect'; const EnhancedPaginationContainer = OriginalComponent => compose( getContext({ components: PropTypes.object, + selectors: PropTypes.object }), connect( (state, props) => ({ - className: classNamesForComponentSelector(state, 'Pagination'), - style: stylesForComponentSelector(state, 'Pagination'), + className: props.selectors.classNamesForComponentSelector(state, 'Pagination'), + style: props.selectors.stylesForComponentSelector(state, 'Pagination'), }) ), mapProps((props) => { diff --git a/src/components/PreviousButton.js b/src/plugins/core/components/PreviousButton.js similarity index 100% rename from src/components/PreviousButton.js rename to src/plugins/core/components/PreviousButton.js diff --git a/src/plugins/core/components/PreviousButtonContainer.js b/src/plugins/core/components/PreviousButtonContainer.js new file mode 100644 index 00000000..f3600eee --- /dev/null +++ b/src/plugins/core/components/PreviousButtonContainer.js @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import compose from 'recompose/compose'; +import getContext from 'recompose/getContext'; + +import { connect } from '../../../utils/griddleConnect'; + +const enhance = OriginalComponent => compose( + getContext({ + selectors: PropTypes.object + }), + connect( + (state, props) => ({ + text: props.selectors.textSelector(state, { key: 'previous' }), + hasPrevious: props.selectors.hasPreviousSelector(state, props), + className: props.selectors.classNamesForComponentSelector(state, 'PreviousButton'), + style: props.selectors.stylesForComponentSelector(state, 'PreviousButton'), + }) + ) +)((props) => ); + +export default enhance; diff --git a/src/components/PreviousButtonEnhancer.js b/src/plugins/core/components/PreviousButtonEnhancer.js similarity index 88% rename from src/components/PreviousButtonEnhancer.js rename to src/plugins/core/components/PreviousButtonEnhancer.js index 0de9ae02..19e25f14 100644 --- a/src/components/PreviousButtonEnhancer.js +++ b/src/plugins/core/components/PreviousButtonEnhancer.js @@ -3,7 +3,8 @@ import PropTypes from 'prop-types'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; -import { combineHandlers } from '../utils/compositionUtils'; + +import { combineHandlers } from '../../../utils/compositionUtils'; const enhance = OriginalComponent => compose( getContext({ diff --git a/src/components/Row.js b/src/plugins/core/components/Row.js similarity index 100% rename from src/components/Row.js rename to src/plugins/core/components/Row.js diff --git a/src/components/RowContainer.js b/src/plugins/core/components/RowContainer.js similarity index 52% rename from src/components/RowContainer.js rename to src/plugins/core/components/RowContainer.js index 5006a78f..5930f860 100644 --- a/src/components/RowContainer.js +++ b/src/plugins/core/components/RowContainer.js @@ -1,30 +1,26 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; -import { - columnIdsSelector, - rowDataSelector, - rowPropertiesSelector, - classNamesForComponentSelector, - stylesForComponentSelector, -} from '../selectors/dataSelectors'; -import { valueOrResult } from '../utils/valueUtils'; +import { connect } from '../../../utils/griddleConnect'; +import { valueOrResult } from '../../../utils/valueUtils'; const ComposedRowContainer = OriginalComponent => compose( getContext({ components: PropTypes.object, + selectors: PropTypes.object }), - connect((state, props) => ({ - columnIds: columnIdsSelector(state), - rowProperties: rowPropertiesSelector(state), - rowData: rowDataSelector(state, props), - className: classNamesForComponentSelector(state, 'Row'), - style: stylesForComponentSelector(state, 'Row'), - })), + connect( + (state, props) => ({ + columnIds: props.selectors.columnIdsSelector(state), + rowProperties: props.selectors.rowPropertiesSelector(state), + rowData: props.selectors.rowDataSelector(state, props), + className: props.selectors.classNamesForComponentSelector(state, 'Row'), + style: props.selectors.stylesForComponentSelector(state, 'Row'), + }) + ), mapProps(props => { const { components, rowProperties, className, ...otherProps } = props; return { @@ -35,7 +31,7 @@ const ComposedRowContainer = OriginalComponent => compose( }), )(props => ( )); diff --git a/src/components/RowDefinition.js b/src/plugins/core/components/RowDefinition.js similarity index 100% rename from src/components/RowDefinition.js rename to src/plugins/core/components/RowDefinition.js diff --git a/src/components/Settings.js b/src/plugins/core/components/Settings.js similarity index 100% rename from src/components/Settings.js rename to src/plugins/core/components/Settings.js diff --git a/src/components/SettingsContainer.js b/src/plugins/core/components/SettingsContainer.js similarity index 81% rename from src/components/SettingsContainer.js rename to src/plugins/core/components/SettingsContainer.js index c88801df..04b6c342 100644 --- a/src/components/SettingsContainer.js +++ b/src/plugins/core/components/SettingsContainer.js @@ -1,11 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; -import { classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; +import { connect } from '../../../utils/griddleConnect'; function getSettingsComponentsArrayFromObject(settingsObject, settingsComponents) { //TODO: determine if we need to make this faster @@ -20,12 +19,13 @@ function getSettingsComponentsArrayFromObject(settingsObject, settingsComponents const EnhancedSettings = OriginalComponent => compose( getContext({ components: PropTypes.object, + selectors: PropTypes.object, settingsComponentObjects: PropTypes.object }), connect( (state, props) => ({ - className: classNamesForComponentSelector(state, 'Settings'), - style: stylesForComponentSelector(state, 'Settings'), + className: props.selectors.classNamesForComponentSelector(state, 'Settings'), + style: props.selectors.stylesForComponentSelector(state, 'Settings'), }) ), mapProps(props => { diff --git a/src/components/SettingsToggle.js b/src/plugins/core/components/SettingsToggle.js similarity index 100% rename from src/components/SettingsToggle.js rename to src/plugins/core/components/SettingsToggle.js diff --git a/src/plugins/core/components/SettingsToggleContainer.js b/src/plugins/core/components/SettingsToggleContainer.js new file mode 100644 index 00000000..3c99b00a --- /dev/null +++ b/src/plugins/core/components/SettingsToggleContainer.js @@ -0,0 +1,28 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import compose from 'recompose/compose'; +import getContext from 'recompose/getContext'; + +import { connect } from '../../../utils/griddleConnect'; + +const enhancedSettingsToggle = OriginalComponent => compose( + getContext({ + selectors: PropTypes.object, + actions: PropTypes.object, + }), + connect( + (state, props) => ({ + text: props.selectors.textSelector(state, { key: 'settingsToggle' }), + className: props.selectors.classNamesForComponentSelector(state, 'SettingsToggle'), + style: props.selectors.stylesForComponentSelector(state, 'SettingsToggle'), + }), + (dispatch, props) => ({ + toggleSettings: () => dispatch(props.actions.toggleSettings()) + }) + ), +)(props => ); + +export default enhancedSettingsToggle; diff --git a/src/components/SettingsWrapper.js b/src/plugins/core/components/SettingsWrapper.js similarity index 100% rename from src/components/SettingsWrapper.js rename to src/plugins/core/components/SettingsWrapper.js diff --git a/src/plugins/core/components/SettingsWrapperContainer.js b/src/plugins/core/components/SettingsWrapperContainer.js new file mode 100644 index 00000000..e84ba06f --- /dev/null +++ b/src/plugins/core/components/SettingsWrapperContainer.js @@ -0,0 +1,34 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import compose from 'recompose/compose'; +import mapProps from 'recompose/mapProps'; +import getContext from 'recompose/getContext'; + +import { connect } from '../../../utils/griddleConnect'; + +const EnhancedSettingsWrapper = OriginalComponent => compose( + getContext({ + components: PropTypes.object, + selectors: PropTypes.object, + }), + connect( + (state, props) => ({ + isEnabled: props.selectors.isSettingsEnabledSelector(state), + isVisible: props.selectors.isSettingsVisibleSelector(state), + className: props.selectors.classNamesForComponentSelector(state, 'SettingsWrapper'), + style: props.selectors.stylesForComponentSelector(state, 'SettingsWrapper'), + }) + ), + mapProps(props => { + const { components, ...otherProps } = props; + return { + Settings: components.Settings, + SettingsToggle: components.SettingsToggle, + ...otherProps + } + }) +)(props => ( + +)); + +export default EnhancedSettingsWrapper; diff --git a/src/components/Table.js b/src/plugins/core/components/Table.js similarity index 100% rename from src/components/Table.js rename to src/plugins/core/components/Table.js diff --git a/src/components/TableBody.js b/src/plugins/core/components/TableBody.js similarity index 100% rename from src/components/TableBody.js rename to src/plugins/core/components/TableBody.js diff --git a/src/components/TableBodyContainer.js b/src/plugins/core/components/TableBodyContainer.js similarity index 52% rename from src/components/TableBodyContainer.js rename to src/plugins/core/components/TableBodyContainer.js index 590aae9b..98cf7447 100644 --- a/src/components/TableBodyContainer.js +++ b/src/plugins/core/components/TableBodyContainer.js @@ -1,24 +1,25 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; -import { visibleRowIdsSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; +import { connect } from '../../../utils/griddleConnect'; const ComposedTableBodyContainer = OriginalComponent => compose( getContext({ components: PropTypes.object, selectors: PropTypes.object, }), - connect((state, props) => ({ - visibleRowIds: visibleRowIdsSelector(state), - className: classNamesForComponentSelector(state, 'TableBody'), - style: stylesForComponentSelector(state, 'TableBody'), - })), + connect( + (state, props) => ({ + visibleRowIds: props.selectors.visibleRowIdsSelector(state), + className: props.selectors.classNamesForComponentSelector(state, 'TableBody'), + style: props.selectors.stylesForComponentSelector(state, 'TableBody'), + }) + ), mapProps(props => { - const { components, ...otherProps } = props; + const { components, selectors, ...otherProps } = props; return { Row: props.components.Row, ...otherProps, @@ -26,10 +27,10 @@ const ComposedTableBodyContainer = OriginalComponent => compose( }), )(({Row, visibleRowIds, style, className}) => ( )); diff --git a/src/plugins/core/components/TableContainer.js b/src/plugins/core/components/TableContainer.js new file mode 100644 index 00000000..19a4d2f1 --- /dev/null +++ b/src/plugins/core/components/TableContainer.js @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import compose from 'recompose/compose'; +import mapProps from 'recompose/mapProps'; +import getContext from 'recompose/getContext'; + +import { connect } from '../../../utils/griddleConnect'; + +const ComposedContainerComponent = OriginalComponent => compose( + getContext({ + components: PropTypes.object, + selectors: PropTypes.object + }), + //TODO: Should we use withHandlers here instead? I realize that's not 100% the intent of that method + connect( + (state, props) => ({ + dataLoading: props.selectors.dataLoadingSelector(state), + visibleRows: props.selectors.visibleRowCountSelector(state), + className: props.selectors.classNamesForComponentSelector(state, 'Table'), + style: props.selectors.stylesForComponentSelector(state, 'Table'), + }) + ), + //TODO: Should we use withHandlers here instead? I realize that's not 100% the intent of that method + mapProps(props => { + const { components, dataLoading, visibleRows, className, style } = props; + return { + TableHeading: components.TableHeading, + TableBody: components.TableBody, + Loading: components.Loading, + NoResults: components.NoResults, + dataLoading, + visibleRows, + className, + style + } + }) +)(props => ); + +export default ComposedContainerComponent; diff --git a/src/components/TableHeading.js b/src/plugins/core/components/TableHeading.js similarity index 100% rename from src/components/TableHeading.js rename to src/plugins/core/components/TableHeading.js diff --git a/src/components/TableHeadingCell.js b/src/plugins/core/components/TableHeadingCell.js similarity index 100% rename from src/components/TableHeadingCell.js rename to src/plugins/core/components/TableHeadingCell.js diff --git a/src/components/TableHeadingCellContainer.js b/src/plugins/core/components/TableHeadingCellContainer.js similarity index 62% rename from src/components/TableHeadingCellContainer.js rename to src/plugins/core/components/TableHeadingCellContainer.js index 02121426..70d8018e 100644 --- a/src/components/TableHeadingCellContainer.js +++ b/src/plugins/core/components/TableHeadingCellContainer.js @@ -1,13 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; -import { sortPropertyByIdSelector, iconsForComponentSelector, classNamesForComponentSelector, stylesForComponentSelector, customHeadingComponentSelector, cellPropertiesSelector } from '../selectors/dataSelectors'; -import { getSortIconProps } from '../utils/sortUtils'; -import { valueOrResult } from '../utils/valueUtils'; +import { connect } from '../../../utils/griddleConnect'; +import { getSortIconProps } from '../../../utils/sortUtils'; +import { valueOrResult } from '../../../utils/valueUtils'; const DefaultTableHeadingCellContent = ({title, icon}) => ( @@ -22,12 +21,12 @@ const EnhancedHeadingCell = OriginalComponent => compose( }), connect( (state, props) => ({ - sortProperty: sortPropertyByIdSelector(state, props), - customHeadingComponent: customHeadingComponentSelector(state, props), - cellProperties: cellPropertiesSelector(state, props), - className: classNamesForComponentSelector(state, 'TableHeadingCell'), - style: stylesForComponentSelector(state, 'TableHeadingCell'), - ...iconsForComponentSelector(state, 'TableHeadingCell'), + sortProperty: props.selectors.sortPropertyByIdSelector(state, props), + customHeadingComponent: props.selectors.customHeadingComponentSelector(state, props), + cellProperties: props.selectors.cellPropertiesSelector(state, props), + className: props.selectors.classNamesForComponentSelector(state, 'TableHeadingCell'), + style: props.selectors.stylesForComponentSelector(state, 'TableHeadingCell'), + ...props.selectors.iconsForComponentSelector(state, 'TableHeadingCell'), }) ), mapProps(props => { diff --git a/src/components/TableHeadingCellEnhancer.js b/src/plugins/core/components/TableHeadingCellEnhancer.js similarity index 89% rename from src/components/TableHeadingCellEnhancer.js rename to src/plugins/core/components/TableHeadingCellEnhancer.js index 3ee6c757..c9c47f0b 100644 --- a/src/components/TableHeadingCellEnhancer.js +++ b/src/plugins/core/components/TableHeadingCellEnhancer.js @@ -3,7 +3,8 @@ import PropTypes from 'prop-types'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; -import { combineHandlers } from '../utils/compositionUtils'; + +import { combineHandlers } from '../../../utils/compositionUtils'; const EnhancedHeadingCell = OriginalComponent => compose( getContext({ diff --git a/src/components/TableHeadingContainer.js b/src/plugins/core/components/TableHeadingContainer.js similarity index 61% rename from src/components/TableHeadingContainer.js rename to src/plugins/core/components/TableHeadingContainer.js index a7bf1377..1b9feede 100644 --- a/src/components/TableHeadingContainer.js +++ b/src/plugins/core/components/TableHeadingContainer.js @@ -1,11 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; -import { columnTitlesSelector, columnIdsSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; +import { connect } from '../../../utils/griddleConnect'; const ComposedContainerComponent = OriginalComponent => compose( getContext({ @@ -13,10 +12,10 @@ const ComposedContainerComponent = OriginalComponent => compose( selectors: PropTypes.object, }), connect((state, props) => ({ - columnTitles: columnTitlesSelector(state), - columnIds: columnIdsSelector(state), - className: classNamesForComponentSelector(state, 'TableHeading'), - style: stylesForComponentSelector(state, 'TableHeading'), + columnTitles: props.selectors.columnTitlesSelector(state), + columnIds: props.selectors.columnIdsSelector(state), + className: props.selectors.classNamesForComponentSelector(state, 'TableHeading'), + style: props.selectors.stylesForComponentSelector(state, 'TableHeading'), })), mapProps(props => { const { components, ...otherProps } = props; diff --git a/src/components/Test.js b/src/plugins/core/components/Test.js similarity index 100% rename from src/components/Test.js rename to src/plugins/core/components/Test.js diff --git a/src/components/__tests__/CellTest.js b/src/plugins/core/components/__tests__/CellTest.js similarity index 100% rename from src/components/__tests__/CellTest.js rename to src/plugins/core/components/__tests__/CellTest.js diff --git a/src/components/__tests__/FilterTest.js b/src/plugins/core/components/__tests__/FilterTest.js similarity index 100% rename from src/components/__tests__/FilterTest.js rename to src/plugins/core/components/__tests__/FilterTest.js diff --git a/src/components/__tests__/NextButtonTest.js b/src/plugins/core/components/__tests__/NextButtonTest.js similarity index 100% rename from src/components/__tests__/NextButtonTest.js rename to src/plugins/core/components/__tests__/NextButtonTest.js diff --git a/src/components/__tests__/PageDropdownTest.js b/src/plugins/core/components/__tests__/PageDropdownTest.js similarity index 100% rename from src/components/__tests__/PageDropdownTest.js rename to src/plugins/core/components/__tests__/PageDropdownTest.js diff --git a/src/components/__tests__/PaginationTest.js b/src/plugins/core/components/__tests__/PaginationTest.js similarity index 100% rename from src/components/__tests__/PaginationTest.js rename to src/plugins/core/components/__tests__/PaginationTest.js diff --git a/src/components/__tests__/PreviousButtonTest.js b/src/plugins/core/components/__tests__/PreviousButtonTest.js similarity index 100% rename from src/components/__tests__/PreviousButtonTest.js rename to src/plugins/core/components/__tests__/PreviousButtonTest.js diff --git a/src/components/__tests__/RowTest.js b/src/plugins/core/components/__tests__/RowTest.js similarity index 100% rename from src/components/__tests__/RowTest.js rename to src/plugins/core/components/__tests__/RowTest.js diff --git a/src/components/__tests__/SettingsTest.js b/src/plugins/core/components/__tests__/SettingsTest.js similarity index 100% rename from src/components/__tests__/SettingsTest.js rename to src/plugins/core/components/__tests__/SettingsTest.js diff --git a/src/components/__tests__/SettingsToggleTest.js b/src/plugins/core/components/__tests__/SettingsToggleTest.js similarity index 100% rename from src/components/__tests__/SettingsToggleTest.js rename to src/plugins/core/components/__tests__/SettingsToggleTest.js diff --git a/src/components/__tests__/SettingsWrapperTest.js b/src/plugins/core/components/__tests__/SettingsWrapperTest.js similarity index 100% rename from src/components/__tests__/SettingsWrapperTest.js rename to src/plugins/core/components/__tests__/SettingsWrapperTest.js diff --git a/src/components/__tests__/TableBodyTest.js b/src/plugins/core/components/__tests__/TableBodyTest.js similarity index 100% rename from src/components/__tests__/TableBodyTest.js rename to src/plugins/core/components/__tests__/TableBodyTest.js diff --git a/src/components/__tests__/TableHeadingCellTest.js b/src/plugins/core/components/__tests__/TableHeadingCellTest.js similarity index 100% rename from src/components/__tests__/TableHeadingCellTest.js rename to src/plugins/core/components/__tests__/TableHeadingCellTest.js diff --git a/src/components/__tests__/TableHeadingTest.js b/src/plugins/core/components/__tests__/TableHeadingTest.js similarity index 100% rename from src/components/__tests__/TableHeadingTest.js rename to src/plugins/core/components/__tests__/TableHeadingTest.js diff --git a/src/components/__tests__/TableTest.js b/src/plugins/core/components/__tests__/TableTest.js similarity index 100% rename from src/components/__tests__/TableTest.js rename to src/plugins/core/components/__tests__/TableTest.js diff --git a/src/components/index.js b/src/plugins/core/components/index.js similarity index 96% rename from src/components/index.js rename to src/plugins/core/components/index.js index 8f285a48..0f253e78 100644 --- a/src/components/index.js +++ b/src/plugins/core/components/index.js @@ -26,7 +26,7 @@ import SettingsWrapper from './SettingsWrapper'; import SettingsWrapperContainer from './SettingsWrapperContainer'; import Settings from './Settings'; import SettingsContainer from './SettingsContainer'; -import { components as SettingsComponents } from '../settingsComponentObjects'; +import { components as SettingsComponents } from '../../../settingsComponentObjects'; import NextButton from './NextButton'; import NextButtonEnhancer from './NextButtonEnhancer'; import NextButtonContainer from './NextButtonContainer'; diff --git a/src/constants/index.js b/src/plugins/core/constants/index.js similarity index 100% rename from src/constants/index.js rename to src/plugins/core/constants/index.js diff --git a/src/plugins/core/index.js b/src/plugins/core/index.js new file mode 100644 index 00000000..fc831987 --- /dev/null +++ b/src/plugins/core/index.js @@ -0,0 +1,15 @@ +import components from './components'; +import * as reducer from './reducers/dataReducer'; +import * as selectors from './selectors/dataSelectors'; +import * as actions from './actions'; +import initialState from './initialState'; + +const CorePlugin = (config) => ({ + components, + reducer, + selectors, + actions, + ...initialState(config) +}); + +export default CorePlugin; diff --git a/src/plugins/core/initialState.js b/src/plugins/core/initialState.js new file mode 100644 index 00000000..5f6d0371 --- /dev/null +++ b/src/plugins/core/initialState.js @@ -0,0 +1,72 @@ +import { getColumnProperties } from '../../utils/columnUtils'; +import { getRowProperties } from '../../utils/rowUtils'; + + +const styleConfig = { + icons: { + TableHeadingCell: { + sortDescendingIcon: '▼', + sortAscendingIcon: '▲' + }, + }, + classNames: { + Cell: 'griddle-cell', + Filter: 'griddle-filter', + Loading: 'griddle-loadingResults', + NextButton: 'griddle-next-button', + NoResults: 'griddle-noResults', + PageDropdown: 'griddle-page-select', + Pagination: 'griddle-pagination', + PreviousButton: 'griddle-previous-button', + Row: 'griddle-row', + RowDefinition: 'griddle-row-definition', + Settings: 'griddle-settings', + SettingsToggle: 'griddle-settings-toggle', + Table: 'griddle-table', + TableBody: 'griddle-table-body', + TableHeading: 'griddle-table-heading', + TableHeadingCell: 'griddle-table-heading-cell', + TableHeadingCellAscending: 'griddle-heading-ascending', + TableHeadingCellDescending: 'griddle-heading-descending', + }, + styles: { + } +}; + +const initialState = (config) => { + + const { + children:rowPropertiesComponent, + } = config; + + const rowProperties = getRowProperties(rowPropertiesComponent); + const columnProperties = getColumnProperties(rowPropertiesComponent); + + const renderProperties = { + rowProperties, + columnProperties + } + + //const pageProperties = { + // currentPage: 1, + // pageSize: 10, + // ...externalPageProperties, + //} + + const localInitialState = { + enableSettings: true, + textProperties: { + next: 'Next', + previous: 'Previous', + settingsToggle: 'Settings' + } + } + + return { + styleConfig, + renderProperties, + initialState: localInitialState + }; +} + +export default initialState; diff --git a/src/reducers/__tests__/dataReducerTest.js b/src/plugins/core/reducers/__tests__/dataReducerTest.js similarity index 100% rename from src/reducers/__tests__/dataReducerTest.js rename to src/plugins/core/reducers/__tests__/dataReducerTest.js diff --git a/src/reducers/dataReducer.js b/src/plugins/core/reducers/dataReducer.js similarity index 93% rename from src/reducers/dataReducer.js rename to src/plugins/core/reducers/dataReducer.js index 26d7c44f..cf17554e 100644 --- a/src/reducers/dataReducer.js +++ b/src/plugins/core/reducers/dataReducer.js @@ -16,7 +16,7 @@ import Immutable from 'immutable'; import { addColumnPropertiesWhenNoneExist, transformData, -} from '../utils/dataUtils'; +} from '../../../utils/dataUtils'; function isColumnVisible(state, columnId) { const hasRenderProperty = state.getIn(['renderProperties', 'columnProperties', columnId]); @@ -134,9 +134,13 @@ export function GRIDDLE_TOGGLE_COLUMN(state, action) { export function GRIDDLE_UPDATE_STATE(state, action) { const { data, ...newState } = action.newState; - const transformedData = transformData(data, state.get('renderProperties').toJSON()); - - return state.mergeDeep(Immutable.fromJS(newState)) - .set('data', transformedData.data) - .set('lookup', transformedData.lookup); + if (data !== undefined) { + const transformedData = transformData(data, state.get('renderProperties').toJSON()); + + return state.mergeDeep(Immutable.fromJS(newState)) + .set('data', transformedData.data) + .set('lookup', transformedData.lookup); + } else { + return state.mergeDeep(Immutable.fromJS(newState)); + } } diff --git a/src/selectors/__tests__/dataSelectorsTest.js b/src/plugins/core/selectors/__tests__/dataSelectorsTest.js similarity index 70% rename from src/selectors/__tests__/dataSelectorsTest.js rename to src/plugins/core/selectors/__tests__/dataSelectorsTest.js index 82e07e9e..ec1f3d4c 100644 --- a/src/selectors/__tests__/dataSelectorsTest.js +++ b/src/plugins/core/selectors/__tests__/dataSelectorsTest.js @@ -2,27 +2,32 @@ import test from 'ava'; import Immutable from 'immutable'; import * as selectors from '../dataSelectors'; +import { composeSelectors } from '../../../../utils/selectorUtils'; + +test.beforeEach((t) => { + t.context.selectors = composeSelectors([{selectors}]); +}); test('gets data', test => { const state = new Immutable.Map().set('data', 'hi'); - test.is(selectors.dataSelector(state), 'hi'); + test.is(test.context.selectors.dataSelector(state), 'hi'); }); test('gets pageSize', test => { const state = new Immutable.Map().setIn(['pageProperties', 'pageSize'], 7); - test.is(selectors.pageSizeSelector(state), 7); + test.is(test.context.selectors.pageSizeSelector(state), 7); }); /* currentPageSelector */ test('gets current page', test => { const state = new Immutable.Map().setIn(['pageProperties', 'currentPage'], 3); - test.is(selectors.currentPageSelector(state), 3); + test.is(test.context.selectors.currentPageSelector(state), 3); }); /* recordCountSelector */ test('gets record count', test => { const state = new Immutable.Map().setIn(['pageProperties', 'recordCount'], 10); - test.is(selectors.recordCountSelector(state), 10); + test.is(test.context.selectors.recordCountSelector(state), 10); }); /* hasNextSelector */ @@ -35,7 +40,7 @@ test('hasNext gets true when there are more pages', test => { } }); - test.true(selectors.hasNextSelector(state)); + test.true(test.context.selectors.hasNextSelector(state)); }); test('hasNext gets false when there are not more pages', test => { @@ -47,7 +52,7 @@ test('hasNext gets false when there are not more pages', test => { } }); - test.false(selectors.hasNextSelector(state)); + test.false(test.context.selectors.hasNextSelector(state)); }); /* this is just double checking that we're not showing next when on record 11-20 of 20 */ @@ -60,24 +65,24 @@ test('hasNext gets false when on the last page', test => { } }); - test.false(selectors.hasNextSelector(state)); + test.false(test.context.selectors.hasNextSelector(state)); }); /* hasPreviousSelector */ test('has previous gets true when there are prior pages', test => { const state = new Immutable.Map().setIn(['pageProperties', 'currentPage'], 2); - test.true(selectors.hasPreviousSelector(state)); + test.true(test.context.selectors.hasPreviousSelector(state)); }); test.skip('has previous gets false when there are not prior pages', test => { const state = new Immutable.Map().setIn(['pageProperties', 'currentPage'], 2); - test.true(selectors.hasPreviousSelector(state)); + test.true(test.context.selectors.hasPreviousSelector(state)); }) /* currentPageSelector */ test('gets current page', test => { const state = new Immutable.Map().setIn(['pageProperties', 'currentPage'], 1); - test.false(selectors.hasPreviousSelector(state)); + test.false(test.context.selectors.hasPreviousSelector(state)); }) /* maxPageSelector */ @@ -90,36 +95,36 @@ test('gets max page', test => { } }); - test.is(selectors.maxPageSelector(state), 2); + test.is(test.context.selectors.maxPageSelector(state), 2); //ensure that we get 2 pages when full pageSize would not be displayed on next page const otherState = state.setIn(['pageProperties', 'pageSize'], 11); - test.is(selectors.maxPageSelector(otherState), 2); + test.is(test.context.selectors.maxPageSelector(otherState), 2); //when pageSize === recordCount should have 1 page const onePageState = state.setIn(['pageProperties', 'pageSize'], 20); - test.is(selectors.maxPageSelector(onePageState), 1); + test.is(test.context.selectors.maxPageSelector(onePageState), 1); //when there are no records, there should be 0 pages const noDataState = state.setIn(['pageProperties', 'recordCount'], 0); - test.is(selectors.maxPageSelector(noDataState), 0); + test.is(test.context.selectors.maxPageSelector(noDataState), 0); }); /* filterSelector */ test('gets filter when present', test => { const state = new Immutable.Map().set('filter', 'some awesome filter'); - test.is(selectors.filterSelector(state), 'some awesome filter'); + test.is(test.context.selectors.filterSelector(state), 'some awesome filter'); }) test('gets empty string when no filter present', test => { const state = new Immutable.Map(); - test.is(selectors.filterSelector(state), ''); + test.is(test.context.selectors.filterSelector(state), ''); }); /* sortColumnsSelector */ test('gets empty array for sortColumns when none specified', test => { const state = new Immutable.Map(); - test.deepEqual(selectors.sortColumnsSelector(state), []); + test.deepEqual(test.context.selectors.sortColumnsSelector(state), []); }); test('gets sort column array when specified', test => { @@ -130,7 +135,7 @@ test('gets sort column array when specified', test => { { column: 'three', sortAscending: true} ]); - test.deepEqual(selectors.sortColumnsSelector(state), [ + test.deepEqual(test.context.selectors.sortColumnsSelector(state), [ { column: 'one', sortAscending: true}, { column: 'two', sortAscending: true}, { column: 'three', sortAscending: true} @@ -145,18 +150,18 @@ test('allColumnsSelector: gets all columns', test => { const state = new Immutable.Map().set('data', data); - test.deepEqual(selectors.allColumnsSelector(state), ['one', 'two', 'three', 'four']); + test.deepEqual(test.context.selectors.allColumnsSelector(state), ['one', 'two', 'three', 'four']); }); test('allColumnsSelector: gets empty array when no data present', test => { const state = new Immutable.Map(); - test.deepEqual(selectors.allColumnsSelector(state), []); + test.deepEqual(test.context.selectors.allColumnsSelector(state), []); }); test('allColumnsSelector: gets empty array when data is empty', test => { const state = new Immutable.Map().set('data', new Immutable.List()); - test.deepEqual(selectors.allColumnsSelector(state), []); + test.deepEqual(test.context.selectors.allColumnsSelector(state), []); }); test('allColumnsSelector accounts for made up columns', test => { @@ -173,7 +178,7 @@ test('allColumnsSelector accounts for made up columns', test => { } }); - test.deepEqual(selectors.allColumnsSelector(state), ['one', 'two', 'three', 'something']); + test.deepEqual(test.context.selectors.allColumnsSelector(state), ['one', 'two', 'three', 'something']); }); test('iconByNameSelector gets given icon', test => { @@ -185,7 +190,7 @@ test('iconByNameSelector gets given icon', test => { } }); - test.is(selectors.iconByNameSelector(state, {name: 'one'}), 'yo'); + test.is(test.context.selectors.iconByNameSelector(state, {name: 'one'}), 'yo'); }); test('iconByNameSelector gets undefined when icon not present in collection', test => { @@ -197,7 +202,7 @@ test('iconByNameSelector gets undefined when icon not present in collection', te } }); - test.is(selectors.iconByNameSelector(state, { name: 'two'}), undefined) + test.is(test.context.selectors.iconByNameSelector(state, { name: 'two'}), undefined) }); test('classNamesForComponentSelector gets given class', test => { @@ -209,7 +214,7 @@ test('classNamesForComponentSelector gets given class', test => { } }); - test.is(selectors.classNamesForComponentSelector(state, 'one'), 'yo'); + test.is(test.context.selectors.classNamesForComponentSelector(state, 'one'), 'yo'); }); test('classNameForComponentSelector gets undefined when icon not present in collection', test => { @@ -221,21 +226,21 @@ test('classNameForComponentSelector gets undefined when icon not present in coll } }); - test.is(selectors.classNamesForComponentSelector(state, 'two'), undefined); + test.is(test.context.selectors.classNamesForComponentSelector(state, 'two'), undefined); }); test('isSettingsEnabled returns true when not set', test => { const state = new Immutable.fromJS({}); - test.is(selectors.isSettingsEnabledSelector(state), true); + test.is(test.context.selectors.isSettingsEnabledSelector(state), true); }); test('isSettingsEnabled returns the value that was set', test => { const enabledState = new Immutable.fromJS({ enableSettings: true }); const disabledState = new Immutable.fromJS({ enableSettings: false }); - test.is(selectors.isSettingsEnabledSelector(enabledState), true); - test.is(selectors.isSettingsEnabledSelector(disabledState), false); + test.is(test.context.selectors.isSettingsEnabledSelector(enabledState), true); + test.is(test.context.selectors.isSettingsEnabledSelector(disabledState), false); }); test('gets text from state', test => { @@ -245,7 +250,7 @@ test('gets text from state', test => { } }); - test.is(selectors.textSelector(state, { key: 'one'}), 'one two three'); + test.is(test.context.selectors.textSelector(state, { key: 'one'}), 'one two three'); }); test('gets metadata columns', test => { @@ -261,7 +266,7 @@ test('gets metadata columns', test => { } }); - test.deepEqual(selectors.metaDataColumnsSelector(state), ['two']); + test.deepEqual(test.context.selectors.metaDataColumnsSelector(state), ['two']); }); test('it gets columnTitles in the correct order', test => { @@ -277,7 +282,7 @@ test('it gets columnTitles in the correct order', test => { } }); - test.deepEqual(selectors.columnTitlesSelector(state), ['Two', 'One']); + test.deepEqual(test.context.selectors.columnTitlesSelector(state), ['Two', 'One']); }); [undefined, null].map(data => @@ -286,7 +291,7 @@ test('it gets columnTitles in the correct order', test => { data }); - assert.deepEqual(selectors.visibleRowIdsSelector(state), new Immutable.List()); + assert.deepEqual(assert.context.selectors.visibleRowIdsSelector(state), new Immutable.List()); }) ); @@ -299,7 +304,7 @@ test('visibleRowIds gets griddleKey from data', (assert) => { ], }); - assert.deepEqual(selectors.visibleRowIdsSelector(state), new Immutable.List([2, 4, 6])); + assert.deepEqual(assert.context.selectors.visibleRowIdsSelector(state), new Immutable.List([2, 4, 6])); }); test('rowDataSelector gets row data', (assert) => { @@ -314,5 +319,5 @@ test('rowDataSelector gets row data', (assert) => { }, }); - assert.deepEqual(selectors.rowDataSelector(state, { griddleKey: 6 }), { griddleKey: 6, id: 1 }); + assert.deepEqual(assert.context.selectors.rowDataSelector(state, { griddleKey: 6 }), { griddleKey: 6, id: 1 }); }); diff --git a/src/selectors/dataSelectors.js b/src/plugins/core/selectors/dataSelectors.js similarity index 92% rename from src/selectors/dataSelectors.js rename to src/plugins/core/selectors/dataSelectors.js index 4f560342..97b1ba4e 100644 --- a/src/selectors/dataSelectors.js +++ b/src/plugins/core/selectors/dataSelectors.js @@ -1,6 +1,7 @@ import Immutable from 'immutable'; -import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect'; import _ from 'lodash'; +import { createSelector } from '../../../utils/selectorUtils'; +import { createSelectorCreator, defaultMemoize } from 'reselect'; const createDeepEqualSelector = createSelectorCreator( defaultMemoize, @@ -29,15 +30,15 @@ export const renderPropertiesSelector = state => (state.get('renderProperties')) /** Determines if there are previous pages */ export const hasPreviousSelector = createSelector( - currentPageSelector, + 'currentPageSelector', (currentPage) => (currentPage > 1) ); /** Gets the max page size */ export const maxPageSelector = createSelector( - pageSizeSelector, - recordCountSelector, + 'pageSizeSelector', + 'recordCountSelector', (pageSize, recordCount) => { const calc = recordCount / pageSize; @@ -49,8 +50,8 @@ export const maxPageSelector = createSelector( /** Determines if there are more pages available. Assumes pageProperties.maxPage is set by the container */ export const hasNextSelector = createSelector( - currentPageSelector, - maxPageSelector, + 'currentPageSelector', + 'maxPageSelector', (currentPage, maxPage) => { return currentPage < maxPage; } @@ -64,8 +65,8 @@ export const sortColumnsSelector = state => state.get('sortColumns') || []; /** Gets all the columns */ export const allColumnsSelector = createSelector( - dataSelector, - renderPropertiesSelector, + 'dataSelector', + 'renderPropertiesSelector', (data, renderProperties) => { const dataColumns = !data || data.size === 0 ? [] : @@ -83,7 +84,7 @@ export const allColumnsSelector = createSelector( /** Gets the column properties objects sorted by order */ export const sortedColumnPropertiesSelector = createSelector( - renderPropertiesSelector, + 'renderPropertiesSelector', (renderProperties) => ( renderProperties && renderProperties.get('columnProperties') && renderProperties.get('columnProperties').size !== 0 ? renderProperties.get('columnProperties') @@ -95,7 +96,7 @@ export const sortedColumnPropertiesSelector = createSelector( /** Gets metadata column ids */ export const metaDataColumnsSelector = createSelector( - sortedColumnPropertiesSelector, + 'sortedColumnPropertiesSelector', (sortedColumnProperties) => ( sortedColumnProperties ? sortedColumnProperties .filter(c => c.get('isMetadata')) @@ -108,8 +109,8 @@ export const metaDataColumnsSelector = createSelector( /** Gets the visible columns either obtaining the sorted column properties or all columns */ export const visibleColumnsSelector = createSelector( - sortedColumnPropertiesSelector, - allColumnsSelector, + 'sortedColumnPropertiesSelector', + 'allColumnsSelector', (sortedColumnProperties, allColumns) => ( sortedColumnProperties ? sortedColumnProperties .filter(c => { @@ -126,8 +127,8 @@ export const visibleColumnsSelector = createSelector( /** TODO: add tests and docs */ export const visibleColumnPropertiesSelector = createSelector( - visibleColumnsSelector, - renderPropertiesSelector, + 'visibleColumnsSelector', + 'renderPropertiesSelector', (visibleColumns=[], renderProperties) => ( visibleColumns.map(c => { const columnProperty = renderProperties.getIn(['columnProperties', c]); @@ -138,9 +139,9 @@ export const visibleColumnPropertiesSelector = createSelector( /** Gets the possible columns that are currently hidden */ export const hiddenColumnsSelector = createSelector( - visibleColumnsSelector, - allColumnsSelector, - metaDataColumnsSelector, + 'visibleColumnsSelector', + 'allColumnsSelector', + 'metaDataColumnsSelector', (visibleColumns, allColumns, metaDataColumns) => { const removeColumns = [...visibleColumns, ...metaDataColumns]; @@ -151,8 +152,8 @@ export const hiddenColumnsSelector = createSelector( /** TODO: add tests and docs */ export const hiddenColumnPropertiesSelector = createSelector( - hiddenColumnsSelector, - renderPropertiesSelector, + 'hiddenColumnsSelector', + 'renderPropertiesSelector', (hiddenColumns=[], renderProperties) => ( hiddenColumns.map(c => { const columnProperty = renderProperties.getIn(['columnProperties', c]); @@ -222,8 +223,8 @@ export const textSelector = (state, { key}) => { /** Gets the column ids for the visible columns */ export const columnIdsSelector = createSelector( - renderPropertiesSelector, - visibleColumnsSelector, + 'renderPropertiesSelector', + 'visibleColumnsSelector', (renderProperties, visibleColumns) => { const offset = 1000; // TODO: Make this better -- This is pretty inefficient @@ -240,20 +241,20 @@ export const columnIdsSelector = createSelector( /** Gets the column titles for the visible columns */ export const columnTitlesSelector = createSelector( - columnIdsSelector, - renderPropertiesSelector, + 'columnIdsSelector', + 'renderPropertiesSelector', (columnIds, renderProperties) => columnIds.map(k => renderProperties.getIn(['columnProperties', k, 'title']) || k) ); /** Gets the griddleIds for the visible rows */ export const visibleRowIdsSelector = createSelector( - dataSelector, + 'dataSelector', currentPageData => currentPageData ? currentPageData.map(c => c.get('griddleKey')) : new Immutable.List() ); /** Gets the count of visible rows */ export const visibleRowCountSelector = createSelector( - visibleRowIdsSelector, + 'visibleRowIdsSelector', (visibleRowIds) => visibleRowIds.size ); diff --git a/src/plugins/local/components/NextButtonContainer.js b/src/plugins/local/components/NextButtonContainer.js index 2f872dc0..d3e6c5c7 100644 --- a/src/plugins/local/components/NextButtonContainer.js +++ b/src/plugins/local/components/NextButtonContainer.js @@ -2,7 +2,7 @@ import React from 'react'; import { connect } from '../../../utils/griddleConnect'; import { textSelector, hasNextSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/localSelectors'; -import { getNext } from '../../../actions'; +import { getNext } from '../../core/actions'; const enhance = OriginalComponent => connect(state => ({ text: textSelector(state, { key: 'next' }), diff --git a/src/plugins/local/components/PageDropdownContainer.js b/src/plugins/local/components/PageDropdownContainer.js index b62242a4..fda89910 100644 --- a/src/plugins/local/components/PageDropdownContainer.js +++ b/src/plugins/local/components/PageDropdownContainer.js @@ -3,7 +3,7 @@ import { connect } from '../../../utils/griddleConnect'; import { createStructuredSelector } from 'reselect'; import { currentPageSelector, maxPageSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/localSelectors'; -import { setPage } from '../../../actions'; +import { setPage } from '../../core/actions'; const enhance = OriginalComponent => connect(state => ({ maxPages: maxPageSelector(state), diff --git a/src/plugins/local/components/PreviousButtonContainer.js b/src/plugins/local/components/PreviousButtonContainer.js index 54af2b14..6fdb224a 100644 --- a/src/plugins/local/components/PreviousButtonContainer.js +++ b/src/plugins/local/components/PreviousButtonContainer.js @@ -2,7 +2,7 @@ import React from 'react'; import { connect } from '../../../utils/griddleConnect'; import { textSelector, hasPreviousSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/localSelectors'; -import { getPrevious } from '../../../actions'; +import { getPrevious } from '../../core/actions'; const enhance = OriginalComponent => connect(state => ({ text: textSelector(state, { key: 'previous' }), diff --git a/src/plugins/local/components/TableHeadingCellContainer.js b/src/plugins/local/components/TableHeadingCellContainer.js index 6937acf6..2e1cb4ad 100644 --- a/src/plugins/local/components/TableHeadingCellContainer.js +++ b/src/plugins/local/components/TableHeadingCellContainer.js @@ -5,8 +5,8 @@ import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; import withHandlers from 'recompose/withHandlers'; -import { sortPropertyByIdSelector, iconsForComponentSelector, customHeadingComponentSelector, stylesForComponentSelector, classNamesForComponentSelector, cellPropertiesSelector } from '../../../selectors/dataSelectors'; -import { setSortColumn } from '../../../actions'; +import { sortPropertyByIdSelector, iconsForComponentSelector, customHeadingComponentSelector, stylesForComponentSelector, classNamesForComponentSelector, cellPropertiesSelector } from '../../core/selectors/dataSelectors'; +import { setSortColumn } from '../../core/actions'; import { getSortIconProps, setSortProperties } from '../../../utils/sortUtils'; import { valueOrResult } from '../../../utils/valueUtils'; diff --git a/src/plugins/local/index.js b/src/plugins/local/index.js index 0c32aac1..02c55a82 100644 --- a/src/plugins/local/index.js +++ b/src/plugins/local/index.js @@ -6,4 +6,4 @@ export default { components, reducer, selectors -}; \ No newline at end of file +}; diff --git a/src/plugins/local/reducers/__tests__/localReducerTests.js b/src/plugins/local/reducers/__tests__/localReducerTests.js index 42e2080d..f370f0f3 100644 --- a/src/plugins/local/reducers/__tests__/localReducerTests.js +++ b/src/plugins/local/reducers/__tests__/localReducerTests.js @@ -2,7 +2,7 @@ import test from 'ava'; import Immutable from 'immutable'; import * as reducers from '../index'; -import constants from '../../../../constants'; +import constants from '../../../core/constants'; test('it loads data', test => { const state = reducers.GRIDDLE_LOADED_DATA(Immutable.fromJS({ renderProperties: { } }), { diff --git a/src/plugins/local/reducers/index.js b/src/plugins/local/reducers/index.js index 7b8dec64..1d77c5e0 100644 --- a/src/plugins/local/reducers/index.js +++ b/src/plugins/local/reducers/index.js @@ -1,6 +1,6 @@ import { maxPageSelector, currentPageSelector } from '../selectors/localSelectors'; -import * as dataReducers from '../../../reducers//dataReducer'; +import * as dataReducers from '../../core/reducers/dataReducer'; export function GRIDDLE_INITIALIZED(state) { return dataReducers.GRIDDLE_INITIALIZED(state); diff --git a/src/plugins/local/selectors/__tests__/localSelectorsTest.js b/src/plugins/local/selectors/__tests__/localSelectorsTest.js index 28b84b12..a1dea52f 100644 --- a/src/plugins/local/selectors/__tests__/localSelectorsTest.js +++ b/src/plugins/local/selectors/__tests__/localSelectorsTest.js @@ -1,12 +1,18 @@ import test from 'ava'; import Immutable from 'immutable'; +//import * as selectors from '../dataSelectors'; import * as selectors from '../localSelectors'; +import { composeSelectors } from '../../../../utils/selectorUtils'; + +test.beforeEach((t) => { + t.context.selectors = composeSelectors([{selectors}]); +}); test('gets data', test => { const state = new Immutable.Map({ data: 'hi' }); - test.deepEqual(selectors.dataSelector(state), 'hi'); + test.deepEqual(test.context.selectors.dataSelector(state), 'hi'); }); test('gets current page', test => { @@ -16,7 +22,7 @@ test('gets current page', test => { } }); - test.is(selectors.currentPageSelector(state), 4); + test.is(test.context.selectors.currentPageSelector(state), 4); }); test('gets current page size', test => { @@ -26,7 +32,7 @@ test('gets current page size', test => { } }); - test.is(selectors.pageSizeSelector(state), 20); + test.is(test.context.selectors.pageSizeSelector(state), 20); }); test('gets the correct max page', test => { @@ -47,19 +53,19 @@ test('gets the correct max page', test => { }); // 8/3 = 2.6... so the number of pages should be 3 - test.is(selectors.maxPageSelector(state), 3); + test.is(test.context.selectors.maxPageSelector(state), 3); }); test('gets the correct filter when filter present', test => { const state = new Immutable.Map({ filter: 'hi' }); - test.is(selectors.filterSelector(state), 'hi'); + test.is(test.context.selectors.filterSelector(state), 'hi'); }); test('gets empty string when filter not present', test => { const state = new Immutable.Map(); - test.is(selectors.filterSelector(state), ''); + test.is(test.context.selectors.filterSelector(state), ''); }); test('gets sort properties', test => { @@ -70,7 +76,7 @@ test('gets sort properties', test => { ] }); - test.deepEqual(selectors.sortPropertiesSelector(state).toJSON(), [ + test.deepEqual(test.context.selectors.sortPropertiesSelector(state).toJSON(), [ { id: 'one', sortAscending: true }, { id: 'two', sortAscending: false } ]); @@ -81,7 +87,7 @@ test('gets render properties', test => { renderProperties: 'hello' }); - test.is(selectors.renderPropertiesSelector(state), 'hello'); + test.is(test.context.selectors.renderPropertiesSelector(state), 'hello'); }); test('gets all columns', test => { @@ -91,7 +97,7 @@ test('gets all columns', test => { ] }); - test.deepEqual(selectors.allColumnsSelector(state), ['one', 'two', 'three']); + test.deepEqual(test.context.selectors.allColumnsSelector(state), ['one', 'two', 'three']); }); test('gets column orders', test => { @@ -104,7 +110,7 @@ test('gets column orders', test => { } }); - test.deepEqual(selectors.sortedColumnPropertiesSelector(state).toJSON(), { + test.deepEqual(test.context.selectors.sortedColumnPropertiesSelector(state).toJSON(), { two: { id: 'two', title: 'Two', order: 1 }, one: { id: 'one', title: 'One', order: 2 } }); @@ -123,7 +129,7 @@ test('gets visible columns when columns specified without order', test => { } }); - test.deepEqual(selectors.visibleColumnsSelector(state), ['one', 'two']); + test.deepEqual(test.context.selectors.visibleColumnsSelector(state), ['one', 'two']); }); test('gets visible columns in order when columns specified', test => { @@ -139,7 +145,7 @@ test('gets visible columns in order when columns specified', test => { } }); - test.deepEqual(selectors.visibleColumnsSelector(state), ['two', 'one']); + test.deepEqual(test.context.selectors.visibleColumnsSelector(state), ['two', 'one']); }); test('gets all columns as visible columns when no columns specified', test => { @@ -149,7 +155,7 @@ test('gets all columns as visible columns when no columns specified', test => { ] }); - test.deepEqual(selectors.visibleColumnsSelector(state), ['one', 'two', 'three']); + test.deepEqual(test.context.selectors.visibleColumnsSelector(state), ['one', 'two', 'three']); }); test('hasNextSelector returns true when more pages', test => { @@ -170,7 +176,7 @@ test('hasNextSelector returns true when more pages', test => { } }); - test.is(selectors.hasNextSelector(state), true); + test.is(test.context.selectors.hasNextSelector(state), true); }); test('hasNextSelector returns false when no more pages', test => { @@ -191,7 +197,7 @@ test('hasNextSelector returns false when no more pages', test => { } }); - test.is(selectors.hasNextSelector(state), false); + test.is(test.context.selectors.hasNextSelector(state), false); }); test('hasPreviousSelector returns true when there is a previous page', test => { @@ -201,7 +207,7 @@ test('hasPreviousSelector returns true when there is a previous page', test => { } }); - test.is(selectors.hasPreviousSelector(state), true); + test.is(test.context.selectors.hasPreviousSelector(state), true); }); test('hasPreviousSelector returns false when there are no previous pages', test => { @@ -211,7 +217,7 @@ test('hasPreviousSelector returns false when there are no previous pages', test } }); - test.is(selectors.hasPreviousSelector(state), false); + test.is(test.context.selectors.hasPreviousSelector(state), false); }); test('filteredDataSelector returns all data when no filter present', test => { @@ -222,7 +228,7 @@ test('filteredDataSelector returns all data when no filter present', test => { ] }); - test.deepEqual(selectors.filteredDataSelector(state).toJSON(), [ + test.deepEqual(test.context.selectors.filteredDataSelector(state).toJSON(), [ { id: '1', name: 'luke skywalker' }, { id: '2', name: 'han solo' } ]); @@ -237,7 +243,7 @@ test('filteredDataSelector filters data when filter string present', test => { ] }); - test.deepEqual(selectors.filteredDataSelector(state).toJSON(), [ + test.deepEqual(test.context.selectors.filteredDataSelector(state).toJSON(), [ { id: '1', name: 'luke skywalker' } ]); }); @@ -258,7 +264,7 @@ test('filteredDataSelector filters data respecting filterable', test => { ] }); - test.deepEqual(selectors.filteredDataSelector(state).toJSON(), [ + test.deepEqual(test.context.selectors.filteredDataSelector(state).toJSON(), [ { id: '1', name: 'luke skywalker', weapon: 'light saber' } ]); }); @@ -274,7 +280,7 @@ test('sortedDataSelector uses default sort if no sort method specifed for column ] }); - test.deepEqual(selectors.sortedDataSelector(state).toJSON(), [ + test.deepEqual(test.context.selectors.sortedDataSelector(state).toJSON(), [ { id: '2', name: 'han solo' }, { id: '1', name: 'luke skywalker' } ]); @@ -300,7 +306,7 @@ test('sortedDataSelector uses specified sort', test => { } }); - test.deepEqual(selectors.sortedDataSelector(state).toJSON(), [ + test.deepEqual(test.context.selectors.sortedDataSelector(state).toJSON(), [ { id: '1', name: 'luke skywalker' }, { id: '2', name: 'han solo' } ]); @@ -322,7 +328,7 @@ test('sortedDataSelector works with multiple sortOptions', test => { ] }); - test.deepEqual(selectors.sortedDataSelector(state).toJSON(), [ + test.deepEqual(test.context.selectors.sortedDataSelector(state).toJSON(), [ { id: '3', name: 'han solo', food: 'apple' }, { id: '2', name: 'han solo', food: 'banana' }, { id: '4', name: 'luke skywalker', food: 'apple' }, @@ -344,7 +350,7 @@ test('current page data selector gets correct page', test => { } }); - test.deepEqual(selectors.currentPageDataSelector(state).toJSON(), [{ id: '3', name: 'han solo', food: 'apple' }]); + test.deepEqual(test.context.selectors.currentPageDataSelector(state).toJSON(), [{ id: '3', name: 'han solo', food: 'apple' }]); }) test('visible data selector gets only visible columns', test => { @@ -371,7 +377,7 @@ test('visible data selector gets only visible columns', test => { } }); - test.deepEqual(selectors.visibleDataSelector(state).toJSON(), [{ name: 'han solo', food: 'apple' }]); + test.deepEqual(test.context.selectors.visibleDataSelector(state).toJSON(), [{ name: 'han solo', food: 'apple' }]); }); test('visibleRowIdsSelector gets row ids', test => { @@ -395,7 +401,7 @@ test('visibleRowIdsSelector gets row ids', test => { } }); - test.deepEqual(selectors.visibleRowIdsSelector(state).toJSON(), [3, 4]); + test.deepEqual(test.context.selectors.visibleRowIdsSelector(state).toJSON(), [3, 4]); }); test('hidden columns selector shows all columns that are not visible', test => { @@ -419,7 +425,7 @@ test('hidden columns selector shows all columns that are not visible', test => { } }); - test.deepEqual(selectors.hiddenColumnsSelector(state), ['id', 'food']); + test.deepEqual(test.context.selectors.hiddenColumnsSelector(state), ['id', 'food']); }); test('columnIdsSelector gets all column ids', test => { @@ -452,7 +458,7 @@ test('columnIdsSelector gets all column ids', test => { } }); - test.deepEqual(selectors.columnIdsSelector(state), ['first', 'second', 'third']); + test.deepEqual(test.context.selectors.columnIdsSelector(state), ['first', 'second', 'third']); }); test('columnTitlesSelector gets all column titles', test => { @@ -485,5 +491,5 @@ test('columnTitlesSelector gets all column titles', test => { } }); - test.deepEqual(selectors.columnTitlesSelector(state), ['Name', 'ID', 'Food Order']); + test.deepEqual(test.context.selectors.columnTitlesSelector(state), ['Name', 'ID', 'Food Order']); }); diff --git a/src/plugins/local/selectors/localSelectors.js b/src/plugins/local/selectors/localSelectors.js index be77f0d5..e2b34ce0 100644 --- a/src/plugins/local/selectors/localSelectors.js +++ b/src/plugins/local/selectors/localSelectors.js @@ -1,10 +1,10 @@ import Immutable from 'immutable'; -import { createSelector } from 'reselect'; +import { createSelector } from '../../../utils/selectorUtils'; import _ from 'lodash'; import { defaultSort } from '../../../utils/sortUtils'; import { getVisibleDataForColumns } from '../../../utils/dataUtils'; -import * as dataSelectors from '../../../selectors/dataSelectors'; +import * as dataSelectors from '../../core/selectors/dataSelectors'; /** Gets the entire data set * @param {Immutable} state - state object @@ -35,14 +35,14 @@ export const renderPropertiesSelector = state => (state.get('renderProperties')) export const metaDataColumnsSelector = dataSelectors.metaDataColumnsSelector; -const columnPropertiesSelector = state => state.getIn(['renderProperties', 'columnProperties']); +export const columnPropertiesSelector = state => state.getIn(['renderProperties', 'columnProperties']); /** Gets the data filtered by the current filter */ export const filteredDataSelector = createSelector( - dataSelector, - filterSelector, - columnPropertiesSelector, + 'dataSelector', + 'filterSelector', + 'columnPropertiesSelector', (data, filter, columnProperties) => { if (!filter || !data) { return data; @@ -67,8 +67,8 @@ export const filteredDataSelector = createSelector( /** Gets the max page size */ export const maxPageSelector = createSelector( - pageSizeSelector, - filteredDataSelector, + 'pageSizeSelector', + 'filteredDataSelector', (pageSize, data) => { const total = data ? data.size : 0; const calc = total / pageSize; @@ -80,7 +80,7 @@ export const maxPageSelector = createSelector( ) export const allColumnsSelector = createSelector( - dataSelector, + 'dataSelector', data => (!data || data.size === 0 ? [] : data.get(0).keySeq().toJSON()) ); @@ -95,8 +95,8 @@ export const visibleColumnsSelector = dataSelectors.visibleColumnsSelector; /** Returns whether or not this result set has more pages */ export const hasNextSelector = createSelector( - currentPageSelector, - maxPageSelector, + 'currentPageSelector', + 'maxPageSelector', (currentPage, maxPage) => (currentPage < maxPage) ); @@ -108,10 +108,10 @@ export const hasPreviousSelector = state => (state.getIn(['pageProperties', 'cur * if no sort method is supplied, it will use the default sort defined in griddle */ export const sortedDataSelector = createSelector( - filteredDataSelector, - sortPropertiesSelector, - renderPropertiesSelector, - sortMethodSelector, + 'filteredDataSelector', + 'sortPropertiesSelector', + 'renderPropertiesSelector', + 'sortMethodSelector', (filteredData, sortProperties, renderProperties, sortMethod = defaultSort) => { if (!sortProperties) { return filteredData; } @@ -128,9 +128,9 @@ export const sortedDataSelector = createSelector( /** Gets the current page of data */ export const currentPageDataSelector = createSelector( - sortedDataSelector, - pageSizeSelector, - currentPageSelector, + 'sortedDataSelector', + 'pageSizeSelector', + 'currentPageSelector', (sortedData, pageSize, currentPage) => { if (!sortedData) { return []; @@ -145,29 +145,29 @@ export const currentPageDataSelector = createSelector( /** Get the visible data (and only the columns that are visible) */ export const visibleDataSelector = createSelector( - currentPageDataSelector, - visibleColumnsSelector, + 'currentPageDataSelector', + 'visibleColumnsSelector', (currentPageData, visibleColumns) => getVisibleDataForColumns(currentPageData, visibleColumns) ); /** Gets the griddleIds for the visible rows */ export const visibleRowIdsSelector = createSelector( - currentPageDataSelector, + 'currentPageDataSelector', currentPageData => (currentPageData ? currentPageData.map(c => c.get('griddleKey')) : new Immutable.List()) ); /** Gets the count of visible rows */ export const visibleRowCountSelector = createSelector( - visibleRowIdsSelector, + 'visibleRowIdsSelector', (visibleRowIds) => visibleRowIds.size ); /** Gets the columns that are not currently visible */ export const hiddenColumnsSelector = createSelector( - visibleColumnsSelector, - allColumnsSelector, - metaDataColumnsSelector, + 'visibleColumnsSelector', + 'allColumnsSelector', + 'metaDataColumnsSelector', (visibleColumns, allColumns, metaDataColumns) => { const removeColumns = [...visibleColumns, ...metaDataColumns]; @@ -178,8 +178,8 @@ export const hiddenColumnsSelector = createSelector( /** Gets the column ids for the visible columns */ export const columnIdsSelector = createSelector( - visibleDataSelector, - renderPropertiesSelector, + 'visibleDataSelector', + 'renderPropertiesSelector', (visibleData, renderProperties) => { if(visibleData.size > 0) { return Object.keys(visibleData.get(0).toJSON()).map(k => diff --git a/src/plugins/position/selectors/__tests__/indexTest.js b/src/plugins/position/selectors/__tests__/indexTest.js index e8503a3c..203ac645 100644 --- a/src/plugins/position/selectors/__tests__/indexTest.js +++ b/src/plugins/position/selectors/__tests__/indexTest.js @@ -1,10 +1,20 @@ import test from 'ava'; import Immutable from 'immutable'; +import { composeSelectors } from '../../../../utils/selectorUtils'; + +import * as coreSelectors from '../../../core/selectors/dataSelectors'; +import * as localSelectors from '../../../local/selectors/localSelectors'; import { visibleRecordCountSelector } from '../index'; +import * as selectors from '../index'; + +test.beforeEach((test) => { + test.context.selectors = composeSelectors([{selectors: {...coreSelectors}}, {selectors: {...localSelectors}}, {selectors}]); +}); + test('visible record count selector', test => { const state = new Immutable.fromJS({ positionSettings: { @@ -16,6 +26,6 @@ test('visible record count selector', test => { }, }); - test.is(visibleRecordCountSelector(state), 12); + test.is(test.context.selectors.visibleRecordCountSelector(state), 12); }); diff --git a/src/plugins/position/selectors/index.js b/src/plugins/position/selectors/index.js index fda22695..17d98d35 100644 --- a/src/plugins/position/selectors/index.js +++ b/src/plugins/position/selectors/index.js @@ -1,4 +1,6 @@ -import { createSelector } from 'reselect'; +import { createSelector } from '../../../utils/selectorUtils'; + +import { getVisibleDataForColumns } from '../../../utils/dataUtils'; import { sortedDataSelector, visibleColumnsSelector } from '../../local/selectors/localSelectors'; @@ -18,15 +20,15 @@ export const tableWidthSelector = state => state.getIn(['positionSettings', 'tab /** Gets the number of viisble rows based on the height of the container and the rowHeight */ export const visibleRecordCountSelector = createSelector( - rowHeightSelector, - currentHeightSelector, + 'rowHeightSelector', + 'currentHeightSelector', (rowHeight, currentHeight) => { return Math.ceil(currentHeight / rowHeight); } ); export const visibleDataLengthSelector = createSelector( - sortedDataSelector, + 'sortedDataSelector', (sortedData) => { return sortedData.size; } @@ -36,9 +38,9 @@ export const hoizontalScrollChangeSelector = state => state.getIn(['currentPosit export const verticalScrollChangeSelector = state => state.getIn(['currentPosition', 'yScrollChangePosition']) || 0; export const startIndexSelector = createSelector( - verticalScrollChangeSelector, - rowHeightSelector, - visibleRecordCountSelector, + 'verticalScrollChangeSelector', + 'rowHeightSelector', + 'visibleRecordCountSelector', (verticalScrollPosition, rowHeight, visibleRecordCount) => { // Inspired by : http://jsfiddle.net/vjeux/KbWJ2/9/ return Math.max(0, Math.floor(Math.floor(verticalScrollPosition / rowHeight) - visibleRecordCount * 0.25)); @@ -46,9 +48,9 @@ export const startIndexSelector = createSelector( ); export const endIndexSelector = createSelector( - startIndexSelector, - visibleRecordCountSelector, - visibleDataLengthSelector, + 'startIndexSelector', + 'visibleRecordCountSelector', + 'visibleDataLengthSelector', (startDisplayIndex, visibleRecordCount, visibleDataLength) => { // Inspired by : http://jsfiddle.net/vjeux/KbWJ2/9/ return Math.min(Math.floor(startDisplayIndex + visibleRecordCount * 2), visibleDataLength - 1) + 1; @@ -56,17 +58,17 @@ export const endIndexSelector = createSelector( ); export const topSpacerSelector = createSelector( - rowHeightSelector, - startIndexSelector, + 'rowHeightSelector', + 'startIndexSelector', (rowHeight, startIndex) => { return rowHeight * startIndex; } ); export const bottomSpacerSelector = createSelector( - rowHeightSelector, - visibleDataLengthSelector, - endIndexSelector, + 'rowHeightSelector', + 'visibleDataLengthSelector', + 'endIndexSelector', (rowHeight, visibleDataLength, endIndex) => { return rowHeight * (visibleDataLength - endIndex); } @@ -77,9 +79,9 @@ export const bottomSpacerSelector = createSelector( */ export const currentPageDataSelector = (...args) => { return createSelector( - sortedDataSelector, - startIndexSelector, - endIndexSelector, + 'sortedDataSelector', + 'startIndexSelector', + 'endIndexSelector', (sortedData, startDisplayIndex, endDisplayIndex) => { return sortedData .skip(startDisplayIndex) @@ -91,13 +93,13 @@ export const currentPageDataSelector = (...args) => { /** Get the visible data (and only the columns that are visible) */ export const visibleDataSelector = createSelector( - currentPageDataSelector, - visibleColumnsSelector, + 'currentPageDataSelector', + 'visibleColumnsSelector', (currentPageData, visibleColumns) => getVisibleDataForColumns(currentPageData, visibleColumns) ); /** Gets the griddleIds for the visible rows */ export const visibleRowIdsSelector = createSelector( - currentPageDataSelector, + 'currentPageDataSelector', (currentPageData) => currentPageData.map(c => c.get('griddleKey')) ); diff --git a/src/settingsComponentObjects/ColumnChooser.js b/src/settingsComponentObjects/ColumnChooser.js index 19fbf93c..a3916ea1 100644 --- a/src/settingsComponentObjects/ColumnChooser.js +++ b/src/settingsComponentObjects/ColumnChooser.js @@ -3,8 +3,8 @@ import { connect } from '../utils/griddleConnect'; import compose from 'recompose/compose'; import withHandlers from 'recompose/withHandlers'; -import { visibleColumnPropertiesSelector, hiddenColumnPropertiesSelector } from '../selectors/dataSelectors'; -import { toggleColumn as toggleColumnAction } from '../actions'; +import { visibleColumnPropertiesSelector, hiddenColumnPropertiesSelector } from '../plugins/core/selectors/dataSelectors'; +import { toggleColumn as toggleColumnAction } from '../plugins/core/actions'; const style = { label: { clear: 'both' } diff --git a/src/settingsComponentObjects/PageSizeSettings.js b/src/settingsComponentObjects/PageSizeSettings.js index 08c8579b..3cd0a73e 100644 --- a/src/settingsComponentObjects/PageSizeSettings.js +++ b/src/settingsComponentObjects/PageSizeSettings.js @@ -4,9 +4,9 @@ import compose from 'recompose/compose'; import withState from 'recompose/withState'; import withHandlers from 'recompose/withHandlers'; -import { pageSizeSelector } from '../selectors/dataSelectors'; +import { pageSizeSelector } from '../plugins/core/selectors/dataSelectors'; -import { setPageSize as setPageSizeAction } from '../actions'; +import { setPageSize as setPageSizeAction } from '../plugins/core/actions'; const ComposedPageSizeSettings = compose( connect( diff --git a/src/utils/__tests__/selectorUtilsTest.js b/src/utils/__tests__/selectorUtilsTest.js new file mode 100644 index 00000000..f26c81d6 --- /dev/null +++ b/src/utils/__tests__/selectorUtilsTest.js @@ -0,0 +1,377 @@ +import test from 'ava'; + +import { + createSelector, + composeSelectors +} from '../selectorUtils'; + +test('createSelector with only 1 argument should throw an Error', (t) => { + const error = t.throws(() => { + createSelector( + (state) => state + ); + }, Error); +}); + +test('createSelector with final results function arg not of function type', (assert) => { + const error = assert.throws(() => { + createSelector( + "someDependency", + "badResultFunctionStringArg" + ); + }, Error); +}); + +test('createSelector with 1 selector function arg and 1 results function arg', (assert) => { + const selector = createSelector( + (state) => state, + (state) => state + ); + + assert.is(typeof selector, "function"); + assert.is(selector.dependencies, undefined); +}); + +test('createSelector with 1 selector dependency and 1 results function', (assert) => { + const selector = createSelector( + "someDependency", + (a) => null + ); + + assert.is(typeof selector, "function"); + assert.is(typeof selector.dependencies, "object"); + assert.is(selector.dependencies.length, 1); + assert.is(selector.dependencies[0], "someDependency"); +}); + +test('createSelector with 1 function dependency, 1 selector dependency, and 1 results function', (assert) => { + const selector = createSelector( + (state) => state, + "someDependency", + (state, x) => null + ); + + assert.is(typeof selector, "function"); + assert.is(typeof selector.dependencies, "object"); + assert.is(selector.dependencies.length, 1); + assert.is(selector.dependencies[0], "someDependency"); +}); + +test('createSelector with a non string or function argument for one of the first n - 1 args', (assert) => { + const error = assert.throws(() => { + createSelector( + 42, + (x) => null + ); + }, Error); +}); + +test('createSelector with 1 selector dependency and 1 results function, ' + + 'then call the returned generator function with valid resolved dependencies', (assert) => { + const resolvedDependencies = { + someDependency: () => 42 + }; + + const selector = createSelector( + "someDependency", + (x) => x + )(resolvedDependencies); + + assert.is(selector(), 42); + }); + +test('createSelector with 1 selector function, 1 selector dependency, and 1 results function ' + + 'then call the returned generator function with valid resolved dependencies', (assert) => { + const someFunction = () => 10; + + const resolvedDependencies = { + someDependency: () => 42 + }; + + const selector = createSelector( + someFunction, + "someDependency", + (x, y) => x * y + )(resolvedDependencies); + + assert.is(selector(), 420); + }); + +test('createSelector with 1 selector dependency, and 1 results function' + + 'then call the returned generator function WITHOUT valid resolved dependencies', (assert) => { + const error = assert.throws(() => { + createSelector( + "someDependency", + (x) => x + )({}); + }, Error); + }); + +test('composeSelectors with 1 simple selector', (assert) => { + const plugin0 = { + selectors: { + simpleSelectorA: (state) => state, + } + }; + + const flattenedSelectors = composeSelectors([plugin0]); + + assert.is(typeof flattenedSelectors, "object"); + assert.is(Object.keys(flattenedSelectors).length, 1); + assert.true(flattenedSelectors.hasOwnProperty("simpleSelectorA")); +}); + +test('composeSelectors with 2 simple selectors and 1 dependency selector dependent on the 2 simple selectors', (assert) => { + const plugin0 = { + selectors: { + simpleSelectorA: () => 10, + simpleSelectorB: () => 2, + dependencySelector1: createSelector( + 'simpleSelectorA', + 'simpleSelectorB', + (x, y) => x * y + ) + } + } + + const flattenedSelectors = composeSelectors([plugin0]); + + assert.is(typeof flattenedSelectors, "object"); + assert.is(Object.keys(flattenedSelectors).length, 3); + assert.true(flattenedSelectors.hasOwnProperty("simpleSelectorA")); + assert.true(flattenedSelectors.hasOwnProperty("simpleSelectorB")); + assert.true(flattenedSelectors.hasOwnProperty("dependencySelector1")); + assert.is(flattenedSelectors.dependencySelector1(), 20); + // this is a crucial test, the composeSelectors function must also trigger + // the first run of createSelector's returned selector generator which + // changes its behaviour. From now on this function will act like + // a selector created by reselect's createSelector + assert.is(plugin0.selectors.simpleSelectorA(), 10); + assert.is(plugin0.selectors.simpleSelectorB(), 2); + assert.is(plugin0.selectors.dependencySelector1(), 20); +}); + +test('name me', (assert) => { + const plugin0 = { + selectors: { + simpleSelectorA: () => 10, + simpleSelectorB: () => 2, + dependencySelector1: createSelector( + 'simpleSelectorA', + 'simpleSelectorB', + (x, y) => x * y + ) + } + } + + const plugin1 = { + selectors: { + dependencySelector1: createSelector( + 'simpleSelectorA', + 'simpleSelectorB', + (x, y) => x + y + ) + } + } + + const flattenedSelectors = composeSelectors([plugin0, plugin1]); + + assert.is(typeof flattenedSelectors, "object"); + assert.is(Object.keys(flattenedSelectors).length, 3); + assert.true(flattenedSelectors.hasOwnProperty("simpleSelectorA")); + assert.true(flattenedSelectors.hasOwnProperty("simpleSelectorB")); + assert.true(flattenedSelectors.hasOwnProperty("dependencySelector1")); + + + assert.is(flattenedSelectors.simpleSelectorA(), 10); + assert.is(flattenedSelectors.simpleSelectorB(), 2); + assert.is(flattenedSelectors.dependencySelector1(), 12); + + // this is a crucial test, the composeSelectors function must also trigger + // the first run of createSelector's returned selector generator which + // changes its behaviour. From now on this function will act like + // a selector created by reselect's createSelector + assert.is(plugin0.selectors.simpleSelectorA(), 10); + assert.is(plugin0.selectors.simpleSelectorB(), 2); + assert.is(plugin0.selectors.dependencySelector1(), 12); + + assert.is(plugin1.selectors.dependencySelector1(), 12); +}); + +test('name me', (assert) => { + const plugin0 = (() => { + const simpleSelectorA = () => 10; + const simpleSelectorB = () => 2; + return { + selectors: { + simpleSelectorA, + simpleSelectorB, + dependencySelector1: createSelector( + simpleSelectorA, + simpleSelectorB, + (x, y) => x * y + ) + } + } + } + )(); + + const plugin1 = { + selectors: { + dependencySelector1: createSelector( + 'simpleSelectorA', + 'simpleSelectorB', + (x, y) => x + y + ) + } + } + + const flattenedSelectors = composeSelectors([plugin0, plugin1]); + + assert.is(typeof flattenedSelectors, "object"); + assert.is(Object.keys(flattenedSelectors).length, 3); + assert.true(flattenedSelectors.hasOwnProperty("simpleSelectorA")); + assert.true(flattenedSelectors.hasOwnProperty("simpleSelectorB")); + assert.true(flattenedSelectors.hasOwnProperty("dependencySelector1")); + + + assert.is(flattenedSelectors.simpleSelectorA(), 10); + assert.is(flattenedSelectors.simpleSelectorB(), 2); + assert.is(flattenedSelectors.dependencySelector1(), 12); + + // this is a crucial test, the composeSelectors function must also trigger + // the first run of createSelector's returned selector generator which + // changes its behaviour. From now on this function will act like + // a selector created by reselect's createSelector + assert.is(plugin0.selectors.simpleSelectorA(), 10); + assert.is(plugin0.selectors.simpleSelectorB(), 2); + // this selector was declared using selector function arguments + // instead of selector dependency arguments, this means it + // will NOT be overridden and it should keep its original + // behaviour. Note that only the function in the PLUGIN + // maintains this behaviour, the function returned in + // flattenedSelectors uses the overridden dependencySelector1 + // from plugin1 as it was the most recently declared selector + assert.is(plugin0.selectors.dependencySelector1(), 20); + + assert.is(plugin1.selectors.dependencySelector1(), 12); +}); + +test('composeSelectors called with mixed selector function and selector dependency createSelector selectors', (assert) => { + const plugin0 = (() => { + const simpleSelectorA = () => 10; + const simpleSelectorB = () => 2; + return { + selectors: { + simpleSelectorA, + simpleSelectorB, + dependencySelector1: createSelector( + 'simpleSelectorA', + 'simpleSelectorB', + (x, y) => x * y + ), + dependencySelector2: createSelector( + simpleSelectorA, + 'simpleSelectorB', + (x, y) => x * y + ) + } + } + } + )(); + + const plugin1 = { + selectors: { + simpleSelectorA: () => 40, + simpleSelectorB: () => 1 + } + } + + const flattenedSelectors = composeSelectors([plugin0, plugin1]); + + assert.is(typeof flattenedSelectors, "object"); + assert.is(Object.keys(flattenedSelectors).length, 4); + assert.true(flattenedSelectors.hasOwnProperty("simpleSelectorA")); + assert.true(flattenedSelectors.hasOwnProperty("simpleSelectorB")); + assert.true(flattenedSelectors.hasOwnProperty("dependencySelector1")); + assert.true(flattenedSelectors.hasOwnProperty("dependencySelector2")); + + /* In the scenario, dependencySelector2 is using hybrid selection function + * and selector dependency arguments. This means that the selector dependency + * argument 'simpleSelectorB' is open to be overridden, it will use whatever + * is the latest version of simpleSelectorB, in this case the one from plugin1. + * However, the selector function argument simpleSelectorA will NEVER be overridden + * for this selector and will thus use the simpleSelectorA function as statically + * defined in plugin0. Selector function arguments will always point to the function + * they were originally referencing. PLEASE NOTE that if the static function + * the argument references is ITSELF LATER OVERRIDDEN it will of course use the new + * overridden version. + */ + + + assert.is(flattenedSelectors.simpleSelectorA(), 40); + assert.is(flattenedSelectors.simpleSelectorB(), 1); + assert.is(flattenedSelectors.dependencySelector1(), 40); + assert.is(flattenedSelectors.dependencySelector2(), 10); + + // this is a crucial test, the composeSelectors function must also trigger + // the first run of createSelector's returned selector generator which + // changes its behaviour. From now on this function will act like + // a selector created by reselect's createSelector + assert.is(plugin0.selectors.simpleSelectorA(), 10); + assert.is(plugin0.selectors.simpleSelectorB(), 2); + // this selector was declared using selector function arguments + // instead of selector dependency arguments, this means it + // will NOT be overridden and it should keep its original + // behaviour. Note that only the function in the PLUGIN + // maintains this behaviour, the function returned in + // flattenedSelectors uses the overridden dependencySelector1 + // from plugin1 as it was the most recently declared selector + assert.is(plugin0.selectors.dependencySelector1(), 40); + assert.is(plugin0.selectors.dependencySelector2(), 10); + + assert.is(plugin1.selectors.simpleSelectorA(), 40); + assert.is(plugin1.selectors.simpleSelectorB(), 1); +}); + +test('name me', (assert) => { + const plugin0 = { + selectors: { + simpleSelectorA: () => 10, + simpleSelectorB: () => 2, + dependencySelector1: createSelector( + 'simpleSelectorA', + 'simpleSelectorB', + (x, y) => x * y + ) + } + } + + const flattenedSelectors = composeSelectors([plugin0]); + + const createdDependencySelector1 = plugin0.selectors.dependencySelector1.factory(); + + assert.is(typeof flattenedSelectors, "object"); + assert.is(Object.keys(flattenedSelectors).length, 3); + assert.true(flattenedSelectors.hasOwnProperty("simpleSelectorA")); + assert.true(flattenedSelectors.hasOwnProperty("simpleSelectorB")); + assert.true(flattenedSelectors.hasOwnProperty("dependencySelector1")); + assert.is(flattenedSelectors.dependencySelector1(), 20); + // this is a crucial test, the composeSelectors function must also trigger + // the first run of createSelector's returned selector generator which + // changes its behaviour. From now on this function will act like + // a selector created by reselect's createSelector + assert.is(plugin0.selectors.simpleSelectorA(), 10); + assert.is(plugin0.selectors.simpleSelectorB(), 2); + assert.is(plugin0.selectors.dependencySelector1(), 20); + + assert.is(plugin0.selectors.dependencySelector1.factory()(), 20); + assert.is(plugin0.selectors.dependencySelector1(), 20); + assert.is(flattenedSelectors.dependencySelector1.factory()(), 20); + assert.is(flattenedSelectors.dependencySelector1(), 20); + + assert.is(flattenedSelectors.dependencySelector1.factory({simpleSelectorA: () => 30})(), 60); + assert.is(flattenedSelectors.dependencySelector1(), 20); + assert.is(plugin0.selectors.dependencySelector1.factory({simpleSelectorA: () => 20})(), 40); + assert.is(plugin0.selectors.dependencySelector1(), 20); +}); diff --git a/src/utils/compositionUtils.js b/src/utils/compositionUtils.js index 78b68177..4e47ebe0 100644 --- a/src/utils/compositionUtils.js +++ b/src/utils/compositionUtils.js @@ -283,22 +283,49 @@ export function getReducersByWordEnding(reducers, ending) { */ export function wrapMethodsByWordEnding(componentArray, wordEnding, keyReplaceString = '') { return componentArray.reduce((previous, current) => { - let newObject = {}; + let newObject = {}, + mergedObject = previous; for(var key in current) { const keyWithoutEnhancer = key.replace(wordEnding, keyReplaceString); if(key.endsWith(wordEnding) && (previous.hasOwnProperty(keyWithoutEnhancer) || current.hasOwnProperty(keyWithoutEnhancer))) { // Determine if we are working with an HoC that wraps another HoC - newObject[keyWithoutEnhancer] = keyWithoutEnhancer.endsWith('Container') || keyWithoutEnhancer.endsWith('Enhancer') ? + if(keyWithoutEnhancer.endsWith('Container') || keyWithoutEnhancer.endsWith('Enhancer')) { // If we are enhancing a container or enhancer flow this stuff since it's likely an HoC - _.flowRight(current[key], (current[keyWithoutEnhancer] || previous[keyWithoutEnhancer])) : + newObject[keyWithoutEnhancer] = _.flowRight(current[key], (current[keyWithoutEnhancer] || previous[keyWithoutEnhancer])); + } else { // Wrap the current component in the Enhancer or container - current[key](current[keyWithoutEnhancer] || previous[keyWithoutEnhancer]) + if(Array.isArray(current[key])) { + newObject[keyWithoutEnhancer] = current[key].reduce((previousComponent, currentComponent) => { + if(previousComponent !== undefined) { + return currentComponent(previousComponent); + } else { + return currentComponent(current[keyWithoutEnhancer]); + } + }, undefined); + } else { + newObject[keyWithoutEnhancer] = current[key](current[keyWithoutEnhancer] || previous[keyWithoutEnhancer]) + } + + } + } + if(mergedObject[key] === undefined) { + mergedObject[key] = current[key]; + } else { + if(key.endsWith('Enhancer')) { + if(Array.isArray(mergedObject[key])) { + mergedObject[key].push(current[key]); + }else { + mergedObject[key] = [mergedObject[key], current[key]]; + } + } else { + mergedObject[key] = current[key]; + } } } - return _.pickBy(Object.assign(previous, current, newObject), (v, k) => (!k.endsWith(wordEnding))) ; + return _.pickBy(Object.assign(mergedObject, newObject), (v, k) => (!k.endsWith(wordEnding))) ; }, {}) } diff --git a/src/utils/index.js b/src/utils/index.js index 94c5d6fa..6e0daef8 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -3,6 +3,7 @@ import * as compositionUtils from './compositionUtils'; import * as dataUtils from './dataUtils'; import * as rowUtils from './rowUtils'; import * as sortUtils from './sortUtils'; +import * as selectorUtils from './selectorUtils'; import { connect } from './griddleConnect'; export default { @@ -12,4 +13,5 @@ export default { rowUtils, sortUtils, connect, + selectorUtils }; diff --git a/src/utils/listenerUtils.js b/src/utils/listenerUtils.js index 086162bd..4868208f 100644 --- a/src/utils/listenerUtils.js +++ b/src/utils/listenerUtils.js @@ -27,7 +27,7 @@ export const StoreListener = class StoreListener { // if no name is provided, do nothing name && this.removeListener(name); const unsubscribe = (() => { - let oldState; + let oldState = this.store.getState(); return this.store.subscribe(() => { const newState = this.store.getState(); listener(oldState, newState, {...otherArgs}); diff --git a/src/utils/selectorUtils.js b/src/utils/selectorUtils.js new file mode 100644 index 00000000..89e42361 --- /dev/null +++ b/src/utils/selectorUtils.js @@ -0,0 +1,397 @@ +import { forOwn, size, values } from 'lodash'; +import { createSelector } from 'reselect' + +const DEP = "D"; +const FUNC = "F"; + +const globalSelectors = {}; + +/* + * Wrapped 'createSelector' that allows for building the selector + * dependency tree. Takes any number of arguments, all arguments but the + * last must be dependencies, which are the string names of selectors + * this selector depends on and the last arg must be the selector function + * itself. This structure mirrors very closely what calling 'createSelector' + * looks like. + * + * const mySelector = createSelector( + * aSelector, + * anotherSelector, + * (a, b) => (someLogic....) + * ); + * + * const mySelector = griddleCreateSelector( + * "aSelector", + * "anotherSelector", + * (a, b) => (someLogic...) + * ); + * + * When the selectors are finally generated, the actual dependency selectors + * are passed to the createSelector function. + */ +const griddleCreateSelector = (...args) => { + + // All selectors that use createSelector must have a minimum of one + // dependency and the selector function itself + if (args.length < 2) { + throw new Error("Cannot create a selector with fewer than 2 arguments, must have at least one dependency and the selector function"); + } + + // The first n - 1 args are the dependencies, they must + // all be strings. + //const dependencies = args.slice(0, args.length - 1); + + + // The first n - 1 args are selector functions AND/OR + // selector dependencies. + // + // If all arguments are selector functions, this would be + // considered a 'legacy' style hard-coded createSelector + // invocation. We can immediately delegate to createSelector + // and return the selector function. + // + // If all of the arguments are selector dependencies -- + // which are strings naming the to-be-resolved selector functions -- + // then we need to return a selector generator function. This will + // later be provided with a list of fully resolved selector + // functions that can then be used to generate this selector + // using createSelector. + // + // If there is a MIXTURE of both selector functions and + // selector dependency strings, then we still need a + // selector generator but only a portion of the functions + // need to be resolved for later. We save the provided selector + // functions and will merge them with the resolved selectors + // when the generator function is called. This could be used + // to force a particular dependency from being overridden. + // This should be considered an advanced feature, you should + // only prevent dependent selector overriding if there is a + // very good reason for it. + // + // As this griddleCreateSelector function can return either the + // output of reselect's 'createSelector', or a selector generator + // function, the distinguishing feature will be that the selector + // generator will have as a prop the .dependencies field which + // is a list of the string names of the dependencies for this + // generator. If this is a mixed type generator, the length of + // the dependencies will be smaller than args.length - 1 based + // on how many selector function args were provided. + const depsOrFuncs = args.slice(0, args.length - 1); + const dofTypeMap = []; + const dependencies = {}; + const functions = {}; + for (const index in depsOrFuncs) { + const depOrFunc = depsOrFuncs[index]; + switch(typeof depOrFunc) { + case "function": + dofTypeMap[index] = FUNC; + functions[index] = depOrFunc; + break; + case "string": + dofTypeMap[index] = DEP; + dependencies[index] = depOrFunc; + break; + default: + throw new Error("The first n - 1 arguments of griddleCreateSelector must be either strings or functions"); + } + } + + // The last of n args is the selector function, + // it must be a function + const selector = args[args.length - 1]; + if (typeof selector !== "function") { + throw new Error("Last argument must be a function"); + } + + // If there are a number of function arguments equal to the + // first n - 1 arguments, this is a fully hard coded selector, + // call createSelector and return. + if (size(functions) === args.length - 1) { + return createSelector(...args); + } + + // Otherwise, this is either a mixed or fully string dependency + // selector. Create a selector generator. + + //const selectorGeneratorWrapper = (() => { + + return (() => { + + const createSelectorFuncs = new Map(); + const ownSelector = selector; + + const factory = (resolvedSelectors = {}) => { + const selectors = []; + if (!createSelectorFuncs.size) { + for (const index in dofTypeMap) { + const dofType = dofTypeMap[index]; + switch(dofType) { + case FUNC: + //createSelectorFuncs.push(functions[index]); + createSelectorFuncs.set(index, functions[index]); + break; + case DEP: + if (resolvedSelectors.hasOwnProperty(dependencies[index])) { + //createSelectorFuncs.push(resolvedSelectors[dependencies[index]]); + createSelectorFuncs.set(dependencies[index], resolvedSelectors[dependencies[index]]); + } else { + throw new Error(`Dependency ${dependencies[index]} not found!`); + } + break; + } + } + createSelectorFuncs.forEach((func) => selectors.push(func)); + selectors.push(ownSelector); + } else { + createSelectorFuncs.forEach((func, key) => { + if (resolvedSelectors.hasOwnProperty(key)) { + selectors.push(resolvedSelectors[key]); + } else { + selectors.push(func); + } + }); + selectors.push(ownSelector); + } + + return createSelector(...selectors); + }; + + const selectorGenerator = (...args) => { + if (!selectorGenerator.generatedSelector) { + const resolvedSelectors = args[0]; + + //const createSelectorFuncs = []; + //for (const index in dofTypeMap) { + // const dofType = dofTypeMap[index]; + // switch(dofType) { + // case FUNC: + // createSelectorFuncs.push(functions[index]); + // break; + // case DEP: + // if (resolvedSelectors.hasOwnProperty(dependencies[index])) { + // createSelectorFuncs.push(resolvedSelectors[dependencies[index]]); + // } else { + // throw new Error(`Dependency ${dependencies[index]} not found!`); + // } + // break; + // } + //} + + //// add this selector + //createSelectorFuncs.push(selector); + + // call createSelector with the final list of args + //selectorGenerator.generatedSelector = createSelector(...createSelectorFuncs); + selectorGenerator.generatedSelector = factory(resolvedSelectors); + //selectorGenerator.generated = true; + + // can probably just return this, as calls to this function + // will now flow into just calling the generated selector function + return selectorGenerator; + + // Selector was called directly in legacy code + //return createSelector(...createSelectorFuncs)(selectors); + } else { + return selectorGenerator.generatedSelector(...args) + } + } + selectorGenerator.createSelectorFuncs = createSelectorFuncs; + selectorGenerator.ownSelector = ownSelector; + selectorGenerator.factory = factory; + selectorGenerator.generatedSelector = undefined; + selectorGenerator.dependencies = values(dependencies); + return selectorGenerator; + })(); + + // attach the list of string dependencies to the + // selector generator + //selectorGeneratorWrapper.dependencies = values(dependencies); + //return selectorGeneratorWrapper; +}; +export { griddleCreateSelector as createSelector }; + + +export const composeSelectors = (plugins) => { + + // STEP 1 + // ========== + // + // Add all selectors to the list of combined selectors. + // + // Each key in combinedSelectors corresponds to + // an array of selectors that were encountered for that given name. + // A newer selector that is encountered for a given name is unshifted + // onto index 0 of the array such at all index 0's of each array + // are the most 'recently' encountered selector for that name. This allows + // use to keep track of all the places these selectors were declared so + // that when finally building the selectors we can go back to these + // references and set them correctly. This specifically allows for the + // overriding functionality to work properly with 'hard' import references + // to selectors. + // + // Each encountered selector function is wrapped in an object which is used + // to keep track of all the data needed to properly build all the + // selector dependency trees + const combinedSelectors = new Map(); + + plugins.forEach((plugin) => { + console.log('Begin parsing selectors for plugin'); + forOwn(plugin.selectors, (selector, name) => { + if (!combinedSelectors.has(name)) { + console.log(` First instance of selector ${name} encountered`); + combinedSelectors.set(name, [{ + name, + selector, + dependencies: selector.dependencies || [], + rank: 0, + traversed: false + }]); + } else { + console.log(` Overriding existing selector named ${name}`); + combinedSelectors.get(name).unshift({ + name, + selector, + dependencies: selector.dependencies || [], + rank: 0, + traversed: false + }); + } + }); + }); + + // RANKS + // ========== + // + // The ranks array is populated when running getDependencies + // It stores the selectors based on their 'rank' + // Rank can be defined recursively as: + // - if a selector has no dependencies, rank is 0 + // - if a selector has 1 or more dependencies, rank is max(all dependency ranks) + 1 + const ranks = []; + + // GET DEPENDENCIES + // ========== + // + // getDependencies recursively descends through the dependencies + // of a given selector doing several things: + // - creates a 'flat' list of dependencies for a given selector, + // which is a list of all of its dependencies + // - calculates the rank of each selector and fills out the above ranks list + // - determines if there are any cycles present in the dependency tree + // + // It also memoizes the results in the combinedSelectors Map by setting the + // 'traversed' flag for a given selector. If a selector has been flagged as + // 'traversed', it simply returns the previously calculated dependencies + const getDependencies = (node, parents) => { + // if this node has already been traversed + // no need to run the get dependencies logic as they + // have already been computed + // simply return its list of flattened dependencies + if (!node.traversed) { + + // if the node has dependencies, add each one to the node's + // list of flattened dependencies and recursively call + // getDependencies on each of them + if (node.dependencies.length > 0) { + + const flattenedDependencies = new Set(); + for (let dependency of node.dependencies) { + //if (typeof dependency === 'function') continue; + if (!combinedSelectors.has(dependency)) { + const err = `Selector ${node.name} has dependency ${dependency} but this is not in the list of dependencies! Did you misspell something?`; + throw new Error(err); + } + + // if any dependency in the recursion chain + // matches one of the parents there is a cycle throw an exception + // this is an unrecoverable runtime error + if (parents.has(dependency)) { + let err = "Dependency cycle detected! "; + for (let e of parents) { + e === dependency ? err += `[[${e}]] -> ` : err += `${e} -> `; + } + err += `[[${dependency}]]`; + console.log(err); + throw new Error(err); + } + flattenedDependencies.add(dependency); + const childParents = new Set(parents); + childParents.add(dependency); + const childsDependencies = getDependencies(combinedSelectors.get(dependency)[0], childParents); + childsDependencies.forEach((key) => flattenedDependencies.add(key)) + const childRank = combinedSelectors.get(dependency)[0].rank; + childRank >= node.rank && (node.rank = childRank + 1); + } + node.flattenedDependencies = flattenedDependencies; + node.traversed = true; + + } else { + + // otherwise, this is a leaf node + // - set the node's rank to 0 + // - set the nodes flattenedDependencies to an empty set + node.flattenedDependencies = new Set(); + node.traversed = true; + } + ranks[node.rank] || (ranks[node.rank] = new Array()); + ranks[node.rank].push(node); + } + return node.flattenedDependencies; + }; + + + // STEP 4 + // ========== + // + // Run getDependencies on each first selector in the 'combinedSelectors' list + // This fills out the 'ranks' list for use in the next step + for (let e of combinedSelectors) { + const [name, selectorChain] = e; + getDependencies(selectorChain[0], new Set([name])); + } + + // STEP 5 + // ========== + // + // Create a flat object of just the actual selector functions + // This will be used as the set of selectors on context + const flattenedSelectors = {}; + //console.log({ allSelectors, combinedSelectors, ranks }); + console.log(ranks); + for (let rank of ranks) { + for (let selector of rank) { + //checking if the selector is generated may not be necessary? + if (selector.dependencies.length && !selector.selector.generatedSelector) { + + const generatedSelector = selector.selector(flattenedSelectors); + + const selectorsOfName = combinedSelectors.get(selector.name); + + selectorsOfName.slice(1, selectorsOfName.length).forEach((selectorOfName) => { + if (selectorOfName.dependencies.length) { + selectorOfName.selector.createSelectorFuncs = generatedSelector.createSelectorFuncs; + selectorOfName.selector.generatedSelector = generatedSelector.generatedSelector; + } + }); + + flattenedSelectors[selector.name] = generatedSelector; + + //const childSelectors = { _dependencies: true }; + //for (let childSelector of selector.dependencies) { + // if (typeof childSelector === 'string') { + // childSelectors[childSelector] = combinedSelectors.get(childSelector).selector; + // } + //} + //flattenedSelectors[selector.name] = selector.selector(childSelectors); + } + else { + flattenedSelectors[selector.name] = selector.selector; + } + } + } + + // Work-around for direct references to composed selectors + //Object.assign(globalSelectors, flattenedSelectors); + + return flattenedSelectors; +} diff --git a/stories/index.tsx b/stories/index.tsx index 58be9ea2..96705708 100644 --- a/stories/index.tsx +++ b/stories/index.tsx @@ -9,13 +9,16 @@ import withHandlers from 'recompose/withHandlers'; import withState from 'recompose/withState'; import { Provider, connect as reduxConnect } from 'react-redux'; import { createStore } from 'redux'; -import { createSelector } from 'reselect'; +//import { createSelector } from 'reselect'; import _ from 'lodash'; +import { createLogger } from 'redux-logger'; import GenericGriddle, { connect, actions, components, selectors, plugins, utils, ColumnDefinition, RowDefinition, GriddleProps } from '../src/module'; const { Cell, Row, Table, TableContainer, TableBody, TableHeading, TableHeadingCell } = components; const { SettingsWrapper, SettingsToggle, Settings } = components; +const { createSelector } = utils.selectorUtils; + const { LegacyStylePlugin, LocalPlugin, PositionPlugin } = plugins; import fakeData, { FakeData } from './fakeData'; @@ -955,6 +958,147 @@ storiesOf('Plugins', module) ); }) + .add('Overridable selectors in plugin', () => { + + const getNext = () => { + return { + type: "GRIDDLE_NEXT_PAGE" + }; + }; + + const getPrevious = () => { + return { + type: "GRIDDLE_PREVIOUS_PAGE" + }; + }; + + const setPage = (pageNumber) => { + return { + type: "GRIDDLE_SET_PAGE", + pageNumber + }; + }; + + const GRIDDLE_NEXT_PAGE = (state, action) => { + const currentPage = state.getIn(["pageProperties", "currentPage"]); + const pageSize = state.getIn(["pageProperties", "pageSize"]); + const recordCount = state.get("data").size; + const maxPage = Math.ceil(recordCount/pageSize); + + if (currentPage + 1 <= maxPage) { + return state.setIn(["pageProperties", "currentPage"], currentPage + 1); + } else { + return state; + } + }; + + const GRIDDLE_PREVIOUS_PAGE = (state, action) => { + const currentPage = state.getIn(["pageProperties", "currentPage"]); + const minPage = 1; + + if (currentPage - 1 >= minPage) { + return state.setIn(["pageProperties", "currentPage"], currentPage - 1); + } else { + return state; + } + }; + + const GRIDDLE_SET_PAGE = (state, action) => { + const pageNumber = action.pageNumber; + const pageSize = state.getIn(["pageProperties", "pageSize"]); + const recordCount = state.get("data").size; + const maxPage = Math.ceil(recordCount/pageSize); + const minPage = 1; + + if (pageNumber >= minPage && pageNumber <= maxPage) { + return state.setIn(["pageProperties", "currentPage"], pageNumber); + } else { + return state; + } + }; + + const allDataSelector = (state) => state.get("data"); + + const recordCountSelector = state => state.get("data").size; + + const dataSelector = createSelector ( + "allDataSelector", + "pageSizeSelector", + "currentPageSelector", + "recordCountSelector", + (data, pageSize, currentPage, recordCount) => { + currentPage = currentPage - 1; + const first = currentPage * pageSize; + const last = Math.min((currentPage + 1) * pageSize, recordCount); + return data.slice(first, last); + } + ); + + const NextButtonEnhancer = OriginalComponent => compose( + connect( + null, + (dispatch, props) => { + return { + getNext: () => dispatch(getNext()) + } + } + ) + )((props) => ); + + const PageDropdownEnhancer = OriginalComponent => compose( + connect( + null, + (dispatch, props) => { + return { + setPage: (page) => dispatch(setPage(page)) + } + } + ) + )((props) => ); + + const PreviousButtonEnhancer = OriginalComponent => compose( + connect( + null, + (dispatch, props) => { + return { + getPrevious: () => dispatch(getPrevious()) + } + } + ) + )((props) => ); + + + const OverridableSelectorsPlugin = { + components: { + NextButtonEnhancer, + PageDropdownEnhancer, + PreviousButtonEnhancer + }, + reducer: { + GRIDDLE_NEXT_PAGE, + GRIDDLE_PREVIOUS_PAGE, + GRIDDLE_SET_PAGE + }, + selectors: { + allDataSelector, + recordCountSelector, + dataSelector + }, + composedSelectors: { + dataSelector + } + }; + + const AnotherPlugin = { + selectors: { + someSelector: (state) => null + } + } + + return ( + + ); + }) storiesOf('Data Missing', module) .add('base (data=undefined)', () => { diff --git a/yarn.lock b/yarn.lock index 4561917b..d859aa1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2316,6 +2316,10 @@ decamelize@^1.0.0, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" +deep-diff@^0.3.5: + version "0.3.8" + resolved "https://registry.npmjs.intuit.net/d/deep-diff/_attachments/deep-diff-0.3.8.tgz#c01de63efb0eec9798801d40c7e0dae25b582c84" + deep-equal@^1.0.0, deep-equal@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" @@ -5354,6 +5358,12 @@ reduce-function-call@^1.0.1: dependencies: balanced-match "^0.4.2" +redux-logger@^3.0.6: + version "3.0.6" + resolved "https://registry.npmjs.intuit.net/r/redux-logger/_attachments/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf" + dependencies: + deep-diff "^0.3.5" + redux@^3.5.2, redux@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/redux/-/redux-3.6.0.tgz#887c2b3d0b9bd86eca2be70571c27654c19e188d"