From 3988d0233c2005b6eb69dcd8c680d37bd67f3299 Mon Sep 17 00:00:00 2001 From: Bartosz Herba Date: Mon, 4 Apr 2022 11:23:06 +0200 Subject: [PATCH 1/4] refactor(filter): rework filters - move category filters to the separate component - add filters renderers and filter config resolver - move configuration from middleware to catalog module - partially add UT, add todo for missing tests --- packages/theme/composables/types.ts | 18 +- packages/theme/composables/useFacet/_utils.ts | 8 - .../theme/composables/useUiHelpers/index.ts | 6 +- packages/theme/getters/facetGetters.ts | 8 +- packages/theme/getters/types.d.ts | 4 +- .../theme/helpers/integrationPlugin/index.ts | 1 - packages/theme/middleware.config.js | 3 - .../components/filters/CategoryFilters.scss | 59 ++++ .../components/filters/CategoryFilters.vue | 139 ++++++++ .../filters/__tests__/useFilters.spec.js | 35 ++ .../filters/renderer/CheckboxType.vue | 39 +++ .../components/filters/renderer/RadioType.vue | 42 +++ .../filters/renderer/RendererTypesEnum.ts | 8 + .../filters/renderer/SwatchColorType.vue | 48 +++ .../category/components/filters/useFilters.ts | 66 ++++ .../catalog/category/config/FiltersConfig.ts | 48 +++ .../config/__tests__/filtersConfig.spec.js | 41 +++ .../theme/modules/catalog/category/types.d.ts | 9 +- packages/theme/modules/catalog/index.js | 3 +- .../pages/{default.vue => category.vue} | 312 ++---------------- packages/theme/nuxt.config.js | 2 - 21 files changed, 583 insertions(+), 316 deletions(-) create mode 100644 packages/theme/modules/catalog/category/components/filters/CategoryFilters.scss create mode 100644 packages/theme/modules/catalog/category/components/filters/CategoryFilters.vue create mode 100644 packages/theme/modules/catalog/category/components/filters/__tests__/useFilters.spec.js create mode 100644 packages/theme/modules/catalog/category/components/filters/renderer/CheckboxType.vue create mode 100644 packages/theme/modules/catalog/category/components/filters/renderer/RadioType.vue create mode 100644 packages/theme/modules/catalog/category/components/filters/renderer/RendererTypesEnum.ts create mode 100644 packages/theme/modules/catalog/category/components/filters/renderer/SwatchColorType.vue create mode 100644 packages/theme/modules/catalog/category/components/filters/useFilters.ts create mode 100644 packages/theme/modules/catalog/category/config/FiltersConfig.ts create mode 100644 packages/theme/modules/catalog/category/config/__tests__/filtersConfig.spec.js rename packages/theme/modules/catalog/pages/{default.vue => category.vue} (64%) diff --git a/packages/theme/composables/types.ts b/packages/theme/composables/types.ts index f2f0ce959..1a0173d43 100644 --- a/packages/theme/composables/types.ts +++ b/packages/theme/composables/types.ts @@ -213,7 +213,7 @@ export enum AgnosticOrderStatus { Refunded = 'Refunded', } -export interface AgnosticFacet { +export interface FacetInterface { type: string; id: string; value: any; @@ -223,15 +223,15 @@ export interface AgnosticFacet { metadata?: any; } -export interface AgnosticGroupedFacet { +export interface GroupedFacetInterface { id: string; label: string; count?: number; - options: AgnosticFacet[]; + options: FacetInterface[]; } export interface AgnosticSort { - options: AgnosticFacet[]; + options: FacetInterface[]; selected: string; } @@ -280,16 +280,6 @@ export interface AgnosticReviewMetadata { }[]; } -export interface AgnosticFacet { - type: string; - id: string; - value: any; - attrName?: string; - count?: number; - selected?: boolean; - metadata?: any; -} - export interface CategoryTreeInterface { label: string; slug?: string; diff --git a/packages/theme/composables/useFacet/_utils.ts b/packages/theme/composables/useFacet/_utils.ts index 2a5252dc0..02339a54e 100644 --- a/packages/theme/composables/useFacet/_utils.ts +++ b/packages/theme/composables/useFacet/_utils.ts @@ -22,13 +22,6 @@ export const buildBreadcrumbs = (rootCat) => buildBreadcrumbsList(rootCat, []) const filterFacets = (criteria) => (f) => (criteria ? criteria.includes(f.attribute_code) : true); -const getFacetTypeByCode = (code) => { - if (code === 'type_of_stones') { - return 'radio'; - } - return 'checkbox'; -}; - const createFacetsFromOptions = (facets, filters, facet) => { const options = facet.options || []; const selectedList = filters && filters[facet.attribute_code] ? filters[facet.attribute_code] : []; @@ -38,7 +31,6 @@ const createFacetsFromOptions = (facets, filters, facet) => { value, count, }) => ({ - type: getFacetTypeByCode(facet.attribute_code), id: label, attrName: label, value, diff --git a/packages/theme/composables/useUiHelpers/index.ts b/packages/theme/composables/useUiHelpers/index.ts index 771fb9ab4..dec3746fe 100644 --- a/packages/theme/composables/useUiHelpers/index.ts +++ b/packages/theme/composables/useUiHelpers/index.ts @@ -1,10 +1,10 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { useRoute, useRouter } from '@nuxtjs/composition-api'; -import { CategoryTreeInterface, Category, AgnosticFacet } from '~/composables/types'; +import { CategoryTreeInterface, Category, FacetInterface } from '~/composables/types'; const nonFilters = new Set(['page', 'sort', 'term', 'itemsPerPage']); -const reduceFilters = (query) => (prev, curr) => { +const reduceFilters = (query) => (prev, curr: string) => { const makeArray = Array.isArray(query[curr]) || nonFilters.has(curr); return { @@ -76,7 +76,7 @@ const useUiHelpers = () => { }); }; - const isFacetColor = (facet: AgnosticFacet): boolean => facet.id === 'color'; + const isFacetColor = (facet: FacetInterface): boolean => facet.id === 'color'; const isFacetCheckbox = (): boolean => false; diff --git a/packages/theme/getters/facetGetters.ts b/packages/theme/getters/facetGetters.ts index 9030fc6fd..9f243f795 100644 --- a/packages/theme/getters/facetGetters.ts +++ b/packages/theme/getters/facetGetters.ts @@ -1,10 +1,10 @@ import { AgnosticCategoryTree, - AgnosticGroupedFacet, + GroupedFacetInterface, AgnosticPagination, AgnosticSort, AgnosticBreadcrumb, - AgnosticFacet, + FacetInterface, } from '~/composables/types'; import { FacetsGetters } from '~/getters/types'; @@ -17,9 +17,9 @@ import { } from '~/composables/useFacet/_utils'; // eslint-disable-next-line @typescript-eslint/no-unused-vars -const getAll = (searchData: SearchData, criteria?: string[]): AgnosticFacet[] => buildFacets(searchData, reduceForFacets, criteria); +const getAll = (searchData: SearchData, criteria?: string[]): FacetInterface[] => buildFacets(searchData, reduceForFacets, criteria); -const getGrouped = (searchData, criteria?: string[]): AgnosticGroupedFacet[] => buildFacets(searchData, reduceForGroupedFacets, criteria) +const getGrouped = (searchData: SearchData, criteria?: string[]): GroupedFacetInterface[] => buildFacets(searchData, reduceForGroupedFacets, criteria) ?.filter((facet) => facet.options && facet.options.length > 0); // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/packages/theme/getters/types.d.ts b/packages/theme/getters/types.d.ts index f59e9c725..1f589d001 100644 --- a/packages/theme/getters/types.d.ts +++ b/packages/theme/getters/types.d.ts @@ -2,7 +2,7 @@ import { Country } from '~/modules/GraphQL/types'; import { AgnosticAttribute, Countries, AgnosticPrice, AgnosticTotals, AgnosticCoupon, AgnosticDiscount, AgnosticCategoryTree, AgnosticBreadcrumb, - AgnosticGroupedFacet, AgnosticFacet, AgnosticSort, AgnosticMediaGalleryItem, + GroupedFacetInterface, AgnosticSort, AgnosticMediaGalleryItem, } from '~/composables/types'; export interface AddressGetter { @@ -65,7 +65,7 @@ export interface ForgotPasswordGetters { export interface FacetsGetters { getAll: (searchData: FacetSearchResult, criteria?: CRITERIA) => AgnosticFacet[]; - getGrouped: (searchData: FacetSearchResult, criteria?: CRITERIA) => AgnosticGroupedFacet[]; + getGrouped: (searchData: FacetSearchResult, criteria?: CRITERIA) => GroupedFacetInterface[]; getCategoryTree: (searchData: FacetSearchResult) => AgnosticCategoryTree; getSortOptions: (searchData: FacetSearchResult) => AgnosticSort; getProducts: (searchData: FacetSearchResult) => RESULTS; diff --git a/packages/theme/helpers/integrationPlugin/index.ts b/packages/theme/helpers/integrationPlugin/index.ts index 47645a394..735292d9f 100644 --- a/packages/theme/helpers/integrationPlugin/index.ts +++ b/packages/theme/helpers/integrationPlugin/index.ts @@ -48,7 +48,6 @@ export const integrationPlugin = (pluginFn: NuxtPluginWithIntegration) => (nuxtC // eslint-disable-next-line @typescript-eslint/no-unsafe-argument client.defaults.headers.cookie = setCookieValues((nuxtCtx.app.i18n as any).cookieValues, (client.defaults.headers as any).cookie); } - injectInContext({ api, client, config }); }; diff --git a/packages/theme/middleware.config.js b/packages/theme/middleware.config.js index 6b6a3b130..35452744c 100755 --- a/packages/theme/middleware.config.js +++ b/packages/theme/middleware.config.js @@ -19,9 +19,6 @@ module.exports = { default: config.get('enableMagentoExternalCheckout'), }, }, - facets: { - available: ['color', 'size', 'price'], - }, customApolloHttpLinkOptions: { useGETForQueries: true, }, diff --git a/packages/theme/modules/catalog/category/components/filters/CategoryFilters.scss b/packages/theme/modules/catalog/category/components/filters/CategoryFilters.scss new file mode 100644 index 000000000..f513f4922 --- /dev/null +++ b/packages/theme/modules/catalog/category/components/filters/CategoryFilters.scss @@ -0,0 +1,59 @@ +.filters { + &__title { + --heading-title-font-size: var(--font-size--xl); + margin: var(--spacer-xl) 0 var(--spacer-base) 0; + + &:first-child { + margin: calc(var(--spacer-xl) + var(--spacer-base)) 0 var(--spacer-xs) 0; + } + } + + &__chosen { + color: var(--c-text-muted); + font-weight: var(--font-weight--normal); + font-family: var(--font-family--secondary); + position: absolute; + right: var(--spacer-xl); + } + + &__item { + --radio-container-padding: 0 var(--spacer-sm) 0 var(--spacer-xl); + --radio-background: transparent; + --filter-label-color: var(--c-secondary-variant); + --filter-count-color: var(--c-secondary-variant); + --checkbox-padding: 0 var(--spacer-sm) 0 var(--spacer-xl); + padding: var(--spacer-sm) 0; + border-bottom: 1px solid var(--c-light); + + &:last-child { + border-bottom: 0; + } + + @include for-desktop { + --checkbox-padding: 0; + margin: var(--spacer-sm) 0; + border: 0; + padding: 0; + } + } + + &__accordion-item { + --accordion-item-content-padding: 0; + position: relative; + left: 50%; + right: 50%; + margin-left: -50vw; + margin-right: -50vw; + width: 100vw; + } + + &__buttons { + margin: var(--spacer-sm) 0; + } + + &__button-clear { + --button-background: var(--c-light); + --button-color: var(--c-dark-variant); + margin: var(--spacer-xs) 0 0 0; + } +} diff --git a/packages/theme/modules/catalog/category/components/filters/CategoryFilters.vue b/packages/theme/modules/catalog/category/components/filters/CategoryFilters.vue new file mode 100644 index 000000000..1041f5590 --- /dev/null +++ b/packages/theme/modules/catalog/category/components/filters/CategoryFilters.vue @@ -0,0 +1,139 @@ + + + + diff --git a/packages/theme/modules/catalog/category/components/filters/__tests__/useFilters.spec.js b/packages/theme/modules/catalog/category/components/filters/__tests__/useFilters.spec.js new file mode 100644 index 000000000..94a374777 --- /dev/null +++ b/packages/theme/modules/catalog/category/components/filters/__tests__/useFilters.spec.js @@ -0,0 +1,35 @@ +import useFilters from '~/modules/catalog/category/components/filters/useFilters'; +import { getFilterableAttributes } from '~/modules/catalog/category/config/FiltersConfig'; + +import { useUiHelpers } from '~/composables'; + +jest.mock('~/composables', () => { + const originalModule = jest.requireActual('~/composables'); + + return { + ...originalModule, + useUiHelpers: jest.fn(), + }; +}); + +jest.mock('~/modules/catalog/category/config/FiltersConfig'); + +describe('useFilter', () => { + it('getSelectedFilters returns empty data if no filter is selected', () => { + useUiHelpers.mockReturnValue({ getFacetsFromURL: jest.fn(() => ({ filters: {} })) }); + getFilterableAttributes.mockReturnValue(['color']); + const { getSelectedFilters } = useFilters(); + expect(getSelectedFilters()).toMatchObject({ value: { color: [] } }); + }); + + it('getSelectedFilters returns selected filters from url data', () => { + useUiHelpers.mockReturnValue({ getFacetsFromURL: jest.fn(() => ({ filters: { color: ['50'] } })) }); + getFilterableAttributes.mockReturnValue(['color']); + const { getSelectedFilters } = useFilters(); + expect(getSelectedFilters()).toMatchObject({ value: { color: ['50'] } }); + }); + + it.todo('isFilterSelected returns true if filter is selected'); + it.todo('isFilterSelected returns false if filter is NOT selected'); + it.todo('selectFilter adds filter to the selected filters pool'); +}); diff --git a/packages/theme/modules/catalog/category/components/filters/renderer/CheckboxType.vue b/packages/theme/modules/catalog/category/components/filters/renderer/CheckboxType.vue new file mode 100644 index 000000000..fd59eec8e --- /dev/null +++ b/packages/theme/modules/catalog/category/components/filters/renderer/CheckboxType.vue @@ -0,0 +1,39 @@ + + diff --git a/packages/theme/modules/catalog/category/components/filters/renderer/RadioType.vue b/packages/theme/modules/catalog/category/components/filters/renderer/RadioType.vue new file mode 100644 index 000000000..9b9ebd64c --- /dev/null +++ b/packages/theme/modules/catalog/category/components/filters/renderer/RadioType.vue @@ -0,0 +1,42 @@ + + diff --git a/packages/theme/modules/catalog/category/components/filters/renderer/RendererTypesEnum.ts b/packages/theme/modules/catalog/category/components/filters/renderer/RendererTypesEnum.ts new file mode 100644 index 000000000..efb35dda7 --- /dev/null +++ b/packages/theme/modules/catalog/category/components/filters/renderer/RendererTypesEnum.ts @@ -0,0 +1,8 @@ +enum RendererTypesEnum { + RADIO = 'RadioType', + CHECKBOX = 'CheckboxType', + SWATCH_COLOR = 'SwatchColorType', + DEFAULT = 'CheckboxType', +} + +export default RendererTypesEnum; diff --git a/packages/theme/modules/catalog/category/components/filters/renderer/SwatchColorType.vue b/packages/theme/modules/catalog/category/components/filters/renderer/SwatchColorType.vue new file mode 100644 index 000000000..258a6fddc --- /dev/null +++ b/packages/theme/modules/catalog/category/components/filters/renderer/SwatchColorType.vue @@ -0,0 +1,48 @@ + + + diff --git a/packages/theme/modules/catalog/category/components/filters/useFilters.ts b/packages/theme/modules/catalog/category/components/filters/useFilters.ts new file mode 100644 index 000000000..0f2b31e54 --- /dev/null +++ b/packages/theme/modules/catalog/category/components/filters/useFilters.ts @@ -0,0 +1,66 @@ +import { ref } from '@nuxtjs/composition-api'; +import { useUiHelpers } from '~/composables'; +import { FacetInterface, GroupedFacetInterface } from '~/modules/catalog/category/types'; +import { getFilterConfig, getFilterableAttributes, FilterTypeEnum } from '~/modules/catalog/category/config/FiltersConfig'; + +export const useFilters = () => { + // @ts-ignore + const { getFacetsFromURL } = useUiHelpers(); + const getSelectedFilterValues = () => { + const availableFacets = getFilterableAttributes(); + + const selectedFilterValues = Object.fromEntries( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + availableFacets.map((curr: string) => [ + curr, + [], + ]), + ); + const { filters } = getFacetsFromURL(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + Object.keys(filters).forEach((filter) => { + selectedFilterValues[filter] = filters[filter]; + }); + + return selectedFilterValues; + }; + + const selectedFilters = ref(getSelectedFilterValues()); + const getSelectedFilters = () => selectedFilters; + + const isFilterSelected = (facet: GroupedFacetInterface, option: FacetInterface, filtersPool = getSelectedFilters().value) => { + const selected = (filtersPool[facet.id] || []).find((filterOpt) => filterOpt === option.value); + + return selected ?? ''; + }; + + const selectFilter = (facet: GroupedFacetInterface, option: FacetInterface) => { + const config = getFilterConfig(facet.id); + if (config.type === FilterTypeEnum.RADIO) { + selectedFilters.value[facet.id] = [option.value]; + return; + } + + if (!selectedFilters.value[facet.id]) { + selectedFilters.value[facet.id] = []; + } + + if (selectedFilters.value[facet.id].find((f) => f === option.value)) { + selectedFilters.value[facet.id] = selectedFilters.value[ + facet.id + ]?.filter((f) => f !== option.value); + return; + } + + selectedFilters.value[facet.id].push(option.value); + }; + + return { + getSelectedFilterValues, + isFilterSelected, + selectFilter, + getSelectedFilters, + }; +}; + +export default useFilters; diff --git a/packages/theme/modules/catalog/category/config/FiltersConfig.ts b/packages/theme/modules/catalog/category/config/FiltersConfig.ts new file mode 100644 index 000000000..85bfdc97e --- /dev/null +++ b/packages/theme/modules/catalog/category/config/FiltersConfig.ts @@ -0,0 +1,48 @@ +import RendererTypesEnum from '~/modules/catalog/category/components/filters/renderer/RendererTypesEnum'; + +export enum FilterTypeEnum { + RADIO = 'radio', + CHECKBOX = 'checkbox', + SWATCH_COLOR = 'swatch_color', +} + +export interface FilterConfigInterface { + attrCode: string; + component?: RendererTypesEnum; + type?: FilterTypeEnum; + [key: string]: any; +} + +/** + * Override this to add/modify filters set and data + */ +function config(): FilterConfigInterface[] { + return [ + { + attrCode: 'price', + type: FilterTypeEnum.RADIO, + component: RendererTypesEnum.RADIO, + }, + { + attrCode: 'size', + }, + { + attrCode: 'color', + type: FilterTypeEnum.SWATCH_COLOR, + component: RendererTypesEnum.SWATCH_COLOR, + }, + ]; +} + +export const getFilterConfig = (attrCode: string): FilterConfigInterface => { + const defaultCfg = { + attrCode, + type: FilterTypeEnum.CHECKBOX, + component: RendererTypesEnum.CHECKBOX, + }; + + const find = config().find((cfgItem) => cfgItem.attrCode === attrCode) ?? defaultCfg; + return { ...defaultCfg, ...find }; +}; + +export const getFilterableAttributes = () => config().map((filter) => filter.attrCode); diff --git a/packages/theme/modules/catalog/category/config/__tests__/filtersConfig.spec.js b/packages/theme/modules/catalog/category/config/__tests__/filtersConfig.spec.js new file mode 100644 index 000000000..03ab230c9 --- /dev/null +++ b/packages/theme/modules/catalog/category/config/__tests__/filtersConfig.spec.js @@ -0,0 +1,41 @@ +import { + getFilterConfig, getFilterableAttributes, FilterTypeEnum, +} from '../FiltersConfig'; +import RendererTypesEnum from '~/modules/catalog/category/components/filters/renderer/RendererTypesEnum'; + +describe('FiltersConfig', () => { + it('getFilterableAttributes', () => { + const result = getFilterableAttributes(); + expect(result).toEqual(['price', 'size', 'color']); + }); + + it('getFilterConfig with a configured attribute', () => { + const result = getFilterConfig('price'); + const expected = { + attrCode: 'price', + type: FilterTypeEnum.RADIO, + component: RendererTypesEnum.RADIO, + }; + expect(result).toEqual(expected); + }); + + it('getFilterConfig with a partially configured attribute', () => { + const result = getFilterConfig('size'); + const expected = { + attrCode: 'size', + type: FilterTypeEnum.CHECKBOX, + component: RendererTypesEnum.CHECKBOX, + }; + expect(result).toEqual(expected); + }); + + it('getFilterConfig with a not-configured attribute (default)', () => { + const result = getFilterConfig('ANYTHING'); + const expected = { + attrCode: 'ANYTHING', + type: FilterTypeEnum.CHECKBOX, + component: RendererTypesEnum.CHECKBOX, + }; + expect(result).toEqual(expected); + }); +}); diff --git a/packages/theme/modules/catalog/category/types.d.ts b/packages/theme/modules/catalog/category/types.d.ts index 2b90a9388..389464815 100644 --- a/packages/theme/modules/catalog/category/types.d.ts +++ b/packages/theme/modules/catalog/category/types.d.ts @@ -130,7 +130,7 @@ export interface AgnosticBreadcrumb { link: string; } -export interface AgnosticFacet { +export interface FacetInterface { type: string; id: string; value: any; @@ -140,6 +140,13 @@ export interface AgnosticFacet { metadata?: any; } +export interface GroupedFacetInterface { + id: string; + label: string; + count?: number; + options: FacetInterface[]; +} + export interface CategoryTreeInterface { label: string; slug?: string; diff --git a/packages/theme/modules/catalog/index.js b/packages/theme/modules/catalog/index.js index 1e010cbcd..cc2b6d501 100644 --- a/packages/theme/modules/catalog/index.js +++ b/packages/theme/modules/catalog/index.js @@ -1,11 +1,12 @@ import path from 'node:path'; +import url from 'node:url'; export default function CatalogModule() { this.extendRoutes((routes) => { routes.unshift({ name: 'category', path: '/c/:slug_1/:slug_2?/:slug_3?/:slug_4?/:slug_5?', - component: path.resolve(__dirname, 'pages/default.vue'), + component: path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), 'pages/category.vue'), }); }); } diff --git a/packages/theme/modules/catalog/pages/default.vue b/packages/theme/modules/catalog/pages/category.vue similarity index 64% rename from packages/theme/modules/catalog/pages/default.vue rename to packages/theme/modules/catalog/pages/category.vue index 3af46c882..70a5e2f8f 100644 --- a/packages/theme/modules/catalog/pages/default.vue +++ b/packages/theme/modules/catalog/pages/category.vue @@ -195,120 +195,12 @@ - - -
-
- -
- -
-
- -
-
- -
-
-
- -
- -
- -
-
- -
-
-
-
- -
-
+ @@ -316,25 +208,22 @@ + diff --git a/packages/theme/modules/catalog/category/components/filters/__tests__/useFilters.spec.js b/packages/theme/modules/catalog/category/components/filters/__tests__/useFilters.spec.js index 94a374777..7b5590971 100644 --- a/packages/theme/modules/catalog/category/components/filters/__tests__/useFilters.spec.js +++ b/packages/theme/modules/catalog/category/components/filters/__tests__/useFilters.spec.js @@ -1,5 +1,4 @@ import useFilters from '~/modules/catalog/category/components/filters/useFilters'; -import { getFilterableAttributes } from '~/modules/catalog/category/config/FiltersConfig'; import { useUiHelpers } from '~/composables'; @@ -15,18 +14,16 @@ jest.mock('~/composables', () => { jest.mock('~/modules/catalog/category/config/FiltersConfig'); describe('useFilter', () => { - it('getSelectedFilters returns empty data if no filter is selected', () => { + it('getSelectedFiltersFromUrl returns empty data if no filter is selected', () => { useUiHelpers.mockReturnValue({ getFacetsFromURL: jest.fn(() => ({ filters: {} })) }); - getFilterableAttributes.mockReturnValue(['color']); - const { getSelectedFilters } = useFilters(); - expect(getSelectedFilters()).toMatchObject({ value: { color: [] } }); + const { getSelectedFiltersFromUrl } = useFilters(); + expect(getSelectedFiltersFromUrl()).toMatchObject({}); }); - it('getSelectedFilters returns selected filters from url data', () => { + it('getSelectedFiltersFromUrl returns selected filters from url data', () => { useUiHelpers.mockReturnValue({ getFacetsFromURL: jest.fn(() => ({ filters: { color: ['50'] } })) }); - getFilterableAttributes.mockReturnValue(['color']); - const { getSelectedFilters } = useFilters(); - expect(getSelectedFilters()).toMatchObject({ value: { color: ['50'] } }); + const { getSelectedFiltersFromUrl } = useFilters(); + expect(getSelectedFiltersFromUrl()).toMatchObject({ color: ['50'] }); }); it.todo('isFilterSelected returns true if filter is selected'); diff --git a/packages/theme/modules/catalog/category/components/filters/command/getProductFilterByCategory.gql.ts b/packages/theme/modules/catalog/category/components/filters/command/getProductFilterByCategory.gql.ts new file mode 100644 index 000000000..b2c5521cb --- /dev/null +++ b/packages/theme/modules/catalog/category/components/filters/command/getProductFilterByCategory.gql.ts @@ -0,0 +1,22 @@ +import { gql } from 'graphql-request'; + +export default gql` + query getProductFiltersByCategory($categoryIdFilter: FilterEqualTypeInput!) { + products(filter: { category_uid: $categoryIdFilter }) { + aggregations { + label + count + attribute_code + options { + count + label + value + __typename + } + position + __typename + } + __typename + } + } +`; diff --git a/packages/theme/modules/catalog/category/components/filters/command/getProductFilterByCategoryCommand.ts b/packages/theme/modules/catalog/category/components/filters/command/getProductFilterByCategoryCommand.ts new file mode 100644 index 000000000..3b72d1cb5 --- /dev/null +++ b/packages/theme/modules/catalog/category/components/filters/command/getProductFilterByCategoryCommand.ts @@ -0,0 +1,14 @@ +import useApi from '~/composables/useApi'; +import { Aggregation, FilterEqualTypeInput } from '~/modules/GraphQL/types'; +import GetProductFilterByCategoryQuery from '~/modules/catalog/category/components/filters/command/getProductFilterByCategory.gql'; + +export const getProductFilterByCategoryCommand = { + execute: async (categoryIdFilter: FilterEqualTypeInput): Promise => { + const { query } = useApi(); + const data = await query(GetProductFilterByCategoryQuery, { categoryIdFilter }); + + return data?.products?.aggregations ?? []; + }, +}; + +export default getProductFilterByCategoryCommand; diff --git a/packages/theme/modules/catalog/category/components/filters/renderer/CheckboxType.vue b/packages/theme/modules/catalog/category/components/filters/renderer/CheckboxType.vue index fd59eec8e..d91a74da7 100644 --- a/packages/theme/modules/catalog/category/components/filters/renderer/CheckboxType.vue +++ b/packages/theme/modules/catalog/category/components/filters/renderer/CheckboxType.vue @@ -1,39 +1,43 @@ diff --git a/packages/theme/modules/catalog/category/components/filters/renderer/RadioType.vue b/packages/theme/modules/catalog/category/components/filters/renderer/RadioType.vue index 9b9ebd64c..326f94c94 100644 --- a/packages/theme/modules/catalog/category/components/filters/renderer/RadioType.vue +++ b/packages/theme/modules/catalog/category/components/filters/renderer/RadioType.vue @@ -1,42 +1,62 @@ + diff --git a/packages/theme/modules/catalog/category/components/filters/renderer/RendererTypesEnum.ts b/packages/theme/modules/catalog/category/components/filters/renderer/RendererTypesEnum.ts index efb35dda7..eeee1939c 100644 --- a/packages/theme/modules/catalog/category/components/filters/renderer/RendererTypesEnum.ts +++ b/packages/theme/modules/catalog/category/components/filters/renderer/RendererTypesEnum.ts @@ -2,6 +2,7 @@ enum RendererTypesEnum { RADIO = 'RadioType', CHECKBOX = 'CheckboxType', SWATCH_COLOR = 'SwatchColorType', + YES_NO = 'YesNoType', DEFAULT = 'CheckboxType', } diff --git a/packages/theme/modules/catalog/category/components/filters/renderer/SwatchColorType.vue b/packages/theme/modules/catalog/category/components/filters/renderer/SwatchColorType.vue index 258a6fddc..cffdf98b6 100644 --- a/packages/theme/modules/catalog/category/components/filters/renderer/SwatchColorType.vue +++ b/packages/theme/modules/catalog/category/components/filters/renderer/SwatchColorType.vue @@ -1,19 +1,21 @@ diff --git a/packages/theme/modules/catalog/category/components/filters/useFilters.ts b/packages/theme/modules/catalog/category/components/filters/useFilters.ts index 0f2b31e54..2301466da 100644 --- a/packages/theme/modules/catalog/category/components/filters/useFilters.ts +++ b/packages/theme/modules/catalog/category/components/filters/useFilters.ts @@ -1,21 +1,19 @@ -import { ref } from '@nuxtjs/composition-api'; +import { + ref, set, +} from '@nuxtjs/composition-api'; import { useUiHelpers } from '~/composables'; -import { FacetInterface, GroupedFacetInterface } from '~/modules/catalog/category/types'; -import { getFilterConfig, getFilterableAttributes, FilterTypeEnum } from '~/modules/catalog/category/config/FiltersConfig'; +import { getFilterConfig } from '~/modules/catalog/category/config/FiltersConfig'; +import { FilterTypeEnum } from '~/modules/catalog/category/config/config'; +import type { Aggregation, AggregationOption } from '~/modules/GraphQL/types'; + +export interface SelectedFiltersInterface {[p: string]: string[]} export const useFilters = () => { // @ts-ignore const { getFacetsFromURL } = useUiHelpers(); - const getSelectedFilterValues = () => { - const availableFacets = getFilterableAttributes(); - const selectedFilterValues = Object.fromEntries( - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - availableFacets.map((curr: string) => [ - curr, - [], - ]), - ); + const getSelectedFiltersFromUrl = () => { + const selectedFilterValues = {}; const { filters } = getFacetsFromURL(); // eslint-disable-next-line @typescript-eslint/no-unsafe-argument Object.keys(filters).forEach((filter) => { @@ -25,41 +23,46 @@ export const useFilters = () => { return selectedFilterValues; }; - const selectedFilters = ref(getSelectedFilterValues()); - const getSelectedFilters = () => selectedFilters; + const selectedFilters = ref(getSelectedFiltersFromUrl()); - const isFilterSelected = (facet: GroupedFacetInterface, option: FacetInterface, filtersPool = getSelectedFilters().value) => { - const selected = (filtersPool[facet.id] || []).find((filterOpt) => filterOpt === option.value); + const isFilterSelected = (name: string, value: string) => { + const selected = (selectedFilters.value[name] ?? []).find((selectedValue) => selectedValue === value); return selected ?? ''; }; - const selectFilter = (facet: GroupedFacetInterface, option: FacetInterface) => { - const config = getFilterConfig(facet.id); - if (config.type === FilterTypeEnum.RADIO) { - selectedFilters.value[facet.id] = [option.value]; - return; + const removeFilter = (attrCode: string, valToRemove: string) => { + if (!selectedFilters.value[attrCode]) return; + selectedFilters.value[attrCode] = selectedFilters.value[attrCode].filter((value) => value !== valToRemove); + }; + + const selectFilter = (filter: Aggregation, option: AggregationOption) => { + const config = getFilterConfig(filter.attribute_code); + if (!selectedFilters.value[filter.attribute_code]) { + set(selectedFilters.value, filter.attribute_code, []); } - if (!selectedFilters.value[facet.id]) { - selectedFilters.value[facet.id] = []; + if (config.type === FilterTypeEnum.RADIO) { + selectedFilters.value[filter.attribute_code] = [option.value]; + return; } - if (selectedFilters.value[facet.id].find((f) => f === option.value)) { - selectedFilters.value[facet.id] = selectedFilters.value[ - facet.id + if (selectedFilters.value[filter.attribute_code].find((f) => f === option.value)) { + selectedFilters.value[filter.attribute_code] = selectedFilters.value[ + filter.attribute_code ]?.filter((f) => f !== option.value); return; } - selectedFilters.value[facet.id].push(option.value); + selectedFilters.value[filter.attribute_code].push(String(option.value)); }; return { - getSelectedFilterValues, + getSelectedFiltersFromUrl, isFilterSelected, + removeFilter, selectFilter, - getSelectedFilters, + selectedFilters, }; }; diff --git a/packages/theme/modules/catalog/category/components/navbar/CategoryNavbar.vue b/packages/theme/modules/catalog/category/components/navbar/CategoryNavbar.vue index 8cc8234eb..62c30be02 100644 --- a/packages/theme/modules/catalog/category/components/navbar/CategoryNavbar.vue +++ b/packages/theme/modules/catalog/category/components/navbar/CategoryNavbar.vue @@ -73,7 +73,7 @@ -