-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Refactor catalog module #2890
Changes from 58 commits
5d2827c
3889092
9d2462a
16ca33e
0207857
087d45e
e834b21
d372696
59f2130
8a9d28d
d2b4664
23e70d7
c60508d
54a231b
f45fe5b
d027a4c
8e71fdf
8f9e795
f5cdd82
0a9f7d5
f979051
fece9d5
fb380e7
bd32c8c
9084808
487a847
a819f6f
25eabd7
652afe0
767aa6d
4ee0279
97e0981
adc60ad
d97b2a1
eab2243
8fd4f42
af8db3a
98034ef
35293e3
b47b34b
7d4633b
e7bdcf2
93d2588
771d734
fa52a84
f0a7259
738ddce
6208010
4a687c5
abfa197
ed6492f
0549431
447130b
6867930
c648c3c
e4665e0
2e9e140
541add0
c908fdb
02f542a
07a52d1
9d6909e
bce5a2d
d1ca817
67087ea
867f582
37aca5e
64aeff6
0fc7b6d
ba9f93b
94e5d23
3da38c0
35638d3
7329923
097eebb
cfafe84
b868176
dee2ead
e345a04
a0e993d
9659e6f
0f61390
aa41c09
8545d57
52e8854
61ae322
6153d69
779ce67
f31ac67
ddc3cde
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { SearchRequest } from '@vue-storefront/core/types/search/SearchRequest'; | ||
import { quickSearchByQuery } from '@vue-storefront/core/lib/search'; | ||
import SearchQuery from '@vue-storefront/core/lib/search/searchQuery'; | ||
import config from 'config'; | ||
|
||
export interface Product { | ||
id: number, | ||
name: any | ||
} | ||
|
||
const getProduct = async (): Promise<Product> => { | ||
const product: Product = { | ||
id: 1, | ||
name: 'sample' | ||
} | ||
return product | ||
} | ||
|
||
function getProducts (): Product[] { | ||
return [{ | ||
id: 1, | ||
name: 'sample1' | ||
}, { | ||
id: 2, | ||
name: 'sample2' | ||
} | ||
] | ||
} | ||
|
||
const getCategories = async ({parent = null, key = null, value = null, level = null, onlyActive = true, onlyNotEmpty = false, size = 4000, start = 0, sort = 'position:asc', includeFields = config.entities.optimize ? config.entities.category.includeFields : null, excludeFields = config.entities.optimize ? config.entities.category.excludeFields : null} = {}) => { | ||
let searchQuery = new SearchQuery() | ||
if (parent) { | ||
searchQuery = searchQuery.applyFilter({key: 'parent_id', value: {'eq': parent.id ? parent.id : parent}}) | ||
} | ||
if (level) { | ||
searchQuery = searchQuery.applyFilter({key: 'level', value: {'eq': level}}) | ||
} | ||
|
||
if (key) { | ||
if (Array.isArray(value)) { | ||
searchQuery = searchQuery.applyFilter({key: key, value: {'in': value}}) | ||
} else { | ||
searchQuery = searchQuery.applyFilter({key: key, value: {'eq': value}}) | ||
} | ||
} | ||
|
||
if (onlyActive === true) { | ||
searchQuery = searchQuery.applyFilter({key: 'is_active', value: {'eq': true}}) | ||
} | ||
|
||
if (onlyNotEmpty === true) { | ||
searchQuery = searchQuery.applyFilter({key: 'product_count', value: {'gt': 0}}) | ||
} | ||
const response = await quickSearchByQuery({ entityType: 'category', query: searchQuery, sort: sort, size: size, start: start, includeFields: includeFields, excludeFields: excludeFields }) | ||
return response.items | ||
} | ||
|
||
interface ProductResolver { | ||
getProduct: (searchRequest: SearchRequest) => Promise<Product>, | ||
getProducts: () => Product[] | ||
} | ||
|
||
interface CategoryService { | ||
getCategories: (searchRequest?: any) => Promise<any> | ||
} | ||
|
||
export const ProductResolver: ProductResolver = { | ||
getProduct, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't it be both services or both resolvers? |
||
getProducts | ||
} | ||
|
||
export const CategoryService: CategoryService = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. put export at the bottom of file, it's easier to read |
||
getCategories | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { parseCategoryPath } from '@vue-storefront/core/modules/breadcrumbs/helpers' | ||
|
||
export const compareByLabel = (a, b) => { | ||
if (a.label < b.label) { | ||
return -1 | ||
} | ||
if (a.label > b.label) { | ||
return 1 | ||
} | ||
return 0 | ||
} | ||
|
||
export const calculateBreadcrumbs = (categories, id, list = []) => { | ||
const category = categories.find(category => category.id === id) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is wrong assumption - that we have all the categories loaded when the https://docs.vuestorefront.io/guide/basics/configuration.html#dynamic-categories-prefetching is on. However, we can optimize - and in the Even as we don't have it in the state (so if even it's not outputed to the |
||
if (!category) return parseCategoryPath(list).reverse() | ||
const result = [...list, category] | ||
|
||
if (category.level > 1 && category.parent_id) { | ||
return calculateBreadcrumbs(categories, category.parent_id, result) | ||
} | ||
return parseCategoryPath(result).reverse() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import FilterVariant from 'core/modules/catalog-next/types/FilterVariant'; | ||
|
||
export const getSystemFilterNames: string[] = ['sort'] | ||
|
||
/** | ||
* Creates new filtersQuery (based on currentQuery) by modifying specific filter variant. | ||
*/ | ||
export const changeFilterQuery = ({currentQuery = {}, filterVariant}: {currentQuery?: any, filterVariant?: FilterVariant} = {}) => { | ||
const newQuery = JSON.parse(JSON.stringify(currentQuery)) | ||
if (!filterVariant) return newQuery | ||
if (getSystemFilterNames.includes(filterVariant.type)) { | ||
if (newQuery[filterVariant.type] && newQuery[filterVariant.type] === filterVariant.id) { | ||
delete newQuery[filterVariant.type] | ||
} else { | ||
newQuery[filterVariant.type] = filterVariant.id | ||
} | ||
} else { | ||
let queryFilter = newQuery[filterVariant.type] || [] | ||
if (!Array.isArray(queryFilter)) queryFilter = [queryFilter] | ||
if (queryFilter.includes(filterVariant.id)) { | ||
queryFilter = queryFilter.filter(value => value !== filterVariant.id) | ||
} else { | ||
queryFilter.push(filterVariant.id) | ||
} | ||
newQuery[filterVariant.type] = queryFilter | ||
} | ||
return newQuery | ||
} | ||
|
||
export const getFiltersFromQuery = ({filtersQuery = {}, availableFilters = {}} = {}) => { | ||
const searchQuery = { | ||
filters: {} | ||
} | ||
Object.keys(filtersQuery).forEach(filterKey => { | ||
const filter = availableFilters[filterKey] | ||
const queryValue = filtersQuery[filterKey] | ||
if (!filter) return | ||
if (getSystemFilterNames.includes(filterKey)) { | ||
searchQuery[filterKey] = queryValue | ||
} else if (Array.isArray(queryValue)) { | ||
queryValue.map(singleValue => { | ||
const variant = filter.find(filterVariant => filterVariant.id === singleValue) | ||
if (!variant) return | ||
if (!searchQuery.filters[filterKey] || !Array.isArray(searchQuery.filters[filterKey])) searchQuery.filters[filterKey] = [] | ||
searchQuery.filters[filterKey].push({...variant, attribute_code: filterKey}) | ||
}) | ||
} else { | ||
const variant = filter.find(filterVariant => filterVariant.id === queryValue) | ||
if (!variant) return | ||
searchQuery.filters[filterKey] = {...variant, attribute_code: filterKey} | ||
} | ||
}) | ||
return searchQuery | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
/** | ||
* Helper method for getting attribute name - TODO: to be moved to external/shared helper | ||
* | ||
* @param {String} attributeCode | ||
* @param {String} optionId - value to get label for | ||
*/ | ||
import toString from 'lodash-es/toString' | ||
|
||
export function optionLabel (state, { attributeKey, searchBy = 'code', optionId }) { | ||
let attrCache = state.labels[attributeKey] | ||
|
||
if (attrCache) { | ||
let label = attrCache[optionId] | ||
|
||
if (label) { | ||
return label | ||
} | ||
} | ||
let attr = state['list_by_' + searchBy][attributeKey] | ||
if (attr) { | ||
let opt = attr.options.find((op) => { // TODO: cache it in memory | ||
if (toString(op.value) === toString(optionId)) { | ||
return op | ||
} | ||
}) // TODO: i18n support with multi-website attribute names | ||
|
||
if (opt) { | ||
if (!state.labels[attributeKey]) { | ||
state.labels[attributeKey] = {} | ||
} | ||
state.labels[attributeKey][optionId] = opt.label | ||
return opt ? opt.label : optionId | ||
} else { | ||
return optionId | ||
} | ||
} else { | ||
return optionId | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// import { productModule } from './store/product' | ||
// import { attributeModule } from './store/attribute' | ||
// import { stockModule } from './store/stock' | ||
// import { taxModule } from './store/tax' | ||
import { categoryModule } from './store/category' | ||
import { createModule } from '@vue-storefront/core/lib/module' | ||
// import { beforeRegistration } from './hooks/beforeRegistration' | ||
|
||
export const KEY = 'catalog-next' | ||
export default createModule({ | ||
key: KEY, | ||
store: { modules: [ | ||
{ key: 'category-next', module: categoryModule } | ||
] } | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export default interface CategoryState { | ||
categories: any, | ||
availableFilters: any, | ||
products: any | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
// import Vue from 'vue' | ||
import { ActionTree } from 'vuex' | ||
import * as types from './mutation-types' | ||
import RootState from '@vue-storefront/core/types/RootState' | ||
import CategoryState from './CategoryState' | ||
import { quickSearchByQuery } from '@vue-storefront/core/lib/search' | ||
import { buildFilterProductsQuery } from '@vue-storefront/core/helpers' | ||
import { router } from '@vue-storefront/core/app' | ||
import FilterVariant from '../../types/FilterVariant' | ||
import { CategoryService } from '@vue-storefront/core/data-resolver' | ||
import { changeFilterQuery } from '../../helpers/filterHelpers' | ||
import { products } from 'config' | ||
import { configureProductAsync } from '@vue-storefront/core/modules/catalog/helpers' | ||
|
||
const actions: ActionTree<CategoryState, RootState> = { | ||
/** | ||
* Initialise category module. | ||
* - gets available categories | ||
* - gets available filters for current category | ||
*/ | ||
async initCategoryModule ({ getters, dispatch }) { | ||
if (!getters.getCategories.length) { | ||
await dispatch('loadCategories') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure but it seems like your approach is to load all the categories. Please remember that the category tree can be as big as up to few mega bytes (take the https://wonect.com). From VS 1.7 categories are dynamically loaded (only the root level - then after clicking in the |
||
} | ||
}, | ||
async loadCategoryProducts ({ commit, getters, dispatch }, { route } = {}) { | ||
await dispatch('initCategoryModule') | ||
const searchCategory = getters.getCategoryFrom(route.path) | ||
await dispatch('loadCategoryFilters', searchCategory) | ||
const searchQuery = getters.getCurrentFiltersFrom(route[products.routerFiltersSource]) | ||
let filterQr = buildFilterProductsQuery(searchCategory, searchQuery.filters) | ||
const searchResult = await quickSearchByQuery({ query: filterQr, sort: searchQuery.sort }) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if'we re using the It seems like not. Moreover, we're not running these 2 calls with the doublecheck (if it gains the performance) as it was originally done. It should work exact the same way There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moreover, the stock prefetching mechanism is also not ported to this refactored version: https://github.com/DivanteLtd/vue-storefront/blob/70543b3cfa379971220f6514348fe0386a108878/core/modules/catalog/store/category/actions.ts#L267 Without prefetching the stocks - when the |
||
let configuredProducts = searchResult.items.map(product => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is still a progressive caching feature missing plus the stock info prefetching - also missing - comparing to category/products. Not sure why we don’t use product/list here but just wuickSearchByQuery. produxt/list also supports the caching by sku plus group products prefetching plus “syncPricesOver” feature which is pretty popular among our users. What’s the plan to have these features still supported @patzick? |
||
const configuredProductVariant = configureProductAsync({}, {product, configuration: searchQuery.filters, selectDefaultVariant: false, fallbackToDefaultWhenNoAvailable: true, setProductErorrs: false}) | ||
return Object.assign(product, configuredProductVariant) | ||
}) | ||
commit(types.CATEGORY_SET_PRODUCTS, configuredProducts) | ||
// await dispatch('loadAvailableFiltersFrom', searchResult) | ||
|
||
return searchResult.items | ||
}, | ||
async cacheProducts ({ commit, getters, dispatch }, { route } = {}) { | ||
const searchCategory = getters.getCategoryFrom(route.path) | ||
const searchQuery = getters.getCurrentFiltersFrom(route[products.routerFiltersSource]) | ||
let filterQr = buildFilterProductsQuery(searchCategory, searchQuery.filters) | ||
|
||
console.error('CACHE 2 step...') | ||
const xx = await dispatch('product/list', { | ||
query: filterQr, | ||
sort: searchQuery.sort, | ||
updateState: false // not update the product listing - this request is only for caching | ||
}, { root: true }) | ||
console.error('RETURNED PRODUCTS', xx) | ||
|
||
// return searchResult.items | ||
}, | ||
async findCategories () { | ||
return CategoryService.getCategories() | ||
}, | ||
async loadCategories ({ commit }) { | ||
const categories = await CategoryService.getCategories() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're not registergin the |
||
commit(types.CATEGORY_SET_CATEGORIES, categories) | ||
return categories | ||
}, | ||
/** | ||
* Fetch and process filters from current category and sets them in available filters. | ||
*/ | ||
async loadCategoryFilters ({ dispatch, getters }, category) { | ||
const searchCategory = category || getters.getCurrentCategory | ||
let filterQr = buildFilterProductsQuery(searchCategory) | ||
const searchResult = await quickSearchByQuery({ query: filterQr }) | ||
await dispatch('loadAvailableFiltersFrom', searchResult) | ||
}, | ||
async loadAvailableFiltersFrom ({ commit, getters }, {aggregations}) { | ||
const filters = getters.getAvailableFiltersFrom(aggregations) | ||
commit(types.CATEGORY_SET_AVAILABLE_FILTERS, filters) | ||
}, | ||
async switchSearchFilter ({ dispatch }, filterVariant: FilterVariant) { | ||
const newQuery = changeFilterQuery({currentQuery: router.currentRoute[products.routerFiltersSource], filterVariant}) | ||
await dispatch('changeRouterFilterParameters', newQuery) | ||
}, | ||
async resetFilters ({dispatch}) { | ||
await dispatch('changeRouterFilterParameters', {}) | ||
}, | ||
async changeRouterFilterParameters (context, query) { | ||
router.push({[products.routerFiltersSource]: query}) | ||
} | ||
} | ||
|
||
export default actions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the
url/registerMapping
call is skipped and the method is called without the filter parameters even when theconfig.entities.categoriesDynamicPrefetch
is set to trueThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
ssrCache
tags are also not set with this changes: https://github.com/DivanteLtd/vue-storefront/blob/70543b3cfa379971220f6514348fe0386a108878/core/modules/catalog/store/category/actions.ts#L141