diff --git a/CHANGELOG.md b/CHANGELOG.md index adec785881..4cbf809ebd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,7 +70,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Refactored the vuex user module - @andrzejewsky (#3095) - Brazilian Portuguese (pt_BR) translation improved - @pxfm (#3288) - Moved store/lib to /lib - @pxfm (#3253) -- Improved some of the german translations in spelling and wording - @MariaKern (#3297) +- Corrected usage of "configurableChildrenStockPrefetchStatic" setting, refactored logic to tested helper - @philippsander (#859) +- Improved some of the german translations in spelling and wording - @MariaKern (#3297) ## [1.10.0] - 2019.08.10 diff --git a/core/lib/multistore.ts b/core/lib/multistore.ts index 5bc1e7def4..e1ffff731a 100644 --- a/core/lib/multistore.ts +++ b/core/lib/multistore.ts @@ -147,11 +147,8 @@ export function storeCodeFromRoute (matchedRouteOrUrl: LocalizedRoute | RawLocat return storeCode } } - - return '' - } else { - return '' - } + } + return '' } export function removeStoreCodeFromRoute (matchedRouteOrUrl: LocalizedRoute | string): LocalizedRoute | string { diff --git a/core/modules/catalog-next/helpers/cacheProductsHelper.ts b/core/modules/catalog-next/helpers/cacheProductsHelper.ts new file mode 100644 index 0000000000..d5fe9a83d8 --- /dev/null +++ b/core/modules/catalog-next/helpers/cacheProductsHelper.ts @@ -0,0 +1,24 @@ +import { products } from 'config' + +export const prefetchStockItems = (cachedProductsResponse, cache = {}) => { + const skus = [] + let prefetchIndex = 0 + cachedProductsResponse.items.map(i => { + if (products.configurableChildrenStockPrefetchStatic && + products.configurableChildrenStockPrefetchStaticPrefetchCount > 0) { + if (prefetchIndex > products.configurableChildrenStockPrefetchStaticPrefetchCount) return + } + skus.push(i.sku) // main product sku to be checked anyway + if (i.type_id === 'configurable' && i.configurable_children && i.configurable_children.length > 0) { + for (const confChild of i.configurable_children) { + const cachedItem = cache[confChild.id] + if (typeof cachedItem === 'undefined' || cachedItem === null) { + skus.push(confChild.sku) + } + } + prefetchIndex++ + } + }) + + return skus +} diff --git a/core/modules/catalog-next/store/category/actions.ts b/core/modules/catalog-next/store/category/actions.ts index 2611077142..98448bf98b 100644 --- a/core/modules/catalog-next/store/category/actions.ts +++ b/core/modules/catalog-next/store/category/actions.ts @@ -14,6 +14,7 @@ import { configureProductAsync } from '@vue-storefront/core/modules/catalog/help import { DataResolver } from 'core/data-resolver/types/DataResolver'; import { Category } from '../../types/Category'; import { _prepareCategoryPathIds } from '../../helpers/categoryHelpers'; +import { prefetchStockItems } from '../../helpers/cacheProductsHelper'; import chunk from 'lodash-es/chunk' const actions: ActionTree = { @@ -75,24 +76,9 @@ const actions: ActionTree = { sort: searchQuery.sort, updateState: false // not update the product listing - this request is only for caching }, { root: true }) - if (products.filterUnavailableVariants && products.configurableChildrenStockPrefetchStatic) { // prefetch the stock items - const skus = [] - let prefetchIndex = 0 - cachedProductsResponse.items.map(i => { - if (products.configurableChildrenStockPrefetchStaticPrefetchCount > 0) { - if (prefetchIndex > products.configurableChildrenStockPrefetchStaticPrefetchCount) return - } - skus.push(i.sku) // main product sku to be checked anyway - if (i.type_id === 'configurable' && i.configurable_children && i.configurable_children.length > 0) { - for (const confChild of i.configurable_children) { - const cachedItem = rootState.stock.cache[confChild.id] - if (typeof cachedItem === 'undefined' || cachedItem === null) { - skus.push(confChild.sku) - } - } - prefetchIndex++ - } - }) + if (products.filterUnavailableVariants) { // prefetch the stock items + const skus = prefetchStockItems(cachedProductsResponse, rootState.stock.cache) + for (const chunkItem of chunk(skus, 15)) { dispatch('stock/list', { skus: chunkItem }, { root: true }) // store it in the cache } diff --git a/core/modules/catalog-next/test/unit/prefetchStockItems.spec.ts b/core/modules/catalog-next/test/unit/prefetchStockItems.spec.ts new file mode 100644 index 0000000000..60662a9ff1 --- /dev/null +++ b/core/modules/catalog-next/test/unit/prefetchStockItems.spec.ts @@ -0,0 +1,71 @@ +import {prefetchStockItems} from '../../helpers/cacheProductsHelper'; +import config from 'config'; + +describe('prefetchStockItems method', () => { + describe('default configurableChildrenStockPrefetchStaticPrefetchCount', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.mock('config', () => ({})); + }) + + it('returns an empty array when no items are provided', () => { + const cachedProductsResponse = { + items: [] + } + const result = prefetchStockItems(cachedProductsResponse) + expect(result).toEqual([]); + }) + + it('returns the skus of the children of a configurable', () => { + const cachedProductsResponse = { + items: [ + {sku: 'foo'}, + { + sku: 'bar', + type_id: 'configurable', + configurable_children: [ + {sku: 'bar.foo'}, + {sku: 'bar.bar'}, + {sku: 'bar.baz'} + ] + }, + {sku: 'baz'} + ] + } + const result = prefetchStockItems(cachedProductsResponse) + expect(result).toEqual(['foo', 'bar', 'bar.foo', 'bar.bar', 'bar.baz', 'baz']); + }) + + it('returns the same skus of the provided simple products', () => { + const cachedProductsResponse = { + items: [ + {sku: 'foo'}, + {sku: 'bar'}, + {sku: 'baz'} + ] + } + const result = prefetchStockItems(cachedProductsResponse) + expect(result).toEqual(['foo', 'bar', 'baz']); + }) + + it('ignores the pre-cached skus of children of a configurable', () => { + const cachedProductsResponse = { + items: [ + {sku: 'foo'}, + { + sku: 'bar', + type_id: 'configurable', + configurable_children: [ + {sku: 'bar.foo', id: 1337}, + {sku: 'bar.bar'}, + {sku: 'bar.baz', id: 4711} + ] + }, + {sku: 'baz'} + ] + } + const result = prefetchStockItems(cachedProductsResponse, {1337: {}, 4711: {}}) + expect(result).toEqual(['foo', 'bar', 'bar.bar', 'baz']); + }) + }) +}) diff --git a/core/modules/catalog/helpers/areAttributesAlreadyLoaded.ts b/core/modules/catalog/helpers/areAttributesAlreadyLoaded.ts new file mode 100644 index 0000000000..967578df43 --- /dev/null +++ b/core/modules/catalog/helpers/areAttributesAlreadyLoaded.ts @@ -0,0 +1,38 @@ +import config from 'config' + +const areAttributesAlreadyLoaded = ({ + filterValues, + filterField, + blacklist, + idsList, + codesList +}: { + filterValues: string[], + filterField: string, + blacklist: string[], + idsList: any, + codesList: any +}): boolean => { + return filterValues.filter(fv => { + if (config.entities.product.standardSystemFields.indexOf(fv) >= 0) { + return false + } + + if (fv.indexOf('.') >= 0) { + return false + } + + if (blacklist !== null && blacklist.includes(fv)) { + return false + } + + if (filterField === 'attribute_id') { + return (typeof idsList[fv] === 'undefined' || idsList[fv] === null) + } + if (filterField === 'attribute_code') { + return (typeof codesList[fv] === 'undefined' || codesList[fv] === null) + } + }).length === 0 +} + +export default areAttributesAlreadyLoaded diff --git a/core/modules/catalog/helpers/createAttributesListQuery.ts b/core/modules/catalog/helpers/createAttributesListQuery.ts new file mode 100644 index 0000000000..a8d9c930fc --- /dev/null +++ b/core/modules/catalog/helpers/createAttributesListQuery.ts @@ -0,0 +1,29 @@ +import SearchQuery from '@vue-storefront/core/lib/search/searchQuery' + +const createAttributesListQuery = ({ + filterValues, + filterField, + onlyDefinedByUser, + onlyVisible +}: { + filterValues: string[], + filterField: string, + onlyDefinedByUser: boolean, + onlyVisible: boolean +}): SearchQuery => { + let searchQuery = new SearchQuery() + + if (filterValues) { + searchQuery = searchQuery.applyFilter({key: filterField, value: {'in': filterValues}}) + } + if (onlyDefinedByUser) { + searchQuery = searchQuery.applyFilter({key: 'is_user_defined', value: {'in': [true]}}) + } + if (onlyVisible) { + searchQuery = searchQuery.applyFilter({key: 'is_visible', value: {'in': [true]}}) + } + + return searchQuery +} + +export default createAttributesListQuery diff --git a/core/modules/catalog/helpers/reduceAttributesLists.ts b/core/modules/catalog/helpers/reduceAttributesLists.ts new file mode 100644 index 0000000000..3662224e62 --- /dev/null +++ b/core/modules/catalog/helpers/reduceAttributesLists.ts @@ -0,0 +1,26 @@ +import Attribute from '@vue-storefront/core/modules/catalog/types/Attribute' + +const reduceAttributes = (prev, curr) => { + if (curr) { + prev.attrHashByCode[curr.attribute_code] = curr + prev.attrHashById[curr.attribute_id] = curr + } + + return prev +} + +const reduceAttributesLists = ({ + codesList, + idsList, + attributes +}: { + codesList: any, + idsList: any, + attributes: Attribute[] +}) => { + return attributes.reduce( + reduceAttributes, { attrHashByCode: codesList, attrHashById: idsList } + ) +} + +export default reduceAttributesLists diff --git a/core/modules/catalog/store/attribute/actions.ts b/core/modules/catalog/store/attribute/actions.ts index 4e399220ef..ffaea32692 100644 --- a/core/modules/catalog/store/attribute/actions.ts +++ b/core/modules/catalog/store/attribute/actions.ts @@ -1,57 +1,86 @@ import * as types from './mutation-types' -import SearchQuery from '@vue-storefront/core/lib/search/searchQuery' import { quickSearchByQuery } from '@vue-storefront/core/lib/search' +import { StorageManager } from '@vue-storefront/core/lib/storage-manager' import AttributeState from '../../types/AttributeState' import RootState from '@vue-storefront/core/types/RootState' import { ActionTree } from 'vuex' import config from 'config' import { Logger } from '@vue-storefront/core/lib/logger' +import { entityKeyName } from '@vue-storefront/core/lib/store/entities' import { prefetchCachedAttributes } from '../../helpers/prefetchCachedAttributes' +import areAttributesAlreadyLoaded from './../../helpers/areAttributesAlreadyLoaded' +import createAttributesListQuery from './../../helpers/createAttributesListQuery' +import reduceAttributesLists from './../../helpers/reduceAttributesLists' const actions: ActionTree = { + async updateAttributes ({ commit, getters }, { attributes }) { + const idsList = getters.attributeListById + const codesList = getters.attributeListByCode + + for (let attr of attributes) { + if (attr && !config.attributes.disablePersistentAttributesCache) { + const attrCollection = StorageManager.get('attributes') + + try { + await attrCollection.setItem(entityKeyName('attribute_code', attr.attribute_code.toLowerCase()), attr) + await attrCollection.setItem(entityKeyName('attribute_id', attr.attribute_id.toString()), attr) + } catch (e) { + Logger.error(e, 'mutations')() + } + } + } + + commit(types.ATTRIBUTE_UPD_ATTRIBUTES, reduceAttributesLists({ codesList, idsList, attributes })) + }, + async loadCachedAttributes ({ dispatch }, { filterField, filterValues }) { + if (!filterValues) { + return + } + + const attributes = await prefetchCachedAttributes(filterField, filterValues) + + if (attributes) { + await dispatch('updateAttributes', { attributes }) + } + }, + updateBlacklist ({ commit, getters }, { filterValues, filterField, attributes }) { + if (attributes && filterValues.length > 0) { + const foundValues = attributes.map(attr => attr[filterField]) + const toBlackList = filterValues.filter(ofv => !foundValues.includes(ofv) && !getters.getBlacklist.includes(ofv)) + commit(types.ATTRIBUTE_UPD_BLACKLIST, toBlackList) + } + }, /** * Load attributes with specific codes * @param {Object} context * @param {Array} attrCodes attribute codes to load */ - async list (context, { filterValues = null, filterField = 'attribute_code', only_user_defined = false, only_visible = false, size = 150, start = 0, includeFields = config.entities.optimize ? config.entities.attribute.includeFields : null }) { - const commit = context.commit - let searchQuery = new SearchQuery() - const orgFilterValues = filterValues ? [...filterValues] : [] - if (filterValues) { - const cachedAttributes = await prefetchCachedAttributes(filterField, filterValues) - if (cachedAttributes) context.commit(types.ATTRIBUTE_UPD_ATTRIBUTES, { items: cachedAttributes }) - filterValues = filterValues.filter(fv => { // check the already loaded - if (config.entities.product.standardSystemFields.indexOf(fv) >= 0) return false // skip standard system fields - if (fv.indexOf('.') >= 0) return false // skip multipart field names - if (context.state.blacklist !== null && context.state.blacklist.includes(fv)) return false // return that this attribute is not on our blacklist - if (filterField === 'attribute_id') return (typeof context.state.list_by_id[fv] === 'undefined' || context.state.list_by_id[fv] === null) - if (filterField === 'attribute_code') return (typeof context.state.list_by_code[fv] === 'undefined' || context.state.list_by_code[fv] === null) - }) - if (!filterValues || filterValues.length === 0) { - Logger.info('Skipping attribute load - attributes already loaded', 'attr', { orgFilterValues, filterField })() - return Promise.resolve({ - items: Object.values(context.state.list_by_code) - }) - } - searchQuery = searchQuery.applyFilter({key: filterField, value: {'in': filterValues}}) - } - if (only_user_defined) { - searchQuery = searchQuery.applyFilter({key: 'is_user_defined', value: {'in': [true]}}) - } - if (only_visible) { - searchQuery = searchQuery.applyFilter({key: 'is_visible', value: {'in': [true]}}) + async list ({ getters, dispatch }, { filterValues = null, filterField = 'attribute_code', only_user_defined = false, only_visible = false, size = 150, start = 0, includeFields = config.entities.optimize ? config.entities.attribute.includeFields : null }) { + const blacklist = getters.getBlacklist + const idsList = getters.attributeListById + const codesList = getters.attributeListByCode + const orgFilterValues = filterValues || [] + + await dispatch('loadCachedAttributes', { filterField, filterValues }) + + if (areAttributesAlreadyLoaded({ filterValues, filterField, blacklist, idsList, codesList })) { + Logger.info('Skipping attribute load - attributes already loaded', 'attr', { orgFilterValues, filterField })() + return { items: Object.values(codesList) } } - return quickSearchByQuery({ entityType: 'attribute', query: searchQuery, includeFields: includeFields }).then((resp) => { - if (resp && Array.isArray(orgFilterValues) && orgFilterValues.length > 0) { - const foundValues = resp.items.map(attr => attr[filterField]) - const toBlackList = filterValues.filter(ofv => !foundValues.includes(ofv)) - toBlackList.map(tbl => { - if (!context.state.blacklist.includes(tbl)) context.state.blacklist.push(tbl) - }) // extend the black list of not-found atrbiutes - } - commit(types.ATTRIBUTE_UPD_ATTRIBUTES, resp) + + const query = createAttributesListQuery({ + filterValues, + filterField, + onlyDefinedByUser: only_user_defined, + onlyVisible: only_visible }) + const resp = await quickSearchByQuery({ entityType: 'attribute', query, includeFields }) + const attributes = resp && orgFilterValues.length > 0 ? resp.items : null + + dispatch('updateBlacklist', { filterValues, filterField, attributes }) + await dispatch('updateAttributes', { attributes }) + + return resp } } diff --git a/core/modules/catalog/store/attribute/getters.ts b/core/modules/catalog/store/attribute/getters.ts index 920d0fbf76..30a3f576a7 100644 --- a/core/modules/catalog/store/attribute/getters.ts +++ b/core/modules/catalog/store/attribute/getters.ts @@ -4,7 +4,8 @@ import RootState from '@vue-storefront/core/types/RootState' const getters: GetterTree = { attributeListByCode: (state) => state.list_by_code, - attributeListById: (state) => state.list_by_id + attributeListById: (state) => state.list_by_id, + getBlacklist: (state) => state.blacklist } export default getters diff --git a/core/modules/catalog/store/attribute/mutation-types.ts b/core/modules/catalog/store/attribute/mutation-types.ts index 018e93ca03..67254b063a 100644 --- a/core/modules/catalog/store/attribute/mutation-types.ts +++ b/core/modules/catalog/store/attribute/mutation-types.ts @@ -1,2 +1,3 @@ export const SN_ATTRIBUTE = 'attribute' export const ATTRIBUTE_UPD_ATTRIBUTES = SN_ATTRIBUTE + '/UPD_ATTRIBUTES' +export const ATTRIBUTE_UPD_BLACKLIST = SN_ATTRIBUTE + '/UPD_BLACKLIST_ATTRIBUTES' diff --git a/core/modules/catalog/store/attribute/mutations.ts b/core/modules/catalog/store/attribute/mutations.ts index 1d8ac130ba..1a7f23a998 100644 --- a/core/modules/catalog/store/attribute/mutations.ts +++ b/core/modules/catalog/store/attribute/mutations.ts @@ -1,12 +1,8 @@ import Vue from 'vue' import { MutationTree } from 'vuex' -import { entityKeyName } from '@vue-storefront/core/lib/store/entities' import * as types from './mutation-types' import AttributeState from '../../types/AttributeState' -import { Logger } from '@vue-storefront/core/lib/logger' import EventBus from '@vue-storefront/core/compatibility/plugins/event-bus' -import { StorageManager } from '@vue-storefront/core/lib/storage-manager' -import config from 'config' const mutations: MutationTree = { /** @@ -14,30 +10,13 @@ const mutations: MutationTree = { * @param {} state * @param {Array} attributes */ - async [types.ATTRIBUTE_UPD_ATTRIBUTES] (state, attributes) { - let attrList = attributes.items // extract fields from ES _source - let attrHashByCode = state.list_by_code - let attrHashById = state.list_by_id - - for (let attr of attrList) { - if (attr) { - attrHashByCode[attr.attribute_code] = attr - attrHashById[attr.attribute_id] = attr - - if (!config.attributes.disablePersistentAttributesCache) { - const attrCollection = StorageManager.get('attributes') - try { - await attrCollection.setItem(entityKeyName('attribute_code', attr.attribute_code.toLowerCase()), attr) - await attrCollection.setItem(entityKeyName('attribute_id', attr.attribute_id.toString()), attr) - } catch (e) { - Logger.error(e, 'mutations')() - } - } - } - } + async [types.ATTRIBUTE_UPD_ATTRIBUTES] (state, { attrHashByCode, attrHashById }) { Vue.set(state, 'list_by_code', attrHashByCode) Vue.set(state, 'list_by_id', attrHashById) EventBus.$emit('product-after-attributes-loaded') + }, + [types.ATTRIBUTE_UPD_BLACKLIST] (state, blacklist) { + state.blacklist = state.blacklist.concat(blacklist) } } diff --git a/core/modules/catalog/types/Attribute.ts b/core/modules/catalog/types/Attribute.ts new file mode 100644 index 0000000000..3ab6143041 --- /dev/null +++ b/core/modules/catalog/types/Attribute.ts @@ -0,0 +1,4 @@ +export default interface Attribute { + attribute_code?: string, + attribute_id?: number +}