From 42c31c8ac335adcc1f7cb337c43bc15bb473ad4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20CG?= Date: Mon, 24 Nov 2025 11:12:05 +0100 Subject: [PATCH 1/5] feat: self review --- .../__tests__/__fixtures__/facets.response.ts | 96 ++++++++------- .../src/__tests__/platform.adapter.spec.ts | 110 ++++++++++-------- .../facets.endpoint-adapter.ts | 2 +- .../facets-response.mapper.spec.ts.snap | 94 ++++++++------- .../fetch-and-save-facets-response.action.ts | 11 ++ .../src/x-modules/facets/store/module.ts | 8 +- 6 files changed, 188 insertions(+), 133 deletions(-) diff --git a/packages/x-adapter-platform/src/__tests__/__fixtures__/facets.response.ts b/packages/x-adapter-platform/src/__tests__/__fixtures__/facets.response.ts index bc28b75c1b..d8fac84726 100644 --- a/packages/x-adapter-platform/src/__tests__/__fixtures__/facets.response.ts +++ b/packages/x-adapter-platform/src/__tests__/__fixtures__/facets.response.ts @@ -5,80 +5,92 @@ export const platformFacetsResponse = { content: [], facets: [ { - facet: 'facetEditorial', - filter: 'editorial', - label: 'Editorial', + facet: 'brand_facet', + filter: 'brand_facet', + label: 'Brand', type: 'value', values: [ { - id: 'DEBOLSILLO', - value: 'DEBOLSILLO', - count: 1149, - filter: 'editorial:DEBOLSILLO', + id: 'Nike', + value: 'Nike', + count: 1249, + filter: 'brand_facet:Nike', }, { - id: 'ALFAGUARA', - value: 'ALFAGUARA', - count: 1005, - filter: 'editorial:ALFAGUARA', + id: 'Adidas', + value: 'Adidas', + count: 1105, + filter: 'brand_facet:Adidas', }, { - id: 'PLANETA', - value: 'PLANETA', - count: 786, - filter: 'editorial:PLANETA', + id: "Levi's", + value: "Levi's", + count: 886, + filter: "brand_facet:Levi's", }, ], }, { - facet: 'facetColecciondilve', - filter: 'facetColecciondilve', - label: 'Colección', + facet: 'gender', + filter: 'gender', + label: 'Gender', type: 'value', values: [ { - id: 'Best Seller', - value: 'Best Seller', - count: 265, - filter: 'facetColecciondilve:Best Seller', + id: 'men', + value: 'men', + count: 421, + filter: 'gender:men', }, { - id: 'Jovenes lectores', - value: 'Jovenes lectores', - count: 238, - filter: 'facetColecciondilve:Jovenes lectores', + id: 'women', + value: 'women', + count: 247, + filter: 'gender:women', }, { - id: 'Ficcion', - value: 'Ficcion', - count: 204, - filter: 'facetColecciondilve:Ficcion', + id: 'boys', + value: 'boys', + count: 35, + filter: 'gender:boys', + }, + { + id: 'girls', + value: 'girls', + count: 28, + filter: 'gender:girls', + }, + { + id: 'unisex', + value: 'unisex', + count: 5, + filter: 'gender:unisex', }, ], }, { - facet: 'facetHierarchicalCategories', - filter: 'filterHierarchicalCategories', - label: 'Categorías', + facet: 'categoryPaths', + filter: 'categoryIds', + label: 'Categories', type: 'hierarchical', values: [ { - id: '121000000', - value: 'Literatura', + id: '78d9b7366', + value: 'Apparel', count: 28672, - filter: 'filterHierarchicalCategories:121000000', + filter: 'categoryIds:78d9b7366', }, { - id: '415000000', - value: 'Libro antiguo y de ocasion', + id: 'b08648dbd', + value: 'Accessories', count: 14165, - filter: 'filterHierarchicalCategories:415000000', + filter: 'categoryIds:b08648dbd', }, { - id: '117000000', - value: 'Infantil', + id: 'e5eef62d8', + value: 'Footwear', count: 10903, - filter: 'filterHierarchicalCategories:117000000', + filter: 'categoryIds:e5eef62d8', }, ], }, diff --git a/packages/x-adapter-platform/src/__tests__/platform.adapter.spec.ts b/packages/x-adapter-platform/src/__tests__/platform.adapter.spec.ts index 1b8194c13a..5b7f2662d3 100644 --- a/packages/x-adapter-platform/src/__tests__/platform.adapter.spec.ts +++ b/packages/x-adapter-platform/src/__tests__/platform.adapter.spec.ts @@ -838,16 +838,16 @@ describe('platformAdapter tests', () => { window.fetch = fetchMock as any const response = await platformAdapter.facets({ - query: 'books', + query: 'jeans', filters: { - editorial: [ + brand_facet: [ { - facetId: 'editorial', - id: 'editorial:ALFAGUARA', - label: 'ALFAGUARA', + facetId: 'brand_facet', + id: 'brand_facet:Adidas', + label: 'Adidas', modelName: 'SimpleFilter', selected: true, - totalResults: 1005, + totalResults: 1105, } as Filter, ], }, @@ -855,7 +855,7 @@ describe('platformAdapter tests', () => { extraParams: { instance: 'empathy', env: 'test', - lang: 'es', + lang: 'en', device: 'desktop', scope: 'desktop', }, @@ -863,7 +863,7 @@ describe('platformAdapter tests', () => { expect(fetchMock).toHaveBeenCalledTimes(1) expect(fetchMock).toHaveBeenCalledWith( - 'https://search.internal.test.empathy.co/query/empathy/facets?query=books&origin=search_box%3Anone&filter=editorial%3AALFAGUARA&instance=empathy&env=test&lang=es&device=desktop&scope=desktop', + 'https://search.internal.test.empathy.co/query/empathy/facets?query=jeans&origin=search_box%3Anone&filter=brand_facet%3AAdidas&instance=empathy&env=test&lang=en&device=desktop&scope=desktop', { signal: expect.anything(), headers: { @@ -877,97 +877,113 @@ describe('platformAdapter tests', () => { { filters: [ { - facetId: 'facetEditorial', - id: 'editorial:DEBOLSILLO', - label: 'DEBOLSILLO', + facetId: 'brand_facet', + id: 'brand_facet:Nike', + label: 'Nike', modelName: 'SimpleFilter', selected: false, - totalResults: 1149, + totalResults: 1249, }, { - facetId: 'facetEditorial', - id: 'editorial:ALFAGUARA', - label: 'ALFAGUARA', + facetId: 'brand_facet', + id: 'brand_facet:Adidas', + label: 'Adidas', modelName: 'SimpleFilter', selected: false, - totalResults: 1005, + totalResults: 1105, }, { - facetId: 'facetEditorial', - id: 'editorial:PLANETA', - label: 'PLANETA', + facetId: 'brand_facet', + id: "brand_facet:Levi's", + label: "Levi's", modelName: 'SimpleFilter', selected: false, - totalResults: 786, + totalResults: 886, }, ], - id: 'facetEditorial', - label: 'facetEditorial', + id: 'brand_facet', + label: 'brand_facet', modelName: 'SimpleFacet', }, { filters: [ { - facetId: 'facetColecciondilve', - id: 'facetColecciondilve:Best Seller', - label: 'Best Seller', + facetId: 'gender', + id: 'gender:men', + label: 'men', + modelName: 'SimpleFilter', + selected: false, + totalResults: 421, + }, + { + facetId: 'gender', + id: 'gender:women', + label: 'women', + modelName: 'SimpleFilter', + selected: false, + totalResults: 247, + }, + { + facetId: 'gender', + id: 'gender:boys', + label: 'boys', modelName: 'SimpleFilter', selected: false, - totalResults: 265, + totalResults: 35, }, { - facetId: 'facetColecciondilve', - id: 'facetColecciondilve:Jovenes lectores', - label: 'Jovenes lectores', + facetId: 'gender', + id: 'gender:girls', + label: 'girls', modelName: 'SimpleFilter', selected: false, - totalResults: 238, + totalResults: 28, }, { - facetId: 'facetColecciondilve', - id: 'facetColecciondilve:Ficcion', - label: 'Ficcion', + facetId: 'gender', + id: 'gender:unisex', + label: 'unisex', modelName: 'SimpleFilter', selected: false, - totalResults: 204, + totalResults: 5, }, ], - id: 'facetColecciondilve', - label: 'facetColecciondilve', + id: 'gender', + label: 'gender', modelName: 'SimpleFacet', }, { filters: [ { - facetId: 'facetHierarchicalCategories', - id: 'filterHierarchicalCategories:121000000', - label: 'Literatura', + facetId: 'categoryPaths', + id: 'categoryIds:78d9b7366', + label: 'Apparel', modelName: 'HierarchicalFilter', parentId: null, selected: false, totalResults: 28672, }, { - facetId: 'facetHierarchicalCategories', - id: 'filterHierarchicalCategories:415000000', - label: 'Libro antiguo y de ocasion', + facetId: 'categoryPaths', + id: 'categoryIds:b08648dbd', + label: 'Accessories', modelName: 'HierarchicalFilter', parentId: null, selected: false, totalResults: 14165, }, { - facetId: 'facetHierarchicalCategories', - id: 'filterHierarchicalCategories:117000000', - label: 'Infantil', + facetId: 'categoryPaths', + id: 'categoryIds:e5eef62d8', + label: 'Footwear', modelName: 'HierarchicalFilter', parentId: null, selected: false, totalResults: 10903, }, ], - id: 'facetHierarchicalCategories', - label: 'facetHierarchicalCategories', + id: 'categoryPaths', + label: 'categoryPaths', modelName: 'HierarchicalFacet', }, ], diff --git a/packages/x-adapter-platform/src/endpoint-adapters/facets.endpoint-adapter.ts b/packages/x-adapter-platform/src/endpoint-adapters/facets.endpoint-adapter.ts index 6dbe800568..3a7c8840e2 100644 --- a/packages/x-adapter-platform/src/endpoint-adapters/facets.endpoint-adapter.ts +++ b/packages/x-adapter-platform/src/endpoint-adapters/facets.endpoint-adapter.ts @@ -5,7 +5,7 @@ import { facetsResponseMapper } from '../mappers/responses/facets-response.mappe import { getDefaultHeaders, getSearchServiceUrl } from './utils' /** - * Default adapter for the search endpoint. + * Default adapter for the facet endpoint. * * @public */ diff --git a/packages/x-adapter-platform/src/mappers/responses/__tests__/__snapshots__/facets-response.mapper.spec.ts.snap b/packages/x-adapter-platform/src/mappers/responses/__tests__/__snapshots__/facets-response.mapper.spec.ts.snap index 40ddf51d9f..44935a392d 100644 --- a/packages/x-adapter-platform/src/mappers/responses/__tests__/__snapshots__/facets-response.mapper.spec.ts.snap +++ b/packages/x-adapter-platform/src/mappers/responses/__tests__/__snapshots__/facets-response.mapper.spec.ts.snap @@ -6,97 +6,113 @@ exports[`facetsResponseMapper tests should map the response 1`] = ` { "filters": [ { - "facetId": "facetEditorial", - "id": "editorial:DEBOLSILLO", - "label": "DEBOLSILLO", + "facetId": "brand_facet", + "id": "brand_facet:Nike", + "label": "Nike", "modelName": "SimpleFilter", "selected": false, - "totalResults": 1149, + "totalResults": 1249, }, { - "facetId": "facetEditorial", - "id": "editorial:ALFAGUARA", - "label": "ALFAGUARA", + "facetId": "brand_facet", + "id": "brand_facet:Adidas", + "label": "Adidas", "modelName": "SimpleFilter", "selected": false, - "totalResults": 1005, + "totalResults": 1105, }, { - "facetId": "facetEditorial", - "id": "editorial:PLANETA", - "label": "PLANETA", + "facetId": "brand_facet", + "id": "brand_facet:Levi's", + "label": "Levi's", "modelName": "SimpleFilter", "selected": false, - "totalResults": 786, + "totalResults": 886, }, ], - "id": "facetEditorial", - "label": "facetEditorial", + "id": "brand_facet", + "label": "brand_facet", "modelName": "SimpleFacet", }, { "filters": [ { - "facetId": "facetColecciondilve", - "id": "facetColecciondilve:Best Seller", - "label": "Best Seller", + "facetId": "gender", + "id": "gender:men", + "label": "men", "modelName": "SimpleFilter", "selected": false, - "totalResults": 265, + "totalResults": 421, }, { - "facetId": "facetColecciondilve", - "id": "facetColecciondilve:Jovenes lectores", - "label": "Jovenes lectores", + "facetId": "gender", + "id": "gender:women", + "label": "women", "modelName": "SimpleFilter", "selected": false, - "totalResults": 238, + "totalResults": 247, }, { - "facetId": "facetColecciondilve", - "id": "facetColecciondilve:Ficcion", - "label": "Ficcion", + "facetId": "gender", + "id": "gender:boys", + "label": "boys", "modelName": "SimpleFilter", "selected": false, - "totalResults": 204, + "totalResults": 35, + }, + { + "facetId": "gender", + "id": "gender:girls", + "label": "girls", + "modelName": "SimpleFilter", + "selected": false, + "totalResults": 28, + }, + { + "facetId": "gender", + "id": "gender:unisex", + "label": "unisex", + "modelName": "SimpleFilter", + "selected": false, + "totalResults": 5, }, ], - "id": "facetColecciondilve", - "label": "facetColecciondilve", + "id": "gender", + "label": "gender", "modelName": "SimpleFacet", }, { "filters": [ { - "facetId": "facetHierarchicalCategories", - "id": "filterHierarchicalCategories:121000000", - "label": "Literatura", + "facetId": "categoryPaths", + "id": "categoryIds:78d9b7366", + "label": "Apparel", "modelName": "HierarchicalFilter", "parentId": null, "selected": false, "totalResults": 28672, }, { - "facetId": "facetHierarchicalCategories", - "id": "filterHierarchicalCategories:415000000", - "label": "Libro antiguo y de ocasion", + "facetId": "categoryPaths", + "id": "categoryIds:b08648dbd", + "label": "Accessories", "modelName": "HierarchicalFilter", "parentId": null, "selected": false, "totalResults": 14165, }, { - "facetId": "facetHierarchicalCategories", - "id": "filterHierarchicalCategories:117000000", - "label": "Infantil", + "facetId": "categoryPaths", + "id": "categoryIds:e5eef62d8", + "label": "Footwear", "modelName": "HierarchicalFilter", "parentId": null, "selected": false, "totalResults": 10903, }, ], - "id": "facetHierarchicalCategories", - "label": "facetHierarchicalCategories", + "id": "categoryPaths", + "label": "categoryPaths", "modelName": "HierarchicalFacet", }, ], diff --git a/packages/x-components/src/x-modules/facets/store/actions/fetch-and-save-facets-response.action.ts b/packages/x-components/src/x-modules/facets/store/actions/fetch-and-save-facets-response.action.ts index ba52963bfb..43d6993f87 100644 --- a/packages/x-components/src/x-modules/facets/store/actions/fetch-and-save-facets-response.action.ts +++ b/packages/x-components/src/x-modules/facets/store/actions/fetch-and-save-facets-response.action.ts @@ -42,5 +42,16 @@ const { fetchAndSave, cancelPrevious } = createFetchAndSaveActions< }, }) +/** + * Default implementation for {@link FacetsActions.fetchAndSaveFacetsResponse} action. + * + * @public + */ export const fetchAndSaveFacetsResponse = fetchAndSave + +/** + * Default implementation for {@link FacetsActions.cancelFetchAndSaveFacetsResponse} action. + * + * @public + */ export const cancelFetchAndSaveFacetsResponse = cancelPrevious diff --git a/packages/x-components/src/x-modules/facets/store/module.ts b/packages/x-components/src/x-modules/facets/store/module.ts index b510c60a02..42473412d9 100644 --- a/packages/x-components/src/x-modules/facets/store/module.ts +++ b/packages/x-components/src/x-modules/facets/store/module.ts @@ -28,19 +28,19 @@ export const facetsXStoreModule: FacetsXStoreModule = { facets: {}, preselectedFilters: [], stickyFilters: {}, + origin: null, + params: {}, config: { filtersStrategyForRequest: 'all', }, status: 'initial', - origin: null, - params: {}, }), getters: { + facets, + request, selectedFilters, selectedFiltersForRequest, selectedFiltersByFacet, - facets, - request, }, mutations: { mutateFilter(state, { filter, newFilterState }) { From 0a9e9b7fc0f406e74664660ccec6f31316892cb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20CG?= Date: Wed, 26 Nov 2025 16:15:03 +0100 Subject: [PATCH 2/5] feat(facets): implement separate facets configuration --- .../schemas/requests/facets-request.schema.ts | 2 +- .../schemas/requests/search-request.schema.ts | 5 +- packages/x-components/src/utils/filters.ts | 81 +++++++++++++++++-- packages/x-components/src/views/home/Home.vue | 2 +- .../src/x-modules/facets/events.types.ts | 7 ++ .../fetch-and-save-facets-response.action.ts | 10 +-- .../src/x-modules/facets/store/emitters.ts | 6 +- .../src/x-modules/facets/wiring.ts | 52 +++++++++++- 8 files changed, 147 insertions(+), 18 deletions(-) diff --git a/packages/x-adapter-platform/src/schemas/requests/facets-request.schema.ts b/packages/x-adapter-platform/src/schemas/requests/facets-request.schema.ts index d71587c8ae..86d335c970 100644 --- a/packages/x-adapter-platform/src/schemas/requests/facets-request.schema.ts +++ b/packages/x-adapter-platform/src/schemas/requests/facets-request.schema.ts @@ -12,5 +12,5 @@ export const facetsRequestSchema = createMutableSchema mapFilters(filters), - extraParams: 'extraParams', + extraParams: ({ extraParams: { separateFacets, ...rest } = {} }) => rest, }) diff --git a/packages/x-adapter-platform/src/schemas/requests/search-request.schema.ts b/packages/x-adapter-platform/src/schemas/requests/search-request.schema.ts index 37434f64f0..b1bf6bfa64 100644 --- a/packages/x-adapter-platform/src/schemas/requests/search-request.schema.ts +++ b/packages/x-adapter-platform/src/schemas/requests/search-request.schema.ts @@ -15,5 +15,8 @@ export const searchRequestSchema = createMutableSchema mapFilters(filters), - extraParams: 'extraParams', + extraParams: ({ extraParams: { separateFacets, ...rest } = {} }) => ({ + ...rest, + facets: !separateFacets, + }), }) diff --git a/packages/x-components/src/utils/filters.ts b/packages/x-components/src/utils/filters.ts index 9aae094a49..c549447f3b 100644 --- a/packages/x-components/src/utils/filters.ts +++ b/packages/x-components/src/utils/filters.ts @@ -1,23 +1,92 @@ -import type { Filter, RawFilter } from '@empathyco/x-types' +import type { FacetsRequest, Filter, RawFilter } from '@empathyco/x-types' /** * Compares if two lists contains the same filters. * - * @param someFilters - A list of filters to compare. - * @param anotherFilters - Another list of filters to compare. + * @param filtersA - A list of filters to compare. + * @param filtersB - Another list of filters to compare. * * @returns True if the two lists of filters are equal, which means that they have the same * filters. The position of the filter does not matter for this check. * * @public */ -export function areFiltersDifferent(someFilters: Filter[], anotherFilters: Filter[]): boolean { +export function areFiltersDifferent(filtersA: Filter[], filtersB: Filter[]): boolean { return ( - someFilters.length !== anotherFilters.length || - someFilters.some(filter => !anotherFilters.find(otherFilter => otherFilter.id === filter.id)) + filtersA.length !== filtersB.length || + filtersA.some(filter => !filtersB.find(otherFilter => otherFilter.id === filter.id)) ) } +/** + * Compares if two filter dictionaries are different, ignoring empty filter arrays. + * + * @param filtersA - A filter dictionary to compare. + * @param filtersB - Another filter dictionary to compare. + * + * @returns True if the filter dictionaries are different, false if they are equivalent. + * Empty objects (\{\}) and objects with only empty arrays (\{brand: [], category: []\}) are + * treated as equivalent since they both represent no active filters. + * + * @public + */ +export function areFilterDictionaryDifferent( + filtersA: Record, + filtersB: Record, +): boolean { + const allKeys = new Set([...Object.keys(filtersA), ...Object.keys(filtersB)]) + + for (const key of allKeys) { + const arrayA = filtersA[key] || [] + const arrayB = filtersB[key] || [] + + // Skip if both arrays are empty (equivalent to no filters) + if (arrayA.length === 0 && arrayB.length === 0) { + continue + } + + // Compare the filter arrays (handles length and content differences) + if (areFiltersDifferent(arrayA, arrayB)) { + return true + } + } + + return false +} + +/** + * Compares if two facets requests are different. + * + * @param requestA - A facets request. + * @param requestB - Another facets request. + * + * @returns True if the requests are different, false if they are equivalent. + * Handles semantic equivalence of filter states (empty object vs object with empty arrays). + * + * @public + */ +export function areRequestsDifferent( + requestA: FacetsRequest | null, + requestB: FacetsRequest | null, +): boolean { + // Reference equality check + if (requestA === requestB) return false + + // Null transition check + if (requestA === null || requestB === null) return true + + // Compare query + if (requestA.query !== requestB.query) return true + + // Compare extraParams (shallow, simple) + if (JSON.stringify(requestA.extraParams || {}) !== JSON.stringify(requestB.extraParams || {})) + return true + + // Use semantic filter comparison to avoid duplicate emissions + // when filters go from {} to {brand: [], category: []} + return areFilterDictionaryDifferent(requestA.filters || {}, requestB.filters || {}) +} + /** * Helper method which creates the filter entity from the filter ir of the url. * diff --git a/packages/x-components/src/views/home/Home.vue b/packages/x-components/src/views/home/Home.vue index 2e92a4f6af..a9070de56d 100644 --- a/packages/x-components/src/views/home/Home.vue +++ b/packages/x-components/src/views/home/Home.vue @@ -724,7 +724,7 @@ export default defineComponent({ setup() { const x = use$x() const stores = ['Spain', 'Portugal', 'Italy'] - const initialExtraParams = { store: 'Portugal' } + const initialExtraParams = { store: 'Portugal', separateFacets: false } const searchInputPlaceholderMessages = [ 'Find shirts', 'Find shoes', diff --git a/packages/x-components/src/x-modules/facets/events.types.ts b/packages/x-components/src/x-modules/facets/events.types.ts index 73e1a4f37a..0fe555927a 100644 --- a/packages/x-components/src/x-modules/facets/events.types.ts +++ b/packages/x-components/src/x-modules/facets/events.types.ts @@ -1,6 +1,7 @@ import type { EditableNumberRangeFilter, Facet, + FacetsRequest, Filter, HierarchicalFilter, NumberRangeFilter, @@ -98,4 +99,10 @@ export interface FacetsXEvents { * Payload: The facets query. */ FacetsQueryChanged: string + /** + * Any property of the search request has been updated. + * Payload: The new facets request or `null` if there is not enough data in the state to + * conform a valid request. + */ + FacetsRequestUpdated: FacetsRequest | null } diff --git a/packages/x-components/src/x-modules/facets/store/actions/fetch-and-save-facets-response.action.ts b/packages/x-components/src/x-modules/facets/store/actions/fetch-and-save-facets-response.action.ts index 43d6993f87..59e4b558f2 100644 --- a/packages/x-components/src/x-modules/facets/store/actions/fetch-and-save-facets-response.action.ts +++ b/packages/x-components/src/x-modules/facets/store/actions/fetch-and-save-facets-response.action.ts @@ -9,18 +9,14 @@ const { fetchAndSave, cancelPrevious } = createFetchAndSaveActions< FacetsRequest | null, FacetsResponse | null >({ - async fetch({ dispatch, getters }) { - return getters.request - ? dispatch('fetchFacetsResponse', getters.request) - : Promise.resolve(null) + async fetch({ dispatch }, request) { + return request ? dispatch('fetchFacetsResponse', request) : Promise.resolve(null) }, onSuccess({ commit, getters }, response) { if (response !== null) { const selectedFilters = getters.selectedFilters const selectedIds = new Set( - Object.values(selectedFilters ?? {}) - .filter((f: Filter) => f.selected) - .map((f: Filter) => f.id), + selectedFilters.filter((f: Filter) => f.selected).map((f: Filter) => f.id), ) const facetsWithSelectedFilters: Facet[] = [] diff --git a/packages/x-components/src/x-modules/facets/store/emitters.ts b/packages/x-components/src/x-modules/facets/store/emitters.ts index c3db79d611..3e525be384 100644 --- a/packages/x-components/src/x-modules/facets/store/emitters.ts +++ b/packages/x-components/src/x-modules/facets/store/emitters.ts @@ -1,5 +1,5 @@ import { createStoreEmitters } from '../../../store/utils/store-emitters.utils' -import { areFiltersDifferent } from '../../../utils/filters' +import { areFiltersDifferent, areRequestsDifferent } from '../../../utils/filters' import { isNewQuery } from '../../../utils/is-new-query' import { facetsXStoreModule } from './module' @@ -27,4 +27,8 @@ export const facetsEmitters = createStoreEmitters(facetsXStoreModule, { selector: state => state.query, filter: isNewQuery, }, + FacetsRequestUpdated: { + selector: (_, getters) => getters.request, + filter: areRequestsDifferent, + }, }) diff --git a/packages/x-components/src/x-modules/facets/wiring.ts b/packages/x-components/src/x-modules/facets/wiring.ts index 1f7e0f08b3..a1f37d4632 100644 --- a/packages/x-components/src/x-modules/facets/wiring.ts +++ b/packages/x-components/src/x-modules/facets/wiring.ts @@ -2,7 +2,11 @@ import type { Facet } from '@empathyco/x-types' import type { UrlParams } from '../../types/url-params' import type { XEventPayload } from '../../wiring/index' import { createRawFilters } from '../../utils/filters' -import { namespacedWireCommit, namespacedWireCommitWithoutPayload } from '../../wiring/index' +import { + namespacedWireCommit, + namespacedWireCommitWithoutPayload, + namespacedWireDispatch, +} from '../../wiring' import { wireService, wireServiceWithoutPayload } from '../../wiring/wires.factory' import { filter, mapWire } from '../../wiring/wires.operators' import { createWiring } from '../../wiring/wiring.utils' @@ -29,6 +33,13 @@ const wireCommit = namespacedWireCommit(moduleName) */ const wireCommitWithoutPayload = namespacedWireCommitWithoutPayload(moduleName) +/** + * WireDispatch for {@link FacetsXModule}. + * + * @internal + */ +const wireDispatch = namespacedWireDispatch(moduleName) + /** * Wires factory for {@link DefaultFacetsService}. */ @@ -141,6 +152,11 @@ const selectPreselectedFilterWire = wireFacetsService('selectPreselectedFilters' */ const setQuery = wireFacetsService('setQuery') +const setQueryFromUrlWire = wireCommit( + 'setQuery', + ({ eventPayload }: { eventPayload: UrlParams }) => eventPayload.query, +) + /** * Removes all the sticky filters from the state. * @@ -180,6 +196,30 @@ export const setFiltersFromHistoryQueries = wireCommit( */ export const setQueryFromPreview = wireCommit('setQuery', ({ eventPayload: { query } }) => query) +/** + * Sets the facets state `params`. + * + * @public + */ +export const setFacetsExtraParams = wireCommit('setParams') + +/** + * Requests and stores the facets response. + * + * @public + */ +export const fetchAndSaveFacetsResponseWire = wireDispatch('fetchAndSaveFacetsResponse') + +/** + * Filtered version of fetchAndSaveFacetsResponseWire that only executes when separateFacets are enabled. + * + * @internal + */ +const fetchAndSaveFacetsResponseWireIfEnabled = filter( + fetchAndSaveFacetsResponseWire, + ({ store }) => !!store.state.x.facets.params.separateFacets, +) + /** * Wiring configuration for the {@link FacetsXModule | facets module}. * @@ -189,6 +229,7 @@ export const facetsWiring = createWiring({ ParamsLoadedFromUrl: { // TODO: move this logic to Facets Service clearAllFiltersWire, + setQueryFromUrlWire, setFiltersFromUrl, }, PreselectedFiltersProvided: { @@ -238,4 +279,13 @@ export const facetsWiring = createWiring({ UserSelectedAHistoryQuery: { setFiltersFromHistoryQueries, }, + ExtraParamsChanged: { + setFacetsExtraParams, + }, + UserOpenedFacetsAside: { + fetchAndSaveFacetsResponseWireIfEnabled, + }, + FacetsRequestUpdated: { + fetchAndSaveFacetsResponseWireIfEnabled, + }, }) From 762d6c39654322aed6e05fff61ce2cc6fda94a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20CG?= Date: Tue, 2 Dec 2025 18:06:02 +0100 Subject: [PATCH 3/5] feat(facets): enhance logic to save raw facet and raw filter state --- .../fetch-and-save-facets-response.action.ts | 29 +++---------------- .../src/x-modules/facets/store/emitters.ts | 6 ++++ .../facets/store/getters/request.getter.ts | 12 ++++---- .../src/x-modules/facets/store/module.ts | 13 +++++++++ .../src/x-modules/facets/store/types.ts | 19 ++++++++++++ .../src/x-modules/facets/wiring.ts | 10 +++++++ 6 files changed, 59 insertions(+), 30 deletions(-) diff --git a/packages/x-components/src/x-modules/facets/store/actions/fetch-and-save-facets-response.action.ts b/packages/x-components/src/x-modules/facets/store/actions/fetch-and-save-facets-response.action.ts index 59e4b558f2..e856b68675 100644 --- a/packages/x-components/src/x-modules/facets/store/actions/fetch-and-save-facets-response.action.ts +++ b/packages/x-components/src/x-modules/facets/store/actions/fetch-and-save-facets-response.action.ts @@ -1,8 +1,6 @@ -import type { Facet, FacetsRequest, FacetsResponse, Filter } from '@empathyco/x-types' +import type { FacetsRequest, FacetsResponse } from '@empathyco/x-types' import type { FacetsActionsContext } from '../types' -import { isHierarchicalFacet } from '@empathyco/x-types' import { createFetchAndSaveActions } from '../../../../store/utils/fetch-and-save-action.utils' -import { applyHierarchicalSelection, flattenAllFilters } from '../../utils' const { fetchAndSave, cancelPrevious } = createFetchAndSaveActions< FacetsActionsContext, @@ -12,28 +10,9 @@ const { fetchAndSave, cancelPrevious } = createFetchAndSaveActions< async fetch({ dispatch }, request) { return request ? dispatch('fetchFacetsResponse', request) : Promise.resolve(null) }, - onSuccess({ commit, getters }, response) { - if (response !== null) { - const selectedFilters = getters.selectedFilters - const selectedIds = new Set( - selectedFilters.filter((f: Filter) => f.selected).map((f: Filter) => f.id), - ) - const facetsWithSelectedFilters: Facet[] = [] - - response.facets?.forEach((facet: Facet) => { - if (isHierarchicalFacet(facet)) { - applyHierarchicalSelection(facet.filters, selectedIds) - } else { - facet.filters.forEach((filter: Filter) => { - filter.selected = selectedIds.has(filter.id) - }) - } - - facetsWithSelectedFilters.push(facet) - commit('setFacet', facet) - }) - - commit('setFilters', flattenAllFilters(facetsWithSelectedFilters)) + onSuccess({ commit }, response) { + if (response !== null && response.facets) { + commit('setRawFacets', response.facets) } }, }) diff --git a/packages/x-components/src/x-modules/facets/store/emitters.ts b/packages/x-components/src/x-modules/facets/store/emitters.ts index 3e525be384..5ec558506c 100644 --- a/packages/x-components/src/x-modules/facets/store/emitters.ts +++ b/packages/x-components/src/x-modules/facets/store/emitters.ts @@ -31,4 +31,10 @@ export const facetsEmitters = createStoreEmitters(facetsXStoreModule, { selector: (_, getters) => getters.request, filter: areRequestsDifferent, }, + FacetsChanged: { + selector: state => state.rawFacets, + filter(newValue, oldValue): boolean { + return newValue.length !== 0 || oldValue.length !== 0 + }, + }, }) diff --git a/packages/x-components/src/x-modules/facets/store/getters/request.getter.ts b/packages/x-components/src/x-modules/facets/store/getters/request.getter.ts index 49b1a5a5d1..c7498ffc02 100644 --- a/packages/x-components/src/x-modules/facets/store/getters/request.getter.ts +++ b/packages/x-components/src/x-modules/facets/store/getters/request.getter.ts @@ -10,15 +10,17 @@ import type { FacetsXStoreModule } from '../types' * @returns The facets request to fetch data from the API. * @public */ -export const request: FacetsXStoreModule['getters']['request'] = ( - { query, origin, params }, - { selectedFiltersByFacet }, -) => { +export const request: FacetsXStoreModule['getters']['request'] = ({ + query, + origin, + selectedFiltersDictionary, + params, +}) => { return query ? { query, origin: origin === null ? undefined : origin, - filters: selectedFiltersByFacet, + filters: selectedFiltersDictionary, extraParams: params, } : null diff --git a/packages/x-components/src/x-modules/facets/store/module.ts b/packages/x-components/src/x-modules/facets/store/module.ts index 42473412d9..d8188b8964 100644 --- a/packages/x-components/src/x-modules/facets/store/module.ts +++ b/packages/x-components/src/x-modules/facets/store/module.ts @@ -1,8 +1,11 @@ import type { Facet } from '@empathyco/x-types' import type { FacetGroupEntry, FacetsXStoreModule } from './types' +import { isFacetFilter } from '@empathyco/x-types' import { setStatus } from '../../../store' import { mergeConfig, setConfig } from '../../../store/utils/config-store.utils' import { setQuery } from '../../../store/utils/query.utils' +import { groupItemsBy } from '../../../utils/array' +import { UNKNOWN_FACET_KEY } from '../../facets/store/constants' import { cancelFetchAndSaveFacetsResponse, fetchAndSaveFacetsResponse, @@ -34,6 +37,8 @@ export const facetsXStoreModule: FacetsXStoreModule = { filtersStrategyForRequest: 'all', }, status: 'initial', + rawFacets: [], + selectedFiltersDictionary: {}, }), getters: { facets, @@ -89,6 +94,14 @@ export const facetsXStoreModule: FacetsXStoreModule = { setParams(state, params) { state.params = params }, + setRawFacets(state, rawFacets) { + state.rawFacets = rawFacets + }, + setSelectedFiltersDictionary(state, selectedFilters) { + state.selectedFiltersDictionary = groupItemsBy(selectedFilters, filter => + isFacetFilter(filter) ? filter.facetId : UNKNOWN_FACET_KEY, + ) + }, }, actions: { fetchFacetsResponse, diff --git a/packages/x-components/src/x-modules/facets/store/types.ts b/packages/x-components/src/x-modules/facets/store/types.ts index 345932d14f..42652bb5bc 100644 --- a/packages/x-components/src/x-modules/facets/store/types.ts +++ b/packages/x-components/src/x-modules/facets/store/types.ts @@ -27,6 +27,13 @@ export interface FacetsState extends StatusState, QueryState { origin: QueryOrigin | null /** The extra params property of the state. */ params: Dictionary + /** The list of the facets, related to the `query` property of the state. */ + rawFacets: Facet[] + /** + * The dictionary of selected filters, used to perform the search request. + * The key is the facet id, and the value the list of filters for that facet. + */ + selectedFiltersDictionary: Dictionary } /** @@ -146,6 +153,18 @@ export interface FacetsMutations * @param params - The new extra params. */ setParams: (params: Dictionary) => void + /** + * Sets the facets of the module. + * + * @param facets - The new facets to save to the state. + */ + setRawFacets: (facets: Facet[]) => void + /** + * Sets the selected filters dictionary of the module. + * + * @param selectedFilters - The new selected filters to save to the state. + */ + setSelectedFiltersDictionary: (selectedFilters: Filter[]) => void } /** diff --git a/packages/x-components/src/x-modules/facets/wiring.ts b/packages/x-components/src/x-modules/facets/wiring.ts index a1f37d4632..b24b4d92ca 100644 --- a/packages/x-components/src/x-modules/facets/wiring.ts +++ b/packages/x-components/src/x-modules/facets/wiring.ts @@ -203,6 +203,13 @@ export const setQueryFromPreview = wireCommit('setQuery', ({ eventPayload: { que */ export const setFacetsExtraParams = wireCommit('setParams') +/** + * Sets the search state `selectedFiltersDictionary`. + * + * @public + */ +export const setSelectedFiltersDictionaryWire = wireCommit('setSelectedFiltersDictionary') + /** * Requests and stores the facets response. * @@ -288,4 +295,7 @@ export const facetsWiring = createWiring({ FacetsRequestUpdated: { fetchAndSaveFacetsResponseWireIfEnabled, }, + SelectedFiltersForRequestChanged: { + setSelectedFiltersDictionaryWire, + }, }) From 9a4863720750dff41a49f7e1124948ebf7ccc4ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20CG?= Date: Tue, 2 Dec 2025 18:42:41 +0100 Subject: [PATCH 4/5] tests(facets): add facets parameter to search requests and update tests --- .../src/__tests__/platform.adapter.spec.ts | 3 ++- .../__tests__/search-request.mapper.spec.ts | 1 + .../facets/store/__tests__/actions.spec.ts | 26 ++++--------------- 3 files changed, 8 insertions(+), 22 deletions(-) diff --git a/packages/x-adapter-platform/src/__tests__/platform.adapter.spec.ts b/packages/x-adapter-platform/src/__tests__/platform.adapter.spec.ts index 5b7f2662d3..f6cc307bf1 100644 --- a/packages/x-adapter-platform/src/__tests__/platform.adapter.spec.ts +++ b/packages/x-adapter-platform/src/__tests__/platform.adapter.spec.ts @@ -134,11 +134,12 @@ describe('platformAdapter tests', () => { lang: 'es', device: 'mobile', scope: 'mobile', + facets: true, }, }) expect(fetchMock).toHaveBeenCalledTimes(1) expect(fetchMock).toHaveBeenCalledWith( - 'https://search.internal.test.empathy.co/query/empathy/search?internal=true&query=chips&origin=popular_search%3Apredictive_layer&start=0&rows=0&sort=price+asc&filter=categoryIds%3Affc61e1e9__be257cb26&filter=gender%3Amen&filter=price%3A10.0-20.0&instance=empathy&env=test&lang=es&device=mobile&scope=mobile', + 'https://search.internal.test.empathy.co/query/empathy/search?internal=true&query=chips&origin=popular_search%3Apredictive_layer&start=0&rows=0&sort=price+asc&filter=categoryIds%3Affc61e1e9__be257cb26&filter=gender%3Amen&filter=price%3A10.0-20.0&instance=empathy&env=test&lang=es&device=mobile&scope=mobile&facets=true', { signal: expect.anything(), headers: { diff --git a/packages/x-adapter-platform/src/mappers/requests/__tests__/search-request.mapper.spec.ts b/packages/x-adapter-platform/src/mappers/requests/__tests__/search-request.mapper.spec.ts index 4e6c9b4c08..dffe4a0c5b 100644 --- a/packages/x-adapter-platform/src/mappers/requests/__tests__/search-request.mapper.spec.ts +++ b/packages/x-adapter-platform/src/mappers/requests/__tests__/search-request.mapper.spec.ts @@ -33,6 +33,7 @@ describe('searchRequestMapper tests', () => { lang: 'en', device: 'mobile', scope: 'mobile', + facets: true, }, }) }) diff --git a/packages/x-components/src/x-modules/facets/store/__tests__/actions.spec.ts b/packages/x-components/src/x-modules/facets/store/__tests__/actions.spec.ts index 0dc34f7a11..6b5b4260d9 100644 --- a/packages/x-components/src/x-modules/facets/store/__tests__/actions.spec.ts +++ b/packages/x-components/src/x-modules/facets/store/__tests__/actions.spec.ts @@ -86,28 +86,12 @@ describe('testing facets module actions', () => { await store.dispatch('fetchAndSaveFacetsResponse', store.getters.request) - // The action should save facets without filters (filters are stored separately in state.filters) - expect(store.state.facets).toMatchObject({ - brand: { id: 'brand', label: 'brand', modelName: 'SimpleFacet' }, - color: { id: 'color', label: 'color', modelName: 'SimpleFacet' }, - }) + // TODO: Update test when the facets state is definitively designed + expect(store.state.facets).toMatchObject({}) - expect(store.state.filters['brand:Nike']).toEqual({ - facetId: 'brand', - id: 'brand:Nike', - label: 'Nike', - modelName: 'SimpleFilter', - selected: false, - totalResults: 10, - }) - expect(store.state.filters['color:Red']).toEqual({ - facetId: 'color', - id: 'color:Red', - label: 'Red', - modelName: 'SimpleFilter', - selected: false, - totalResults: 3, - }) + expect(store.state.filters['brand:Nike']).toEqual(undefined) + + expect(store.state.filters['color:Red']).toEqual(undefined) }) it('should preserve selected state of filters when saving facets response', async () => { From 6a6393de060d59656f4c81d04a3e302ca68822d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20CG?= Date: Wed, 3 Dec 2025 12:25:10 +0100 Subject: [PATCH 5/5] test(separateFacets): test values --- packages/x-components/src/views/adapter.ts | 36 ++++++++++++++++++- .../x-components/src/views/base-config.ts | 4 +-- packages/x-components/src/views/home/Home.vue | 2 +- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/packages/x-components/src/views/adapter.ts b/packages/x-components/src/views/adapter.ts index ef5ae93e09..ef7e860ce8 100644 --- a/packages/x-components/src/views/adapter.ts +++ b/packages/x-components/src/views/adapter.ts @@ -1,5 +1,7 @@ +/* eslint-disable ts/no-unsafe-member-access */ +/* eslint-disable ts/no-unsafe-assignment */ import type { PlatformAdapter } from '@empathyco/x-adapter-platform' -import { platformAdapter } from '@empathyco/x-adapter-platform' +import { platformAdapter, resultSchema } from '@empathyco/x-adapter-platform' import { e2eAdapter } from '../adapter/e2e-adapter' export const adapterConfig = { @@ -16,3 +18,35 @@ export const adapter = new Proxy(platformAdapter, { get: (obj: PlatformAdapter, prop: keyof PlatformAdapter) => adapterConfig.e2e ? e2eAdapter[prop] : obj[prop], }) + +resultSchema.$override>({ + availability: 'availability', + description: 'description', + collection: 'collection', + brand: 'brand', + encuadernation: 'encuadernation', + isSigned: 'isSigned', + isLocalProduct: 'isLocalProduct', + freeShipping: 'freeShipping', + externalScore: ({ externalScore }) => Number(externalScore), + externalVotes: 'externalVotes', + isNovelty: 'isNovelty', + productType: 'productType', + identifier: { + value: 'isbn', + }, + price: ({ price: rawPrices, availability }) => { + if (availability === 'descatalogado' || availability === 'agotado') { + return undefined + } + if (rawPrices) { + return { + value: rawPrices.current, + originalValue: rawPrices.previous ?? rawPrices.current, + futureValue: 0, + hasDiscount: rawPrices.current < (rawPrices.previous ?? rawPrices.current), + } + } + return undefined + }, +}) diff --git a/packages/x-components/src/views/base-config.ts b/packages/x-components/src/views/base-config.ts index cea82006bb..3b0d6bb4b1 100644 --- a/packages/x-components/src/views/base-config.ts +++ b/packages/x-components/src/views/base-config.ts @@ -3,8 +3,8 @@ import type { InstallXOptions } from '../x-installer/x-installer/types' import { adapter } from './adapter' export const baseSnippetConfig: SnippetConfig = { - instance: 'empathy', - lang: 'en', + instance: 'cdl', + lang: 'es', env: 'staging', scope: 'x-components-development', } diff --git a/packages/x-components/src/views/home/Home.vue b/packages/x-components/src/views/home/Home.vue index a9070de56d..3170fe691f 100644 --- a/packages/x-components/src/views/home/Home.vue +++ b/packages/x-components/src/views/home/Home.vue @@ -724,7 +724,7 @@ export default defineComponent({ setup() { const x = use$x() const stores = ['Spain', 'Portugal', 'Italy'] - const initialExtraParams = { store: 'Portugal', separateFacets: false } + const initialExtraParams = { store: 'ES', separateFacets: false } const searchInputPlaceholderMessages = [ 'Find shirts', 'Find shoes',