diff --git a/js/helpers/__tests__/dataHelperTests.js b/js/helpers/__tests__/dataHelperTests.js index dbf41a0..26ae19b 100644 --- a/js/helpers/__tests__/dataHelperTests.js +++ b/js/helpers/__tests__/dataHelperTests.js @@ -11,7 +11,6 @@ import { getColumnTitles, getColumnProperties, getAllPossibleColumns, - getSortedColumns, getDataColumns } from '../data-helpers'; @@ -55,6 +54,13 @@ describe('data helpers', () => { expect(results.toJSON().map(withoutMetadata)).toEqual([{two: 'two', one: 'one'}, {two: 'four', one: 'three'}]); }); + it('sorts the columns', () => { + const state = getBasicState(); + const sorted = getVisibleDataColumns(state.get('data'), ['two', 'one']); + + expect(Object.keys(sorted.toJSON()[0])).toEqual(['two', 'one', '__metadata']); + }); + //test for the columns that are made up from thin air it('gets null for magic column', () => { const state = getBasicState(); @@ -62,6 +68,13 @@ describe('data helpers', () => { expect(results.toJSON().map(withoutMetadata)).toEqual([{two: 'two', onepointfive: null, one: 'one'}, {two: 'four', onepointfive: null, one: 'three'}]); }); + + it('gets correct order for magic column', () => { + const state = getBasicState(); + const results = getVisibleDataColumns(state.get('data'), ['one', 'onepointfive', 'two']); + + expect(Object.keys(results.toJSON()[0])).toEqual(['one', 'onepointfive', 'two', '__metadata']); + }); }) describe('updateVisibleData', () => { @@ -144,15 +157,6 @@ describe('data helpers', () => { }); }); - describe('getSortedColumns', () => { - it('sorts the columns', () => { - const state = getBasicState(); - const sorted = getSortedColumns(state.get('data'), ['two', 'one']); - - expect(sorted.toJSON()).toEqual([{two: 'two', one: 'one'}, {two: 'four', one: 'three'}]); - }); - }); - describe('getDataColumns', () => { it('gets the columns arranged by order', () => { const state = getBasicState(); diff --git a/js/helpers/__tests__/localHelperTests.js b/js/helpers/__tests__/localHelperTests.js index 3eba7a2..7e80556 100644 --- a/js/helpers/__tests__/localHelperTests.js +++ b/js/helpers/__tests__/localHelperTests.js @@ -2,6 +2,7 @@ import Immutable from 'immutable'; import { getVisibleData, + getOriginalData, hasNext, hasPrevious, getDataSet, @@ -29,6 +30,14 @@ describe('localHelpers', () => { expect(visibleData.toJSON().map(withoutMetadata)).toEqual([{two: 'two', one: 'one'}]); }); + it('gets original data', () => { + const state = getBasicState(); + const data = getOriginalData(state); + + expect(data.size).toEqual(state.getIn(['pageProperties', 'pageSize'])); + expect(data.toJSON()).toEqual([{two: 'two', one: 'one'}]); + }); + describe('hasNext', () => { it('returns true when on first page and there are more results', () => { //this is currently one page and one result per page and there are two items diff --git a/js/helpers/data-helpers.js b/js/helpers/data-helpers.js index 1662bce..420fc6f 100644 --- a/js/helpers/data-helpers.js +++ b/js/helpers/data-helpers.js @@ -5,7 +5,7 @@ export function getVisibleData(state) { const data = state.get('data'); const columns = getDataColumns(state, data); - return getVisibleDataColumns(getSortedColumns(data, columns), columns); + return getVisibleDataColumns(data, columns); } export function updateVisibleData(state) { @@ -69,11 +69,6 @@ export function getAllPossibleColumns(state) { return state.get('data').get(0).keySeq(); } -export function getSortedColumns(data, columns) { - return data - .map(item => item.sortBy((val, key) => columns.indexOf(key))); -} - //From Immutable docs - https://github.com/facebook/immutable-js/wiki/Predicates function keyInArray(keys) { var keySet = Immutable.Set(keys); @@ -105,7 +100,8 @@ export function getVisibleDataColumns(data, columns) { const result = data.map(d => d.filter(keyInArray(columns))); - return result.mergeDeep(extra); + return result.mergeDeep(extra) + .map(item => item.sortBy((val, key) => columns.indexOf(key) > -1 ? columns.indexOf(key) : MAX_SAFE_INTEGER )); } export function getDataColumns(state, data) { diff --git a/js/helpers/local-helpers.js b/js/helpers/local-helpers.js index 89f2900..7eedc4b 100644 --- a/js/helpers/local-helpers.js +++ b/js/helpers/local-helpers.js @@ -1,7 +1,6 @@ import { getPageCount, getDataColumns, - getSortedColumns, getVisibleDataColumns, addKeyToRows } from './data-helpers'; @@ -18,9 +17,17 @@ export function getVisibleData(state) { const data = getDataSet(state) .skip(pageSize * (currentPage-1)).take(pageSize); - const columns = getDataColumns(state, data); - return getVisibleDataColumns(getSortedColumns(data, columns), columns); + return getVisibleDataColumns(data, columns); +} + +export function getOriginalData(state) { + //get the max page / current page and the current page of data + const pageSize = state.getIn(['pageProperties', 'pageSize']); + const currentPage = state.getIn(['pageProperties', 'currentPage']); + + return getDataSet(state) + .skip(pageSize * (currentPage-1)).take(pageSize); } export function hasNext(state) { diff --git a/js/initialStates/local-state.js b/js/initialStates/local-state.js index 9cc7242..f2df657 100644 --- a/js/initialStates/local-state.js +++ b/js/initialStates/local-state.js @@ -4,8 +4,9 @@ export default Immutable.fromJS({ pageProperties: { pageSize: 10, currentPage: 1, - sortColumns: [], - sortAscending: true }, - filter: '' -}); \ No newline at end of file + metadataColumns: ['griddleKey'], + filter: '', + sortColumns: [], + sortDirections: [] +}); diff --git a/js/module.js b/js/module.js index ab230fb..39ede2c 100644 --- a/js/module.js +++ b/js/module.js @@ -3,9 +3,13 @@ import * as states from './initialStates/index'; import GriddleReducer from './reducers/griddle-reducer'; import * as GriddleActions from './actions/local-actions'; import * as GriddleHelpers from './helpers'; +import * as selectors from './selectors'; +import * as utils from './utils'; export { reducers as Reducers } +export { utils as Utils } +export { selectors as Selectors } export { states as States } export { GriddleActions as GriddleActions }; export { GriddleReducer as GriddleReducer }; -export {GriddleHelpers as GriddleHelpers} \ No newline at end of file +export {GriddleHelpers as GriddleHelpers} diff --git a/js/reducers/__tests__/localReducerTest.js b/js/reducers/__tests__/localReducerTest.js index 636cf17..0b4759d 100644 --- a/js/reducers/__tests__/localReducerTest.js +++ b/js/reducers/__tests__/localReducerTest.js @@ -13,7 +13,12 @@ import LocalReducer, { import extend from 'lodash.assign'; -const initialState = {renderProperties: {columnProperties: null}}; +const initialState = { + renderProperties: {columnProperties: null}, + sortDirections: [], + sortColumns: [] +}; + //TODO: Import the testHelpers instead of using this directly const getMethod = (options) => { if(!options.method) { @@ -32,6 +37,9 @@ const defaultData = [ {one: "ichi", two: "ni", three: "san"}, ]; +const withDefaultKeys = (data) => data.toJSON().map(row => ({ one: row["one"], two: row["two"], three: row["three"]})) + + describe('localDataReducer', () => { describe('load data', () => { const loadData = (options) => { @@ -42,7 +50,7 @@ describe('localDataReducer', () => { const helpers = extend(Helpers, { addKeyToRows: (state) => {return state} }); const state = loadData({ helpers, payload: { data: defaultData }}); - expect(state.get('data').toJSON()).toEqual(defaultData); + expect(withDefaultKeys(state.get('data'))).toEqual(defaultData); }); it('sets all columns', () => { @@ -50,17 +58,6 @@ describe('localDataReducer', () => { const state = loadData({ helpers, payload: { data: defaultData }}); expect(state.get('allColumns')).toEqual(['one', 'two', 'three']) }); - - it('sets max page', () => { - const helpers = extend(Helpers, { - addKeyToRows: (state) => {return state}, - getPageCount: (state) => { return 3} - }); - - const state = loadData({ helpers, payload: { data: defaultData }}); - - expect(state.getIn(['pageProperties', 'maxPage'])).toEqual(3); - }); }); describe('after reduce', () => { @@ -71,47 +68,11 @@ describe('localDataReducer', () => { const defaultHelpers = extend(Helpers, { getDataSet: (state) => { return state; }, getVisibleData: (state) => { return state; }, + getOriginalData: (state) => { return state; }, hasNext: (state) => { return true; }, hasPrevious: (state) => { return true; }, getDataSetSize: (state) => { return state.count(); } }); - - it('sets visible data', () => { - const visibleData = [{ one: "one", two: "two", three: "three" }]; - const helpers = extend({}, defaultHelpers, { - getVisibleData: (state) => { return visibleData; }, - }); - - const state = afterReduce({ helpers }); - expect(state.get('visibleData')).toEqual(visibleData); - }); - - it('sets has next', () => { - const helpers = extend({}, defaultHelpers, { - hasNext: (state) => { return true; }, - }); - - const state = afterReduce({ helpers }); - expect(state.get('hasNext')).toEqual(true); - }); - - it('sets has previous', () => { - const helpers = extend({}, defaultHelpers, { - hasPrevious: (state) => { return true; }, - }); - - const state = afterReduce({ helpers }); - expect(state.get('hasPrevious')).toEqual(true); - }); - - it('sets max page', () => { - const helpers = extend({}, defaultHelpers, { - getPageCount: (state) => { return 3} - }); - - const state = afterReduce({ helpers }); - expect(state.getIn(['pageProperties', 'maxPage'])).toEqual(3); - }); }); describe('set page size', () => { @@ -123,14 +84,12 @@ describe('localDataReducer', () => { getPageCount: (state) => { return 3; } }); - it('sets the page size', () => { const state = setPageSize({ helpers: defaultHelpers, state: Immutable.fromJS({ data: defaultData }), payload: { pageSize: 10 } }); - expect(state.getIn(['pageProperties', 'maxPage'])).toEqual(3); expect(state.getIn(['pageProperties', 'pageSize'])).toEqual(10); }); }); @@ -155,7 +114,7 @@ describe('localDataReducer', () => { payload: { pageNumber: 2 } }, GRIDDLE_GET_PAGE); - expect(state.get('data')).toEqual(defaultPage[2]); + expect(state.getIn(['pageProperties', 'currentPage'])).toEqual(2); }); it('gets next page', () => { @@ -163,7 +122,7 @@ describe('localDataReducer', () => { state: Immutable.fromJS({ pageProperties: { currentPage: 0, maxPage: 2 } }), }, GRIDDLE_NEXT_PAGE); - expect(state.get('data')).toEqual(defaultPage[1]); + expect(state.getIn(['pageProperties', 'currentPage'])).toEqual(1); }); it('gets last page when calling next page on last page', () => { @@ -171,7 +130,7 @@ describe('localDataReducer', () => { state: Immutable.fromJS({ pageProperties: { currentPage: 2, maxPage: 2 } }), }, GRIDDLE_NEXT_PAGE); - expect(state.get('data')).toEqual(defaultPage[2]); + expect(state.getIn(['pageProperties', 'currentPage'])).toEqual(2); }); it('gets previous page', () => { @@ -179,7 +138,7 @@ describe('localDataReducer', () => { state: Immutable.fromJS({ pageProperties: { currentPage: 1 } }), }, GRIDDLE_PREVIOUS_PAGE); - expect(state.get('data')).toEqual(defaultPage[0]); + expect(state.getIn(['pageProperties', 'currentPage'])).toEqual(0); }); it('gets first page when calling previous on first page', () => { @@ -187,7 +146,7 @@ describe('localDataReducer', () => { state: Immutable.fromJS({ pageProperties: { currentPage: 0 } }), }, GRIDDLE_PREVIOUS_PAGE); - expect(state.get('data')).toEqual(defaultPage[0]); + expect(state.getIn(['pageProperties', 'currentPage'])).toEqual(0); }); }); @@ -212,22 +171,10 @@ describe('localDataReducer', () => { return getMethod(extend(options, { method })); } - it('returns the state when no sort columns are present', () => { - const state = reducer({}, GRIDDLE_SORT); - - expect(state.toJSON()).toEqual(initialState); - }); - - it('calls sortDataByColumns when sort column present', () => { - let count = 0; - - const helpers = extend(Helpers, - { sortDataByColumns: (state, pageNumber) => new Immutable.Map({count: count++ }) }); - const payload = { sortColumns: ['one'] }; - const state = reducer({ helpers, payload }, GRIDDLE_SORT) + it('sets sort column', () => { + const state = reducer({payload: { sortColumns: ['one']}}, GRIDDLE_SORT); - //the sortByColumns method should increment the count -- this is a cheezy shouldHaveBeenCalled - expect(count).toEqual(1); + expect(state.get('sortColumns')).toEqual(['one']); }); }); }); diff --git a/js/reducers/local-reducer.js b/js/reducers/local-reducer.js index f3267f9..efdd6c2 100644 --- a/js/reducers/local-reducer.js +++ b/js/reducers/local-reducer.js @@ -1,7 +1,7 @@ -'use strict'; - import * as types from '../constants/action-types'; import Immutable from 'immutable'; +import reselect from 'reselect'; +import { addKeyToRows } from '../utils/dataUtils'; /* The handler that happens when data is loaded. @@ -12,41 +12,19 @@ import Immutable from 'immutable'; visible data it should sort the data if a sort is specified */ -export function GRIDDLE_LOADED_DATA(state, action, helpers) { +export function GRIDDLE_LOADED_DATA(state, action) { const columns = action.data.length > 0 ? Object.keys(action.data[0]) : []; + //set state's data to this - const tempState = state - .set('data', helpers.addKeyToRows(Immutable.fromJS(action.data))) + return state + .set('data', addKeyToRows(Immutable.fromJS(action.data))) .set('allColumns', columns) - .setIn( - ['pageProperties', 'maxPage'], - helpers.getPageCount( - action.data.length, - state.getIn(['pageProperties', 'pageSize']))) .set('loading', false); - - const columnProperties = tempState.get('renderProperties').get('columnProperties'); - - return columnProperties ? - helpers - .sortDataByColumns(tempState, helpers) - .setIn(['pageProperties', 'currentPage'], 1) : - tempState; - } -export function AFTER_REDUCE(state, action, helpers) { - const tempState = state - .set('visibleData', helpers.getVisibleData(state)) - .setIn( - ['pageProperties', 'maxPage'], - helpers.getPageCount( - helpers.getDataSetSize(state), - state.getIn(['pageProperties', 'pageSize']))) - - return tempState - .set('hasNext', helpers.hasNext(tempState)) - .set('hasPrevious', helpers.hasPrevious(tempState)); +//TODO: This should go away +export function AFTER_REDUCE(state, action) { + return state; } /* @@ -56,48 +34,39 @@ export function AFTER_REDUCE(state, action, helpers) { hasNext, hasPrevious */ -export function GRIDDLE_SET_PAGE_SIZE(state, action, helpers) { - const pageSizeState = state +export function GRIDDLE_SET_PAGE_SIZE(state, action) { + return state .setIn(['pageProperties', 'currentPage'], 1) .setIn(['pageProperties', 'pageSize'], action.pageSize); - - const stateWithMaxPage = pageSizeState - .setIn( - ['pageProperties', 'maxPage'], - helpers.getPageCount( - pageSizeState.get('data').size, - action.pageSize)); - - return stateWithMaxPage; } //TODO: Move the helper function to the method body and call this // from next / previous. This will be easier since we have // the AFTER_REDUCE stuff now. -export function GRIDDLE_GET_PAGE(state, action, helpers) { - return(helpers - .getPage(state, action.pageNumber)); +export function GRIDDLE_GET_PAGE(state, action) { + return state.setIn(['pageProperties', 'currentPage'], action.pageNumber) } -export function GRIDDLE_NEXT_PAGE(state, action, helpers) { + +export function GRIDDLE_NEXT_PAGE(state, action) { const currentPage = state.getIn(['pageProperties', 'currentPage']); const maxPage = state.getIn(['pageProperties', 'maxPage']); - return(helpers - .getPage(state, - currentPage < maxPage ? currentPage + 1 : currentPage)); + return currentPage < maxPage ? + state.setIn(['pageProperties', 'currentPage'], currentPage + 1) : + state; } -export function GRIDDLE_PREVIOUS_PAGE(state, action, helpers) { +export function GRIDDLE_PREVIOUS_PAGE(state, action) { const currentPage = state.getIn(['pageProperties', 'currentPage']); - return(helpers - .getPage(state, - currentPage > 0 ? currentPage - 1 : currentPage )); + return currentPage > 0 ? + state.setIn(['pageProperties', 'currentPage'], currentPage - 1) : + state } -export function GRIDDLE_FILTERED(state, action, helpers) { +export function GRIDDLE_FILTERED(state, action) { //TODO: Just set the filter and let the visible data handle what is actually shown + next / previous return state .set('filter', action.filter) @@ -106,15 +75,19 @@ export function GRIDDLE_FILTERED(state, action, helpers) { //TODO: This is a really simple sort, for now // We need to add sort type and different sort operations -export function GRIDDLE_SORT(state, action, helpers) { +export function GRIDDLE_SORT(state, action) { if(!action.sortColumns || action.sortColumns.length < 1) { return state } - // Update the sort columns - let tempState = helpers.updateSortColumns(state, action.sortColumns); + const sortDirections = state.get('sortDirections').size !== 0 && (state.get('sortDirections').size > 0 && + state.get('sortDirections').first() === true && + state.get('sortColumns')[0] === action.sortColumns[0]) ? + Immutable.List([!state.get('sortDirections').first()]) : + Immutable.List([true]); - const columnProperties = state.get('renderProperties').get('columnProperties'); - // Sort the data - return helpers - .sortDataByColumns(tempState, helpers) + return state .setIn(['pageProperties', 'currentPage'], 1) + .set('sortColumns', action.sortColumns) + .set('sortDirections', sortDirections) } + + diff --git a/js/selectors/__tests__/localSelectorsTest.js b/js/selectors/__tests__/localSelectorsTest.js new file mode 100644 index 0000000..53465a5 --- /dev/null +++ b/js/selectors/__tests__/localSelectorsTest.js @@ -0,0 +1,295 @@ +import Immutable from 'immutable'; +import * as selectors from '../localSelectors'; +import sortUtils from '../../utils/sortUtils'; + +//TODO: this needs to go away +selectors.registerUtils(sortUtils) + + +function getBasicState() { + return Immutable.fromJS({ + data: [ + { one: 'one', two: 'two' }, + { one: 'three', two: 'four' } + ], + pageProperties: { + property1: 'one', + property2: 'two', + pageSize: 1, + currentPage: 0, + maxPage: 2 + }, + sortColumns: [], + sortDirections: [] + }); +} + +function withRenderProperties(state) { + return state.set('renderProperties', new Immutable.fromJS({ + columnProperties: { + one: { id: 'one', displayName: 'One', order: 2 }, + two: { id: 'two', displayName: 'Two', order: 1 } + } + }) + ) +} + +export function get3ColState() { + return Immutable.fromJS({ + data: [ + { one: 'one', two: 'two', three: 'three' }, + { one: 'four', two: 'five', three: 'six' } + ], + pageProperties: { + property1: 'one', + property2: 'two', + pageSize: 1, + currentPage: 0, + maxPage: 2 + }, + sortColumns: [], + sortDirections: [] + }); +} + +describe('localSelectors', () => { + var initialState; + + beforeEach(() => { + initialState = new Immutable.Map(); + }) + + it('gets data', () => { + const state = initialState.set('data', 'hi'); + expect(selectors.dataSelector(state)).toEqual('hi'); + }) + + it('gets pageSize', () => { + const state = initialState.setIn(['pageProperties', 'pageSize'], 20); + expect(selectors.pageSizeSelector(state)).toEqual(20); + }) + + describe('hasNextSelector', () => { + it('gets true when there are more possible pages', () => { + const state = getBasicState() + .setIn(['pageProperties', 'currentPage'], 1); + + expect(selectors.hasNextSelector(state)).toEqual(true); + }) + + it('gets false when no more possible pages', () => { + const state = getBasicState() + .setIn(['pageProperties', 'currentPage'], 5) + + expect(selectors.hasNextSelector(state)).toEqual(false); + }) + }) + + describe('hasPreviousSelector', () => { + it('gets true when there are previous pages', () => { + const state = initialState + .setIn(['pageProperties', 'currentPage'], 5); + + expect(selectors.hasPreviousSelector(state)).toEqual(true); + }) + + it('gets false when no previous pages', () => { + const state = initialState + .setIn(['pageProperties', 'currentPage'], 1); + + expect(selectors.hasPreviousSelector(state)).toEqual(false); + }) + }) + + it('gets current page', () => { + const state = initialState.setIn(['pageProperties', 'currentPage'], 5); + expect(selectors.currentPageSelector(state)).toEqual(5); + }); + + it('gets max page', () => { + const state = getBasicState(); + expect(selectors.maxPageSelector(state)).toEqual(2); + }) + + it('gets filter', () => { + const state = initialState.set('filter', 'filtered'); + expect(selectors.filterSelector(state)).toEqual('filtered'); + }); + + it('gets empty string when no filter present', () => { + const state = initialState; + expect(selectors.filterSelector(state)).toEqual(''); + }) + + describe('filteredDataSelector', () => { + it('gets filtered data', () => { + const state = getBasicState() + .set('filter', 'four'); + + const filteredData = selectors.filteredDataSelector(state); + + expect(filteredData.size).toEqual(1); + expect(filteredData.toJSON()).toEqual([{one: 'three', two: 'four'}]); + }) + + it('gets the entire dataset when no filter is present', () => { + const state = getBasicState(); + const filteredData = selectors.filteredDataSelector(state); + + expect(filteredData.size).toEqual(2); + expect(filteredData.toJSON()).toEqual(state.get('data').toJSON()); + }) + }) + + describe ('with metadata columns', () => { + it('gets metadata columns', () => { + const state = get3ColState().set('metadataColumns', Immutable.List(['two'])); + const metaDataColumns = selectors.metaDataColumnsSelector(state); + expect(metaDataColumns.toJSON()).toEqual(['two']) + }) + }) + + describe ('hidden columns', () => { + it('gets hidden columns without visible / metadata columns', () => { + const state = withRenderProperties(get3ColState() + .set('metadataColumns', Immutable.List(['two']))); + + const hidden = selectors.hiddenColumnsSelector(state) + expect(hidden).toEqual(['three']) + }); + + it('returns empty when none', () => { + const state = withRenderProperties(getBasicState()) + const hidden = selectors.hiddenColumnsSelector(state) + expect(hidden).toEqual([]); + }); + }) + + describe('renderable columns', () => { + it('gets renderable columns', () => { + const state = get3ColState() + .set('metadataColumns', Immutable.List(['two'])) + .set('renderProperties', new Immutable.fromJS({ + columnProperties: { + one: { id: 'one', displayName: 'One', order: 2 }, + } + })) + + const renderableColumns = selectors.renderableColumnsSelector(state) + expect(renderableColumns).toEqual(['one', 'three']) + }) + }) + + describe('visible columns', () => { + it('gets the renderProperties', () => { + const state = withRenderProperties(getBasicState()); + + const renderProperties = selectors.renderPropertiesSelector(state); + expect(renderProperties.toJSON()).toEqual({ + columnProperties: { + one: { id: 'one', displayName: 'One', order: 2 }, + two: { id: 'two', displayName: 'Two', order: 1 } + } + }); + }) + + it('gets sorted columns', () => { + const state = withRenderProperties(getBasicState()); + const sortedColumnSettings = selectors.sortedColumnPropertiesSelector(state); + + expect(sortedColumnSettings.toJSON()).toEqual({ + two: { id: 'two', displayName: 'Two', order: 1 }, + one: { id: 'one', displayName: 'One', order: 2 } + }); + }) + + it('gets the correct order for magic columns when no order specified', () => { + const state = getBasicState() + .set('renderProperties', new Immutable.fromJS({ + columnProperties: { + one: { id: 'one', displayName: 'One' }, + onepointfive: { id: 'onepointfive' }, + two: { id: 'two', displayName: 'Two' } + } + })) + + const sortedSettings = selectors.sortedColumnPropertiesSelector(state); + expect(Object.keys(sortedSettings.toJSON())).toEqual(['one', 'onepointfive', 'two']); + }) + + it('gets the correct order for magic columns when order specified', () => { + const state = getBasicState() + .set('renderProperties', new Immutable.fromJS({ + columnProperties: { + one: { id: 'one', displayName: 'One', order: 3 }, + onepointfive: { id: 'onepointfive', order: 1 }, + two: { id: 'two', displayName: 'Two', order: 2 } + } + })) + + const sortedSettings = selectors.sortedColumnPropertiesSelector(state); + expect(Object.keys(sortedSettings.toJSON())).toEqual(['onepointfive', 'two', 'one']); + }) + + it('gets sorted data', () => { + const state = withRenderProperties(getBasicState()) + .set('sortColumns', ['two']) + .set('sortAscending', [true]); + + const sortedData = selectors.sortedDataSelector(state); + + expect(sortedData.toJSON()).toEqual([{one: 'three', two: 'four'}, {one: 'one', two: 'two'}]); + }) + + it('gets all columns', () => { + const state = getBasicState(); + + const allColumns = selectors.allColumnsSelector(state); + expect(allColumns).toEqual(['one', 'two']); + }) + + it('gets visible columns', () => { + const state = withRenderProperties(get3ColState()); + + const visibleColumns = selectors.visibleColumnsSelector(state); + expect(visibleColumns).toEqual(['two', 'one']); + }); + + it('gets all columns when no columns specified', () => { + const state = get3ColState(); + + const visibleColumns = selectors.visibleColumnsSelector(state); + expect(visibleColumns).toEqual(['one', 'two', 'three']) + }) + }) + + describe('current data page', () => { + it ('gets current data', () => { + const state = get3ColState(); + + const data = selectors.currentPageDataSelector(state); + expect(data.toJSON()).toEqual([{one: 'one', two: 'two', three: 'three'}]); + }) + }) + + describe('visible data', () => { + it ('gets columns in the right order', () => { + const state = withRenderProperties(get3ColState()); + const data = selectors.visibleDataSelector(state); + expect(Object.keys(data.toJSON()[0])).toEqual(['two', 'one']) + }) + + it('gets magic columns', () => { + const state = withRenderProperties(getBasicState()) + .mergeDeepIn(['renderProperties', 'columnProperties'], { onepointfive: null }); + + + const data = selectors.visibleDataSelector(state); + + expect(data.toJSON()).toEqual([ + {two: 'two', onepointfive: null, one: 'one'}, + ]); + }) + + }) +}) diff --git a/js/selectors/index.js b/js/selectors/index.js new file mode 100644 index 0000000..ad56c98 --- /dev/null +++ b/js/selectors/index.js @@ -0,0 +1,3 @@ +import * as localSelectors from './localSelectors'; + +export { localSelectors as localSelectors } diff --git a/js/selectors/localSelectors.js b/js/selectors/localSelectors.js new file mode 100644 index 0000000..71853ff --- /dev/null +++ b/js/selectors/localSelectors.js @@ -0,0 +1,203 @@ +import Immutable from 'immutable'; +import MAX_SAFE_INTEGER from 'max-safe-integer'; +import { createSelector } from 'reselect'; +import { getVisibleDataColumns, getDataForColumns } from '../utils/dataUtils' + +//oy - not a fan -- refactor asap because this is no good +let localUtils = null; +export const registerUtils = utils => { localUtils = utils } + +//this will get the utils or throw an error +export const getUtils = () => { + if (localUtils) { + return localUtils; + } + + console.error("Please call registerUtils with a util object when initializing the selectors"); +} + +//gets the full dataset currently tracked by griddle +export const dataSelector = state => state.get('data'); + +//gets the number of records to display +export const pageSizeSelector = state => state.getIn(['pageProperties', 'pageSize']); + +//what's the current page +export const currentPageSelector = state => state.getIn(['pageProperties', 'currentPage']); + +//max page number +export const maxPageSelector = createSelector( + pageSizeSelector, + dataSelector, + (pageSize, data) => { + const total = data.size; + const calc = total / pageSize; + + return calc > Math.floor(calc) ? Math.floor(calc) + 1 : Math.floor(calc); + } +) +//what's the current selector +export const filterSelector = state => state.get('filter')||''; + +//gets the current sort columns +export const sortColumnsSelector = state => (state.get('sortColumns')||[]) + +//gets the current sort direction (this is an array that corresponds to columns) records are true if sortAscending +export const sortColumnsShouldSortAscendingSelector = state => (state.get('sortDirections') || []) + +//the properties that determine how things are rendered +export const renderPropertiesSelector = state => state.get('renderProperties'); + +export const allColumnsSelector = createSelector( + dataSelector, + (data) => (data.size === 0 ? [] : data.get(0).keySeq().toJSON()) +) + +//gets the metadata columns or nothing +export const metaDataColumnsSelector = state => (state.get('metadataColumns') || []) + +//gets the column property objects ordered by order +export const sortedColumnPropertiesSelector = 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 + ) +) + +//gets the visible columns +export const visibleColumnsSelector = createSelector( + sortedColumnPropertiesSelector, + allColumnsSelector, + (sortedColumnProperties, allColumns) => ( + sortedColumnProperties ? sortedColumnProperties + .keySeq() + .toJSON() : + allColumns + ) +) + +//is there a next page +export const hasNextSelector = createSelector( + currentPageSelector, + maxPageSelector, + (currentPage, maxPage) => (currentPage < maxPage) +); + +//is there a previous page? +export const hasPreviousSelector = state => (state.getIn(['pageProperties', 'currentPage']) > 1); + +//get the filtered data +export const filteredDataSelector = createSelector( + dataSelector, + filterSelector, + (data, filter) => { + return data.filter(row => { + return Object.keys(row.toJSON()) + .some(key => { + return row.get(key) && row.get(key).toString().toLowerCase().indexOf(filter.toLowerCase()) > -1 + }) + }) + } +) + +export const sortedDataSelector = createSelector( + filteredDataSelector, + sortColumnsSelector, + sortColumnsShouldSortAscendingSelector, + renderPropertiesSelector, + (filteredData, sortColumns, sortColumnsShouldSortAscending, renderProperties) => { + const sortType = renderProperties && renderProperties.get('columnProperties') + return getUtils().getSortedData(filteredData, sortColumns, sortColumnsShouldSortAscending.first()) + } +) + +export const currentPageDataSelector = createSelector( + sortedDataSelector, + pageSizeSelector, + currentPageSelector, + (sortedData, pageSize, currentPage) => { + return sortedData + .skip(pageSize * (currentPage - 1)) + .take(pageSize); + } +) + +//get the visible data (and only the columns that are visible) +export const visibleDataSelector = createSelector( + currentPageDataSelector, + visibleColumnsSelector, + (currentPageData, visibleColumns) => getVisibleDataColumns(currentPageData, visibleColumns) +) + +export const hiddenColumnsSelector = createSelector( + visibleColumnsSelector, + allColumnsSelector, + metaDataColumnsSelector, + (visibleColumns, allColumns, metaDataColumns) => { + const removeColumns = [...visibleColumns, ...metaDataColumns]; + + return allColumns.filter(c => removeColumns.indexOf(c) === -1); + } +) + +export const renderableColumnsSelector = createSelector( + visibleColumnsSelector, + hiddenColumnsSelector, + (visibleColumns, hiddenColumns) => [...visibleColumns, ...hiddenColumns] +) + +//TODO: this needs some tests +export const hiddenDataSelector = createSelector( + currentPageDataSelector, + visibleColumnsSelector, + allColumnsSelector, + metaDataColumnsSelector, + (currentPageData, visibleColumns, allColumns, metaDataColumns) => { + return getDataForColumns(currentPageData, keys) + } +) + +//TODO: this needs some tests +export const metaDataSelector = createSelector( + currentPageDataSelector, + metaDataColumnsSelector, + (currentPageData, metaDataColumns) => { return getDataForColumns(currentPageData, metaDataColumns) } +) + +//TODO: This NEEDS tests +export const columnTitlesSelector = createSelector( + visibleDataSelector, + metaDataSelector, + renderPropertiesSelector, + (visibleData, metaData, renderProperties) => { + if(visibleData.size > 0) { + return Object.keys(visibleData.get(0).toJSON()).map(k => + renderProperties.get('columnProperties').get(k).get('displayName') || k + ) + } + + return []; + } +) + +export const gridStateSelector = createSelector( + visibleDataSelector, + metaDataSelector, + currentPageDataSelector, + renderPropertiesSelector, + columnTitlesSelector, + allColumnsSelector, + renderableColumnsSelector, + (visibleData, metaData, currentPageData, renderProperties, columnTitles, allColumns, renderableColumns) => ({ + visibleData, + metaData, + currentPageData, + renderProperties, + columnTitles, + allColumns, + renderableColumns + }) +) diff --git a/js/utils/__tests__/sortUtilsTest.js b/js/utils/__tests__/sortUtilsTest.js new file mode 100644 index 0000000..e7ac5d9 --- /dev/null +++ b/js/utils/__tests__/sortUtilsTest.js @@ -0,0 +1,104 @@ +import Immutable from 'immutable'; +import sortUtils from '../sortUtils'; + +function getBasicState() { + return Immutable.fromJS({ + data: [ + { one: 'one', two: 'two' }, + { one: 'three', two: 'four' } + ], + pageProperties: { + property1: 'one', + property2: 'two', + pageSize: 1, + currentPage: 0, + maxPage: 2 + } + }); +} + +describe('SortUtils', () => { + it('sorts the data', () => { + const state = getBasicState(); + const sortedData = sortUtils.getSortedData(state.get('data'), ['two'], true); + + expect(sortedData.toJSON()).toEqual([{one: 'three', two: 'four'}, {one: 'one', two: 'two'}]); + }); + + it('sorts the data in reverse when ascending is false', () => { + const state = getBasicState(); + const sortedData = sortUtils.getSortedData(state.get('data'), ['two'], false); + + expect(sortedData.toJSON()).toEqual([{one: 'one', two: 'two'}, {one: 'three', two: 'four'}]); + }) + + it('sorts by date', () => { + const state = getBasicState() + .set('data', Immutable.fromJS([ + {one: '1/1/1900', two: '1/5/2016'}, + {one: '7/20/1982', two: '8/21/2015'}])) + .setIn( + ['renderProperties', 'columnProperties', 'two'], + new Immutable.Map({ sortType: 'date', id: 'two', displayName: 'Two'})) + + const sortedData = sortUtils.getSortedData(state.get('data'), ['two'], true, 'date'); + + expect(sortedData.toJSON()) + .toEqual([{one: '7/20/1982', two: '8/21/2015'}, {one: '1/1/1900', two: '1/5/2016'}]); + }) + + it('sorts by date descending', () => { + const state = getBasicState() + .set('data', Immutable.fromJS([ + {one: '1/1/1900', two: '1/5/2016'}, + {one: '7/20/1982', two: '8/21/2015'}])) + + .setIn( + ['renderProperties', 'columnProperties', 'two'], + new Immutable.Map({ sortType: 'date', id: 'two', displayName: 'Two'})) + + const sortedData = sortUtils.getSortedData(state.get('data'), ['two'], false, 'date'); + + expect(sortedData.toJSON()) + .toEqual([{one: '1/1/1900', two: '1/5/2016'}, {one: '7/20/1982', two: '8/21/2015'}]); + }) + + it('works with custom sort types', () => { + const state = getBasicState() + .set('data', Immutable.fromJS([ + {one: 'hi', two: 'something'}, + {one: 'yo', two: 'another'}, + {one: 'greetings', two: 'other'}, + {one: 'hello', two: 'final'}])) + + + const newSort = (data, column, sortAscending = true) => { + return data.sort( + (original, newRecord) => { + original = (!!original.get(column) && original.get(column)) || ""; + newRecord = (!!newRecord.get(column) && newRecord.get(column)) || ""; + + if(original[1] === newRecord[1]) { + return 0; + } else if (original[1] > newRecord[1]) { + return sortAscending ? 1 : -1; + } + else { + return sortAscending ? -1 : 1; + } + }) + } + + const newSortTypes = Object.assign({}, sortUtils.sortTypes, {secondLetter: newSort }); + const newSortUtils = Object.assign({}, sortUtils, { sortTypes: newSortTypes }); + + const sortedData = newSortUtils.getSortedData(state.get('data'), ['one'], true, 'secondLetter'); + + expect(sortedData.toJSON()).toEqual([ + {one: 'hello', two: 'final'}, + {one: 'hi', two: 'something'}, + {one: 'yo', two: 'another'}, + {one: 'greetings', two: 'other'}, + ]) + }) +}) diff --git a/js/utils/dataUtils.js b/js/utils/dataUtils.js new file mode 100644 index 0000000..d4c17d5 --- /dev/null +++ b/js/utils/dataUtils.js @@ -0,0 +1,56 @@ +import Immutable from 'immutable'; +import MAX_SAFE_INTEGER from 'max-safe-integer'; + +//From Immutable docs - https://github.com/facebook/immutable-js/wiki/Predicates +function keyInArray(keys) { + var keySet = Immutable.Set(keys); + return function (v, k) { + + return keySet.has(k); + } +} + +//TODO: Move the test from helpers to here +export function addKeyToRows(data) { + let key = 0; + const getKey = (() => key++); + + return data.map(row => row.set('griddleKey', getKey())); +} + +export function getDataForColumns(data, columns) { + if (data.size < 1) { + return data; + } + + const dataColumns = data.get(0).keySeq().toArray(); + const resultColumns = dataColumns.filter(item => columns.indexOf(item) >= 0); + return data.map(d => d.filter(keyInArray(resultColumns))); +} + +//, {__metadata: d.filter(keyInArray(metadataColumns)).set('index', i)} +export function getVisibleDataColumns(data, columns) { + if (data.size < 1) { + return data; + } + + const dataColumns = data.get(0).keySeq().toArray(); + + const metadataColumns = dataColumns.filter(item => columns.indexOf(item) < 0); + + //if columns are specified but aren't in the data + //make it up (as null). We will append this column + //to the resultant data + const magicColumns = columns + .filter(item => dataColumns.indexOf(item) < 0) + .reduce((original, item) => { original[item] = null; return original}, {}) + //combine the metadata and the "magic" columns + const extra = data.map((d, i) => new Immutable.Map( + Object.assign(magicColumns) + )); + + const result = data.map(d => d.filter(keyInArray(columns))); + + return result.mergeDeep(extra) + .map(item => item.sortBy((val, key) => columns.indexOf(key) > -1 ? columns.indexOf(key) : MAX_SAFE_INTEGER )); +} diff --git a/js/utils/index.js b/js/utils/index.js new file mode 100644 index 0000000..75a12a5 --- /dev/null +++ b/js/utils/index.js @@ -0,0 +1,5 @@ +import * as dataUtils from './dataUtils'; +import sortUtils from './sortUtils'; + +export { dataUtils as dataUtils } +export { sortUtils as sortUtils } diff --git a/js/utils/sortUtils.js b/js/utils/sortUtils.js new file mode 100644 index 0000000..757d23f --- /dev/null +++ b/js/utils/sortUtils.js @@ -0,0 +1,48 @@ +function defaultSort(data, column, sortAscending = true) { + return data.sort( + (original, newRecord) => { + original = (!!original.get(column) && original.get(column)) || ""; + newRecord = (!!newRecord.get(column) && newRecord.get(column)) || ""; + + //TODO: This is about the most cheezy sorting check ever. + //Make it be able to sort for dates / monetary / regex / whatever + if(original === newRecord) { + return 0; + } else if (original > newRecord) { + return sortAscending ? 1 : -1; + } + else { + return sortAscending ? -1 : 1; + } + }); +} + +function dateSort(data, column, sortAscending = true) { + return data.sort( + (original, newRecord) => { + original = (!!original.get(column) && new Date(original.get(column))) || null; + newRecord = (!!newRecord.get(column) && new Date(newRecord.get(column))) || null; + if(original.getTime() === newRecord.getTime()) { + return 0; + } else if (original > newRecord) { + return sortAscending ? 1 : -1; + } else { + return sortAscending ? -1 : 1; + } + }) +} + +export default { + getSortedData: function(data, columns, sortAscending = true, sortType = 'default') { + return this.getSortByType(sortType)(data, columns[0], sortAscending) + }, + + getSortByType: function(type) { + const sortType = this.sortTypes; + return sortType.hasOwnProperty(type) ? sortType[type] : defaultSort + }, + sortTypes: { + "default": defaultSort, + "date": dateSort, + } +} diff --git a/package.json b/package.json index 3cc2261..ba3d8ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "griddle-core", - "version": "0.0.19", + "version": "0.0.20", "description": "The core code used in the griddle-react grid. ", "main": "build/griddle.js", "scripts": { @@ -37,6 +37,7 @@ "immutable": "^3.7.2", "lodash.assign": "^3.2.0", "lodash.pick": "^3.1.0", - "max-safe-integer": "^1.0.0" + "max-safe-integer": "^1.0.0", + "reselect": "^2.0.3" } }