From 89ecac27c8fdf234ca0d7bac842d17d0dae3e462 Mon Sep 17 00:00:00 2001 From: "Short, James" Date: Tue, 26 Sep 2017 01:16:29 -0600 Subject: [PATCH 01/17] 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 02/17] 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 bf2f54e5d5849a0d0494322722e12ddfd16e5195 Mon Sep 17 00:00:00 2001 From: "Short, James" Date: Tue, 26 Sep 2017 01:16:29 -0600 Subject: [PATCH 03/17] Composable selectors --- src/index.js | 3 +- src/plugins/local/selectors/localSelectors.js | 2 +- src/plugins/position/selectors/index.js | 4 +- src/selectors/dataSelectors.js | 2 +- src/utils/selectorUtils.js | 228 ++++++++++++++++++ 5 files changed, 235 insertions(+), 4 deletions(-) create mode 100644 src/utils/selectorUtils.js diff --git a/src/index.js b/src/index.js index cc54f81f..f8ce26e1 100644 --- a/src/index.js +++ b/src/index.js @@ -15,6 +15,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 = { @@ -98,7 +99,7 @@ class Griddle extends Component { this.events = Object.assign({}, events, ...plugins.map(p => p.events)); - this.selectors = plugins.reduce((combined, plugin) => ({ ...combined, ...plugin.selectors }), {...selectors}); + this.selectors = composeSelectors(selectors, plugins); const mergedStyleConfig = _.merge({}, defaultStyleConfig, ...plugins.map(p => p.styleConfig), styleConfig); diff --git a/src/plugins/local/selectors/localSelectors.js b/src/plugins/local/selectors/localSelectors.js index 2ae6a8b8..8c049eff 100644 --- a/src/plugins/local/selectors/localSelectors.js +++ b/src/plugins/local/selectors/localSelectors.js @@ -1,5 +1,5 @@ import Immutable from 'immutable'; -import { createSelector } from 'reselect'; +import { createSelector } from '../../../utils/selectorUtils'; import MAX_SAFE_INTEGER from 'max-safe-integer' import { defaultSort } from '../../../utils/sortUtils'; diff --git a/src/plugins/position/selectors/index.js b/src/plugins/position/selectors/index.js index fda22695..79c0df4a 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'; diff --git a/src/selectors/dataSelectors.js b/src/selectors/dataSelectors.js index d96c1487..93deaa6b 100644 --- a/src/selectors/dataSelectors.js +++ b/src/selectors/dataSelectors.js @@ -1,6 +1,6 @@ import Immutable from 'immutable'; -import { createSelector } from 'reselect'; import _ from 'lodash'; +import { createSelector } from '../utils/selectorUtils'; import MAX_SAFE_INTEGER from 'max-safe-integer' //import { createSelector } from 'reselect'; diff --git a/src/utils/selectorUtils.js b/src/utils/selectorUtils.js new file mode 100644 index 00000000..b5a1e8a5 --- /dev/null +++ b/src/utils/selectorUtils.js @@ -0,0 +1,228 @@ +import { forOwn } from 'lodash'; +import { createSelector } from 'reselect' + +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 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"); + } + + const composedSelector = (selectors) => { + // Legacy components might call this selector directly with state + // so use global cache of selectors instead + const dependencySelectors = selectors._dependencies ? selectors : globalSelectors; + + // extract the dependency selectors using the list + // of dependencies + const createSelectorFuncs = []; + for (const dependency of dependencies) { + if (typeof dependency !== 'string') { + createSelectorFuncs.push(dependency); + } else { + createSelectorFuncs.push(dependencySelectors[dependency] + || console.warn(`Dependency ${dependency} not found!`)); + } + } + + // add this selector + createSelectorFuncs.push(selector); + + // call createSelector with the final list of args + if (selectors._dependencies) { + return createSelector(...createSelectorFuncs); + } + + // Selector was called directly in legacy code + return createSelector(...createSelectorFuncs)(selectors); + }; + composedSelector.dependencies = dependencies; + return composedSelector; +}; +export { griddleCreateSelector as createSelector }; + + +export const composeSelectors = (defaultSelectors, plugins) => { + + // STEP 1 + // ========== + // + // Add all 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 + const combinedSelectors = new Map(); + const allSelectors = [defaultSelectors].concat(...plugins.map(p => p.selectors)); + + allSelectors.forEach((selectors) => { + forOwn(selectors, (selector, name) => { + if (combinedSelectors.has(name)) { + console.log(` Overriding existing selector named ${name}`); + } + + combinedSelectors.set(name, { + 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), 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 = {}; + console.log({ allSelectors, combinedSelectors, ranks }); + for (let rank of ranks) { + for (let selector of rank) { + if (selector.dependencies.length) { + 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; +} From bc770f6c79e1376c714678bca9df3aa023e86773 Mon Sep 17 00:00:00 2001 From: Keith Dahlby Date: Sat, 7 Oct 2017 17:18:33 -0500 Subject: [PATCH 04/17] Convert existing selectors to dynamic dependency resolution --- src/plugins/local/selectors/localSelectors.js | 50 +++++++++---------- src/plugins/position/selectors/index.js | 40 +++++++-------- src/selectors/dataSelectors.js | 48 +++++++++--------- 3 files changed, 69 insertions(+), 69 deletions(-) diff --git a/src/plugins/local/selectors/localSelectors.js b/src/plugins/local/selectors/localSelectors.js index 8c049eff..d420afc8 100644 --- a/src/plugins/local/selectors/localSelectors.js +++ b/src/plugins/local/selectors/localSelectors.js @@ -33,14 +33,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) { return data; @@ -65,8 +65,8 @@ export const filteredDataSelector = createSelector( /** Gets the max page size */ export const maxPageSelector = createSelector( - pageSizeSelector, - filteredDataSelector, + 'pageSizeSelector', + 'filteredDataSelector', (pageSize, data) => { const total = data.size; const calc = total / pageSize; @@ -76,7 +76,7 @@ export const maxPageSelector = createSelector( ) export const allColumnsSelector = createSelector( - dataSelector, + 'dataSelector', (data) => (data.size === 0 ? [] : data.get(0).keySeq().toJSON()) ); @@ -91,8 +91,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) ); @@ -104,10 +104,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; } @@ -124,9 +124,9 @@ export const sortedDataSelector = createSelector( /** Gets the current page of data */ export const currentPageDataSelector = createSelector( - sortedDataSelector, - pageSizeSelector, - currentPageSelector, + 'sortedDataSelector', + 'pageSizeSelector', + 'currentPageSelector', (sortedData, pageSize, currentPage) => { return sortedData .skip(pageSize * (currentPage - 1)) @@ -137,29 +137,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.map(c => c.get('griddleKey')) ); /** 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]; @@ -170,8 +170,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/index.js b/src/plugins/position/selectors/index.js index 79c0df4a..17d98d35 100644 --- a/src/plugins/position/selectors/index.js +++ b/src/plugins/position/selectors/index.js @@ -20,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; } @@ -38,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)); @@ -48,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; @@ -58,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); } @@ -79,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) @@ -93,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/selectors/dataSelectors.js b/src/selectors/dataSelectors.js index 93deaa6b..d242d566 100644 --- a/src/selectors/dataSelectors.js +++ b/src/selectors/dataSelectors.js @@ -22,15 +22,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; @@ -42,8 +42,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; } @@ -57,8 +57,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 ? [] : @@ -76,7 +76,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') @@ -88,7 +88,7 @@ export const sortedColumnPropertiesSelector = createSelector( /** Gets metadata column ids */ export const metaDataColumnsSelector = createSelector( - sortedColumnPropertiesSelector, + 'sortedColumnPropertiesSelector', (sortedColumnProperties) => ( sortedColumnProperties ? sortedColumnProperties .filter(c => c.get('isMetadata')) @@ -101,8 +101,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 => { @@ -119,8 +119,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]); @@ -131,9 +131,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]; @@ -144,8 +144,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]); @@ -215,8 +215,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 @@ -233,20 +233,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 ); From 4089fad60c7576cf6a02587718bbfc74babb3ed2 Mon Sep 17 00:00:00 2001 From: "Lis, Scott" Date: Thu, 21 Sep 2017 13:36:44 -0600 Subject: [PATCH 05/17] Compose enhancers instead of overriding them --- src/module.d.ts | 3 +++ src/module.js | 4 ++++ src/plugins/aPlugin/RowEnhancer.js | 19 +++++++++++++++ src/plugins/aPlugin/index.js | 7 ++++++ src/plugins/bPlugin/RowEnhancer.js | 19 +++++++++++++++ src/plugins/bPlugin/index.js | 7 ++++++ src/utils/compositionUtils.js | 37 ++++++++++++++++++++++++++---- stories/index.tsx | 12 +++++++++- 8 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 src/plugins/aPlugin/RowEnhancer.js create mode 100644 src/plugins/aPlugin/index.js create mode 100644 src/plugins/bPlugin/RowEnhancer.js create mode 100644 src/plugins/bPlugin/index.js diff --git a/src/module.d.ts b/src/module.d.ts index 5ae07a04..4cfbcd03 100644 --- a/src/module.d.ts +++ b/src/module.d.ts @@ -465,6 +465,9 @@ export namespace plugins { disablePointerEvents?: boolean; } var PositionPlugin : (settings: PositionSettings) => GriddlePlugin; + + var APlugin: GriddlePlugin; + var BPlugin: GriddlePlugin; } export const ColumnDefinition : typeof components.ColumnDefinition; diff --git a/src/module.js b/src/module.js index 66a854d4..05e63aea 100644 --- a/src/module.js +++ b/src/module.js @@ -10,11 +10,15 @@ import utils from './utils'; import LegacyStylePlugin from './plugins/legacyStyle'; import LocalPlugin from './plugins/local'; import PositionPlugin from './plugins/position'; +import APlugin from './plugins/aPlugin'; +import BPlugin from './plugins/bPlugin'; const plugins = { LegacyStylePlugin, LocalPlugin, PositionPlugin, + APlugin, + BPlugin }; const ColumnDefinition = components.ColumnDefinition; diff --git a/src/plugins/aPlugin/RowEnhancer.js b/src/plugins/aPlugin/RowEnhancer.js new file mode 100644 index 00000000..82f5bd4b --- /dev/null +++ b/src/plugins/aPlugin/RowEnhancer.js @@ -0,0 +1,19 @@ +import React from 'react'; +import { connect } from '../../utils/griddleConnect'; +import { compose, mapProps } from 'recompose'; + +const EnhancedRow = OriginalComponent => compose( + connect((state, props) => { + return { + }; + }) +)(props => { + return ( +
+ a + +
+ ); +}); + +export default EnhancedRow; \ No newline at end of file diff --git a/src/plugins/aPlugin/index.js b/src/plugins/aPlugin/index.js new file mode 100644 index 00000000..0b12deba --- /dev/null +++ b/src/plugins/aPlugin/index.js @@ -0,0 +1,7 @@ +import RowEnhancer from './RowEnhancer'; + +export default { + components: { + RowEnhancer: RowEnhancer + } +}; \ No newline at end of file diff --git a/src/plugins/bPlugin/RowEnhancer.js b/src/plugins/bPlugin/RowEnhancer.js new file mode 100644 index 00000000..e4284d5b --- /dev/null +++ b/src/plugins/bPlugin/RowEnhancer.js @@ -0,0 +1,19 @@ +import React from 'react'; +import { connect } from '../../utils/griddleConnect'; +import { compose, mapProps } from 'recompose'; + +const EnhancedRow = OriginalComponent => compose( + connect((state, props) => { + return { + }; + }) +)(props => { + return ( +
+ b + +
+ ); +}); + +export default EnhancedRow; \ No newline at end of file diff --git a/src/plugins/bPlugin/index.js b/src/plugins/bPlugin/index.js new file mode 100644 index 00000000..0b12deba --- /dev/null +++ b/src/plugins/bPlugin/index.js @@ -0,0 +1,7 @@ +import RowEnhancer from './RowEnhancer'; + +export default { + components: { + RowEnhancer: RowEnhancer + } +}; \ No newline at end of file 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/stories/index.tsx b/stories/index.tsx index aba578da..17609cd5 100644 --- a/stories/index.tsx +++ b/stories/index.tsx @@ -17,7 +17,7 @@ const { connect } = utils; const { Cell, Row, Table, TableContainer, TableBody, TableHeading, TableHeadingCell } = components; const { SettingsWrapper, SettingsToggle, Settings } = components; -const { LegacyStylePlugin, LocalPlugin, PositionPlugin } = plugins; +const { LegacyStylePlugin, LocalPlugin, PositionPlugin, APlugin, BPlugin } = plugins; import fakeData, { FakeData } from './fakeData'; import { person, fakeData2, personClass, fakeData3 } from './fakeData2'; @@ -1544,6 +1544,16 @@ storiesOf('Settings', module) ); }) + .add('multi-enhancer-test', () => { + + return ( + + ); + }) + storiesOf('TypeScript', module) .add('GriddleComponent accepts expected types', () => { class Custom extends React.Component<{ value }> { From 2c6e6105628c936ad74f8564e1d8b6fd909447a2 Mon Sep 17 00:00:00 2001 From: "Short, James" Date: Wed, 11 Oct 2017 19:41:02 -0600 Subject: [PATCH 06/17] Roll back some changes --- src/module.d.ts | 3 --- src/module.js | 6 +----- src/plugins/aPlugin/RowEnhancer.js | 19 ------------------- src/plugins/aPlugin/index.js | 7 ------- src/plugins/bPlugin/RowEnhancer.js | 19 ------------------- src/plugins/bPlugin/index.js | 7 ------- stories/index.tsx | 12 +----------- 7 files changed, 2 insertions(+), 71 deletions(-) delete mode 100644 src/plugins/aPlugin/RowEnhancer.js delete mode 100644 src/plugins/aPlugin/index.js delete mode 100644 src/plugins/bPlugin/RowEnhancer.js delete mode 100644 src/plugins/bPlugin/index.js diff --git a/src/module.d.ts b/src/module.d.ts index 4cfbcd03..5ae07a04 100644 --- a/src/module.d.ts +++ b/src/module.d.ts @@ -465,9 +465,6 @@ export namespace plugins { disablePointerEvents?: boolean; } var PositionPlugin : (settings: PositionSettings) => GriddlePlugin; - - var APlugin: GriddlePlugin; - var BPlugin: GriddlePlugin; } export const ColumnDefinition : typeof components.ColumnDefinition; diff --git a/src/module.js b/src/module.js index 05e63aea..bcaaafdb 100644 --- a/src/module.js +++ b/src/module.js @@ -10,15 +10,11 @@ import utils from './utils'; import LegacyStylePlugin from './plugins/legacyStyle'; import LocalPlugin from './plugins/local'; import PositionPlugin from './plugins/position'; -import APlugin from './plugins/aPlugin'; -import BPlugin from './plugins/bPlugin'; const plugins = { LegacyStylePlugin, LocalPlugin, - PositionPlugin, - APlugin, - BPlugin + PositionPlugin }; const ColumnDefinition = components.ColumnDefinition; diff --git a/src/plugins/aPlugin/RowEnhancer.js b/src/plugins/aPlugin/RowEnhancer.js deleted file mode 100644 index 82f5bd4b..00000000 --- a/src/plugins/aPlugin/RowEnhancer.js +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import { connect } from '../../utils/griddleConnect'; -import { compose, mapProps } from 'recompose'; - -const EnhancedRow = OriginalComponent => compose( - connect((state, props) => { - return { - }; - }) -)(props => { - return ( -
- a - -
- ); -}); - -export default EnhancedRow; \ No newline at end of file diff --git a/src/plugins/aPlugin/index.js b/src/plugins/aPlugin/index.js deleted file mode 100644 index 0b12deba..00000000 --- a/src/plugins/aPlugin/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import RowEnhancer from './RowEnhancer'; - -export default { - components: { - RowEnhancer: RowEnhancer - } -}; \ No newline at end of file diff --git a/src/plugins/bPlugin/RowEnhancer.js b/src/plugins/bPlugin/RowEnhancer.js deleted file mode 100644 index e4284d5b..00000000 --- a/src/plugins/bPlugin/RowEnhancer.js +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import { connect } from '../../utils/griddleConnect'; -import { compose, mapProps } from 'recompose'; - -const EnhancedRow = OriginalComponent => compose( - connect((state, props) => { - return { - }; - }) -)(props => { - return ( -
- b - -
- ); -}); - -export default EnhancedRow; \ No newline at end of file diff --git a/src/plugins/bPlugin/index.js b/src/plugins/bPlugin/index.js deleted file mode 100644 index 0b12deba..00000000 --- a/src/plugins/bPlugin/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import RowEnhancer from './RowEnhancer'; - -export default { - components: { - RowEnhancer: RowEnhancer - } -}; \ No newline at end of file diff --git a/stories/index.tsx b/stories/index.tsx index 17609cd5..aba578da 100644 --- a/stories/index.tsx +++ b/stories/index.tsx @@ -17,7 +17,7 @@ const { connect } = utils; const { Cell, Row, Table, TableContainer, TableBody, TableHeading, TableHeadingCell } = components; const { SettingsWrapper, SettingsToggle, Settings } = components; -const { LegacyStylePlugin, LocalPlugin, PositionPlugin, APlugin, BPlugin } = plugins; +const { LegacyStylePlugin, LocalPlugin, PositionPlugin } = plugins; import fakeData, { FakeData } from './fakeData'; import { person, fakeData2, personClass, fakeData3 } from './fakeData2'; @@ -1544,16 +1544,6 @@ storiesOf('Settings', module) ); }) - .add('multi-enhancer-test', () => { - - return ( - - ); - }) - storiesOf('TypeScript', module) .add('GriddleComponent accepts expected types', () => { class Custom extends React.Component<{ value }> { From a04c699c8b28d3fcb9e4a487fb2e164fd4b0e581 Mon Sep 17 00:00:00 2001 From: "Short, James" Date: Wed, 11 Oct 2017 19:49:17 -0600 Subject: [PATCH 07/17] 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 0567dfcdd30357944c35df4190696804ca2ba511 Mon Sep 17 00:00:00 2001 From: "Short, James" Date: Mon, 18 Sep 2017 16:03:53 -0600 Subject: [PATCH 08/17] Move selectors on context change to its own branch --- src/components/CellContainer.js | 24 +++++++-------- src/components/FilterContainer.js | 20 +++++++++---- src/components/LayoutContainer.js | 7 +++-- src/components/NextButtonContainer.js | 22 +++++++++----- src/components/NoResultsContainer.js | 9 +++--- src/components/PageDropdownContainer.js | 17 ++++++----- src/components/PaginationContainer.js | 7 +++-- src/components/PreviousButtonContainer.js | 24 ++++++++++----- src/components/RowContainer.js | 33 +++++++++++---------- src/components/SettingsContainer.js | 7 +++-- src/components/SettingsToggleContainer.js | 11 +++++-- src/components/SettingsWrapperContainer.js | 27 ++++++++++------- src/components/TableBodyContainer.js | 24 ++++++++------- src/components/TableContainer.js | 26 ++++++++++------ src/components/TableHeadingCellContainer.js | 14 ++++----- src/components/TableHeadingContainer.js | 10 +++---- 16 files changed, 171 insertions(+), 111 deletions(-) diff --git a/src/components/CellContainer.js b/src/components/CellContainer.js index eecf37bd..b796cfa5 100644 --- a/src/components/CellContainer.js +++ b/src/components/CellContainer.js @@ -5,13 +5,13 @@ import getContext from 'recompose/getContext'; import mapProps from 'recompose/mapProps'; import compose from 'recompose/compose'; -import { - customComponentSelector, - cellValueSelector, - cellPropertiesSelector, - classNamesForComponentSelector, - stylesForComponentSelector -} from '../selectors/dataSelectors'; +//import { +// customComponentSelector, +// cellValueSelector, +// cellPropertiesSelector, +// classNamesForComponentSelector, +// stylesForComponentSelector +//} from '../selectors/dataSelectors'; import { valueOrResult } from '../utils/valueUtils'; function hasWidthOrStyles(cellProperties) { @@ -41,11 +41,11 @@ const ComposedCellContainer = OriginalComponent => compose( }), connect((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'), }; }), mapProps(props => { diff --git a/src/components/FilterContainer.js b/src/components/FilterContainer.js index 7d46bc05..e46422d4 100644 --- a/src/components/FilterContainer.js +++ b/src/components/FilterContainer.js @@ -1,13 +1,23 @@ import React from 'react'; import PropTypes from 'prop-types'; +import compose from 'recompose/compose'; +import getContext from 'recompose/getContext'; import { connect } from '../utils/griddleConnect'; -import { classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; +//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 => ); +const EnhancedFilter = OriginalComponent => compose( + getContext({ + selectors: PropTypes.object + }), + connect( + (state, props) => ({ + className: props.selectors.classNamesForComponentSelector(state, 'Filter'), + style: props.selectors.stylesForComponentSelector(state, 'Filter'), + }), + { setFilter } + ) +)(props => ); export default EnhancedFilter; diff --git a/src/components/LayoutContainer.js b/src/components/LayoutContainer.js index e44570b6..4b09b208 100644 --- a/src/components/LayoutContainer.js +++ b/src/components/LayoutContainer.js @@ -5,16 +5,17 @@ import getContext from 'recompose/getContext'; import mapProps from 'recompose/mapProps'; import compose from 'recompose/compose'; -import { classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; +//import { classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; 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/NextButtonContainer.js b/src/components/NextButtonContainer.js index 278d9eb6..d85d48cc 100644 --- a/src/components/NextButtonContainer.js +++ b/src/components/NextButtonContainer.js @@ -1,13 +1,21 @@ import React from 'react'; +import PropTypes from 'prop-types'; +import compose from 'recompose/compose'; +import getContext from 'recompose/getContext'; import { connect } from '../utils/griddleConnect'; -import { textSelector, hasNextSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; +//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) => ); +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/NoResultsContainer.js b/src/components/NoResultsContainer.js index 2496b0e5..b560516f 100644 --- a/src/components/NoResultsContainer.js +++ b/src/components/NoResultsContainer.js @@ -5,16 +5,17 @@ import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; -import { classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; +//import { classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; 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/PageDropdownContainer.js b/src/components/PageDropdownContainer.js index 88b2bd8c..b1f78284 100644 --- a/src/components/PageDropdownContainer.js +++ b/src/components/PageDropdownContainer.js @@ -4,18 +4,21 @@ 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 { currentPageSelector, maxPageSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; 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/PaginationContainer.js b/src/components/PaginationContainer.js index c82b570f..22e05c7c 100644 --- a/src/components/PaginationContainer.js +++ b/src/components/PaginationContainer.js @@ -5,16 +5,17 @@ import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; -import { classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; +//import { classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; 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/PreviousButtonContainer.js b/src/components/PreviousButtonContainer.js index 628e227c..e959783d 100644 --- a/src/components/PreviousButtonContainer.js +++ b/src/components/PreviousButtonContainer.js @@ -1,12 +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'; -import { textSelector, hasPreviousSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; +//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) => ); +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/RowContainer.js b/src/components/RowContainer.js index 5006a78f..d68b5a39 100644 --- a/src/components/RowContainer.js +++ b/src/components/RowContainer.js @@ -5,26 +5,29 @@ 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 { +// columnIdsSelector, +// rowDataSelector, +// rowPropertiesSelector, +// classNamesForComponentSelector, +// stylesForComponentSelector, +//} from '../selectors/dataSelectors'; 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 +38,7 @@ const ComposedRowContainer = OriginalComponent => compose( }), )(props => ( )); diff --git a/src/components/SettingsContainer.js b/src/components/SettingsContainer.js index c88801df..2eedc563 100644 --- a/src/components/SettingsContainer.js +++ b/src/components/SettingsContainer.js @@ -5,7 +5,7 @@ import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; -import { classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; +//import { classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; function getSettingsComponentsArrayFromObject(settingsObject, settingsComponents) { //TODO: determine if we need to make this faster @@ -20,12 +20,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/SettingsToggleContainer.js b/src/components/SettingsToggleContainer.js index 16037ed3..8a81ea7e 100644 --- a/src/components/SettingsToggleContainer.js +++ b/src/components/SettingsToggleContainer.js @@ -1,14 +1,19 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { connect } from '../utils/griddleConnect'; import compose from 'recompose/compose'; +import getContext from 'recompose/getContext'; import { textSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; import { toggleSettings as toggleSettingsAction } from '../actions'; const enhancedSettingsToggle = OriginalComponent => compose( + getContext({ + selectors: PropTypes.object + }), connect((state, props) => ({ - text: textSelector(state, { key: 'settingsToggle' }), - className: classNamesForComponentSelector(state, 'SettingsToggle'), - style: stylesForComponentSelector(state, 'SettingsToggle'), + text: props.selectors.textSelector(state, { key: 'settingsToggle' }), + className: props.selectors.classNamesForComponentSelector(state, 'SettingsToggle'), + style: props.selectors.stylesForComponentSelector(state, 'SettingsToggle'), }), { toggleSettings: toggleSettingsAction diff --git a/src/components/SettingsWrapperContainer.js b/src/components/SettingsWrapperContainer.js index 8475ad60..57ee0026 100644 --- a/src/components/SettingsWrapperContainer.js +++ b/src/components/SettingsWrapperContainer.js @@ -10,17 +10,24 @@ import { isSettingsEnabledSelector, isSettingsVisibleSelector, classNamesForComp const EnhancedSettingsWrapper = OriginalComponent => compose( getContext({ components: PropTypes.object, + selectors: 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'), - })) + 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 => ( )); diff --git a/src/components/TableBodyContainer.js b/src/components/TableBodyContainer.js index 590aae9b..62e8ea86 100644 --- a/src/components/TableBodyContainer.js +++ b/src/components/TableBodyContainer.js @@ -5,20 +5,22 @@ import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; -import { visibleRowIdsSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; +//import { visibleRowIdsSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; 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 +28,10 @@ const ComposedTableBodyContainer = OriginalComponent => compose( }), )(({Row, visibleRowIds, style, className}) => ( )); diff --git a/src/components/TableContainer.js b/src/components/TableContainer.js index 7c754851..534e698e 100644 --- a/src/components/TableContainer.js +++ b/src/components/TableContainer.js @@ -5,12 +5,10 @@ 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 + 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 mapProps(props => ({ @@ -21,12 +19,22 @@ const ComposedContainerComponent = OriginalComponent => compose( })), connect( (state, props) => ({ - dataLoading: dataLoadingSelector(state), - visibleRows: visibleRowCountSelector(state), - className: classNamesForComponentSelector(state, 'Table'), - style: stylesForComponentSelector(state, 'Table'), + 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, ...otherProps } = props; + return { + TableHeading: components.TableHeading, + TableBody: components.TableBody, + NoResults: components.NoResults, + ...otherProps + } + }) )(props => ); export default ComposedContainerComponent; diff --git a/src/components/TableHeadingCellContainer.js b/src/components/TableHeadingCellContainer.js index 02121426..9d5d866f 100644 --- a/src/components/TableHeadingCellContainer.js +++ b/src/components/TableHeadingCellContainer.js @@ -5,7 +5,7 @@ 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 { sortPropertyByIdSelector, iconsForComponentSelector, classNamesForComponentSelector, stylesForComponentSelector, customHeadingComponentSelector, cellPropertiesSelector } from '../selectors/dataSelectors'; import { getSortIconProps } from '../utils/sortUtils'; import { valueOrResult } from '../utils/valueUtils'; @@ -22,12 +22,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/TableHeadingContainer.js b/src/components/TableHeadingContainer.js index a7bf1377..4f204cef 100644 --- a/src/components/TableHeadingContainer.js +++ b/src/components/TableHeadingContainer.js @@ -5,7 +5,7 @@ import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; -import { columnTitlesSelector, columnIdsSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; +//import { columnTitlesSelector, columnIdsSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; const ComposedContainerComponent = OriginalComponent => compose( getContext({ @@ -13,10 +13,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; From 8f9c74cda42766ad2ca3b787124174a08083246e Mon Sep 17 00:00:00 2001 From: "Short, James" Date: Wed, 11 Oct 2017 20:33:42 -0600 Subject: [PATCH 09/17] 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 ea0e50c267e5074a3ef6009d78a4c038f022f27b Mon Sep 17 00:00:00 2001 From: "Short, James" Date: Wed, 11 Oct 2017 20:47:45 -0600 Subject: [PATCH 10/17] Small changes to TableContainer --- src/components/TableContainer.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/components/TableContainer.js b/src/components/TableContainer.js index 534e698e..54590737 100644 --- a/src/components/TableContainer.js +++ b/src/components/TableContainer.js @@ -11,12 +11,6 @@ const ComposedContainerComponent = OriginalComponent => compose( selectors: 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: props.selectors.dataLoadingSelector(state), @@ -27,12 +21,16 @@ const ComposedContainerComponent = OriginalComponent => compose( ), //TODO: Should we use withHandlers here instead? I realize that's not 100% the intent of that method mapProps(props => { - const { components, ...otherProps } = props; + const { components, dataLoading, visibleRows, className, style } = props; return { TableHeading: components.TableHeading, TableBody: components.TableBody, + Loading: components.Loading, NoResults: components.NoResults, - ...otherProps + dataLoading, + visibleRows, + className, + style } }) )(props => ); From 6da06fab73d3345f9b7d615fac6722d6ff8d3617 Mon Sep 17 00:00:00 2001 From: "Short, James" Date: Wed, 11 Oct 2017 20:51:14 -0600 Subject: [PATCH 11/17] 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 ( From 88052253db882fbd887c68610c4c414ab7f93977 Mon Sep 17 00:00:00 2001 From: "Short, James" Date: Wed, 11 Oct 2017 23:42:24 -0600 Subject: [PATCH 12/17] Add actions to the context. Use the simple later plugins override method for now. --- src/index.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 2ec017d4..fbecc482 100644 --- a/src/index.js +++ b/src/index.js @@ -62,7 +62,8 @@ class Griddle extends Component { events: PropTypes.object, selectors: PropTypes.object, storeKey: PropTypes.string, - storeListener: PropTypes.object + storeListener: PropTypes.object, + actions: PropTypes.object, } constructor(props) { @@ -100,6 +101,8 @@ class Griddle extends Component { this.selectors = plugins.reduce((combined, plugin) => ({ ...combined, ...plugin.selectors }), {...selectors}); + this.actions = plugins.reduce((combined, plugin) => ({ ...combined, ...plugin.actions }), {...actions}); + const mergedStyleConfig = _.merge({}, defaultStyleConfig, ...plugins.map(p => p.styleConfig), styleConfig); const pageProperties = Object.assign({}, { @@ -172,7 +175,8 @@ class Griddle extends Component { events: this.events, selectors: this.selectors, storeKey: this.getStoreKey(), - storeListener: this.storeListener + storeListener: this.storeListener, + actions: this.actions, }; } From 983375d6b6efe4689e8573879e4c8b39cb621caf Mon Sep 17 00:00:00 2001 From: "Short, James" Date: Thu, 12 Oct 2017 00:22:36 -0600 Subject: [PATCH 13/17] Removed hard references to actions in the core code, deleted commented imports of selectors and moved import statements of any Griddle code to be separated from module imports --- src/components/CellContainer.js | 9 +------- src/components/FilterContainer.js | 13 ++++++----- src/components/LayoutContainer.js | 3 +-- src/components/LoadingContainer.js | 10 ++++----- src/components/NextButtonContainer.js | 3 +-- src/components/NextButtonEnhancer.js | 1 + src/components/NoResultsContainer.js | 3 +-- src/components/PageDropdownContainer.js | 4 ++-- src/components/PaginationContainer.js | 3 +-- src/components/PreviousButtonContainer.js | 2 +- src/components/PreviousButtonEnhancer.js | 1 + src/components/RowContainer.js | 9 +------- src/components/SettingsContainer.js | 3 +-- src/components/SettingsToggleContainer.js | 25 +++++++++++---------- src/components/SettingsWrapperContainer.js | 3 +-- src/components/TableBodyContainer.js | 3 +-- src/components/TableContainer.js | 3 ++- src/components/TableHeadingCellContainer.js | 3 +-- src/components/TableHeadingCellEnhancer.js | 1 + src/components/TableHeadingContainer.js | 3 +-- 20 files changed, 44 insertions(+), 61 deletions(-) diff --git a/src/components/CellContainer.js b/src/components/CellContainer.js index b796cfa5..53b94f9d 100644 --- a/src/components/CellContainer.js +++ b/src/components/CellContainer.js @@ -1,17 +1,10 @@ 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, -// cellPropertiesSelector, -// classNamesForComponentSelector, -// stylesForComponentSelector -//} from '../selectors/dataSelectors'; +import { connect } from '../utils/griddleConnect'; import { valueOrResult } from '../utils/valueUtils'; function hasWidthOrStyles(cellProperties) { diff --git a/src/components/FilterContainer.js b/src/components/FilterContainer.js index e46422d4..658e8a0e 100644 --- a/src/components/FilterContainer.js +++ b/src/components/FilterContainer.js @@ -2,21 +2,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'; -//import { classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; -import { setFilter } from '../actions'; +import { connect } from '../utils/griddleConnect'; const EnhancedFilter = OriginalComponent => compose( getContext({ - selectors: PropTypes.object + selectors: PropTypes.object, + actions: PropTypes.object, }), connect( (state, props) => ({ className: props.selectors.classNamesForComponentSelector(state, 'Filter'), style: props.selectors.stylesForComponentSelector(state, 'Filter'), - }), - { setFilter } + }), + (dispatch, props) => ({ + setFilter: (filter) => dispatch(props.actions.setFilter(filter)), + }) ) )(props => ); diff --git a/src/components/LayoutContainer.js b/src/components/LayoutContainer.js index 4b09b208..c7435bd1 100644 --- a/src/components/LayoutContainer.js +++ b/src/components/LayoutContainer.js @@ -1,11 +1,10 @@ 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({ diff --git a/src/components/LoadingContainer.js b/src/components/LoadingContainer.js index 3c17b6d0..4714046e 100644 --- a/src/components/LoadingContainer.js +++ b/src/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/NextButtonContainer.js b/src/components/NextButtonContainer.js index d85d48cc..43dabca3 100644 --- a/src/components/NextButtonContainer.js +++ b/src/components/NextButtonContainer.js @@ -2,9 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import compose from 'recompose/compose'; import getContext from 'recompose/getContext'; -import { connect } from '../utils/griddleConnect'; -//import { textSelector, hasNextSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; +import { connect } from '../utils/griddleConnect'; const enhance = OriginalComponent => compose( getContext({ diff --git a/src/components/NextButtonEnhancer.js b/src/components/NextButtonEnhancer.js index 46ae444f..c6bd2c1c 100644 --- a/src/components/NextButtonEnhancer.js +++ b/src/components/NextButtonEnhancer.js @@ -3,6 +3,7 @@ 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'; const enhance = OriginalComponent => compose( diff --git a/src/components/NoResultsContainer.js b/src/components/NoResultsContainer.js index b560516f..77bfa8e4 100644 --- a/src/components/NoResultsContainer.js +++ b/src/components/NoResultsContainer.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'; const NoResultsContainer = OriginalComponent => compose( getContext({ diff --git a/src/components/PageDropdownContainer.js b/src/components/PageDropdownContainer.js index b1f78284..125a3127 100644 --- a/src/components/PageDropdownContainer.js +++ b/src/components/PageDropdownContainer.js @@ -1,10 +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 { currentPageSelector, maxPageSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; + +import { connect } from '../utils/griddleConnect'; const enhance = OriginalComponent => compose( getContext({ diff --git a/src/components/PaginationContainer.js b/src/components/PaginationContainer.js index 22e05c7c..8e120213 100644 --- a/src/components/PaginationContainer.js +++ b/src/components/PaginationContainer.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'; const EnhancedPaginationContainer = OriginalComponent => compose( getContext({ diff --git a/src/components/PreviousButtonContainer.js b/src/components/PreviousButtonContainer.js index e959783d..2ff8083a 100644 --- a/src/components/PreviousButtonContainer.js +++ b/src/components/PreviousButtonContainer.js @@ -2,8 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import compose from 'recompose/compose'; import getContext from 'recompose/getContext'; + import { connect } from '../utils/griddleConnect'; -//import { textSelector, hasPreviousSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; const enhance = OriginalComponent => compose( getContext({ diff --git a/src/components/PreviousButtonEnhancer.js b/src/components/PreviousButtonEnhancer.js index 0de9ae02..ac4f5612 100644 --- a/src/components/PreviousButtonEnhancer.js +++ b/src/components/PreviousButtonEnhancer.js @@ -3,6 +3,7 @@ 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'; const enhance = OriginalComponent => compose( diff --git a/src/components/RowContainer.js b/src/components/RowContainer.js index d68b5a39..69bb4ea9 100644 --- a/src/components/RowContainer.js +++ b/src/components/RowContainer.js @@ -1,17 +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 { -// columnIdsSelector, -// rowDataSelector, -// rowPropertiesSelector, -// classNamesForComponentSelector, -// stylesForComponentSelector, -//} from '../selectors/dataSelectors'; +import { connect } from '../utils/griddleConnect'; import { valueOrResult } from '../utils/valueUtils'; const ComposedRowContainer = OriginalComponent => compose( diff --git a/src/components/SettingsContainer.js b/src/components/SettingsContainer.js index 2eedc563..1e02f0cd 100644 --- a/src/components/SettingsContainer.js +++ b/src/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 diff --git a/src/components/SettingsToggleContainer.js b/src/components/SettingsToggleContainer.js index 8a81ea7e..94719c8f 100644 --- a/src/components/SettingsToggleContainer.js +++ b/src/components/SettingsToggleContainer.js @@ -1,23 +1,24 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; import compose from 'recompose/compose'; import getContext from 'recompose/getContext'; -import { textSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; -import { toggleSettings as toggleSettingsAction } from '../actions'; + +import { connect } from '../utils/griddleConnect'; const enhancedSettingsToggle = OriginalComponent => compose( getContext({ - selectors: PropTypes.object - }), - connect((state, props) => ({ - text: props.selectors.textSelector(state, { key: 'settingsToggle' }), - className: props.selectors.classNamesForComponentSelector(state, 'SettingsToggle'), - style: props.selectors.stylesForComponentSelector(state, 'SettingsToggle'), + selectors: PropTypes.object, + actions: PropTypes.object, }), - { - toggleSettings: toggleSettingsAction - } + 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 => compose( getContext({ diff --git a/src/components/TableBodyContainer.js b/src/components/TableBodyContainer.js index 62e8ea86..6f5b014f 100644 --- a/src/components/TableBodyContainer.js +++ b/src/components/TableBodyContainer.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 { visibleRowIdsSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; +import { connect } from '../utils/griddleConnect'; const ComposedTableBodyContainer = OriginalComponent => compose( getContext({ diff --git a/src/components/TableContainer.js b/src/components/TableContainer.js index 54590737..ec43d9b1 100644 --- a/src/components/TableContainer.js +++ b/src/components/TableContainer.js @@ -1,10 +1,11 @@ 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 { connect } from '../utils/griddleConnect'; + const ComposedContainerComponent = OriginalComponent => compose( getContext({ components: PropTypes.object, diff --git a/src/components/TableHeadingCellContainer.js b/src/components/TableHeadingCellContainer.js index 9d5d866f..d36b3ca0 100644 --- a/src/components/TableHeadingCellContainer.js +++ b/src/components/TableHeadingCellContainer.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 { sortPropertyByIdSelector, iconsForComponentSelector, classNamesForComponentSelector, stylesForComponentSelector, customHeadingComponentSelector, cellPropertiesSelector } from '../selectors/dataSelectors'; +import { connect } from '../utils/griddleConnect'; import { getSortIconProps } from '../utils/sortUtils'; import { valueOrResult } from '../utils/valueUtils'; diff --git a/src/components/TableHeadingCellEnhancer.js b/src/components/TableHeadingCellEnhancer.js index 3ee6c757..5349caba 100644 --- a/src/components/TableHeadingCellEnhancer.js +++ b/src/components/TableHeadingCellEnhancer.js @@ -3,6 +3,7 @@ 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'; const EnhancedHeadingCell = OriginalComponent => compose( diff --git a/src/components/TableHeadingContainer.js b/src/components/TableHeadingContainer.js index 4f204cef..1271955f 100644 --- a/src/components/TableHeadingContainer.js +++ b/src/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({ From f1fa4ddec1390d3d49918fa32051625f6af3e829 Mon Sep 17 00:00:00 2001 From: "Short, James" Date: Fri, 13 Oct 2017 17:26:25 -0600 Subject: [PATCH 14/17] First pass at moving 'core' code into a plugin. Stories seem to be working fine with this code. Going to pull a lot of the 'core' specific stuff into that plugin so that the initialization phase has no dependencies on its specific structure. --- src/index.js | 33 +++++++++++-------- src/module.js | 8 ++--- src/{ => plugins/core}/actions/index.js | 0 src/{ => plugins/core}/components/Cell.js | 0 .../core}/components/CellContainer.js | 4 +-- .../core}/components/ColumnDefinition.js | 0 src/{ => plugins/core}/components/Filter.js | 0 .../core}/components/FilterContainer.js | 2 +- .../core}/components/FilterEnhancer.js | 2 +- src/{ => plugins/core}/components/Layout.js | 0 .../core}/components/LayoutContainer.js | 2 +- src/{ => plugins/core}/components/Loading.js | 0 .../core}/components/LoadingContainer.js | 2 +- .../core}/components/NextButton.js | 0 .../core}/components/NextButtonContainer.js | 2 +- .../core}/components/NextButtonEnhancer.js | 2 +- .../core}/components/NoResults.js | 0 .../core}/components/NoResultsContainer.js | 2 +- .../core}/components/PageDropdown.js | 0 .../core}/components/PageDropdownContainer.js | 2 +- .../core}/components/Pagination.js | 0 .../core}/components/PaginationContainer.js | 2 +- .../core}/components/PreviousButton.js | 0 .../components/PreviousButtonContainer.js | 2 +- .../components/PreviousButtonEnhancer.js | 2 +- src/{ => plugins/core}/components/Row.js | 0 .../core}/components/RowContainer.js | 4 +-- .../core}/components/RowDefinition.js | 0 src/{ => plugins/core}/components/Settings.js | 0 .../core}/components/SettingsContainer.js | 2 +- .../core}/components/SettingsToggle.js | 0 .../components/SettingsToggleContainer.js | 2 +- .../core}/components/SettingsWrapper.js | 0 .../components/SettingsWrapperContainer.js | 2 +- src/{ => plugins/core}/components/Table.js | 0 .../core}/components/TableBody.js | 0 .../core}/components/TableBodyContainer.js | 2 +- .../core}/components/TableContainer.js | 2 +- .../core}/components/TableHeading.js | 0 .../core}/components/TableHeadingCell.js | 0 .../components/TableHeadingCellContainer.js | 6 ++-- .../components/TableHeadingCellEnhancer.js | 2 +- .../core}/components/TableHeadingContainer.js | 2 +- src/{ => plugins/core}/components/Test.js | 0 .../core}/components/__tests__/CellTest.js | 0 .../core}/components/__tests__/FilterTest.js | 0 .../components/__tests__/NextButtonTest.js | 0 .../components/__tests__/PageDropdownTest.js | 0 .../components/__tests__/PaginationTest.js | 0 .../__tests__/PreviousButtonTest.js | 0 .../core}/components/__tests__/RowTest.js | 0 .../components/__tests__/SettingsTest.js | 0 .../__tests__/SettingsToggleTest.js | 0 .../__tests__/SettingsWrapperTest.js | 0 .../components/__tests__/TableBodyTest.js | 0 .../__tests__/TableHeadingCellTest.js | 0 .../components/__tests__/TableHeadingTest.js | 0 .../core}/components/__tests__/TableTest.js | 0 src/{ => plugins/core}/components/index.js | 2 +- src/{ => plugins/core}/constants/index.js | 0 src/plugins/core/index.js | 11 +++++++ .../reducers/__tests__/dataReducerTest.js | 0 .../core}/reducers/dataReducer.js | 2 +- .../selectors/__tests__/dataSelectorsTest.js | 0 .../core}/selectors/dataSelectors.js | 0 .../local/components/NextButtonContainer.js | 2 +- .../local/components/PageDropdownContainer.js | 2 +- .../components/PreviousButtonContainer.js | 2 +- .../components/TableHeadingCellContainer.js | 4 +-- src/plugins/local/index.js | 2 +- src/plugins/local/reducers/index.js | 2 +- src/plugins/local/selectors/localSelectors.js | 2 +- src/settingsComponentObjects/ColumnChooser.js | 4 +-- .../PageSizeSettings.js | 4 +-- 74 files changed, 74 insertions(+), 56 deletions(-) rename src/{ => plugins/core}/actions/index.js (100%) rename src/{ => plugins/core}/components/Cell.js (100%) rename src/{ => plugins/core}/components/CellContainer.js (94%) rename src/{ => plugins/core}/components/ColumnDefinition.js (100%) rename src/{ => plugins/core}/components/Filter.js (100%) rename src/{ => plugins/core}/components/FilterContainer.js (89%) rename src/{ => plugins/core}/components/FilterEnhancer.js (88%) rename src/{ => plugins/core}/components/Layout.js (100%) rename src/{ => plugins/core}/components/LayoutContainer.js (94%) rename src/{ => plugins/core}/components/Loading.js (100%) rename src/{ => plugins/core}/components/LoadingContainer.js (92%) rename src/{ => plugins/core}/components/NextButton.js (100%) rename src/{ => plugins/core}/components/NextButtonContainer.js (90%) rename src/{ => plugins/core}/components/NextButtonEnhancer.js (87%) rename src/{ => plugins/core}/components/NoResults.js (100%) rename src/{ => plugins/core}/components/NoResultsContainer.js (93%) rename src/{ => plugins/core}/components/PageDropdown.js (100%) rename src/{ => plugins/core}/components/PageDropdownContainer.js (93%) rename src/{ => plugins/core}/components/Pagination.js (100%) rename src/{ => plugins/core}/components/PaginationContainer.js (94%) rename src/{ => plugins/core}/components/PreviousButton.js (100%) rename src/{ => plugins/core}/components/PreviousButtonContainer.js (90%) rename src/{ => plugins/core}/components/PreviousButtonEnhancer.js (88%) rename src/{ => plugins/core}/components/Row.js (100%) rename src/{ => plugins/core}/components/RowContainer.js (90%) rename src/{ => plugins/core}/components/RowDefinition.js (100%) rename src/{ => plugins/core}/components/Settings.js (100%) rename src/{ => plugins/core}/components/SettingsContainer.js (96%) rename src/{ => plugins/core}/components/SettingsToggle.js (100%) rename src/{ => plugins/core}/components/SettingsToggleContainer.js (92%) rename src/{ => plugins/core}/components/SettingsWrapper.js (100%) rename src/{ => plugins/core}/components/SettingsWrapperContainer.js (94%) rename src/{ => plugins/core}/components/Table.js (100%) rename src/{ => plugins/core}/components/TableBody.js (100%) rename src/{ => plugins/core}/components/TableBodyContainer.js (94%) rename src/{ => plugins/core}/components/TableContainer.js (95%) rename src/{ => plugins/core}/components/TableHeading.js (100%) rename src/{ => plugins/core}/components/TableHeadingCell.js (100%) rename src/{ => plugins/core}/components/TableHeadingCellContainer.js (91%) rename src/{ => plugins/core}/components/TableHeadingCellEnhancer.js (89%) rename src/{ => plugins/core}/components/TableHeadingContainer.js (94%) rename src/{ => plugins/core}/components/Test.js (100%) rename src/{ => plugins/core}/components/__tests__/CellTest.js (100%) rename src/{ => plugins/core}/components/__tests__/FilterTest.js (100%) rename src/{ => plugins/core}/components/__tests__/NextButtonTest.js (100%) rename src/{ => plugins/core}/components/__tests__/PageDropdownTest.js (100%) rename src/{ => plugins/core}/components/__tests__/PaginationTest.js (100%) rename src/{ => plugins/core}/components/__tests__/PreviousButtonTest.js (100%) rename src/{ => plugins/core}/components/__tests__/RowTest.js (100%) rename src/{ => plugins/core}/components/__tests__/SettingsTest.js (100%) rename src/{ => plugins/core}/components/__tests__/SettingsToggleTest.js (100%) rename src/{ => plugins/core}/components/__tests__/SettingsWrapperTest.js (100%) rename src/{ => plugins/core}/components/__tests__/TableBodyTest.js (100%) rename src/{ => plugins/core}/components/__tests__/TableHeadingCellTest.js (100%) rename src/{ => plugins/core}/components/__tests__/TableHeadingTest.js (100%) rename src/{ => plugins/core}/components/__tests__/TableTest.js (100%) rename src/{ => plugins/core}/components/index.js (96%) rename src/{ => plugins/core}/constants/index.js (100%) create mode 100644 src/plugins/core/index.js rename src/{ => plugins/core}/reducers/__tests__/dataReducerTest.js (100%) rename src/{ => plugins/core}/reducers/dataReducer.js (99%) rename src/{ => plugins/core}/selectors/__tests__/dataSelectorsTest.js (100%) rename src/{ => plugins/core}/selectors/dataSelectors.js (100%) diff --git a/src/index.js b/src/index.js index d16989ac..94300bcb 100644 --- a/src/index.js +++ b/src/index.js @@ -5,23 +5,25 @@ 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 { setSortProperties } from './utils/sortUtils'; import { StoreListener } from './utils/listenerUtils'; -import * as actions from './actions'; +//import * as actions from './actions'; -const defaultEvents = { - ...actions, - onFilter: actions.setFilter, - setSortProperties -}; +import CorePlugin from './plugins/core'; + +//const defaultEvents = { +// ...actions, +// onFilter: actions.setFilter, +// setSortProperties +//}; const defaultStyleConfig = { @@ -69,6 +71,7 @@ class Griddle extends Component { super(props); const { + baselinePlugin=CorePlugin, plugins=[], data, children:rowPropertiesComponent, @@ -85,20 +88,24 @@ class Griddle extends Component { ...userInitialState } = props; + this.baselinePlugin = baselinePlugin; + const rowProperties = getRowProperties(rowPropertiesComponent); const columnProperties = getColumnProperties(rowPropertiesComponent); //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([baselinePlugin.reducer, ...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([baselinePlugin.components, ...plugins.map(p => p.components), userComponents]); this.settingsComponentObjects = Object.assign({}, settingsComponentObjects, ...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 = plugins.reduce((combined, plugin) => ({ ...combined, ...plugin.selectors }), {...baselinePlugin.selectors}); const mergedStyleConfig = _.merge({}, defaultStyleConfig, ...plugins.map(p => p.styleConfig), styleConfig); @@ -158,7 +165,7 @@ class Griddle extends Component { componentWillReceiveProps(nextProps) { const { data, pageProperties, sortProperties } = nextProps; - this.store.dispatch(actions.updateState({ data, pageProperties, sortProperties })); + this.store.dispatch(this.baselinePlugin.actions.updateState({ data, pageProperties, sortProperties })); } getStoreKey = () => { diff --git a/src/module.js b/src/module.js index a5d36a0c..416088ab 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'; 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 94% rename from src/components/CellContainer.js rename to src/plugins/core/components/CellContainer.js index eecf37bd..094c7990 100644 --- a/src/components/CellContainer.js +++ b/src/plugins/core/components/CellContainer.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; +import { connect } from '../../../utils/griddleConnect'; import getContext from 'recompose/getContext'; import mapProps from 'recompose/mapProps'; import compose from 'recompose/compose'; @@ -12,7 +12,7 @@ import { classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; -import { valueOrResult } from '../utils/valueUtils'; +import { valueOrResult } from '../../../utils/valueUtils'; function hasWidthOrStyles(cellProperties) { return cellProperties.hasOwnProperty('width') || cellProperties.hasOwnProperty('styles'); 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/components/FilterContainer.js b/src/plugins/core/components/FilterContainer.js similarity index 89% rename from src/components/FilterContainer.js rename to src/plugins/core/components/FilterContainer.js index 7d46bc05..65440f1b 100644 --- a/src/components/FilterContainer.js +++ b/src/plugins/core/components/FilterContainer.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; +import { connect } from '../../../utils/griddleConnect'; import { classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; import { setFilter } from '../actions'; 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 94% rename from src/components/LayoutContainer.js rename to src/plugins/core/components/LayoutContainer.js index e44570b6..b83ff431 100644 --- a/src/components/LayoutContainer.js +++ b/src/plugins/core/components/LayoutContainer.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; +import { connect } from '../../../utils/griddleConnect'; import getContext from 'recompose/getContext'; import mapProps from 'recompose/mapProps'; import compose from 'recompose/compose'; 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 92% rename from src/components/LoadingContainer.js rename to src/plugins/core/components/LoadingContainer.js index 3c17b6d0..2ee8f4c8 100644 --- a/src/components/LoadingContainer.js +++ b/src/plugins/core/components/LoadingContainer.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; +import { connect } from '../../../utils/griddleConnect'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; 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/components/NextButtonContainer.js b/src/plugins/core/components/NextButtonContainer.js similarity index 90% rename from src/components/NextButtonContainer.js rename to src/plugins/core/components/NextButtonContainer.js index 278d9eb6..1bd3736a 100644 --- a/src/components/NextButtonContainer.js +++ b/src/plugins/core/components/NextButtonContainer.js @@ -1,5 +1,5 @@ import React from 'react'; -import { connect } from '../utils/griddleConnect'; +import { connect } from '../../../utils/griddleConnect'; import { textSelector, hasNextSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; 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..cf28b967 100644 --- a/src/components/NextButtonEnhancer.js +++ b/src/plugins/core/components/NextButtonEnhancer.js @@ -3,7 +3,7 @@ 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 93% rename from src/components/NoResultsContainer.js rename to src/plugins/core/components/NoResultsContainer.js index 2496b0e5..3967ae27 100644 --- a/src/components/NoResultsContainer.js +++ b/src/plugins/core/components/NoResultsContainer.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; +import { connect } from '../../../utils/griddleConnect'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; 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 93% rename from src/components/PageDropdownContainer.js rename to src/plugins/core/components/PageDropdownContainer.js index 88b2bd8c..6fd793b5 100644 --- a/src/components/PageDropdownContainer.js +++ b/src/plugins/core/components/PageDropdownContainer.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; +import { connect } from '../../../utils/griddleConnect'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; 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 94% rename from src/components/PaginationContainer.js rename to src/plugins/core/components/PaginationContainer.js index c82b570f..c8542571 100644 --- a/src/components/PaginationContainer.js +++ b/src/plugins/core/components/PaginationContainer.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; +import { connect } from '../../../utils/griddleConnect'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; 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/components/PreviousButtonContainer.js b/src/plugins/core/components/PreviousButtonContainer.js similarity index 90% rename from src/components/PreviousButtonContainer.js rename to src/plugins/core/components/PreviousButtonContainer.js index 628e227c..0693a89d 100644 --- a/src/components/PreviousButtonContainer.js +++ b/src/plugins/core/components/PreviousButtonContainer.js @@ -1,5 +1,5 @@ import React from 'react'; -import { connect } from '../utils/griddleConnect'; +import { connect } from '../../../utils/griddleConnect'; import { textSelector, hasPreviousSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; const enhance = OriginalComponent => connect((state, props) => ({ 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..e435b43f 100644 --- a/src/components/PreviousButtonEnhancer.js +++ b/src/plugins/core/components/PreviousButtonEnhancer.js @@ -3,7 +3,7 @@ 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 90% rename from src/components/RowContainer.js rename to src/plugins/core/components/RowContainer.js index 5006a78f..b4f54fee 100644 --- a/src/components/RowContainer.js +++ b/src/plugins/core/components/RowContainer.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; +import { connect } from '../../../utils/griddleConnect'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; @@ -12,7 +12,7 @@ import { classNamesForComponentSelector, stylesForComponentSelector, } from '../selectors/dataSelectors'; -import { valueOrResult } from '../utils/valueUtils'; +import { valueOrResult } from '../../../utils/valueUtils'; const ComposedRowContainer = OriginalComponent => compose( getContext({ 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 96% rename from src/components/SettingsContainer.js rename to src/plugins/core/components/SettingsContainer.js index c88801df..e06e6eda 100644 --- a/src/components/SettingsContainer.js +++ b/src/plugins/core/components/SettingsContainer.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; +import { connect } from '../../../utils/griddleConnect'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; 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/components/SettingsToggleContainer.js b/src/plugins/core/components/SettingsToggleContainer.js similarity index 92% rename from src/components/SettingsToggleContainer.js rename to src/plugins/core/components/SettingsToggleContainer.js index 16037ed3..cf35b281 100644 --- a/src/components/SettingsToggleContainer.js +++ b/src/plugins/core/components/SettingsToggleContainer.js @@ -1,5 +1,5 @@ import React from 'react'; -import { connect } from '../utils/griddleConnect'; +import { connect } from '../../../utils/griddleConnect'; import compose from 'recompose/compose'; import { textSelector, classNamesForComponentSelector, stylesForComponentSelector } from '../selectors/dataSelectors'; import { toggleSettings as toggleSettingsAction } from '../actions'; 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/components/SettingsWrapperContainer.js b/src/plugins/core/components/SettingsWrapperContainer.js similarity index 94% rename from src/components/SettingsWrapperContainer.js rename to src/plugins/core/components/SettingsWrapperContainer.js index 8475ad60..d3c8f4e6 100644 --- a/src/components/SettingsWrapperContainer.js +++ b/src/plugins/core/components/SettingsWrapperContainer.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; +import { connect } from '../../../utils/griddleConnect'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; 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 94% rename from src/components/TableBodyContainer.js rename to src/plugins/core/components/TableBodyContainer.js index 590aae9b..ba17cddd 100644 --- a/src/components/TableBodyContainer.js +++ b/src/plugins/core/components/TableBodyContainer.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; +import { connect } from '../../../utils/griddleConnect'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; diff --git a/src/components/TableContainer.js b/src/plugins/core/components/TableContainer.js similarity index 95% rename from src/components/TableContainer.js rename to src/plugins/core/components/TableContainer.js index 7c754851..403edc47 100644 --- a/src/components/TableContainer.js +++ b/src/plugins/core/components/TableContainer.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; +import { connect } from '../../../utils/griddleConnect'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; 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 91% rename from src/components/TableHeadingCellContainer.js rename to src/plugins/core/components/TableHeadingCellContainer.js index 02121426..af5fcbfd 100644 --- a/src/components/TableHeadingCellContainer.js +++ b/src/plugins/core/components/TableHeadingCellContainer.js @@ -1,13 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; +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 { getSortIconProps } from '../../../utils/sortUtils'; +import { valueOrResult } from '../../../utils/valueUtils'; const DefaultTableHeadingCellContent = ({title, icon}) => ( 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..8930f38c 100644 --- a/src/components/TableHeadingCellEnhancer.js +++ b/src/plugins/core/components/TableHeadingCellEnhancer.js @@ -3,7 +3,7 @@ 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 94% rename from src/components/TableHeadingContainer.js rename to src/plugins/core/components/TableHeadingContainer.js index a7bf1377..27989fab 100644 --- a/src/components/TableHeadingContainer.js +++ b/src/plugins/core/components/TableHeadingContainer.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from '../utils/griddleConnect'; +import { connect } from '../../../utils/griddleConnect'; import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import getContext from 'recompose/getContext'; 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..03235496 --- /dev/null +++ b/src/plugins/core/index.js @@ -0,0 +1,11 @@ +import components from './components'; +import * as reducer from './reducers/dataReducer'; +import * as selectors from './selectors/dataSelectors'; +import * as actions from './actions'; + +export default { + components, + reducer, + selectors, + actions +}; 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 99% rename from src/reducers/dataReducer.js rename to src/plugins/core/reducers/dataReducer.js index 2c641732..c6e10871 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]); diff --git a/src/selectors/__tests__/dataSelectorsTest.js b/src/plugins/core/selectors/__tests__/dataSelectorsTest.js similarity index 100% rename from src/selectors/__tests__/dataSelectorsTest.js rename to src/plugins/core/selectors/__tests__/dataSelectorsTest.js diff --git a/src/selectors/dataSelectors.js b/src/plugins/core/selectors/dataSelectors.js similarity index 100% rename from src/selectors/dataSelectors.js rename to src/plugins/core/selectors/dataSelectors.js 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/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/localSelectors.js b/src/plugins/local/selectors/localSelectors.js index be77f0d5..120cbbd2 100644 --- a/src/plugins/local/selectors/localSelectors.js +++ b/src/plugins/local/selectors/localSelectors.js @@ -4,7 +4,7 @@ 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 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( From 1ce5b574a511748c2304ec1897ea6e21f1a15dea Mon Sep 17 00:00:00 2001 From: "Short, James" Date: Mon, 16 Oct 2017 16:58:58 -0600 Subject: [PATCH 15/17] Most of the stuff associated with the 'core' plugin in src/index.js has been moved out. There are still some lingering bits to do with the redux state I would like to fix but they need selector refactors. --- src/index.js | 79 +++++++++++--------------------- src/plugins/core/index.js | 10 ++-- src/plugins/core/initialState.js | 72 +++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 56 deletions(-) create mode 100644 src/plugins/core/initialState.js diff --git a/src/index.js b/src/index.js index 94300bcb..d58a7ef9 100644 --- a/src/index.js +++ b/src/index.js @@ -25,38 +25,6 @@ import CorePlugin from './plugins/core'; // 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: { - } -}; - class Griddle extends Component { static childContextTypes = { components: PropTypes.object.isRequired, @@ -88,27 +56,38 @@ class Griddle extends Component { ...userInitialState } = props; - this.baselinePlugin = baselinePlugin; + switch(typeof baselinePlugin) { + case 'function': + plugins.unshift(baselinePlugin(props)); + break; + case 'object': + plugins.unshift(baselinePlugin); + break; + }; - const rowProperties = getRowProperties(rowPropertiesComponent); - const columnProperties = getColumnProperties(rowPropertiesComponent); + 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([baselinePlugin.reducer, ...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([baselinePlugin.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 }), {...baselinePlugin.selectors}); + this.selectors = plugins.reduce((combined, plugin) => ({ ...combined, ...plugin.selectors }), {}); - 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 @@ -117,21 +96,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, { @@ -165,7 +138,7 @@ class Griddle extends Component { componentWillReceiveProps(nextProps) { const { data, pageProperties, sortProperties } = nextProps; - this.store.dispatch(this.baselinePlugin.actions.updateState({ data, pageProperties, sortProperties })); + this.store.dispatch(this.plugins[0].actions.updateState({ data, pageProperties, sortProperties })); } getStoreKey = () => { diff --git a/src/plugins/core/index.js b/src/plugins/core/index.js index 03235496..fc831987 100644 --- a/src/plugins/core/index.js +++ b/src/plugins/core/index.js @@ -2,10 +2,14 @@ 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'; -export default { +const CorePlugin = (config) => ({ components, reducer, selectors, - actions -}; + 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; From 0d510aab219515a7b87cc23e824b71bcce6fde72 Mon Sep 17 00:00:00 2001 From: "Short, James" Date: Tue, 17 Oct 2017 15:11:35 -0600 Subject: [PATCH 16/17] Updated store listener code to use the most current store state as its 'beginning' oldState instead of undefined, updated the core GRIDDLE_UPDATE_STATE reducer to not attempt a change on data if it is undefined. This may warrant some further looking at. Also updated the story for overridable selectors so that it should be working fine now. --- src/plugins/core/reducers/dataReducer.js | 14 +++++++++----- src/utils/listenerUtils.js | 2 +- stories/index.tsx | 3 ++- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/plugins/core/reducers/dataReducer.js b/src/plugins/core/reducers/dataReducer.js index f50e6c71..cf17554e 100644 --- a/src/plugins/core/reducers/dataReducer.js +++ b/src/plugins/core/reducers/dataReducer.js @@ -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/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/stories/index.tsx b/stories/index.tsx index 9d901a10..96705708 100644 --- a/stories/index.tsx +++ b/stories/index.tsx @@ -1081,7 +1081,8 @@ storiesOf('Plugins', module) }, selectors: { allDataSelector, - recordCountSelector + recordCountSelector, + dataSelector }, composedSelectors: { dataSelector From 28cc4ff936d0360475db13a46a29ca2da711ef8d Mon Sep 17 00:00:00 2001 From: "Short, James" Date: Thu, 19 Oct 2017 12:08:18 -0600 Subject: [PATCH 17/17] Fixed core, local, and position plugin tests to work with new selectors and directory structure. Selectors now have a factory prop that can build copies of this selector. --- src/index.js | 6 +- .../selectors/__tests__/dataSelectorsTest.js | 75 +++-- .../reducers/__tests__/localReducerTests.js | 2 +- .../selectors/__tests__/localSelectorsTest.js | 64 ++-- .../position/selectors/__tests__/indexTest.js | 12 +- src/utils/__tests__/selectorUtilsTest.js | 316 +++++++++++++++--- src/utils/selectorUtils.js | 161 ++++++--- 7 files changed, 488 insertions(+), 148 deletions(-) diff --git a/src/index.js b/src/index.js index 6347a593..cd8724e1 100644 --- a/src/index.js +++ b/src/index.js @@ -84,7 +84,7 @@ class Griddle extends Component { this.events = Object.assign({}, events, ...plugins.map(p => p.events)); - this.selectors = composeSelectors(plugins[0].selectors, plugins); + this.selectors = composeSelectors(plugins); this.actions = plugins.reduce((combined, plugin) => ({ ...combined, ...plugin.actions }), {}); @@ -145,6 +145,10 @@ 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(this.plugins[0].actions.updateState(newState)); } diff --git a/src/plugins/core/selectors/__tests__/dataSelectorsTest.js b/src/plugins/core/selectors/__tests__/dataSelectorsTest.js index 82e07e9e..ec1f3d4c 100644 --- a/src/plugins/core/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/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/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/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/utils/__tests__/selectorUtilsTest.js b/src/utils/__tests__/selectorUtilsTest.js index aba1b0ba..f26c81d6 100644 --- a/src/utils/__tests__/selectorUtilsTest.js +++ b/src/utils/__tests__/selectorUtilsTest.js @@ -68,51 +68,53 @@ test('createSelector with a non string or function argument for one of the first 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 resolvedDependencies = { + someDependency: () => 42 + }; - const selector = createSelector( - "someDependency", - (x) => x - )(resolvedDependencies); + const selector = createSelector( + "someDependency", + (x) => x + )(resolvedDependencies); - assert.is(selector(), 42); -}); + 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 someFunction = () => 10; - const resolvedDependencies = { - someDependency: () => 42 - }; + const resolvedDependencies = { + someDependency: () => 42 + }; - const selector = createSelector( - someFunction, - "someDependency", - (x, y) => x * y - )(resolvedDependencies); + const selector = createSelector( + someFunction, + "someDependency", + (x, y) => x * y + )(resolvedDependencies); - assert.is(selector(), 420); -}); + 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); -}); + const error = assert.throws(() => { + createSelector( + "someDependency", + (x) => x + )({}); + }, Error); + }); test('composeSelectors with 1 simple selector', (assert) => { - const defaultSelectors = { - simpleSelectorA: (state) => state, + const plugin0 = { + selectors: { + simpleSelectorA: (state) => state, + } }; - const flattenedSelectors = composeSelectors(defaultSelectors, []); + const flattenedSelectors = composeSelectors([plugin0]); assert.is(typeof flattenedSelectors, "object"); assert.is(Object.keys(flattenedSelectors).length, 1); @@ -120,17 +122,19 @@ test('composeSelectors with 1 simple selector', (assert) => { }); test('composeSelectors with 2 simple selectors and 1 dependency selector dependent on the 2 simple selectors', (assert) => { - const defaultSelectors = { - simpleSelectorA: () => 10, - simpleSelectorB: () => 2, - dependencySelector1: createSelector( - "simpleSelectorA", - "simpleSelectorB", - (x, y) => x * y - ) - }; + const plugin0 = { + selectors: { + simpleSelectorA: () => 10, + simpleSelectorB: () => 2, + dependencySelector1: createSelector( + 'simpleSelectorA', + 'simpleSelectorB', + (x, y) => x * y + ) + } + } - const flattenedSelectors = composeSelectors(defaultSelectors, []); + const flattenedSelectors = composeSelectors([plugin0]); assert.is(typeof flattenedSelectors, "object"); assert.is(Object.keys(flattenedSelectors).length, 3); @@ -138,4 +142,236 @@ test('composeSelectors with 2 simple selectors and 1 dependency selector depende 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/selectorUtils.js b/src/utils/selectorUtils.js index b4580d46..89e42361 100644 --- a/src/utils/selectorUtils.js +++ b/src/utils/selectorUtils.js @@ -113,83 +113,149 @@ const griddleCreateSelector = (...args) => { // Otherwise, this is either a mixed or fully string dependency // selector. Create a selector generator. - const selectorGenerator = (() => { - let generatedSelector; - return (...args) => { - if (!generatedSelector) { - // Legacy components might call this selector directly with state - // so use global cache of selectors instead - //const dependencySelectors = selectors._dependencies ? selectors : globalSelectors; + //const selectorGeneratorWrapper = (() => { - const resolvedSelectors = args[0]; + return (() => { + + const createSelectorFuncs = new Map(); + const ownSelector = selector; - const createSelectorFuncs = []; + 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.push(functions[index]); + createSelectorFuncs.set(index, functions[index]); break; case DEP: if (resolvedSelectors.hasOwnProperty(dependencies[index])) { - createSelectorFuncs.push(resolvedSelectors[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); + } - // add this selector - createSelectorFuncs.push(selector); + 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 - generatedSelector = createSelector(...createSelectorFuncs); - selectorGenerator.generated = true; - return generatedSelector; + //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 generatedSelector(...args) + 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 - selectorGenerator.dependencies = values(dependencies); - return selectorGenerator; + //selectorGeneratorWrapper.dependencies = values(dependencies); + //return selectorGeneratorWrapper; }; export { griddleCreateSelector as createSelector }; -export const composeSelectors = (defaultSelectors, plugins) => { +export const composeSelectors = (plugins) => { // STEP 1 // ========== // // Add all selectors to the list of combined selectors. - // The actuall selector functions are wrapped in an object which is used + // + // 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(); - const allSelectors = [defaultSelectors].concat(...plugins.map(p => p.selectors)); - allSelectors.forEach((selectors) => { - console.log('Begin selector block'); - forOwn(selectors, (selector, name) => { - if (combinedSelectors.has(name)) { + 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 + }); } - - combinedSelectors.set(name, { - name, - selector, - dependencies: selector.dependencies || [], - rank: 0, - traversed: false - }); }); }); @@ -251,9 +317,9 @@ export const composeSelectors = (defaultSelectors, plugins) => { flattenedDependencies.add(dependency); const childParents = new Set(parents); childParents.add(dependency); - const childsDependencies = getDependencies(combinedSelectors.get(dependency), childParents); + const childsDependencies = getDependencies(combinedSelectors.get(dependency)[0], childParents); childsDependencies.forEach((key) => flattenedDependencies.add(key)) - const childRank = combinedSelectors.get(dependency).rank; + const childRank = combinedSelectors.get(dependency)[0].rank; childRank >= node.rank && (node.rank = childRank + 1); } node.flattenedDependencies = flattenedDependencies; @@ -277,25 +343,38 @@ export const composeSelectors = (defaultSelectors, plugins) => { // STEP 4 // ========== // - // Run getDependencies on each selector in the 'combinedSelectors' list + // 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, selector] = e; - getDependencies(selector, new Set([name])); + 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) { - if (selector.dependencies.length && !selector.selector.generated) { + //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] = selector.selector(flattenedSelectors); + flattenedSelectors[selector.name] = generatedSelector; //const childSelectors = { _dependencies: true }; //for (let childSelector of selector.dependencies) {