From a766069a54340bf3eba33143121bf5bf016697fe Mon Sep 17 00:00:00 2001 From: Marcin Kwiatkowski Date: Thu, 10 Feb 2022 09:41:50 +0100 Subject: [PATCH] refactor: refactored useFacet composable --- .../src/composables/useFacet/_utils.ts | 2 + .../src/composables/useFacet/index.ts | 5 + packages/theme/components/AppHeader.vue | 4 +- packages/theme/composables/index.ts | 1 + packages/theme/composables/useFacet/_utils.ts | 76 ++++++++++ packages/theme/composables/useFacet/index.ts | 136 ++++++++++++++++++ .../theme/composables/useFacet/useFacet.d.ts | 23 +++ packages/theme/pages/Category.vue | 5 +- 8 files changed, 247 insertions(+), 5 deletions(-) create mode 100644 packages/theme/composables/useFacet/_utils.ts create mode 100644 packages/theme/composables/useFacet/index.ts create mode 100644 packages/theme/composables/useFacet/useFacet.d.ts diff --git a/packages/composables/src/composables/useFacet/_utils.ts b/packages/composables/src/composables/useFacet/_utils.ts index 83a4441e4..9d8297f2a 100644 --- a/packages/composables/src/composables/useFacet/_utils.ts +++ b/packages/composables/src/composables/useFacet/_utils.ts @@ -1,3 +1,5 @@ +// @depracated - moved to theme + import { SearchData } from '../../types'; const buildBreadcrumbsList = (rootCat, bc) => { diff --git a/packages/composables/src/composables/useFacet/index.ts b/packages/composables/src/composables/useFacet/index.ts index ea6fb2581..354a0195d 100644 --- a/packages/composables/src/composables/useFacet/index.ts +++ b/packages/composables/src/composables/useFacet/index.ts @@ -58,6 +58,11 @@ const constructSortObject = (sortData: string) => { return baseData.length > 0 ? Object.fromEntries([baseData]) : {}; }; +/** + * @deprecated since version + * + * @see + */ const factoryParams = { // eslint-disable-next-line @typescript-eslint/no-unused-vars search: async (context: Context, params: ComposableFunctionArgs>) => { diff --git a/packages/theme/components/AppHeader.vue b/packages/theme/components/AppHeader.vue index d10befc4d..92671470e 100644 --- a/packages/theme/components/AppHeader.vue +++ b/packages/theme/components/AppHeader.vue @@ -172,7 +172,6 @@ import { categoryGetters, useCart, useCategorySearch, - useFacet, wishlistGetters, } from '@vue-storefront/magento'; import { @@ -197,6 +196,7 @@ import { useUiState, useWishlist, useUser, + useFacet, } from '~/composables'; import StoreSwitcher from '~/components/StoreSwitcher.vue'; @@ -224,7 +224,7 @@ export default defineComponent({ result: searchResult, search: productsSearch, // loading: productsLoading, - } = useFacet('AppHeader:Products'); + } = useFacet(); const { result: categories, search: categoriesSearch, diff --git a/packages/theme/composables/index.ts b/packages/theme/composables/index.ts index 5a6368aeb..c56a87ba9 100644 --- a/packages/theme/composables/index.ts +++ b/packages/theme/composables/index.ts @@ -10,3 +10,4 @@ export { default as useWishlist } from './useWishlist'; export { default as useUser } from './useUser'; export { default as useForgotPassword } from './useForgotPassword'; export { default as useCategory } from './useCategory'; +export { default as useFacet } from './useFacet'; diff --git a/packages/theme/composables/useFacet/_utils.ts b/packages/theme/composables/useFacet/_utils.ts new file mode 100644 index 000000000..2a5252dc0 --- /dev/null +++ b/packages/theme/composables/useFacet/_utils.ts @@ -0,0 +1,76 @@ +import { SearchData } from './useFacet'; + +const buildBreadcrumbsList = (rootCat, bc) => { + const newBc = [...bc, { + text: rootCat.name, + link: rootCat.slug, + }]; + return rootCat.parent ? buildBreadcrumbsList(rootCat.parent, newBc) : newBc; +}; + +export const buildBreadcrumbs = (rootCat) => buildBreadcrumbsList(rootCat, []) + .reverse() + .reduce( + (prev, curr, index) => ([ + ...prev, + { + ...curr, + link: `${prev[index - 1]?.link || ''}/${curr.link}`, + }]), + [], + ); + +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] : []; + return options + .map(({ + label, + value, + count, + }) => ({ + type: getFacetTypeByCode(facet.attribute_code), + id: label, + attrName: label, + value, + selected: selectedList.includes(value), + count, + })); +}; + +export const reduceForFacets = (facets, filters) => (prev, curr) => ([ + ...prev, + ...createFacetsFromOptions(facets, filters, curr), +]); + +export const reduceForGroupedFacets = (facets, filters) => (prev, curr) => ([ + ...prev, + { + id: curr.attribute_code, + label: curr.label, + options: createFacetsFromOptions(facets, filters, curr), + count: null, + }, +]); + +export const buildFacets = (searchData: SearchData, reduceFn, criteria?: string[]) => { + if (!searchData.data) { + return []; + } + + const { + data: { availableFilters: facets }, + input: { filters }, + } = searchData; + + return facets?.filter(filterFacets(criteria)).reduce(reduceFn(facets, filters), []); +}; diff --git a/packages/theme/composables/useFacet/index.ts b/packages/theme/composables/useFacet/index.ts new file mode 100644 index 000000000..bb1252c1e --- /dev/null +++ b/packages/theme/composables/useFacet/index.ts @@ -0,0 +1,136 @@ +import { Ref, ref, useContext } from '@nuxtjs/composition-api'; +import { + AgnosticFacetSearchParams, ComposableFunctionArgs, Logger, ProductsSearchParams } from '@vue-storefront/core'; +import { FacetSearchResult, UseFacet, UseFacetErrors} from './useFacet'; +import { GetProductSearchParams } from '@vue-storefront/magento-api/src/types/API'; + +const availableSortingOptions = [ + { + label: 'Sort: Default', + value: '', + }, + { + label: 'Sort: Name A-Z', + value: 'name_ASC', + }, + { + label: 'Sort: Name Z-A', + value: 'name_DESC', + }, + { + label: 'Sort: Price from low to high', + value: 'price_ASC', + }, { + label: 'Sort: Price from high to low', + value: 'price_DESC', + }, +]; + +const constructFilterObject = (inputFilters: Object) => { + const filter = {}; + + Object.keys(inputFilters).forEach((key) => { + if (key === 'price') { + const price = { from: 0, to: 0 }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const flatPrices = inputFilters[key].flatMap((inputFilter) => inputFilter.split('_').map((str) => Number.parseFloat(str))).sort((a, b) => a - b); + + [price.from] = flatPrices; + price.to = flatPrices[flatPrices.length - 1]; + + filter[key] = price; + } else if (typeof inputFilters[key] === 'string') { + filter[key] = { in: [inputFilters[key]] }; + } else { + filter[key] = { in: inputFilters[key] }; + } + }); + + return filter; +}; + +const constructSortObject = (sortData: string) => { + const baseData = sortData.split(/_/gi); + + return baseData.length > 0 ? Object.fromEntries([baseData]) : {}; +}; + +export const useFacet = (): UseFacet => { + const { app } = useContext(); + const loading: Ref = ref(false); + const result: Ref> = ref({ data: null, input: null }); + const error: Ref = ref({ + search: null, + }); + + const search = async (params?: ComposableFunctionArgs) => { + Logger.debug('useFacet/search', params); + + result.value.input = params; + try { + loading.value = true; + + const itemsPerPage = (params.itemsPerPage) ? params.itemsPerPage : 20; + const inputFilters = (params.filters) ? params.filters : {}; + const categoryId = (params.categoryId) ? { + category_uid: { + ...(Array.isArray(params.categoryId) + ? { in: params.categoryId } + : { eq: params.categoryId }), + }, + } : {}; + + const productParams: ProductsSearchParams = { + filter: { + ...categoryId, + ...constructFilterObject({ + ...inputFilters, + }), + }, + perPage: itemsPerPage, + offset: (params.page - 1) * itemsPerPage, + page: params.page, + search: (params.term) ? params.term : '', + sort: constructSortObject(params.sort || ''), + }; + + const productSearchParams: GetProductSearchParams = { + pageSize: productParams.perPage, + search: productParams.search, + filter: productParams.filter, + sort: productParams.sort, + currentPage: productParams.page, + }; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const { data } = await app.context.$vsf.$magento.api.products(productSearchParams, params?.customQuery || { products: 'products' }); + + Logger.debug('[Result]:', { data }); + + result.value.data = { + items: data?.products?.items || [], + total: data?.products?.total_count, + availableFilters: data?.products?.aggregations, + category: { id: params.categoryId }, + availableSortingOptions, + perPageOptions: [10, 20, 50], + itemsPerPage, + }; + error.value.search = null; + } catch (err) { + error.value.search = err; + Logger.error(`useFacet/search`, err); + } finally { + loading.value = false; + } + }; + + return { + result, + loading, + error, + search, + }; +}; + +export default useFacet; diff --git a/packages/theme/composables/useFacet/useFacet.d.ts b/packages/theme/composables/useFacet/useFacet.d.ts new file mode 100644 index 000000000..d630ad378 --- /dev/null +++ b/packages/theme/composables/useFacet/useFacet.d.ts @@ -0,0 +1,23 @@ +import { AgnosticFacetSearchParams, ComposableFunctionArgs} from '@vue-storefront/core'; +import { Ref } from '@nuxtjs/composition-api'; +// @ts-ignore +import { FacetResultsData } from '@vue-storefront/magento/types'; + +export interface FacetSearchResult { + data: S; + input: AgnosticFacetSearchParams; +} + +export interface UseFacetErrors { + search: Error; +} + + +export type SearchData = FacetSearchResult; + +export interface UseFacet { + result: Ref>; + loading: Ref; + search: (params?: ComposableFunctionArgs) => Promise; + error: Ref; +} diff --git a/packages/theme/pages/Category.vue b/packages/theme/pages/Category.vue index 172ed5c40..91cf152b8 100644 --- a/packages/theme/pages/Category.vue +++ b/packages/theme/pages/Category.vue @@ -425,13 +425,12 @@ import { categoryGetters, facetGetters, productGetters, - useFacet, } from '@vue-storefront/magento'; import { onSSR, useVSFContext } from '@vue-storefront/core'; import { useCache, CacheTagPrefix } from '@vue-storefront/cache'; import { useUrlResolver } from '~/composables/useUrlResolver.ts'; import { - useUiHelpers, useUiState, useImage, useWishlist, useUser, useCategory, + useUiHelpers, useUiState, useImage, useWishlist, useUser, useCategory, useFacet } from '~/composables'; import cacheControl from '~/helpers/cacheControl'; import { useAddToCart } from '~/helpers/cart/addToCart'; @@ -484,7 +483,7 @@ export default defineComponent({ const { result, search, - } = useFacet(`facetId:${path}`); + } = useFacet(); const { toggleFilterSidebar } = useUiState(); const { categories,