From 4fb53e5009e8b83ef2901502fe150e403b63d920 Mon Sep 17 00:00:00 2001 From: pkarw Date: Fri, 8 Feb 2019 23:58:40 +0100 Subject: [PATCH 01/14] PoC - multipart, dynamic URL rewrite --- config/default.json | 2 +- core/modules/url/hooks/afterRegistration.ts | 5 +++ core/modules/url/hooks/beforeRegistration.ts | 5 +++ core/modules/url/index.ts | 20 +++++++++ core/modules/url/pages/UrlDispatcher.vue | 36 ++++++++++++++++ core/modules/url/store/actions.ts | 44 ++++++++++++++++++++ core/modules/url/store/index.ts | 13 ++++++ core/modules/url/store/mutation-types.ts | 1 + core/modules/url/store/mutations.ts | 8 ++++ core/modules/url/store/state.ts | 5 +++ core/modules/url/types/UrlState.ts | 5 +++ src/modules/index.ts | 4 +- 12 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 core/modules/url/hooks/afterRegistration.ts create mode 100644 core/modules/url/hooks/beforeRegistration.ts create mode 100644 core/modules/url/index.ts create mode 100755 core/modules/url/pages/UrlDispatcher.vue create mode 100644 core/modules/url/store/actions.ts create mode 100644 core/modules/url/store/index.ts create mode 100644 core/modules/url/store/mutation-types.ts create mode 100644 core/modules/url/store/mutations.ts create mode 100644 core/modules/url/store/state.ts create mode 100644 core/modules/url/types/UrlState.ts diff --git a/config/default.json b/config/default.json index 3e7ed38ce2..7184499247 100644 --- a/config/default.json +++ b/config/default.json @@ -17,7 +17,7 @@ "elasticCacheQuota": 4096 }, "seo": { - "useUrlDispatcher": false + "useUrlDispatcher": true }, "console": { "showErrorOnProduction" : true, diff --git a/core/modules/url/hooks/afterRegistration.ts b/core/modules/url/hooks/afterRegistration.ts new file mode 100644 index 0000000000..c0c57193d1 --- /dev/null +++ b/core/modules/url/hooks/afterRegistration.ts @@ -0,0 +1,5 @@ +import { Logger } from '@vue-storefront/core/lib/logger' + +// This function will be fired both on server and client side context after registering other parts of the module +export function afterRegistration({ Vue, config, store, isServer }){ +} diff --git a/core/modules/url/hooks/beforeRegistration.ts b/core/modules/url/hooks/beforeRegistration.ts new file mode 100644 index 0000000000..f29f9c72f8 --- /dev/null +++ b/core/modules/url/hooks/beforeRegistration.ts @@ -0,0 +1,5 @@ +import { AsyncDataLoader } from '@vue-storefront/core/lib/async-data-loader' + +// This function will be fired both on server and client side context before registering other parts of the module +export function beforeRegistration({ Vue, config, store, isServer }) { +} diff --git a/core/modules/url/index.ts b/core/modules/url/index.ts new file mode 100644 index 0000000000..28ade6fe24 --- /dev/null +++ b/core/modules/url/index.ts @@ -0,0 +1,20 @@ +import { module } from './store' +import UrlDispatcher from './pages/UrlDispatcher.vue' +import { beforeRegistration } from './hooks/beforeRegistration' +import { afterRegistration } from './hooks/afterRegistration' +import { VueStorefrontModule, VueStorefrontModuleConfig } from '@vue-storefront/core/lib/module' +import { initCacheStorage } from '@vue-storefront/core/helpers/initCacheStorage' +export const KEY = 'url' +export const cacheStorage = initCacheStorage(KEY) + +const moduleConfig: VueStorefrontModuleConfig = { + key: KEY, + store: { modules: [{ key: KEY, module }] }, + beforeRegistration, + afterRegistration, + router: { routes: [ + { name: 'urldispatcher', path: '*', component: UrlDispatcher } + ] } +} + +export const Url = new VueStorefrontModule(moduleConfig) diff --git a/core/modules/url/pages/UrlDispatcher.vue b/core/modules/url/pages/UrlDispatcher.vue new file mode 100755 index 0000000000..428adb0a39 --- /dev/null +++ b/core/modules/url/pages/UrlDispatcher.vue @@ -0,0 +1,36 @@ + + + diff --git a/core/modules/url/store/actions.ts b/core/modules/url/store/actions.ts new file mode 100644 index 0000000000..bb9cd7b5bf --- /dev/null +++ b/core/modules/url/store/actions.ts @@ -0,0 +1,44 @@ +import { UrlState } from '../types/UrlState' +import { ActionTree } from 'vuex'; +import * as types from './mutation-types' +// you can use this storage if you want to enable offline capabilities +import { cacheStorage } from '../' + +// 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 + registerMapping ({ commit }, { url, routeData }) { + return new Promise ((resolve, reject) => { + commit(types.REGISTER_MAPPING, { url, routeData }) + cacheStorage.setItem(url, routeData).then(result => { + resolve(routeData) + }).catch(() => reject()) + }) + }, + mapUrl ({ state, dispatch }, { url }) { + return new Promise ((resolve, reject) => { + if (state.dispatcherMap.hasOwnProperty(url)) { + return resolve (state.dispatcherMap[url]) + } + cacheStorage.getItem(url).then(routeData => { + if (routeData !== null) { + return resolve(routeData) + } else { + return resolve(dispatch('mappingFallback', { url })) + } + }).catch(() => reject()) + }) + }, + /** + * Router mapping fallback - get the proper URL from API + */ + mappingFallback ({ commit, dispatch }, { url }) { + return new Promise ((resolve, reject) => { + if (url === '/c/women-20') { + resolve('/c/men-11') + } else { + resolve(null) + } + }) + } +} \ No newline at end of file diff --git a/core/modules/url/store/index.ts b/core/modules/url/store/index.ts new file mode 100644 index 0000000000..535e65aa2a --- /dev/null +++ b/core/modules/url/store/index.ts @@ -0,0 +1,13 @@ +import { Module } from 'vuex' +import { UrlState } from '../types/UrlState' +import { mutations } from './mutations' +import { actions } from './actions' + +export const module: Module = { + namespaced: true, + mutations, + actions, + state: { + dispatcherMap: {} + } +} \ No newline at end of file diff --git a/core/modules/url/store/mutation-types.ts b/core/modules/url/store/mutation-types.ts new file mode 100644 index 0000000000..9371d91eb4 --- /dev/null +++ b/core/modules/url/store/mutation-types.ts @@ -0,0 +1 @@ +export const REGISTER_MAPPING = 'REGISTER_MAPPING' diff --git a/core/modules/url/store/mutations.ts b/core/modules/url/store/mutations.ts new file mode 100644 index 0000000000..9dcae4e7b5 --- /dev/null +++ b/core/modules/url/store/mutations.ts @@ -0,0 +1,8 @@ +import { MutationTree } from 'vuex' +import * as types from './mutation-types' + +export const mutations: MutationTree = { + [types.REGISTER_MAPPING] (state, payload) { + state.dispatcherMap[payload.url] = payload.routeData + } +} \ No newline at end of file diff --git a/core/modules/url/store/state.ts b/core/modules/url/store/state.ts new file mode 100644 index 0000000000..ad5e784b78 --- /dev/null +++ b/core/modules/url/store/state.ts @@ -0,0 +1,5 @@ +import { UrlState } from '../types/UrlState' + +export const state: UrlState = { + dispatcherMap: {} +} \ No newline at end of file diff --git a/core/modules/url/types/UrlState.ts b/core/modules/url/types/UrlState.ts new file mode 100644 index 0000000000..bab5207de0 --- /dev/null +++ b/core/modules/url/types/UrlState.ts @@ -0,0 +1,5 @@ +// 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) +export interface UrlState { + dispatcherMap: {} +} \ No newline at end of file diff --git a/src/modules/index.ts b/src/modules/index.ts index 580045c92f..16a85e302c 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -10,6 +10,7 @@ import { Wishlist } from '@vue-storefront/core/modules/wishlist' import { Mailchimp } from '../modules/mailchimp' import { Notification } from '@vue-storefront/core/modules/notification' import { RecentlyViewed } from '@vue-storefront/core/modules/recently-viewed' +import { Url } from '@vue-storefront/core/modules/url' import { Homepage } from "./homepage" import { Claims } from './claims' import { PromotedOffers } from './promoted-offers' @@ -69,6 +70,7 @@ export const registerModules: VueStorefrontModule[] = [ PaymentBackendMethods, PaymentCashOnDelivery, RawOutputExample, - AmpRenderer/*, + AmpRenderer, + Url/*, Example*/ ] From a2f34059d2a4c7f464a72c56a01189d4655fc98a Mon Sep 17 00:00:00 2001 From: pkarw Date: Tue, 12 Feb 2019 07:11:22 +0100 Subject: [PATCH 02/14] Logger fixes + url mapping --- core/lib/logger.ts | 45 ++++++++++++++------- core/modules/url/hooks/afterRegistration.ts | 8 ++++ core/modules/url/index.ts | 32 ++++++++++++++- core/modules/url/store/actions.ts | 2 + 4 files changed, 70 insertions(+), 17 deletions(-) diff --git a/core/lib/logger.ts b/core/lib/logger.ts index 22830e8d65..bf5f1cc681 100644 --- a/core/lib/logger.ts +++ b/core/lib/logger.ts @@ -1,6 +1,5 @@ import { isServer } from '@vue-storefront/core/helpers' import buildTimeConfig from 'config' - const bgColorStyle = (color) => `color: white; background: ${color}; padding: 4px; font-weight: bold; font-size: 0.8em'` /** VS message logger. By default works only on dev mode */ @@ -34,6 +33,22 @@ class Logger this.isProduction = process.env.NODE_ENV === 'production'; } + /** + * Convert message to string - as it may be object, array either primitive + * @param payload + */ + convertToString (payload: any) { + if (typeof payload === 'string' || typeof payload === 'boolean' || typeof payload === 'number') return payload + if (Array.isArray(payload)) return JSON.stringify(payload) + if (typeof payload === 'object') { + if (payload.hasOwnProperty('message')) { + return payload.message + } else { + return JSON.stringify(payload) + } + } + } + /** * Check if method can print into console * @@ -65,12 +80,12 @@ class Logger * @param tag short tag specifying area where message was spawned (eg. cart, sync, module) * @param context meaningful data related to this message */ - debug (message: string, tag: string = null, context: any = null) : () => void { + debug (message: any, tag: string = null, context: any = null) : () => void { if (!isServer && this.canPrint('debug')) { if (tag) { - return console.debug.bind(window.console, '%cVSF%c %c' + tag +'%c ' + message, bgColorStyle('grey'), 'color: inherit', bgColorStyle('gray'), 'font-weight: normal', context); + return console.debug.bind(window.console, '%cVSF%c %c' + tag +'%c ' + this.convertToString(message), bgColorStyle('grey'), 'color: inherit', bgColorStyle('gray'), 'font-weight: normal', context); } else { - return console.debug.bind(window.console, '%cVSF%c ' + message, bgColorStyle('white'), 'font-weight: normal', context); + return console.debug.bind(window.console, '%cVSF%c ' + this.convertToString(message), bgColorStyle('grey'), 'font-weight: normal', context); } } else { return function () {} @@ -85,7 +100,7 @@ class Logger * @param tag short tag specifying area where message was spawned (eg. cart, sync, module) * @param context meaningful data related to this message */ - log (message: string, tag: string = null, context: any = null) : () => void { + log (message: any, tag: string = null, context: any = null) : () => void { return this.info(message, tag, context); } @@ -97,12 +112,12 @@ class Logger * @param tag short tag specifying area where message was spawned (eg. cart, sync, module) * @param context meaningful data related to this message */ - info (message: string, tag: string = null, context: any = null) : () => void { + info (message: any, tag: string = null, context: any = null) : () => void { if (!isServer && this.canPrint('info')) { if (tag) { - return console.log.bind(window.console, '%cVSF%c %c' + tag +'%c ' + message, bgColorStyle('green'), 'color: inherit', bgColorStyle('gray'), 'font-weight: bold', context); + return console.log.bind(window.console, '%cVSF%c %c' + tag +'%c ' + this.convertToString(message), bgColorStyle('green'), 'color: inherit', bgColorStyle('gray'), 'font-weight: bold', context); } else { - return console.log.bind(window.console, '%cVSF%c ' + message, bgColorStyle('green'), 'font-weight: bold', context); + return console.log.bind(window.console, '%cVSF%c ' + this.convertToString(message), bgColorStyle('green'), 'font-weight: bold', context); } } else { return function () {} @@ -117,12 +132,12 @@ class Logger * @param tag short tag specifying area where message was spawned (eg. cart, sync, module) * @param context meaningful data related to this message */ - warn (message: string, tag: string = null, context: any = null) : () => void { + warn (message: any, tag: string = null, context: any = null) : () => void { if (!isServer && this.canPrint('warn')) { if (tag) { - return console.warn.bind(window.console, '%cVSF%c %c' + tag +'%c ' + message, bgColorStyle('orange'), 'color: inherit', bgColorStyle('gray'), 'font-weight: bold', context); + return console.warn.bind(window.console, '%cVSF%c %c' + tag +'%c ' + this.convertToString(message), bgColorStyle('orange'), 'color: inherit', bgColorStyle('gray'), 'font-weight: bold', context); } else { - return console.warn.bind(window.console, '%cVSF%c ' + message, bgColorStyle('orange'), 'font-weight: bold', context); + return console.warn.bind(window.console, '%cVSF%c ' + this.convertToString(message), bgColorStyle('orange'), 'font-weight: bold', context); } } else { return function () {} @@ -137,12 +152,12 @@ class Logger * @param tag short tag specifying area where message was spawned (eg. cart, sync, module) * @param context meaningful data related to this message */ - error (message: string, tag: string = null, context: any = null) : () => void { - if (!isServer && this.canPrint('error')) { + error (message: any, tag: string = null, context: any = null) : () => void { + if (this.canPrint('error')) { // we should display SSR errors for better monitoring + error handling if (tag) { - return console.error.bind(window.console, '%cVSF%c %c' + tag +'%c ' + message, bgColorStyle('red'), 'color: inherit', bgColorStyle('gray'), 'font-weight: bold', context); + return console.error.bind(window.console, '%cVSF%c %c' + tag +'%c ' + this.convertToString(message), bgColorStyle('red'), 'color: inherit', bgColorStyle('gray'), 'font-weight: bold', context); } else { - return console.error.bind(window.console, '%cVSF%c ' + message, bgColorStyle('red'), 'font-weight: bold', context); + return console.error.bind(window.console, '%cVSF%c ' + this.convertToString(message), bgColorStyle('red'), 'font-weight: bold', context); } } else { return function () {} diff --git a/core/modules/url/hooks/afterRegistration.ts b/core/modules/url/hooks/afterRegistration.ts index c0c57193d1..ac26ff46f8 100644 --- a/core/modules/url/hooks/afterRegistration.ts +++ b/core/modules/url/hooks/afterRegistration.ts @@ -2,4 +2,12 @@ import { Logger } from '@vue-storefront/core/lib/logger' // This function will be fired both on server and client side context after registering other parts of the module export function afterRegistration({ Vue, config, store, isServer }){ + store.dispatch('url/registerMapping', { + url: '/fake/product/url.html', + routeData: { + 'parentSku': 'WS01', + 'slug': 'gwyn-endurance-tee', + 'name': 'configurable-product' + } + }, { root: true }) } diff --git a/core/modules/url/index.ts b/core/modules/url/index.ts index 28ade6fe24..adf5af10bc 100644 --- a/core/modules/url/index.ts +++ b/core/modules/url/index.ts @@ -1,19 +1,47 @@ import { module } from './store' -import UrlDispatcher from './pages/UrlDispatcher.vue' import { beforeRegistration } from './hooks/beforeRegistration' import { afterRegistration } from './hooks/afterRegistration' import { VueStorefrontModule, VueStorefrontModuleConfig } from '@vue-storefront/core/lib/module' import { initCacheStorage } from '@vue-storefront/core/helpers/initCacheStorage' +import store from '@vue-storefront/store' +import userRoutes from 'theme/router' + export const KEY = 'url' export const cacheStorage = initCacheStorage(KEY) +let _matchedRouteData = null +const UrlDispatcher = ():any => { + if (store.state.config.seo.useUrlDispatcher && _matchedRouteData) { + const userRoute = userRoutes.find(r => r.name === _matchedRouteData['name']) + if (userRoute) { + return userRoute.component() + } else { + } + } +} const moduleConfig: VueStorefrontModuleConfig = { key: KEY, store: { modules: [{ key: KEY, module }] }, beforeRegistration, afterRegistration, router: { routes: [ - { name: 'urldispatcher', path: '*', component: UrlDispatcher } + { name: 'urldispatcher', path: '*', component: UrlDispatcher, beforeEnter: (to, from, next) => { + if (store.state.config.seo.useUrlDispatcher) { + store.dispatch('url/mapUrl', { url: to.fullPath }, { root: true }).then((routeData) => { + Object.keys(routeData).map(key => { + to.params[key] = routeData[key] + }) + _matchedRouteData = routeData + next() + }).catch(e => { + console.error(e) + next() + }) + } else { + next() + } + } + } ] } } diff --git a/core/modules/url/store/actions.ts b/core/modules/url/store/actions.ts index bb9cd7b5bf..61ec4ef8fd 100644 --- a/core/modules/url/store/actions.ts +++ b/core/modules/url/store/actions.ts @@ -17,6 +17,8 @@ export const actions: ActionTree = { }, mapUrl ({ state, dispatch }, { url }) { return new Promise ((resolve, reject) => { + console.log(url) + console.log(state.dispatcherMap) if (state.dispatcherMap.hasOwnProperty(url)) { return resolve (state.dispatcherMap[url]) } From e499ac50616b32a80d21f9cb11968c691beeb773 Mon Sep 17 00:00:00 2001 From: pkarw Date: Tue, 12 Feb 2019 08:36:50 +0100 Subject: [PATCH 03/14] Error handling on URL dispatcher --- config/default.json | 2 +- core/lib/search/adapter/api/searchAdapter.ts | 2 +- .../adapter/graphql/processor/processType.ts | 8 ++--- .../catalog/store/category/mutations.ts | 2 +- core/modules/url/index.ts | 21 ++++++++--- core/modules/url/pages/UrlDispatcher.vue | 36 ------------------- core/scripts/server.js | 2 +- 7 files changed, 24 insertions(+), 49 deletions(-) delete mode 100755 core/modules/url/pages/UrlDispatcher.vue diff --git a/config/default.json b/config/default.json index 7184499247..86569b28a9 100644 --- a/config/default.json +++ b/config/default.json @@ -248,7 +248,7 @@ "applycoupon_endpoint": "http://localhost:8080/api/cart/apply-coupon?token={{token}}&cartId={{cartId}}&coupon={{coupon}}" }, "products": { - "useShortCatalogUrls": false, + "useShortCatalogUrls": true, "useMagentoUrlKeys": true, "setFirstVarianAsDefaultInURL": false, "configurableChildrenStockPrefetchStatic": false, diff --git a/core/lib/search/adapter/api/searchAdapter.ts b/core/lib/search/adapter/api/searchAdapter.ts index 7ba0ee1a95..4e7102b049 100644 --- a/core/lib/search/adapter/api/searchAdapter.ts +++ b/core/lib/search/adapter/api/searchAdapter.ts @@ -94,7 +94,7 @@ export class SearchAdapter { if (resp.hasOwnProperty('hits')) { return { items: map(resp.hits.hits, hit => { - return Object.assign(hit._source, { _score: hit._score, slug: (hit._source.hasOwnProperty('url_key') && rootStore.state.config.products.useMagentoUrlKeys) ? hit._source.url_key : (hit._source.hasOwnProperty('name') ? slugify(hit._source.name) + '-' + hit._source.id : '') }) // TODO: assign slugs server side + return Object.assign(hit._source, { _score: hit._score, slug: hit._source.hasOwnProperty('slug') ? hit._source.slug : ((hit._source.hasOwnProperty('url_key') && rootStore.state.config.products.useMagentoUrlKeys) ? hit._source.url_key : (hit._source.hasOwnProperty('name') ? slugify(hit._source.name) + '-' + hit._source.id : '')) }) // TODO: assign slugs server side }), // TODO: add scoring information total: resp.hits.total, start: start, diff --git a/core/lib/search/adapter/graphql/processor/processType.ts b/core/lib/search/adapter/graphql/processor/processType.ts index c63c1a0545..91ac5f9004 100644 --- a/core/lib/search/adapter/graphql/processor/processType.ts +++ b/core/lib/search/adapter/graphql/processor/processType.ts @@ -8,9 +8,9 @@ export function processESResponseType (resp, start, size): SearchResponse { items: map(resp.hits.hits, hit => { return Object.assign(hit._source, { _score: hit._score, - slug: (hit._source.hasOwnProperty('url_key') && rootStore.state.config.products.useMagentoUrlKeys) + slug: hit._source.hasOwnProperty('slug') ? hit._source.slug : ((hit._source.hasOwnProperty('url_key') && rootStore.state.config.products.useMagentoUrlKeys) ? hit._source.url_key - : (hit._source.hasOwnProperty('name') ? slugify(hit._source.name) + '-' + hit._source.id : '') + : (hit._source.hasOwnProperty('name') ? slugify(hit._source.name) + '-' + hit._source.id : '')) }) // TODO: assign slugs server side }), // TODO: add scoring information total: resp.hits.total, @@ -30,10 +30,10 @@ export function processProductsType (resp, start, size): SearchResponse { options['_score'] = item._score delete item._score } - options['slug'] = (item.hasOwnProperty('url_key') && + options['slug'] = item.hasOwnProperty('slug') ? item.slug : ((item.hasOwnProperty('url_key') && rootStore.state.config.products.useMagentoUrlKeys) ? item.url_key : (item.hasOwnProperty('name') - ? slugify(item.name) + '-' + item.id : '') + ? slugify(item.name) + '-' + item.id : '')) return Object.assign(item, options) // TODO: assign slugs server side }), // TODO: add scoring information diff --git a/core/modules/catalog/store/category/mutations.ts b/core/modules/catalog/store/category/mutations.ts index 4dc7cad990..f497e37cce 100644 --- a/core/modules/catalog/store/category/mutations.ts +++ b/core/modules/catalog/store/category/mutations.ts @@ -22,7 +22,7 @@ const mutations: MutationTree = { if (category.children_data) { for (let subcat of category.children_data) { // TODO: fixme and move slug setting to vue-storefront-api if (subcat.name) { - subcat = Object.assign(subcat, { slug: (subcat.hasOwnProperty('url_key') && rootStore.state.config.products.useMagentoUrlKeys) ? subcat.url_key : (subcat.hasOwnProperty('name') ? slugify(subcat.name) + '-' + subcat.id : '') }) + subcat = Object.assign(subcat, { slug: subcat.hasOwnProperty('slug') ? subcat.slug : ((subcat.hasOwnProperty('url_key') && rootStore.state.config.products.useMagentoUrlKeys) ? subcat.url_key : (subcat.hasOwnProperty('name') ? slugify(subcat.name) + '-' + subcat.id : '')) }) catSlugSetter(subcat) } } diff --git a/core/modules/url/index.ts b/core/modules/url/index.ts index adf5af10bc..675742f052 100644 --- a/core/modules/url/index.ts +++ b/core/modules/url/index.ts @@ -5,6 +5,7 @@ import { VueStorefrontModule, VueStorefrontModuleConfig } from '@vue-storefront/ import { initCacheStorage } from '@vue-storefront/core/helpers/initCacheStorage' import store from '@vue-storefront/store' import userRoutes from 'theme/router' +import { HttpError } from '@vue-storefront/core/helpers/exceptions' export const KEY = 'url' export const cacheStorage = initCacheStorage(KEY) @@ -14,9 +15,16 @@ const UrlDispatcher = ():any => { if (store.state.config.seo.useUrlDispatcher && _matchedRouteData) { const userRoute = userRoutes.find(r => r.name === _matchedRouteData['name']) if (userRoute) { - return userRoute.component() + if (typeof userRoute.component === 'function') { + return userRoute.component() // supports only lazy loaded components; in case of eagerly loaded components it should be like: `return userRoute.component` + } else { + return userRoute.component + } } else { + throw new HttpError('UrlDispatcher query returned empty result', 404) } + } else { + throw new HttpError('UrlDispatcher query returned empty result', 404) } } const moduleConfig: VueStorefrontModuleConfig = { @@ -27,11 +35,14 @@ const moduleConfig: VueStorefrontModuleConfig = { router: { routes: [ { name: 'urldispatcher', path: '*', component: UrlDispatcher, beforeEnter: (to, from, next) => { if (store.state.config.seo.useUrlDispatcher) { + console.log(to) store.dispatch('url/mapUrl', { url: to.fullPath }, { root: true }).then((routeData) => { - Object.keys(routeData).map(key => { - to.params[key] = routeData[key] - }) - _matchedRouteData = routeData + if (routeData) { + Object.keys(routeData).map(key => { + to.params[key] = routeData[key] + }) + _matchedRouteData = routeData + } next() }).catch(e => { console.error(e) diff --git a/core/modules/url/pages/UrlDispatcher.vue b/core/modules/url/pages/UrlDispatcher.vue deleted file mode 100755 index 428adb0a39..0000000000 --- a/core/modules/url/pages/UrlDispatcher.vue +++ /dev/null @@ -1,36 +0,0 @@ - - - diff --git a/core/scripts/server.js b/core/scripts/server.js index 60b5ef0d64..3834600e99 100755 --- a/core/scripts/server.js +++ b/core/scripts/server.js @@ -124,7 +124,7 @@ app.get('/invalidate', invalidateCache) app.get('*', (req, res, next) => { const s = Date.now() const errorHandler = err => { - if (err && err.code === 404) { + if (err && err.code === 404 || err.message.indexOf('Failed to resolve') >= 0/* Vue Router error - Vue Router encapsulates the HttpError returned by UrlDispatcher; haven't found a better way to handle it */) { res.redirect('/page-not-found') } else { res.redirect('/error') From a365f4bfa51d270b84f52ea3bd5b503d22e1b5af Mon Sep 17 00:00:00 2001 From: pkarw Date: Tue, 12 Feb 2019 15:07:43 +0100 Subject: [PATCH 04/14] Product route links + error handling improvements --- config/default.json | 6 +++--- core/app.ts | 2 +- core/helpers/index.ts | 2 +- core/lib/sync/index.ts | 3 ++- core/modules/breadcrumbs/helpers/index.ts | 2 +- core/modules/cart/store/actions.ts | 9 +++++---- core/modules/catalog/components/ProductTile.ts | 10 ++++++++++ core/modules/catalog/helpers/index.ts | 3 ++- core/modules/catalog/store/category/actions.ts | 7 ++++--- core/modules/catalog/store/product/actions.ts | 8 +++++--- core/modules/url/index.ts | 17 ++++++++++++++--- core/modules/url/store/actions.ts | 2 -- core/pages/Category.js | 12 ++++++------ core/store/lib/storage.ts | 9 +++++---- docs/guide/integrations/payment-gateway.md | 2 +- .../module-template/hooks/beforeRegistration.ts | 2 +- .../hooks/afterRegistration.ts | 2 +- .../hooks/afterRegistration.ts | 2 +- .../default-amp/components/core/ProductTile.vue | 9 +-------- .../default/components/core/ProductTile.vue | 9 +-------- .../core/blocks/Checkout/ThankYouPage.vue | 8 ++++---- 21 files changed, 69 insertions(+), 57 deletions(-) diff --git a/config/default.json b/config/default.json index 86569b28a9..239ad71676 100644 --- a/config/default.json +++ b/config/default.json @@ -144,7 +144,7 @@ "twoStageCaching": true, "optimizeShoppingCart": true, "category": { - "includeFields": [ "id", "*.children_data.id", "*.id", "children_count", "sku", "name", "is_active", "parent_id", "level", "url_key", "product_count", "path"], + "includeFields": [ "id", "*.children_data.id", "*.id", "children_count", "sku", "name", "is_active", "parent_id", "level", "url_key", "url_path", "product_count", "path"], "excludeFields": [ "sgn" ], "categoriesRootCategorylId": 2, "categoriesDynamicPrefetchLevel": 2, @@ -155,11 +155,11 @@ }, "productList": { "sort": "", - "includeFields": [ "type_id", "sku", "product_links", "tax_class_id", "special_price", "special_to_date", "special_from_date", "name", "price", "priceInclTax", "originalPriceInclTax", "originalPrice", "specialPriceInclTax", "id", "image", "sale", "new", "url_key", "status", "tier_prices", "configurable_children.sku", "configurable_children.price", "configurable_children.special_price", "configurable_children.priceInclTax", "configurable_children.specialPriceInclTax", "configurable_children.originalPrice", "configurable_children.originalPriceInclTax" ], + "includeFields": [ "type_id", "sku", "product_links", "tax_class_id", "special_price", "special_to_date", "special_from_date", "name", "price", "priceInclTax", "originalPriceInclTax", "originalPrice", "specialPriceInclTax", "id", "image", "sale", "new", "url_path", "url_key", "status", "tier_prices", "configurable_children.sku", "configurable_children.price", "configurable_children.special_price", "configurable_children.priceInclTax", "configurable_children.specialPriceInclTax", "configurable_children.originalPrice", "configurable_children.originalPriceInclTax" ], "excludeFields": [ "description", "configurable_options", "sgn", "*.sgn", "msrp_display_actual_price_type", "*.msrp_display_actual_price_type", "required_options" ] }, "productListWithChildren": { - "includeFields": [ "type_id", "sku", "name", "tax_class_id", "special_price", "special_to_date", "special_from_date", "price", "priceInclTax", "originalPriceInclTax", "originalPrice", "specialPriceInclTax", "id", "image", "sale", "new", "configurable_children.image", "configurable_children.sku", "configurable_children.price", "configurable_children.special_price", "configurable_children.priceInclTax", "configurable_children.specialPriceInclTax", "configurable_children.originalPrice", "configurable_children.originalPriceInclTax", "configurable_children.color", "configurable_children.size", "configurable_children.id", "configurable_children.tier_prices", "product_links", "url_key", "status", "tier_prices"], + "includeFields": [ "type_id", "sku", "name", "tax_class_id", "special_price", "special_to_date", "special_from_date", "price", "priceInclTax", "originalPriceInclTax", "originalPrice", "specialPriceInclTax", "id", "image", "sale", "new", "configurable_children.image", "configurable_children.sku", "configurable_children.price", "configurable_children.special_price", "configurable_children.priceInclTax", "configurable_children.specialPriceInclTax", "configurable_children.originalPrice", "configurable_children.originalPriceInclTax", "configurable_children.color", "configurable_children.size", "configurable_children.id", "configurable_children.tier_prices", "product_links", "url_path", "url_key", "status", "tier_prices"], "excludeFields": [ "description", "sgn", "*.sgn", "msrp_display_actual_price_type", "*.msrp_display_actual_price_type", "required_options"] }, "review": { diff --git a/core/app.ts b/core/app.ts index a393fda71e..47f6e6ed85 100755 --- a/core/app.ts +++ b/core/app.ts @@ -65,7 +65,7 @@ const createApp = async (ssrContext, config): Promise<{app: Vue, router: VueRou // sync router with vuex 'router' store sync(store, router) // TODO: Don't mutate the state directly, use mutation instead - store.state.version = '1.7.0' + store.state.version = '1.8.2' store.state.config = config store.state.__DEMO_MODE__ = (config.demomode === true) ? true : false if(ssrContext) Vue.prototype.$ssrRequestContext = ssrContext diff --git a/core/helpers/index.ts b/core/helpers/index.ts index d18f70a954..98d766f0fa 100644 --- a/core/helpers/index.ts +++ b/core/helpers/index.ts @@ -54,7 +54,7 @@ export function breadCrumbRoutes (categoryPath) { for (let sc of categoryPath) { tmpRts.push({ name: sc.name, - route_link: (rootStore.state.config.products.useShortCatalogUrls ? '/' : '/c/') + sc.slug + route_link: rootStore.state.config.seo.useUrlDispatcher ? ('/' + sc.url_path) : ((rootStore.state.config.products.useShortCatalogUrls ? '/' : '/c/') + sc.slug) }) } diff --git a/core/lib/sync/index.ts b/core/lib/sync/index.ts index c72dca54d4..5b365efa4e 100644 --- a/core/lib/sync/index.ts +++ b/core/lib/sync/index.ts @@ -6,6 +6,7 @@ import { execute as taskExecute, _prepareTask } from './task' import * as localForage from 'localforage' import UniversalStorage from '@vue-storefront/store/lib/storage' import { currentStoreView } from '../multistore' +import { isServer } from '@vue-storefront/core/helpers' /** Syncs given task. If user is offline requiest will be sent to the server after restored connection */ function queue (task) { @@ -41,7 +42,7 @@ function execute (task) { // not offline task driver: localForage[rootStore.state.config.localForage.defaultDrivers['carts']] })) return new Promise((resolve, reject) => { - if (Vue.prototype.$isServer) { + if (isServer) { taskExecute(task, null, null).then((result) => { resolve(result) }).catch(err => { diff --git a/core/modules/breadcrumbs/helpers/index.ts b/core/modules/breadcrumbs/helpers/index.ts index 294e2fc19f..05d4f8f22a 100644 --- a/core/modules/breadcrumbs/helpers/index.ts +++ b/core/modules/breadcrumbs/helpers/index.ts @@ -7,7 +7,7 @@ export function parseCategoryPath (categoryPath) { for (let sc of categoryPath) { tmpRts.push({ name: sc.name, - route_link: (rootStore.state.config.products.useShortCatalogUrls ? '/' : '/c/') + sc.slug + route_link: rootStore.state.config.seo.useUrlDispatcher ? sc.url_path : ((rootStore.state.config.products.useShortCatalogUrls ? '/' : '/c/') + sc.slug) }) } diff --git a/core/modules/cart/store/actions.ts b/core/modules/cart/store/actions.ts index 7eec208418..a4b76ac436 100644 --- a/core/modules/cart/store/actions.ts +++ b/core/modules/cart/store/actions.ts @@ -14,6 +14,7 @@ import { Logger } from '@vue-storefront/core/lib/logger' import { TaskQueue } from '@vue-storefront/core/lib/sync' import { router } from '@vue-storefront/core/app' import SearchQuery from '@vue-storefront/core/lib/search/searchQuery' +import { isServer } from '@vue-storefront/core/helpers' const CART_PULL_INTERVAL_MS = 2000 const CART_CREATE_INTERVAL_MS = 1000 @@ -56,7 +57,7 @@ const actions: ActionTree = { context.commit(types.CART_SAVE) }, serverPull (context, { forceClientState = false, dryRun = false }) { // pull current cart FROM the server - if (rootStore.state.config.cart.synchronize && !Vue.prototype.$isServer) { + if (rootStore.state.config.cart.synchronize && !isServer) { const newItemsHash = sha3_224(JSON.stringify({ items: context.state.cartItems, token: context.state.cartServerToken })) if ((Date.now() - context.state.cartServerPullAt) >= CART_PULL_INTERVAL_MS || (newItemsHash !== context.state.cartItemsHash)) { context.state.cartServerPullAt = Date.now() @@ -91,7 +92,7 @@ const actions: ActionTree = { } }, serverTotals (context, { forceClientState = false }) { // pull current cart FROM the server - if (rootStore.state.config.cart.synchronize_totals && !Vue.prototype.$isServer) { + if (rootStore.state.config.cart.synchronize_totals && !isServer) { if ((Date.now() - context.state.cartServerTotalsAt) >= CART_TOTALS_INTERVAL_MS) { context.state.cartServerPullAt = Date.now() TaskQueue.execute({ url: rootStore.state.config.cart.totals_endpoint, // sync the cart @@ -110,7 +111,7 @@ const actions: ActionTree = { } }, serverCreate (context, { guestCart = false }) { - if (rootStore.state.config.cart.synchronize && !Vue.prototype.$isServer) { + if (rootStore.state.config.cart.synchronize && !isServer) { if ((Date.now() - context.state.cartServerCreatedAt) >= CART_CREATE_INTERVAL_MS) { const task = { url: guestCart ? rootStore.state.config.cart.create_endpoint.replace('{{token}}', '') : rootStore.state.config.cart.create_endpoint, // sync the cart payload: { @@ -174,7 +175,7 @@ const actions: ActionTree = { }, load (context) { return new Promise((resolve, reject) => { - if (Vue.prototype.$isServer) return + if (isServer) return const commit = context.commit const state = context.state diff --git a/core/modules/catalog/components/ProductTile.ts b/core/modules/catalog/components/ProductTile.ts index 06dd48b462..8fb0c52397 100644 --- a/core/modules/catalog/components/ProductTile.ts +++ b/core/modules/catalog/components/ProductTile.ts @@ -15,6 +15,16 @@ export const ProductTile = { } }, computed: { + productLink () { + return this.localizedRoute(this.$store.state.config.seo.useUrlDispatcher ? this.product.url_path : { + name: this.product.type_id + '-product', + params: { + parentSku: this.product.parentSku ? this.product.parentSku : this.product.sku, + slug: this.product.slug, + childSku: this.product.sku + } + }) + }, thumbnail () { // todo: play with the image based on category page filters - eg. when 'red' color is chosen, image is going to be 'red' let thumbnail = productThumbnailPath(this.product) diff --git a/core/modules/catalog/helpers/index.ts b/core/modules/catalog/helpers/index.ts index 2d1071f98c..718f2f3af3 100644 --- a/core/modules/catalog/helpers/index.ts +++ b/core/modules/catalog/helpers/index.ts @@ -13,6 +13,7 @@ import i18n from '@vue-storefront/i18n' import { currentStoreView } from '@vue-storefront/core/lib/multistore' import { getThumbnailPath } from '@vue-storefront/core/helpers' import { Logger } from '@vue-storefront/core/lib/logger' +import { isServer } from '@vue-storefront/core/helpers' function _filterRootProductByStockitem (context, stockItem, product, errorCallback) { if (stockItem) { @@ -235,7 +236,7 @@ export function doPlatformPricesSync (products) { } else { // empty list of products resolve(products) } - if (!rootStore.state.config.products.waitForPlatformSync && !Vue.prototype.$isServer) { + if (!rootStore.state.config.products.waitForPlatformSync && !isServer) { Logger.log('Returning products, the prices yet to come from backend!')() for (let product of products) { product.price_is_current = false // in case we're syncing up the prices we should mark if we do have current or not diff --git a/core/modules/catalog/store/category/actions.ts b/core/modules/catalog/store/category/actions.ts index b73ef7323e..307ac26709 100644 --- a/core/modules/catalog/store/category/actions.ts +++ b/core/modules/catalog/store/category/actions.ts @@ -14,6 +14,7 @@ import CategoryState from '../../types/CategoryState' import SearchQuery from '@vue-storefront/core/lib/search/searchQuery' import { currentStoreView } from '@vue-storefront/core/lib/multistore' import { Logger } from '@vue-storefront/core/lib/logger' +import { isServer } from '@vue-storefront/core/helpers' const actions: ActionTree = { @@ -170,7 +171,7 @@ const actions: ActionTree = { } } if (!foundInLocalCache) { - if (skipCache || Vue.prototype.$isServer) { + if (skipCache || isServer) { fetchCat({ key, value }) } else { const catCollection = Vue.prototype.$db.categoriesCollection @@ -197,7 +198,7 @@ const actions: ActionTree = { }) let prefetchGroupProducts = true - if (rootStore.state.config.entities.twoStageCaching && rootStore.state.config.entities.optimize && !Vue.prototype.$isServer && !rootStore.state.twoStageCachingDisabled) { // only client side, only when two stage caching enabled + if (rootStore.state.config.entities.twoStageCaching && rootStore.state.config.entities.optimize && !isServer && !rootStore.state.twoStageCachingDisabled) { // only client side, only when two stage caching enabled includeFields = rootStore.state.config.entities.productListWithChildren.includeFields // we need configurable_children for filters to work excludeFields = rootStore.state.config.entities.productListWithChildren.excludeFields prefetchGroupProducts = false @@ -322,7 +323,7 @@ const actions: ActionTree = { }) }) - if (rootStore.state.config.entities.twoStageCaching && rootStore.state.config.entities.optimize && !Vue.prototype.$isServer && !rootStore.state.twoStageCachingDisabled) { // second stage - request for caching entities + if (rootStore.state.config.entities.twoStageCaching && rootStore.state.config.entities.optimize && !isServer && !rootStore.state.twoStageCachingDisabled) { // second stage - request for caching entities Logger.log('Using two stage caching for performance optimization - executing second stage product caching', 'category') // TODO: in this case we can pre-fetch products in advance getting more products than set by pageSize() rootStore.dispatch('product/list', { query: precachedQuery, diff --git a/core/modules/catalog/store/product/actions.ts b/core/modules/catalog/store/product/actions.ts index bea890d986..820f2d38ab 100644 --- a/core/modules/catalog/store/product/actions.ts +++ b/core/modules/catalog/store/product/actions.ts @@ -25,6 +25,8 @@ import RootState from '@vue-storefront/core/types/RootState' import ProductState from '../../types/ProductState' import { Logger } from '@vue-storefront/core/lib/logger'; import { TaskQueue } from '@vue-storefront/core/lib/sync' +import { isServer } from '@vue-storefront/core/helpers' + const PRODUCT_REENTER_TIMEOUT = 20000 const actions: ActionTree = { @@ -282,7 +284,7 @@ const actions: ActionTree = { * @param {Int} size page size * @return {Promise} */ - list (context, { query, start = 0, size = 50, entityType = 'product', sort = '', cacheByKey = 'sku', prefetchGroupProducts = !Vue.prototype.$isServer, updateState = false, meta = {}, excludeFields = null, includeFields = null, configuration = null, append = false, populateRequestCacheTags = true }) { + list (context, { query, start = 0, size = 50, entityType = 'product', sort = '', cacheByKey = 'sku', prefetchGroupProducts = !isServer, updateState = false, meta = {}, excludeFields = null, includeFields = null, configuration = null, append = false, populateRequestCacheTags = true }) { let isCacheable = (includeFields === null && excludeFields === null) if (isCacheable) { Logger.debug('Entity cache is enabled for productList')() @@ -341,7 +343,7 @@ const actions: ActionTree = { Logger.error('Cannot store cache for ' + cacheKey, err)() }) } - if ((prod.type_id === 'grouped' || prod.type_id === 'bundle') && prefetchGroupProducts && !Vue.prototype.$isServer) { + if ((prod.type_id === 'grouped' || prod.type_id === 'bundle') && prefetchGroupProducts && !isServer) { context.dispatch('setupAssociated', { product: prod }) } } @@ -623,7 +625,7 @@ const actions: ActionTree = { only_user_defined: true, includeFields: rootStore.state.config.entities.optimize ? rootStore.state.config.entities.attribute.includeFields : null }, { root: true })) - if (Vue.prototype.$isServer) { + if (isServer) { subloaders.push(context.dispatch('filterUnavailableVariants', { product: product })) } else { context.dispatch('filterUnavailableVariants', { product: product }) // exec async diff --git a/core/modules/url/index.ts b/core/modules/url/index.ts index 675742f052..a70e9126d1 100644 --- a/core/modules/url/index.ts +++ b/core/modules/url/index.ts @@ -6,11 +6,23 @@ import { initCacheStorage } from '@vue-storefront/core/helpers/initCacheStorage' import store from '@vue-storefront/store' import userRoutes from 'theme/router' import { HttpError } from '@vue-storefront/core/helpers/exceptions' +import { router } from '@vue-storefront/core/app' +import { isServer } from '@vue-storefront/core/helpers' +import { Logger } from '@vue-storefront/core/lib/logger'; export const KEY = 'url' export const cacheStorage = initCacheStorage(KEY) let _matchedRouteData = null +const _handleDispatcherNotFound = (routeName: string):void => { + Logger.error('Route not found ' + routeName, 'dispatcher')() + if (isServer) { + console.log('Klejnot') + throw new HttpError('UrlDispatcher query returned empty result', 404) + } else { + router.push('/page-not-found') + } +} const UrlDispatcher = ():any => { if (store.state.config.seo.useUrlDispatcher && _matchedRouteData) { const userRoute = userRoutes.find(r => r.name === _matchedRouteData['name']) @@ -21,10 +33,10 @@ const UrlDispatcher = ():any => { return userRoute.component } } else { - throw new HttpError('UrlDispatcher query returned empty result', 404) + _handleDispatcherNotFound(_matchedRouteData['name']) } } else { - throw new HttpError('UrlDispatcher query returned empty result', 404) + _handleDispatcherNotFound(null) } } const moduleConfig: VueStorefrontModuleConfig = { @@ -35,7 +47,6 @@ const moduleConfig: VueStorefrontModuleConfig = { router: { routes: [ { name: 'urldispatcher', path: '*', component: UrlDispatcher, beforeEnter: (to, from, next) => { if (store.state.config.seo.useUrlDispatcher) { - console.log(to) store.dispatch('url/mapUrl', { url: to.fullPath }, { root: true }).then((routeData) => { if (routeData) { Object.keys(routeData).map(key => { diff --git a/core/modules/url/store/actions.ts b/core/modules/url/store/actions.ts index 61ec4ef8fd..bb9cd7b5bf 100644 --- a/core/modules/url/store/actions.ts +++ b/core/modules/url/store/actions.ts @@ -17,8 +17,6 @@ export const actions: ActionTree = { }, mapUrl ({ state, dispatch }, { url }) { return new Promise ((resolve, reject) => { - console.log(url) - console.log(state.dispatcherMap) if (state.dispatcherMap.hasOwnProperty(url)) { return resolve (state.dispatcherMap[url]) } diff --git a/core/pages/Category.js b/core/pages/Category.js index aba76a0315..c5365dc0bd 100644 --- a/core/pages/Category.js +++ b/core/pages/Category.js @@ -4,7 +4,7 @@ import toString from 'lodash-es/toString' import i18n from '@vue-storefront/i18n' import store from '@vue-storefront/store' import EventBus from '@vue-storefront/core/compatibility/plugins/event-bus' -import { baseFilterProductsQuery, buildFilterProductsQuery } from '@vue-storefront/core/helpers' +import { baseFilterProductsQuery, buildFilterProductsQuery, isServer } from '@vue-storefront/core/helpers' import { htmlDecode } from '@vue-storefront/core/filters/html-decode' import { currentStoreView, localizedRoute } from '@vue-storefront/core/lib/multistore' import Composite from '@vue-storefront/core/mixins/composite' @@ -76,8 +76,8 @@ export default { perPage: 50, sort: store.state.config.entities.productList.sort, filters: store.state.config.products.defaultFilters, - includeFields: store.state.config.entities.optimize && Vue.prototype.$isServer ? store.state.config.entities.productList.includeFields : null, - excludeFields: store.state.config.entities.optimize && Vue.prototype.$isServer ? store.state.config.entities.productList.excludeFields : null, + includeFields: store.state.config.entities.optimize && isServer ? store.state.config.entities.productList.includeFields : null, + excludeFields: store.state.config.entities.optimize && isServer ? store.state.config.entities.productList.excludeFields : null, append: false }) }, @@ -86,10 +86,10 @@ export default { Logger.info('Entering asyncData in Category Page (core)')() if (context) context.output.cacheTags.add(`category`) const defaultFilters = store.state.config.products.defaultFilters - store.dispatch('category/list', { level: store.state.config.entities.category.categoriesDynamicPrefetch && store.state.config.entities.category.categoriesDynamicPrefetchLevel ? store.state.config.entities.category.categoriesDynamicPrefetchLevel : null, includeFields: store.state.config.entities.optimize && Vue.prototype.$isServer ? store.state.config.entities.category.includeFields : null }).then((categories) => { + store.dispatch('category/list', { level: store.state.config.entities.category.categoriesDynamicPrefetch && store.state.config.entities.category.categoriesDynamicPrefetchLevel ? store.state.config.entities.category.categoriesDynamicPrefetchLevel : null, includeFields: store.state.config.entities.optimize && isServer ? store.state.config.entities.category.includeFields : null }).then((categories) => { store.dispatch('attribute/list', { // load filter attributes for this specific category filterValues: defaultFilters, // TODO: assign specific filters/ attribute codes dynamicaly to specific categories - includeFields: store.state.config.entities.optimize && Vue.prototype.$isServer ? store.state.config.entities.attribute.includeFields : null + includeFields: store.state.config.entities.optimize && isServer ? store.state.config.entities.attribute.includeFields : null }).catch(err => { Logger.error(err)() reject(err) @@ -141,7 +141,7 @@ export default { this.$bus.$on('user-after-loggedin', this.onUserPricesRefreshed) this.$bus.$on('user-after-logout', this.onUserPricesRefreshed) } - if (!Vue.prototype.$isServer && this.lazyLoadProductsOnscroll) { + if (!isServer && this.lazyLoadProductsOnscroll) { window.addEventListener('scroll', () => { this.bottom = this.bottomVisible() }, {passive: true}) diff --git a/core/store/lib/storage.ts b/core/store/lib/storage.ts index 451a620662..f9bd2cb93e 100644 --- a/core/store/lib/storage.ts +++ b/core/store/lib/storage.ts @@ -1,6 +1,7 @@ import Vue from 'vue' import * as localForage from 'localforage' import { Logger } from '@vue-storefront/core/lib/logger' +import { isServer } from '@vue-storefront/core/helpers' const CACHE_TIMEOUT = 800 const CACHE_TIMEOUT_ITERATE = 2000 @@ -51,7 +52,7 @@ class LocalForageCacheDriver { const dbName = collection._config.name this._storageQuota = storageQuota - if (this._storageQuota && !Vue.prototype.$isServer) { + if (this._storageQuota && !isServer) { const storageQuota = this._storageQuota const iterateFnc = this.iterate.bind(this) const removeItemFnc = this.removeItem.bind(this) @@ -84,7 +85,7 @@ class LocalForageCacheDriver { if (typeof this.cacheErrorsCount[collectionName] === 'undefined') { this.cacheErrorsCount[collectionName] = 0 } - if (Vue.prototype.$isServer) { + if (isServer) { this._localCache = {} } else { if (typeof Vue.prototype.$localCache === 'undefined') { @@ -144,7 +145,7 @@ class LocalForageCacheDriver { }) } - if (!Vue.prototype.$isServer) { + if (!isServer) { if (this.cacheErrorsCount[this._collectionName] >= DISABLE_PERSISTANCE_AFTER && this._useLocalCacheByDefault) { if (!this._persistenceErrorNotified) { Logger.error('Persistent cache disabled becasue of previous errors [get]', key)() @@ -294,7 +295,7 @@ class LocalForageCacheDriver { resolve(null) }) } - if (!Vue.prototype.$isServer) { + if (!isServer) { if (this.cacheErrorsCount[this._collectionName] >= DISABLE_PERSISTANCE_AFTER_SAVE && this._useLocalCacheByDefault) { if (!this._persistenceErrorNotified) { Logger.error('Persistent cache disabled becasue of previous errors [set]', key)() diff --git a/docs/guide/integrations/payment-gateway.md b/docs/guide/integrations/payment-gateway.md index d1c15c2a1b..c333a0842a 100644 --- a/docs/guide/integrations/payment-gateway.md +++ b/docs/guide/integrations/payment-gateway.md @@ -40,7 +40,7 @@ export function afterRegistration({ Vue, config, store, isServer }) { Vue.prototype.$bus.$emit('checkout-do-placeOrder', {}) } - if (!Vue.prototype.$isServer) { + if (!isServer) { // Update the methods let paymentMethodConfig = { 'title': 'Cash on delivery', diff --git a/src/modules/module-template/hooks/beforeRegistration.ts b/src/modules/module-template/hooks/beforeRegistration.ts index 5ad8deadef..6395dcd9cf 100644 --- a/src/modules/module-template/hooks/beforeRegistration.ts +++ b/src/modules/module-template/hooks/beforeRegistration.ts @@ -2,7 +2,7 @@ import { AsyncDataLoader } from '@vue-storefront/core/lib/async-data-loader' // This function will be fired both on server and client side context before registering other parts of the module export function beforeRegistration({ Vue, config, store, isServer }) { - if (!Vue.prototype.$isServer) console.info('This will be called before extension registration and only on client side') + if (!isServer) console.info('This will be called before extension registration and only on client side') AsyncDataLoader.push({ // this is an example showing how to call data loader from another module execute: ({ route, store, context }) => { return new Promise ((resolve, reject) => { diff --git a/src/modules/payment-backend-methods/hooks/afterRegistration.ts b/src/modules/payment-backend-methods/hooks/afterRegistration.ts index e507e93d61..5a53357d77 100644 --- a/src/modules/payment-backend-methods/hooks/afterRegistration.ts +++ b/src/modules/payment-backend-methods/hooks/afterRegistration.ts @@ -11,7 +11,7 @@ export function afterRegistration({ Vue, config, store, isServer }) { } } - if (!Vue.prototype.$isServer) { + if (!isServer) { // Update the methods Vue.prototype.$bus.$on('set-unique-payment-methods', methods => { store.commit('payment-backend-methods/' + types.SET_BACKEND_PAYMENT_METHODS, methods) diff --git a/src/modules/payment-cash-on-delivery/hooks/afterRegistration.ts b/src/modules/payment-cash-on-delivery/hooks/afterRegistration.ts index d5f42bcf09..65ac65ab85 100644 --- a/src/modules/payment-cash-on-delivery/hooks/afterRegistration.ts +++ b/src/modules/payment-cash-on-delivery/hooks/afterRegistration.ts @@ -7,7 +7,7 @@ export function afterRegistration({ Vue, config, store, isServer }) { Vue.prototype.$bus.$emit('checkout-do-placeOrder', {}) } - if (!Vue.prototype.$isServer) { + if (!isServer) { // Update the methods let paymentMethodConfig = { 'title': 'Cash on delivery', diff --git a/src/themes/default-amp/components/core/ProductTile.vue b/src/themes/default-amp/components/core/ProductTile.vue index f81668b623..72b37c9e0b 100755 --- a/src/themes/default-amp/components/core/ProductTile.vue +++ b/src/themes/default-amp/components/core/ProductTile.vue @@ -5,14 +5,7 @@ >
diff --git a/core/pages/Category.js b/core/pages/Category.js index 5a99c8ce9c..9c4d0b997f 100644 --- a/core/pages/Category.js +++ b/core/pages/Category.js @@ -10,7 +10,6 @@ import { currentStoreView, localizedRoute } from '@vue-storefront/core/lib/multi import Composite from '@vue-storefront/core/mixins/composite' import { Logger } from '@vue-storefront/core/lib/logger' import { mapGetters, mapActions } from 'vuex' -import { UrlDispatchMapper } from '@vue-storefront/core/modules/url' export default { name: 'Category', @@ -156,16 +155,7 @@ export default { } }, beforeRouteUpdate (to, from, next) { - if (this.$store.state.config.seo.useUrlDispatcher) { - return UrlDispatchMapper(to).then(routeData => { - if (routeData.name.indexOf('category') >= 0) { - this.validateRoute(routeData) - next() - } else { - document.location = to.fullPath - } - }) - } else { + if (!this.$store.state.config.seo.useUrlDispatcher) { this.validateRoute(to) next() } diff --git a/core/pages/Product.js b/core/pages/Product.js index f2e07bebd0..9f1795488a 100644 --- a/core/pages/Product.js +++ b/core/pages/Product.js @@ -10,7 +10,6 @@ import { isOptionAvailableAsync } from '@vue-storefront/core/modules/catalog/hel import omit from 'lodash-es/omit' import Composite from '@vue-storefront/core/mixins/composite' import { Logger } from '@vue-storefront/core/lib/logger' -import { UrlDispatchMapper } from '@vue-storefront/core/modules/url' export default { name: 'Product', @@ -64,16 +63,7 @@ export default { return store.dispatch('product/fetchAsync', { parentSku: route.params.parentSku, childSku: route && route.params && route.params.childSku ? route.params.childSku : null }) }, beforeRouteUpdate (to, from, next) { - if (this.$store.state.config.seo.useUrlDispatcher) { - return UrlDispatchMapper(to).then(routeData => { - if (routeData.name.indexOf('product') >= 0) { - this.validateRoute(routeData) - next() - } else { - document.location = to.fullPath - } - }) - } else { + if (!this.$store.state.config.seo.useUrlDispatcher) { this.validateRoute(to) next() } diff --git a/src/themes/default/App.vue b/src/themes/default/App.vue index 3edb78f685..62ad5b9c85 100755 --- a/src/themes/default/App.vue +++ b/src/themes/default/App.vue @@ -1,7 +1,7 @@ From 0d28943d6bfe224b19ee770d20240cfd99a6b4b1 Mon Sep 17 00:00:00 2001 From: pkarw Date: Fri, 15 Feb 2019 20:45:17 +0100 Subject: [PATCH 10/14] preAsyncData action support added to UrlClientDispatcher + breadcrumbs fixed --- core/client-entry.ts | 9 +++-- core/modules/catalog/store/product/actions.ts | 1 + .../modules/url/pages/UrlClientDispatcher.vue | 39 ++++++++++++------- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/core/client-entry.ts b/core/client-entry.ts index f54135ffbe..4c45414515 100755 --- a/core/client-entry.ts +++ b/core/client-entry.ts @@ -70,11 +70,12 @@ const invokeClientEntry = async () => { } let _appMounted = false router.onReady(() => { - router.beforeResolve((to, from, next) => { - if (config.seo.useUrlDispatcher && !_appMounted) { // async components that are used by UrlDispatcher can't be hydrated in router.onReady() as in that case we havent' yet had the final, post-routed component loaded + router.afterEach((to, from) => { + if (config.seo.useUrlDispatcher && !_appMounted) { // hydrate after each other request app.$mount('#app') - _appMounted = true - } + } + }) + router.beforeResolve((to, from, next) => { // this is NOT CALLED after SSR request as no component is being resolved client side if (!from.name) return next() // do not resolve asyncData on server render - already been done if (Vue.prototype.$ssrRequestContext) Vue.prototype.$ssrRequestContext.output.cacheTags = new Set() const matched = router.getMatchedComponents(to) diff --git a/core/modules/catalog/store/product/actions.ts b/core/modules/catalog/store/product/actions.ts index 820f2d38ab..a72dd11ad6 100644 --- a/core/modules/catalog/store/product/actions.ts +++ b/core/modules/catalog/store/product/actions.ts @@ -49,6 +49,7 @@ const actions: ActionTree = { return itm.slug === context.rootGetters['category/getCurrentCategory'].slug }) < 0) { path.push({ + url_path: context.rootGetters['category/getCurrentCategory'].url_path, slug: context.rootGetters['category/getCurrentCategory'].slug, name: context.rootGetters['category/getCurrentCategory'].name }) // current category at the end diff --git a/core/modules/url/pages/UrlClientDispatcher.vue b/core/modules/url/pages/UrlClientDispatcher.vue index ff90c9f191..3a18e4b717 100644 --- a/core/modules/url/pages/UrlClientDispatcher.vue +++ b/core/modules/url/pages/UrlClientDispatcher.vue @@ -17,19 +17,32 @@ const dispatcherRoutine = (to, from, next) => { if (userRoute) { userRoute.component().then(rootComponent => { _matchingComponentInstance = rootComponent.default - if (_matchingComponentInstance.asyncData) { - AsyncDataLoader.push({ // this is an example showing how to call data loader from another module - execute: _matchingComponentInstance.asyncData - }) - } - if (_matchingComponentInstance.mixins) { - _matchingComponentInstance.mixins.map(m => { - if (m.asyncData) { - AsyncDataLoader.push({ // this is an example showing how to call data loader from another module - execute: m.asyncData - }) - } - }) + + if (from.name) { // don't execute the action on the Client's side of the SSR actions + if (_matchingComponentInstance.asyncData) { + AsyncDataLoader.push({ + execute: _matchingComponentInstance.asyncData + }) + } + if (_matchingComponentInstance.preAsyncData) { + AsyncDataLoader.push({ + execute: _matchingComponentInstance.preAsyncData + }) + } + if (_matchingComponentInstance.mixins) { + _matchingComponentInstance.mixins.map(m => { + if (m.preAsyncData) { + AsyncDataLoader.push({ // this is an example showing how to call data loader from another module + execute: m.preAsyncData + }) + } + if (m.asyncData) { + AsyncDataLoader.push({ // this is an example showing how to call data loader from another module + execute: m.asyncData + }) + } + }) + } } next() }) From f9f73c8de2b46047b92d6aeb983c3caa63f2e444 Mon Sep 17 00:00:00 2001 From: pkarw Date: Fri, 15 Feb 2019 21:12:26 +0100 Subject: [PATCH 11/14] Category links --- core/lib/multistore.ts | 4 ++++ .../components/core/blocks/SidebarMenu/SidebarMenu.vue | 3 ++- .../components/core/blocks/SidebarMenu/SubCategory.vue | 10 ++++++++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/core/lib/multistore.ts b/core/lib/multistore.ts index 93ed200c23..ec2b6b95db 100644 --- a/core/lib/multistore.ts +++ b/core/lib/multistore.ts @@ -93,6 +93,9 @@ export function adjustMultistoreApiUrl (url: string) : string { } export function localizedRoute (routeObj: Route | string, storeCode: string) { + if (rootStore.state.config.seo.useUrlDispatcher) { + if (routeObj && typeof routeObj === 'object' && routeObj.fullPath) return localizedDispatcherRoute(Object.assign({}, routeObj, { params: null }), storeCode) + } if (storeCode && routeObj && rootStore.state.config.defaultStoreCode !== storeCode) { if (typeof routeObj === 'object') { if (routeObj.name) { @@ -115,6 +118,7 @@ export function localizedDispatcherRoute (routeObj: Route | string, storeCode: s if (routeObj && typeof routeObj === 'object' && routeObj.fullPath) { // case of using dispatcher return '/' + ((rootStore.state.config.defaultStoreCode !== storeCode) ? (storeCode + '/') : '') + routeObj.fullPath + (routeObj.params ? ('?' + buildURLQuery(routeObj.params)) : '') } else { + return routeObj } } diff --git a/src/themes/default/components/core/blocks/SidebarMenu/SidebarMenu.vue b/src/themes/default/components/core/blocks/SidebarMenu/SidebarMenu.vue index efd5bc2d25..4f6e3c562e 100644 --- a/src/themes/default/components/core/blocks/SidebarMenu/SidebarMenu.vue +++ b/src/themes/default/components/core/blocks/SidebarMenu/SidebarMenu.vue @@ -43,7 +43,7 @@ {{ category.name }} @@ -53,6 +53,7 @@ :category-links="category.children_data" :id="category.id" :parent-slug="category.slug" + :parent-path="category.url_path" />
  • diff --git a/src/themes/default/components/core/blocks/SidebarMenu/SubCategory.vue b/src/themes/default/components/core/blocks/SidebarMenu/SubCategory.vue index 046e709c5b..3b68a3b9f0 100644 --- a/src/themes/default/components/core/blocks/SidebarMenu/SubCategory.vue +++ b/src/themes/default/components/core/blocks/SidebarMenu/SubCategory.vue @@ -11,7 +11,7 @@ > {{ $t('View all') }} @@ -32,7 +32,7 @@ {{ link.name }} @@ -42,6 +42,7 @@ :id="link.id" v-if="link.children_count > 0" :parent-slug="link.slug" + :parent-path="link.url_path" />
  • @@ -96,6 +97,11 @@ export default { required: false, default: '' }, + parentPath: { + type: String, + required: false, + default: '' + }, myAccountLinks: { type: null, required: false, From 3885e6a6362678c3c1e9eadcca13bab53aa6e6a4 Mon Sep 17 00:00:00 2001 From: pkarw Date: Fri, 15 Feb 2019 21:13:53 +0100 Subject: [PATCH 12/14] Departments menu removed from Footer This static section very depends on the current database structure - so I removed it --- .../components/core/blocks/Footer/Footer.vue | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/src/themes/default/components/core/blocks/Footer/Footer.vue b/src/themes/default/components/core/blocks/Footer/Footer.vue index e8bff8d2bf..375598f288 100644 --- a/src/themes/default/components/core/blocks/Footer/Footer.vue +++ b/src/themes/default/components/core/blocks/Footer/Footer.vue @@ -10,31 +10,6 @@
    -
    -

    - {{ $t('Departments') }} -

    -
    - - {{ $t('Women fashion') }} - -
    -
    - - {{ $t("Men's fashion") }} - -
    -
    - - {{ $t('Kidswear') }} - -
    -
    - - {{ $t('Home') }} - -
    -

    {{ $t('Orders') }} From 426d37ee39d3f931431287b0ec02a6d6361d7d95 Mon Sep 17 00:00:00 2001 From: pkarw Date: Fri, 15 Feb 2019 21:27:42 +0100 Subject: [PATCH 13/14] product/list extended for registerMapping --- core/modules/catalog/store/product/actions.ts | 12 ++++++++++++ core/modules/url/hooks/afterRegistration.ts | 8 -------- .../components/core/blocks/Wishlist/Product.vue | 2 ++ .../theme/blocks/Inspirations/InspirationTile.vue | 2 +- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/core/modules/catalog/store/product/actions.ts b/core/modules/catalog/store/product/actions.ts index a72dd11ad6..ae0da9a627 100644 --- a/core/modules/catalog/store/product/actions.ts +++ b/core/modules/catalog/store/product/actions.ts @@ -319,6 +319,18 @@ const actions: ActionTree = { let selectedVariant = configureProductAsync(context, { product: product, configuration: configuration, selectDefaultVariant: false }) Object.assign(product, selectedVariant) } + if (rootStore.state.config.seo.useUrlDispatcher && product.url_path) { + rootStore.dispatch('url/registerMapping', { + url: product.url_path, + routeData: { + params: { + 'parentSku': product.parentSku, + 'slug': product.slug + }, + 'name': product.type_id + '-product' + } + }, { root: true }) + } } } return calculateTaxes(resp.items, context).then((updatedProducts) => { diff --git a/core/modules/url/hooks/afterRegistration.ts b/core/modules/url/hooks/afterRegistration.ts index ac26ff46f8..c0c57193d1 100644 --- a/core/modules/url/hooks/afterRegistration.ts +++ b/core/modules/url/hooks/afterRegistration.ts @@ -2,12 +2,4 @@ import { Logger } from '@vue-storefront/core/lib/logger' // This function will be fired both on server and client side context after registering other parts of the module export function afterRegistration({ Vue, config, store, isServer }){ - store.dispatch('url/registerMapping', { - url: '/fake/product/url.html', - routeData: { - 'parentSku': 'WS01', - 'slug': 'gwyn-endurance-tee', - 'name': 'configurable-product' - } - }, { root: true }) } diff --git a/src/themes/default/components/core/blocks/Wishlist/Product.vue b/src/themes/default/components/core/blocks/Wishlist/Product.vue index f6bba96033..fb96e1138e 100644 --- a/src/themes/default/components/core/blocks/Wishlist/Product.vue +++ b/src/themes/default/components/core/blocks/Wishlist/Product.vue @@ -3,6 +3,7 @@
    @@ -12,6 +13,7 @@
    {{ product.name | htmlDecode }} diff --git a/src/themes/default/components/theme/blocks/Inspirations/InspirationTile.vue b/src/themes/default/components/theme/blocks/Inspirations/InspirationTile.vue index fe67f89879..a8accdefec 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 @@