Skip to content

Commit ee01961

Browse files
committed
feat: add configurable filters
- filters can now be whitelisted - update how yes_no filter is rendered on the applied filrer list - update types for removable filters - add docs for the filters managing
1 parent 12bf594 commit ee01961

File tree

9 files changed

+88
-18
lines changed

9 files changed

+88
-18
lines changed

docs/.vuepress/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ module.exports = {
7373
children: [
7474
['/guide/image-optimization', 'Image optimization'],
7575
['/guide/override-queries', 'Override queries'],
76+
['/guide/filters', 'Filters'],
7677
['/guide/testing', 'Testing'],
7778
['/guide/recaptcha', 'ReCaptcha'],
7879
],

docs/guide/filters.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Filters
2+
Filters are filterable attributes available on the category page to narrow the result set. Natively Magetno 2 supports a lot of configuration options for each attribute including renderer, where to display the attribute or how to do that. Unfortunately GraphQL has its limitations and most of the filter options are unavailable for headless fronted purposes. That is why we introduced extensive and configurable implementation to address that issue.
3+
4+
### Renderers
5+
In the core we implemented four most commonly used renderer types:
6+
7+
* Checkbox
8+
* Radio
9+
* Color Swatch
10+
* Yes No
11+
12+
You can find all of them in `<root>/modules/catalog/category/components/filters/renderer`
13+
14+
### Configuration
15+
Filter's config file is located in `<root>/modules/catalog/category/config/config.ts`. You can configure what filters you want to display, which renderer to use, what is the type of filter and eventually disable some of them. The list of filters is tha actual set of filter that will be displayed in the sidebar on the category page. If there is no filter configured - at least its code must be set - it will be not displayed.
16+
The default configuration fallback is as follows
17+
18+
```javascript
19+
const defaultCfg = {
20+
attrCode,
21+
type: FilterTypeEnum.CHECKBOX,
22+
component: RendererTypesEnum.CHECKBOX,
23+
disabled: false,
24+
};
25+
```
26+
As you can see only the `attrCode` is required because it is not possible to guess or assume this parameter.
27+
28+
### Query
29+
The command responsible for fetching all filters is `<root>>/modules/catalog/category/components/filters/command/getProductFilterByCategoryCommand.ts` and the query `<root>/modules/catalog/category/components/filters/command/getProductFilterByCategory.gql.ts`. This is the best place to modify filters fetching logic.

packages/theme/modules/catalog/category/components/filters/CategoryFilters.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ import {
127127
128128
import SkeletonLoader from '~/components/SkeletonLoader/index.vue';
129129
import { useUiHelpers } from '~/composables';
130-
import { getFilterConfig, getDisabledFilters } from '~/modules/catalog/category/config/FiltersConfig';
130+
import { getFilterConfig, isFilterEnabled } from '~/modules/catalog/category/config/FiltersConfig';
131131
import SelectedFilters from '~/modules/catalog/category/components/filters/FiltersSidebar/SelectedFilters.vue';
132132
import { getProductFilterByCategoryCommand } from '~/modules/catalog/category/components/filters/command/getProductFilterByCategoryCommand';
133133
@@ -208,7 +208,7 @@ export default defineComponent({
208208
209209
onMounted(async () => {
210210
const loadedFilters = await getProductFilterByCategoryCommand.execute({ eq: props.catUid });
211-
filters.value = loadedFilters.filter((filter) => !getDisabledFilters().includes(filter.attribute_code));
211+
filters.value = loadedFilters.filter((filter) => isFilterEnabled(filter.attribute_code));
212212
updateRemovableFilters();
213213
isLoading.value = false;
214214
});

packages/theme/modules/catalog/category/components/filters/FiltersSidebar/SelectedFilters.vue

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
>
88
<HTMLContent
99
class="selected-filter__label"
10-
:tag="span"
11-
:content="filter.label"
10+
tag="span"
11+
:content="getLabel(filter)"
1212
/>
1313
<button
1414
type="button"
@@ -27,9 +27,11 @@
2727
</div>
2828
</template>
2929
<script lang="ts">
30-
import { defineComponent } from '@nuxtjs/composition-api';
30+
import { defineComponent, PropType, useContext } from '@nuxtjs/composition-api';
3131
import { SfBadge, SfIcon } from '@storefront-ui/vue';
3232
import HTMLContent from '~/components/HTMLContent.vue';
33+
import type { RemovableFilterInterface } from '~/modules/catalog/category/components/filters/useFilters';
34+
import { FilterTypeEnum } from '~/modules/catalog/category/config/config';
3335
3436
export default defineComponent({
3537
components: {
@@ -39,10 +41,26 @@ export default defineComponent({
3941
},
4042
props: {
4143
removableFilters: {
42-
type: Array,
44+
type: Array as PropType<RemovableFilterInterface[]>,
4345
default: () => [],
4446
},
4547
},
48+
setup(props) {
49+
const { app: { i18n } } = useContext();
50+
console.log(props.removableFilters);
51+
const getLabel = (filter: RemovableFilterInterface) => {
52+
if (filter.type === FilterTypeEnum.YES_NO) {
53+
const yesNo = filter.label === '1' ? i18n.t('Yes') : i18n.t('No');
54+
return `${filter.name}: ${yesNo}`;
55+
}
56+
57+
return filter.label;
58+
};
59+
60+
return {
61+
getLabel,
62+
};
63+
},
4664
});
4765
</script>
4866
<style lang="scss">

packages/theme/modules/catalog/category/components/filters/renderer/YesNoType.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ export default defineComponent({
3535
const { isFilterSelected } = inject('UseFiltersProvider');
3636
const selected = computed(() => ((id: string, optVal: string) => isFilterSelected(id, optVal)));
3737
const label = (option: AggregationOption) => `${(option.value === '1' ? i18n.t('Yes') : i18n.t('No'))} ${`(${option.count})`}`;
38-
3938
return { selected, label };
4039
},
4140
});

packages/theme/modules/catalog/category/components/filters/useFilters.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ import type { Aggregation, AggregationOption } from '~/modules/GraphQL/types';
88

99
export interface SelectedFiltersInterface {[p: string]: string[]}
1010

11+
export interface RemovableFilterInterface {
12+
id: string;
13+
name: string;
14+
label: string;
15+
value: string;
16+
type: string;
17+
}
18+
1119
export function useFilters() {
1220
// @ts-ignore
1321
const { getFacetsFromURL } = useUiHelpers();
@@ -42,7 +50,7 @@ export function useFilters() {
4250
set(selectedFilters.value, filter.attribute_code, []);
4351
}
4452

45-
if (config.type === FilterTypeEnum.RADIO) {
53+
if (config.type === FilterTypeEnum.RADIO || config.type === FilterTypeEnum.YES_NO) {
4654
selectedFilters.value[filter.attribute_code] = [option.value];
4755
return;
4856
}
@@ -57,14 +65,16 @@ export function useFilters() {
5765
selectedFilters.value[filter.attribute_code].push(String(option.value));
5866
};
5967

60-
const getRemovableFilters = (filters: Aggregation[], selected: SelectedFiltersInterface) => {
68+
const getRemovableFilters = (filters: Aggregation[], selected: SelectedFiltersInterface): RemovableFilterInterface[] => {
6169
const result = [];
6270

6371
filters.forEach((filter) => {
6472
filter.options.forEach((option) => {
6573
if ((selected[filter.attribute_code] ?? []).includes(option.value)) {
74+
const filterConfig = getFilterConfig(filter.attribute_code);
75+
6676
result.push({
67-
id: filter.attribute_code, name: filter.label, label: option.label, value: option.value,
77+
id: filter.attribute_code, name: filter.label, label: option.label, value: option.value, type: filterConfig.type,
6878
});
6979
}
7080
});

packages/theme/modules/catalog/category/config/FiltersConfig.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ export const getFilterConfig = (attrCode: string): FilterConfigInterface => {
1313
attrCode,
1414
type: FilterTypeEnum.CHECKBOX,
1515
component: RendererTypesEnum.CHECKBOX,
16+
disabled: false,
1617
};
1718

1819
const find = config().find((cfgItem) => cfgItem.attrCode === attrCode) ?? {};
1920
return { ...defaultCfg, ...find };
2021
};
2122

2223
export const getDisabledFilters = () => config().filter((filter) => filter.disabled).map((filter) => filter.attrCode);
24+
export const isFilterEnabled = (attrCode: string) => config().find((attr) => attr.attrCode === attrCode && !attr.disabled);

packages/theme/modules/catalog/category/config/__tests__/filtersConfig.spec.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import config, { FilterTypeEnum } from '~/modules/catalog/category/config/config';
22
import RendererTypesEnum from '~/modules/catalog/category/components/filters/renderer/RendererTypesEnum';
33
import {
4-
getFilterConfig, getDisabledFilters,
4+
getFilterConfig, getDisabledFilters, isFilterEnabled,
55
} from '../FiltersConfig';
66

77
jest.mock('~/modules/catalog/category/config/config');
@@ -72,4 +72,10 @@ describe('FiltersConfig', () => {
7272
const expected = ['sale'];
7373
expect(result).toEqual(expected);
7474
});
75+
76+
it('isFilterEnabled', () => {
77+
(config as jest.Mock).mockReturnValue(defaultFiltersConfig);
78+
expect(isFilterEnabled('invalid')).toBeFalsy();
79+
expect(isFilterEnabled('size')).toBeTruthy();
80+
});
7581
});

packages/theme/modules/catalog/category/config/config.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export enum FilterTypeEnum {
55
RADIO = 'radio',
66
CHECKBOX = 'checkbox',
77
SWATCH_COLOR = 'swatch_color',
8+
YES_NO = 'yes_no',
89
}
910

1011
/**
@@ -25,24 +26,28 @@ export default function config(): FilterConfigInterface[] {
2526
{
2627
attrCode: 'size',
2728
},
29+
{
30+
attrCode: 'material',
31+
},
32+
{
33+
attrCode: 'strap_bags',
34+
},
35+
{
36+
attrCode: 'style_bottom',
37+
},
2838
{
2939
attrCode: 'color',
3040
type: FilterTypeEnum.SWATCH_COLOR,
3141
component: RendererTypesEnum.SWATCH_COLOR,
3242
},
3343
{
3444
attrCode: 'new',
35-
type: FilterTypeEnum.RADIO,
36-
component: RendererTypesEnum.YES_NO,
37-
},
38-
{
39-
attrCode: 'performance_fabric',
40-
type: FilterTypeEnum.RADIO,
45+
type: FilterTypeEnum.YES_NO,
4146
component: RendererTypesEnum.YES_NO,
4247
},
4348
{
4449
attrCode: 'sale',
45-
type: FilterTypeEnum.RADIO,
50+
type: FilterTypeEnum.YES_NO,
4651
component: RendererTypesEnum.YES_NO,
4752
},
4853
];

0 commit comments

Comments
 (0)