From 3edf8f45a82a3656b11ebe05e12b20c1eb5bf580 Mon Sep 17 00:00:00 2001 From: Haroen Viaene Date: Fri, 25 Jun 2021 16:12:05 +0200 Subject: [PATCH] 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 --- .../__tests__/connectHierarchicalMenu.js | 154 +++++++++++++++++- .../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, 399 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..1d9d2a9f4b 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,158 @@ 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: [ + { + 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) {