From 89ecac27c8fdf234ca0d7bac842d17d0dae3e462 Mon Sep 17 00:00:00 2001 From: "Short, James" Date: Tue, 26 Sep 2017 01:16:29 -0600 Subject: [PATCH 1/5] Composable selectors --- src/index.js | 204 ++++++++++++++++++++++++++- src/selectors/composedSelectors.js | 218 +++++++++++++++++++++++++++++ 2 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 src/selectors/composedSelectors.js diff --git a/src/index.js b/src/index.js index cc54f81f..a504bfbc 100644 --- a/src/index.js +++ b/src/index.js @@ -8,7 +8,8 @@ import _ from 'lodash'; import * as dataReducers from './reducers/dataReducer'; import components from './components'; import settingsComponentObjects from './settingsComponentObjects'; -import * as selectors from './selectors/dataSelectors'; +import * as baseSelectors from './selectors/dataSelectors'; +import * as composedSelectors from './selectors/composedSelectors'; import { buildGriddleReducer, buildGriddleComponents } from './utils/compositionUtils'; import { getColumnProperties } from './utils/columnUtils'; @@ -98,7 +99,206 @@ class Griddle extends Component { this.events = Object.assign({}, events, ...plugins.map(p => p.events)); - this.selectors = plugins.reduce((combined, plugin) => ({ ...combined, ...plugin.selectors }), {...selectors}); + // STEP 1 + // ========== + // + // Add all of the 'base' selectors to the list of combined selectors. + // The actuall selector functions are wrapped in an object which is used + // to keep track of all the data needed to properly build all the + // selector dependency trees + console.log("Parsing built-in selectors"); + const combinedSelectors = new Map(); + const _baseSelectors = _.reduce(baseSelectors, (map, baseSelector, name) => { + const selector = { + name, + selector: baseSelector, + dependencies: [], + rank: 0, + traversed: false + }; + combinedSelectors.set(name, selector); + map.set(name, selector); + return map; + }, new Map()); + + // STEP 2 + // ========== + // + // Add all of the 'composed' selectors to the list of combined selectors. + // Composed selectors use the 'createSelector' function provided by reselect + // and depend on other selectors. These new selectors are located in a + // new file named 'composedSelectors' and are now an object that looks like this: + // { + // creator: ({dependency1, dependency2, ...}) => return createSelector(dependency1, dependency2, (...) => (...)), + // dependencies: ["dependency1", "dependency2"] + // } + // 'creator' will return the selector when it is run with the dependency selectors + // 'dependencies' are the string names of the dependency selectors, these will be used to + // build the tree of selectors + const _composedSelectors = _.reduce(composedSelectors, (map, composedSelector, name) => { + const selector = { + name, + ...composedSelector, + rank: 0, + traversed: false + }; + combinedSelectors.has(name) && console.log(` Overriding existing selector named ${name}`); + combinedSelectors.set(name, selector); + map.set(name, selector); + return map; + }, new Map()); + + // STEP 3 + // ========== + // + // Once the built-in 'base' and 'composed' selectors are added to the list, + // repeat the same process for each of the plugins. + // + // Plugins can now redefine a single existing selector without having to + // include the full list of dependency selectors since the dependencies + // are now created dynamically + for (let i in plugins) { + console.log(`Parsing selectors for plugin ${i}`); + const plugin = plugins[i]; + _.forOwn(plugin.selectors, (baseSelector, name) => { + const selector = { + name, + selector: baseSelector, + dependencies: [], + rank: 0, + traversed: false + }; + + // console log for demonstration purposes + combinedSelectors.has(name) && console.log(` Overriding existing selector named ${name} with base selector`); + combinedSelectors.set(name, selector); + }); + + _.forOwn(plugin.composedSelectors, (composedSelector, name) => { + const selector = { + name, + ...composedSelector, + rank: 0, + traversed: false + }; + + // console log for demonstration purposes + combinedSelectors.has(name) && console.log(` Overriding existing selector named ${name} with composed selector`); + combinedSelectors.set(name, selector); + }); + } + + + // 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 (!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), childParents); + childsDependencies.forEach((key) => flattenedDependencies.add(key)) + const childRank = combinedSelectors.get(dependency).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 selector in the 'combinedSelectors' list + // This fills out the 'ranks' list for use in the next step + for (let e of combinedSelectors) { + const [name, selector] = e; + getDependencies(selector, new Set([name])); + } + + // STEP 5 + // ========== + // + // Create a flat object of just the actual selector functions + const flattenedSelectors = {}; + for (let rank of ranks) { + for (let selector of rank) { + if (selector.creator) { + const childSelectors = {}; + for (let childSelector of selector.dependencies) { + childSelectors[childSelector] = combinedSelectors.get(childSelector).selector; + } + selector.selector = selector.creator(childSelectors); + } + flattenedSelectors[selector.name] = selector.selector; + } + } + + //this.selectors = plugins.reduce((combined, plugin) => ({ ...combined, ...plugin.selectors }), {...selectors}); + this.selectors = flattenedSelectors; const mergedStyleConfig = _.merge({}, defaultStyleConfig, ...plugins.map(p => p.styleConfig), styleConfig); diff --git a/src/selectors/composedSelectors.js b/src/selectors/composedSelectors.js new file mode 100644 index 00000000..e657cf32 --- /dev/null +++ b/src/selectors/composedSelectors.js @@ -0,0 +1,218 @@ +import Immutable from 'immutable'; +import { createSelector } from 'reselect'; +import _ from 'lodash'; +import MAX_SAFE_INTEGER from 'max-safe-integer' + +export const hasPreviousSelector = { + creator: ({currentPageSelector}) => { + return createSelector( + currentPageSelector, + (currentPage) => (currentPage > 1) + ); + }, + dependencies: ["currentPageSelector"] +}; + +export const maxPageSelector = { + creator: ({pageSizeSelector, recordCountSelector}) => { + return createSelector( + pageSizeSelector, + recordCountSelector, + (pageSize, recordCount) => { + const calc = recordCount / pageSize; + const result = calc > Math.floor(calc) ? Math.floor(calc) + 1 : Math.floor(calc); + return _.isFinite(result) ? result : 1; + } + ); + }, + dependencies: ["pageSizeSelector", "recordCountSelector"] +}; + +export const hasNextSelector = { + creator: ({currentPageSelector, maxPageSelector}) => { + return createSelector( + currentPageSelector, + maxPageSelector, + (currentPage, maxPage) => { + return currentPage < maxPage; + } + ); + }, + dependencies: ["currentPageSelector", "maxPageSelector"] +}; + +export const allColumnsSelector = { + creator: ({dataSelector, renderPropertiesSelector}) => { + return createSelector( + dataSelector, + renderPropertiesSelector, + (data, renderProperties) => { + const dataColumns = !data || data.size === 0 ? + [] : + data.get(0).keySeq().toJSON(); + + const columnPropertyColumns = (renderProperties && renderProperties.size > 0) ? + // TODO: Make this not so ugly + Object.keys(renderProperties.get('columnProperties').toJSON()) : + []; + + return _.union(dataColumns, columnPropertyColumns); + } + ); + }, + dependencies: ["dataSelector", "renderPropertiesSelector"] +}; + +export const sortedColumnPropertiesSelector = { + creator: ({renderPropertiesSelector}) => { + return createSelector( + renderPropertiesSelector, + (renderProperties) => ( + renderProperties && renderProperties.get('columnProperties') && renderProperties.get('columnProperties').size !== 0 ? + renderProperties.get('columnProperties') + .sortBy(col => (col && col.get('order'))||MAX_SAFE_INTEGER) : + null + ) + ); + }, + dependencies: ["renderPropertiesSelector"] +}; + +export const metaDataColumnsSelector = { + creator: ({sortedColumnPropertiesSelector}) => { + return createSelector( + sortedColumnPropertiesSelector, + (sortedColumnProperties) => ( + sortedColumnProperties ? sortedColumnProperties + .filter(c => c.get('isMetadata')) + .keySeq() + .toJSON() : + [] + ) + ); + }, + dependencies: ["sortedColumnPropertiesSelector"] +}; + + +export const visibleColumnsSelector = { + creator: ({sortedColumnPropertiesSelector, allColumnsSelector}) => { + return createSelector( + sortedColumnPropertiesSelector, + allColumnsSelector, + (sortedColumnProperties, allColumns) => ( + sortedColumnProperties ? sortedColumnProperties + .filter(c => { + const isVisible = c.get('visible') || c.get('visible') === undefined; + const isMetadata = c.get('isMetadata'); + return isVisible && !isMetadata; + }) + .keySeq() + .toJSON() : + allColumns + ) + ); + }, + dependencies: ["sortedColumnPropertiesSelector", "allColumnsSelector"] +}; + +export const visibleColumnPropertiesSelector = { + creator: ({visibleColumnsSelector, renderPropertiesSelector}) => { + return createSelector( + visibleColumnsSelector, + renderPropertiesSelector, + (visibleColumns=[], renderProperties) => ( + visibleColumns.map(c => { + const columnProperty = renderProperties.getIn(['columnProperties', c]); + return (columnProperty && columnProperty.toJSON()) || { id: c } + }) + ) + ); + }, + dependencies: ["visibleColumnsSelector", "renderPropertiesSelector"] +}; + +export const hiddenColumnsSelector = { + creator: ({visibleColumnsSelector, allColumnsSelector, metaDataColumnsSelector}) => { + return createSelector( + visibleColumnsSelector, + allColumnsSelector, + metaDataColumnsSelector, + (visibleColumns, allColumns, metaDataColumns) => { + const removeColumns = [...visibleColumns, ...metaDataColumns]; + + return allColumns.filter(c => removeColumns.indexOf(c) === -1); + } + ); + }, + dependencies: ["visibleColumnsSelector", "allColumnsSelector", "metaDataColumnsSelector"] +}; + +export const hiddenColumnPropertiesSelector = { + creator: ({hiddenColumnsSelector, renderPropertiesSelector}) => { + return createSelector( + hiddenColumnsSelector, + renderPropertiesSelector, + (hiddenColumns=[], renderProperties) => ( + hiddenColumns.map(c => { + const columnProperty = renderProperties.getIn(['columnProperties', c]); + + return (columnProperty && columnProperty.toJSON()) || { id: c } + }) + ) + ); + }, + dependencies: ["hiddenColumnsSelector", "renderPropertiesSelector"] +}; + +export const columnIdsSelector = { + creator: ({renderPropertiesSelector, visibleColumnsSelector}) => { + return createSelector( + renderPropertiesSelector, + visibleColumnsSelector, + (renderProperties, visibleColumns) => { + const offset = 1000; + // TODO: Make this better -- This is pretty inefficient + return visibleColumns + .map((k, index) => ({ + id: renderProperties.getIn(['columnProperties', k, 'id']) || k, + order: renderProperties.getIn(['columnProperties', k, 'order']) || offset + index + })) + .sort((first, second) => first.order - second.order) + .map(item => item.id); + } + ); + }, + dependencies: ["renderPropertiesSelector", "visibleColumnsSelector"] +}; + +export const columnTitlesSelector = { + creator: ({columnIdsSelector, renderPropertiesSelector}) => { + return createSelector( + columnIdsSelector, + renderPropertiesSelector, + (columnIds, renderProperties) => columnIds.map(k => renderProperties.getIn(['columnProperties', k, 'title']) || k) + ); + }, + dependencies: ["columnIdsSelector", "renderPropertiesSelector"] +}; + +export const visibleRowIdsSelector = { + creator: ({dataSelector}) => { + return createSelector( + dataSelector, + currentPageData => currentPageData ? currentPageData.map(c => c.get('griddleKey')) : new Immutable.List() + ); + }, + dependencies: ["dataSelector"] +}; + +export const visibleRowCountSelector = { + creator: ({visibleRowIdsSelector}) => { + return createSelector( + visibleRowIdsSelector, + (visibleRowIds) => visibleRowIds.size + ); + }, + dependencies: ["visibleRowIdsSelector"] +}; From 5fca14d35c04768a0293f32f0e1a4215fc2dd896 Mon Sep 17 00:00:00 2001 From: "Short, James" Date: Thu, 5 Oct 2017 14:26:37 -0600 Subject: [PATCH 2/5] Refactored the selector composing function into a utils class, added a composable selector generator function to be used to create composable selectors. --- src/index.js | 402 +++++++++++----------- src/selectors/composedSelectors.js | 519 ++++++++++++++++++----------- src/utils/selectorUtils.js | 277 +++++++++++++++ 3 files changed, 810 insertions(+), 388 deletions(-) create mode 100644 src/utils/selectorUtils.js diff --git a/src/index.js b/src/index.js index a504bfbc..6ff62bb3 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,7 @@ import { getColumnProperties } from './utils/columnUtils'; import { getRowProperties } from './utils/rowUtils'; import { setSortProperties } from './utils/sortUtils'; import { StoreListener } from './utils/listenerUtils'; +import { composeSelectors } from './utils/selectorUtils'; import * as actions from './actions'; const defaultEvents = { @@ -99,206 +100,207 @@ class Griddle extends Component { this.events = Object.assign({}, events, ...plugins.map(p => p.events)); - // STEP 1 - // ========== - // - // Add all of the 'base' selectors to the list of combined selectors. - // The actuall selector functions are wrapped in an object which is used - // to keep track of all the data needed to properly build all the - // selector dependency trees - console.log("Parsing built-in selectors"); - const combinedSelectors = new Map(); - const _baseSelectors = _.reduce(baseSelectors, (map, baseSelector, name) => { - const selector = { - name, - selector: baseSelector, - dependencies: [], - rank: 0, - traversed: false - }; - combinedSelectors.set(name, selector); - map.set(name, selector); - return map; - }, new Map()); - - // STEP 2 - // ========== - // - // Add all of the 'composed' selectors to the list of combined selectors. - // Composed selectors use the 'createSelector' function provided by reselect - // and depend on other selectors. These new selectors are located in a - // new file named 'composedSelectors' and are now an object that looks like this: - // { - // creator: ({dependency1, dependency2, ...}) => return createSelector(dependency1, dependency2, (...) => (...)), - // dependencies: ["dependency1", "dependency2"] - // } - // 'creator' will return the selector when it is run with the dependency selectors - // 'dependencies' are the string names of the dependency selectors, these will be used to - // build the tree of selectors - const _composedSelectors = _.reduce(composedSelectors, (map, composedSelector, name) => { - const selector = { - name, - ...composedSelector, - rank: 0, - traversed: false - }; - combinedSelectors.has(name) && console.log(` Overriding existing selector named ${name}`); - combinedSelectors.set(name, selector); - map.set(name, selector); - return map; - }, new Map()); - - // STEP 3 - // ========== - // - // Once the built-in 'base' and 'composed' selectors are added to the list, - // repeat the same process for each of the plugins. - // - // Plugins can now redefine a single existing selector without having to - // include the full list of dependency selectors since the dependencies - // are now created dynamically - for (let i in plugins) { - console.log(`Parsing selectors for plugin ${i}`); - const plugin = plugins[i]; - _.forOwn(plugin.selectors, (baseSelector, name) => { - const selector = { - name, - selector: baseSelector, - dependencies: [], - rank: 0, - traversed: false - }; - - // console log for demonstration purposes - combinedSelectors.has(name) && console.log(` Overriding existing selector named ${name} with base selector`); - combinedSelectors.set(name, selector); - }); - - _.forOwn(plugin.composedSelectors, (composedSelector, name) => { - const selector = { - name, - ...composedSelector, - rank: 0, - traversed: false - }; - - // console log for demonstration purposes - combinedSelectors.has(name) && console.log(` Overriding existing selector named ${name} with composed selector`); - combinedSelectors.set(name, selector); - }); - } - - - // 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 (!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), childParents); - childsDependencies.forEach((key) => flattenedDependencies.add(key)) - const childRank = combinedSelectors.get(dependency).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 selector in the 'combinedSelectors' list - // This fills out the 'ranks' list for use in the next step - for (let e of combinedSelectors) { - const [name, selector] = e; - getDependencies(selector, new Set([name])); - } - - // STEP 5 - // ========== - // - // Create a flat object of just the actual selector functions - const flattenedSelectors = {}; - for (let rank of ranks) { - for (let selector of rank) { - if (selector.creator) { - const childSelectors = {}; - for (let childSelector of selector.dependencies) { - childSelectors[childSelector] = combinedSelectors.get(childSelector).selector; - } - selector.selector = selector.creator(childSelectors); - } - flattenedSelectors[selector.name] = selector.selector; - } - } - - //this.selectors = plugins.reduce((combined, plugin) => ({ ...combined, ...plugin.selectors }), {...selectors}); - this.selectors = flattenedSelectors; + //// STEP 1 + //// ========== + //// + //// Add all of the 'base' selectors to the list of combined selectors. + //// The actuall selector functions are wrapped in an object which is used + //// to keep track of all the data needed to properly build all the + //// selector dependency trees + //console.log("Parsing built-in selectors"); + //const combinedSelectors = new Map(); + //const _baseSelectors = _.reduce(baseSelectors, (map, baseSelector, name) => { + // const selector = { + // name, + // selector: baseSelector, + // dependencies: [], + // rank: 0, + // traversed: false + // }; + // combinedSelectors.set(name, selector); + // map.set(name, selector); + // return map; + //}, new Map()); + + //// STEP 2 + //// ========== + //// + //// Add all of the 'composed' selectors to the list of combined selectors. + //// Composed selectors use the 'createSelector' function provided by reselect + //// and depend on other selectors. These new selectors are located in a + //// new file named 'composedSelectors' and are now an object that looks like this: + //// { + //// creator: ({dependency1, dependency2, ...}) => return createSelector(dependency1, dependency2, (...) => (...)), + //// dependencies: ["dependency1", "dependency2"] + //// } + //// 'creator' will return the selector when it is run with the dependency selectors + //// 'dependencies' are the string names of the dependency selectors, these will be used to + //// build the tree of selectors + //const _composedSelectors = _.reduce(composedSelectors, (map, composedSelector, name) => { + // const selector = { + // name, + // ...composedSelector, + // rank: 0, + // traversed: false + // }; + // combinedSelectors.has(name) && console.log(` Overriding existing selector named ${name}`); + // combinedSelectors.set(name, selector); + // map.set(name, selector); + // return map; + //}, new Map()); + + //// STEP 3 + //// ========== + //// + //// Once the built-in 'base' and 'composed' selectors are added to the list, + //// repeat the same process for each of the plugins. + //// + //// Plugins can now redefine a single existing selector without having to + //// include the full list of dependency selectors since the dependencies + //// are now created dynamically + //for (let i in plugins) { + // console.log(`Parsing selectors for plugin ${i}`); + // const plugin = plugins[i]; + // _.forOwn(plugin.selectors, (baseSelector, name) => { + // const selector = { + // name, + // selector: baseSelector, + // dependencies: [], + // rank: 0, + // traversed: false + // }; + + // // console log for demonstration purposes + // combinedSelectors.has(name) && console.log(` Overriding existing selector named ${name} with base selector`); + // combinedSelectors.set(name, selector); + // }); + + // _.forOwn(plugin.composedSelectors, (composedSelector, name) => { + // const selector = { + // name, + // ...composedSelector, + // rank: 0, + // traversed: false + // }; + + // // console log for demonstration purposes + // combinedSelectors.has(name) && console.log(` Overriding existing selector named ${name} with composed selector`); + // combinedSelectors.set(name, selector); + // }); + //} + + + //// 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 (!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), childParents); + // childsDependencies.forEach((key) => flattenedDependencies.add(key)) + // const childRank = combinedSelectors.get(dependency).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 selector in the 'combinedSelectors' list + //// This fills out the 'ranks' list for use in the next step + //for (let e of combinedSelectors) { + // const [name, selector] = e; + // getDependencies(selector, new Set([name])); + //} + + //// STEP 5 + //// ========== + //// + //// Create a flat object of just the actual selector functions + //const flattenedSelectors = {}; + //for (let rank of ranks) { + // for (let selector of rank) { + // if (selector.creator) { + // const childSelectors = {}; + // for (let childSelector of selector.dependencies) { + // childSelectors[childSelector] = combinedSelectors.get(childSelector).selector; + // } + // selector.selector = selector.creator(childSelectors); + // } + // flattenedSelectors[selector.name] = selector.selector; + // } + //} + + ////this.selectors = plugins.reduce((combined, plugin) => ({ ...combined, ...plugin.selectors }), {...selectors}); + //this.selectors = flattenedSelectors; + this.selectors = composeSelectors(baseSelectors, composedSelectors, plugins); const mergedStyleConfig = _.merge({}, defaultStyleConfig, ...plugins.map(p => p.styleConfig), styleConfig); diff --git a/src/selectors/composedSelectors.js b/src/selectors/composedSelectors.js index e657cf32..c51b0248 100644 --- a/src/selectors/composedSelectors.js +++ b/src/selectors/composedSelectors.js @@ -2,175 +2,284 @@ import Immutable from 'immutable'; import { createSelector } from 'reselect'; import _ from 'lodash'; import MAX_SAFE_INTEGER from 'max-safe-integer' +import { griddleCreateSelector } from '../utils/selectorUtils'; -export const hasPreviousSelector = { - creator: ({currentPageSelector}) => { - return createSelector( - currentPageSelector, - (currentPage) => (currentPage > 1) - ); - }, - dependencies: ["currentPageSelector"] -}; - -export const maxPageSelector = { - creator: ({pageSizeSelector, recordCountSelector}) => { - return createSelector( - pageSizeSelector, - recordCountSelector, - (pageSize, recordCount) => { - const calc = recordCount / pageSize; - const result = calc > Math.floor(calc) ? Math.floor(calc) + 1 : Math.floor(calc); - return _.isFinite(result) ? result : 1; - } - ); - }, - dependencies: ["pageSizeSelector", "recordCountSelector"] -}; - -export const hasNextSelector = { - creator: ({currentPageSelector, maxPageSelector}) => { - return createSelector( - currentPageSelector, - maxPageSelector, - (currentPage, maxPage) => { - return currentPage < maxPage; - } - ); - }, - dependencies: ["currentPageSelector", "maxPageSelector"] -}; - -export const allColumnsSelector = { - creator: ({dataSelector, renderPropertiesSelector}) => { - return createSelector( - dataSelector, - renderPropertiesSelector, - (data, renderProperties) => { - const dataColumns = !data || data.size === 0 ? - [] : - data.get(0).keySeq().toJSON(); - - const columnPropertyColumns = (renderProperties && renderProperties.size > 0) ? - // TODO: Make this not so ugly - Object.keys(renderProperties.get('columnProperties').toJSON()) : - []; - - return _.union(dataColumns, columnPropertyColumns); - } - ); - }, - dependencies: ["dataSelector", "renderPropertiesSelector"] -}; - -export const sortedColumnPropertiesSelector = { - creator: ({renderPropertiesSelector}) => { - return createSelector( - renderPropertiesSelector, - (renderProperties) => ( - renderProperties && renderProperties.get('columnProperties') && renderProperties.get('columnProperties').size !== 0 ? - renderProperties.get('columnProperties') - .sortBy(col => (col && col.get('order'))||MAX_SAFE_INTEGER) : - null - ) - ); - }, - dependencies: ["renderPropertiesSelector"] -}; - -export const metaDataColumnsSelector = { - creator: ({sortedColumnPropertiesSelector}) => { - return createSelector( - sortedColumnPropertiesSelector, - (sortedColumnProperties) => ( - sortedColumnProperties ? sortedColumnProperties - .filter(c => c.get('isMetadata')) - .keySeq() - .toJSON() : - [] - ) - ); - }, - dependencies: ["sortedColumnPropertiesSelector"] -}; - - -export const visibleColumnsSelector = { - creator: ({sortedColumnPropertiesSelector, allColumnsSelector}) => { - return createSelector( - sortedColumnPropertiesSelector, - allColumnsSelector, - (sortedColumnProperties, allColumns) => ( - sortedColumnProperties ? sortedColumnProperties - .filter(c => { - const isVisible = c.get('visible') || c.get('visible') === undefined; - const isMetadata = c.get('isMetadata'); - return isVisible && !isMetadata; - }) - .keySeq() - .toJSON() : - allColumns - ) - ); - }, - dependencies: ["sortedColumnPropertiesSelector", "allColumnsSelector"] -}; - -export const visibleColumnPropertiesSelector = { - creator: ({visibleColumnsSelector, renderPropertiesSelector}) => { - return createSelector( - visibleColumnsSelector, - renderPropertiesSelector, - (visibleColumns=[], renderProperties) => ( +export const hasPreviousSelector = griddleCreateSelector( + "currentPageSelector", + (currentPage) => (currentPage > 1) +); + +//export const hasPreviousSelector = { +// creator: ({currentPageSelector}) => { +// return createSelector( +// currentPageSelector, +// (currentPage) => (currentPage > 1) +// ); +// }, +// dependencies: ["currentPageSelector"] +//}; + +export const maxPageSelector = griddleCreateSelector( + "pageSizeSelector", + "recordCountSelector", + (pageSize, recordCount) => { + const calc = recordCount / pageSize; + const result = calc > Math.floor(calc) ? Math.floor(calc) + 1 : Math.floor(calc); + return _.isFinite(result) ? result : 1; + } +); + +//export const maxPageSelector = { +// creator: ({pageSizeSelector, recordCountSelector}) => { +// return createSelector( +// pageSizeSelector, +// recordCountSelector, +// (pageSize, recordCount) => { +// const calc = recordCount / pageSize; +// const result = calc > Math.floor(calc) ? Math.floor(calc) + 1 : Math.floor(calc); +// return _.isFinite(result) ? result : 1; +// } +// ); +// }, +// dependencies: ["pageSizeSelector", "recordCountSelector"] +//}; + +export const hasNextSelector = griddleCreateSelector( + "currentPageSelector", + "maxPageSelector", + (currentPage, maxPage) => { + return currentPage < maxPage; + } +); + +//export const hasNextSelector = { +// creator: ({currentPageSelector, maxPageSelector}) => { +// return createSelector( +// currentPageSelector, +// maxPageSelector, +// (currentPage, maxPage) => { +// return currentPage < maxPage; +// } +// ); +// }, +// dependencies: ["currentPageSelector", "maxPageSelector"] +//}; + +export const allColumnsSelector = griddleCreateSelector( + "dataSelector", + "renderPropertiesSelector", + (data, renderProperties) => { + const dataColumns = !data || data.size === 0 ? + [] : + data.get(0).keySeq().toJSON(); + + const columnPropertyColumns = (renderProperties && renderProperties.size > 0) ? + // TODO: Make this not so ugly + Object.keys(renderProperties.get('columnProperties').toJSON()) : + []; + + return _.union(dataColumns, columnPropertyColumns); + } +); + +//export const allColumnsSelector = { +// creator: ({dataSelector, renderPropertiesSelector}) => { +// return createSelector( +// dataSelector, +// renderPropertiesSelector, +// (data, renderProperties) => { +// const dataColumns = !data || data.size === 0 ? +// [] : +// data.get(0).keySeq().toJSON(); +// +// const columnPropertyColumns = (renderProperties && renderProperties.size > 0) ? +// // TODO: Make this not so ugly +// Object.keys(renderProperties.get('columnProperties').toJSON()) : +// []; +// +// return _.union(dataColumns, columnPropertyColumns); +// } +// ); +// }, +// dependencies: ["dataSelector", "renderPropertiesSelector"] +//}; + +export const sortedColumnPropertiesSelector = griddleCreateSelector( + "renderPropertiesSelector", + (renderProperties) => ( + renderProperties && renderProperties.get('columnProperties') && renderProperties.get('columnProperties').size !== 0 ? + renderProperties.get('columnProperties') + .sortBy(col => (col && col.get('order'))||MAX_SAFE_INTEGER) : + null + ) +); + +//export const sortedColumnPropertiesSelector = { +// creator: ({renderPropertiesSelector}) => { +// return createSelector( +// renderPropertiesSelector, +// (renderProperties) => ( +// renderProperties && renderProperties.get('columnProperties') && renderProperties.get('columnProperties').size !== 0 ? +// renderProperties.get('columnProperties') +// .sortBy(col => (col && col.get('order'))||MAX_SAFE_INTEGER) : +// null +// ) +// ); +// }, +// dependencies: ["renderPropertiesSelector"] +//}; + +export const metaDataColumnsSelector = griddleCreateSelector( + "sortedColumnPropertiesSelector", + (sortedColumnProperties) => ( + sortedColumnProperties ? sortedColumnProperties + .filter(c => c.get('isMetadata')) + .keySeq() + .toJSON() : + [] + ) +); + +//export const metaDataColumnsSelector = { +// creator: ({sortedColumnPropertiesSelector}) => { +// return createSelector( +// sortedColumnPropertiesSelector, +// (sortedColumnProperties) => ( +// sortedColumnProperties ? sortedColumnProperties +// .filter(c => c.get('isMetadata')) +// .keySeq() +// .toJSON() : +// [] +// ) +// ); +// }, +// dependencies: ["sortedColumnPropertiesSelector"] +//}; + +export const visibleColumnsSelector = griddleCreateSelector( + "sortedColumnPropertiesSelector", + "allColumnsSelector", + (sortedColumnProperties, allColumns) => ( + sortedColumnProperties ? sortedColumnProperties + .filter(c => { + const isVisible = c.get('visible') || c.get('visible') === undefined; + const isMetadata = c.get('isMetadata'); + return isVisible && !isMetadata; + }) + .keySeq() + .toJSON() : + allColumns + ) +); + +//export const visibleColumnsSelector = { +// creator: ({sortedColumnPropertiesSelector, allColumnsSelector}) => { +// return createSelector( +// sortedColumnPropertiesSelector, +// allColumnsSelector, +// (sortedColumnProperties, allColumns) => ( +// sortedColumnProperties ? sortedColumnProperties +// .filter(c => { +// const isVisible = c.get('visible') || c.get('visible') === undefined; +// const isMetadata = c.get('isMetadata'); +// return isVisible && !isMetadata; +// }) +// .keySeq() +// .toJSON() : +// allColumns +// ) +// ); +// }, +// dependencies: ["sortedColumnPropertiesSelector", "allColumnsSelector"] +//}; + +export const visibleColumnPropertiesSelector = griddleCreateSelector( + "visibleColumnsSelector", + "renderPropertiesSelector", +(visibleColumns=[], renderProperties) => ( visibleColumns.map(c => { const columnProperty = renderProperties.getIn(['columnProperties', c]); return (columnProperty && columnProperty.toJSON()) || { id: c } }) ) - ); - }, - dependencies: ["visibleColumnsSelector", "renderPropertiesSelector"] -}; - -export const hiddenColumnsSelector = { - creator: ({visibleColumnsSelector, allColumnsSelector, metaDataColumnsSelector}) => { - return createSelector( - visibleColumnsSelector, - allColumnsSelector, - metaDataColumnsSelector, - (visibleColumns, allColumns, metaDataColumns) => { +); + +//export const visibleColumnPropertiesSelector = { +// creator: ({visibleColumnsSelector, renderPropertiesSelector}) => { +// return createSelector( +// visibleColumnsSelector, +// renderPropertiesSelector, +// (visibleColumns=[], renderProperties) => ( +// visibleColumns.map(c => { +// const columnProperty = renderProperties.getIn(['columnProperties', c]); +// return (columnProperty && columnProperty.toJSON()) || { id: c } +// }) +// ) +// ); +// }, +// dependencies: ["visibleColumnsSelector", "renderPropertiesSelector"] +//}; + +export const hiddenColumnsSelector = griddleCreateSelector( + "visibleColumnsSelector", + "allColumnsSelector", + "metaDataColumnsSelector", +(visibleColumns, allColumns, metaDataColumns) => { const removeColumns = [...visibleColumns, ...metaDataColumns]; return allColumns.filter(c => removeColumns.indexOf(c) === -1); } - ); - }, - dependencies: ["visibleColumnsSelector", "allColumnsSelector", "metaDataColumnsSelector"] -}; - -export const hiddenColumnPropertiesSelector = { - creator: ({hiddenColumnsSelector, renderPropertiesSelector}) => { - return createSelector( - hiddenColumnsSelector, - renderPropertiesSelector, - (hiddenColumns=[], renderProperties) => ( +) + +//export const hiddenColumnsSelector = { +// creator: ({visibleColumnsSelector, allColumnsSelector, metaDataColumnsSelector}) => { +// return createSelector( +// visibleColumnsSelector, +// allColumnsSelector, +// metaDataColumnsSelector, +// (visibleColumns, allColumns, metaDataColumns) => { +// const removeColumns = [...visibleColumns, ...metaDataColumns]; +// +// return allColumns.filter(c => removeColumns.indexOf(c) === -1); +// } +// ); +// }, +// dependencies: ["visibleColumnsSelector", "allColumnsSelector", "metaDataColumnsSelector"] +//}; + +export const hiddenColumnPropertiesSelector = griddleCreateSelector( + "hiddenColumnsSelector", + "renderPropertiesSelector", +(hiddenColumns=[], renderProperties) => ( hiddenColumns.map(c => { const columnProperty = renderProperties.getIn(['columnProperties', c]); return (columnProperty && columnProperty.toJSON()) || { id: c } }) ) - ); - }, - dependencies: ["hiddenColumnsSelector", "renderPropertiesSelector"] -}; - -export const columnIdsSelector = { - creator: ({renderPropertiesSelector, visibleColumnsSelector}) => { - return createSelector( - renderPropertiesSelector, - visibleColumnsSelector, - (renderProperties, visibleColumns) => { +); + +//export const hiddenColumnPropertiesSelector = { +// creator: ({hiddenColumnsSelector, renderPropertiesSelector}) => { +// return createSelector( +// hiddenColumnsSelector, +// renderPropertiesSelector, +// (hiddenColumns=[], renderProperties) => ( +// hiddenColumns.map(c => { +// const columnProperty = renderProperties.getIn(['columnProperties', c]); +// +// return (columnProperty && columnProperty.toJSON()) || { id: c } +// }) +// ) +// ); +// }, +// dependencies: ["hiddenColumnsSelector", "renderPropertiesSelector"] +//}; + +export const columnIdsSelector = griddleCreateSelector( + "renderPropertiesSelector", + "visibleColumnsSelector", +(renderProperties, visibleColumns) => { const offset = 1000; // TODO: Make this better -- This is pretty inefficient return visibleColumns @@ -181,38 +290,72 @@ export const columnIdsSelector = { .sort((first, second) => first.order - second.order) .map(item => item.id); } - ); - }, - dependencies: ["renderPropertiesSelector", "visibleColumnsSelector"] -}; - -export const columnTitlesSelector = { - creator: ({columnIdsSelector, renderPropertiesSelector}) => { - return createSelector( - columnIdsSelector, - renderPropertiesSelector, - (columnIds, renderProperties) => columnIds.map(k => renderProperties.getIn(['columnProperties', k, 'title']) || k) - ); - }, - dependencies: ["columnIdsSelector", "renderPropertiesSelector"] -}; - -export const visibleRowIdsSelector = { - creator: ({dataSelector}) => { - return createSelector( - dataSelector, - currentPageData => currentPageData ? currentPageData.map(c => c.get('griddleKey')) : new Immutable.List() - ); - }, - dependencies: ["dataSelector"] -}; - -export const visibleRowCountSelector = { - creator: ({visibleRowIdsSelector}) => { - return createSelector( - visibleRowIdsSelector, - (visibleRowIds) => visibleRowIds.size - ); - }, - dependencies: ["visibleRowIdsSelector"] -}; +); + +//export const columnIdsSelector = { +// creator: ({renderPropertiesSelector, visibleColumnsSelector}) => { +// return createSelector( +// renderPropertiesSelector, +// visibleColumnsSelector, +// (renderProperties, visibleColumns) => { +// const offset = 1000; +// // TODO: Make this better -- This is pretty inefficient +// return visibleColumns +// .map((k, index) => ({ +// id: renderProperties.getIn(['columnProperties', k, 'id']) || k, +// order: renderProperties.getIn(['columnProperties', k, 'order']) || offset + index +// })) +// .sort((first, second) => first.order - second.order) +// .map(item => item.id); +// } +// ); +// }, +// dependencies: ["renderPropertiesSelector", "visibleColumnsSelector"] +//}; + +export const columnTitlesSelector = griddleCreateSelector( + "columnIdsSelector", + "renderPropertiesSelector", + (columnIds, renderProperties) => columnIds.map(k => renderProperties.getIn(['columnProperties', k, 'title']) || k) +); + +//export const columnTitlesSelector = { +// creator: ({columnIdsSelector, renderPropertiesSelector}) => { +// return createSelector( +// columnIdsSelector, +// renderPropertiesSelector, +// (columnIds, renderProperties) => columnIds.map(k => renderProperties.getIn(['columnProperties', k, 'title']) || k) +// ); +// }, +// dependencies: ["columnIdsSelector", "renderPropertiesSelector"] +//}; + +export const visibleRowIdsSelector = griddleCreateSelector( + "dataSelector", + currentPageData => currentPageData ? currentPageData.map(c => c.get('griddleKey')) : new Immutable.List() +); + +//export const visibleRowIdsSelector = { +// creator: ({dataSelector}) => { +// return createSelector( +// dataSelector, +// currentPageData => currentPageData ? currentPageData.map(c => c.get('griddleKey')) : new Immutable.List() +// ); +// }, +// dependencies: ["dataSelector"] +//}; + +export const visibleRowCountSelector = griddleCreateSelector( + "visibleRowIdsSelector", + (visibleRowIds) => visibleRowIds.size +); + +//export const visibleRowCountSelector = { +// creator: ({visibleRowIdsSelector}) => { +// return createSelector( +// visibleRowIdsSelector, +// (visibleRowIds) => visibleRowIds.size +// ); +// }, +// dependencies: ["visibleRowIdsSelector"] +//}; diff --git a/src/utils/selectorUtils.js b/src/utils/selectorUtils.js new file mode 100644 index 00000000..c6521570 --- /dev/null +++ b/src/utils/selectorUtils.js @@ -0,0 +1,277 @@ +import { forOwn } from 'lodash'; +import { createSelector } from 'reselect' + +/* + * 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. + */ +export 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); + for (let dependency of dependencies) { + if (typeof dependency !== "string") { + throw new Error("Args 0..n-1 must be strings"); + } + } + + // 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"); + } + + return { + // the creator function is called to generate the + // selector function. It is passed the object containing all + // of the static/generated selector functions to be potentially + // used as dependencies + creator: (selectors) => { + + // extract the dependency selectors using the list + // of dependencies + const createSelectorFuncs = []; + for (let dependency of dependencies) { + createSelectorFuncs.push(selectors[dependency]); + } + + // add this selector + createSelectorFuncs.push(selector); + + // call createSelector with the final list of args + return createSelector(...createSelectorFuncs); + }, + + // the list of dependencies is needed to build the dependency + // tree + dependencies + }; +}; + + +export const composeSelectors = (baseSelectors, composedSelectors, plugins) => { + + // STEP 1 + // ========== + // + // Add all of the 'base' selectors to the list of combined selectors. + // The actuall selector functions are wrapped in an object which is used + // to keep track of all the data needed to properly build all the + // selector dependency trees + console.log("Parsing built-in selectors"); + const combinedSelectors = new Map(); + + forOwn(baseSelectors, (baseSelector, name) => { + const selector = { + name, + selector: baseSelector, + dependencies: [], + rank: 0, + traversed: false + }; + combinedSelectors.set(name, selector); + }); + + // STEP 2 + // ========== + // + // Add all of the 'composed' selectors to the list of combined selectors. + // Composed selectors use the 'createSelector' function provided by reselect + // and depend on other selectors. These new selectors are located in a + // new file named 'composedSelectors' and are now an object that looks like this: + // { + // creator: ({dependency1, dependency2, ...}) => return createSelector(dependency1, dependency2, (...) => (...)), + // dependencies: ["dependency1", "dependency2"] + // } + // 'creator' will return the selector when it is run with the dependency selectors + // 'dependencies' are the string names of the dependency selectors, these will be used to + // build the tree of selectors + forOwn(composedSelectors, (composedSelector, name) => { + const selector = { + name, + ...composedSelector, + rank: 0, + traversed: false + }; + combinedSelectors.has(name) && console.log(` Overriding existing selector named ${name}`); + combinedSelectors.set(name, selector); + }); + + // STEP 3 + // ========== + // + // Once the built-in 'base' and 'composed' selectors are added to the list, + // repeat the same process for each of the plugins. + // + // Plugins can now redefine a single existing selector without having to + // include the full list of dependency selectors since the dependencies + // are now created dynamically + for (let i in plugins) { + console.log(`Parsing selectors for plugin ${i}`); + const plugin = plugins[i]; + forOwn(plugin.selectors, (baseSelector, name) => { + const selector = { + name, + selector: baseSelector, + dependencies: [], + rank: 0, + traversed: false + }; + + // console log for demonstration purposes + combinedSelectors.has(name) && console.log(` Overriding existing selector named ${name} with base selector`); + combinedSelectors.set(name, selector); + }); + + forOwn(plugin.composedSelectors, (composedSelector, name) => { + const selector = { + name, + ...composedSelector, + rank: 0, + traversed: false + }; + + // console log for demonstration purposes + combinedSelectors.has(name) && console.log(` Overriding existing selector named ${name} with composed selector`); + combinedSelectors.set(name, selector); + }); + } + + + // 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 (!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), childParents); + childsDependencies.forEach((key) => flattenedDependencies.add(key)) + const childRank = combinedSelectors.get(dependency).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 selector in the 'combinedSelectors' list + // This fills out the 'ranks' list for use in the next step + for (let e of combinedSelectors) { + const [name, selector] = e; + getDependencies(selector, new Set([name])); + } + + // STEP 5 + // ========== + // + // Create a flat object of just the actual selector functions + const flattenedSelectors = {}; + for (let rank of ranks) { + for (let selector of rank) { + if (selector.creator) { + const childSelectors = {}; + for (let childSelector of selector.dependencies) { + childSelectors[childSelector] = combinedSelectors.get(childSelector).selector; + } + selector.selector = selector.creator(childSelectors); + } + flattenedSelectors[selector.name] = selector.selector; + } + } + + return flattenedSelectors; +} From a04c699c8b28d3fcb9e4a487fb2e164fd4b0e581 Mon Sep 17 00:00:00 2001 From: "Short, James" Date: Wed, 11 Oct 2017 19:49:17 -0600 Subject: [PATCH 3/5] Refactor and add story --- package.json | 3 +- src/index.js | 200 --------------------------- src/module.d.ts | 1 + src/selectors/composedSelectors.js | 215 +---------------------------- src/utils/index.js | 2 + stories/index.tsx | 126 +++++++++++++++++ yarn.lock | 10 ++ 7 files changed, 142 insertions(+), 415 deletions(-) diff --git a/package.json b/package.json index 09c4c17b..f5c75852 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/index.js b/src/index.js index 6ff62bb3..8466195b 100644 --- a/src/index.js +++ b/src/index.js @@ -100,206 +100,6 @@ class Griddle extends Component { this.events = Object.assign({}, events, ...plugins.map(p => p.events)); - //// STEP 1 - //// ========== - //// - //// Add all of the 'base' selectors to the list of combined selectors. - //// The actuall selector functions are wrapped in an object which is used - //// to keep track of all the data needed to properly build all the - //// selector dependency trees - //console.log("Parsing built-in selectors"); - //const combinedSelectors = new Map(); - //const _baseSelectors = _.reduce(baseSelectors, (map, baseSelector, name) => { - // const selector = { - // name, - // selector: baseSelector, - // dependencies: [], - // rank: 0, - // traversed: false - // }; - // combinedSelectors.set(name, selector); - // map.set(name, selector); - // return map; - //}, new Map()); - - //// STEP 2 - //// ========== - //// - //// Add all of the 'composed' selectors to the list of combined selectors. - //// Composed selectors use the 'createSelector' function provided by reselect - //// and depend on other selectors. These new selectors are located in a - //// new file named 'composedSelectors' and are now an object that looks like this: - //// { - //// creator: ({dependency1, dependency2, ...}) => return createSelector(dependency1, dependency2, (...) => (...)), - //// dependencies: ["dependency1", "dependency2"] - //// } - //// 'creator' will return the selector when it is run with the dependency selectors - //// 'dependencies' are the string names of the dependency selectors, these will be used to - //// build the tree of selectors - //const _composedSelectors = _.reduce(composedSelectors, (map, composedSelector, name) => { - // const selector = { - // name, - // ...composedSelector, - // rank: 0, - // traversed: false - // }; - // combinedSelectors.has(name) && console.log(` Overriding existing selector named ${name}`); - // combinedSelectors.set(name, selector); - // map.set(name, selector); - // return map; - //}, new Map()); - - //// STEP 3 - //// ========== - //// - //// Once the built-in 'base' and 'composed' selectors are added to the list, - //// repeat the same process for each of the plugins. - //// - //// Plugins can now redefine a single existing selector without having to - //// include the full list of dependency selectors since the dependencies - //// are now created dynamically - //for (let i in plugins) { - // console.log(`Parsing selectors for plugin ${i}`); - // const plugin = plugins[i]; - // _.forOwn(plugin.selectors, (baseSelector, name) => { - // const selector = { - // name, - // selector: baseSelector, - // dependencies: [], - // rank: 0, - // traversed: false - // }; - - // // console log for demonstration purposes - // combinedSelectors.has(name) && console.log(` Overriding existing selector named ${name} with base selector`); - // combinedSelectors.set(name, selector); - // }); - - // _.forOwn(plugin.composedSelectors, (composedSelector, name) => { - // const selector = { - // name, - // ...composedSelector, - // rank: 0, - // traversed: false - // }; - - // // console log for demonstration purposes - // combinedSelectors.has(name) && console.log(` Overriding existing selector named ${name} with composed selector`); - // combinedSelectors.set(name, selector); - // }); - //} - - - //// 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 (!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), childParents); - // childsDependencies.forEach((key) => flattenedDependencies.add(key)) - // const childRank = combinedSelectors.get(dependency).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 selector in the 'combinedSelectors' list - //// This fills out the 'ranks' list for use in the next step - //for (let e of combinedSelectors) { - // const [name, selector] = e; - // getDependencies(selector, new Set([name])); - //} - - //// STEP 5 - //// ========== - //// - //// Create a flat object of just the actual selector functions - //const flattenedSelectors = {}; - //for (let rank of ranks) { - // for (let selector of rank) { - // if (selector.creator) { - // const childSelectors = {}; - // for (let childSelector of selector.dependencies) { - // childSelectors[childSelector] = combinedSelectors.get(childSelector).selector; - // } - // selector.selector = selector.creator(childSelectors); - // } - // flattenedSelectors[selector.name] = selector.selector; - // } - //} - - ////this.selectors = plugins.reduce((combined, plugin) => ({ ...combined, ...plugin.selectors }), {...selectors}); - //this.selectors = flattenedSelectors; this.selectors = composeSelectors(baseSelectors, composedSelectors, plugins); const mergedStyleConfig = _.merge({}, defaultStyleConfig, ...plugins.map(p => p.styleConfig), styleConfig); diff --git a/src/module.d.ts b/src/module.d.ts index 5ae07a04..b621a212 100644 --- a/src/module.d.ts +++ b/src/module.d.ts @@ -422,6 +422,7 @@ export namespace utils { const compositionUtils: PropertyBag; const dataUtils: PropertyBag; const rowUtils: PropertyBag; + const selectorUtils: PropertyBag; const connect : typeof originalConnect; diff --git a/src/selectors/composedSelectors.js b/src/selectors/composedSelectors.js index c51b0248..42f89331 100644 --- a/src/selectors/composedSelectors.js +++ b/src/selectors/composedSelectors.js @@ -9,16 +9,6 @@ export const hasPreviousSelector = griddleCreateSelector( (currentPage) => (currentPage > 1) ); -//export const hasPreviousSelector = { -// creator: ({currentPageSelector}) => { -// return createSelector( -// currentPageSelector, -// (currentPage) => (currentPage > 1) -// ); -// }, -// dependencies: ["currentPageSelector"] -//}; - export const maxPageSelector = griddleCreateSelector( "pageSizeSelector", "recordCountSelector", @@ -29,21 +19,6 @@ export const maxPageSelector = griddleCreateSelector( } ); -//export const maxPageSelector = { -// creator: ({pageSizeSelector, recordCountSelector}) => { -// return createSelector( -// pageSizeSelector, -// recordCountSelector, -// (pageSize, recordCount) => { -// const calc = recordCount / pageSize; -// const result = calc > Math.floor(calc) ? Math.floor(calc) + 1 : Math.floor(calc); -// return _.isFinite(result) ? result : 1; -// } -// ); -// }, -// dependencies: ["pageSizeSelector", "recordCountSelector"] -//}; - export const hasNextSelector = griddleCreateSelector( "currentPageSelector", "maxPageSelector", @@ -52,19 +27,6 @@ export const hasNextSelector = griddleCreateSelector( } ); -//export const hasNextSelector = { -// creator: ({currentPageSelector, maxPageSelector}) => { -// return createSelector( -// currentPageSelector, -// maxPageSelector, -// (currentPage, maxPage) => { -// return currentPage < maxPage; -// } -// ); -// }, -// dependencies: ["currentPageSelector", "maxPageSelector"] -//}; - export const allColumnsSelector = griddleCreateSelector( "dataSelector", "renderPropertiesSelector", @@ -82,28 +44,6 @@ export const allColumnsSelector = griddleCreateSelector( } ); -//export const allColumnsSelector = { -// creator: ({dataSelector, renderPropertiesSelector}) => { -// return createSelector( -// dataSelector, -// renderPropertiesSelector, -// (data, renderProperties) => { -// const dataColumns = !data || data.size === 0 ? -// [] : -// data.get(0).keySeq().toJSON(); -// -// const columnPropertyColumns = (renderProperties && renderProperties.size > 0) ? -// // TODO: Make this not so ugly -// Object.keys(renderProperties.get('columnProperties').toJSON()) : -// []; -// -// return _.union(dataColumns, columnPropertyColumns); -// } -// ); -// }, -// dependencies: ["dataSelector", "renderPropertiesSelector"] -//}; - export const sortedColumnPropertiesSelector = griddleCreateSelector( "renderPropertiesSelector", (renderProperties) => ( @@ -114,21 +54,6 @@ export const sortedColumnPropertiesSelector = griddleCreateSelector( ) ); -//export const sortedColumnPropertiesSelector = { -// creator: ({renderPropertiesSelector}) => { -// return createSelector( -// renderPropertiesSelector, -// (renderProperties) => ( -// renderProperties && renderProperties.get('columnProperties') && renderProperties.get('columnProperties').size !== 0 ? -// renderProperties.get('columnProperties') -// .sortBy(col => (col && col.get('order'))||MAX_SAFE_INTEGER) : -// null -// ) -// ); -// }, -// dependencies: ["renderPropertiesSelector"] -//}; - export const metaDataColumnsSelector = griddleCreateSelector( "sortedColumnPropertiesSelector", (sortedColumnProperties) => ( @@ -140,22 +65,6 @@ export const metaDataColumnsSelector = griddleCreateSelector( ) ); -//export const metaDataColumnsSelector = { -// creator: ({sortedColumnPropertiesSelector}) => { -// return createSelector( -// sortedColumnPropertiesSelector, -// (sortedColumnProperties) => ( -// sortedColumnProperties ? sortedColumnProperties -// .filter(c => c.get('isMetadata')) -// .keySeq() -// .toJSON() : -// [] -// ) -// ); -// }, -// dependencies: ["sortedColumnPropertiesSelector"] -//}; - export const visibleColumnsSelector = griddleCreateSelector( "sortedColumnPropertiesSelector", "allColumnsSelector", @@ -172,27 +81,6 @@ export const visibleColumnsSelector = griddleCreateSelector( ) ); -//export const visibleColumnsSelector = { -// creator: ({sortedColumnPropertiesSelector, allColumnsSelector}) => { -// return createSelector( -// sortedColumnPropertiesSelector, -// allColumnsSelector, -// (sortedColumnProperties, allColumns) => ( -// sortedColumnProperties ? sortedColumnProperties -// .filter(c => { -// const isVisible = c.get('visible') || c.get('visible') === undefined; -// const isMetadata = c.get('isMetadata'); -// return isVisible && !isMetadata; -// }) -// .keySeq() -// .toJSON() : -// allColumns -// ) -// ); -// }, -// dependencies: ["sortedColumnPropertiesSelector", "allColumnsSelector"] -//}; - export const visibleColumnPropertiesSelector = griddleCreateSelector( "visibleColumnsSelector", "renderPropertiesSelector", @@ -204,22 +92,6 @@ export const visibleColumnPropertiesSelector = griddleCreateSelector( ) ); -//export const visibleColumnPropertiesSelector = { -// creator: ({visibleColumnsSelector, renderPropertiesSelector}) => { -// return createSelector( -// visibleColumnsSelector, -// renderPropertiesSelector, -// (visibleColumns=[], renderProperties) => ( -// visibleColumns.map(c => { -// const columnProperty = renderProperties.getIn(['columnProperties', c]); -// return (columnProperty && columnProperty.toJSON()) || { id: c } -// }) -// ) -// ); -// }, -// dependencies: ["visibleColumnsSelector", "renderPropertiesSelector"] -//}; - export const hiddenColumnsSelector = griddleCreateSelector( "visibleColumnsSelector", "allColumnsSelector", @@ -229,23 +101,7 @@ export const hiddenColumnsSelector = griddleCreateSelector( return allColumns.filter(c => removeColumns.indexOf(c) === -1); } -) - -//export const hiddenColumnsSelector = { -// creator: ({visibleColumnsSelector, allColumnsSelector, metaDataColumnsSelector}) => { -// return createSelector( -// visibleColumnsSelector, -// allColumnsSelector, -// metaDataColumnsSelector, -// (visibleColumns, allColumns, metaDataColumns) => { -// const removeColumns = [...visibleColumns, ...metaDataColumns]; -// -// return allColumns.filter(c => removeColumns.indexOf(c) === -1); -// } -// ); -// }, -// dependencies: ["visibleColumnsSelector", "allColumnsSelector", "metaDataColumnsSelector"] -//}; +); export const hiddenColumnPropertiesSelector = griddleCreateSelector( "hiddenColumnsSelector", @@ -259,23 +115,6 @@ export const hiddenColumnPropertiesSelector = griddleCreateSelector( ) ); -//export const hiddenColumnPropertiesSelector = { -// creator: ({hiddenColumnsSelector, renderPropertiesSelector}) => { -// return createSelector( -// hiddenColumnsSelector, -// renderPropertiesSelector, -// (hiddenColumns=[], renderProperties) => ( -// hiddenColumns.map(c => { -// const columnProperty = renderProperties.getIn(['columnProperties', c]); -// -// return (columnProperty && columnProperty.toJSON()) || { id: c } -// }) -// ) -// ); -// }, -// dependencies: ["hiddenColumnsSelector", "renderPropertiesSelector"] -//}; - export const columnIdsSelector = griddleCreateSelector( "renderPropertiesSelector", "visibleColumnsSelector", @@ -292,70 +131,18 @@ export const columnIdsSelector = griddleCreateSelector( } ); -//export const columnIdsSelector = { -// creator: ({renderPropertiesSelector, visibleColumnsSelector}) => { -// return createSelector( -// renderPropertiesSelector, -// visibleColumnsSelector, -// (renderProperties, visibleColumns) => { -// const offset = 1000; -// // TODO: Make this better -- This is pretty inefficient -// return visibleColumns -// .map((k, index) => ({ -// id: renderProperties.getIn(['columnProperties', k, 'id']) || k, -// order: renderProperties.getIn(['columnProperties', k, 'order']) || offset + index -// })) -// .sort((first, second) => first.order - second.order) -// .map(item => item.id); -// } -// ); -// }, -// dependencies: ["renderPropertiesSelector", "visibleColumnsSelector"] -//}; - export const columnTitlesSelector = griddleCreateSelector( "columnIdsSelector", "renderPropertiesSelector", (columnIds, renderProperties) => columnIds.map(k => renderProperties.getIn(['columnProperties', k, 'title']) || k) ); -//export const columnTitlesSelector = { -// creator: ({columnIdsSelector, renderPropertiesSelector}) => { -// return createSelector( -// columnIdsSelector, -// renderPropertiesSelector, -// (columnIds, renderProperties) => columnIds.map(k => renderProperties.getIn(['columnProperties', k, 'title']) || k) -// ); -// }, -// dependencies: ["columnIdsSelector", "renderPropertiesSelector"] -//}; - export const visibleRowIdsSelector = griddleCreateSelector( "dataSelector", currentPageData => currentPageData ? currentPageData.map(c => c.get('griddleKey')) : new Immutable.List() ); -//export const visibleRowIdsSelector = { -// creator: ({dataSelector}) => { -// return createSelector( -// dataSelector, -// currentPageData => currentPageData ? currentPageData.map(c => c.get('griddleKey')) : new Immutable.List() -// ); -// }, -// dependencies: ["dataSelector"] -//}; - export const visibleRowCountSelector = griddleCreateSelector( "visibleRowIdsSelector", (visibleRowIds) => visibleRowIds.size ); - -//export const visibleRowCountSelector = { -// creator: ({visibleRowIdsSelector}) => { -// return createSelector( -// visibleRowIdsSelector, -// (visibleRowIds) => visibleRowIds.size -// ); -// }, -// dependencies: ["visibleRowIdsSelector"] -//}; 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/stories/index.tsx b/stories/index.tsx index 6738ea37..98f88894 100644 --- a/stories/index.tsx +++ b/stories/index.tsx @@ -11,12 +11,15 @@ import { Provider } from 'react-redux'; import { createStore } from 'redux'; import { createSelector } from 'reselect'; import _ from 'lodash'; +import { createLogger } from 'redux-logger'; import GenericGriddle, { actions, components, selectors, plugins, utils, ColumnDefinition, RowDefinition } from '../src/module'; const { connect } = utils; const { Cell, Row, Table, TableContainer, TableBody, TableHeading, TableHeadingCell } = components; const { SettingsWrapper, SettingsToggle, Settings } = components; +const { griddleCreateSelector } = utils.selectorUtils; + const { LegacyStylePlugin, LocalPlugin, PositionPlugin } = plugins; import fakeData, { FakeData } from './fakeData'; @@ -845,6 +848,129 @@ 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 = griddleCreateSelector ( + "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 OverridableSelectorsPlugin = { + components: { + NextButtonEnhancer, + PageDropdownEnhancer + }, + reducer: { + GRIDDLE_NEXT_PAGE, + GRIDDLE_PREVIOUS_PAGE, + GRIDDLE_SET_PAGE + }, + selectors: { + allDataSelector, + recordCountSelector + }, + composedSelectors: { + dataSelector + } + } + + return ( + + ); + }) storiesOf('Cell', module) .add('base cell', () => { 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" From 8f9c74cda42766ad2ca3b787124174a08083246e Mon Sep 17 00:00:00 2001 From: "Short, James" Date: Wed, 11 Oct 2017 20:33:42 -0600 Subject: [PATCH 4/5] add dataLoadingSelector to composedSelectors --- src/selectors/composedSelectors.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/selectors/composedSelectors.js b/src/selectors/composedSelectors.js index 42f89331..beaea878 100644 --- a/src/selectors/composedSelectors.js +++ b/src/selectors/composedSelectors.js @@ -4,6 +4,11 @@ import _ from 'lodash'; import MAX_SAFE_INTEGER from 'max-safe-integer' import { griddleCreateSelector } from '../utils/selectorUtils'; +export const dataLoadingSelector = griddleCreateSelector( + "dataSelector", + data => !data +); + export const hasPreviousSelector = griddleCreateSelector( "currentPageSelector", (currentPage) => (currentPage > 1) From 6da06fab73d3345f9b7d615fac6722d6ff8d3617 Mon Sep 17 00:00:00 2001 From: "Short, James" Date: Wed, 11 Oct 2017 20:51:14 -0600 Subject: [PATCH 5/5] Complete example story --- stories/index.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/stories/index.tsx b/stories/index.tsx index ecde1e07..e3b37954 100644 --- a/stories/index.tsx +++ b/stories/index.tsx @@ -973,12 +973,23 @@ storiesOf('Plugins', module) ) )((props) => ); + const PreviousButtonEnhancer = OriginalComponent => compose( + connect( + null, + (dispatch, props) => { + return { + getPrevious: () => dispatch(getPrevious()) + } + } + ) + )((props) => ); const OverridableSelectorsPlugin = { components: { NextButtonEnhancer, - PageDropdownEnhancer + PageDropdownEnhancer, + PreviousButtonEnhancer }, reducer: { GRIDDLE_NEXT_PAGE, @@ -992,7 +1003,7 @@ storiesOf('Plugins', module) composedSelectors: { dataSelector } - } + }; return (