diff --git a/CHANGELOG.md b/CHANGELOG.md index 1298ff80b..9c67975b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,23 +5,65 @@ 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.11.2] - 2020.03.10 + +### Added + +- Add `isBackRoute` that informs if user returns to route, skip loading products for category if he does - @gibkigonzo (#4066) +- Add server context to async data loader - @gibkigonzo (#4113) +- Add preload and preconnect for google font - @gibkigonzo (#4121) + +### Changed / Improved + +- optimizations - improved prefetch strategy - @gibkigonzo (#4080) +- improvements to Finnish translations - @evktalo (#4116) +- Radio button now allows separate checked, value and name attributes - @EndPositive (#4098) +- Update backwards compatible dependencies - @simonmaass (#4126) + +### Fixed + +- add disconnect and sync options for cart/clear - @gibkigonzo (#4062) +- add '1' as searched value for 'is_user_defined' and 'is_visible' (createAttributesListQuery) - @gibkigonzo (#4075) +- Fix possibility to add same SKU with different custom options to the cart - @Michal-Dziedzinski (#3595) +- Fix `calculateProductTax` to find matching tax rules from ES for current product - @DylannCordel (#4056) +- Set `totals` in products in cart always in reactive way - @psmyrek (#4079) +- Fix sync cart between tabs - @Michal-Dziedzinski (#3838) +- Add currentRoute to url module and return cached requests - @gibkigonzo (#4077, #4066) +- Hide original radio button behind built label - @EndPositive (#4098) +- Disable overriding `route` state in **INITIAL_STATE** - @gibkigonzo (#4095) +- Fix gtm order placement event when user was guest - @Michal-Dziedzinski (#4064) +- Fix gtm event switched properties - @Michal-Dziedzinski (#4106) +- Group 'productChecksum' and 'productsEquals' logic for all supported products types. Remove 'checksum' when editing product. + Remove and add coupon when user login Remove 'NA' as default company. Show qty in microcart for all types of product. + Remove preload font - it gives good performance, but vue-meta refresh page, because there is script onload. - @gibkigonzo (#4128) +- Keep old category before route is resolved - @gibkigonzo (#4124) +- Added comments in 'productsEqual' and change logic for different types of products. Remove login user after order in Checkout. Allow changing qty for 'group' and 'bundle'.products - @gibkigonzo (#4144) +- Fix incorrect root categories when extending includeFields - @Michal-Dziedzinski (#4090) +- Add onlyPositive prop to BaseInputNumber to not allow user type negative value - @Michal-Dziedzinski (#4136) +- Await for cart/authorize while login user - @gibkigonzo (#4133) +- Fixed `NOT_ALLOWED_SSR_EXTENSIONS_REGEX` to only match with file extensions having a dot - @haelbichalex (#4100) +- Add lazy load for vue-carousel - @gibkigonzo (#4157) + ## [1.11.1] - 2020.02.05 ### Added + - Add `ProductPrice` component with bundleOptions and customOptions prices - @gibkigonzo (#3978) - Add lazy create cart token - @gibkigonzo (#3994) ### Changed / Improved + - Set cache tag when loading a category - @haelbichalex (#3940) -- In development build `webpack.config.js` in theme folder is now called without the `default` key +- In development build `webpack.config.js` in theme folder is now called without the `default` key - @psmyrek ### Fixed + - Added Finnish translations - @mattiteraslahti and @alphpkeemik - Updated Estonian translations to match 1.11 - @alphpkeemik - CookieNotification CSR&SSR mismatch fixed - @Fifciu (#3922) - The attribute filter in `attribute/list` was not filtering the already loaded attributes properly - @pkarw (#3964) - Update `hasProductErrors` in Product component and support additional sku in custom options - @gibkigonzo (#3976) -- Fixed logic for generating ${lang}.json files in multi-store setup - @jpkempf +- Fixed logic for generating \${lang}.json files in multi-store setup - @jpkempf - Fixed logic for collecting valid locales in single-store, multi-lang setup - @jpkempf - Make initial custom option value reactive - @gibkigonzo - Fixed No image thumbnails leaded on 404 page - @andrzejewsky (#3955) @@ -30,7 +72,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed product bundle comparison condition - @gk-daniel (#4004) - Add event callback for checkout load initial data - @gibkigonzo(#3985) - Fixed `Processing order...` modal closing too early - @grimasod (#4021) -- Keep registered payment methods after `syncTotals` - @grimasod (#4020) +- Keep registered payment methods after `syncTotals` - @grimasod (#4020) - Added status code to the cache content and use it in cache response - @resubaka (#4014) - Fixed sku attribute is missing on compare page - @gibkigonzo (#4036) - Fixed z-index for aside in compare list - @gibkigonzo (#4037) @@ -44,6 +86,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.11.0] - 2019.12.20 ### Added + - Add unit tests for `core/modules/url` - @dz3n (#3469) - Add unit test for `core/modules/checkout` - @psmyrek (#3460) - Add defense against incomplete config in ssr renderer - @oskar1233 (#3774) @@ -55,6 +98,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added price filtering key as config - @roywcm ### Fixed + - Fixed missing parameter to query function from cms/store/block/actions - @georgiev-ivan (#3909) - Always close zoom overlay after changing product - @psmyrek (#3818) - Fixed problem with cutting image height in category page on 1024px+ screen res - @AdKamil (#3781) @@ -68,22 +112,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed Product page breadcrumbs problem when products are in multiple categories in different branches of the category tree - @grimasod (#3691) - Change translation from jp-JP to ja-JP - @gibkigonzo (#3824) - Fixed ecosystem config for pm2 - @andrzejewsky (#3842) -- Fixed `mappingFallback` for extending modules - @andrzejewsky (#3822) +- Fixed `mappingFallback` for extending modules - @andrzejewsky (#3822) - Fixed adding products search results to category-next product store - @grimasod (#3877) - Use `defaultSortBy` for sorting category products by default @haelbichalex (#3873) - Fixed some potential mutations of Config object in `catalog` and `catalog-next` - @grimasod (#3843) -- Set `null` as default value for custom option in product page - @gibkigonzo (#3885) -- Fixed Breadcrumb filters - apply to second category fetch - @grimasod (#3887) +- Set `null` as default value for custom option in product page - @gibkigonzo (#3885) +- Fixed Breadcrumb filters - apply to second category fetch - @grimasod (#3887) - Fixed `config.storeViews.commonCache` being ignored - @grimasod (#3895) - Fixed static pages, password notification, offline mode #3902 - @andrzejewsky (#3902) - Fixed error page display with enabled multistore - @gibkigonzo (#3890) - Fixed edit shipping address in my account - @gibkigonzo (#3921) - Fetch cms_block content in serverPrefetch method - @gibkigonzo (#3910) -- Fixed saving invalidated user token - @andrzejewsky (#3923) +- Fixed saving invalidated user token - @andrzejewsky (#3923) - Keep category products objects on ssr - @gibkigonzo (#3924) - product breadcrumbs - check if current category is not highest one - @gibkigonzo (#3933) ### Changed / Improved + - Changed pre commit hook to use NODE_ENV production to check for debugger statements - @resubaka (#3686) - Improve the readability of 'getShippingDetails()' and 'updateDetails()' method of UserShippingDetails component - @adityasharma7 (#3770) - Keep git after yarn install in dockerfile - @ddanier (#3826) @@ -92,15 +137,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.11.0-rc.2] - 2019.10.31 ### Added + - Add defense for incomplete config in preferchCachedAttributes helper - Add unit test for \`core/modules/cms\` - @krskibin (#3738) ### Fixed + - Fixed deprecated getter in cmsBlock store - @resubaka (#3683) - Fixed problem around dynamic urls when default storeView is set with appendStoreCode false and url set to / . @resubaka (#3685) - Fixed three problems you can run into when you have bundle products - @resubaka (#3692) - Reset nested menu after logout - @gibkigonzo (#3680) -- Fixed handling checkbox custom option - @gibkigonzo (#2781) +- Fixed handling checkbox custom option - @gibkigonzo (#2781) - Fixed typos in docs - @afozbek (#3709) - Fixed VSF build fails for some people due to lack of dependencies in the container - @krskibin (#3699) - Fixed two graphql problems, one with cms_blocks and the other with default sort order - @resubaka (#3718) @@ -120,6 +167,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Custom module `ConfigProvider` aren't called anymore - @cewald (#3797) ### Added + - Added Estonian translations - @alphpkeemik - Added support for ES7 - @andrzejewsky (#3690) - Added unit tests for `core/modules/mailer` - @krskibin (#3710) @@ -131,6 +179,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add unit test for `core/modules/wishlist` - @psmyrek (#3471) ### Changed / Improved + - Use `encodeURIComponent` to encode get parameters in `multimatch.js` - @adityasharma7 (#3736) ## [1.11.0-rc.1] - 2019.10.03 @@ -248,7 +297,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Remove modifying config by reference in multistore - @gibkigonzo (#3617) - Add translation key for add review - @gibkigonzo (#3611) - Add product name prop to reviews component - @gibkigonzo (#3607) -- Show default cms pages when current store code is not equals to default - @andrzejewsky (#3579) +- Show default cms pages when current store code is not equals to default - @andrzejewsky (#3579) - Fix login errors with mailchimp - @gibkigonzo (#3612) - Hydration error on homepage - @patzick (#3609) - Fix adding products with custom options - @andrzejewsky (#3597) @@ -317,9 +366,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Pass to `registerModule` all parameters as one object - @gibkigonzo (#3634) - Include shipping address data in request for shipping methods for more accurate filtering - @rain2o (#2515) - remove 'disabled' flag in storeViews config - @gibkigonzo (#3659) + ## [1.10.5] - 28.11.2019 ### Fixed + - Disable product mutation when assigning product variant - @gibkigonzo (#3735) - Fix issue with Cannot assign to read only property 'storeCode' - @yuriboyko (#3748) - Render correct category links when multistore is active - @gibkigonzo (#3753) @@ -333,6 +384,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [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) @@ -355,17 +407,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed evaluate detailsLink in the cookie notification - @benjick (#3689) ## Added + - Added german translations - @schwerdt-ke (3076) ## [1.10.3] - 2019.09.18 ### Fixed + - Broken sidebar menu in mobile view - @przspa (#3549) - UrlDispatcher issues with multistore routes - @pkarw (#3568) ## [1.10.2] - 2019.09.06 ### Fixed + - Product image is missing on PDP - @przspa, @NavaneethVijay (#3483) - Mounting app when routes are resolved, should completly remove recent SSR errors - patzick (#3499) - Fixed `categoriesDynamicPrefetchLevel` that now can be equal to 0 - @pkarw (#3495) @@ -373,6 +428,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.10.1] - 2019.09.03 ### Fixed + - Invalid Discount code error handled by theme - @grimasod (#3385) - Fallback for empty value or no_selection child image - @ngongoll (#3397) - `order.order_id` was not assigned in the `orders.directBackendSync` mode - @pkarw (#3398) @@ -519,7 +575,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Refactored Travis config - @Tjitse-E (#3035) - Renamed the `stock/check` to `stock/queueCheck` to better emphasize it's async nature; added `stock/check` which does exactly what name suggests - returning the true stock values - @pkarw (#3150) - Cart unit tests throwing lots of type warnings - @lukeromanowicz (#3185) -- Lack of possibility to mock src modules and theme components - @lukeromanowicz (#3185) +- Lack of possibility to mock src modules and theme components - @lukeromanowicz (#3185) - Outdated signature of Registration hooks for google-tag-manager - @vishal-7037 (#3208) - Added serveral missing german translations and fixed german language file structure - @unherz (#3202) - Refactored the informal way of adressing to formal in german translation files - @unherz (#3213) diff --git a/config/default.json b/config/default.json index 5399fe6cd..f23130cdf 100644 --- a/config/default.json +++ b/config/default.json @@ -23,7 +23,7 @@ "dynamicConfigInclude": [], "elasticCacheQuota": 4096, "ssrDisabledFor": { - "extensions": [".png", ".gif", ".jpg", ".jpeg", ".woff", ".eot", ".woff2", ".ttf", ".svg", ".css", ".js", ".json", ".ico", ".tiff", ".tif", ".raw"] + "extensions": ["png", "gif", "jpg", "jpeg", "woff", "eot", "woff2", "ttf", "svg", "css", "js", "json", "ico", "tiff", "tif", "raw"] }, "trace": { "enabled": false, @@ -449,7 +449,7 @@ "i18n": { "defaultCountry": "US", "defaultLanguage": "EN", - "availableLocale": ["en-US","de-DE","fr-FR","es-ES","nl-NL", "ja-JP", "ru-RU", "it-IT", "pt-BR", "pl-PL", "cs-CZ"], + "availableLocale": ["en-US"], "defaultLocale": "en-US", "currencyCode": "USD", "currencySign": "$", @@ -457,7 +457,7 @@ "dateFormat": "HH:mm D/M/YYYY", "fullCountryName": "United States", "fullLanguageName": "English", - "bundleAllStoreviewLanguages": true + "bundleAllStoreviewLanguages": false }, "expireHeaders": { "default": "30d", diff --git a/core/build/webpack.client.config.ts b/core/build/webpack.client.config.ts index 7eb681458..6a3d71fea 100644 --- a/core/build/webpack.client.config.ts +++ b/core/build/webpack.client.config.ts @@ -9,7 +9,7 @@ const config = merge(base, { splitChunks: { cacheGroups: { commons: { - test: /[\\/]node_modules[\\/](vue|vuex|vue-router|vue-meta|vue-i18n|vuex-router-sync|localforage)[\\/]/, + test: /[\\/]node_modules[\\/](vue|vuex|vue-router|vue-meta|vue-i18n|vuex-router-sync|localforage|lean-he|vue-lazyload|js-sha3|dayjs|core-js|whatwg-fetch|vuelidate)[\\/]/, name: 'vendor', chunks: 'all' } diff --git a/core/client-entry.ts b/core/client-entry.ts index c7135d0ad..4296766fa 100755 --- a/core/client-entry.ts +++ b/core/client-entry.ts @@ -25,7 +25,7 @@ const invokeClientEntry = async () => { if (window.__INITIAL_STATE__) { // skip fields that were set by createApp const initialState = coreHooksExecutors.beforeHydrated( - omit(window.__INITIAL_STATE__, ['storeView', 'config', 'version']) + omit(window.__INITIAL_STATE__, ['storeView', 'config', 'version', 'route']) ) store.replaceState(Object.assign({}, store.state, initialState, { config: globalConfig })) } @@ -103,6 +103,9 @@ const invokeClientEntry = async () => { if (!matched.length || !matched[0]) { return next() } + + store.dispatch('url/setCurrentRoute', {to, from}) + Promise.all(matched.map((c: any) => { // TODO: update me for mixins support const components = c.mixins && globalConfig.ssr.executeMixedinAsyncData ? Array.from(c.mixins) : [] union(components, [c]).map(SubComponent => { diff --git a/core/helpers/index.ts b/core/helpers/index.ts index 1addece6b..1d69132b2 100644 --- a/core/helpers/index.ts +++ b/core/helpers/index.ts @@ -218,7 +218,7 @@ export const serial = async promises => { return results } -// helper to calcuate the hash of the shopping cart +// helper to calculate the hash of the shopping cart export const calcItemsHmac = (items, token) => { return sha3_224(JSON.stringify({ items, token: token })) } diff --git a/core/helpers/router.ts b/core/helpers/router.ts index c625d7d4f..5163a2f69 100644 --- a/core/helpers/router.ts +++ b/core/helpers/router.ts @@ -1,5 +1,15 @@ import VueRouter, { RouteConfig } from 'vue-router' -import { RouterManager } from '@vue-storefront/core/lib/router-manager'; +import { RouterManager } from '@vue-storefront/core/lib/router-manager' +import { ErrorHandler, RawLocation, Route } from 'vue-router/types/router' +import { once } from '@vue-storefront/core/helpers' + +once('__VUE_EXTEND_PUSH_RR__', () => { + const originalPush = VueRouter.prototype.push + VueRouter.prototype.push = function push (location: RawLocation, onComplete: Function = () => {}, onAbort?: ErrorHandler): Promise { + if (onComplete || onAbort) return originalPush.call(this, location, onComplete, onAbort) + return originalPush.call(this, location).catch(err => err) + } +}) export const createRouter = (): VueRouter => { return new VueRouter({ @@ -14,7 +24,7 @@ export const createRouter = (): VueRouter => { if (savedPosition) { return savedPosition } else if (to.path !== from.path) { // do not change scroll position when navigating on the same page (ex. change filters) - return {x: 0, y: 0} + return { x: 0, y: 0 } } } }) @@ -25,15 +35,15 @@ export const createRouterProxy = (router: VueRouter): VueRouter => { return new ProxyConstructor(router, { get (target, propKey) { - const origMethod = target[propKey]; + const origMethod = target[propKey] if (propKey === 'addRoutes') { return function (routes: RouteConfig[], ...args): void { - return RouterManager.addRoutes(routes, ...args); - }; + return RouterManager.addRoutes(routes, ...args) + } } - return origMethod; + return origMethod } }) } diff --git a/core/i18n/package.json b/core/i18n/package.json index a85381169..3006d375b 100644 --- a/core/i18n/package.json +++ b/core/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@vue-storefront/i18n", - "version": "1.11.1", + "version": "1.11.2", "description": "Vue Storefront i18n", "license": "MIT", "main": "index.ts", diff --git a/core/i18n/resource/i18n/fi-FI.csv b/core/i18n/resource/i18n/fi-FI.csv index 40cd4fe51..df8564cd0 100644 --- a/core/i18n/resource/i18n/fi-FI.csv +++ b/core/i18n/resource/i18n/fi-FI.csv @@ -4,7 +4,7 @@ "Add review","Lisää arvostelu" "Adding a review ...","Lisätään arvostelua…" "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.","Kassalla annettu osoite on virheellinen. Tarkista, että kaikki pakolliset kentät on täytetty. Tilauksesi on peruttu. Ongelman jatkuessa ota yhteyttä sähköpostilla osoitteeseen {email}." -"Allow notification about the order","Vastaanota ilmoitus tilauksesta" +"Allow notification about the order","Salli ilmoitus tilauksesta" "Are you sure you would like to remove this item from the shopping cart?","Haluatko varmasti poistaa tuotteen ostoskorista?" "Compare Products","Vertaile tuotteita" "Compare products","Vertaile tuotteita" @@ -46,7 +46,7 @@ "Product {productName} has been added to wishlist!","{productName} on lisätty toivelistaan." "Product {productName} has been removed from compare!","{productName} on poistettu tuotevertailusta." "Product {productName} has been removed from wishlist!","{productName} on poistettu toivelistasta" -"Quantity available offline","Määrä saatavilla yhteydettömässä" +"Quantity available offline","Määrä saatavilla yhteydettömässä tilassa" "Quantity available","Määrä saatavilla" "Quantity must be above 0","Määrän pitää olla suurempi kuin 0" "Quantity must be below {quantity}","Määrän pitää olla alle {quantity}" @@ -55,7 +55,7 @@ "Reset password feature does not work while offline!","Salasanan uusiminen ei ole käytössä yhteydettömässä tilassa." "Select 0","Valitse 0" "Select 1","Valitse 1" -"Shopping cart is empty. Please add some products before entering Checkout","Ostoskori on tyhjä." +"Shopping cart is empty. Please add some products before entering Checkout","Ostoskori on tyhjä. Lisää tuotteita ennen kuin siirryt kassalle." "Some of the ordered products are not available!","Jotkut tilatuista tuotteista eivät ole saatavilla." "Stock check in progress, please wait while available stock quantities are checked","Odota hetki, varastosaldoja tarkistetaan" "Subtotal incl. tax","Välisumma (sis. ALV)" @@ -63,7 +63,7 @@ "The product, category or CMS page is not available in Offline mode. Redirecting to Home.","Sivu ei ole saatavilla yhteydettömässä tilassa. Ohjataan etusivulle." "The system is not sure about the stock quantity (volatile). Product has been added to the cart for pre-reservation.","Tuotteen saatavuus on epävarma. Tuote on lisätty ostoskoriin ennakkovarauksena." "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.","Verkkoyhteys ei ole saatavilla. Voit silti tehdä tilauksen. Saat myöhemmin ilmoituksen, mikäli jokin tilaamistasi tuotteista ei ole saatavilla." -"This feature is not implemented yet! Please take a look at https://github.com/DivanteLtd/vue-storefront/issues for our Roadmap!","Tätä ominaisuutta ei ole vielä toteutettu. Järjestelmän roadmap löytyy osoitteesta https://github.com/DivanteLtd/vue-storefront/issues." +"This feature is not implemented yet! Please take a look at https://github.com/DivanteLtd/vue-storefront/issues for our Roadmap!","Tätä ominaisuutta ei ole vielä toteutettu. Järjestelmän kehityssuunnitelma löytyy osoitteesta https://github.com/DivanteLtd/vue-storefront/issues." "Type what you are looking for...",Hae "Unexpected authorization error. Check your Network conection.","Odottamaton virhe oikeuksien tarkistamisessa. Tarkista verkkoyhteys." "Unhandled error, wrong response format!","Odottamaton virhe, väärä vastauksen formaatti." diff --git a/core/lib/search.ts b/core/lib/search.ts index 761d99a8b..e0a5cacea 100644 --- a/core/lib/search.ts +++ b/core/lib/search.ts @@ -33,7 +33,7 @@ export const quickSearchByQuery = async ({ query = {}, start = 0, size = 50, ent if (size <= 0) size = 50 if (start < 0) start = 0 - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { const storeView = currentStoreView() const Request: SearchRequest = { store: storeCode || storeView.storeCode, // TODO: add grouped product and bundled product support @@ -58,20 +58,21 @@ export const quickSearchByQuery = async ({ query = {}, start = 0, size = 50, ent const cacheKey = sha3_224(JSON.stringify(Request)) const benchmarkTime = new Date() - cache.getItem(cacheKey, (err, res) => { - if (err) console.log(err) + try { + const res = await cache.getItem(cacheKey) if (res !== null) { res.cache = true res.noresults = false res.offline = !isOnline() // TODO: refactor it to checking ES heartbit - resolve(res) Logger.debug('Result from cache for ' + cacheKey + ' (' + entityType + '), ms=' + (new Date().getTime() - benchmarkTime.getTime()))() servedFromCache = true + resolve(res) + return } - }).catch((err) => { + } catch (err) { console.error('Cannot read cache for ' + cacheKey + ', ' + err) - }) + } /* use only for cache */ if (Request.groupId) { @@ -87,7 +88,7 @@ export const quickSearchByQuery = async ({ query = {}, start = 0, size = 50, ent } searchAdapter.search(Request).then((resp) => { // we're always trying to populate cache - when online - const res = searchAdapter.entities[Request.type].resultPorcessor(resp, start, size) + const res = searchAdapter.entities[Request.type].resultProcessor(resp, start, size) if (res) { // otherwise it can be just a offline mode cache.setItem(cacheKey, res, null, config.elasticsearch.disablePersistentQueriesCache).catch((err) => { console.error('Cannot store cache for ' + cacheKey + ', ' + err) }) diff --git a/core/lib/search/adapter/api/searchAdapter.ts b/core/lib/search/adapter/api/searchAdapter.ts index b40e8fb20..d5c1ed683 100644 --- a/core/lib/search/adapter/api/searchAdapter.ts +++ b/core/lib/search/adapter/api/searchAdapter.ts @@ -102,18 +102,19 @@ export class SearchAdapter { suggestions: resp.suggest } } else { - if (resp.error) { - throw new Error(JSON.stringify(resp.error)) + const isErrorObject = (resp && resp.code) >= 400 ? resp : null + if (resp.error || isErrorObject) { + throw new Error(JSON.stringify(resp.error || resp)) } else { - throw new Error('Unknown error with elasticsearch result in resultPorcessor for entity type \'' + type + '\'') + throw new Error('Unknown error with elasticsearch result in resultProcessor for entity type \'' + type + '\'') } } } - public registerEntityType (entityType, { url = '', queryProcessor, resultPorcessor }) { + public registerEntityType (entityType, { url = '', queryProcessor, resultProcessor }) { this.entities[entityType] = { queryProcessor: queryProcessor, - resultPorcessor: resultPorcessor + resultProcessor: resultProcessor } if (url !== '') { this.entities[entityType]['url'] = url @@ -127,7 +128,7 @@ export class SearchAdapter { // function that can modify the query each time before it's being executed return query }, - resultPorcessor: (resp, start, size) => { + resultProcessor: (resp, start, size) => { return this.handleResult(resp, 'product', start, size) } }) @@ -137,7 +138,7 @@ export class SearchAdapter { // function that can modify the query each time before it's being executed return query }, - resultPorcessor: (resp, start, size) => { + resultProcessor: (resp, start, size) => { return this.handleResult(resp, 'attribute', start, size) } }) @@ -147,7 +148,7 @@ export class SearchAdapter { // function that can modify the query each time before it's being executed return query }, - resultPorcessor: (resp, start, size) => { + resultProcessor: (resp, start, size) => { return this.handleResult(resp, 'category', start, size) } }) @@ -157,7 +158,7 @@ export class SearchAdapter { // function that can modify the query each time before it's being executed return query }, - resultPorcessor: (resp, start, size) => { + resultProcessor: (resp, start, size) => { return this.handleResult(resp, 'taxrule', start, size) } }) @@ -167,7 +168,7 @@ export class SearchAdapter { // function that can modify the query each time before it's being executed return query }, - resultPorcessor: (resp, start, size) => { + resultProcessor: (resp, start, size) => { return this.handleResult(resp, 'review', start, size) } }) @@ -176,7 +177,7 @@ export class SearchAdapter { // function that can modify the query each time before it's being executed return query }, - resultPorcessor: (resp, start, size) => { + resultProcessor: (resp, start, size) => { return this.handleResult(resp, 'cms_page', start, size) } }) @@ -185,7 +186,7 @@ export class SearchAdapter { // function that can modify the query each time before it's being executed return query }, - resultPorcessor: (resp, start, size) => { + resultProcessor: (resp, start, size) => { return this.handleResult(resp, 'cms_block', start, size) } }) @@ -194,7 +195,7 @@ export class SearchAdapter { // function that can modify the query each time before it's being executed return query }, - resultPorcessor: (resp, start, size) => { + resultProcessor: (resp, start, size) => { return this.handleResult(resp, 'cms_hierarchy', start, size) } }) diff --git a/core/lib/search/adapter/graphql/searchAdapter.ts b/core/lib/search/adapter/graphql/searchAdapter.ts index 382abfa1f..099cd7c84 100644 --- a/core/lib/search/adapter/graphql/searchAdapter.ts +++ b/core/lib/search/adapter/graphql/searchAdapter.ts @@ -70,14 +70,14 @@ export class SearchAdapter { * @param {string} gql gql file path * @param {String} url server URL * @param {function} queryProcessor some function which can update query if needed - * @param {function} resultPorcessor process results of response + * @param {function} resultProcessor process results of response * @return {Object} */ - public registerEntityType (entityType, { url = '', gql, queryProcessor, resultPorcessor }) { + public registerEntityType (entityType, { url = '', gql, queryProcessor, resultProcessor }) { this.entities[entityType] = { query: require(`${gql}`), queryProcessor: queryProcessor, - resultPorcessor: resultPorcessor + resultProcessor: resultProcessor } if (url !== '') { this.entities[entityType]['url'] = url @@ -90,14 +90,14 @@ export class SearchAdapter { * @param {graphQl} query is the GraphQL query * @param {String} url server URL * @param {function} queryProcessor some function which can update query if needed - * @param {function} resultPorcessor process results of response + * @param {function} resultProcessor process results of response * @return {Object} */ - public registerEntityTypeByQuery (entityType, { url = '', query, queryProcessor, resultPorcessor }) { + public registerEntityTypeByQuery (entityType, { url = '', query, queryProcessor, resultProcessor }) { this.entities[entityType] = { query: query, queryProcessor: queryProcessor, - resultPorcessor: resultPorcessor + resultProcessor: resultProcessor } if (url !== '') { this.entities[entityType]['url'] = url @@ -113,7 +113,7 @@ export class SearchAdapter { // function that can modify the query each time before it's being executed return query }, - resultPorcessor: (resp, start, size) => { + resultProcessor: (resp, start, size) => { if (resp === null) { throw new Error('Invalid graphQl result - null not exepcted') } @@ -123,7 +123,7 @@ export class SearchAdapter { if (resp.error) { throw new Error(JSON.stringify(resp.error)) } else { - throw new Error('Unknown error with graphQl result in resultPorcessor for entity type \'product\'') + throw new Error('Unknown error with graphQl result in resultProcessor for entity type \'product\'') } } } @@ -135,7 +135,7 @@ export class SearchAdapter { // function that can modify the query each time before it's being executed return query }, - resultPorcessor: (resp, start, size) => { + resultProcessor: (resp, start, size) => { if (resp === null) { throw new Error('Invalid graphQl result - null not exepcted') } @@ -145,7 +145,7 @@ export class SearchAdapter { if (resp.error) { throw new Error(JSON.stringify(resp.error)) } else { - throw new Error('Unknown error with graphQl result in resultPorcessor for entity type \'attribute\'') + throw new Error('Unknown error with graphQl result in resultProcessor for entity type \'attribute\'') } } } @@ -156,7 +156,7 @@ export class SearchAdapter { // function that can modify the query each time before it's being executed return query }, - resultPorcessor: (resp, start, size) => { + resultProcessor: (resp, start, size) => { if (resp === null) { throw new Error('Invalid graphQl result - null not exepcted') } @@ -166,7 +166,7 @@ export class SearchAdapter { if (resp.error) { throw new Error(JSON.stringify(resp.error)) } else { - throw new Error('Unknown error with graphQl result in resultPorcessor for entity type \'review\'') + throw new Error('Unknown error with graphQl result in resultProcessor for entity type \'review\'') } } } @@ -177,7 +177,7 @@ export class SearchAdapter { // function that can modify the query each time before it's being executed return query }, - resultPorcessor: (resp, start, size) => { + resultProcessor: (resp, start, size) => { if (resp === null) { throw new Error('Invalid graphQl result - null not exepcted') } @@ -187,7 +187,7 @@ export class SearchAdapter { if (resp.error) { throw new Error(JSON.stringify(resp.error)) } else { - throw new Error('Unknown error with graphQl result in resultPorcessor for entity type \'category\'') + throw new Error('Unknown error with graphQl result in resultProcessor for entity type \'category\'') } } } @@ -199,7 +199,7 @@ export class SearchAdapter { // function that can modify the query each time before it's being executed return query }, - resultPorcessor: (resp, start, size) => { + resultProcessor: (resp, start, size) => { if (resp === null) { throw new Error('Invalid graphQl result - null not exepcted') } @@ -209,7 +209,7 @@ export class SearchAdapter { if (resp.error) { throw new Error(JSON.stringify(resp.error)) } else { - throw new Error('Unknown error with graphQl result in resultPorcessor for entity type \'taxrule\'') + throw new Error('Unknown error with graphQl result in resultProcessor for entity type \'taxrule\'') } } } @@ -221,7 +221,7 @@ export class SearchAdapter { // function that can modify the query each time before it's being executed return query }, - resultPorcessor: (resp, start, size) => { + resultProcessor: (resp, start, size) => { if (resp === null) { throw new Error('Invalid graphQl result - null not exepcted') } @@ -231,7 +231,7 @@ export class SearchAdapter { if (resp.error) { throw new Error(JSON.stringify(resp.error)) } else { - throw new Error('Unknown error with graphQl result in resultPorcessor for entity type \'cmsPage\'') + throw new Error('Unknown error with graphQl result in resultProcessor for entity type \'cmsPage\'') } } } @@ -243,7 +243,7 @@ export class SearchAdapter { // function that can modify the query each time before it's being executed return query }, - resultPorcessor: (resp, start, size) => { + resultProcessor: (resp, start, size) => { if (resp === null) { throw new Error('Invalid graphQl result - null not exepcted') } @@ -253,7 +253,7 @@ export class SearchAdapter { if (resp.error) { throw new Error(JSON.stringify(resp.error)) } else { - throw new Error('Unknown error with graphQl result in resultPorcessor for entity type \'cmsBlock\'') + throw new Error('Unknown error with graphQl result in resultProcessor for entity type \'cmsBlock\'') } } } @@ -265,7 +265,7 @@ export class SearchAdapter { // function that can modify the query each time before it's being executed return query }, - resultPorcessor: (resp, start, size) => { + resultProcessor: (resp, start, size) => { if (resp === null) { throw new Error('Invalid graphQl result - null not exepcted') } @@ -275,7 +275,7 @@ export class SearchAdapter { if (resp.error) { throw new Error(JSON.stringify(resp.error)) } else { - throw new Error('Unknown error with graphQl result in resultPorcessor for entity type \'cmsHierarchy\'') + throw new Error('Unknown error with graphQl result in resultProcessor for entity type \'cmsHierarchy\'') } } } diff --git a/core/modules/cart/helpers/index.ts b/core/modules/cart/helpers/index.ts index 49e97d1bd..555b6e5a5 100644 --- a/core/modules/cart/helpers/index.ts +++ b/core/modules/cart/helpers/index.ts @@ -15,6 +15,7 @@ import getProductOptions from './getProductOptions' import getProductConfiguration from './getProductConfiguration' import createOrderData from './createOrderData' import createShippingInfoData from './createShippingInfoData' +import * as syncCartWhenLocalStorageChange from './syncCartWhenLocalStorageChange' export { cartCacheHandlerFactory, @@ -33,5 +34,6 @@ export { getProductOptions, getProductConfiguration, createOrderData, - createShippingInfoData + createShippingInfoData, + syncCartWhenLocalStorageChange } diff --git a/core/modules/cart/helpers/prepareProductsToAdd.ts b/core/modules/cart/helpers/prepareProductsToAdd.ts index fdae40599..d464694f3 100644 --- a/core/modules/cart/helpers/prepareProductsToAdd.ts +++ b/core/modules/cart/helpers/prepareProductsToAdd.ts @@ -12,8 +12,7 @@ const applyQty = product => ({ qty: product.qty && typeof product.qty !== 'number' ? parseInt(product.qty) : product.qty }); -const applyChecksumForBundles = product => - product.type_id === 'bundle' ? { ...product, checksum: productChecksum(product) } : product +const applyChecksum = product => ({ ...product, checksum: productChecksum(product) }) const prepareProductsToAdd = (product: CartItem): CartItem[] => { const products = product.type_id === 'grouped' ? readAssociated(product) : [product] @@ -22,7 +21,7 @@ const prepareProductsToAdd = (product: CartItem): CartItem[] => { .filter(isDefined) .map(applyQty) .map(p => optimizeProduct(p)) - .map(applyChecksumForBundles); + .map(applyChecksum); }; export default prepareProductsToAdd; diff --git a/core/modules/cart/helpers/productChecksum.ts b/core/modules/cart/helpers/productChecksum.ts index ea13e0137..21c27ae3a 100644 --- a/core/modules/cart/helpers/productChecksum.ts +++ b/core/modules/cart/helpers/productChecksum.ts @@ -1,22 +1,49 @@ import CartItem from '@vue-storefront/core/modules/cart/types/CartItem' import { sha3_224 } from 'js-sha3' +import get from 'lodash-es/get' +import flow from 'lodash-es/flow' +import cloneDeep from 'lodash-es/cloneDeep'; + +const replaceNumberToString = obj => { + Object.keys(obj).forEach(key => { + if (typeof obj[key] === 'object') { + return replaceNumberToString(obj[key]); + } + obj[key] = String(obj[key]); + }); + return obj; +} + +const transformToArray = value => Array.isArray(value) ? value : Object.values(value) + +export const getProductOptions = (product, optionsName) => { + return flow([ + get, + cloneDeep, + transformToArray, + replaceNumberToString + ])(product, `product_option.extension_attributes.${optionsName}`, []) +} const getDataToHash = (product: CartItem): any => { if (!product.product_option) { return null } - const { extension_attributes } = product.product_option + const supportedProductOptions = ['bundle_options', 'custom_options', 'configurable_item_options'] - if (extension_attributes.bundle_options) { - const { bundle_options } = extension_attributes - return Array.isArray(bundle_options) ? bundle_options : Object.values(bundle_options) + // returns first options that has array with options + for (let optionName of supportedProductOptions) { + const options = getProductOptions(product, optionName) + if (options.length) { + return options + } } + // if there are options that are not supported then just return all options return product.product_option } -const productChecksum = (product: CartItem): string => - sha3_224(JSON.stringify(getDataToHash(product))) +const productChecksum = (product: CartItem): string => sha3_224(JSON.stringify(getDataToHash(product))) export default productChecksum diff --git a/core/modules/cart/helpers/productsEquals.ts b/core/modules/cart/helpers/productsEquals.ts index 69156aee0..ad95e4989 100644 --- a/core/modules/cart/helpers/productsEquals.ts +++ b/core/modules/cart/helpers/productsEquals.ts @@ -1,20 +1,11 @@ import CartItem from '@vue-storefront/core/modules/cart/types/CartItem' -import productChecksum from './productChecksum'; +import productChecksum, { getProductOptions } from './productChecksum'; -const getChecksum = (product: CartItem) => { - if (product.checksum) { - return product.checksum - } - - return productChecksum(product) -} - -const getProductType = (product: CartItem): string => - product.type_id || product.product_type +type ProductEqualCheckFn = (product1: CartItem, product2: CartItem) => boolean +// 'id' check const getServerItemId = (product: CartItem): string | number => product.server_item_id || product.item_id - const isServerIdsEquals = (product1: CartItem, product2: CartItem): boolean => { const product1ItemId = getServerItemId(product1) const product2ItemId = getServerItemId(product2) @@ -24,24 +15,81 @@ const isServerIdsEquals = (product1: CartItem, product2: CartItem): boolean => { return areItemIdsDefined && product1ItemId === product2ItemId } +// 'checksum' check +const getChecksum = (product: CartItem) => { + if (product.checksum) { + return product.checksum + } + return productChecksum(product) +} const isChecksumEquals = (product1: CartItem, product2: CartItem): boolean => getChecksum(product1) === getChecksum(product2) +// 'sku' check +const isSkuEqual = (product1: CartItem, product2: CartItem): boolean => + String(product1.sku) === String(product2.sku) + +/** + * Returns product equality check function + * @param checkName - determines what type of check we want to do + */ +const getCheckFn = (checkName: string): ProductEqualCheckFn => { + switch (checkName) { + case 'id': { + return isServerIdsEquals + } + case 'checksum': { + return isChecksumEquals + } + case 'sku': { + return isSkuEqual + } + default: { + return isSkuEqual + } + } +} + +/** + * It passes all types of checks and returns the first passed. The order of checks matters! + */ +const makeCheck = (product1: CartItem, product2: CartItem, checks: string[]): boolean => { + for (let checkName of checks) { + const fn = getCheckFn(checkName) + if (fn(product1, product2)) { + return true + } + } +} + const productsEquals = (product1: CartItem, product2: CartItem): boolean => { if (!product1 || !product2) { return false } - const typeProduct1 = getProductType(product1) - const typeProduct2 = getProductType(product2) + const check = makeCheck.bind(null, product1, product2) - if (typeProduct1 === 'bundle' || typeProduct2 === 'bundle') { - if (isServerIdsEquals(product1, product2) || isChecksumEquals(product1, product2)) { - return true - } + if (getProductOptions(product1, 'bundle_options').length || getProductOptions(product2, 'bundle_options').length) { + // bundle options skus are merged into one sku so we can't rely on 'sku' + // by default we want to check server_item_id ('id'), we can also use 'checksum' + return check(['id', 'checksum']) + } + + if (getProductOptions(product1, 'custom_options').length || getProductOptions(product2, 'custom_options').length) { + // in admin panel we can add different sku for specific custom option so we can't rely on 'sku' + // by default we want to check server_item_id ('id'), we can also use 'checksum' + return check(['id', 'checksum']) + } + + if (getProductOptions(product1, 'configurable_item_options').length || getProductOptions(product2, 'configurable_item_options').length) { + // 'sku' should be uniq for configurable products + // we can't check 'id' because it is the same when user edit product in microcart, so it can give wrong result + return check(['sku']) } - return isServerIdsEquals(product1, product2) || String(product1.sku) === String(product2.sku) + // by default we want to check if server_item_id is equal and check sku as fallback + // this is for 'simple' and 'group' products + return check(['id', 'sku']) } export default productsEquals diff --git a/core/modules/cart/helpers/syncCartWhenLocalStorageChange.ts b/core/modules/cart/helpers/syncCartWhenLocalStorageChange.ts new file mode 100644 index 000000000..e8fa00446 --- /dev/null +++ b/core/modules/cart/helpers/syncCartWhenLocalStorageChange.ts @@ -0,0 +1,21 @@ +import rootStore from '@vue-storefront/core/store'; + +function getItemsFromStorage ({key}) { + if (key === 'shop/cart/current-cart') { + const storedItems = JSON.parse(localStorage[key]) + rootStore.dispatch('cart/syncCartWhenLocalStorageChange', {items: storedItems}) + } +} + +function addEventListener () { + window.addEventListener('storage', getItemsFromStorage) +} + +function removeEventListener () { + window.removeEventListener('storage', getItemsFromStorage) +} + +export { + addEventListener, + removeEventListener +} diff --git a/core/modules/cart/store/actions/connectActions.ts b/core/modules/cart/store/actions/connectActions.ts index 37b423a63..03d91d252 100644 --- a/core/modules/cart/store/actions/connectActions.ts +++ b/core/modules/cart/store/actions/connectActions.ts @@ -9,23 +9,38 @@ const connectActions = { toggleMicrocart ({ commit }) { commit(types.CART_TOGGLE_MICROCART) }, - async clear ({ commit, dispatch, getters }) { + /** + * It will always clear cart items on frontend. + * Options: + * sync - if you want to sync it with backend. + * disconnect - if you want to clear cart token. + */ + async clear ({ commit, dispatch }, { disconnect = true, sync = true } = {}) { await commit(types.CART_LOAD_CART, []) - await commit(types.CART_LOAD_CART_SERVER_TOKEN, null) - await commit(types.CART_SET_ITEMS_HASH, null) + if (sync) { + await dispatch('sync', { forceClientState: true, forceSync: true }) + } + if (disconnect) { + await commit(types.CART_SET_ITEMS_HASH, null) + await dispatch('disconnect') + } }, async disconnect ({ commit }) { commit(types.CART_LOAD_CART_SERVER_TOKEN, null) }, async authorize ({ dispatch, getters }) { - await dispatch('connect', { guestCart: false }) - const coupon = getters.getCoupon.code - if (!getters.getCoupon) { + if (coupon) { + await dispatch('removeCoupon', { sync: false }) + } + + await dispatch('connect', { guestCart: false, mergeQty: true }) + + if (coupon) { await dispatch('applyCoupon', coupon) } }, - async connect ({ getters, dispatch, commit }, { guestCart = false, forceClientState = false }) { + async connect ({ getters, dispatch, commit }, { guestCart = false, forceClientState = false, mergeQty = false }) { if (!getters.isCartSyncEnabled) return const { result, resultCode } = await CartService.getCartToken(guestCart, forceClientState) @@ -33,7 +48,7 @@ const connectActions = { Logger.info('Server cart token created.', 'cart', result)() commit(types.CART_LOAD_CART_SERVER_TOKEN, result) - return dispatch('sync', { forceClientState, dryRun: !config.cart.serverMergeByDefault, mergeQty: true }) + return dispatch('sync', { forceClientState, dryRun: !config.cart.serverMergeByDefault, mergeQty }) } if (resultCode === 401 && getters.bypassCounter < config.queues.maxCartBypassAttempts) { diff --git a/core/modules/cart/store/actions/couponActions.ts b/core/modules/cart/store/actions/couponActions.ts index 404c37e42..1cebd576e 100644 --- a/core/modules/cart/store/actions/couponActions.ts +++ b/core/modules/cart/store/actions/couponActions.ts @@ -1,12 +1,12 @@ import { CartService } from '@vue-storefront/core/data-resolver' const couponActions = { - async removeCoupon ({ getters, dispatch }) { + async removeCoupon ({ getters, dispatch }, { sync = true } = {}) { if (getters.canSyncTotals) { const { result } = await CartService.removeCoupon() - if (result) { - dispatch('syncTotals', { forceServerSync: true }) + if (result && sync) { + await dispatch('syncTotals', { forceServerSync: true }) return result } } @@ -16,7 +16,7 @@ const couponActions = { const { result } = await CartService.applyCoupon(couponCode) if (result) { - dispatch('syncTotals', { forceServerSync: true }) + await dispatch('syncTotals', { forceServerSync: true }) } return result } diff --git a/core/modules/cart/store/actions/itemActions.ts b/core/modules/cart/store/actions/itemActions.ts index ec8e5399c..b658494d5 100644 --- a/core/modules/cart/store/actions/itemActions.ts +++ b/core/modules/cart/store/actions/itemActions.ts @@ -11,7 +11,7 @@ import { import { cartHooksExecutors } from './../../hooks' const itemActions = { - configureItem (context, { product, configuration }) { + async configureItem (context, { product, configuration }) { const { commit, dispatch, getters } = context const variant = configureProductAsync(context, { product, @@ -29,7 +29,7 @@ const itemActions = { commit(types.CART_UPD_ITEM_PROPS, { product: { ...product, ...variant } }) if (getters.isCartSyncEnabled && product.server_item_id) { - dispatch('sync', { forceClientState: true }) + await dispatch('sync', { forceClientState: true }) } }, updateItem ({ commit }, { product }) { diff --git a/core/modules/cart/store/actions/mergeActions.ts b/core/modules/cart/store/actions/mergeActions.ts index 0b024b500..36b500af9 100644 --- a/core/modules/cart/store/actions/mergeActions.ts +++ b/core/modules/cart/store/actions/mergeActions.ts @@ -199,7 +199,7 @@ const mergeActions = { } const mergeClientItemsDiffLog = await dispatch('mergeClientItems', mergeParameters) const mergeServerItemsDiffLog = await dispatch('mergeServerItems', mergeParameters) - dispatch('updateTotalsAfterMerge', { clientItems, dryRun }) + await dispatch('updateTotalsAfterMerge', { clientItems, dryRun }) diffLog .merge(mergeClientItemsDiffLog) diff --git a/core/modules/cart/store/actions/synchronizeActions.ts b/core/modules/cart/store/actions/synchronizeActions.ts index 066ea70eb..cf4b8b477 100644 --- a/core/modules/cart/store/actions/synchronizeActions.ts +++ b/core/modules/cart/store/actions/synchronizeActions.ts @@ -20,6 +20,9 @@ const synchronizeActions = { cartHooksExecutors.afterLoad(storedItems) }, + syncCartWhenLocalStorageChange ({commit}, {items}) { + commit(types.CART_LOAD_CART, items) + }, async synchronizeCart ({ commit, dispatch }, { forceClientState }) { const { synchronize, serverMergeByDefault } = config.cart if (!synchronize) return @@ -44,10 +47,10 @@ const synchronizeActions = { Logger.warn('The "cart/serverPull" action is deprecated and will not be supported with the Vue Storefront 1.11', 'cart')() return dispatch('sync', { forceClientState, dryRun }) }, - async sync ({ getters, rootGetters, commit, dispatch, state }, { forceClientState = false, dryRun = false, mergeQty = false }) { + async sync ({ getters, rootGetters, commit, dispatch, state }, { forceClientState = false, dryRun = false, mergeQty = false, forceSync = false }) { const shouldUpdateClientState = rootGetters['checkout/isUserInCheckout'] || forceClientState const { getCartItems, canUpdateMethods, isSyncRequired, bypassCounter } = getters - if (!canUpdateMethods || !isSyncRequired) return createDiffLog() + if ((!canUpdateMethods || !isSyncRequired) && !forceSync) return createDiffLog() commit(types.CART_SET_SYNC) const { result, resultCode } = await CartService.getItems() const { serverItems, clientItems } = cartHooksExecutors.beforeSync({ clientItems: getCartItems, serverItems: result }) diff --git a/core/modules/cart/store/actions/totalsActions.ts b/core/modules/cart/store/actions/totalsActions.ts index d6e687136..b70020ce3 100644 --- a/core/modules/cart/store/actions/totalsActions.ts +++ b/core/modules/cart/store/actions/totalsActions.ts @@ -62,10 +62,11 @@ const totalsActions = { }) if (shippingMethodsData.country) { - return dispatch('overrideServerTotals', { + await dispatch('overrideServerTotals', { hasShippingInformation: shippingMethodsData.method_code || shippingMethodsData.carrier_code, addressInformation: createShippingInfoData(shippingMethodsData) }) + return } Logger.error('Please do set the tax.defaultCountry in order to calculate totals', 'cart')() @@ -73,7 +74,7 @@ const totalsActions = { }, async refreshTotals ({ dispatch }, payload) { Logger.warn('The "cart/refreshTotals" action is deprecated and will not be supported with the Vue Storefront 1.11', 'cart')() - return dispatch('syncTotals', payload) + await dispatch('syncTotals', payload) } } diff --git a/core/modules/cart/store/mutations.ts b/core/modules/cart/store/mutations.ts index eb016bb08..a0bfce3a1 100644 --- a/core/modules/cart/store/mutations.ts +++ b/core/modules/cart/store/mutations.ts @@ -1,3 +1,4 @@ +import Vue from 'vue' import { MutationTree } from 'vuex' import * as types from './mutation-types' import CartState from '../types/CartState' @@ -56,7 +57,7 @@ const mutations: MutationTree = { let record = state.cartItems.find(p => (productsEquals(p, product) || (p.server_item_id && p.server_item_id === product.server_item_id))) if (record) { EventBus.$emit('cart-before-itemchanged', { item: record }) - record = Object.assign(record, product) + Object.entries(product).forEach(([key, value]) => Vue.set(record, key, value)) EventBus.$emit('cart-after-itemchanged', { item: record }) } }, diff --git a/core/modules/cart/test/unit/components/AddToCart.spec.ts b/core/modules/cart/test/unit/components/AddToCart.spec.ts index 552a23ebe..1bd639701 100644 --- a/core/modules/cart/test/unit/components/AddToCart.spec.ts +++ b/core/modules/cart/test/unit/components/AddToCart.spec.ts @@ -34,6 +34,6 @@ describe('AddToCart', () => { (wrapper.vm as any).addToCart(product); - expect(storeMock.modules.cart.actions.addItem).toBeCalledWith(expect.anything(), { productToAdd: product }, undefined); + expect(storeMock.modules.cart.actions.addItem).toBeCalledWith(expect.anything(), { productToAdd: product }); }) }); diff --git a/core/modules/cart/test/unit/components/Microcart.spec.ts b/core/modules/cart/test/unit/components/Microcart.spec.ts index 4b0920ef2..5c3c30ec8 100644 --- a/core/modules/cart/test/unit/components/Microcart.spec.ts +++ b/core/modules/cart/test/unit/components/Microcart.spec.ts @@ -96,7 +96,7 @@ describe('Microcart', () => { (wrapper.vm as any).applyCoupon(couponCode); - expect(storeMock.modules.cart.actions.applyCoupon).toBeCalledWith(expect.anything(), 'foo', undefined); + expect(storeMock.modules.cart.actions.applyCoupon).toBeCalledWith(expect.anything(), 'foo'); }); it('removeCoupon dispatches removeCoupon action to delete it', () => { diff --git a/core/modules/cart/test/unit/components/Product.spec.ts b/core/modules/cart/test/unit/components/Product.spec.ts index 178a04164..220d5528d 100644 --- a/core/modules/cart/test/unit/components/Product.spec.ts +++ b/core/modules/cart/test/unit/components/Product.spec.ts @@ -13,6 +13,7 @@ jest.mock('@vue-storefront/i18n', () => ({ t: jest.fn(str => str) })); jest.mock('@vue-storefront/core/app', () => jest.fn()) jest.mock('@vue-storefront/core/lib/multistore', () => jest.fn()) jest.mock('@vue-storefront/core/lib/storage-manager', () => jest.fn()) +jest.mock('@vue-storefront/core/store', () => ({})) describe('MicrocartProduct', () => { beforeEach(() => { @@ -89,7 +90,7 @@ describe('MicrocartProduct', () => { (wrapper.vm as any).removeFromCart(); - expect(storeMock.modules.cart.actions.removeItem).toBeCalledWith(expect.anything(), { product }, undefined); + expect(storeMock.modules.cart.actions.removeItem).toBeCalledWith(expect.anything(), { product }); }); it('updateQuantity dispatches updateQuantity update product quantity in cart', () => { @@ -112,8 +113,7 @@ describe('MicrocartProduct', () => { expect(storeMock.modules.cart.actions.updateQuantity).toBeCalledWith( expect.anything(), - { product, qty }, - undefined + { product, qty } ); }); }); diff --git a/core/modules/cart/test/unit/helpers/prepareProductsToAdd.spec.ts b/core/modules/cart/test/unit/helpers/prepareProductsToAdd.spec.ts index 11506aec8..4af8c9556 100644 --- a/core/modules/cart/test/unit/helpers/prepareProductsToAdd.spec.ts +++ b/core/modules/cart/test/unit/helpers/prepareProductsToAdd.spec.ts @@ -19,7 +19,7 @@ const createProduct = ({ type_id }): CartItem => ({ describe('Cart prepareProductsToAdd', () => { it('returns associated products', async () => { const product = createProduct({ type_id: 'grouped' }) - expect(prepareProductsToAdd(product)).toEqual([{ sku: 'SK-001' }]) + expect(prepareProductsToAdd(product)).toEqual([{ sku: 'SK-001', checksum: 'some checksum' }]) }); it('returns products with checksum applied', async () => { diff --git a/core/modules/cart/test/unit/helpers/productChecksum.spec.ts b/core/modules/cart/test/unit/helpers/productChecksum.spec.ts index f3a5c9a9d..3ed37bda0 100644 --- a/core/modules/cart/test/unit/helpers/productChecksum.spec.ts +++ b/core/modules/cart/test/unit/helpers/productChecksum.spec.ts @@ -5,14 +5,8 @@ const configurableProduct: CartItem = { product_option: { extension_attributes: { configurable_item_options: [ - { - option_id: '93', - option_value: 53 - }, - { - option_id: '142', - option_value: 169 - } + { option_id: '93', option_value: '53' }, + { option_id: '142', option_value: '169' } ] } } @@ -22,26 +16,10 @@ const bundleProduct: CartItem = { product_option: { extension_attributes: { bundle_options: [ - { - option_id: 1, - option_qty: 1, - option_selections: [2] - }, - { - option_id: 2, - option_qty: 1, - option_selections: [4] - }, - { - option_id: 3, - option_qty: 1, - option_selections: [5] - }, - { - option_id: 4, - option_qty: 1, - option_selections: [8] - } + { option_id: '1', option_qty: '1', option_selections: [ '2' ] }, + { option_id: '2', option_qty: '1', option_selections: [ '4' ] }, + { option_id: '3', option_qty: '1', option_selections: [ '5' ] }, + { option_id: '4', option_qty: '1', option_selections: [ '8' ] } ] } } @@ -49,10 +27,10 @@ const bundleProduct: CartItem = { describe('Cart productChecksum', () => { it('returns checksum for bundle product', async () => { - expect(productChecksum(bundleProduct)).toBe('d8ba5d5baf59fe28647d6a08fdaeb683a7b39ccdebc77eecabc6457c'); + expect(productChecksum(bundleProduct)).toBe('3e183f026489207a9cd535d20f141e07ddfea729af58a9088b82612f'); }); it('returns checksum for configurable product', async () => { - expect(productChecksum(configurableProduct)).toBe('0bbb27ec7a3cb5dfd1d3f6c4ee54c8b522c4063fe6ea0571794d446f'); + expect(productChecksum(configurableProduct)).toBe('357e8f9f8918873f12ed993c3073ddd3e8980c933034b5e2fdab10b6'); }); }); diff --git a/core/modules/cart/test/unit/helpers/productEquals.spec.ts b/core/modules/cart/test/unit/helpers/productEquals.spec.ts index bb9fe4eab..d54957961 100644 --- a/core/modules/cart/test/unit/helpers/productEquals.spec.ts +++ b/core/modules/cart/test/unit/helpers/productEquals.spec.ts @@ -37,6 +37,27 @@ const createBundleProduct = ({ id, sku, type_id, options }): CartItem => ({ } } as any as CartItem) +const createCustomOptions = (options) => { + if (!options) { + return [] + } + + return options.map((option, index) => ({ + option_id: index + 1, + option_value: option + })) +} + +const createCustomOptionsProduct = ({ id, sku, options }): CartItem => ({ + sku, + server_item_id: id, + product_option: { + extension_attributes: { + custom_options: createCustomOptions(options) + } + } +} as any as CartItem) + const createConfigurableProduct = ({ id, sku }): CartItem => ({ sku, type_id: 'configurable', @@ -58,31 +79,58 @@ const createConfigurableProduct = ({ id, sku }): CartItem => ({ } as any as CartItem) describe('Cart productEquals', () => { - it('returns true because bundle products have the same options selected', async () => { - const product1 = createBundleProduct({ id: 1, sku: 'WG-001', type_id: 'bundle', options: [2, 4, 5, 8] }) - const product2 = createBundleProduct({ id: 2, sku: 'WG-001', type_id: 'bundle', options: [2, 4, 5, 8] }) + describe('bundle product', () => { + it('returns true because products have the same options selected', async () => { + const product1 = createBundleProduct({ id: 1, sku: 'WG-001', type_id: 'bundle', options: [2, 4, 5, 8] }) + const product2 = createBundleProduct({ id: 2, sku: 'WG-001', type_id: 'bundle', options: [2, 4, 5, 8] }) + + expect(productsEquals(product1, product2)).toBeTruthy() + }); + + it('returns true because products have the same server id', async () => { + const product1 = createBundleProduct({ id: 1, sku: 'WG-001', type_id: 'bundle', options: null }) + const product2 = createBundleProduct({ id: 1, sku: 'WG-001', type_id: 'none', options: [2, 4, 5, 8] }) + + expect(productsEquals(product1, product2)).toBeTruthy() + }); + + it('returns false because products have not the same options selected', async () => { + const product1 = createBundleProduct({ id: 1, sku: 'WG-001', type_id: 'bundle', options: [2, 2, 5, 8] }) + const product2 = createBundleProduct({ id: 2, sku: 'WG-001', type_id: 'bundle', options: [2, 4, 5, 8] }) + + expect(productsEquals(product1, product2)).toBeFalsy() + }); + }) + + describe('custom options product', () => { + it('returns true because products have the same options selected', async () => { + const product1 = createCustomOptionsProduct({ id: 1, sku: 'WG-001', options: [2, 4, 5, 8] }) + const product2 = createCustomOptionsProduct({ id: 2, sku: 'WG-001', options: [2, 4, 5, 8] }) + + expect(productsEquals(product1, product2)).toBeTruthy() + }); - expect(productsEquals(product1, product2)).toBeTruthy() - }); - - it('returns true because bundle products have not the same options selected', async () => { - const product1 = createBundleProduct({ id: 1, sku: 'WG-001', type_id: 'bundle', options: [2, 2, 5, 8] }) - const product2 = createBundleProduct({ id: 2, sku: 'WG-001', type_id: 'bundle', options: [2, 4, 5, 8] }) + it('returns true because products have the same server id', async () => { + const product1 = createCustomOptionsProduct({ id: 1, sku: 'WG-001', options: null }) + const product2 = createCustomOptionsProduct({ id: 1, sku: 'WG-001', options: [2, 4, 5, 8] }) - expect(productsEquals(product1, product2)).toBeTruthy() - }); + expect(productsEquals(product1, product2)).toBeTruthy() + }); - it('returns true because bundle products have the same server id', async () => { - const product1 = createBundleProduct({ id: 1, sku: 'WG-001', type_id: 'bundle', options: null }) - const product2 = createBundleProduct({ id: 1, sku: 'WG-001', type_id: 'none', options: [2, 4, 5, 8] }) + it('returns false because products have not the same options selected', async () => { + const product1 = createCustomOptionsProduct({ id: 1, sku: 'WG-001', options: [2, 2, 5, 8] }) + const product2 = createCustomOptionsProduct({ id: 2, sku: 'WG-001', options: [2, 4, 5, 8] }) - expect(productsEquals(product1, product2)).toBeTruthy() - }); + expect(productsEquals(product1, product2)).toBeFalsy() + }); + }) - it('returns true because configurable products have the same sku', async () => { - const product1 = createConfigurableProduct({ id: 1, sku: 'WG-001' }) - const product2 = createConfigurableProduct({ id: 2, sku: 'WG-001' }) + describe('configurable product', () => { + it('returns true because products have the same sku', async () => { + const product1 = createConfigurableProduct({ id: 1, sku: 'WG-001' }) + const product2 = createConfigurableProduct({ id: 2, sku: 'WG-001' }) - expect(productsEquals(product1, product2)).toBeTruthy() - }); + expect(productsEquals(product1, product2)).toBeTruthy() + }); + }) }); diff --git a/core/modules/cart/test/unit/index.spec.ts b/core/modules/cart/test/unit/index.spec.ts index 4b00340d6..0e405fcc6 100644 --- a/core/modules/cart/test/unit/index.spec.ts +++ b/core/modules/cart/test/unit/index.spec.ts @@ -7,6 +7,7 @@ jest.mock('@vue-storefront/core/helpers', () => ({ isServer: false })) jest.mock('@vue-storefront/core/lib/storage-manager', () => ({ initCacheStorage: jest.fn() })); jest.mock('@vue-storefront/i18n', () => ({ t: jest.fn(str => str) })); jest.mock('@vue-storefront/core/app', () => jest.fn()) +jest.mock('@vue-storefront/core/store', () => ({})) jest.mock('@vue-storefront/core/lib/multistore', () => ({ currentStoreView: jest.fn(), localizedRoute: jest.fn() diff --git a/core/modules/cart/test/unit/store/connectActions.spec.ts b/core/modules/cart/test/unit/store/connectActions.spec.ts index 7ebd24a6f..fe248332b 100644 --- a/core/modules/cart/test/unit/store/connectActions.spec.ts +++ b/core/modules/cart/test/unit/store/connectActions.spec.ts @@ -48,22 +48,55 @@ jest.mock('@vue-storefront/core/helpers', () => ({ })); describe('Cart connectActions', () => { - it('clears cart token and server hash', async () => { - const contextMock = createContextMock({ - getters: { - isCartSyncEnabled: true - } - }) - config.orders = { - directBackendSync: false - } + it('clear deletes all cart products and token', async () => { + const contextMock = { + commit: jest.fn(), + dispatch: jest.fn(), + getters: { isCartSyncEnabled: false } + }; + const wrapper = (actions: any) => actions.clear(contextMock); + config.cart = { synchronize: false }; - await (cartActions as any).clear(contextMock) + await wrapper(cartActions); expect(contextMock.commit).toHaveBeenNthCalledWith(1, types.CART_LOAD_CART, []); - expect(contextMock.commit).toHaveBeenNthCalledWith(2, types.CART_LOAD_CART_SERVER_TOKEN, null); - expect(contextMock.commit).toHaveBeenNthCalledWith(3, types.CART_SET_ITEMS_HASH, null); - }) + expect(contextMock.dispatch).toHaveBeenNthCalledWith(1, 'sync', { forceClientState: true, forceSync: true }); + expect(contextMock.commit).toHaveBeenNthCalledWith(2, types.CART_SET_ITEMS_HASH, null); + expect(contextMock.dispatch).toHaveBeenNthCalledWith(2, 'disconnect'); + }); + + it('clear deletes all cart products but keep token', async () => { + const contextMock = { + commit: jest.fn(), + dispatch: jest.fn(), + getters: { isCartSyncEnabled: false } + }; + const wrapper = (actions: any) => actions.clear(contextMock, { disconnect: false }); + + config.cart = { synchronize: false }; + + await wrapper(cartActions); + + expect(contextMock.commit).toHaveBeenNthCalledWith(1, types.CART_LOAD_CART, []); + expect(contextMock.dispatch).toHaveBeenNthCalledWith(1, 'sync', { forceClientState: true, forceSync: true }); + }); + + it('clear deletes all cart products and token, but not sync with backend', async () => { + const contextMock = { + commit: jest.fn(), + dispatch: jest.fn(), + getters: { isCartSyncEnabled: false } + }; + const wrapper = (actions: any) => actions.clear(contextMock, { sync: false }); + + config.cart = { synchronize: false }; + + await wrapper(cartActions); + + expect(contextMock.commit).toHaveBeenNthCalledWith(1, types.CART_LOAD_CART, []); + expect(contextMock.commit).toHaveBeenNthCalledWith(2, types.CART_SET_ITEMS_HASH, null); + expect(contextMock.dispatch).toHaveBeenNthCalledWith(1, 'disconnect'); + }); it('disconnects cart', async () => { const contextMock = createContextMock() @@ -85,7 +118,7 @@ describe('Cart connectActions', () => { }) await (cartActions as any).authorize(contextMock) - expect(contextMock.dispatch).toHaveBeenNthCalledWith(1, 'connect', { guestCart: false }); + expect(contextMock.dispatch).toHaveBeenNthCalledWith(1, 'connect', { guestCart: false, mergeQty: true }); }) it('creates cart token', async () => { @@ -105,7 +138,7 @@ describe('Cart connectActions', () => { await (cartActions as any).connect(contextMock, {}) expect(contextMock.commit).toBeCalledWith(types.CART_LOAD_CART_SERVER_TOKEN, 'server-cart-token') - expect(contextMock.dispatch).toBeCalledWith('sync', { forceClientState: false, dryRun: true, mergeQty: true }) + expect(contextMock.dispatch).toBeCalledWith('sync', { forceClientState: false, dryRun: true, mergeQty: false }) }) it('attempts bypassing guest cart', async () => { diff --git a/core/modules/cart/test/unit/store/getters.spec.ts b/core/modules/cart/test/unit/store/getters.spec.ts index 166c6b69d..3eb451710 100644 --- a/core/modules/cart/test/unit/store/getters.spec.ts +++ b/core/modules/cart/test/unit/store/getters.spec.ts @@ -6,6 +6,7 @@ jest.mock('@vue-storefront/i18n', () => ({ t: jest.fn(str => str) })); jest.mock('@vue-storefront/core/lib/storage-manager', () => jest.fn()) jest.mock('@vue-storefront/core/app', () => jest.fn()) jest.mock('@vue-storefront/core/lib/multistore', () => jest.fn()) +jest.mock('@vue-storefront/core/store', () => ({})) jest.mock('@vue-storefront/core/helpers', () => ({ onlineHelper: { get isOnline () { diff --git a/core/modules/catalog-next/store/category/getters.ts b/core/modules/catalog-next/store/category/getters.ts index 364142bdb..a7463a735 100644 --- a/core/modules/catalog-next/store/category/getters.ts +++ b/core/modules/catalog-next/store/category/getters.ts @@ -9,6 +9,7 @@ import { optionLabel } from '../../helpers/optionLabel' import trim from 'lodash-es/trim' import toString from 'lodash-es/toString' import forEach from 'lodash-es/forEach' +import get from 'lodash-es/get' import { getFiltersFromQuery } from '../../helpers/filterHelpers' import { Category } from '../../types/Category' import { parseCategoryPath } from '@vue-storefront/core/modules/breadcrumbs/helpers' @@ -42,7 +43,7 @@ const getters: GetterTree = { return valueCheck.filter(check => check === true).length === Object.keys(searchOptions).length }) || {} }, - getCurrentCategory: (state, getters, rootState) => { + getCurrentCategory: (state, getters, rootState, rootGetters) => { return getters.getCategoryByParams(rootState.route.params) }, getAvailableFiltersFrom: (state, getters, rootState) => (aggregations) => { @@ -110,7 +111,10 @@ const getters: GetterTree = { return filters }, getFiltersMap: state => state.filtersMap, - getAvailableFilters: (state, getters) => getters.getCurrentCategory ? state.filtersMap[getters.getCurrentCategory.id] : {}, + getAvailableFilters: (state, getters) => { + const categoryId = get(getters.getCurrentCategory, 'id', null) + return state.filtersMap[categoryId] || {} + }, getCurrentFiltersFrom: (state, getters, rootState) => (filters, categoryFilters) => { const currentQuery = filters || rootState.route[products.routerFiltersSource] const availableFilters = categoryFilters || getters.getAvailableFilters diff --git a/core/modules/catalog-next/store/category/mutations.ts b/core/modules/catalog-next/store/category/mutations.ts index c391af5f4..7dc398d08 100644 --- a/core/modules/catalog-next/store/category/mutations.ts +++ b/core/modules/catalog-next/store/category/mutations.ts @@ -34,7 +34,7 @@ const mutations: MutationTree = { state.notFoundCategoryIds = [...state.notFoundCategoryIds, ...categoryIds] }, [types.CATEGORY_SET_CATEGORY_FILTERS] (state, {category, filters}) { - state.filtersMap[category.id] = filters + Vue.set(state.filtersMap, category.id, filters) }, [types.CATEGORY_SET_SEARCH_PRODUCTS_STATS] (state, stats = {}) { state.searchProductsStats = stats diff --git a/core/modules/catalog/helpers/createAttributesListQuery.ts b/core/modules/catalog/helpers/createAttributesListQuery.ts index a8d9c930f..3dd25310c 100644 --- a/core/modules/catalog/helpers/createAttributesListQuery.ts +++ b/core/modules/catalog/helpers/createAttributesListQuery.ts @@ -17,10 +17,10 @@ const createAttributesListQuery = ({ searchQuery = searchQuery.applyFilter({key: filterField, value: {'in': filterValues}}) } if (onlyDefinedByUser) { - searchQuery = searchQuery.applyFilter({key: 'is_user_defined', value: {'in': [true]}}) + searchQuery = searchQuery.applyFilter({key: 'is_user_defined', value: {'in': [true, '1']}}) } if (onlyVisible) { - searchQuery = searchQuery.applyFilter({key: 'is_visible', value: {'in': [true]}}) + searchQuery = searchQuery.applyFilter({key: 'is_visible', value: {'in': [true, '1']}}) } return searchQuery diff --git a/core/modules/catalog/helpers/prefetchCachedAttributes.ts b/core/modules/catalog/helpers/prefetchCachedAttributes.ts index 174eb73ac..6a40b10cf 100644 --- a/core/modules/catalog/helpers/prefetchCachedAttributes.ts +++ b/core/modules/catalog/helpers/prefetchCachedAttributes.ts @@ -6,7 +6,7 @@ async function prefetchCachedAttributes (filterField, filterValues) { if (!config.attributes || !config.attributes.disablePersistentAttributesCache) { const attrCollection = StorageManager.get('attributes') const cachedAttributes = filterValues.map( - async filterValue => attrCollection.getItem(entityKeyName(filterField, filterValue.toLowerCase())) + async filterValue => attrCollection.getItem(entityKeyName(filterField, String(filterValue).toLowerCase())) ) return Promise.all(cachedAttributes) } diff --git a/core/modules/catalog/helpers/slugifyCategories.ts b/core/modules/catalog/helpers/slugifyCategories.ts index 73266fb8d..b8eb6a08f 100644 --- a/core/modules/catalog/helpers/slugifyCategories.ts +++ b/core/modules/catalog/helpers/slugifyCategories.ts @@ -3,30 +3,21 @@ import { slugify } from '@vue-storefront/core/helpers' import { Category, ChildrenData } from '@vue-storefront/core/modules/catalog-next/types/Category' const createSlug = (category: ChildrenData): string => { - if (category.slug) { - return category.slug - } - if (category.url_key && config.products.useMagentoUrlKeys) { return category.url_key } - if (category.name) { - return `${slugify(category.name)}-${category.id}` - } - - return '' + return `${slugify(category.name)}-${category.id}` } const slugifyCategories = (category: Category | ChildrenData): Category | ChildrenData => { if (category.children_data) { for (let subcat of category.children_data) { - if (subcat.name) { - return slugifyCategories({ ...subcat, slug: createSlug(subcat) } as any as ChildrenData) + if (subcat.name && !subcat.slug) { + slugifyCategories({...subcat, slug: createSlug(subcat)} as any as ChildrenData) } } } - return category } diff --git a/core/modules/catalog/helpers/taxCalc.ts b/core/modules/catalog/helpers/taxCalc.ts index bf8795f8a..b705ae2cf 100644 --- a/core/modules/catalog/helpers/taxCalc.ts +++ b/core/modules/catalog/helpers/taxCalc.ts @@ -167,15 +167,16 @@ export function updateProductPrices ({ product, rate, sourcePriceInclTax = false export function calculateProductTax ({ product, taxClasses, taxCountry = 'PL', taxRegion = '', sourcePriceInclTax = false, deprecatedPriceFieldsSupport = false, finalPriceInclTax = true, userGroupId = null, isTaxWithUserGroupIsActive }) { let rateFound = false - if (product.tax_class_id > 0) { + let product_tax_class_id = parseInt(product.tax_class_id) + if (product_tax_class_id > 0) { let taxClass if (isTaxWithUserGroupIsActive) { taxClass = taxClasses.find((el) => - el.product_tax_class_ids.indexOf(parseInt(product.tax_class_id)) >= 0 && + el.product_tax_class_ids.indexOf(product_tax_class_id) >= 0 && el.customer_tax_class_ids.indexOf(userGroupId) >= 0 ) } else { - taxClass = taxClasses.find((el) => el.product_tax_class_ids.indexOf(parseInt(product.tax_class_id) >= 0)) + taxClass = taxClasses.find((el) => el.product_tax_class_ids.indexOf(product_tax_class_id) >= 0) } if (taxClass) { diff --git a/core/modules/catalog/store/attribute/getters.ts b/core/modules/catalog/store/attribute/getters.ts index bda43dcc0..13a87bce0 100644 --- a/core/modules/catalog/store/attribute/getters.ts +++ b/core/modules/catalog/store/attribute/getters.ts @@ -13,7 +13,7 @@ const getters: GetterTree = { getBlacklist: (state) => state.blacklist, getAllComparableAttributes: (state, getters) => { const attributesByCode = getters.getAttributeListByCode - return Object.values(attributesByCode).filter((a: any) => ["1", true].includes(a.is_comparable)) //In some cases we get boolean instead of "0"/"1" that why we support both options + return Object.values(attributesByCode).filter((a: any) => ['1', true].includes(a.is_comparable)) // In some cases we get boolean instead of "0"/"1" that why we support both options } } diff --git a/core/modules/checkout/components/OrderReview.ts b/core/modules/checkout/components/OrderReview.ts index 4118bb19b..b350ba89d 100644 --- a/core/modules/checkout/components/OrderReview.ts +++ b/core/modules/checkout/components/OrderReview.ts @@ -55,8 +55,8 @@ export const OrderReview = { }] }) - this.$bus.$emit('notification-progress-stop') if (result.code !== 200) { + this.$bus.$emit('notification-progress-stop') this.onFailure(result) // If error includes a word 'password', emit event that eventually focuses on a corresponding field if (result.result.includes(i18n.t('password'))) { @@ -72,6 +72,7 @@ export const OrderReview = { username: this.getPersonalDetails.emailAddress, password: this.getPersonalDetails.password }) + this.$bus.$emit('notification-progress-stop') this.$bus.$emit('checkout-before-placeOrder', result.result.id) this.onSuccess() } diff --git a/core/modules/checkout/components/Payment.ts b/core/modules/checkout/components/Payment.ts index f22cf22a2..c797b862e 100644 --- a/core/modules/checkout/components/Payment.ts +++ b/core/modules/checkout/components/Payment.ts @@ -244,7 +244,9 @@ export const Payment = { } // Let anyone listening know that we've changed payment method, usually a payment extension. - this.$bus.$emit('checkout-payment-method-changed', this.payment.paymentMethod) + if (this.payment.paymentMethod) { + this.$bus.$emit('checkout-payment-method-changed', this.payment.paymentMethod) + } }, changeCountry () { this.$store.dispatch('checkout/updatePaymentDetails', { country: this.payment.country }) diff --git a/core/modules/checkout/store/checkout/actions.ts b/core/modules/checkout/store/checkout/actions.ts index bd26d0807..753ebb67b 100644 --- a/core/modules/checkout/store/checkout/actions.ts +++ b/core/modules/checkout/store/checkout/actions.ts @@ -11,7 +11,8 @@ const actions: ActionTree = { const result = await dispatch('order/placeOrder', order, { root: true }) if (!result.resultCode || result.resultCode === 200) { await dispatch('updateOrderTimestamp') - await dispatch('cart/clear', null, { root: true }) + // clear cart without sync, because after order cart will be already cleared on backend + await dispatch('cart/clear', { sync: false }, {root: true}) await dispatch('dropPassword') } } catch (e) { diff --git a/core/modules/checkout/test/unit/components/OrderReview.spec.ts b/core/modules/checkout/test/unit/components/OrderReview.spec.ts index d99fad172..69e005d1e 100644 --- a/core/modules/checkout/test/unit/components/OrderReview.spec.ts +++ b/core/modules/checkout/test/unit/components/OrderReview.spec.ts @@ -186,7 +186,7 @@ describe('OrderReview', () => { telephone: 'example phone number', default_shipping: true }] - }, undefined); + }); }); it('emits events about start and stop of notification progress', async () => { @@ -285,7 +285,7 @@ describe('OrderReview', () => { expect(mockStore.modules.user.actions.login).toHaveBeenCalledWith(expect.anything(), { username: 'example email address', password: 'example password' - }, undefined); + }); }); }); }); diff --git a/core/modules/checkout/test/unit/components/Payment.spec.ts b/core/modules/checkout/test/unit/components/Payment.spec.ts index f61a24353..921ebe3b6 100644 --- a/core/modules/checkout/test/unit/components/Payment.spec.ts +++ b/core/modules/checkout/test/unit/components/Payment.spec.ts @@ -587,8 +587,11 @@ describe('Payment', () => { expect((wrapper.vm as any).notInMethods('invalid payment method')).toBe(true); }); - it('changePaymentMethod method should emit an event', () => { + it('changePaymentMethod method should emit an event when there is paymentMethod', () => { mockMethods['changePaymentMethod'].mockRestore(); + mockStore.modules.checkout.getters.getPaymentDetails.mockImplementation(() => ({ + paymentMethod: 'payment method' + })); const wrapper = mountMixinWithStore(Payment, mockStore, mockMountingOptions); (wrapper.vm as any).changePaymentMethod(); diff --git a/core/modules/checkout/test/unit/components/PersonalDetails.spec.ts b/core/modules/checkout/test/unit/components/PersonalDetails.spec.ts index 80b568682..7ff6a46b1 100644 --- a/core/modules/checkout/test/unit/components/PersonalDetails.spec.ts +++ b/core/modules/checkout/test/unit/components/PersonalDetails.spec.ts @@ -1,5 +1,6 @@ import { mountMixinWithStore } from '@vue-storefront/unit-tests/utils'; import { PersonalDetails } from '../../../components/PersonalDetails'; +import Vue from 'vue' describe('PersonalDetails', () => { let mockStore; @@ -74,13 +75,15 @@ describe('PersonalDetails', () => { expect(mockMountingOptions.mocks.$bus.$off).toHaveBeenCalledWith('user-after-loggedin', (wrapper.vm as any).onLoggedIn); }); - it('updated hook should set focus on password field', () => { + it('updated hook should set focus on password field', async () => { const wrapper = mountMixinWithStore(PersonalDetails, mockStore, mockMountingOptions); (wrapper.vm as any).$refs.password = { setFocus: jest.fn() }; wrapper.setData({ isValidationError: false }); wrapper.setProps({ focusedField: 'password' }); + await Vue.nextTick() + expect((wrapper.vm as any).isValidationError).toBe(true); expect((wrapper.vm as any).password).toBe(''); expect((wrapper.vm as any).rPassword).toBe(''); diff --git a/core/modules/checkout/test/unit/components/Shipping.spec.ts b/core/modules/checkout/test/unit/components/Shipping.spec.ts index 7b004cb05..d2ddfae15 100644 --- a/core/modules/checkout/test/unit/components/Shipping.spec.ts +++ b/core/modules/checkout/test/unit/components/Shipping.spec.ts @@ -216,9 +216,9 @@ describe('Shipping', () => { (wrapper.vm as any).onAfterPersonalDetails(personalData); expect(mockStore.modules.checkout.actions.updatePropValue) - .toHaveBeenCalledWith(expect.anything(), ['firstName', 'example first name'], undefined); + .toHaveBeenCalledWith(expect.anything(), ['firstName', 'example first name']); expect(mockStore.modules.checkout.actions.updatePropValue) - .toHaveBeenCalledWith(expect.anything(), ['lastName', 'example last name'], undefined); + .toHaveBeenCalledWith(expect.anything(), ['lastName', 'example last name']); }); it('sendDataToCheckout should emit event', () => { diff --git a/core/modules/checkout/test/unit/store/checkout/actions.spec.ts b/core/modules/checkout/test/unit/store/checkout/actions.spec.ts index 273f6bbad..df3d7b8bb 100644 --- a/core/modules/checkout/test/unit/store/checkout/actions.spec.ts +++ b/core/modules/checkout/test/unit/store/checkout/actions.spec.ts @@ -43,7 +43,7 @@ describe('Checkout actions', () => { expect(mockContext.dispatch).toHaveBeenCalledTimes(4); expect(mockContext.dispatch).toHaveBeenNthCalledWith(1, 'order/placeOrder', order, { root: true }); expect(mockContext.dispatch).toHaveBeenNthCalledWith(2, 'updateOrderTimestamp'); - expect(mockContext.dispatch).toHaveBeenNthCalledWith(3, 'cart/clear', null, { root: true }); + expect(mockContext.dispatch).toHaveBeenNthCalledWith(3, 'cart/clear', { sync: false }, { root: true }); expect(mockContext.dispatch).toHaveBeenNthCalledWith(4, 'dropPassword'); }); @@ -54,7 +54,7 @@ describe('Checkout actions', () => { expect(mockContext.dispatch).toHaveBeenCalledTimes(4); expect(mockContext.dispatch).toHaveBeenNthCalledWith(1, 'order/placeOrder', order, { root: true }); expect(mockContext.dispatch).toHaveBeenNthCalledWith(2, 'updateOrderTimestamp'); - expect(mockContext.dispatch).toHaveBeenNthCalledWith(3, 'cart/clear', null, { root: true }); + expect(mockContext.dispatch).toHaveBeenNthCalledWith(3, 'cart/clear', { sync: false }, { root: true }); expect(mockContext.dispatch).toHaveBeenNthCalledWith(4, 'dropPassword'); }); @@ -65,7 +65,7 @@ describe('Checkout actions', () => { expect(mockContext.dispatch).toHaveBeenCalledTimes(1); expect(mockContext.dispatch).toHaveBeenNthCalledWith(1, 'order/placeOrder', order, { root: true }); expect(mockContext.dispatch).not.toHaveBeenNthCalledWith(2, 'updateOrderTimestamp'); - expect(mockContext.dispatch).not.toHaveBeenNthCalledWith(3, 'cart/clear', null, { root: true }); + expect(mockContext.dispatch).not.toHaveBeenNthCalledWith(3, 'cart/clear', { sync: false }, { root: true }); expect(mockContext.dispatch).not.toHaveBeenNthCalledWith(4, 'dropPassword'); }); @@ -76,7 +76,7 @@ describe('Checkout actions', () => { expect(mockContext.dispatch).toHaveBeenCalledTimes(1); expect(mockContext.dispatch).toHaveBeenNthCalledWith(1, 'order/placeOrder', order, { root: true }); expect(mockContext.dispatch).not.toHaveBeenNthCalledWith(2, 'updateOrderTimestamp'); - expect(mockContext.dispatch).not.toHaveBeenNthCalledWith(3, 'cart/clear', null, { root: true }); + expect(mockContext.dispatch).not.toHaveBeenNthCalledWith(3, 'cart/clear', { sync: false }, { root: true }); expect(mockContext.dispatch).not.toHaveBeenNthCalledWith(4, 'dropPassword'); expect(Logger.error).toHaveBeenCalled(); }); diff --git a/core/modules/compare/test/unit/components/AddToCompare.spec.ts b/core/modules/compare/test/unit/components/AddToCompare.spec.ts index 64c5cd77c..a1b8feb4f 100644 --- a/core/modules/compare/test/unit/components/AddToCompare.spec.ts +++ b/core/modules/compare/test/unit/components/AddToCompare.spec.ts @@ -31,7 +31,7 @@ describe('AddToCompare', () => { (wrapper.vm as any).addToCompare(product); - expect(storeMock.modules.compare.actions.addItem).toBeCalledWith(expect.anything(), product, undefined); + expect(storeMock.modules.compare.actions.addItem).toBeCalledWith(expect.anything(), product); }) it('compare module has been registered on created', () => { diff --git a/core/modules/compare/test/unit/components/Compare.spec.ts b/core/modules/compare/test/unit/components/Compare.spec.ts index 3c1462f51..e31b56e62 100644 --- a/core/modules/compare/test/unit/components/Compare.spec.ts +++ b/core/modules/compare/test/unit/components/Compare.spec.ts @@ -24,7 +24,7 @@ describe('Compare', () => { expect(storeMock.modules.attribute.actions.list).toBeCalledWith(expect.anything(), { filterValues: [], filterField: 'is_user_defined' - }, undefined); + }); }) it('removeFromCompare dispatches addItem action', () => { @@ -51,6 +51,6 @@ describe('Compare', () => { (wrapper.vm as any).removeFromCompare(product); - expect(storeMock.modules.compare.actions.removeItem).toBeCalledWith(expect.anything(), product, undefined); + expect(storeMock.modules.compare.actions.removeItem).toBeCalledWith(expect.anything(), product); }) }); diff --git a/core/modules/compare/test/unit/components/Product.spec.ts b/core/modules/compare/test/unit/components/Product.spec.ts index 18902be2d..b60a930c3 100644 --- a/core/modules/compare/test/unit/components/Product.spec.ts +++ b/core/modules/compare/test/unit/components/Product.spec.ts @@ -25,6 +25,6 @@ describe('Product', () => { (wrapper.vm as any).removeFromCompare(product); - expect(storeMock.modules.compare.actions.removeItem).toBeCalledWith(expect.anything(), product, undefined); + expect(storeMock.modules.compare.actions.removeItem).toBeCalledWith(expect.anything(), product); }) }); diff --git a/core/modules/newsletter/test/unit/mixins/Subscribe.spec.ts b/core/modules/newsletter/test/unit/mixins/Subscribe.spec.ts index 9f4ad3354..ff29a25b1 100644 --- a/core/modules/newsletter/test/unit/mixins/Subscribe.spec.ts +++ b/core/modules/newsletter/test/unit/mixins/Subscribe.spec.ts @@ -34,7 +34,7 @@ describe('Subscribe', () => { (wrapper.vm as any).subscribe() - expect(storeMock.modules.newsletter.actions.subscribe).toBeCalledWith(expect.anything(), '', undefined); + expect(storeMock.modules.newsletter.actions.subscribe).toBeCalledWith(expect.anything(), ''); }) it('method subscribe dispatches subscription action successfully with success Callback', async () => { @@ -61,7 +61,7 @@ describe('Subscribe', () => { await (wrapper.vm as any).subscribe(successCallback) - expect(storeMock.modules.newsletter.actions.subscribe).toBeCalledWith(expect.anything(), '', undefined); + expect(storeMock.modules.newsletter.actions.subscribe).toBeCalledWith(expect.anything(), ''); expect(successCallback).toBeCalledWith(true) }) @@ -87,7 +87,7 @@ describe('Subscribe', () => { await (wrapper.vm as any).subscribe(() => {}) - expect(storeMock.modules.newsletter.actions.subscribe).toBeCalledWith(expect.anything(), '', undefined); + expect(storeMock.modules.newsletter.actions.subscribe).toBeCalledWith(expect.anything(), ''); }) it('method subscribe dispatches subscription action that fails given an error handler', async () => { @@ -114,7 +114,7 @@ describe('Subscribe', () => { await (wrapper.vm as any).subscribe(() => {}, errorCallback) - expect(storeMock.modules.newsletter.actions.subscribe).toBeCalledWith(expect.anything(), '', undefined); + expect(storeMock.modules.newsletter.actions.subscribe).toBeCalledWith(expect.anything(), ''); expect(errorCallback).toBeCalledWith('subscription failed') }) }); diff --git a/core/modules/newsletter/test/unit/mixins/Unsubscribe.spec.ts b/core/modules/newsletter/test/unit/mixins/Unsubscribe.spec.ts index 69e8c7120..ea4cd9928 100644 --- a/core/modules/newsletter/test/unit/mixins/Unsubscribe.spec.ts +++ b/core/modules/newsletter/test/unit/mixins/Unsubscribe.spec.ts @@ -36,7 +36,7 @@ describe('Unsubscribe', () => { await (wrapper.vm as any).unsubscribe() - expect(storeMock.modules.newsletter.actions.unsubscribe).toBeCalledWith(expect.anything(), '', undefined); + expect(storeMock.modules.newsletter.actions.unsubscribe).toBeCalledWith(expect.anything(), ''); expect(emit).toBeCalledWith('unsubscribed', true) }) @@ -64,7 +64,7 @@ describe('Unsubscribe', () => { await (wrapper.vm as any).unsubscribe() - expect(storeMock.modules.newsletter.actions.unsubscribe).toBeCalledWith(expect.anything(), '', undefined); + expect(storeMock.modules.newsletter.actions.unsubscribe).toBeCalledWith(expect.anything(), ''); expect(emit).toBeCalledWith('unsubscription-error', 'error') }) }); diff --git a/core/modules/order/test/unit/store/mutations.spec.ts b/core/modules/order/test/unit/store/mutations.spec.ts index 8dcfeb011..c136841bb 100644 --- a/core/modules/order/test/unit/store/mutations.spec.ts +++ b/core/modules/order/test/unit/store/mutations.spec.ts @@ -1922,5 +1922,4 @@ describe('Order mutations', () => { expect(stateMock).toEqual(expectedState) }) }) - }); diff --git a/core/modules/url/store/actions.ts b/core/modules/url/store/actions.ts index 8cac1ff8a..2532b8d6d 100644 --- a/core/modules/url/store/actions.ts +++ b/core/modules/url/store/actions.ts @@ -9,6 +9,7 @@ import SearchQuery from '@vue-storefront/core/lib/search/searchQuery' import { preProcessDynamicRoutes, normalizeUrlPath, parametrizeRouteData, getFallbackRouteData } from '../helpers' import { removeStoreCodeFromRoute, currentStoreView, localizedDispatcherRouteName } from '@vue-storefront/core/lib/multistore' import storeCodeFromRoute from '@vue-storefront/core/lib/storeCodeFromRoute' +import isEqual from 'lodash-es/isEqual' // it's a good practice for all actions to return Promises with effect of their execution export const actions: ActionTree = { @@ -93,5 +94,10 @@ export const actions: ActionTree = { } } } + }, + setCurrentRoute ({ commit, state }, {to, from} = {}) { + commit(types.SET_CURRENT_ROUTE, to) + commit(types.IS_BACK_ROUTE, isEqual(state.prevRoute, state.currentRoute) && state.currentRoute.path !== from.path) + commit(types.SET_PREV_ROUTE, from) } } diff --git a/core/modules/url/store/getters.ts b/core/modules/url/store/getters.ts new file mode 100644 index 000000000..ee3e1a36c --- /dev/null +++ b/core/modules/url/store/getters.ts @@ -0,0 +1,4 @@ +export const getters = { + getCurrentRoute: (state) => state.currentRoute, + isBackRoute: (state) => state.isBackRoute +} diff --git a/core/modules/url/store/index.ts b/core/modules/url/store/index.ts index 336bade11..315e3fc21 100644 --- a/core/modules/url/store/index.ts +++ b/core/modules/url/store/index.ts @@ -3,10 +3,12 @@ import { UrlState } from '../types/UrlState' import { mutations } from './mutations' import { actions } from './actions' import { state } from './state' +import { getters } from './getters' export const urlStore: Module = { namespaced: true, mutations, actions, - state + state, + getters } diff --git a/core/modules/url/store/mutation-types.ts b/core/modules/url/store/mutation-types.ts index 5f047c63b..7d47ee8ca 100644 --- a/core/modules/url/store/mutation-types.ts +++ b/core/modules/url/store/mutation-types.ts @@ -1 +1,4 @@ export const REGISTER_MAPPING = 'URL/REGISTER_MAPPING' +export const SET_CURRENT_ROUTE = 'URL/SET_CURRENT_ROUTE' +export const SET_PREV_ROUTE = 'URL/SET_PREV_ROUTE' +export const IS_BACK_ROUTE = 'URL/IS_BACK_ROUTE' diff --git a/core/modules/url/store/mutations.ts b/core/modules/url/store/mutations.ts index ed2a7a802..6523420c4 100644 --- a/core/modules/url/store/mutations.ts +++ b/core/modules/url/store/mutations.ts @@ -1,8 +1,18 @@ import { MutationTree } from 'vuex' import * as types from './mutation-types' +import omit from 'lodash-es/omit' export const mutations: MutationTree = { [types.REGISTER_MAPPING] (state, payload) { state.dispatcherMap = Object.assign({}, state.dispatcherMap, { [payload.url]: payload.routeData }) + }, + [types.SET_CURRENT_ROUTE] (state, payload = {}) { + state.currentRoute = omit(payload, ['matched']) + }, + [types.SET_PREV_ROUTE] (state, payload = {}) { + state.prevRoute = omit(payload, ['matched']) + }, + [types.IS_BACK_ROUTE] (state, payload) { + state.isBackRoute = payload } } diff --git a/core/modules/url/store/state.ts b/core/modules/url/store/state.ts index 58a10c29a..bc87f8491 100644 --- a/core/modules/url/store/state.ts +++ b/core/modules/url/store/state.ts @@ -1,5 +1,8 @@ import { UrlState } from '../types/UrlState' export const state: UrlState = { - dispatcherMap: {} + dispatcherMap: {}, + currentRoute: {}, + prevRoute: {}, + isBackRoute: false } diff --git a/core/modules/url/types/UrlState.ts b/core/modules/url/types/UrlState.ts index fba3babff..20c65b976 100644 --- a/core/modules/url/types/UrlState.ts +++ b/core/modules/url/types/UrlState.ts @@ -1,7 +1,11 @@ +import { Route } from 'vue-router'; 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) export interface UrlState { - dispatcherMap: { [path: string]: LocalizedRoute} + dispatcherMap: { [path: string]: LocalizedRoute}, + currentRoute: Partial, + prevRoute: Partial, + isBackRoute: boolean } diff --git a/core/modules/user/store/actions.ts b/core/modules/user/store/actions.ts index d5899743e..1b13787d0 100644 --- a/core/modules/user/store/actions.ts +++ b/core/modules/user/store/actions.ts @@ -126,7 +126,7 @@ const actions: ActionTree = { if (!resolvedFromCache && resp.resultCode === 200) { EventBus.$emit('user-after-loggedin', resp.result) - dispatch('cart/authorize', {}, { root: true }) + await dispatch('cart/authorize', {}, { root: true }) return resp } }, @@ -226,7 +226,9 @@ const actions: ActionTree = { await dispatch('cart/disconnect', {}, { root: true }) await dispatch('clearCurrentUser') EventBus.$emit('user-after-logout') - await dispatch('cart/clear', null, { root: true }) + // clear cart without sync, because after logout we don't want to clear cart on backend + // user should have items when he comes back + await dispatch('cart/clear', { sync: false }, { root: true }) if (!silent) { await dispatch('notification/spawnNotification', { @@ -267,7 +269,7 @@ const actions: ActionTree = { */ async getOrdersHistory ({ dispatch, getters }, { refresh = true, useCache = true, pageSize = 20, currentPage = 1 }) { if (!getters.getToken) { - Logger.debug('No User token, user unathorized', 'user')() + Logger.debug('No User token, user unauthorized', 'user')() return Promise.resolve(null) } let resolvedFromCache = false diff --git a/core/modules/user/test/unit/store/actions.spec.ts b/core/modules/user/test/unit/store/actions.spec.ts index f4cee6dd7..e8f5bf274 100644 --- a/core/modules/user/test/unit/store/actions.spec.ts +++ b/core/modules/user/test/unit/store/actions.spec.ts @@ -414,7 +414,7 @@ describe('User actions', () => { expect(contextMock.commit).toBeCalledWith(types.USER_END_SESSION) expect(contextMock.dispatch).toHaveBeenNthCalledWith(1, 'cart/disconnect', {}, {root: true}) expect(contextMock.dispatch).toHaveBeenNthCalledWith(2, 'clearCurrentUser') - expect(contextMock.dispatch).toHaveBeenNthCalledWith(3, 'cart/clear', null, {root: true}) + expect(contextMock.dispatch).toHaveBeenNthCalledWith(3, 'cart/clear', { sync: false }, {root: true}) expect(contextMock.dispatch).toHaveBeenNthCalledWith(4, 'notification/spawnNotification', { type: 'success', message: "You're logged out", diff --git a/core/modules/user/test/unit/store/mutations.spec.ts b/core/modules/user/test/unit/store/mutations.spec.ts index 58ff2f8f8..f1bb0f6bf 100644 --- a/core/modules/user/test/unit/store/mutations.spec.ts +++ b/core/modules/user/test/unit/store/mutations.spec.ts @@ -49,21 +49,18 @@ describe('User mutations', () => { }) describe('USER_START_SESSION', () => { - it('should assign session_started', () => { - jest.isolateModules(() => { - let dateTest = new Date(Date.now()); jest .spyOn(global, 'Date') .mockImplementationOnce(() => dateTest.toDateString()); const stateMock = { - session_started: new Date + session_started: new Date() } const expectedState = { - session_started: new Date + session_started: new Date() } const wrapper = (mutations: any) => mutations[types.USER_START_SESSION](stateMock) diff --git a/core/modules/wishlist/test/unit/components/AddToWishlist.spec.ts b/core/modules/wishlist/test/unit/components/AddToWishlist.spec.ts index 00963618b..baf86b3a0 100644 --- a/core/modules/wishlist/test/unit/components/AddToWishlist.spec.ts +++ b/core/modules/wishlist/test/unit/components/AddToWishlist.spec.ts @@ -56,6 +56,6 @@ describe('AddToWishlist', () => { (wrapper.vm as any).addToWishlist(product); - expect(mockStore.modules.wishlist.actions.addItem).toHaveBeenCalledWith(expect.anything(), product, undefined); + expect(mockStore.modules.wishlist.actions.addItem).toHaveBeenCalledWith(expect.anything(), product); }); }); diff --git a/core/modules/wishlist/test/unit/components/Product.spec.ts b/core/modules/wishlist/test/unit/components/Product.spec.ts index dcd194f1b..207cc9e8a 100644 --- a/core/modules/wishlist/test/unit/components/Product.spec.ts +++ b/core/modules/wishlist/test/unit/components/Product.spec.ts @@ -54,6 +54,6 @@ describe('Product', () => { (wrapper.vm as any).removeFromWishlist(product); - expect(mockStore.modules.wishlist.actions.removeItem).toHaveBeenCalledWith(expect.anything(), product, undefined); + expect(mockStore.modules.wishlist.actions.removeItem).toHaveBeenCalledWith(expect.anything(), product); }); }); diff --git a/core/modules/wishlist/test/unit/components/RemoveFromWishlist.spec.ts b/core/modules/wishlist/test/unit/components/RemoveFromWishlist.spec.ts index df38ebf7e..f12cfb67e 100644 --- a/core/modules/wishlist/test/unit/components/RemoveFromWishlist.spec.ts +++ b/core/modules/wishlist/test/unit/components/RemoveFromWishlist.spec.ts @@ -49,6 +49,6 @@ describe('RemoveFromWishlist', () => { (wrapper.vm as any).removeFromWishlist(product); expect(registerModule).toHaveBeenCalledWith(WishlistModule); - expect(mockStore.modules.wishlist.actions.removeItem).toHaveBeenCalledWith(expect.anything(), product, undefined); + expect(mockStore.modules.wishlist.actions.removeItem).toHaveBeenCalledWith(expect.anything(), product); }); }); diff --git a/core/package.json b/core/package.json index 6b524ba40..950a554ea 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@vue-storefront/core", - "version": "1.11.1", + "version": "1.11.2", "description": "Vue Storefront Core", "license": "MIT", "main": "app.js", @@ -8,30 +8,30 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { - "bodybuilder": "2.2.13", + "bodybuilder": "2.2.21", "compression": "^1.7.4", "config": "^1.30.0", "express": "^4.14.0", "html-minifier": "^4.0.0", "lean-he": "^2.0.0", "localforage": "^1.7.2", - "lodash-es": "^4.17.10", + "lodash-es": "^4.17", "lru-cache": "^4.0.1", "query-string": "^6.2.0", "redis-tag-cache": "^1.2.1", "remove-accents": "^0.4.2", - "uuid": "^3.3.2", - "vue": "^2.6.6", - "vue-carousel": "^0.6.9", + "uuid": "^7.0.2", + "vue": "^2.6.11", + "vue-carousel": "^0.18", "vue-i18n": "^8.0.0", "vue-lazyload": "^1.2.6", - "vue-meta": "^1.5.3", + "vue-meta": "^2.3.3", "vue-observe-visibility": "^0.4.1", "vue-offline": "^1.0.8", "vue-router": "^3.0.1", - "vue-server-renderer": "^2.6.6", - "vuelidate": "^0.6.2", - "vuex": "^3.0.1", + "vue-server-renderer": "^2.6.11", + "vuelidate": "^0.7.5", + "vuex": "^3.1.2", "vuex-router-sync": "^5.0.0" }, "devDependencies": { @@ -39,7 +39,7 @@ "app-root-path": "^2.0.1", "autoprefixer": "^8.6.2", "babel-core": "^6.26.0", - "babel-loader": "^7.1.3", + "babel-loader": "^8.0.6", "babel-preset-env": "^1.6.x", "babel-preset-stage-2": "^6.13.0", "case-sensitive-paths-webpack-plugin": "^2.1.2", @@ -81,12 +81,12 @@ "vue-eslint-parser": "^2.0.3", "vue-loader": "^15.5.1", "vue-ssr-webpack-plugin": "^3.0.0", - "vue-template-compiler": "^2.6.6", + "vue-template-compiler": "^2.6.11", "webpack": "^4.25.1", - "webpack-cli": "^3.1.2", + "webpack-cli": "^3.3.11", "webpack-dev-middleware": "^3.4.0", "webpack-hot-middleware": "^2.24.3", - "webpack-merge": "^4.1.4" + "webpack-merge": "^4.2.2" }, "publishConfig": { "access": "public" diff --git a/core/pages/Checkout.js b/core/pages/Checkout.js index c2687d4a0..e11133914 100644 --- a/core/pages/Checkout.js +++ b/core/pages/Checkout.js @@ -144,9 +144,6 @@ export default { }, async onAfterPlaceOrder (payload) { this.confirmation = payload.confirmation - if (this.$store.state.checkout.personalDetails.createAccount) { - await this.$store.dispatch('user/login', { username: this.$store.state.checkout.personalDetails.emailAddress, password: this.$store.state.checkout.personalDetails.password }) - } this.$store.dispatch('checkout/setThankYouPage', true) this.$store.dispatch('user/getOrdersHistory', { refresh: true, useCache: true }) Logger.debug(payload.order)() @@ -295,7 +292,7 @@ export default { region_id: this.shipping.region_id ? this.shipping.region_id : 0, country_id: this.shipping.country, street: [this.shipping.streetAddress, this.shipping.apartmentNumber], - company: 'NA', // TODO: Fix me! https://github.com/DivanteLtd/vue-storefront/issues/224 + company: '', telephone: this.shipping.phoneNumber, postcode: this.shipping.zipCode, city: this.shipping.city, diff --git a/core/scripts/server.ts b/core/scripts/server.ts index de6a8dc2a..0976f2199 100755 --- a/core/scripts/server.ts +++ b/core/scripts/server.ts @@ -28,7 +28,7 @@ const compileOptions = { escape: /{{([^{][\s\S]+?[^}])}}/g, interpolate: /{{{([\s\S]+?)}}}/g } -const NOT_ALLOWED_SSR_EXTENSIONS_REGEX = new RegExp(`(.*)(${config.server.ssrDisabledFor.extensions.join('|')})$`) +const NOT_ALLOWED_SSR_EXTENSIONS_REGEX = new RegExp(`^.*\\.(${config.server.ssrDisabledFor.extensions.join('|')})$`) const isProd = process.env.NODE_ENV === 'production' process['noDeprecation'] = true diff --git a/core/server-entry.ts b/core/server-entry.ts index 530bef6ec..561adbb6b 100755 --- a/core/server-entry.ts +++ b/core/server-entry.ts @@ -31,7 +31,7 @@ function _ssrHydrateSubcomponents (components, store, router, resolve, reject, a return Promise.resolve(null) } })).then(() => { - AsyncDataLoader.flush({ store, route: router.currentRoute, context: null } /* AsyncDataLoaderActionContext */).then((r) => { + AsyncDataLoader.flush({ store, route: router.currentRoute, context } /* AsyncDataLoaderActionContext */).then((r) => { context.state = store.state if (buildTimeConfig.server.dynamicConfigReload) { const excludeFromConfig = buildTimeConfig.server.dynamicConfigExclude @@ -78,6 +78,7 @@ export default async context => { if (!matchedComponents.length || !matchedComponents[0]) { return reject(new HttpError('No components matched', 404)) // TODO - don't redirect if already on page-not-found } + store.dispatch('url/setCurrentRoute', { to: router.currentRoute }) Promise.all(matchedComponents.map((Component: any) => { const components = Component.mixins ? Array.from(Component.mixins) : [] union(components, [Component]).map(SubComponent => { diff --git a/core/store/index.ts b/core/store/index.ts index ee38dbdd0..bbdac9a06 100644 --- a/core/store/index.ts +++ b/core/store/index.ts @@ -48,7 +48,8 @@ const state = { twoStageCachingDisabled: false, userTokenInvalidated: null, userTokenInvalidateAttemptsCount: 0, - userTokenInvalidateLock: 0 + userTokenInvalidateLock: 0, + url: {} } let rootStore = new Vuex.Store({ diff --git a/core/types/RootState.ts b/core/types/RootState.ts index bec8d6bbc..017ed7f7b 100644 --- a/core/types/RootState.ts +++ b/core/types/RootState.ts @@ -32,5 +32,6 @@ export default interface RootState { userTokenInvalidated: string | null, userTokenInvalidateAttemptsCount: number, userTokenInvalidateLock: number, - route?: any + route?: any, + url: any } diff --git a/docs/package.json b/docs/package.json index 2b5a28f2e..e2242812a 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,7 +1,7 @@ { "name": "@vue-storefront/docs", "private": true, - "version": "1.11.1", + "version": "1.11.2", "scripts": { "docs:dev": "vuepress dev", "docs:build": "vuepress build", diff --git a/package.json b/package.json index 4aee6da6c..b8d13fd5d 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-storefront", - "version": "1.11.1", + "version": "1.11.2", "description": "A Vue.js, PWA eCommerce frontend", "private": true, "engines": { @@ -49,23 +49,25 @@ "lerna": "lerna" }, "dependencies": { - "@types/webpack": "^4.4.23", - "@types/webpack-dev-server": "^3.1.1", - "apollo-cache-inmemory": "^1.2.5", - "apollo-client": "^2.3.5", - "apollo-link": "^1.2.2", - "apollo-link-http": "^1.5.4", + "@types/webpack": "^4.41", + "@types/webpack-dev-server": "^3.10", + "apollo-cache-inmemory": "^1.6.5", + "apollo-client": "^2.6.8", + "apollo-link": "^1.2.13", + "apollo-link-http": "^1.5.16", "body-scroll-lock": "^2.6.4", - "bodybuilder": "2.2.13", + "bodybuilder": "2.2.21", "config": "^1.30.0", "cross-env": "^3.1.4", - "dayjs": "^1.8.15", - "es6-promise": "^4.2.4", + "dayjs": "^1.8.21", + "es6-promise": "^4.2.8", "express": "^4.14.0", "fs-extra": "^8.1.0", - "glob": "^7.1.4", + "glob": "^7.1.6", "graphql": "^0.13.2", - "graphql-tag": "^2.9.2", + "graphql-tag": "^2.10.3", + "intl": "^1.2.5", + "intl-locales-supported": "^1.8.2", "isomorphic-fetch": "^2.2.1", "js-sha3": "^0.8.0", "localforage": "^1.7.2", @@ -77,31 +79,31 @@ "reflect-metadata": "^0.1.12", "register-service-worker": "^1.5.2", "ts-node": "^8.6.2", - "vue": "^2.6.6", + "vue": "^2.6.11", "vue-analytics": "^5.16.1", - "vue-apollo": "^3.0.0-beta.19", - "vue-carousel": "^0.6.9", + "vue-apollo": "^3.0.3", + "vue-carousel": "^0.18", "vue-gtm": "^2.0.0", "vue-i18n": "^8.0.0", "vue-lazy-hydration": "^1.0.0-beta.9", "vue-lazyload": "^1.2.6", - "vue-meta": "^1.5.3", + "vue-meta": "^2.3.3", "vue-no-ssr": "^0.2.2", "vue-observe-visibility": "^0.4.1", "vue-offline": "^1.0.8", "vue-router": "^3.0.1", - "vue-server-renderer": "^2.6.6", - "vuelidate": "^0.6.2", - "vuex": "^3.0.1", + "vue-server-renderer": "^2.6.11", + "vuelidate": "^0.7.5", + "vuex": "^3.1.2", "vuex-router-sync": "^5.0.0" }, "devDependencies": { - "@babel/core": "^7.3.4", - "@babel/plugin-syntax-dynamic-import": "^7.2.0", - "@babel/polyfill": "^7.2.5", - "@babel/preset-env": "^7.3.4", - "@types/jest": "^24.0.11", - "@types/node": "^10.12.18", + "@babel/core": "^7.8.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/polyfill": "^7.8.3", + "@babel/preset-env": "^7.8.6", + "@types/jest": "^25.1.3", + "@types/node": "^13.7.7", "@typescript-eslint/eslint-plugin": "^1.7.1-alpha.17", "@typescript-eslint/parser": "^1.7.1-alpha.17", "@vue/test-utils": "^1.0.0-beta.29", @@ -110,7 +112,7 @@ "babel-core": "^7.0.0-bridge.0", "babel-eslint": "^9.0.0", "babel-jest": "^24.1.0", - "babel-loader": "^8.0.5", + "babel-loader": "^8.0.6", "babel-plugin-dynamic-import-node": "^2.2.0", "case-sensitive-paths-webpack-plugin": "^2.1.2", "command-exists": "^1.2.2", @@ -136,8 +138,8 @@ "husky": "^2.6.0", "inquirer": "^3.3.0", "is-windows": "^1.0.1", - "jest": "^24.8.0", - "jest-fetch-mock": "^2.1.2", + "jest": "^25.1.0", + "jest-fetch-mock": "^3.0.1", "jest-serializer-vue": "^2.0.2", "jsonfile": "^4.0.0", "lerna": "^3.14.1", @@ -152,25 +154,25 @@ "sass-loader": "^7.1.0", "shelljs": "^0.8.1", "sw-precache-webpack-plugin": "^0.11.5", - "ts-jest": "^24.0.2", + "ts-jest": "^25.2.1", "ts-loader": "^5.3.0", "typescript": "^3.1.6", "url-loader": "^1.1.2", "url-parse": "^1.4.4", "vue-eslint-parser": "^2.0.3", - "vue-jest": "^3.0.2", + "vue-jest": "^3.0.5", "vue-loader": "^15.4.2", "vue-ssr-webpack-plugin": "^3.0.0", - "vue-template-compiler": "^2.6.6", + "vue-template-compiler": "^2.6.11", "webpack": "^4.25.1", "webpack-bundle-analyzer": "^3.3.2", - "webpack-cli": "^3.1.2", + "webpack-cli": "^3.3.11", "webpack-dev-middleware": "^3.4.0", "webpack-hot-middleware": "^2.24.3", - "webpack-merge": "^4.1.4" + "webpack-merge": "^4.2.2" }, "peerDependencies": { - "vue-template-compiler": "^2.6.6" + "vue-template-compiler": "^2.6.11" }, "browserslist": { "development": [ diff --git a/packages/cli/boilerplates/module/package.json b/packages/cli/boilerplates/module/package.json index 4c814aa42..7f1745975 100644 --- a/packages/cli/boilerplates/module/package.json +++ b/packages/cli/boilerplates/module/package.json @@ -16,7 +16,7 @@ "ts-loader": "^6.0.4", "typescript": "^3.5.2", "webpack": "^4.35.2", - "webpack-cli": "^3.3.5" + "webpack-cli": "^3.3.11" }, "peerDependencies": { "@vue-storefront/core": "^1.11.1" diff --git a/packages/cli/package.json b/packages/cli/package.json index 423fec890..3fe495996 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@vue-storefront/cli", - "version": "0.0.15", + "version": "0.0.16", "description": "", "main": "index.js", "bin": { diff --git a/src/modules/google-cloud-trace/package.json b/src/modules/google-cloud-trace/package.json index 9f3b9cdbe..3c21d5eb2 100644 --- a/src/modules/google-cloud-trace/package.json +++ b/src/modules/google-cloud-trace/package.json @@ -1,10 +1,10 @@ { "name": "google-cloud-tracing", - "version": "1.0.0", + "version": "1.0.1", "main": "index.js", "license": "MIT", "private": true, "dependencies": { - "@google-cloud/trace-agent": "^4.1.1" + "@google-cloud/trace-agent": "^4.2.5" } } diff --git a/src/modules/google-tag-manager/hooks/afterRegistration.ts b/src/modules/google-tag-manager/hooks/afterRegistration.ts index a75e436e6..2e5c85ac5 100644 --- a/src/modules/google-tag-manager/hooks/afterRegistration.ts +++ b/src/modules/google-tag-manager/hooks/afterRegistration.ts @@ -79,7 +79,7 @@ export function afterRegistration (config, store: Store) { } // Measuring Purchases - if (type === 'order/order/LAST_ORDER_CONFIRMATION') { + if (type === 'order/orders/LAST_ORDER_CONFIRMATION') { const orderId = payload.confirmation.backendOrderId const products = payload.order.products.map(product => getProduct(product)) store.dispatch( @@ -87,24 +87,22 @@ export function afterRegistration (config, store: Store) { { refresh: true, useCache: false } ).then(() => { const orderHistory = state.user.orders_history - const order = orderHistory.items.find((order) => order['entity_id'].toString() === orderId) - if (order) { - GTM.trackEvent({ - 'ecommerce': { - 'purchase': { - 'actionField': { - 'id': orderId, - 'affiliation': order.store_name, - 'revenue': order.total_due, - 'tax': order.tax_amount, - 'shipping': order.shipping_amount, - 'coupon': '' - }, - 'products': products - } + const order = state.user.orders_history ? orderHistory.items.find((order) => order['entity_id'].toString() === orderId) : null + GTM.trackEvent({ + 'ecommerce': { + 'purchase': { + 'actionField': { + 'id': orderId, + 'affiliation': order ? order.store_name : '', + 'revenue': order ? order.total_due : state.cart.platformTotals && state.cart.platformTotals.base_grand_total ? state.cart.platformTotals.base_grand_total : '', + 'tax': order ? order.total_due : state.cart.platformTotals && state.cart.platformTotals.base_tax_amount ? state.cart.platformTotals.base_tax_amount : '', + 'shipping': order ? order.total_due : state.cart.platformTotals && state.cart.platformTotals.base_shipping_amount ? state.cart.platformTotals.base_shipping_amount : '', + 'coupon': '' + }, + 'products': products } - }) - } + } + }) }) } }) diff --git a/src/modules/instant-checkout/components/InstantCheckout.vue b/src/modules/instant-checkout/components/InstantCheckout.vue index 9060feaff..86f4f9d2b 100644 --- a/src/modules/instant-checkout/components/InstantCheckout.vue +++ b/src/modules/instant-checkout/components/InstantCheckout.vue @@ -149,7 +149,8 @@ export default { this.$store.dispatch('checkout/setThankYouPage', true) this.$store.commit('ui/setMicrocart', false) this.$router.push(this.localizedRoute('/checkout')) - this.$store.dispatch('cart/clear', null, {root: true}) + // clear cart without sync, because after order cart will be already cleared on backend + this.$store.dispatch('cart/clear', { sync: false }, {root: true}) } }) }) @@ -265,7 +266,7 @@ export default { region_id: 0, country_id: paymentResponse.shippingAddress.country, street: [paymentResponse.shippingAddress.addressLine[0], paymentResponse.shippingAddress.addressLine[1]], - company: paymentResponse.shippingAddress.organization ? paymentResponse.shippingAddress.organization : 'NA', + company: paymentResponse.shippingAddress.organization ? paymentResponse.shippingAddress.organization : '', telephone: paymentResponse.shippingAddress.phone, postcode: paymentResponse.shippingAddress.postalCode, city: paymentResponse.shippingAddress.city, @@ -279,7 +280,7 @@ export default { region_id: 0, country_id: paymentResponse.shippingAddress.country, street: [paymentResponse.shippingAddress.addressLine[0], paymentResponse.shippingAddress.addressLine[1]], - company: paymentResponse.shippingAddress.organization ? paymentResponse.shippingAddress.organization : 'NA', + company: paymentResponse.shippingAddress.organization ? paymentResponse.shippingAddress.organization : '', telephone: paymentResponse.payerPhone, postcode: paymentResponse.shippingAddress.postalCode, city: paymentResponse.shippingAddress.city, diff --git a/src/themes/default-amp/package.json b/src/themes/default-amp/package.json index 041aaf4c8..6eda02a9a 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.11.1", + "version": "1.11.2", "description": "Default AMP theme for Vue Storefront", "main": "index.js", "scripts": { @@ -10,13 +10,13 @@ "author": "pkarw and contributors", "license": "MIT", "dependencies": { - "bodybuilder": "2.2.13", - "vue": "^2.6.6", - "vue-carousel": "^0.6.9", + "bodybuilder": "2.2.21", + "vue": "^2.6.11", + "vue-carousel": "^0.18", "vue-no-ssr": "^0.2.2", "vue-progressbar": "^0.7.5", - "vuelidate": "^0.6.2", - "vuex": "^3.0.1" + "vuelidate": "^0.7.5", + "vuex": "^3.1.2" }, "publishConfig": { "access": "public" diff --git a/src/themes/default/components/core/ProductCustomOptions.vue b/src/themes/default/components/core/ProductCustomOptions.vue index 7a59caeb3..a3ebba2fb 100644 --- a/src/themes/default/components/core/ProductCustomOptions.vue +++ b/src/themes/default/components/core/ProductCustomOptions.vue @@ -16,29 +16,33 @@ :placeholder="option.title" @change="optionChanged(option)" > -
-