From 731d9ba01f3dcf28afa63ca2faf40c7701a08860 Mon Sep 17 00:00:00 2001 From: Haroen Viaene Date: Mon, 5 Jul 2021 14:41:01 +0200 Subject: [PATCH] feat(facets): add a new option "facetOrdering" to Menu, RefinementList & HierarchicalMenu (#3067) * feat(facetOrdering): add a new option "facetOrdering" to Menu, RefinementList & HierarchicalMenu If `facetOrdering` is enabled (the default behaviour), before the default sortBy is used, the result from renderingContent.facetOrdering.values is first checked. If that's present, it will be used to sort the items. You can still change that ordering afterwards with the existing transformItems, so if you are sorting in transformItems, you actually override the sorting done by facet ordering, and won't see the effect. To use facetOrdering, you thus need to remove any sorting done in transformItems. If there is a facetOrdering present in the index, but you don't want to use it for a certain widget, you need to explicitly pass `facetOrdering: false` to the widget or connector References: - [RFC 45](https://github.com/algolia/instantsearch-rfcs/blob/master/accepted/flexible-facet-values.md) - https://github.com/algolia/instantsearch.js/pull/4784 - https://github.com/algolia/algoliasearch-helper-js/pull/822 * clarify --- .../__tests__/connectHierarchicalMenu.js | 155 +++++++++++++++++- .../src/connectors/__tests__/connectMenu.js | 123 +++++++++++++- .../__tests__/connectRefinementList.js | 112 ++++++++++++- .../src/connectors/connectHierarchicalMenu.js | 6 +- .../src/connectors/connectMenu.js | 5 +- .../src/connectors/connectRefinementList.js | 6 +- .../src/core/indexUtils.js | 3 + 7 files changed, 400 insertions(+), 10 deletions(-) diff --git a/packages/react-instantsearch-core/src/connectors/__tests__/connectHierarchicalMenu.js b/packages/react-instantsearch-core/src/connectors/__tests__/connectHierarchicalMenu.js index b49fbb5232..4f44c4f4cc 100644 --- a/packages/react-instantsearch-core/src/connectors/__tests__/connectHierarchicalMenu.js +++ b/packages/react-instantsearch-core/src/connectors/__tests__/connectHierarchicalMenu.js @@ -1,4 +1,4 @@ -import { SearchParameters } from 'algoliasearch-helper'; +import { SearchResults, SearchParameters } from 'algoliasearch-helper'; import connect from '../connectHierarchicalMenu'; jest.mock('../../core/createConnector', () => x => x); @@ -174,6 +174,159 @@ describe('connectHierarchicalMenu', () => { expect(props.items).toEqual(['items']); }); + it('facetValues results uses facetOrdering by default', () => { + const props = { + ...connect.defaultProps, + attributes: ['lvl0', 'lvl1'], + contextValue, + }; + const searchState = { hierarchicalMenu: { lvl0: 'wat' } }; + const state = connect.getSearchParameters( + new SearchParameters(), + props, + searchState + ); + const results = new SearchResults(state, [ + { + hits: [], + renderingContent: { + facetOrdering: { + values: { + lvl0: { + order: ['wat'], + }, + lvl1: { + order: ['wat > wut'], + }, + }, + }, + }, + facets: { + lvl0: { + wat: 20, + oy: 10, + }, + lvl1: { + 'wat > wot': 15, + 'wat > wut': 5, + }, + }, + }, + ]); + + const providedProps = connect.getProvidedProps(props, searchState, { + results, + }); + expect(providedProps.items).toEqual([ + { + label: 'wat', + value: undefined, + count: 20, + isRefined: true, + items: [ + { + label: 'wut', + value: 'wat > wut', + count: 5, + isRefined: false, + items: null, + }, + { + label: 'wot', + value: 'wat > wot', + count: 15, + isRefined: false, + items: null, + }, + ], + }, + { + label: 'oy', + value: 'oy', + count: 10, + isRefined: false, + items: null, + }, + ]); + }); + + it('facetValues results does not use facetOrdering if disabled', () => { + const props = { + attributes: ['lvl0', 'lvl1'], + facetOrdering: false, + contextValue, + }; + const searchState = { hierarchicalMenu: { lvl0: 'wat' } }; + const state = connect.getSearchParameters( + new SearchParameters(), + props, + searchState + ); + const results = new SearchResults(state, [ + { + hits: [], + renderingContent: { + facetOrdering: { + values: { + lvl0: { + order: ['wat'], + }, + lvl1: { + order: ['wat > wut'], + }, + }, + }, + }, + facets: { + lvl0: { + wat: 20, + oy: 10, + }, + lvl1: { + 'wat > wot': 15, + 'wat > wut': 5, + }, + }, + }, + ]); + + const providedProps = connect.getProvidedProps(props, searchState, { + results, + }); + expect(providedProps.items).toEqual([ + { + label: 'oy', + value: 'oy', + count: 10, + isRefined: false, + items: null, + }, + { + label: 'wat', + value: undefined, + count: 20, + isRefined: true, + items: [ + // default ordering: alphabetical + { + label: 'wot', + value: 'wat > wot', + count: 15, + isRefined: false, + items: null, + }, + { + label: 'wut', + value: 'wat > wut', + count: 5, + isRefined: false, + items: null, + }, + ], + }, + ]); + }); + it('shows the effect of showMoreLimit when there is no transformItems', () => { const results = { getFacetValues: jest.fn(), diff --git a/packages/react-instantsearch-core/src/connectors/__tests__/connectMenu.js b/packages/react-instantsearch-core/src/connectors/__tests__/connectMenu.js index 263e87bfd1..28925a4215 100644 --- a/packages/react-instantsearch-core/src/connectors/__tests__/connectMenu.js +++ b/packages/react-instantsearch-core/src/connectors/__tests__/connectMenu.js @@ -1,4 +1,4 @@ -import { SearchParameters } from 'algoliasearch-helper'; +import { SearchParameters, SearchResults } from 'algoliasearch-helper'; import connect from '../connectMenu'; jest.mock('../../core/createConnector', () => x => x); @@ -245,6 +245,116 @@ describe('connectMenu', () => { ]); }); + it('facetValues have facetOrdering by default', () => { + const userProps = { + ...connect.defaultProps, + attribute: 'ok', + contextValue, + }; + const searchState = { + menu: { ok: 'wat' }, + }; + const parameters = connect.getSearchParameters( + new SearchParameters(), + userProps, + searchState + ); + + const searchResults = new SearchResults(parameters, [ + { + hits: [], + renderingContent: { + facetOrdering: { + values: { + ok: { + order: ['wat'], + }, + }, + }, + }, + facets: { + ok: { + wat: 20, + lol: 2000, + }, + }, + }, + ]); + + const providedProps = connect.getProvidedProps(userProps, searchState, { + results: searchResults, + }); + + expect(providedProps.items).toEqual([ + { + count: 20, + isRefined: true, + label: 'wat', + value: '', + }, + { + count: 2000, + isRefined: false, + label: 'lol', + value: 'lol', + }, + ]); + expect(providedProps.isFromSearch).toBe(false); + }); + + it('facetValues results does not use facetOrdering if disabled', () => { + const userProps = { attribute: 'ok', facetOrdering: false, contextValue }; + const searchState = { + menu: { ok: 'wat' }, + }; + const parameters = connect.getSearchParameters( + new SearchParameters(), + userProps, + searchState + ); + + const searchResults = new SearchResults(parameters, [ + { + hits: [], + renderingContent: { + facetOrdering: { + values: { + ok: { + order: ['wat'], + }, + }, + }, + }, + facets: { + ok: { + wat: 20, + lol: 2000, + }, + }, + }, + ]); + + const providedProps = connect.getProvidedProps(userProps, searchState, { + results: searchResults, + }); + + expect(providedProps.items).toEqual([ + { + count: 2000, + isRefined: false, + label: 'lol', + value: 'lol', + }, + { + count: 20, + isRefined: true, + label: 'wat', + value: '', + }, + ]); + expect(providedProps.isFromSearch).toBe(false); + }); + it("calling refine updates the widget's search state", () => { const nextState = connect.refine( { attribute: 'ok', contextValue }, @@ -435,13 +545,14 @@ describe('connectMenu', () => { }; props = connect.getProvidedProps( - { attribute: 'ok', contextValue }, + { ...connect.defaultProps, attribute: 'ok', contextValue }, {}, { results } ); expect(results.getFacetValues).toHaveBeenCalledWith('ok', { sortBy: ['count:desc', 'name:asc'], + facetOrdering: true, }); expect(props.items).toEqual([ @@ -479,13 +590,19 @@ describe('connectMenu', () => { }; props = connect.getProvidedProps( - { attribute: 'ok', searchable: true, contextValue }, + { + ...connect.defaultProps, + attribute: 'ok', + searchable: true, + contextValue, + }, {}, { results } ); expect(results.getFacetValues).toHaveBeenCalledWith('ok', { sortBy: undefined, + facetOrdering: true, }); expect(props.items).toEqual([ diff --git a/packages/react-instantsearch-core/src/connectors/__tests__/connectRefinementList.js b/packages/react-instantsearch-core/src/connectors/__tests__/connectRefinementList.js index 21421bead3..93db3a16b0 100644 --- a/packages/react-instantsearch-core/src/connectors/__tests__/connectRefinementList.js +++ b/packages/react-instantsearch-core/src/connectors/__tests__/connectRefinementList.js @@ -1,4 +1,4 @@ -import { SearchParameters } from 'algoliasearch-helper'; +import { SearchResults, SearchParameters } from 'algoliasearch-helper'; import connect from '../connectRefinementList'; jest.mock('../../core/createConnector', () => x => x); @@ -241,6 +241,116 @@ describe('connectRefinementList', () => { expect(props.isFromSearch).toBe(true); }); + it('facetValues have facetOrdering by default', () => { + const userProps = { + ...connect.defaultProps, + attribute: 'ok', + contextValue, + }; + const searchState = { + refinementList: { ok: ['wat'] }, + }; + const parameters = connect.getSearchParameters( + new SearchParameters(), + userProps, + searchState + ); + + const searchResults = new SearchResults(parameters, [ + { + hits: [], + renderingContent: { + facetOrdering: { + values: { + ok: { + order: ['lol'], + }, + }, + }, + }, + facets: { + ok: { + wat: 20, + lol: 2000, + }, + }, + }, + ]); + + const providedProps = connect.getProvidedProps(userProps, searchState, { + results: searchResults, + }); + + expect(providedProps.items).toEqual([ + { + count: 2000, + isRefined: false, + label: 'lol', + value: ['wat', 'lol'], + }, + { + count: 20, + isRefined: true, + label: 'wat', + value: [], + }, + ]); + expect(providedProps.isFromSearch).toBe(false); + }); + + it('facetValues results does not use facetOrdering if disabled', () => { + const userProps = { attribute: 'ok', facetOrdering: false, contextValue }; + const searchState = { + refinementList: { ok: ['wat'] }, + }; + const parameters = connect.getSearchParameters( + new SearchParameters(), + userProps, + searchState + ); + + const searchResults = new SearchResults(parameters, [ + { + hits: [], + renderingContent: { + facetOrdering: { + values: { + ok: { + order: ['lol'], + }, + }, + }, + }, + facets: { + ok: { + wat: 20, + lol: 2000, + }, + }, + }, + ]); + + const providedProps = connect.getProvidedProps(userProps, searchState, { + results: searchResults, + }); + + expect(providedProps.items).toEqual([ + { + count: 20, + isRefined: true, + label: 'wat', + value: [], + }, + { + count: 2000, + isRefined: false, + label: 'lol', + value: ['wat', 'lol'], + }, + ]); + expect(providedProps.isFromSearch).toBe(false); + }); + it("calling refine updates the widget's search state", () => { const nextState = connect.refine( { attribute: 'ok', contextValue }, diff --git a/packages/react-instantsearch-core/src/connectors/connectHierarchicalMenu.js b/packages/react-instantsearch-core/src/connectors/connectHierarchicalMenu.js index 3abc77555f..65674dc68e 100644 --- a/packages/react-instantsearch-core/src/connectors/connectHierarchicalMenu.js +++ b/packages/react-instantsearch-core/src/connectors/connectHierarchicalMenu.js @@ -161,6 +161,7 @@ export default createConnector({ limit: PropTypes.number, showMoreLimit: PropTypes.number, transformItems: PropTypes.func, + facetOrdering: PropTypes.bool, }, defaultProps: { @@ -170,10 +171,11 @@ export default createConnector({ separator: ' > ', rootPath: null, showParentLevel: true, + facetOrdering: true, }, getProvidedProps(props, searchState, searchResults) { - const { showMore, limit, showMoreLimit } = props; + const { showMore, limit, showMoreLimit, facetOrdering } = props; const id = getId(props); const results = getResults(searchResults, { @@ -194,7 +196,7 @@ export default createConnector({ }; } const itemsLimit = showMore ? showMoreLimit : limit; - const value = results.getFacetValues(id, { sortBy }); + const value = results.getFacetValues(id, { sortBy, facetOrdering }); const items = value.data ? transformValue(value.data, props, searchState, { ais: props.contextValue, diff --git a/packages/react-instantsearch-core/src/connectors/connectMenu.js b/packages/react-instantsearch-core/src/connectors/connectMenu.js index 4b67d2d6ea..d3f1e7def9 100644 --- a/packages/react-instantsearch-core/src/connectors/connectMenu.js +++ b/packages/react-instantsearch-core/src/connectors/connectMenu.js @@ -82,12 +82,14 @@ export default createConnector({ defaultRefinement: PropTypes.string, transformItems: PropTypes.func, searchable: PropTypes.bool, + facetOrdering: PropTypes.bool, }, defaultProps: { showMore: false, limit: 10, showMoreLimit: 20, + facetOrdering: true, }, getProvidedProps( @@ -97,7 +99,7 @@ export default createConnector({ meta, searchForFacetValuesResults ) { - const { attribute, searchable, indexContextValue } = props; + const { attribute, searchable, indexContextValue, facetOrdering } = props; const results = getResults(searchResults, { ais: props.contextValue, multiIndexContext: props.indexContextValue, @@ -149,6 +151,7 @@ export default createConnector({ items = results .getFacetValues(attribute, { sortBy: searchable ? undefined : defaultSortBy, + facetOrdering, }) .map(v => ({ label: v.name, diff --git a/packages/react-instantsearch-core/src/connectors/connectRefinementList.js b/packages/react-instantsearch-core/src/connectors/connectRefinementList.js index 6d4159589a..99cbe1ea07 100644 --- a/packages/react-instantsearch-core/src/connectors/connectRefinementList.js +++ b/packages/react-instantsearch-core/src/connectors/connectRefinementList.js @@ -104,6 +104,7 @@ export default createConnector({ ), searchable: PropTypes.bool, transformItems: PropTypes.func, + facetOrdering: PropTypes.bool, }, defaultProps: { @@ -111,6 +112,7 @@ export default createConnector({ showMore: false, limit: 10, showMoreLimit: 20, + facetOrdering: true, }, getProvidedProps( @@ -120,7 +122,7 @@ export default createConnector({ metadata, searchForFacetValuesResults ) { - const { attribute, searchable, indexContextValue } = props; + const { attribute, searchable, indexContextValue, facetOrdering } = props; const results = getResults(searchResults, { ais: props.contextValue, multiIndexContext: props.indexContextValue, @@ -167,7 +169,7 @@ export default createConnector({ count: v.count, isRefined: v.isRefined, })) - : results.getFacetValues(attribute, { sortBy }).map(v => ({ + : results.getFacetValues(attribute, { sortBy, facetOrdering }).map(v => ({ label: v.name, value: getValue(v.name, props, searchState, { ais: props.contextValue, diff --git a/packages/react-instantsearch-core/src/core/indexUtils.js b/packages/react-instantsearch-core/src/core/indexUtils.js index f6d02837c3..e5401c572b 100644 --- a/packages/react-instantsearch-core/src/core/indexUtils.js +++ b/packages/react-instantsearch-core/src/core/indexUtils.js @@ -6,6 +6,9 @@ export function getIndexId(context) { : context.ais.mainTargetedIndex; } +/** + * @returns {import('algoliasearch-helper').SearchResults} results + */ export function getResults(searchResults, context) { if (searchResults.results) { if (searchResults.results.hits) {