diff --git a/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.ts b/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.ts index c24981ace5..59cc466947 100644 --- a/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.ts +++ b/src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.ts @@ -691,6 +691,166 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hierarchica canToggleShowMore: false, }); }); + + describe('facetOrdering', () => { + const resultsViaFacetOrdering = [ + { + count: 47, + data: null, + exhaustive: true, + isRefined: false, + label: 'Outdoor', + value: 'Outdoor', + }, + { + count: 880, + data: [ + { + count: 173, + data: null, + exhaustive: true, + isRefined: false, + label: 'Frames & pictures', + value: 'Decoration > Frames & pictures', + }, + { + count: 193, + data: null, + exhaustive: true, + isRefined: false, + label: 'Candle holders & candles', + value: 'Decoration > Candle holders & candles', + }, + ], + exhaustive: true, + isRefined: true, + label: 'Decoration', + value: 'Decoration', + }, + ]; + const resultsViaSortBy = [ + { + count: 880, + data: [ + { + count: 193, + data: null, + exhaustive: true, + isRefined: false, + label: 'Candle holders & candles', + value: 'Decoration > Candle holders & candles', + }, + { + count: 173, + data: null, + exhaustive: true, + isRefined: false, + label: 'Frames & pictures', + value: 'Decoration > Frames & pictures', + }, + ], + exhaustive: true, + isRefined: true, + label: 'Decoration', + value: 'Decoration', + }, + { + count: 47, + data: null, + exhaustive: true, + isRefined: false, + label: 'Outdoor', + value: 'Outdoor', + }, + ]; + + test.each` + facetOrderingInResult | sortBy | expected + ${true} | ${undefined} | ${resultsViaFacetOrdering} + ${false} | ${undefined} | ${resultsViaSortBy} + ${true} | ${['name:asc']} | ${resultsViaSortBy} + ${false} | ${['name:asc']} | ${resultsViaSortBy} + `( + 'renderingContent present: $facetOrderingInResult, sortBy: $sortBy', + ({ facetOrderingInResult, sortBy, expected }) => { + const renderFn = jest.fn(); + const unmountFn = jest.fn(); + const createHierarchicalMenu = connectHierarchicalMenu( + renderFn, + unmountFn + ); + const hierarchicalMenu = createHierarchicalMenu({ + attributes: ['category', 'subCategory'], + sortBy, + }); + const helper = algoliasearchHelper( + createSearchClient(), + 'indexName', + hierarchicalMenu.getWidgetSearchParameters!( + new SearchParameters(), + { + uiState: { + hierarchicalMenu: { + category: ['Decoration'], + }, + }, + } + ) + ); + + hierarchicalMenu.init!(createInitOptions({ helper })); + + const renderingContent = facetOrderingInResult + ? { + facetOrdering: { + values: { + category: { + order: ['Outdoor'], + sortRemainingBy: 'alpha' as const, + }, + subCategory: { + order: ['Decoration > Frames & pictures'], + sortRemainingBy: 'count' as const, + }, + }, + }, + } + : undefined; + + const results = new SearchResults(helper.state, [ + createSingleSearchResponse({ + renderingContent, + facets: { + category: { + Decoration: 880, + }, + subCategory: { + 'Decoration > Candle holders & candles': 193, + 'Decoration > Frames & pictures': 173, + }, + }, + }), + createSingleSearchResponse({ + facets: { + category: { + Decoration: 880, + Outdoor: 47, + }, + }, + }), + ]); + + const renderState = hierarchicalMenu.getWidgetRenderState( + createRenderOptions({ + helper, + results, + }) + ); + + expect(renderState.items).toEqual(expected); + } + ); + }); }); describe('getWidgetUiState', () => { diff --git a/src/connectors/hierarchical-menu/connectHierarchicalMenu.ts b/src/connectors/hierarchical-menu/connectHierarchicalMenu.ts index d71d7ebf1c..eb99992882 100644 --- a/src/connectors/hierarchical-menu/connectHierarchicalMenu.ts +++ b/src/connectors/hierarchical-menu/connectHierarchicalMenu.ts @@ -23,6 +23,8 @@ const withUsage = createDocumentationMessageGenerator({ connector: true, }); +const DEFAULT_SORT = ['name:asc']; + export type HierarchicalMenuItem = { /** * Value of the menu item. @@ -79,6 +81,8 @@ export type HierarchicalMenuConnectorParams = { /** * How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`. * You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax). + * + * If a facetOrdering is set in the index settings, it is used when sortBy isn't passed */ sortBy?: SortBy; /** @@ -174,7 +178,7 @@ const connectHierarchicalMenu: HierarchicalMenuConnector = function connectHiera limit = 10, showMore = false, showMoreLimit = 20, - sortBy = ['name:asc'], + sortBy = DEFAULT_SORT, transformItems = (items => items) as TransformItems, } = widgetParams || {}; @@ -273,11 +277,6 @@ const connectHierarchicalMenu: HierarchicalMenuConnector = function connectHiera ); }, - /** - * @param {Object} param0 cleanup arguments - * @param {any} param0.state current search parameters - * @returns {any} next search parameters - */ dispose({ state }) { unmountFn(); @@ -336,6 +335,7 @@ const connectHierarchicalMenu: HierarchicalMenuConnector = function connectHiera if (results) { const facetValues = results.getFacetValues(hierarchicalFacetName, { sortBy, + facetOrdering: sortBy === DEFAULT_SORT, }); const facetItems = facetValues && !Array.isArray(facetValues) && facetValues.data diff --git a/src/connectors/menu/__tests__/connectMenu-test.ts b/src/connectors/menu/__tests__/connectMenu-test.ts index 45bc496ca0..e4a2535c03 100644 --- a/src/connectors/menu/__tests__/connectMenu-test.ts +++ b/src/connectors/menu/__tests__/connectMenu-test.ts @@ -560,7 +560,7 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co }); describe('getWidgetRenderState', () => { - test('returns the widget render state', () => { + test('returns the widget render state (init)', () => { const renderFn = jest.fn(); const unmountFn = jest.fn(); const createMenu = connectMenu(renderFn, unmountFn); @@ -589,6 +589,179 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/menu/js/#co widgetParams: { attribute: 'brand' }, }); }); + + test('returns the widget render state (render)', () => { + const renderFn = jest.fn(); + const unmountFn = jest.fn(); + const createMenu = connectMenu(renderFn, unmountFn); + const menu = createMenu({ + attribute: 'brand', + }); + const helper = jsHelper( + createSearchClient(), + 'indexName', + menu.getWidgetSearchParameters!(new SearchParameters(), { uiState: {} }) + ); + + const renderState1 = menu.getWidgetRenderState( + createRenderOptions({ + helper, + results: new SearchResults(helper.state, [ + createSingleSearchResponse({ + facets: { + brand: { + Apple: 100, + Samsung: 1, + }, + }, + }), + ]), + }) + ); + + expect(renderState1).toEqual({ + items: [ + { + count: 100, + data: null, + exhaustive: true, + isRefined: false, + label: 'Apple', + value: 'Apple', + }, + { + count: 1, + data: null, + exhaustive: true, + isRefined: false, + label: 'Samsung', + value: 'Samsung', + }, + ], + createURL: expect.any(Function), + refine: expect.any(Function), + sendEvent: expect.any(Function), + canRefine: true, + isShowingMore: false, + toggleShowMore: expect.any(Function), + canToggleShowMore: false, + widgetParams: { attribute: 'brand' }, + }); + }); + + describe('facetOrdering', () => { + const resultsViaFacetOrdering = [ + { + count: 1, + data: null, + exhaustive: true, + isRefined: false, + label: 'Samsung', + value: 'Samsung', + }, + { + count: 100, + data: null, + exhaustive: true, + isRefined: false, + label: 'Apple', + value: 'Apple', + }, + { + count: 3, + data: null, + exhaustive: true, + isRefined: false, + label: 'Algolia', + value: 'Algolia', + }, + ]; + const resultsViaSortBy = [ + { + count: 3, + data: null, + exhaustive: true, + isRefined: false, + label: 'Algolia', + value: 'Algolia', + }, + { + count: 100, + data: null, + exhaustive: true, + isRefined: false, + label: 'Apple', + value: 'Apple', + }, + { + count: 1, + data: null, + exhaustive: true, + isRefined: false, + label: 'Samsung', + value: 'Samsung', + }, + ]; + + test.each` + facetOrderingInResult | sortBy | expected + ${true} | ${undefined} | ${resultsViaFacetOrdering} + ${false} | ${undefined} | ${resultsViaSortBy} + ${true} | ${['name:asc']} | ${resultsViaSortBy} + ${false} | ${['name:asc']} | ${resultsViaSortBy} + `( + 'renderingContent present: $facetOrderingInResult, sortBy: $sortBy', + ({ facetOrderingInResult, sortBy, expected }) => { + const renderFn = jest.fn(); + const unmountFn = jest.fn(); + const createMenu = connectMenu(renderFn, unmountFn); + const menu = createMenu({ + attribute: 'brand', + sortBy, + }); + const helper = jsHelper( + createSearchClient(), + 'indexName', + menu.getWidgetSearchParameters!(new SearchParameters(), { + uiState: {}, + }) + ); + + const renderingContent = facetOrderingInResult + ? { + facetOrdering: { + values: { + brand: { + order: ['Samsung'], + sortRemainingBy: 'count' as const, + }, + }, + }, + } + : undefined; + + const renderState1 = menu.getWidgetRenderState( + createRenderOptions({ + helper, + results: new SearchResults(helper.state, [ + createSingleSearchResponse({ + renderingContent, + facets: { + brand: { + Apple: 100, + Algolia: 3, + Samsung: 1, + }, + }, + }), + ]), + }) + ); + + expect(renderState1.items).toEqual(expected); + } + ); + }); }); describe('showMore', () => { diff --git a/src/connectors/menu/connectMenu.ts b/src/connectors/menu/connectMenu.ts index 1fe4bf610e..4ad7148a02 100644 --- a/src/connectors/menu/connectMenu.ts +++ b/src/connectors/menu/connectMenu.ts @@ -19,6 +19,8 @@ const withUsage = createDocumentationMessageGenerator({ connector: true, }); +const DEFAULT_SORT = ['isRefined', 'name:asc']; + export type MenuItem = { /** * The value of the menu item. @@ -59,6 +61,8 @@ export type MenuConnectorParams = { * How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`. * * You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax). + * + * If a facetOrdering is set in the index settings, it is used when sortBy isn't passed */ sortBy?: SortBy; /** @@ -147,7 +151,7 @@ const connectMenu: MenuConnector = function connectMenu( limit = 10, showMore = false, showMoreLimit = 20, - sortBy = ['isRefined', 'name:asc'], + sortBy = DEFAULT_SORT, transformItems = (items => items) as TransformItems, } = widgetParams || {}; @@ -286,6 +290,7 @@ const connectMenu: MenuConnector = function connectMenu( if (results) { const facetValues = results.getFacetValues(attribute, { sortBy, + facetOrdering: sortBy === DEFAULT_SORT, }); const facetItems = facetValues && !Array.isArray(facetValues) && facetValues.data diff --git a/src/connectors/refinement-list/__tests__/connectRefinementList-test.ts b/src/connectors/refinement-list/__tests__/connectRefinementList-test.ts index 6c03aec07c..e14c8ab88b 100644 --- a/src/connectors/refinement-list/__tests__/connectRefinementList-test.ts +++ b/src/connectors/refinement-list/__tests__/connectRefinementList-test.ts @@ -2589,6 +2589,119 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/refinement- }) ); }); + + describe('facetOrdering', () => { + const resultsViaFacetOrdering = [ + { + count: 66, + highlighted: 'Microsoft', + isRefined: false, + label: 'Microsoft', + value: 'Microsoft', + }, + { + count: 88, + highlighted: 'Apple', + isRefined: true, + label: 'Apple', + value: 'Apple', + }, + { + count: 44, + highlighted: 'Samsung', + isRefined: true, + label: 'Samsung', + value: 'Samsung', + }, + ]; + const resultsViaSortBy = [ + { + count: 88, + highlighted: 'Apple', + isRefined: true, + label: 'Apple', + value: 'Apple', + }, + { + count: 44, + highlighted: 'Samsung', + isRefined: true, + label: 'Samsung', + value: 'Samsung', + }, + { + count: 66, + highlighted: 'Microsoft', + isRefined: false, + label: 'Microsoft', + value: 'Microsoft', + }, + ]; + + test.each` + facetOrderingInResult | sortBy | expected + ${true} | ${undefined} | ${resultsViaFacetOrdering} + ${false} | ${undefined} | ${resultsViaSortBy} + ${true} | ${['isRefined']} | ${resultsViaSortBy} + ${false} | ${['isRefined']} | ${resultsViaSortBy} + `( + 'renderingContent present: $facetOrderingInResult, sortBy: $sortBy', + ({ facetOrderingInResult, sortBy, expected }) => { + const renderFn = jest.fn(); + const unmountFn = jest.fn(); + const createRefinementList = connectRefinementList( + renderFn, + unmountFn + ); + const refinementList = createRefinementList({ + attribute: 'brand', + sortBy, + }); + const helper = jsHelper( + createSearchClient(), + 'indexName', + refinementList.getWidgetSearchParameters!(new SearchParameters(), { + uiState: { + refinementList: { brand: ['Apple', 'Samsung'] }, + }, + }) + ); + + const renderingContent = facetOrderingInResult + ? { + facetOrdering: { + values: { + brand: { + order: ['Microsoft'], + sortRemainingBy: 'alpha' as const, + }, + }, + }, + } + : undefined; + + const renderState1 = refinementList.getWidgetRenderState( + createRenderOptions({ + helper, + results: new SearchResults(helper.state, [ + createSingleSearchResponse({ + renderingContent, + facets: { + brand: { + Apple: 88, + Microsoft: 66, + Samsung: 44, + }, + }, + }), + ]), + }) + ); + + expect(renderState1.items).toEqual(expected); + } + ); + }); }); describe('getWidgetSearchParameters', () => { diff --git a/src/connectors/refinement-list/connectRefinementList.ts b/src/connectors/refinement-list/connectRefinementList.ts index 22c5e44c31..5951d9b956 100644 --- a/src/connectors/refinement-list/connectRefinementList.ts +++ b/src/connectors/refinement-list/connectRefinementList.ts @@ -26,6 +26,8 @@ const withUsage = createDocumentationMessageGenerator({ connector: true, }); +const DEFAULT_SORT = ['isRefined', 'count:desc', 'name:asc']; + export type RefinementListItem = { /** * The value of the refinement list item. @@ -74,6 +76,10 @@ export type RefinementListConnectorParams = { showMoreLimit?: number; /** * How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`. + * + * You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax). + * + * If a facetOrdering is set in the index settings, it is used when sortBy isn't passed */ sortBy?: SortBy; /** @@ -182,7 +188,7 @@ const connectRefinementList: RefinementListConnector = function connectRefinemen limit = 10, showMore = false, showMoreLimit = 20, - sortBy = ['isRefined', 'count:desc', 'name:asc'], + sortBy = DEFAULT_SORT, escapeFacetValues = true, transformItems = (items => items) as TransformItems, } = widgetParams || {}; @@ -386,6 +392,7 @@ const connectRefinementList: RefinementListConnector = function connectRefinemen if (results) { const values = results.getFacetValues(attribute, { sortBy, + facetOrdering: sortBy === DEFAULT_SORT, }); facetValues = values && Array.isArray(values) ? values : []; items = transformItems( diff --git a/src/widgets/hierarchical-menu/__tests__/hierarchical-menu-test.ts b/src/widgets/hierarchical-menu/__tests__/hierarchical-menu-test.ts index ea91fbe8c4..0da2df13e4 100644 --- a/src/widgets/hierarchical-menu/__tests__/hierarchical-menu-test.ts +++ b/src/widgets/hierarchical-menu/__tests__/hierarchical-menu-test.ts @@ -133,19 +133,21 @@ See documentation: https://www.algolia.com/doc/api-reference/widgets/hierarchica expect(results.getFacetValues).toHaveBeenCalledTimes(1); expect(results.getFacetValues).toHaveBeenCalledWith('hello', { + facetOrdering: true, sortBy: ['name:asc'], }); }); it('has a sortBy option', () => { - widget = hierarchicalMenu({ ...options, sortBy: ['name:asc'] }); + widget = hierarchicalMenu({ ...options, sortBy: ['name:desc'] }); widget.init!(createInitOptions({ helper })); widget.render!(createRenderOptions({ results, state })); expect(results.getFacetValues).toHaveBeenCalledTimes(1); expect(results.getFacetValues).toHaveBeenCalledWith('hello', { - sortBy: ['name:asc'], + facetOrdering: false, + sortBy: ['name:desc'], }); }); diff --git a/tsconfig.v3.json b/tsconfig.v3.json index f7f443fd37..356a202897 100644 --- a/tsconfig.v3.json +++ b/tsconfig.v3.json @@ -13,6 +13,9 @@ // v3 has a wrong definition for optionalWords (only accepts string[]) "src/connectors/voice-search/__tests__/connectVoiceSearch-test.ts", // v3 does not have renderingContent (only errors in the test) - "src/connectors/dynamic-widgets/__tests__/connectDynamicWidgets-test.ts" + "src/connectors/dynamic-widgets/__tests__/connectDynamicWidgets-test.ts", + "src/connectors/hierarchical-menu/__tests__/connectHierarchicalMenu-test.ts", + "src/connectors/menu/__tests__/connectMenu-test.ts", + "src/connectors/refinement-list/__tests__/connectRefinementList-test.ts" ] }