diff --git a/docs/search-headless.querystate.ispagination.md b/docs/search-headless.querystate.ispagination.md new file mode 100644 index 0000000..bb32a33 --- /dev/null +++ b/docs/search-headless.querystate.ispagination.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/search-headless](./search-headless.md) > [QueryState](./search-headless.querystate.md) > [isPagination](./search-headless.querystate.ispagination.md) + +## QueryState.isPagination property + +Whether the next query represents a pagination - in which case queryId will be maintained + +Signature: + +```typescript +isPagination?: boolean; +``` diff --git a/docs/search-headless.querystate.md b/docs/search-headless.querystate.md index 78cde84..3bd35e3 100644 --- a/docs/search-headless.querystate.md +++ b/docs/search-headless.querystate.md @@ -17,6 +17,7 @@ export interface QueryState | Property | Type | Description | | --- | --- | --- | | [input?](./search-headless.querystate.input.md) | string | (Optional) The user input used for the next search query. | +| [isPagination?](./search-headless.querystate.ispagination.md) | boolean | (Optional) Whether the next query represents a pagination - in which case queryId will be maintained | | [mostRecentSearch?](./search-headless.querystate.mostrecentsearch.md) | string | (Optional) The query of the most recent search. | | [queryId?](./search-headless.querystate.queryid.md) | string | (Optional) The ID of the query from the latest search. | | [querySource?](./search-headless.querystate.querysource.md) | [QuerySource](./search-headless.querysource.md) | (Optional) The source of the query (from a standard Search integration, a Search overlay, or from visual autocomplete). | diff --git a/docs/search-headless.searchheadless.md b/docs/search-headless.searchheadless.md index 4eb2e96..4440b07 100644 --- a/docs/search-headless.searchheadless.md +++ b/docs/search-headless.searchheadless.md @@ -41,6 +41,7 @@ export default class SearchHeadless | [setFacetOption(fieldId, facetOption, selected)](./search-headless.searchheadless.setfacetoption.md) | | Sets a specified facet option to be selected or unselected. | | [setFacets(facets)](./search-headless.searchheadless.setfacets.md) | | Sets [FiltersState.facets](./search-headless.filtersstate.facets.md) to the specified facets. | | [setFilterOption(filter)](./search-headless.searchheadless.setfilteroption.md) | | Sets a static filter option and whether or not it is selected in state. | +| [setIsPagination(input)](./search-headless.searchheadless.setispagination.md) | | Sets [QueryState.isPagination](./search-headless.querystate.ispagination.md) to the specified input. | | [setLocationRadius(locationRadius)](./search-headless.searchheadless.setlocationradius.md) | | Sets [VerticalSearchState.locationRadius](./search-headless.verticalsearchstate.locationradius.md) to the specified number of meters. | | [setOffset(offset)](./search-headless.searchheadless.setoffset.md) | | Sets [VerticalSearchState.offset](./search-headless.verticalsearchstate.offset.md) to the specified offset. | | [setQuery(input)](./search-headless.searchheadless.setquery.md) | | Sets [QueryState.input](./search-headless.querystate.input.md) to the specified input. | diff --git a/docs/search-headless.searchheadless.setispagination.md b/docs/search-headless.searchheadless.setispagination.md new file mode 100644 index 0000000..9a071bd --- /dev/null +++ b/docs/search-headless.searchheadless.setispagination.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [@yext/search-headless](./search-headless.md) > [SearchHeadless](./search-headless.searchheadless.md) > [setIsPagination](./search-headless.searchheadless.setispagination.md) + +## SearchHeadless.setIsPagination() method + +Sets [QueryState.isPagination](./search-headless.querystate.ispagination.md) to the specified input. + +Signature: + +```typescript +setIsPagination(input: boolean): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| input | boolean | The input to set | + +Returns: + +void + diff --git a/etc/search-headless.api.md b/etc/search-headless.api.md index da54ff0..e2fe627 100644 --- a/etc/search-headless.api.md +++ b/etc/search-headless.api.md @@ -654,6 +654,7 @@ export enum QuerySource { // @public export interface QueryState { input?: string; + isPagination?: boolean; mostRecentSearch?: string; queryId?: string; querySource?: QuerySource; @@ -780,6 +781,7 @@ export class SearchHeadless { setFacetOption(fieldId: string, facetOption: FacetOption, selected: boolean): void; setFacets(facets: DisplayableFacet[]): void; setFilterOption(filter: SelectableStaticFilter): void; + setIsPagination(input: boolean): void; setLocationRadius(locationRadius: number | undefined): void; setOffset(offset: number): void; setQuery(input: string): void; diff --git a/package-lock.json b/package-lock.json index 2da3ed3..1ec8cc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@yext/search-headless", - "version": "2.5.0", + "version": "2.5.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@yext/search-headless", - "version": "2.5.0", + "version": "2.5.1", "license": "BSD-3-Clause", "dependencies": { "@reduxjs/toolkit": "^1.8.1", diff --git a/package.json b/package.json index ab66c19..c571822 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@yext/search-headless", - "version": "2.5.0", + "version": "2.5.1", "description": "A library for powering UI components for Yext Search integrations", "author": "slapshot@yext.com", "license": "BSD-3-Clause", diff --git a/src/models/slices/query.ts b/src/models/slices/query.ts index d114171..760faab 100644 --- a/src/models/slices/query.ts +++ b/src/models/slices/query.ts @@ -30,5 +30,9 @@ export interface QueryState { /** * The computed intents of the mostRecentSearch, as returned by the Search API. */ - searchIntents?: SearchIntent[] -} \ No newline at end of file + searchIntents?: SearchIntent[], + /** + * Whether the next query represents a pagination - in which case queryId will be maintained + */ + isPagination?: boolean +} diff --git a/src/search-headless.ts b/src/search-headless.ts index 6d98134..3af535c 100644 --- a/src/search-headless.ts +++ b/src/search-headless.ts @@ -55,6 +55,15 @@ export default class SearchHeadless { private additionalHttpHeaders?: AdditionalHttpHeaders ) {} + /** + * Sets {@link QueryState.isPagination} to the specified input. + * + * @param input - The input to set + */ + setIsPagination(input: boolean): void { + this.stateManager.dispatchEvent('query/setIsPagination', input); + } + /** * Sets {@link QueryState.input} to the specified input. * @@ -402,7 +411,7 @@ export default class SearchHeadless { return; } this.stateManager.dispatchEvent('searchStatus/setIsLoading', true); - const { input, querySource, queryTrigger } = this.state.query; + const { input, isPagination, queryId, querySource, queryTrigger } = this.state.query; const skipSpellCheck = !this.state.spellCheck.enabled; const sessionTrackingEnabled = this.state.sessionTracking.enabled; const sessionId = this.state.sessionTracking.sessionId; @@ -411,6 +420,7 @@ export default class SearchHeadless { const { limit, offset, sortBys, locationRadius } = this.state.vertical; const { referrerPageUrl, context } = this.state.meta; const { userLocation } = this.state.location; + const nextQueryId = isPagination ? queryId : undefined; const facetsToApply = facets?.map(facet => { return { @@ -437,6 +447,7 @@ export default class SearchHeadless { context, referrerPageUrl, locationRadius, + queryId: nextQueryId, additionalHttpHeaders: this.additionalHttpHeaders }; @@ -457,6 +468,7 @@ export default class SearchHeadless { } this.stateManager.dispatchEvent('query/setQueryId', response.queryId); this.stateManager.dispatchEvent('query/setMostRecentSearch', input); + this.stateManager.dispatchEvent('query/setIsPagination', false); this.stateManager.dispatchEvent('filters/setFacets', response.facets); this.stateManager.dispatchEvent('spellCheck/setResult', response.spellCheck); this.stateManager.dispatchEvent('query/setSearchIntents', response.searchIntents || []); diff --git a/src/slices/query.ts b/src/slices/query.ts index 84fb2f4..cc50078 100644 --- a/src/slices/query.ts +++ b/src/slices/query.ts @@ -8,6 +8,9 @@ const reducers = { setInput: (state, action: PayloadAction) => { state.input = action.payload; }, + setIsPagination: (state, action: PayloadAction) => { + state.isPagination = action.payload; + }, setTrigger: (state, action: PayloadAction) => { state.queryTrigger = action.payload; }, diff --git a/tests/integration/verticalsearch.ts b/tests/integration/verticalsearch.ts index bddabaa..cb3a92a 100644 --- a/tests/integration/verticalsearch.ts +++ b/tests/integration/verticalsearch.ts @@ -169,6 +169,23 @@ it('answers.setVerticalLimit sets the vertical limit when a number is passed to expect(answers.state.vertical.limit).toEqual(7); }); +it('vertical searches re-use queryId when isPagination is true', async () => { + const answers = createMockedHeadless( { verticalSearch: createMockSearch() }, initialState); + await answers.executeVerticalQuery(); + const firstQueryId = answers.state.query.queryId; + answers.setIsPagination(true); + await answers.executeVerticalQuery(); + expect(answers.state.query.queryId).toEqual(firstQueryId); +}); + +it('vertical searches change queryId when isPagination is not true', async () => { + const answers = createMockedHeadless( { verticalSearch: createMockSearch() }, initialState); + await answers.executeVerticalQuery(); + const firstQueryId = answers.state.query.queryId; + await answers.executeVerticalQuery(); + expect(answers.state.query.queryId).not.toEqual(firstQueryId); +}); + it('handle a rejected promise from core', async () => { const mockSearch = createMockRejectedSearch(); const mockCore = { verticalSearch: mockSearch }; @@ -199,7 +216,11 @@ it('executeVerticalQuery passes the additional HTTP headers', async () => { function createMockSearch() { return jest.fn(async (_request: VerticalSearchRequest) => { await setTimeout(0); - return Promise.resolve({}); + return Promise.resolve({ + // preserves queryId if passed-in, else generates a random string + queryId: _request.queryId == undefined ? + (Math.random()).toString(36).substring(2) : _request.queryId + }); }); } @@ -208,4 +229,4 @@ function createMockRejectedSearch() { await setTimeout(0); return Promise.reject('mock error message'); }); -} \ No newline at end of file +} diff --git a/tests/unit/slices/query.ts b/tests/unit/slices/query.ts index 97c2966..622d692 100644 --- a/tests/unit/slices/query.ts +++ b/tests/unit/slices/query.ts @@ -2,7 +2,7 @@ import { QuerySource, QueryTrigger } from '@yext/search-core'; import createQuerySlice from '../../../src/slices/query'; const { reducer, actions } = createQuerySlice(''); -const { setInput, setQueryId, setSource, setTrigger } = actions; +const { setInput, setIsPagination, setQueryId, setSource, setTrigger } = actions; describe('query slice reducer works as expected', () => { it('setQuery action is handled properly', () => { @@ -13,6 +13,14 @@ describe('query slice reducer works as expected', () => { expect(actualState).toEqual(expectedState); }); + it('setIsPagination action is handled properly', () => { + const isPagination = true; + const expectedState = { isPagination }; + const actualState = reducer({}, setIsPagination(isPagination)); + + expect(actualState).toEqual(expectedState); + }); + it('setTrigger action is handled properly', () => { const queryTrigger = QueryTrigger.Initialize; const expectedState = { queryTrigger }; @@ -36,4 +44,4 @@ describe('query slice reducer works as expected', () => { expect(actualState).toEqual(expectedState); }); -}); \ No newline at end of file +});