diff --git a/src/components/ConnectedFilter/index.jsx b/src/components/ConnectedFilter/index.jsx index 5146e05a..7903f3a6 100644 --- a/src/components/ConnectedFilter/index.jsx +++ b/src/components/ConnectedFilter/index.jsx @@ -14,7 +14,7 @@ import { askGuppyForAggregationData, getAllFieldsFromFilterConfigs, } from '../Utils/queries'; -import { mergeFilters } from '../Utils/filters'; +import { mergeFilters, updateCountsInInitialTabsOptions, sortTabsOptions } from '../Utils/filters'; class ConnectedFilter extends React.Component { constructor(props) { @@ -25,6 +25,7 @@ class ConnectedFilter extends React.Component { ? _.union(filterConfigsFields, props.accessibleFieldCheckList) : filterConfigsFields; + this.initialTabsOptions = {}; this.state = { allFields, initialAggsData: {}, @@ -32,6 +33,7 @@ class ConnectedFilter extends React.Component { accessibility: ENUM_ACCESSIBILITY.ALL, adminAppliedPreFilters: Object.assign({}, this.props.adminAppliedPreFilters), filter: Object.assign({}, this.props.adminAppliedPreFilters), + filtersApplied: {}, }; this.filterGroupRef = React.createRef(); this.adminPreFiltersFrozen = JSON.stringify(this.props.adminAppliedPreFilters).slice(); @@ -68,7 +70,17 @@ class ConnectedFilter extends React.Component { * component could do some pre-processing modification about filter. */ getFilterTabs() { - const processedTabsOptions = this.props.onProcessFilterAggsData(this.state.receivedAggsData); + let processedTabsOptions = this.props.onProcessFilterAggsData(this.state.receivedAggsData); + if (Object.keys(this.initialTabsOptions).length === 0) { + this.initialTabsOptions = processedTabsOptions; + } + + processedTabsOptions = updateCountsInInitialTabsOptions( + this.initialTabsOptions, processedTabsOptions, this.state.filtersApplied, + ); + + processedTabsOptions = sortTabsOptions(processedTabsOptions); + if (!processedTabsOptions || Object.keys(processedTabsOptions).length === 0) return null; const { fieldMapping } = this.props; const tabs = this.props.filterConfig.tabs.map(({ fields }, index) => ( @@ -112,7 +124,8 @@ class ConnectedFilter extends React.Component { */ handleFilterChange(filterResults) { this.setState({ adminAppliedPreFilters: JSON.parse(this.adminPreFiltersFrozen) }); - const mergedFilterResults = mergeFilters(filterResults, this.state.adminAppliedPreFilters); + const mergedFilterResults = mergeFilters(filterResults, JSON.parse(this.adminPreFiltersFrozen)); + this.setState({ filtersApplied: mergedFilterResults }); askGuppyForAggregationData( this.props.guppyConfig.path, this.props.guppyConfig.type, @@ -172,7 +185,6 @@ ConnectedFilter.propTypes = { }).isRequired, onFilterChange: PropTypes.func, onReceiveNewAggsData: PropTypes.func, - hideZero: PropTypes.bool, className: PropTypes.string, fieldMapping: PropTypes.arrayOf(PropTypes.shape({ field: PropTypes.string, @@ -185,12 +197,12 @@ ConnectedFilter.propTypes = { lockedTooltipMessage: PropTypes.string, disabledTooltipMessage: PropTypes.string, accessibleFieldCheckList: PropTypes.arrayOf(PropTypes.string), + hideZero: PropTypes.bool, }; ConnectedFilter.defaultProps = { onFilterChange: () => {}, onReceiveNewAggsData: () => {}, - hideZero: true, className: '', fieldMapping: [], tierAccessLimit: undefined, @@ -200,6 +212,7 @@ ConnectedFilter.defaultProps = { lockedTooltipMessage: '', disabledTooltipMessage: '', accessibleFieldCheckList: undefined, + hideZero: false, }; export default ConnectedFilter; diff --git a/src/components/ConnectedFilter/utils.js b/src/components/ConnectedFilter/utils.js index 8dce30bd..83c54b3b 100644 --- a/src/components/ConnectedFilter/utils.js +++ b/src/components/ConnectedFilter/utils.js @@ -65,7 +65,6 @@ export const getFilterSections = ( options: defaultOptions, }; }); - return sections; }; diff --git a/src/components/Utils/filters.js b/src/components/Utils/filters.js index 3491b87b..ae29c9dc 100644 --- a/src/components/Utils/filters.js +++ b/src/components/Utils/filters.js @@ -31,3 +31,84 @@ export const mergeFilters = (userFilter, adminAppliedPreFilter) => { return filterAB; }; + +function isFilterOptionToBeHidden(option, filtersApplied, fieldName) { + if (option.count > 0) { + return false; + } + + if (typeof filtersApplied !== 'undefined' + && filtersApplied[fieldName] + && filtersApplied[fieldName].selectedValues.includes(option.key)) { + return false; + } + + return true; +} + +/** + * This function updates the counts in the initial set of tab options + * calculated from unfiltered data. + * It is used to retain field options in the rendering if + * they are still checked but their counts are zero. + */ +export const updateCountsInInitialTabsOptions = ( + initialTabsOptions, processedTabsOptions, filtersApplied, +) => { + const updatedTabsOptions = JSON.parse(JSON.stringify(initialTabsOptions)); + const initialFields = Object.keys(initialTabsOptions); + for (let i = 0; i < initialFields.length; i += 1) { + const fieldName = initialFields[i]; + const initialFieldOptions = initialTabsOptions[fieldName].histogram.map(x => x.key); + let processedFieldOptions = []; + if (Object.prototype.hasOwnProperty.call(processedTabsOptions, fieldName)) { + processedFieldOptions = processedTabsOptions[fieldName].histogram.map(x => x.key); + } + + for (let j = 0; j < initialFieldOptions.length; j += 1) { + const optionName = initialFieldOptions[j]; + let newCount; + if (processedFieldOptions.includes(optionName)) { + newCount = processedTabsOptions[fieldName].histogram.filter( + x => x.key === optionName, + )[0].count; + } else { + newCount = 0; + } + for (let k = 0; k < updatedTabsOptions[fieldName].histogram.length; k += 1) { + const option = updatedTabsOptions[fieldName].histogram[k]; + if (option.key === optionName) { + updatedTabsOptions[fieldName].histogram[k].count = newCount; + if (isFilterOptionToBeHidden( + updatedTabsOptions[fieldName].histogram[k], filtersApplied, fieldName, + )) { + updatedTabsOptions[fieldName].histogram.splice(k, 1); + break; + } + } + } + } + } + + return updatedTabsOptions; +}; + +function sortCountThenAlpha(a, b) { + if (a.count === b.count) { + return a.key < b.key ? -1 : 1; + } + return b.count - a.count; +} + +export const sortTabsOptions = (tabsOptions) => { + const fields = Object.keys(tabsOptions); + const sortedTabsOptions = Object.assign({}, tabsOptions); + for (let x = 0; x < fields.length; x += 1) { + const field = fields[x]; + + const optionsForThisField = sortedTabsOptions[field].histogram; + optionsForThisField.sort(sortCountThenAlpha); + sortedTabsOptions[field].histogram = optionsForThisField; + } + return sortedTabsOptions; +}; diff --git a/src/components/__tests__/filters.test.js b/src/components/__tests__/filters.test.js index 0083ddbd..cd318ee6 100644 --- a/src/components/__tests__/filters.test.js +++ b/src/components/__tests__/filters.test.js @@ -1,6 +1,6 @@ /* eslint-disable global-require,import/no-dynamic-require */ // Tests for Utils/filters.js -import { mergeFilters } from '../Utils/filters'; +import { mergeFilters, updateCountsInInitialTabsOptions, sortTabsOptions } from '../Utils/filters'; describe('can merge simple selectedValue filters', () => { const userFilter = { data_format: { selectedValues: ['VCF'] } }; @@ -60,3 +60,109 @@ describe('will select user-applied filter for a given key if it is more exclusiv .toEqual(mergedFilterExpected); }); }); + + +describe('can update a small set of tabs with new counts', () => { + const initialTabsOptions = { + annotated_sex: { + histogram: [ + { key: 'yellow', count: 137675 }, + { key: 'pink', count: 56270 }, + { key: 'silver', count: 2020 }, + { key: 'orange', count: 107574 }, + ], + }, + extra_data: { + histogram: [ + { key: 'a', count: 2 }, + ], + }, + }; + + const processedTabsOptions = { + annotated_sex: { + histogram: [ + { key: 'yellow', count: 1 }, + { key: 'orange', count: 107574 }, + ], + }, + extra_data: { histogram: [] }, + }; + + const filtersApplied = { annotated_sex: { selectedValues: ['silver'] } }; + + // Silver has a count of zero, but it is in the filter, so it should remain visible + const expectedUpdatedTabsOptions = { + annotated_sex: { + histogram: [ + { key: 'yellow', count: 1 }, + { key: 'silver', count: 0 }, + { key: 'orange', count: 107574 }, + ], + }, + extra_data: { + histogram: [], + }, + }; + + const actualUpdatedTabsOptions = updateCountsInInitialTabsOptions( + initialTabsOptions, processedTabsOptions, filtersApplied, + ); + + test('update tab counts', async () => { + expect(actualUpdatedTabsOptions) + .toEqual(expectedUpdatedTabsOptions); + }); +}); + + +describe('can sort tabs options', () => { + const tabsOptionsOne = { + annotated_sex: { + histogram: [ + { key: 'orange', count: 30 }, + { key: 'pink', count: 21 }, + { key: 'yellow', count: 99 }, + { key: 'zorp', count: 4162 }, + { key: 'shiny', count: 0 }, + { key: 'green', count: 0 }, + { key: 'blue', count: 0 }, + ], + }, + extra_data: { + histogram: [ + { key: 'a', count: 0 }, + { key: 'b', count: 0 }, + { key: 'c', count: 1 }, + ], + }, + }; + + const expectedSort = { + annotated_sex: { + histogram: [ + { key: 'zorp', count: 4162 }, + { key: 'yellow', count: 99 }, + { key: 'orange', count: 30 }, + { key: 'pink', count: 21 }, + { key: 'blue', count: 0 }, + { key: 'green', count: 0 }, + { key: 'shiny', count: 0 }, + ], + }, + extra_data: { + histogram: [ + { key: 'c', count: 1 }, + { key: 'a', count: 0 }, + { key: 'b', count: 0 }, + ], + }, + }; + + const actualSort = sortTabsOptions(tabsOptionsOne); + + test('test sorting tabs options', async () => { + expect(actualSort) + .toEqual(expectedSort); + }); +});