vue-storefront
Version:
A Vue.js, PWA eCommerce frontend
331 lines (324 loc) • 14.1 kB
JavaScript
import config from 'config'
import * as types from '../../mutation-types'
import { breadCrumbRoutes } from 'core/helpers'
import { configureProductAsync, doPlatformPricesSync, calculateTaxes } from './helpers'
import bodybuilder from 'bodybuilder'
import { entityKeyName } from 'core/lib/entities'
import { optionLabel } from 'core/store/modules/attribute/helpers'
import { quickSearchByQuery } from 'core/lib/search'
import EventBus from 'core/plugins/event-bus'
import _ from 'lodash'
export default {
/**
* Reset current configuration and selected variatnts
*/
reset (context) {
const productOriginal = context.getters.productOriginal
context.commit(types.CATALOG_RESET_PRODUCT, productOriginal)
},
/**
* Setup product breadcrumbs path
*/
setupBreadcrumbs (context, { product }) {
let subloaders = []
let setbrcmb = (path) => {
if (path.findIndex(itm => {
return itm.slug === context.rootState.category.current.slug
}) < 0) {
path.push({
slug: context.rootState.category.current.slug,
name: context.rootState.category.current.name
}) // current category at the end
}
context.dispatch('meta/set', { title: product.name }, { root: true })
context.state.breadcrumbs.routes = breadCrumbRoutes(path) // TODO: change to store.commit call?
}
// TODO: Fix it when product is enterd from outside the category page
let currentPath = context.rootState.category.current_path
let currentCat = context.rootState.category.current
if (currentPath.length > 0 && currentCat) {
setbrcmb(currentPath)
} else {
if (product.category && product.category.length > 0) {
subloaders.push(
context.dispatch('category/list', {}, { root: true }).then((categories) => {
for (let cat of product.category.reverse()) {
let category = categories.items.find((itm) => { return itm['id'] === cat.category_id })
if (category) {
context.dispatch('category/single', { key: 'id', value: category.id }, { root: true }).then((category) => { // this sets up category path and current category
setbrcmb(context.rootState.category.current_path)
}).catch(err => {
setbrcmb(context.rootState.category.current_path)
console.error(err)
})
break
}
}
}, { root: true }).catch(err => {
console.error(err)
})
)
}
}
context.state.breadcrumbs.name = product.name
return Promise.all(subloaders)
},
doPlatformPricesSync (context, { products }) {
return doPlatformPricesSync(products)
},
/**
* Download Magento2 / other platform prices to put them over ElasticSearch prices
*/
syncPlatformPricesOver (context, { skus }) {
return context.dispatch('sync/execute', { url: config.products.endpoint + '/render-list?skus=' + encodeURIComponent(skus.join(',') + '¤cyCode=' + config.i18n.currencyCode), // sync the cart
payload: {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
mode: 'cors'
},
callback_event: 'prices-after-sync'
}, { root: true }).then(task => {
return task.result
})
},
/**
* Setup associated products
*/
setupAssociated (context, { product }) {
let subloaders = []
if (product.type_id === 'grouped') {
product.price = 0
product.priceInclTax = 0
console.log(product.name + ' SETUP ASSOCIATED')
for (let pl of product.product_links) {
if (pl.link_type === 'associated' && pl.linked_product_type === 'simple') { // prefetch links
console.log('Prefetching grouped product link for ' + pl.sku + ' = ' + pl.linked_product_sku)
subloaders.push(context.dispatch('single', {
options: { sku: pl.linked_product_sku },
setCurrentProduct: false,
selectDefaultVariant: false
}).catch(err => { console.log('err'); console.error(err) }).then((asocProd) => {
pl.product = asocProd
pl.product.qty = 1
product.price += pl.product.price
product.priceInclTax += pl.product.priceInclTax
product.tax += pl.product.tax
}))
}
}
}
return Promise.all(subloaders)
},
/**
* This is fix for https://github.com/DivanteLtd/vue-storefront/issues/508
* TODO: probably it would be better to have "parent_id" for simple products or to just ensure configurable variants are not visible in categories/search
*/
checkConfigurableParent (context, {product}) {
if (product.type_id === 'simple') {
console.log('Checking configurable parent')
let query = bodybuilder()
.query('match', 'configurable_children.sku', context.state.current.sku)
.build()
return context.dispatch('list', {query, start: 0, size: 1, updateState: false}).then((resp) => {
if (resp.items.length >= 1) {
const parentProduct = resp.items[0]
context.commit(types.CATALOG_SET_PRODUCT_PARENT, parentProduct)
}
}).catch(function (err) {
console.error(err)
})
}
},
/**
* Setup product current variants
*/
setupVariants (context, { product }) {
let subloaders = []
if (product.type_id === 'configurable') {
const configurableAttrIds = product.configurable_options.map(opt => opt.attribute_id)
subloaders.push(context.dispatch('attribute/list', {
filterValues: configurableAttrIds,
filterField: 'attribute_id'
}, { root: true }).then((attributes) => {
for (let option of product.configurable_options) {
for (let ov of option.values) {
let lb = optionLabel(context.rootState.attribute, { attributeKey: option.attribute_id, searchBy: 'id', optionId: ov.value_index })
if (_.trim(lb) !== '') {
let optionKey = option.label.toLowerCase()
if (!context.state.current_options[optionKey]) {
context.state.current_options[optionKey] = []
}
context.state.current_options[optionKey].push({
label: lb,
id: ov.value_index
})
}
}
}
let selectedVariant = context.state.current
for (let option of product.configurable_options) {
let attr = context.rootState.attribute.list_by_id[option.attribute_id]
if (selectedVariant.custom_attributes) {
let selectedOption = selectedVariant.custom_attributes.find((a) => {
return (a.attribute_code === attr.attribute_code)
})
context.state.current_configuration[attr.attribute_code] = {
attribute_code: attr.attribute_code,
id: selectedOption.value,
label: optionLabel(context.rootState.attribute, { attributeKey: selectedOption.attribute_code, searchBy: 'code', optionId: selectedOption.value })
}
}
}
}).catch(err => {
console.error(err)
}))
}
return Promise.all(subloaders)
},
/**
* Search ElasticSearch catalog of products using simple text query
* Use bodybuilder to build the query, aggregations etc: http://bodybuilder.js.org/
* @param {Object} query elasticSearch request body
* @param {Int} start start index
* @param {Int} size page size
* @return {Promise}
*/
list (context, { query, start = 0, size = 50, entityType = 'product', sort = '', cacheByKey = 'sku', prefetchGroupProducts = true, updateState = true, meta = {} }) {
return quickSearchByQuery({ query, start, size, entityType, sort }).then((resp) => {
return calculateTaxes(resp.items, context).then((updatedProducts) => {
// handle cache
const cache = global.db.elasticCacheCollection
for (let prod of resp.items) { // we store each product separately in cache to have offline access to products/single method
if (!prod[cacheByKey]) {
cacheByKey = 'id'
}
const cacheKey = entityKeyName(cacheByKey, prod[cacheByKey])
cache.setItem(cacheKey, prod)
.catch((err) => {
console.error('Cannot store cache for ' + cacheKey + ', ' + err)
})
if (prod.type_id === 'grouped' && prefetchGroupProducts) {
context.dispatch('setupAssociated', { product: prod })
}
}
// commit update products list mutation
if (updateState) {
context.commit(types.CATALOG_UPD_PRODUCTS, resp)
}
EventBus.$emit('product-after-list', { query: query, start: start, size: size, sort: sort, entityType: entityType, meta: meta, result: resp })
return resp
})
}).catch(function (err) {
console.error(err)
})
},
/**
* Search products by specific field
* @param {Object} options
*/
single (context, { options, setCurrentProduct = true, selectDefaultVariant = true, key = 'sku' }) {
if (!options[key]) {
throw Error('Please provide the search key ' + key + ' for product/single action!')
}
const cacheKey = entityKeyName(key, options[key])
return new Promise((resolve, reject) => {
const benchmarkTime = new Date()
const cache = global.db.elasticCacheCollection
cache.getItem(cacheKey, (err, res) => {
// report errors
if (err) {
console.error({
info: 'Get item from cache in ./store/modules/product.js',
err
})
}
const setupProduct = (prod) => {
// set original product
if (setCurrentProduct) {
context.dispatch('setOriginal', prod)
}
// check is prod has configurable children
const hasConfigurableChildren = prod && prod.configurable_children && prod.configurable_children.length
// set current product - configurable or not
if (prod.type_id === 'configurable' && hasConfigurableChildren) {
// set first available configuration
// todo: probably a good idea is to change this [0] to specific id
configureProductAsync(context, { product: prod, configuration: { sku: options.childSku }, selectDefaultVariant: selectDefaultVariant })
} else {
if (setCurrentProduct) context.dispatch('setCurrent', prod)
}
return prod
}
if (res !== null) {
console.debug('Product:single - result from localForage (for ' + cacheKey + '), ms=' + (new Date().getTime() - benchmarkTime.getTime()))
const cachedProduct = setupProduct(res)
if (config.products.alwaysSyncPlatformPricesOver) {
doPlatformPricesSync([cachedProduct]).then((products) => {
EventBus.$emitFilter('product-after-single', { key: key, options: options, product: products[0] })
resolve(products[0])
})
if (!config.products.waitForPlatformSync) {
EventBus.$emitFilter('product-after-single', { key: key, options: options, product: cachedProduct })
resolve(cachedProduct)
}
} else {
EventBus.$emitFilter('product-after-single', { key: key, options: options, product: cachedProduct })
resolve(cachedProduct)
}
} else {
context.dispatch('list', { // product list syncs the platform price on it's own
query: bodybuilder()
.query('match', key, options[key])
.build(),
prefetchGroupProducts: false
}).then((res) => {
if (res && res.items && res.items.length) {
EventBus.$emitFilter('product-after-single', { key: key, options: options, product: res.items[0] })
resolve(setupProduct(res.items[0]))
} else {
reject(Error('Product query returned empty result'))
}
})
}
})// .catch((err) => { console.error('Cannot read cache for ' + cacheKey + ', ' + err) })
})
},
/**
* Configure product with given configuration and set it as current
* @param {Object} context
* @param {Object} product
* @param {Array} configuration
*/
configure (context, { product = null, configuration, selectDefaultVariant = true }) {
return configureProductAsync(context, { product: product, configuration: configuration, selectDefaultVariant: selectDefaultVariant })
},
/**
* Set current product with given variant's properties
* @param {Object} context
* @param {Object} productVariant
*/
setCurrent (context, productVariant) {
if (productVariant && typeof productVariant === 'object') {
// get original product
const productOriginal = context.getters.productOriginal
// check if passed variant is the same as original
const productUpdated = Object.assign({}, productOriginal, productVariant)
context.commit(types.CATALOG_SET_PRODUCT_CURRENT, productUpdated)
} else console.debug('Unable to update current product.')
},
/**
* Set given product as original
* @param {Object} context
* @param {Object} originalProduct
*/
setOriginal (context, originalProduct) {
if (originalProduct && typeof originalProduct === 'object') context.commit(types.CATALOG_SET_PRODUCT_ORIGINAL, originalProduct)
else console.debug('Unable to setup original product.')
},
/**
* Set related products
*/
related (context, { key = 'related-products', items }) {
context.commit(types.CATALOG_UPD_RELATED, { key, items })
}
}