diff --git a/CHANGELOG.md b/CHANGELOG.md index 74194c84c6..c30940fc74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,33 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.10.4] - 18.10.2019 + +### Fixed +- Added try/catch for fetching single product in cart synchronization - @gibkigonzo (#3632) +- Removed infinite loop when changing checkbox in shipping details - @gibkigonzo (#3656) +- Remove modifying config by reference in multistore - @gibkigonzo (#3617) +- Fix displaying same country twice in the in the country switcher - @andrzejewsky (#3587) +- Remove race condition while loading locale messages - @gibkigonzo (#3602) +- Fixed special price that can break when you change pages (browser navigation for/back) or just go from category to product page - @resubaka (#3638) +- Change sku to string when checking products equality - @gibkigonzo (#3606) +- Fixed problem with losing browser history - @andrzejewsky (#3642) +- Fixed resolving store code on SSR - @andrzejewsky (#3576) +- Fixed styles for original price on Wishlist sidebar - @przspa (#3392) +- Added debounce for updating quantity method in the cart - @andrzejewsky (#3191) +- Improved scrolling in Safari on iOS devices (sidebars) - @phoenixdev-kl (#3551) +- Improved cookie and offline badges (z-index, overflow) - @phoenixdev-kl (#3552) +- Added config to set Cache-Control header for static assets based on mime type - @phoenix-bjoern (#3268) +- Added catching of errors when ES is down - @qiqqq +- `localizedRoute()` doesn't return urlDispatcher routes anymore. Use localizedDispatcherRoute instead - @lukeromanowicz (#3548) +- Fixed hash in dynamically resolved urls causing resolving issues - @lukeromanowicz (#3515) +- `localizedRoute()` now supports path (and prefers over fullPath) in LocalizedRoute objects - @lukeromanowicz (#3515) +- Decreased the `localStorage` quota usage + error handling by introducing new config variables: `config.products.disablePersistentProductsCache` to not store products by SKU (by default it's on). Products are cached in ServiceWorker cache anyway so the `product/list` will populate the in-memory cache (`cache.setItem(..., memoryOnly = true)`); `config.seo.disableUrlRoutesPersistentCache` - to not store the url mappings; they're stored in in-memory cache anyway so no additional requests will be made to the backend for url mapping; however it might cause some issues with url routing in the offline mode (when the offline mode PWA installed on homescreen got reloaded, the in-memory cache will be cleared so there won't potentially be the url mappings; however the same like with `product/list` the ServiceWorker cache SHOULD populate url mappings anyway); `config.syncTasks.disablePersistentTaskQueue` to not store the network requests queue in service worker. Currently only the stock-check and user-data changes were using this queue. The only downside it introuces can be related to the offline mode and these tasks will not be re-executed after connectivity established, but just in a case when the page got reloaded while offline (yeah it might happen using ServiceWorker; `syncTasks` can't be re-populated in cache from SW) - @pkarw (#2985) +- Fixed evaluate detailsLink in the cookie notification - @benjick (#3689) + +## Added +- Added german translations - @schwerdt-ke (3076) + ## [1.10.3] - 2019.09.18 ### Fixed diff --git a/config/default.json b/config/default.json index a88d04003a..4cfa44ac8c 100644 --- a/config/default.json +++ b/config/default.json @@ -20,7 +20,8 @@ } }, "seo": { - "useUrlDispatcher": true + "useUrlDispatcher": true, + "disableUrlRoutesPersistentCache": true }, "console": { "showErrorOnProduction" : false, @@ -46,7 +47,7 @@ "csrTimeout": 5000, "ssrTimeout": 1000, "queryMethod": "GET", - "disableLocalStorageQueriesCache": true, + "disablePersistentQueriesCache": true, "searchScoring": { "attributes": { "attribute_code": { @@ -262,6 +263,7 @@ "applycoupon_endpoint": "/api/cart/apply-coupon?token={{token}}&cartId={{cartId}}&coupon={{coupon}}" }, "products": { + "disablePersistentProductsCache": true, "useMagentoUrlKeys": true, "setFirstVarianAsDefaultInURL": false, "configurableChildrenStockPrefetchStatic": false, @@ -340,7 +342,6 @@ "wishlist": "LOCALSTORAGE", "categories": "LOCALSTORAGE", "attributes": "LOCALSTORAGE", - "products": "INDEXEDDB", "elasticCache": "LOCALSTORAGE", "claims": "LOCALSTORAGE", "syncTasks": "LOCALSTORAGE", @@ -396,6 +397,9 @@ } ] }, + "syncTasks": { + "disablePersistentTaskQueue": true + }, "i18n": { "defaultCountry": "US", "defaultLanguage": "EN", @@ -409,6 +413,11 @@ "fullLanguageName": "English", "bundleAllStoreviewLanguages": true }, + "expireHeaders": { + "default": "30d", + "application/json": "24h", + "image/png": "7d" + }, "newsletter": { "endpoint": "/api/ext/mailchimp-subscribe/subscribe" }, diff --git a/core/app.ts b/core/app.ts index d798b656eb..132aa63373 100755 --- a/core/app.ts +++ b/core/app.ts @@ -70,7 +70,7 @@ const createApp = async (ssrContext, config, storeCode = null): Promise<{app: Vu store.state.__DEMO_MODE__ = (config.demomode === true) if (ssrContext) Vue.prototype.$ssrRequestContext = ssrContext if (!store.state.config) store.state.config = globalConfig // @deprecated - we should avoid the `config` - const storeView = prepareStoreView(storeCode) // prepare the default storeView + const storeView = await prepareStoreView(storeCode) // prepare the default storeView store.state.storeView = storeView // to depreciate in near future diff --git a/core/client-entry.ts b/core/client-entry.ts index 4b82d68338..38f242ab50 100755 --- a/core/client-entry.ts +++ b/core/client-entry.ts @@ -5,7 +5,8 @@ import { createApp } from '@vue-storefront/core/app' import rootStore from '@vue-storefront/core/store' import { registerSyncTaskProcessor } from '@vue-storefront/core/lib/sync/task' import i18n from '@vue-storefront/i18n' -import { prepareStoreView, storeCodeFromRoute, currentStoreView, localizedRoute } from '@vue-storefront/core/lib/multistore' +import storeCodeFromRoute from '@vue-storefront/core/lib/storeCodeFromRoute' +import { prepareStoreView, currentStoreView, localizedRoute } from '@vue-storefront/core/lib/multistore' import { onNetworkStatusChange } from '@vue-storefront/core/modules/offline-order/helpers/onNetworkStatusChange' import '@vue-storefront/core/service-worker/registration' // register the service worker import { AsyncDataLoader } from './lib/async-data-loader' diff --git a/core/compatibility/components/blocks/Microcart/Product.js b/core/compatibility/components/blocks/Microcart/Product.js index 9a0fa8c4f9..a4d239ef73 100644 --- a/core/compatibility/components/blocks/Microcart/Product.js +++ b/core/compatibility/components/blocks/Microcart/Product.js @@ -1,5 +1,6 @@ import { MicrocartProduct } from '@vue-storefront/core/modules/cart/components/Product.ts' import i18n from '@vue-storefront/i18n' +import debounce from 'lodash-es/debounce' import config from 'config' export default { @@ -12,11 +13,13 @@ export default { // deprecated, will be moved to theme or removed in the near future #1742 this.$bus.$on('cart-after-itemchanged', this.onProductChanged) this.$bus.$on('notification-after-itemremoved', this.onProductRemoved) + this.updateQuantity = debounce(this.updateQuantity, 5000) }, beforeDestroy () { // deprecated, will be moved to theme or removed in the near future #1742 this.$bus.$off('cart-after-itemchanged', this.onProductChanged) this.$bus.$off('notification-after-itemremoved', this.onProductRemoved) + this.updateQuantity.cancel() }, methods: { removeItem () { diff --git a/core/helpers/index.ts b/core/helpers/index.ts index 249e4f4915..6ba0ae6663 100644 --- a/core/helpers/index.ts +++ b/core/helpers/index.ts @@ -165,8 +165,13 @@ export const onlineHelper = Vue.observable({ isOnline: isServer || navigator.onLine }) +export const routerHelper = Vue.observable({ + popStateDetected: false +}) + !isServer && window.addEventListener('online', () => { onlineHelper.isOnline = true }) !isServer && window.addEventListener('offline', () => { onlineHelper.isOnline = false }) +!isServer && window.addEventListener('popstate', () => { routerHelper.popStateDetected = true }) export const processURLAddress = (url: string = '') => { if (url.startsWith('/')) return `${config.api.url}${url}` diff --git a/core/i18n/package.json b/core/i18n/package.json index 626001f517..ae715c9361 100644 --- a/core/i18n/package.json +++ b/core/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@vue-storefront/i18n", - "version": "1.10.3", + "version": "1.10.4", "description": "Vue Storefront i18n", "license": "MIT", "main": "index.ts", diff --git a/core/i18n/resource/i18n/de-DE.csv b/core/i18n/resource/i18n/de-DE.csv index 458dc1b96c..0572e06690 100644 --- a/core/i18n/resource/i18n/de-DE.csv +++ b/core/i18n/resource/i18n/de-DE.csv @@ -4,6 +4,7 @@ "Out of stock!","Nicht auf Lager!" " is out of the stock!"," ist nicht auf Lager!" "Some of the ordered products are not available!","Einige der bestellten Produkte sind nicht auf Lager!" +"Please wait ...","Bitte warten ..." "Stock check in progress, please wait while available stock quantities are checked","Bestandskontrolle läuft. Bitte warte einen Moment bis die verfügbare Bestandsmenge geprüft worden ist" "There is no Internet connection. You can still place your order. We will notify you if any of ordered products is not available because we cannot check it right now.","Es besteht aktuell keine Verbindung zum Internet. Du kannst deine Bestellung dennoch aufgeben. Falls eines der bestellten Produkte bei Wiederaufbau der Verbindung nicht mehr verfügbar sein sollte, werden wir dich umgehend benachrichtigen." "No such configuration for the product. Please do choose another combination of attributes.","Diese Konfiguration ist für dieses Produkt nicht möglich. Bitte wähle eine andere Kombination von Eigenschaften." @@ -11,6 +12,7 @@ "This feature is not implemented yet! Please take a look at https://github.com/DivanteLtd/vue-storefront/issues for our Roadmap!","Diese Funktion wurde noch nicht implementiert. Für weitere Details schau dir bitte auf https://github.com/DivanteLtd/vue-storefront/issues unsere Roadmap an!" "The product is out of stock and cannot be added to the cart!","Das Produkt ist nicht auf Lager und kann daher nicht zum Warenkorb hinzugefügt werden!" "Product has been added to the cart!","Produkt wurde zum Warenkorb hinzugefügt!" +"Product quantity has been updated!","Die Anzahl wurde upgedated!" "Internal validation error. Please check if all required fields are filled in. Please contact us on {email}","Interner Validierungsfehler. Bitte überprüfe, ob alle erforderlichen Felder ausgefüllt sind. Bei Problemen kontaktiere uns bitte über {email}" "Address provided in checkout contains invalid data. Please check if all required fields are filled in and also contact us on {email} to resolve this issue for future. Your order has been canceled.","Die angegebene Adresse ist nicht gültig. Bitte überprüfe, ob alle notwenigen Felder ausgefüllt sind und kontaktiere uns per {email} um den Fehler für die Zukunft zu beheben. Deine Bestellung wurde abgebrochen." "Product {productName} has been added to the compare!","Das Produkt {productName} wurde zur Vergleichsliste hinzugefügt!" @@ -71,3 +73,5 @@ "You need to be logged in to see this page","Sie müssen angemeldet sein, um diese Seite anzuzeigen" "Quantity must be above 0","Die Menge muss größer als 0 sein" "Error: Error while adding products","Error: Fehler beim hinzufügen der Produkte" +"Unexpected authorization error. Check your Network conection.","Unerwarteter Fehler bei der Authentifizierung. Bitte überprüfen Sie Ihre Internetverbindung." +"Columns","Spalten" diff --git a/core/i18n/resource/i18n/jp-JP.csv b/core/i18n/resource/i18n/jp-JP.csv index 40a4617656..dcf151cf58 100644 --- a/core/i18n/resource/i18n/jp-JP.csv +++ b/core/i18n/resource/i18n/jp-JP.csv @@ -154,7 +154,6 @@ "We will send you the invoice to given e-mail address","頂いたメールアドレスに請求書を送ります" "Wishlist","お気に入りリスト" "You are offline","オフラインです" -"You are offline, some of the functionalities are limited","現在オフラインですので、使えない機能があります" "You can also use","またこれらを使うことができます" "You can log to your account using e-mail and password defined earlier. On your account you can edit your profile data, check history of transactions, edit subscription to newsletter.","登録されたメールアドレスとパスワードを使ってログインできます。アカウント内ではプロフィールの編集注文の履歴の確認ニュースレターへの購読が行えます。" "You have been successfully subscribed to our newsletter!","ニュースレターへの購読が完了しました!" diff --git a/core/lib/multistore.ts b/core/lib/multistore.ts index 7ccf27fb10..6c019ffcc8 100644 --- a/core/lib/multistore.ts +++ b/core/lib/multistore.ts @@ -6,54 +6,19 @@ import queryString from 'query-string' import { RouterManager } from '@vue-storefront/core/lib/router-manager' import VueRouter, { RouteConfig, RawLocation } from 'vue-router' import config from 'config' - -export interface LocalizedRoute { - path?: string, - name?: string, - hash?: string, - params?: object, - fullPath?: string, - host?: string -} - -export interface StoreView { - storeCode: string, - disabled?: boolean, - storeId: any, - name?: string, - url?: string, - elasticsearch: { - host: string, - index: string - }, - tax: { - sourcePriceIncludesTax: boolean, - defaultCountry: string, - defaultRegion: null | string, - calculateServerSide: boolean - }, - i18n: { - fullCountryName: string, - fullLanguageName: string, - defaultLanguage: string, - defaultCountry: string, - defaultLocale: string, - currencyCode: string, - currencySign: string, - dateFormat: string - } -} +import { LocalizedRoute, StoreView } from './types' +import storeCodeFromRoute from './storeCodeFromRoute' export function currentStoreView (): StoreView { // TODO: Change to getter all along our code return rootStore.state.storeView } -export function prepareStoreView (storeCode: string): StoreView { +export async function prepareStoreView (storeCode: string): Promise { let storeView = { // current, default store - tax: config.tax, - i18n: config.i18n, - elasticsearch: config.elasticsearch, + tax: Object.assign({}, config.tax), + i18n: Object.assign({}, config.i18n), + elasticsearch: Object.assign({}, config.elasticsearch), storeCode: '', storeId: config.defaultStoreCode && config.defaultStoreCode !== '' ? config.storeViews[config.defaultStoreCode].storeId : 1 } @@ -69,7 +34,7 @@ export function prepareStoreView (storeCode: string): StoreView { } if (storeViewHasChanged) { rootStore.state.storeView = storeView - loadLanguageAsync(storeView.i18n.defaultLocale) + await loadLanguageAsync(storeView.i18n.defaultLocale) } if (storeViewHasChanged || Vue.prototype.$db.currentStoreCode !== storeCode) { if (typeof Vue.prototype.$db === 'undefined') { @@ -81,46 +46,6 @@ export function prepareStoreView (storeCode: string): StoreView { return storeView } -export function storeCodeFromRoute (matchedRouteOrUrl: LocalizedRoute | RawLocation | string): string { - if (matchedRouteOrUrl) { - for (let storeCode of config.storeViews.mapStoreUrlsFor) { - const store = config.storeViews[storeCode] - - // handle resolving by path - const matchingPath = typeof matchedRouteOrUrl === 'object' ? matchedRouteOrUrl.path : matchedRouteOrUrl - let normalizedPath = matchingPath // assume that matching string is a path - if (matchingPath.length > 0 && matchingPath[0] !== '/') { - normalizedPath = '/' + matchingPath - } - - if (normalizedPath.startsWith(`${store.url}/`) || normalizedPath === store.url) { - return storeCode - } - - // handle resolving by domain+path - let url = '' - - if (typeof matchedRouteOrUrl === 'object') { - if (matchedRouteOrUrl['host']) { - url = matchedRouteOrUrl['host'] + normalizedPath - } else { - return '' // this route does not have url so there is nothing to do here - } - } else { - url = matchedRouteOrUrl as string - } - - if (url.startsWith(`${store.url}/`) || url === store.url) { - return storeCode - } - } - - return '' - } else { - return '' - } -} - export function removeStoreCodeFromRoute (matchedRouteOrUrl: LocalizedRoute | string): LocalizedRoute | string { const storeCodeInRoute = storeCodeFromRoute(matchedRouteOrUrl) if (storeCodeInRoute !== '') { @@ -141,22 +66,29 @@ export function adjustMultistoreApiUrl (url: string): string { } export function localizedDispatcherRoute (routeObj: LocalizedRoute | string, storeCode: string): LocalizedRoute | string { - if (!storeCode) { - storeCode = currentStoreView().storeCode + const { storeCode: currentStoreCode, appendStoreCode } = currentStoreView() + if (!storeCode || !config.storeViews[storeCode]) { + storeCode = currentStoreCode } - const appendStoreCodePrefix = config.storeViews[storeCode] ? config.storeViews[storeCode].appendStoreCode : false + const appendStoreCodePrefix = storeCode && appendStoreCode if (typeof routeObj === 'string') { if (routeObj[0] !== '/') routeObj = `/${routeObj}` return appendStoreCodePrefix ? `/${storeCode}${routeObj}` : routeObj } - if (routeObj && routeObj.fullPath) { // case of using dispatcher - const routeCodePrefix = config.defaultStoreCode !== storeCode && appendStoreCodePrefix ? `/${storeCode}` : '' - const qrStr = queryString.stringify(routeObj.params) + if (routeObj) { + if ((routeObj as LocalizedRoute).fullPath && !(routeObj as LocalizedRoute).path) { // support both path and fullPath + routeObj['path'] = (routeObj as LocalizedRoute).fullPath + } + + if (routeObj.path) { // case of using dispatcher + const routeCodePrefix = appendStoreCodePrefix ? `/${storeCode}` : '' + const qrStr = queryString.stringify(routeObj.params); - const normalizedPath = routeObj.fullPath[0] !== '/' ? `/${routeObj.fullPath}` : routeObj.fullPath - return `${routeCodePrefix}${normalizedPath}${qrStr ? `?${qrStr}` : ''}` + const normalizedPath = routeObj.path[0] !== '/' ? `/${routeObj.path}` : routeObj.path + return `${routeCodePrefix}${normalizedPath}${qrStr ? `?${qrStr}` : ''}` + } } return routeObj @@ -166,8 +98,14 @@ export function localizedRoute (routeObj: LocalizedRoute | string | RouteConfig if (!storeCode) { storeCode = currentStoreView().storeCode } - if (routeObj && (routeObj as LocalizedRoute).fullPath && config.seo.useUrlDispatcher) { - return localizedDispatcherRoute(Object.assign({}, routeObj, { params: null }) as LocalizedRoute, storeCode) + if (!routeObj) { + return routeObj + } + + if ((typeof routeObj === 'object') && (routeObj as LocalizedRoute)) { + if ((routeObj as LocalizedRoute).fullPath && !(routeObj as LocalizedRoute).path) { // support both path and fullPath + routeObj['path'] = (routeObj as LocalizedRoute).fullPath + } } if (storeCode && routeObj && config.defaultStoreCode !== storeCode && config.storeViews[storeCode].appendStoreCode) { diff --git a/core/lib/router-manager.ts b/core/lib/router-manager.ts index eaf4a19322..87795f6b5c 100644 --- a/core/lib/router-manager.ts +++ b/core/lib/router-manager.ts @@ -23,8 +23,8 @@ const RouterManager = { findByName: function (name: string): RouteConfig { return this._registeredRoutes.find(r => r.name === name) }, - findByPath: function (fullPath: string): RouteConfig { - return this._registeredRoutes.find(r => r.fullPath === fullPath) + findByPath: function (path: string): RouteConfig { + return this._registeredRoutes.find(r => r.path === path) }, lockRoute: function () { let resolver diff --git a/core/lib/search.ts b/core/lib/search.ts index 9ac147fdcf..8342ea7f3b 100644 --- a/core/lib/search.ts +++ b/core/lib/search.ts @@ -89,7 +89,7 @@ export const quickSearchByQuery = async ({ query, start = 0, size = 50, entityTy const res = searchAdapter.entities[Request.type].resultPorcessor(resp, start, size) if (res) { // otherwise it can be just a offline mode - cache.setItem(cacheKey, res, null, config.elasticsearch.disableLocalStorageQueriesCache).catch((err) => { console.error('Cannot store cache for ' + cacheKey + ', ' + err) }) + cache.setItem(cacheKey, res, null, config.elasticsearch.disablePersistentQueriesCache).catch((err) => { console.error('Cannot store cache for ' + cacheKey + ', ' + err) }) if (!servedFromCache) { // if navigator onLine == false means ES is unreachable and probably this will return false; sometimes returned false faster than indexedDb cache returns result ... Logger.debug('Result from ES for ' + cacheKey + ' (' + entityType + '), ms=' + (new Date().getTime() - benchmarkTime.getTime()))() res.cache = false diff --git a/core/lib/search/adapter/api/searchAdapter.ts b/core/lib/search/adapter/api/searchAdapter.ts index 9dd8c7845e..556c6f6603 100644 --- a/core/lib/search/adapter/api/searchAdapter.ts +++ b/core/lib/search/adapter/api/searchAdapter.ts @@ -38,7 +38,7 @@ export class SearchAdapter { ElasticsearchQueryBody['groupToken'] = Request.groupToken } - const storeView = (Request.store === null) ? currentStoreView() : prepareStoreView(Request.store) + const storeView = (Request.store === null) ? currentStoreView() : await prepareStoreView(Request.store) Request.index = storeView.elasticsearch.index @@ -79,7 +79,11 @@ export class SearchAdapter { 'Content-Type': 'application/json' }, body: config.elasticsearch.queryMethod === 'POST' ? JSON.stringify(ElasticsearchQueryBody) : null - }).then(resp => { return resp.json() }) + }) + .then(resp => { return resp.json() }) + .catch(error => { + throw new Error('FetchError in request to ES: ' + error.toString()) + }) } public handleResult (resp, type, start = 0, size = 50): SearchResponse { diff --git a/core/lib/search/adapter/graphql/searchAdapter.ts b/core/lib/search/adapter/graphql/searchAdapter.ts index 00e0e31eda..382abfa1f7 100644 --- a/core/lib/search/adapter/graphql/searchAdapter.ts +++ b/core/lib/search/adapter/graphql/searchAdapter.ts @@ -17,7 +17,7 @@ export class SearchAdapter { * @param {Request} Request request object * @return {Promise} */ - public search (Request) { + public async search (Request) { if (!(Request.searchQuery instanceof SearchQuery)) { throw new Error('SearchQuery instance has wrong class required to process with graphQl request.') } @@ -26,7 +26,7 @@ export class SearchAdapter { throw new Error('No entity type registered for ' + Request.type) } - const storeView = (Request.store === null) ? currentStoreView() : prepareStoreView(Request.store) + const storeView = (Request.store === null) ? currentStoreView() : await prepareStoreView(Request.store) if (storeView.storeCode === undefined || storeView.storeCode == null || !Request.type) { throw new Error('Store and SearchRequest.type are required arguments for executing Graphql query') } @@ -60,6 +60,9 @@ export class SearchAdapter { .then(resp => { return resp.json() }) + .catch(error => { + throw new Error('FetchError in request to ES: ' + error.toString()) + }) } /** diff --git a/core/lib/storeCodeFromRoute.ts b/core/lib/storeCodeFromRoute.ts new file mode 100644 index 0000000000..57b9567141 --- /dev/null +++ b/core/lib/storeCodeFromRoute.ts @@ -0,0 +1,49 @@ +import { RawLocation } from 'vue-router' +import config from 'config' +import { LocalizedRoute } from './types' + +const getNormalizedPath = (matchedRouteOrUrl) => { + const matchingPath = matchedRouteOrUrl && (matchedRouteOrUrl.path || matchedRouteOrUrl) + + return matchingPath && (matchingPath.length > 0 && matchingPath[0] !== '/') ? `/${matchingPath}` : matchingPath +} + +const getUrl = (matchedRouteOrUrl) => { + const normalizedPath = getNormalizedPath(matchedRouteOrUrl) + + if (matchedRouteOrUrl && typeof matchedRouteOrUrl === 'object') { + if (matchedRouteOrUrl['host']) { + return matchedRouteOrUrl['host'] + normalizedPath + } + + return '' + } + + return matchedRouteOrUrl +} + +const isMatchingByPath = (matchedRouteOrUrl, store) => { + const normalizedPath = getNormalizedPath(matchedRouteOrUrl) + return normalizedPath.startsWith(`${store.url}/`) || normalizedPath === store.url +} + +const isMatchingByDomainAndPath = (matchedRouteOrUrl, store) => { + const url = getUrl(matchedRouteOrUrl) + return url.startsWith(`${store.url}/`) || url === store.url +} + +const storeCodeFromRoute = (matchedRouteOrUrl: LocalizedRoute | RawLocation | string): string => { + if (!matchedRouteOrUrl) return '' + + for (let storeCode of config.storeViews.mapStoreUrlsFor) { + const store = config.storeViews[storeCode] + + if (isMatchingByPath(matchedRouteOrUrl, store) || isMatchingByDomainAndPath(matchedRouteOrUrl, store)) { + return storeCode + } + } + + return '' +} + +export default storeCodeFromRoute diff --git a/core/lib/sync/index.ts b/core/lib/sync/index.ts index 91efdfc4e2..5c272e48d3 100644 --- a/core/lib/sync/index.ts +++ b/core/lib/sync/index.ts @@ -17,7 +17,7 @@ async function queue (task) { if (err) Logger.error(err, 'sync')() Vue.prototype.$bus.$emit('sync/PROCESS_QUEUE', { config: config }) // process checkout queue resolve(task) - }).catch((reason) => { + }, config.syncTasks.disablePersistentTaskQueue).catch((reason) => { Logger.error(reason, 'sync')() // it doesn't work on SSR reject(reason) }) diff --git a/core/lib/test/unit/multistore.spec.ts b/core/lib/test/unit/multistore.spec.ts index e819bf3a62..0ddf62be10 100644 --- a/core/lib/test/unit/multistore.spec.ts +++ b/core/lib/test/unit/multistore.spec.ts @@ -1,4 +1,4 @@ -import { storeCodeFromRoute } from '../../multistore' +import storeCodeFromRoute from '../../storeCodeFromRoute' import config from 'config' jest.mock('../../../store', () => ({})) jest.mock('@vue-storefront/i18n', () => ({loadLanguageAsync: jest.fn()})) diff --git a/core/lib/types.ts b/core/lib/types.ts new file mode 100644 index 0000000000..0f76fadfea --- /dev/null +++ b/core/lib/types.ts @@ -0,0 +1,37 @@ +export interface LocalizedRoute { + path?: string, + name?: string, + hash?: string, + params?: object, + fullPath?: string, + host?: string +} + +export interface StoreView { + storeCode: string, + disabled?: boolean, + storeId: any, + name?: string, + url?: string, + appendStoreCode?: boolean, + elasticsearch: { + host: string, + index: string + }, + tax: { + sourcePriceIncludesTax: boolean, + defaultCountry: string, + defaultRegion: null | string, + calculateServerSide: boolean + }, + i18n: { + fullCountryName: string, + fullLanguageName: string, + defaultLanguage: string, + defaultCountry: string, + defaultLocale: string, + currencyCode: string, + currencySign: string, + dateFormat: string + } +} diff --git a/core/modules/cart/store/actions.ts b/core/modules/cart/store/actions.ts index 8b8dc1e1eb..b0b63c15c7 100644 --- a/core/modules/cart/store/actions.ts +++ b/core/modules/cart/store/actions.ts @@ -545,20 +545,16 @@ const actions: ActionTree = { const clientCartAddItems = [] /** helper to find the item to be added to the cart by sku */ - let productActionOptions = (serverItem) => { - return new Promise(resolve => { - if (serverItem.product_type === 'configurable') { - let searchQuery = new SearchQuery() - searchQuery = searchQuery.applyFilter({key: 'configurable_children.sku', value: {'eq': serverItem.sku}}) - dispatch('product/list', {query: searchQuery, start: 0, size: 1, updateState: false}, { root: true }).then((resp) => { - if (resp.items.length >= 1) { - resolve({ sku: resp.items[0].sku, childSku: serverItem.sku }) - } - }) - } else { - resolve({ sku: serverItem.sku }) - } - }) + const productActionOptions = async (serverItem) => { + if (serverItem.product_type === 'configurable') { + let query = new SearchQuery() + query = query.applyFilter({key: 'configurable_children.sku', value: {'eq': serverItem.sku}}) + + const { items } = await dispatch('product/list', { query, start: 0, size: 1, updateState: false }, { root: true }) + + return items.length >= 1 ? { sku: items[0].sku, childSku: serverItem.sku } : null + } + return { sku: serverItem.sku } } /** helper - sub method to update the item in the cart */ const _updateClientItem = async function ({ dispatch }, event, clientItem) { @@ -621,7 +617,7 @@ const actions: ActionTree = { for (const clientItem of clientItems) { cartHasItems = true const serverItem = serverItems.find((itm) => { - return itm.sku === clientItem.sku || itm.sku.indexOf(clientItem.sku + '-') === 0 /* bundle products */ + return String(itm.sku) === String(clientItem.sku) || itm.sku.indexOf(clientItem.sku + '-') === 0 /* bundle products */ }) if (!serverItem) { @@ -703,38 +699,52 @@ const actions: ActionTree = { }) diffLog.serverResponses.push({ 'status': res.resultCode, 'sku': serverItem.sku, 'result': res }) } else { - clientCartAddItems.push( - new Promise(resolve => { - productActionOptions(serverItem).then((actionOtions) => { - dispatch('product/single', { options: actionOtions, assignDefaultVariant: true, setCurrentProduct: false, selectDefaultVariant: false }, { root: true }).then((product) => { - resolve({ product: product, serverItem: serverItem }) - }) - }) - }) - ) + const getServerCartItem = async () => { + try { + const actionOtions = await productActionOptions(serverItem) + + if (!actionOtions) { + return null + } + + const product = await dispatch('product/single', { options: actionOtions, assignDefaultVariant: true, setCurrentProduct: false, selectDefaultVariant: false }, { root: true }) + + if (!product) { + return null + } + + return { product: product, serverItem: serverItem } + } catch (err) { + return null + } + } + clientCartAddItems.push(getServerCartItem()) } } } } } - if (clientCartAddItems.length) { + + const resolvedCartItems = await Promise.all(clientCartAddItems) + const validCartItems = resolvedCartItems.filter(Boolean) + + if (validCartItems.length) { totalsShouldBeRefreshed = true clientCartUpdateRequired = true cartHasItems = true } diffLog.items.push({ 'party': 'client', 'status': clientCartUpdateRequired ? 'update-required' : 'no-changes' }) diffLog.items.push({ 'party': 'server', 'status': serverCartUpdateRequired ? 'update-required' : 'no-changes' }) - Promise.all(clientCartAddItems).then((items) => { - items.map(({ product, serverItem }) => { - product.server_item_id = serverItem.item_id - product.qty = serverItem.qty - product.server_cart_id = serverItem.quote_id - if (serverItem.product_option) { - product.product_option = serverItem.product_option - } - dispatch('addItem', { productToAdd: product, forceServerSilence: true }) - }) - }) + + for (const { product, serverItem } of validCartItems) { + product.server_item_id = serverItem.item_id + product.qty = serverItem.qty + product.server_cart_id = serverItem.quote_id + if (serverItem.product_option) { + product.product_option = serverItem.product_option + } + dispatch('addItem', { productToAdd: product, forceServerSilence: true }) + } if (!dryRun) { if (totalsShouldBeRefreshed && cartHasItems) { diff --git a/core/modules/catalog/helpers/tax.ts b/core/modules/catalog/helpers/tax.ts index 1416339ba0..91d8688ba0 100644 --- a/core/modules/catalog/helpers/tax.ts +++ b/core/modules/catalog/helpers/tax.ts @@ -32,6 +32,12 @@ export function updateProductPrices (product, rate, sourcePriceInclTax = false) product.priceTax = priceExclTax * rateFactor product.priceInclTax = priceExclTax + product.priceTax + if (!product.original_price) { + product.original_price = priceExclTax + product.original_price_incl_tax = product.price_incl_tax + product.original_price_tax = product.price_tax + } + let specialPriceExclTax = product.special_price if (sourcePriceInclTax) { specialPriceExclTax = product.special_price / (1 + rateFactor) @@ -41,7 +47,7 @@ export function updateProductPrices (product, rate, sourcePriceInclTax = false) product.specialPriceTax = specialPriceExclTax * rateFactor product.specialPriceInclTax = specialPriceExclTax + product.specialPriceTax - if (product.special_price && (product.special_price < product.price)) { + if (product.special_price && (product.special_price < product.original_price)) { if (!isSpecialPriceActive(product.special_from_date, product.special_to_date)) { product.special_price = 0 // out of the dates period } else { diff --git a/core/modules/catalog/hooks/beforeRegistration.ts b/core/modules/catalog/hooks/beforeRegistration.ts index 0535e5a459..9a529e6c21 100644 --- a/core/modules/catalog/hooks/beforeRegistration.ts +++ b/core/modules/catalog/hooks/beforeRegistration.ts @@ -23,10 +23,4 @@ export function beforeRegistration ({ Vue, config, store, isServer }) { storeName: 'elasticCache', driver: localForage[config.localForage.defaultDrivers['elasticCache']] }), true, config.server.elasticCacheQuota) - - Vue.prototype.$db.productsCollection = new UniversalStorage(localForage.createInstance({ - name: dbNamePrefix + 'shop', - storeName: 'products', - driver: localForage[config.localForage.defaultDrivers['products']] - })) } diff --git a/core/modules/catalog/store/product/actions.ts b/core/modules/catalog/store/product/actions.ts index ecfe6a0395..535a6e01af 100644 --- a/core/modules/catalog/store/product/actions.ts +++ b/core/modules/catalog/store/product/actions.ts @@ -341,9 +341,15 @@ const actions: ActionTree = { } const cacheKey = entityKeyName(cacheByKey, prod[(cacheByKey === 'sku' && prod['parentSku']) ? 'parentSku' : cacheByKey]) // to avoid caching products by configurable_children.sku if (isCacheable) { // store cache only for full loads - cache.setItem(cacheKey, prod) + cache.setItem(cacheKey, prod, null, config.products.disablePersistentProductsCache) .catch((err) => { Logger.error('Cannot store cache for ' + cacheKey, err)() + if ( + err.name === 'QuotaExceededError' || + err.name === 'NS_ERROR_DOM_QUOTA_REACHED' + ) { // quota exceeded error + cache.clear() // clear products cache if quota exceeded + } }) } if ((prod.type_id === 'grouped' || prod.type_id === 'bundle') && prefetchGroupProducts && !isServer) { diff --git a/core/modules/compare/store/plugin.ts b/core/modules/compare/store/plugin.ts index 2d2378bd17..46a2ac434d 100644 --- a/core/modules/compare/store/plugin.ts +++ b/core/modules/compare/store/plugin.ts @@ -7,7 +7,7 @@ export function plugin (mutation, state) { if ([types.COMPARE_ADD_ITEM, types.COMPARE_DEL_ITEM, types.COMPARE_LOAD_COMPARE].includes(type)) { // check if this mutation is comapre related cacheStorage.setItem('current-compare', state.compare.items).catch((reason) => { - Logger.error(reason, 'compare') // it doesn't work on SSR + Logger.error(reason, 'compare') }) } } diff --git a/core/modules/url/helpers/index.ts b/core/modules/url/helpers/index.ts index 9029ee7a8f..be799affe6 100644 --- a/core/modules/url/helpers/index.ts +++ b/core/modules/url/helpers/index.ts @@ -1,6 +1,7 @@ import { router } from '@vue-storefront/core/app' import config from 'config' -import { localizedDispatcherRoute, localizedRoute, LocalizedRoute, currentStoreView } from '@vue-storefront/core/lib/multistore' +import { LocalizedRoute } from '@vue-storefront/core/lib/types' +import { localizedDispatcherRoute, localizedRoute, currentStoreView } from '@vue-storefront/core/lib/multistore' import { RouteConfig } from 'vue-router/types/router'; import { RouterManager } from '@vue-storefront/core/lib/router-manager' @@ -13,20 +14,20 @@ export function parametrizeRouteData (routeData: LocalizedRoute, query: { [id: s return parametrizedRoute } -function prepareDynamicRoutes (routeData: LocalizedRoute, fullPath: string): RouteConfig[] { +function prepareDynamicRoutes (routeData: LocalizedRoute, path: string): RouteConfig[] { const userRoute = RouterManager.findByName(routeData.name) if (userRoute) { const currentStoreCode = currentStoreView().storeCode - const dynamicRouteName = (config.defaultStoreCode !== currentStoreCode) ? `urldispatcher-${fullPath}-${currentStoreCode}` : `urldispatcher-${fullPath}` - const dynamicRoute = Object.assign({}, userRoute, routeData, { path: '/' + fullPath, name: dynamicRouteName }) + const dynamicRouteName = (config.defaultStoreCode !== currentStoreCode) ? `urldispatcher-${path}-${currentStoreCode}` : `urldispatcher-${path}` + const dynamicRoute = Object.assign({}, userRoute, routeData, { path: '/' + path, name: dynamicRouteName }) return [dynamicRoute] } else { return [] } } -export function processDynamicRoute (routeData: LocalizedRoute, fullPath: string, addToRoutes: boolean = true): LocalizedRoute[] { - const preparedRoutes = prepareDynamicRoutes(routeData, fullPath) +export function processDynamicRoute (routeData: LocalizedRoute, path: string, addToRoutes: boolean = true): LocalizedRoute[] { + const preparedRoutes = prepareDynamicRoutes(routeData, path) if (addToRoutes && preparedRoutes) { RouterManager.addRoutes(preparedRoutes, router) } @@ -44,8 +45,8 @@ export function processMultipleDynamicRoutes (dispatcherMap: {}, addToRoutes: bo return preparedRoutes } -export function findRouteByPath (fullPath: string): RouteConfig { - return RouterManager.findByPath(fullPath) +export function findRouteByPath (path: string): RouteConfig { + return RouterManager.findByPath(path) } export function normalizeUrlPath (url: string): string { @@ -77,11 +78,11 @@ export function formatProductLink ( let routeData: LocalizedRoute; if (product.configurable_children && product.configurable_children.length > 0) { routeData = { - fullPath: product.url_path, + path: product.url_path, params: { childSku: product.sku } } } else { - routeData = { fullPath: product.url_path } + routeData = { path: product.url_path } } return localizedDispatcherRoute(routeData, storeCode) } else { diff --git a/core/modules/url/router/beforeEach.ts b/core/modules/url/router/beforeEach.ts index 0d79ce3eea..72ad0d74d0 100644 --- a/core/modules/url/router/beforeEach.ts +++ b/core/modules/url/router/beforeEach.ts @@ -6,12 +6,14 @@ import store from '@vue-storefront/core/store' import { Logger } from '@vue-storefront/core/lib/logger' import { processDynamicRoute, normalizeUrlPath } from '../helpers' import { isServer } from '@vue-storefront/core/helpers' -import { currentStoreView, LocalizedRoute, localizedRoute } from '@vue-storefront/core/lib/multistore' +import { currentStoreView, localizedRoute } from '@vue-storefront/core/lib/multistore' +import { LocalizedRoute } from '@vue-storefront/core/lib/types' import Vue from 'vue' import { RouterManager } from '@vue-storefront/core/lib/router-manager' +import { routerHelper } from '@vue-storefront/core/helpers' export const UrlDispatchMapper = async (to) => { - const routeData = await store.dispatch('url/mapUrl', { url: to.fullPath, query: to.query }) + const routeData = await store.dispatch('url/mapUrl', { url: to.path, query: to.query }) return Object.assign({}, to, routeData) } @@ -23,21 +25,24 @@ export async function beforeEach (to: Route, from: Route, next) { } RouterManager.lockRoute() - const fullPath = normalizeUrlPath(to.fullPath) + const path = normalizeUrlPath(to.path) const hasRouteParams = to.hasOwnProperty('params') && Object.values(to.params).length > 0 const isPreviouslyDispatchedDynamicRoute = to.matched.length > 0 && to.name && to.name.startsWith('urldispatcher') if (!to.matched.length || (isPreviouslyDispatchedDynamicRoute && !hasRouteParams)) { UrlDispatchMapper(to).then((routeData) => { if (routeData) { - let dynamicRoutes: LocalizedRoute[] = processDynamicRoute(routeData, fullPath, !isPreviouslyDispatchedDynamicRoute) + let dynamicRoutes: LocalizedRoute[] = processDynamicRoute(routeData, path, !isPreviouslyDispatchedDynamicRoute) if (dynamicRoutes && dynamicRoutes.length > 0) { - next(dynamicRoutes[0]) + next({ + ...dynamicRoutes[0], + replace: routerHelper.popStateDetected || dynamicRoutes[0].fullPath === from.fullPath + }) } else { Logger.error('Route not found ' + routeData['name'], 'dispatcher')() next(localizedRoute('/page-not-found', currentStoreView().storeCode)) } } else { - Logger.error('No mapping found for ' + fullPath, 'dispatcher')() + Logger.error('No mapping found for ' + path, 'dispatcher')() next(localizedRoute('/page-not-found', currentStoreView().storeCode)) } }).catch(e => { @@ -50,10 +55,12 @@ export async function beforeEach (to: Route, from: Route, next) { // ps. we can't use the next() call here as it's not doing the real redirect in SSR mode (just processing different component without changing the URL and that causes the CSR / SSR DOM mismatch while hydrating) } }).finally(() => { + routerHelper.popStateDetected = false RouterManager.unlockRoute() }) } else { next() RouterManager.unlockRoute() + routerHelper.popStateDetected = false } } diff --git a/core/modules/url/store/actions.ts b/core/modules/url/store/actions.ts index ccafbc2a10..86f8f7ca50 100644 --- a/core/modules/url/store/actions.ts +++ b/core/modules/url/store/actions.ts @@ -4,17 +4,27 @@ import * as types from './mutation-types' // you can use this storage if you want to enable offline capabilities import { cacheStorage } from '../' import queryString from 'query-string' +import config from 'config' import SearchQuery from '@vue-storefront/core/lib/search/searchQuery' import { processMultipleDynamicRoutes, normalizeUrlPath, parametrizeRouteData } from '../helpers' -import { storeCodeFromRoute, removeStoreCodeFromRoute } from '@vue-storefront/core/lib/multistore' -import config from 'config' +import { removeStoreCodeFromRoute } from '@vue-storefront/core/lib/multistore' +import storeCodeFromRoute from '@vue-storefront/core/lib/storeCodeFromRoute' // it's a good practice for all actions to return Promises with effect of their execution export const actions: ActionTree = { // if you want to use cache in your module you can load cached data like this async registerMapping ({ commit }, { url, routeData }: { url: string, routeData: any}) { commit(types.REGISTER_MAPPING, { url, routeData }) - await cacheStorage.setItem(normalizeUrlPath(url), routeData) + try { + await cacheStorage.setItem(normalizeUrlPath(url), routeData, null, config.seo.disableUrlRoutesPersistentCache) + } catch (err) { + if ( + err.name === 'QuotaExceededError' || + err.name === 'NS_ERROR_DOM_QUOTA_REACHED' + ) { // quota exceeded error + cacheStorage.clear() // clear the url cache if quota has been exceeded + } + } return routeData }, /** diff --git a/core/modules/url/types/UrlState.ts b/core/modules/url/types/UrlState.ts index 7fed8b4469..fba3babff0 100644 --- a/core/modules/url/types/UrlState.ts +++ b/core/modules/url/types/UrlState.ts @@ -1,4 +1,4 @@ -import { LocalizedRoute } from 'core/lib/multistore'; +import { LocalizedRoute } from '@vue-storefront/core/lib/types' // This object should represent structure of your modules Vuex state // It's a good practice is to name this interface accordingly to the KET (for example mailchimpState) diff --git a/core/modules/user/components/UserShippingDetails.ts b/core/modules/user/components/UserShippingDetails.ts index 271fc4f735..020c9fd054 100644 --- a/core/modules/user/components/UserShippingDetails.ts +++ b/core/modules/user/components/UserShippingDetails.ts @@ -130,23 +130,17 @@ export const UserShippingDetails = { } }, fillCompanyAddress () { - this.useCompanyAddress = !this.useCompanyAddress if (this.useCompanyAddress) { - let index - for (let i = 0; i < this.currentUser.addresses.length; i++) { - if (toString(this.currentUser.addresses[i].id) === toString(this.currentUser.default_billing)) { - index = i - } - } - if (index >= 0) { - this.shippingDetails.firstName = this.currentUser.addresses[index].firstname - this.shippingDetails.lastName = this.currentUser.addresses[index].lastname - this.shippingDetails.street = this.currentUser.addresses[index].street[0] - this.shippingDetails.house = this.currentUser.addresses[index].street[1] - this.shippingDetails.city = this.currentUser.addresses[index].city - this.shippingDetails.postcode = this.currentUser.addresses[index].postcode - this.shippingDetails.region = this.currentUser.addresses[index].region.region ? this.currentUser.addresses[index].region.region : '' - this.shippingDetails.country = this.currentUser.addresses[index].country_id + const companyAddress = this.currentUser.addresses.find((address) => toString(address.id) === toString(this.currentUser.default_billing)) + if (companyAddress) { + this.shippingDetails.firstName = companyAddress.firstname + this.shippingDetails.lastName = companyAddress.lastname + this.shippingDetails.street = companyAddress.street[0] + this.shippingDetails.house = companyAddress.street[1] + this.shippingDetails.city = companyAddress.city + this.shippingDetails.postcode = companyAddress.postcode + this.shippingDetails.region = companyAddress.region.region ? companyAddress.region.region : '' + this.shippingDetails.country = companyAddress.country_id } } else { this.shippingDetails = this.getShippingDetails() diff --git a/core/scripts/server.js b/core/scripts/server.js index 670d2c7c3b..b29a59a85d 100755 --- a/core/scripts/server.js +++ b/core/scripts/server.js @@ -1,6 +1,7 @@ const fs = require('fs') const path = require('path') const express = require('express') +const ms = require('ms') const compile = require('lodash.template') const rootPath = require('app-root-path').path const resolve = file => path.resolve(rootPath, file) @@ -104,8 +105,15 @@ function invalidateCache (req, res) { } const serve = (path, cache, options) => express.static(resolve(path), Object.assign({ - maxAge: cache && isProd ? 2592000000 : 0, // 1 month in milliseconds = 1000 * 60 * 60 * 24 * 30 = 2592000000 - fallthrough: false + fallthrough: false, + setHeaders: cache && isProd ? function (res, path) { + const mimeType = express.static.mime.lookup(path); + let maxAge = config.expireHeaders.default; + if (config.expireHeaders.hasOwnProperty(mimeType)) { + maxAge = config.expireHeaders.get(mimeType); + } + res.setHeader('Cache-Control', 'public, max-age=' + ms(maxAge)); + } : null }, options)) const themeRoot = require('../build/theme-path') diff --git a/core/server-entry.ts b/core/server-entry.ts index b555876d52..2569e88a7a 100755 --- a/core/server-entry.ts +++ b/core/server-entry.ts @@ -2,7 +2,7 @@ import union from 'lodash-es/union' import { createApp } from '@vue-storefront/core/app' import { HttpError } from '@vue-storefront/core/helpers/exceptions' -import { storeCodeFromRoute } from '@vue-storefront/core/lib/multistore' +import storeCodeFromRoute from '@vue-storefront/core/lib/storeCodeFromRoute' import omit from 'lodash-es/omit' import pick from 'lodash-es/pick' import buildTimeConfig from 'config' diff --git a/core/store/lib/storage.ts b/core/store/lib/storage.ts index 673d2fa075..0ee70ac661 100644 --- a/core/store/lib/storage.ts +++ b/core/store/lib/storage.ts @@ -42,7 +42,6 @@ class LocalForageCacheDriver { private _persistenceErrorNotified: boolean; private _useLocalCacheByDefault: boolean; private cacheErrorsCount: any; - private localCache: any; private _storageQuota: number; public constructor (collection, useLocalCacheByDefault = true, storageQuota = 0) { @@ -105,6 +104,14 @@ class LocalForageCacheDriver { this._persistenceErrorNotified = false } + public getLastError () { + return this._lastError + } + + public getDbName () { + return this._dbName + } + // Remove all keys from the datastore, effectively destroying all data in // the app's key/value store! public clear (callback?) { @@ -313,6 +320,7 @@ class LocalForageCacheDriver { }).catch(err => { isResolved = true this._lastError = err + throw err })) setTimeout(() => { if (!isResolved) { // this is cache time out check diff --git a/docs/guide/upgrade-notes/README.md b/docs/guide/upgrade-notes/README.md index 7ff4708621..83bee3a5d6 100644 --- a/docs/guide/upgrade-notes/README.md +++ b/docs/guide/upgrade-notes/README.md @@ -2,6 +2,17 @@ We're trying to keep the upgrade process as easy as possible. Unfortunately, sometimes manual code changes are required. Before pulling out the latest version, please take a look at the upgrade notes below: +## 1.10 -> 1.10.4 + +We've decreased the `localStorage` quota usage + error handling by introducing new config variables: + +- `config.products.disablePersistentProductsCache` to not store products by SKU (by default it's on). Products are cached in ServiceWorker cache anyway so the `product/list` will populate the in-memory cache (`cache.setItem(..., memoryOnly = true)`); +- `config.seo.disableUrlRoutesPersistentCache` - to not store the url mappings; they're stored in in-memory cache anyway so no additional requests will be made to the backend for url mapping; however it might cause some issues with url routing in the offline mode (when the offline mode PWA installed on homescreen got reloaded, the in-memory cache will be cleared so there won't potentially be the url mappings; however the same like with `product/list` the ServiceWorker cache SHOULD populate url mappings anyway); +- `config.syncTasks.disablePersistentTaskQueue` to not store the network requests queue in service worker. Currently only the stock-check and user-data changes were using this queue. The only downside it introuces can be related to the offline mode and these tasks will not be re-executed after connectivity established, but just in a case when the page got reloaded while offline (yeah it might happen using ServiceWorker; `syncTasks` can't be re-populated in cache from SW) + +If by some reasons you wan't to have the `localStorage` back on for `Products by SKU`, `Url Routes` and `SyncTasks` - please juset set these variables back to `false` in your `config/local.json`. + + ## 1.9 -> 1.10 - Event `application-after-init` is now emitted by event bus instead of root Vue instance (app), so you need to listen to `Vue.prototype.$bus` (`Vue.prototype.$bus.$on()`) now - The lowest supported node version is currently 8.10.0, @@ -20,7 +31,7 @@ We're trying to keep the upgrade process as easy as possible. Unfortunately, som - The getter `cart/totalQuantity` has been replaced with `cart/getItemsTotalQuantity` - @pkarw (#2522) ## 1.8 -> 1.9 -- The Url Dispatcher feature added for friendly URLs. When `config.seo.useUrlDispatcher` set to true the `product.url_path` and `category.url_path` fields are used as absolute URL addresses (no `/c` and `/p` prefixes anymore). Check the latest `mage2vuestorefront` snapshot and **reimport Your products** to properly set `url_path` fields +- The Url Dispatcher feature added for friendly URLs. When `config.seo.useUrlDispatcher` set to true the `product.url_path` and `category.url_path` fields are used as absolute URL addresses (no `/c` and `/p` prefixes anymore). Check the latest `mage2vuestorefront` snapshot and **reimport Your products** to properly set `url_path` fields - `cart.multisiteCommonCart` config property changed to `storeViews.commonCache` - Way of creating VS Modules was changed to use factory method instead of explict object creation. Even though the feature is backward compatible we highly encourage all developers to refactor their modules to use new syntax. @@ -28,8 +39,8 @@ The process of creating a new module with the factory method looks like the foll ````js import { createModule } from '@vue-storefront/core/lib/module' -const moduleConfig: VueStorefrontModuleConfig = { - // VS module config +const moduleConfig: VueStorefrontModuleConfig = { + // VS module config } const module = createModule(moduleConfig) @@ -41,7 +52,7 @@ const module = createModule(moduleConfig) - Added validation for UTF8 alpha and alphanumeric characters in most checkout fields - Update your local.json config and set default `api.url` path, without it you may have problems with elasticsearch queries. -### Troubleshooting +### Troubleshooting - In case of CORS problem after upgrade check your elasticsearch url in config file. Best practice for that change can be found [here](https://github.com/DivanteLtd/vue-storefront/commit/77fc9c2765068303879c75ef9ed4a4b98f6763b6) - In case of app crashing, especially when opening `vue devtools` in browser, try setting `storeViews.commonCache` to `false`. @@ -64,14 +75,14 @@ Full changelog is available [here](https://github.com/DivanteLtd/vue-storefront/ Starting from Vue Storefront 1.7, we changed the caching strategy and offline-ready features: - By default, the Elasticsearch Queries are executed using `GET` method and therefore are cached by Service Worker (`config.elasticsearch.queryMethod` — set it to POST for the previous behavior and if you're using graphql). - By default, products and queries cache is set in `LocalStorage` with a quota set to 4MB (`config.server.elasticCacheQuota`). If the storage quota is set, the cache purging is executed every 30 seconds using the LRU algorithm. Local Storage is limited to 5MB in most browsers. -- We added `config.server. disableLocalStorageQueriesCache`, which is set to `true` by default. When this option is on, we're not storing the Elasticsearch results in the local cache because results are by default cached in the Service Worker cache anyway. +- We added `config.server. disablePersistentQueriesCache`, which is set to `true` by default. When this option is on, we're not storing the Elasticsearch results in the local cache because results are by default cached in the Service Worker cache anyway. - `module.extend` has been changed to `extendModule`. You can find usage examples in `src/modules/index.ts`. - [routes](https://github.com/patzick/vue-storefront/commit/a97eb11868de2915e86d57c4279caf944d4de422#diff-a334a7caeb7f61836f8c1178d92de3e0), [layouts](https://github.com/patzick/vue-storefront/commit/a97eb11868de2915e86d57c4279caf944d4de422#diff-48863b9fe31d7713222ec5709ef5a4fa), and component, which are not visible at page rendering are now loaded when they are needed. - Every store manipulation should be done by dispatching actions. Invoke store dispatch on `category/mergeSearchOptions` when manipulating `store.state.category.current_product_query` somewhere. - [here](https://github.com/patzick/vue-storefront/commit/a97eb11868de2915e86d57c4279caf944d4de422) are all changes in default themes Backward compatibility: To reverse to the 1.0–1.6 behavior: -- Set `config.server.disableLocalStorageQueriesCache` = `false`, +- Set `config.server.disablePersistentQueriesCache` = `false`, - Set `config.elasticsearch.queryMethod` = `POST` - Set `config.localForage.defaultDrivers.elasticCache` = `INDEXEDDB` @@ -260,7 +271,7 @@ We added Reviews support, however, Magento 2 is still lacking Reviews support in - **removeCoupon** -> **clearCoupon** - removing coupon by dispatch removeCoupon API method and toggle coupon form - **applyCoupon** -> **setCoupon** - submit coupon form by dispatch applyCoupon API method - **enterCoupon** - was removed, because @keyup="enterCoupon" we changed to @keyup.enter="setCoupon" - + 3. We moved $emit with notification about appliedCoupon and removedCoupon from Vuex store to the default theme. Now applyCoupon and removeCoupon returns promise, which you can handle by yourself. 4. We moved VueOfflineMixin and onEscapePress mixins to the theme component. The core component is clean from UI stuff now. @@ -380,7 +391,7 @@ export default { ``` -4. We've added Multistore support. It shouldn't imply any breaking changes to the existing themes / extensions (by default it's just disabled). +4. We've added Multistore support. It shouldn't imply any breaking changes to the existing themes / extensions (by default it's just disabled). ## 1.0RC-2 -> 1.0RC-3 ([release notes](https://github.com/DivanteLtd/vue-storefront/releases/tag/v1.0.0-rc.3)) diff --git a/docs/package.json b/docs/package.json index 2a3c556d66..b8d1bba713 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,7 +1,7 @@ { "name": "@vue-storefront/docs", "private": true, - "version": "1.10.3", + "version": "1.10.4", "scripts": { "docs:dev": "vuepress dev", "docs:build": "vuepress build", diff --git a/lerna.json b/lerna.json index 718ef47218..a227fe611b 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "lerna": "3.14.1", - "version": "1.10.3", + "version": "1.10.4", "npmClient": "yarn", "useWorkspaces": true, "registry": "https://registry.npmjs.org/" diff --git a/package.json b/package.json index 1231098778..30db526c00 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-storefront", - "version": "1.10.3", + "version": "1.10.4", "description": "A Vue.js, PWA eCommerce frontend", "private": true, "engines": { @@ -68,6 +68,7 @@ "js-sha3": "^0.8.0", "localforage": "^1.7.2", "magento2-rest-client": "github:DivanteLtd/magento2-rest-client", + "ms": "^2.1.2", "pm2": "^2.10.4", "redis-tag-cache": "^1.2.1", "reflect-metadata": "^0.1.12", diff --git a/src/themes/default-amp/package.json b/src/themes/default-amp/package.json index df50b67bc8..cb5fe589ea 100755 --- a/src/themes/default-amp/package.json +++ b/src/themes/default-amp/package.json @@ -1,6 +1,6 @@ { "name": "@vue-storefront/theme-default-amp", - "version": "1.10.3", + "version": "1.10.4", "description": "Default AMP theme for Vue Storefront", "main": "index.js", "scripts": { diff --git a/src/themes/default/components/core/CookieNotification.vue b/src/themes/default/components/core/CookieNotification.vue index 93686a9cfd..6014a89b8e 100644 --- a/src/themes/default/components/core/CookieNotification.vue +++ b/src/themes/default/components/core/CookieNotification.vue @@ -7,7 +7,7 @@ {{ message }} - + {{ detailsLinkText }} @@ -74,13 +74,15 @@ export default { diff --git a/src/themes/default/components/core/blocks/SearchPanel/SearchPanel.gql.vue b/src/themes/default/components/core/blocks/SearchPanel/SearchPanel.gql.vue index e662a39aca..3bdfb4335d 100644 --- a/src/themes/default/components/core/blocks/SearchPanel/SearchPanel.gql.vue +++ b/src/themes/default/components/core/blocks/SearchPanel/SearchPanel.gql.vue @@ -67,6 +67,7 @@ export default { transition: transform 300ms $motion-main; overflow-y: auto; overflow-x: hidden; + -webkit-overflow-scrolling: touch; &.active { transform: translateX(0); diff --git a/src/themes/default/components/core/blocks/Wishlist/Product.vue b/src/themes/default/components/core/blocks/Wishlist/Product.vue index 9a71ef317b..11fe9f325e 100644 --- a/src/themes/default/components/core/blocks/Wishlist/Product.vue +++ b/src/themes/default/components/core/blocks/Wishlist/Product.vue @@ -75,4 +75,9 @@ input { .image{ flex: 0 0 121px; } +.price-original { + text-decoration: line-through; + color: #828282; + font-size: .95rem; +} diff --git a/src/themes/default/components/theme/blocks/Inspirations/InspirationTile.vue b/src/themes/default/components/theme/blocks/Inspirations/InspirationTile.vue index cc81402b3d..eda480a67c 100644 --- a/src/themes/default/components/theme/blocks/Inspirations/InspirationTile.vue +++ b/src/themes/default/components/theme/blocks/Inspirations/InspirationTile.vue @@ -1,7 +1,7 @@