diff --git a/packages/x-components/src/x-modules/search/events.types.ts b/packages/x-components/src/x-modules/search/events.types.ts index 267884e35d..28d1a9ccdd 100644 --- a/packages/x-components/src/x-modules/search/events.types.ts +++ b/packages/x-components/src/x-modules/search/events.types.ts @@ -1,11 +1,11 @@ import { + Banner, Facet, + Promoted, + Redirection, Result, Sort, - Redirection, - TaggingRequest, - Promoted, - Banner + TaggingRequest } from '@empathyco/x-types'; import { InternalSearchRequest, InternalSearchResponse } from './types'; @@ -26,6 +26,10 @@ export interface SearchXEvents { * Payload: The new page number. */ PageChanged: number; + /** + * Reload the current search has been requested. + */ + ReloadSearchRequested: undefined; /** * Results have been changed. * Payload: The new {@link @empathyco/x-types#Result | results}. diff --git a/packages/x-components/src/x-modules/search/store/__tests__/actions.spec.ts b/packages/x-components/src/x-modules/search/store/__tests__/actions.spec.ts index d89afad018..19e5630f9e 100644 --- a/packages/x-components/src/x-modules/search/store/__tests__/actions.spec.ts +++ b/packages/x-components/src/x-modules/search/store/__tests__/actions.spec.ts @@ -1,17 +1,18 @@ import { createLocalVue } from '@vue/test-utils'; import Vuex, { Store, StoreOptions } from 'vuex'; -import { getBannersStub } from '../../../../__stubs__/banners-stubs.factory'; -//eslint-disable-next-line max-len -import { getEmptySearchResponseStub } from '../../../../__stubs__/empty-search-response-stubs.factory'; -import { getFacetsStub } from '../../../../__stubs__/facets-stubs.factory'; -import { getPartialResultsStub } from '../../../../__stubs__/partials-results-stubs.factory'; -import { getPromotedsStub } from '../../../../__stubs__/promoteds-stubs.factory'; -import { getRedirectionsStub } from '../../../../__stubs__/redirections-stubs.factory'; -import { getResultsStub } from '../../../../__stubs__/results-stubs.factory'; -import { getSearchResponseStub } from '../../../../__stubs__/search-response-stubs.factory'; +import { + getBannersStub, + getEmptySearchResponseStub, + getFacetsStub, + getPartialResultsStub, + getPromotedsStub, + getRedirectionsStub, + getResultsStub, + getSearchResponseStub +} from '../../../../__stubs__'; import { getMockedAdapter, installNewXPlugin } from '../../../../__tests__/utils'; import { SafeStore } from '../../../../store/__tests__/utils'; -import { UrlParams } from '../../../../types/url-params'; +import { UrlParams } from '../../../../types'; import { searchXStoreModule } from '../module'; import { SearchActions, SearchGetters, SearchMutations, SearchState } from '../types'; import { resetSearchStateWith } from './utils'; @@ -682,4 +683,110 @@ describe('testing search module actions', () => { expect(store.state.origin).toBeNull(); }); }); + + describe('reloadSearch', () => { + it('should include the origin, start and rows properties in the request', async () => { + resetSearchStateWith(store, { query: 'lego', origin: 'search_box:external' }); + const { page, ...restRequest } = store.getters.request!; + await store.dispatch('reloadSearch'); + + expect(adapter.search).toHaveBeenCalledTimes(1); + expect(adapter.search).toHaveBeenCalledWith({ + ...restRequest, + origin: 'search_box:external', + start: 0, + rows: 24 + }); + }); + + it('should calculate correctly the start and rows properties', async () => { + resetSearchStateWith(store, { + config: { pageSize: 48 }, + page: 2, + query: 'lego', + results: getResultsStub(48) + }); + const { page, ...restRequest } = store.getters.request!; + await store.dispatch('reloadSearch'); + + expect(adapter.search).toHaveBeenCalledTimes(1); + expect(adapter.search).toHaveBeenCalledWith({ + ...restRequest, + start: 0, + rows: 48 + }); + }); + + it('should request and store total results in the state', async () => { + resetSearchStateWith(store, { + query: 'lego' + }); + + adapter.search.mockResolvedValueOnce({ + ...emptySearchResponseStub, + totalResults: 116 + }); + + const actionPromise = store.dispatch('reloadSearch'); + await actionPromise; + expect(store.state.totalResults).toBe(116); + }); + + it('should clear the total results in the state', async () => { + resetSearchStateWith(store, { + query: '' + }); + const actionPromise = store.dispatch('reloadSearch'); + await actionPromise; + expect(store.state.totalResults).toBe(0); + }); + + it('should cancel the previous request if it is not yet resolved', async () => { + resetSearchStateWith(store, { query: 'beer' }); + const { + results: initialResults, + facets: initialFacets, + banners: initialBanners, + promoteds: initialPromoteds, + redirections: initialRedirections + } = store.state; + adapter.search.mockResolvedValueOnce({ + ...emptySearchResponseStub, + results: resultsStub.slice(0, 1), + facets: facetsStub.slice(0, 1) + }); + + const firstRequest = store.dispatch('reloadSearch'); + const secondRequest = store.dispatch('reloadSearch'); + + await firstRequest; + expect(store.state.status).toEqual('loading'); + expect(store.state.results).toBe(initialResults); + expect(store.state.facets).toBe(initialFacets); + expect(store.state.banners).toEqual(initialBanners); + expect(store.state.promoteds).toEqual(initialPromoteds); + expect(store.state.promoteds).toEqual(initialPromoteds); + expect(store.state.redirections).toEqual(initialRedirections); + await secondRequest; + expect(store.state.status).toEqual('success'); + expect(store.state.results).toEqual(resultsStub); + expect(store.state.facets).toEqual(facetsStub); + expect(store.state.banners).toEqual(bannersStub); + expect(store.state.promoteds).toEqual(promotedsStub); + expect(store.state.redirections).toEqual(redirectionsStub); + }); + + it('should set the status to error when it fails', async () => { + resetSearchStateWith(store, { query: 'lego' }); + adapter.search.mockRejectedValueOnce('Generic error'); + const { results, facets, banners, promoteds } = store.state; + await store.dispatch('reloadSearch'); + + expect(store.state.results).toBe(results); + expect(store.state.facets).toBe(facets); + expect(store.state.banners).toBe(banners); + expect(store.state.promoteds).toBe(promoteds); + expect(store.state.status).toEqual('error'); + }); + }); }); diff --git a/packages/x-components/src/x-modules/search/store/actions/index.ts b/packages/x-components/src/x-modules/search/store/actions/index.ts index 8acf1d7315..1e93174daf 100644 --- a/packages/x-components/src/x-modules/search/store/actions/index.ts +++ b/packages/x-components/src/x-modules/search/store/actions/index.ts @@ -1,6 +1,7 @@ export * from './fetch-and-save-search-response.action'; export * from './fetch-search-response.action'; export * from './increase-page-apending-results.action'; +export * from './reload-search.action'; export * from './reset-request-on-refinement.action'; export { saveOrigin as saveSearchOrigin } from './save-origin.action'; export * from './save-search-response.action'; diff --git a/packages/x-components/src/x-modules/search/store/actions/reload-search.action.ts b/packages/x-components/src/x-modules/search/store/actions/reload-search.action.ts new file mode 100644 index 0000000000..2ed1d3029d --- /dev/null +++ b/packages/x-components/src/x-modules/search/store/actions/reload-search.action.ts @@ -0,0 +1,64 @@ +import { SearchRequest, SearchResponse } from '@empathyco/x-types'; +import { createFetchAndSaveActions } from '../../../../store'; +import { InternalSearchRequest } from '../../types'; +import { SearchActionContext, SearchState } from '../types'; + +const { fetchAndSave, cancelPrevious } = createFetchAndSaveActions< + SearchActionContext, + InternalSearchRequest | null, + SearchResponse | null +>({ + fetch({ dispatch, state }, request) { + return request + ? dispatch('fetchSearchResponse', enrichRequest(request, state)) + : Promise.resolve(null); + }, + onSuccess({ dispatch }, response) { + if (response !== null) { + dispatch('saveSearchResponse', response); + } + } +}); + +/** + * Enriches the {@link SearchRequest} object with the origin and page properties taken from the + * {@link SearchState | search state}. + * + * @param request - The {@link InternalSearchRequest}. + * @param state - {@link SearchState}. + * + * @returns The search request. + * @internal + */ +function enrichRequest(request: InternalSearchRequest, state: SearchState): SearchRequest { + const { page, ...restRequest } = request; + const { + config: { pageSize }, + origin + } = state; + + return { + ...restRequest, + ...(origin && { origin }), + start: 0, + rows: pageSize + }; +} + +/** + * Default implementation for {@link SearchActions.fetchAndSaveSearchResponse} action. + * + * @param context - The {@link https://vuex.vuejs.org/guide/actions.html | context} of the + * actions, provided by Vuex. + * @returns A promise that resolves after saving the response. + * @public + */ +export const reloadSearch = (context: SearchActionContext) => + fetchAndSave(context, context.getters.request); + +/** + * Default implementation for {@link SearchActions.cancelFetchAndSaveSearchResponse} action. + * + * @public + */ +export const cancelReloadSearch = cancelPrevious; diff --git a/packages/x-components/src/x-modules/search/store/module.ts b/packages/x-components/src/x-modules/search/store/module.ts index 2da7948952..3874d37378 100644 --- a/packages/x-components/src/x-modules/search/store/module.ts +++ b/packages/x-components/src/x-modules/search/store/module.ts @@ -1,18 +1,20 @@ import { isFacetFilter } from '@empathyco/x-types'; import { setQuery } from '../../../store/utils/query.utils'; -import { setStatus } from '../../../store/utils/status-store.utils'; +import { setStatus } from '../../../store'; import { groupItemsBy } from '../../../utils/array'; import { mergeConfig, setConfig } from '../../../store/utils/config-store.utils'; import { UNKNOWN_FACET_KEY } from '../../facets/store/constants'; import { cancelFetchAndSaveSearchResponse, - fetchAndSaveSearchResponse -} from './actions/fetch-and-save-search-response.action'; -import { fetchSearchResponse } from './actions/fetch-search-response.action'; -import { increasePageAppendingResults } from './actions/increase-page-apending-results.action'; -import { resetRequestOnRefinement } from './actions/reset-request-on-refinement.action'; + cancelReloadSearch, + fetchAndSaveSearchResponse, + fetchSearchResponse, + increasePageAppendingResults, + reloadSearch, + resetRequestOnRefinement, + saveSearchResponse +} from './actions'; import { saveOrigin } from './actions/save-origin.action'; -import { saveSearchResponse } from './actions/save-search-response.action'; import { setUrlParams } from './actions/set-url-params.action'; import { query } from './getters/query.getter'; import { request } from './getters/request.getter'; @@ -118,9 +120,11 @@ export const searchXStoreModule: SearchXStoreModule = { }, actions: { cancelFetchAndSaveSearchResponse, + cancelReloadSearch, fetchSearchResponse, fetchAndSaveSearchResponse, increasePageAppendingResults, + reloadSearch, resetRequestOnRefinement, saveSearchResponse, setUrlParams, diff --git a/packages/x-components/src/x-modules/search/store/types.ts b/packages/x-components/src/x-modules/search/store/types.ts index 848532f4da..bdad836680 100644 --- a/packages/x-components/src/x-modules/search/store/types.ts +++ b/packages/x-components/src/x-modules/search/store/types.ts @@ -7,17 +7,15 @@ import { Redirection, RelatedTag, Result, - Sort, - TaggingRequest, SearchRequest, - SearchResponse + SearchResponse, + Sort, + TaggingRequest } from '@empathyco/x-types'; import { Dictionary } from '@empathyco/x-utils'; -import { XActionContext, XStoreModule } from '../../../store'; +import { StatusMutations, StatusState, XActionContext, XStoreModule } from '../../../store'; import { QueryMutations, QueryState } from '../../../store/utils/query.utils'; -import { StatusMutations, StatusState } from '../../../store/utils/status-store.utils'; -import { QueryOrigin, QueryOriginInit } from '../../../types/origin'; -import { UrlParams } from '../../../types/url-params'; +import { QueryOrigin, QueryOriginInit, UrlParams } from '../../../types'; import { SearchConfig } from '../config.types'; import { InternalSearchRequest, WatchedInternalSearchRequest } from '../types'; import { ConfigMutations } from '../../../store/utils/config-store.utils'; @@ -246,6 +244,10 @@ export interface SearchActions { * Cancels / interrupt {@link SearchActions.fetchAndSaveSearchResponse} synchronous promise. */ cancelFetchAndSaveSearchResponse(): void; + /** + * Cancels / interrupt {@link SearchActions.reloadSearch} synchronous promise. + */ + cancelReloadSearch(): void; /** * Fetches a new search response and stores them in the module state. */ @@ -264,6 +266,10 @@ export interface SearchActions { * for other purposes, please use the {@link SearchMutations.setPage} mutation. */ increasePageAppendingResults(): void; + /** + * Fetches again the current search and stores its response in the module state. + */ + reloadSearch(): void; /** * Checks if the url has params on it and then updates the state with these values. * diff --git a/packages/x-components/src/x-modules/search/wiring.ts b/packages/x-components/src/x-modules/search/wiring.ts index 7679bce076..5bf768c2aa 100644 --- a/packages/x-components/src/x-modules/search/wiring.ts +++ b/packages/x-components/src/x-modules/search/wiring.ts @@ -53,6 +53,13 @@ export const cancelFetchAndSaveSearchResponseWire = wireDispatchWithoutPayload( 'cancelFetchAndSaveSearchResponse' ); +/** + * Cancels the {@link SearchActions.reloadSearch} request promise. + * + * @public + */ +export const cancelReloadSearchWire = wireDispatchWithoutPayload('cancelReloadSearch'); + /** * Sets the search state `origin`. * @@ -130,6 +137,13 @@ export const setSearchPage = wireCommit('setPage'); */ export const setSearchExtraParams = wireCommit('setParams'); +/** + * Requests and stores the search current search response. + * + * @public + */ +export const reloadSearchWire = wireDispatch('reloadSearch'); + /** * Resets the search state `isNoResults`. * @@ -244,6 +258,7 @@ export const searchWiring = createWiring({ UserClearedQuery: { setSearchQuery, cancelFetchAndSaveSearchResponseWire, + cancelReloadSearchWire, resetFromNoResultsWithFilters, resetIsNoResults }, @@ -272,6 +287,9 @@ export const searchWiring = createWiring({ ResultsChanged: { resetAppending }, + ReloadSearchRequested: { + reloadSearchWire + }, SelectedSortProvided: { setSort },