diff --git a/CHANGELOG.md b/CHANGELOG.md index aefc8a3d0f..556cfc6e2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ 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.5.0] - 2018.10.22 + +### Added +- Contact form mailer - #1875 - Akbar Abdrakhmanov @akbarik +- oauth2 configuration in setup - #1865 - Krister Andersson @Cyclonecode +- GraphQL schema extendibility in the API - Yoann Vié +- A lot of new docs - Natalia Tepluhina @NataliTepluhina +- Magento2 integrated importer + +### Changed +- New Modules API, and base modules (cart, wishlist, newsletter ...) refactored [read more...](https://github.com/DivanteLtd/vue-storefront/blob/master/doc/api-modules/about-modules.md) - Filip Rakowski @filrak + +### Fixed +- The `regionId` field added to Order interface - #1258 - Jim Hil @jimcreate78 +- SSR Memory leaks fixed - #1882 Tomasz Duda @tomasz-duda +- E2E tests fixed - #1861 - Patryk Tomczyk @patzik +- UI animations - #1857 - Javier Villanueva @jahvi +- Disabled buttons fixed - #1852 - Patryk Tomczyk @patzik +- Mailchimp / Newsletter modules rebuilt - Filip Rakowski @filrak +- Search component UX fixes - #1862 - Adrian Cagaanan @diboy2 + ## [1.4.0] - 2018.10.05 ### Added diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 29c33ba8b8..e68a29fe02 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -23,4 +23,4 @@ - [ ] I read and followed [contribution rules](https://github.com/DivanteLtd/vue-storefront/blob/master/CONTRIBUTING.md) - [ ] I read the [TypeScript Action Plan](https://github.com/DivanteLtd/vue-storefront/blob/master/doc/TypeScript%20Action%20Plan.md) and adjusted my PR according to it -- [ ] I read about [Vue Storefront Modules](https://github.com/DivanteLtd/vue-storefront/blob/master/doc/api-modules/about-modules.md) and [refactoring plan for them](https://github.com/DivanteLtd/vue-storefront/blob/master/doc/api-modules/refactoring-to-modules.md) +- [ ] I read about [Vue Storefront Modules](https://github.com/DivanteLtd/vue-storefront/blob/master/doc/api-modules/about-modules.md) and I am aware that every new feature should be a module diff --git a/README.md b/README.md index 8b01c7569d..46a56e91c2 100644 --- a/README.md +++ b/README.md @@ -205,9 +205,9 @@ Vue Storefront is a Community effort brought to You by our great Core Team and s Divante @@ -436,6 +436,53 @@ Vue Storefront is a Community effort brought to You by our great Core Team and s + + + + Copex.io + + + + + Badger Blue + + + + + + + + + + + + + + + + + + diff --git a/config/default.json b/config/default.json index a782fb6fe8..38723c8f25 100644 --- a/config/default.json +++ b/config/default.json @@ -274,7 +274,10 @@ "endpoint": "http://localhost:8080/api/ext/mailchimp-subscribe/subscribe" }, "mailer": { - "endpoint": "http://localhost:8080/api/ext/mail-service/send-email", + "endpoint": { + "send": "http://localhost:8080/api/ext/mail-service/send-email", + "token": "http://localhost:8080/api/ext/mail-service/get-token" + }, "contactAddress": "contributors@vuestorefront.io", "sendConfirmation": true }, diff --git a/core/app.ts b/core/app.ts index ddbdceb9d1..9e2260ccf1 100755 --- a/core/app.ts +++ b/core/app.ts @@ -20,6 +20,7 @@ import App from 'theme/App.vue' import themeModules from 'theme/store' import themeExtensionEntryPoints from 'theme/extensions' import extensionEntryPoints from 'src/extensions' +import { once } from './helpers' // Declare Apollo graphql client import ApolloClient from 'apollo-client' @@ -32,13 +33,15 @@ import { enabledModules } from './modules-entry' import { takeOverConsole } from '@vue-storefront/core/helpers/log' if (buildTimeConfig.console.verbosityLevel !== 'display-everything') { - takeOverConsole(buildTimeConfig.console.verbosityLevel) + once('__TAKE_OVER_CONSOLE__', () => { + takeOverConsole(buildTimeConfig.console.verbosityLevel) + }) } export function createApp (ssrContext, config): { app: Vue, router: any, store: any } { sync(store, router) - store.state.version = '1.4.0' + store.state.version = '1.5.0' store.state.config = config store.state.__DEMO_MODE__ = (config.demomode === true) ? true : false if(ssrContext) Vue.prototype.$ssrRequestContext = ssrContext @@ -50,27 +53,31 @@ export function createApp (ssrContext, config): { app: Vue, router: any, store: console.debug('Registering Vuex module', moduleName) store.registerModule(moduleName, storeModules[moduleName]) } - + const storeView = prepareStoreView(null) // prepare the default storeView store.state.storeView = storeView // store.state.shipping.methods = shippingMethods - + Vue.use(Vuelidate) Vue.use(VueLazyload, {attempt: 2}) Vue.use(Meta) Vue.use(VueObserveVisibility) - - require('theme/plugins') - const pluginsObject = plugins() - Object.keys(pluginsObject).forEach(key => { - Vue.use(pluginsObject[key]) - }) - - const mixinsObject = mixins() - Object.keys(mixinsObject).forEach(key => { - Vue.mixin(mixinsObject[key]) + + once('__VUE_EXTEND__', () => { + console.debug('Registering Vue plugins') + require('theme/plugins') + const pluginsObject = plugins() + Object.keys(pluginsObject).forEach(key => { + Vue.use(pluginsObject[key]) + }) + + console.debug('Registering Vue mixins') + const mixinsObject = mixins() + Object.keys(mixinsObject).forEach(key => { + Vue.mixin(mixinsObject[key]) + }) }) - + const filtersObject = filters() Object.keys(filtersObject).forEach(key => { Vue.filter(key, filtersObject[key]) @@ -78,15 +85,15 @@ export function createApp (ssrContext, config): { app: Vue, router: any, store: const httpLink = new HttpLink({ uri: store.state.config.graphql.host.indexOf('://') >= 0 ? store.state.config.graphql.host : (store.state.config.server.protocol + '://' + store.state.config.graphql.host + ':' + store.state.config.graphql.port + '/graphql') }) - + const apolloClient = new ApolloClient({ link: httpLink, cache: new InMemoryCache(), connectToDevTools: true }) - + let loading = 0 - + const apolloProvider = new VueApollo({ clients: { a: apolloClient @@ -104,9 +111,9 @@ export function createApp (ssrContext, config): { app: Vue, router: any, store: console.error(error) } }) - + Vue.use(VueApollo) - // End declare Apollo graphql client + // End declare Apollo graphql client const app = new Vue({ router, store, diff --git a/core/components/Notification.js b/core/components/Notification.js index 35471ae01d..619ee56d75 100644 --- a/core/components/Notification.js +++ b/core/components/Notification.js @@ -21,16 +21,15 @@ export default { this.action('close', this.notifications.length - 1) }, data.timeToLive || 5000) }, - action (action, id) { - this.$bus.$emit('notification-after-' + action, id) + action (action, id, notification) { + this.$bus.$emit('notification-after-' + action, notification) switch (action) { - case 'close': - this.notifications.splice(id, 1) - break case 'goToCheckout': this.$router.push(this.localizedRoute('/checkout')) this.notifications.splice(id, 1) break + default: + this.notifications.splice(id, 1) } } } diff --git a/core/components/blocks/Checkout/Shipping.js b/core/components/blocks/Checkout/Shipping.js index 2906d02c32..c4e0528a5b 100644 --- a/core/components/blocks/Checkout/Shipping.js +++ b/core/components/blocks/Checkout/Shipping.js @@ -137,7 +137,7 @@ export default { }, getCurrentShippingMethod () { let shippingCode = this.shipping.shippingMethod - let currentMethod = this.shippingMethods.find(item => item.method_code === shippingCode) + let currentMethod = this.shippingMethods ? this.shippingMethods.find(item => item.method_code === shippingCode) : {} return currentMethod }, changeShippingMethod () { diff --git a/core/helpers/index.js b/core/helpers/index.js index 2dcfdf62ef..9ea8af951f 100644 --- a/core/helpers/index.js +++ b/core/helpers/index.js @@ -9,3 +9,13 @@ export function slugify (text) { .replace(/[^\w-]+/g, '') // Remove all non-word chars .replace(/--+/g, '-') // Replace multiple - with single - } + +export function once (key, fn) { + const { process = {} } = global + const processKey = key + '__ONCE__' + if (!process.hasOwnProperty(processKey)) { + console.debug(`Once ${key}`) + process[processKey] = true + fn() + } +} diff --git a/core/helpers/log.js b/core/helpers/log.js index 1d77812e8c..005655e73c 100644 --- a/core/helpers/log.js +++ b/core/helpers/log.js @@ -2,7 +2,7 @@ * @param {string} level available options: 'no-console', 'only-errors', 'all' */ export function takeOverConsole (level = 'no-console') { - var console = typeof window !== 'undefined' ? window.console : global.console + const console = typeof window !== 'undefined' ? window.console : global.console if (!console) return function intercept (method) { @@ -26,7 +26,7 @@ export function takeOverConsole (level = 'no-console') { original.apply(console, arguments) } else { // Do this for IE - var message = Array.prototype.slice.apply(arguments).join(' ') + const message = Array.prototype.slice.apply(arguments).join(' ') original(message) } } diff --git a/core/modules/cart/store/actions.ts b/core/modules/cart/store/actions.ts index 72ad1da192..da18581535 100644 --- a/core/modules/cart/store/actions.ts +++ b/core/modules/cart/store/actions.ts @@ -609,7 +609,13 @@ const actions: ActionTree = { Vue.prototype.$bus.$emit('servercart-after-diff', { diffLog: diffLog, serverItems: serverItems, clientItems: clientItems, dryRun: event.dry_run, event: event }) // send the difflog console.log('Server sync diff', diffLog) } else { - console.error(event.result) + console.error(event.result) // override with guest cart + if (rootStore.state.cart.bypassCount < MAX_BYPASS_COUNT) { + console.log('Bypassing with guest cart', rootStore.state.cart.bypassCount) + rootStore.state.cart.bypassCount = rootStore.state.cart.bypassCount + 1 + rootStore.dispatch('cart/serverCreate', { guestCart: true }, { root: true }) + console.error(event.result) + } } }, servercartAfterItemUpdated (context, event) { diff --git a/core/modules/cart/store/mutations.ts b/core/modules/cart/store/mutations.ts index 837ddb7d80..858a0e054c 100644 --- a/core/modules/cart/store/mutations.ts +++ b/core/modules/cart/store/mutations.ts @@ -66,7 +66,7 @@ const mutations: MutationTree = { state.cartIsLoaded = true state.cartSavedAt = Date.now() - Vue.prototype.$bus.$emit('order/PROCESS_QUEUE', { config: rootStore.state.config }) // process checkout queue + // Vue.prototype.$bus.$emit('order/PROCESS_QUEUE', { config: rootStore.state.config }) // process checkout queue Vue.prototype.$bus.$emit('sync/PROCESS_QUEUE', { config: rootStore.state.config }) // process checkout queue Vue.prototype.$bus.$emit('application-after-loaded') Vue.prototype.$bus.$emit('cart-after-loaded') diff --git a/core/modules/mailer/components/EmailForm.ts b/core/modules/mailer/components/EmailForm.ts index 4f1be44ec1..6d14ee8462 100644 --- a/core/modules/mailer/components/EmailForm.ts +++ b/core/modules/mailer/components/EmailForm.ts @@ -9,21 +9,10 @@ export const EmailForm = { }, methods: { sendEmail (letter: MailItem, success, failure) { - this.$store.dispatch('mailer/getToken') + this.$store.dispatch('mailer/sendEmail', letter) .then(res => { if (res.ok) { - return res.json() - } - throw new Error() - }) - .then(tokenResponse => { - this.token = tokenResponse.result - return this.$store.dispatch('mailer/sendEmail', { ...letter, token: this.token }) - }) - .then(res => { - if (res.ok) { - success(i18n.t('Email has successfully been sent')) - this.$store.dispatch('mailer/sendConfirmation', { ...letter, token: this.token }) + if (success) success(i18n.t('Email has successfully been sent')) } else { return res.json() } @@ -31,11 +20,11 @@ export const EmailForm = { .then(errorResponse => { if (errorResponse) { const errorMessage = errorResponse.result - failure(i18n.t(errorMessage)) + if (failure) failure(i18n.t(errorMessage)) } }) .catch(() => { - failure(i18n.t('Could not send an email. Please try again later.')) + if (failure) failure(i18n.t('Could not send an email. Please try again later.')) }) } } diff --git a/core/modules/mailer/store/index.ts b/core/modules/mailer/store/index.ts index 84a25039e3..aaa33be8bf 100644 --- a/core/modules/mailer/store/index.ts +++ b/core/modules/mailer/store/index.ts @@ -6,42 +6,32 @@ import { Module } from 'vuex' export const module: Module = { namespaced: true, actions: { - getToken ({}) { - return fetch(config.mailer.endpoint.token) - }, sendEmail ({}, letter: MailItem) { - return fetch(config.mailer.endpoint.send, { - method: 'POST', - mode: 'cors', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify(letter) - }) - }, - sendConfirmation ({}, letter: MailItem) { - if (config.mailer.sendConfirmation) { - const confirmationLetter = { - sourceAddress: letter.targetAddress, - targetAddress: letter.sourceAddress, - subject: i18n.t('Confirmation of receival'), - emailText: i18n.t(`Dear customer,\n\nWe have received your letter.\nThank you for your feedback!`), - token: letter.token, - confirmation: true - } - - fetch(config.mailer.endpoint.send, { - method: 'POST', - mode: 'cors', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify(confirmationLetter) + return new Promise((resolve, reject) => { + fetch(config.mailer.endpoint.token) + .then(res => res.json()) + .then(res => { + if (res.code === 200) { + fetch(config.mailer.endpoint.send, { + method: 'POST', + mode: 'cors', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + ...letter, + token: res.result + }) + }) + .then(res => resolve(res)) + .catch(() => reject()) + } else { + reject() + } }) - .catch(error => console.error(error)) - } + .catch(() => reject()) + }) } } } diff --git a/core/modules/mailer/types/MailItem.ts b/core/modules/mailer/types/MailItem.ts index 71fdc3e0b3..e956a1bd9f 100644 --- a/core/modules/mailer/types/MailItem.ts +++ b/core/modules/mailer/types/MailItem.ts @@ -3,5 +3,5 @@ export default interface MailItem { targetAddress: string, subject: string, emailText: string, - token?: string + confirmation?: boolean } diff --git a/core/modules/module-template/index.ts b/core/modules/module-template/index.ts index 36f9580358..2ad76e3aad 100644 --- a/core/modules/module-template/index.ts +++ b/core/modules/module-template/index.ts @@ -1,4 +1,4 @@ -// This is VS modules entry point. +// This is VS module entry point. // Read more about modules: https://github.com/DivanteLtd/vue-storefront/blob/master/doc/api-modules/about-modules.md import { module } from './store' import { plugin } from './store/plugin' @@ -23,4 +23,4 @@ const moduleConfig: VueStorefrontModuleConfig = { router: { routes, beforeEach, afterEach } } -export const Example = new VueStorefrontModule(moduleConfig) \ No newline at end of file +export const Example = new VueStorefrontModule(moduleConfig) diff --git a/core/package.json b/core/package.json index 0a2882c724..98b7eb9752 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@vue-storefront/core", - "version": "1.4.5", + "version": "1.4.6", "description": "Vue Storefront Core", "license": "MIT", "main": "app.js", diff --git a/core/scripts/installer.js b/core/scripts/installer.js index 2431277411..376fa98197 100644 --- a/core/scripts/installer.js +++ b/core/scripts/installer.js @@ -444,7 +444,8 @@ class Storefront extends Abstract { config.reviews.create_endpoint = `${backendPath}/api/review/create?token={{token}}` config.mailchimp.endpoint = `${backendPath}/api/ext/mailchimp-subscribe/subscribe` - config.mailer.endpoint = `${backendPath}/api/ext/mail-service/send-email` + config.mailer.endpoint.send = `${backendPath}/api/ext/mail-service/send-email` + config.mailer.endpoint.token = `${backendPath}/api/ext/mail-service/get-token` config.images.baseUrl = this.answers.images_endpoint config.cms.endpoint = `${backendPath}/api/ext/cms-data/cms{{type}}/{{cmsId}}` config.cms.endpointIdentifier = `${backendPath}/api/ext/cms-data/cms{{type}}Identifier/{{cmsIdentifier}}/storeId/{{storeId}}` @@ -743,7 +744,7 @@ let questions = [ type: 'input', name: 'm2_url', message: 'Please provide your magento url', - default: 'http://magento2.demo-1.xyz.com', + default: 'http://demo-magento2.vuestorefront.io', when: function (answers) { return answers.m2_api_oauth2 === true } diff --git a/core/scripts/server.js b/core/scripts/server.js index b4dd32209e..bd1e4b6e5e 100755 --- a/core/scripts/server.js +++ b/core/scripts/server.js @@ -57,6 +57,7 @@ function createRenderer (bundle, clientManifest, template) { // https://github.com/vuejs/vue/blob/dev/packages/vue-server-renderer/README.md#why-use-bundlerenderer return require('vue-server-renderer').createBundleRenderer(bundle, { clientManifest, + // runInNewContext: false, cache: require('lru-cache')({ max: 1000, maxAge: 1000 * 60 * 15 diff --git a/core/store/lib/storage.ts b/core/store/lib/storage.ts index 59f02a11e2..cf8455f194 100644 --- a/core/store/lib/storage.ts +++ b/core/store/lib/storage.ts @@ -26,19 +26,23 @@ class LocalForageCacheDriver { if (typeof this.cacheErrorsCount[collectionName] === 'undefined') { this.cacheErrorsCount[collectionName] = 0 } - if (typeof Vue.prototype.$localCache === 'undefined') { - Vue.prototype.$localCache = {} - } - if (typeof Vue.prototype.$localCache[dbName] === 'undefined') { - Vue.prototype.$localCache[dbName] = {} - } - if (typeof Vue.prototype.$localCache[dbName][collectionName] === 'undefined') { - Vue.prototype.$localCache[dbName][collectionName] = {} + if (Vue.prototype.$isServer) { + this._localCache = {} + } else { + if (typeof Vue.prototype.$localCache === 'undefined') { + Vue.prototype.$localCache = {} + } + if (typeof Vue.prototype.$localCache[dbName] === 'undefined') { + Vue.prototype.$localCache[dbName] = {} + } + if (typeof Vue.prototype.$localCache[dbName][collectionName] === 'undefined') { + Vue.prototype.$localCache[dbName][collectionName] = {} + } + this._localCache = Vue.prototype.$localCache[dbName][collectionName] } this._collectionName = collectionName this._dbName = dbName this._useLocalCacheByDefault = useLocalCacheByDefault - this._localCache = Vue.prototype.$localCache[dbName][collectionName] this._localForageCollection = collection this._lastError = null this._persistenceErrorNotified = false diff --git a/core/store/modules/checkout/index.ts b/core/store/modules/checkout/index.ts index 216ac4b1be..e4271d53ae 100644 --- a/core/store/modules/checkout/index.ts +++ b/core/store/modules/checkout/index.ts @@ -24,6 +24,7 @@ const checkout: Module = { apartmentNumber: '', city: '', state: '', + region_id: 0, zipCode: '', phoneNumber: '', shippingMethod: '' @@ -37,6 +38,7 @@ const checkout: Module = { apartmentNumber: '', city: '', state: '', + region_id: 0, zipCode: '', phoneNumber: '', taxId: '', diff --git a/core/store/modules/checkout/types/CheckoutState.ts b/core/store/modules/checkout/types/CheckoutState.ts index 062bf13b68..dd8477f495 100644 --- a/core/store/modules/checkout/types/CheckoutState.ts +++ b/core/store/modules/checkout/types/CheckoutState.ts @@ -15,6 +15,7 @@ export default interface CheckoutState { apartmentNumber: string, city: string, state: string, + region_id: number, zipCode: string, phoneNumber: string, shippingMethod: string @@ -27,6 +28,7 @@ export default interface CheckoutState { streetAddress: string, apartmentNumber: string, city: string, + region_id: number, state: string, zipCode: string, phoneNumber: string, @@ -35,4 +37,4 @@ export default interface CheckoutState { paymentMethodAdditional: any }, isThankYouPage: boolean -} \ No newline at end of file +} diff --git a/doc/Upgrade notes.md b/doc/Upgrade notes.md index 49978281cf..fb6b28c04a 100644 --- a/doc/Upgrade notes.md +++ b/doc/Upgrade notes.md @@ -2,6 +2,22 @@ We're trying to keep the upgrade process as easy as it's possible. Unfortunately sometimes manual code changes are required. Before pulling out the latest version, please take a look at the upgrade notes below:. +## 1.4 -> 1.5 + +### Modifications + +#### New Modules API + +With 1.5.0 we've introduced new hevily refactored modules API. We've tried to keep the old theme components backward compatible - so now You can few some "mock" components in the `/core/components` just referencing to the `/modules/{{module}}/components` original. Please read [how modules work and are structured](https://github.com/DivanteLtd/vue-storefront/blob/master/doc/api-modules/about-modules.md) to check if it's implies any changes to Your theme. As it may seem like massive changes (lot of files added/removed/renamed) - It should not impact Your custom code. + +#### New Newsletter module + +The exsiting newsletter integration module was pretty chaotic and messy. @filrak has rewritten it from scratch. If You've relied on exisitng newsletter module logic / events / etc. it could have affected Your code (low probability). + +#### Memory leaks fixed + +We've fixed SSR memory leaks with #1882. It should not affect Your custom code - but if You've modified any SSR features please just make sure that everything still works just fine. + ## 1.3 -> 1.4 ### Modifications diff --git a/doc/api-modules/about-modules.md b/doc/api-modules/about-modules.md index ffa46ddc43..3407675a0c 100644 --- a/doc/api-modules/about-modules.md +++ b/doc/api-modules/about-modules.md @@ -39,19 +39,124 @@ The purpose is well described in [this discussion](https://github.com/DivanteLtd # How module should look like -Module by it's definition should encapsulate all logic required for the feature it represents. You can think about each module as a micro application that exposes it's parts to the outside world (Vue Storefront). + +# Module config and capabilities + +Module config is the object that is required to instantiate VS module. The config object you'll provide is later used to extend and hook into different parts of the application (like router, Vuex etc). +Please use this object as the only part that is responsible for extending Vue Storefront. Otherwise it may stop working after some breaking core updates. + +Vue Storefront module object with provided config should be exported in `index.ts` entry point. Ideally it should be a named export named the same as modules key. + +This is how the signature of Vue Storefront Module looks like: +```js +interface VueStorefrontModuleConfig { + key: string; + store?: { module?: Module, plugin?: Function }; + router?: { routes?: RouteConfig[], beforeEach?: NavigationGuard, afterEach?: NavigationGuard }, + beforeRegistration?: (Vue: VueConstructor, config: Object) => void, + afterRegistration?: (Vue: VueConstructor, config: Object) => void, +} +``` +#### `key` (required) + +Key is an ID of your module. It's used to identify your module and to set keys in all key-based extendings that module is doing (like creating namespaced store). This key should be unique. You can duplicate the keys of some other modules only if you want to extend them. Modules with the same keys will be merged. + +#### `store` + +Extension point for Vuex. It can be provided with vuex module and Vuex plugin object to subscribe for mutations. In case of conflicting module keys they are deep merged in favour of most recent instantiated one. + +#### `router` Normally module can (but not must) contain following folders: -- `components` - components related to this module (eg. Microcart for Cart module) -- `store` - Vuex store associated to module -- `helpers` - everything else that is meant to support modules behavior -- `types` - TypeScript types associated with module -- `test` - folder with unit tests which is *required* for every new or rewritten module. This folder can be placed outside of the module in 'tests' folder. -- `extends` - code that you need to include into core files such as client/server entry, app entry, webpack config or service worker. If you need to extend, let's say `client-entry.js`just create a file with the same name and import it in the core `client-entry.js` by invoking files content with `import core/module/module-name/extends/client-entry.js +#### `beforeRegistration` + +Function that'll be called before registering the module both on server and client side. You have access to `Vue` and `config` objects inside. + +#### `afterRegistration` + +Function that'll be called after registering the module both on server and client side. You have access to `Vue` and `config` objects inside. +# Module file structure -[*] currently we are using `core/api/module_name` instead of `module/module_name` but it's about to change soon +Below you can see recommended file structure for VS module. All of the core ones are organised in this way. +Try to have similar file structure inside the ones that you create. If all of modules will implement similar architeture it'll be easier to maintain and understand them. If there is no purpose in organising some of it's parts differently try to avoid it. + +Not all of this folders and files needs to be in every module. The only mandatory file is `index.ts` which is the entry point. The rest depends on your needs and module functionality. + +- `components` - Components logic related to this module (eg. Microcart for Cart module). Normally it contains `.ts` files but you can also create `.vue` files and provide some baseline markup if it is required for the compoennt to work out of the box. +- `pages` - If you want to provide full pages with your module palce them here. It's also a good practice to extend router configuration for this pages +- `store` - Vuex Module associated to this module + - `index.ts` - Entry point and main export of your Vuex Module. Ations/getters/mutations can be splitted into different files if logic is too complex to keep it in one file. Should be used in `store` config property. + - `mutation-types.ts` - Mutation strings represented by variables to use instead of plain strings + - `plugins.ts` - Good place to put vuex plugin. Should be used in `store.plugins` config object +- `types` - TypeScript types associated with module +- `test` - Folder with unit tests which is *required* for every new or rewritten module. +- `hooks` - before/after hooks that are called before and after registration of module. + - `beforeRegistration.ts` - Should be used in `beforeRegistration` config property. + - `bafterRegistration.ts` - Should be used in `afterRegistration` config property. +- `router` - routes and navigation guards associated to this module + - `routes.ts`- array of route objects that will be added to current router configuration. Should be used in `router.routes` config property. + - `beforeEach.ts` - beforEeach navigation guard. Should be used in `router.beforeEach` config property. + - `afterEach.ts`- afterEach navigation guard. Should be used in `router.afterEach` config property. +- `queries` - GraphQL queries +- `helpers` - everything else that is meant to support modules behavior +- `index.js` - entry point for the module. Should export VueStorefrontModule. It's also a good palce to instantiate cache storage. + +# Rules and good practices + +First take a look at module template. It cointains great examples, good practices and explainations for everything that can be putted in module. + +1. **Try not to rely on any other data sources than `config`**. Use other stores only if it's the only way to achieve some functionality and import `rootStore` for this purposes. Modules should be standalone and rely only on themeselves +2. Place all reusable features as a Vuex actions (e.g. `addToCart(product)`, `subscribeNewsletter()` etc) instead of placing them in components. try to use getters for modified or filtered values from state. We are trying to place most of the logic in Vuex stores to allow easier core updates. Here is a good example of such externalisation. +````js +export const Microcart = { + name: 'Microcart', + computed: { + productsInCart () : Product[] { + return this.$store.state.cart.cartItems + }, + appliedCoupon () : AppliedCoupon | false { + return this.$store.getters['cart/coupon'] + }, + totals () : CartTotalSegments { + return this.$store.getters['cart/totals'] + }, + isMicrocartOpen () : boolean { + return this.$store.state.ui.microcart + } + }, + methods: { + applyCoupon (code: String) : Promise { + return this.$store.dispatch('cart/applyCoupon', code) + }, + removeCoupon () : Promise { + return this.$store.dispatch('cart/removeCoupon') + }, + toggleMicrocart () : void { + this.$store.dispatch('ui/toggleMicrocart') + } + } +} +```` +3. Don't use EventBus. +4. If you want to inform about success/failure of core component's method you can eaither use a callback or scoped event. Omit Promises if you thing that function can be called from the template and you'll need the resolved value. This is a good example of method that you can call either on `template` ot `script` section: +````js +addToCart(product, success, failure) { + this.$store.dispatch('cart/addToCart').then(res => + success(res) + ).catch(err => + failure(err) + ) +} +```` + +Try to choose method basing on use case. [This](https://github.com/DivanteLtd/vue-storefront/blob/develop/core/modules/mailchimp/components/Subscribe.ts#L28) is a good example of using callbacks. + +5. Create pure functions that can be easly called with different argument. Rely on `data` properties instead of arguments only if it's required (for example they are validated like [here](https://github.com/DivanteLtd/vue-storefront/blob/develop/core/modules/mailchimp/components/Subscribe.ts#L28). +6. Document exported compoennts like in example: https://github.com/DivanteLtd/vue-storefront/blob/develop/core/modules/mailchimp/components/Subscribe.ts +7. If your module core functionality is an integration with external service better name it the same as this service (for example `mailchimp`) +8. Use named exports and typecheck. # Contributions diff --git a/doc/api-modules/cart.md b/doc/api-modules/cart.md index c62b4fc120..87a21c6fd2 100644 --- a/doc/api-modules/cart.md +++ b/doc/api-modules/cart.md @@ -1,81 +1,49 @@ # Cart module -The cart module as name suggests is a set of mixins responsible for interacting with Cart. You can find methods responsible for adding/removing/getting cart items along with optional UI interactions for microcart. - -## Content - -#### addToCart -- [method] addToCart(product) - -#### removeFromCart -- [method] removeFromCart(product) - -#### applyCoupon -- [method] applyCoupon(code) - -#### removeCoupon -- [method] removeCoupon() - -#### productsInCart -- [computed] productsInCart - -#### cartTotals -- [computed] cartTotals - -#### cartShipping -- [computed] cartShipping - -#### cartPayment -- [computed] cartPayment - -#### appliedCoupon -- [computed] appliedCoupon - -## UI helpers - -#### openMicrocart -- [method] openMicrocart() - -#### closeMicrocart -- [method] openMicrocart() - -#### isMicrocartOpen -- [computed] isMicrocartOpen - -## Example - -````javascript -// Inside Vue component -import { - addToCart, - removeFromCart, - applyCoupon, - removeCoupon, - productsInCart, - appliedCoupon, - totals, - shipping, - payment, - closeMicrocart, - openMicrocart, - isMicrocartOpen -} from '@vue-storefront/core/modules/cart/features' - -export default { - //...other properties - mixins: [ - addToCart, - removeFromCart, - applyCoupon, - removeCoupon, - productsInCart, - appliedCoupon, - totals, - shipping, - payment, - closeMicrocart, - openMicrocart, - isMicrocartOpen - ] -} -```` +This module contains all logic and components related to cart operations. + +## Components + +### AddToCart +Component responsible for adding product to the cart + +**Props** +- `product` - product that'll be added to cart + +**Methods** +- `addToCart(product)` - adds passed product to the cart. By default correlates with `product` prop + +### Microcart +Microcart component. + +**Computed** +- `productsInCart` - array of products that are currently in the cart +- `appliedCoupon` - return applied cart coupon or false if no coupon was applied +-` totals` - cart totals +- `isMicrocartOpen` - returns true if microcart is open + +**Methods** +- `applyCoupon(code)` - appies cart coupon +- `removeCoupon()` removes currently applied cart coupon +- 'toggleMicrocart' - open/close microcart + +### MicrocartButton +Component responsible for opening/closing Microcart + +**Computed** +- `quantity` - number of products in cart + +**Methods** +- `toggleMicrocart` - open/close microcart + +### Product +Component representing product in microcart. Allows to modify it's quantity or remove from cart. + +**Compued** +- `thumbnail` - returns src of products thumbnail + +**Methods** +- `removeFromCart` - removes current product (data property `product`) from cart +- `updateQuantity` - updates cart quantity for current product (data property `product`) + + diff --git a/doc/api-modules/mailer.md b/doc/api-modules/mailer.md new file mode 100644 index 0000000000..45cac88ca5 --- /dev/null +++ b/doc/api-modules/mailer.md @@ -0,0 +1,43 @@ +# Mailer module + +The Mailer module is responsible for sending emails. Currently this module consists of EmailForm component that has sendEmail method for sending emails. This method can also be used for sending confirmation emails. **PLEASE NOTE** You have to set an SMTP transport in vs-api configuration file (local.json) before you start using this module. Transport properties can be found in `extensions.mailService.transport`. + +## Content + +### sendEmail +- [method] **sendEmail**(letter, success, failure) +* **letter** - an object, that defines the details of the email, namely: + 1. sourceAddress - mandatory field, string, defines the source email address from which email will be sent (if smtp transport supports it, otherwise transport's address will be used). + 2. targetAddress - mandatory field, string, defines the address to which email will be sent. This address has to be from the white list, defined in vs-api configuration file. + 3. subject - mandatory field, string, email's subject. + 4. emailText - mandatory field, string, email's body text. + 5. confirmation - optional field, boolean, defines whether this is a confirmation email. The only difference of a confirmation email from a normal email is that the source address needs to be from the white list, defined in vs-api configuration file. +* **success** - a callback function that is called if email has successfully been sent. This callback has a single `message` parameter that contains a default text about successfull sending. +* **failure** - a callback function that is called if email could not be sent. This callback also has a single `message` parameter that contains a text with details about unsuccessfull sending. + +## Example + +````javascript +// Inside Vue component +import { + EmailForm +} from '@vue-storefront/core/modules/mailer/components/EmailForm' + +export default { + //...other properties + mixins: [ + EmailForm + ], + // if we use it inside of a method + methods: { + someMethod () { + this.sendEmail({ + sourceAddress: '', + targetAddress: '', + subject: '', + emailText: '' + }) + } + } +} +```` diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 8bceed2974..bb65071d6a 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -39,26 +39,52 @@ module.exports = { 'basics/feature-list', 'basics/recipes', 'basics/typescript', + 'basics/graphql', + 'basics/ssr-cache', ], }, - // { - // title: 'Vue Storefront core and themes', - // collapsable: false, - // children: [ - // 'core-themes/themes', - // 'core-themes/webpack', - // 'core-themes/core-components', - // 'core-themes/plugins', - // 'core-themes/vuex', - // 'core-themes/data', - // 'core-themes/extensions', - // ], - // }, - // { - // title: 'Data in Vue Storefront', - // collapsable: false, - // children: ['data/data'], - // }, + { + title: 'Core and themes', + collapsable: false, + children: [ + 'core-themes/themes', + 'core-themes/layouts', + 'core-themes/core-components', + 'core-themes/ui-store', + 'core-themes/translations', + 'core-themes/service-workers', + 'core-themes/webpack', + 'core-themes/plugins', + 'core-themes/core-components-api', + ], + }, + { + title: 'Components', + collapsable: false, + children: [ + 'components/home-page', + 'components/category-page', + 'components/product', + 'components/modal', + ], + }, + { + title: 'Data in Vue Storefront', + collapsable: false, + children: [ + 'data/data', + 'data/elasticsearch', + 'data/data-migrations', + 'data/elastic-queries', + 'data/database-tool', + 'data/entity-types', + ], + }, + { + title: 'Working with Vuex', + collapsable: false, + children: ['vuex/introduction', 'vuex/product-store'], + }, // { // title: 'Working with extensions', // collapsable: false, diff --git a/docs/.vuepress/public/Vue-storefront-architecture.png b/docs/.vuepress/public/Vue-storefront-architecture.png new file mode 100644 index 0000000000..d9d27aeee2 Binary files /dev/null and b/docs/.vuepress/public/Vue-storefront-architecture.png differ diff --git a/docs/.vuepress/public/cart-localstorage.png b/docs/.vuepress/public/cart-localstorage.png new file mode 100644 index 0000000000..eba2da24e4 Binary files /dev/null and b/docs/.vuepress/public/cart-localstorage.png differ diff --git a/docs/.vuepress/public/categories-localstorage.png b/docs/.vuepress/public/categories-localstorage.png new file mode 100644 index 0000000000..3be79872e8 Binary files /dev/null and b/docs/.vuepress/public/categories-localstorage.png differ diff --git a/docs/.vuepress/public/chrome-dev-console.png b/docs/.vuepress/public/chrome-dev-console.png new file mode 100644 index 0000000000..707b5c5f41 Binary files /dev/null and b/docs/.vuepress/public/chrome-dev-console.png differ diff --git a/docs/.vuepress/public/orders-localstorage.png b/docs/.vuepress/public/orders-localstorage.png new file mode 100644 index 0000000000..34c1ffa548 Binary files /dev/null and b/docs/.vuepress/public/orders-localstorage.png differ diff --git a/docs/guide/basics/graphql.md b/docs/guide/basics/graphql.md new file mode 100644 index 0000000000..d4f2a9fd21 --- /dev/null +++ b/docs/guide/basics/graphql.md @@ -0,0 +1,65 @@ +# GraphQL Action Plan + +Starting with Vue Storefront 1.4.0 (currently on develop branch) we're supporting two ways of getting data from the backend: + +- existing `api` mode - which is using ElasticSearch DSL as a query language, +- new `graphql` mode - which is using GraphQL queries. + +You can set the desired API format in the `config/local.json` - and `vue-storefront-api` is supporting both of them, however [the default is still set to `api`](https://github.com/DivanteLtd/vue-storefront/blob/4cbf866ca93f917b04461d3ae139a2d26ddf552a/config/default.json#L6). + +We've introduced an abstract [`SearchQuery`](https://github.com/DivanteLtd/vue-storefront/tree/develop/core/store/lib/search) interface with switchable Query Adapters to provide the abstraction layer. This is ultra cool feature especially when you're integrating Vue Storefront with custom backend application: you're able [to create your own adapter](https://github.com/DivanteLtd/vue-storefront/blob/develop/core/store/lib/search/adapter/factory.js) to customize the way data is gathered from the backend. + +From now on the **bodybuilder** package is **deprecated** and you should start using the `SearchQuery` interface to build the search queries that will be translated to GraphQL / API queries. + +Here is an example on how to build the Query: + +```js +export function prepareRelatedQuery(key, sku) { + let relatedProductsQuery = new SearchQuery(); + + relatedProductsQuery = relatedProductsQuery.applyFilter({ + key: key, + value: { in: sku }, + }); + + relatedProductsQuery = relatedProductsQuery + .applyFilter({ key: 'visibility', value: { in: [2, 3, 4] } }) + .applyFilter({ key: 'status', value: { in: [0, 1, 2] } }); // @TODO Check if status 2 (disabled) was set not by occasion here + + if (config.products.listOutOfStockProducts === false) { + relatedProductsQuery = relatedProductsQuery.applyFilter({ + key: 'stock.is_in_stock', + value: { eq: true }, + }); + } + + return relatedProductsQuery; +} + +let relatedProductsQuery = prepareRelatedQuery(key, sku); + +this.$store + .dispatch('product/list', { + query: relatedProductsQuery, + size: 8, + prefetchGroupProducts: false, + updateState: false, + }) + .then(response => { + if (response) { + this.$store.dispatch('product/related', { + key: this.type, + items: response.items, + }); + this.$forceUpdate(); + } + }); +``` + +[More information on how to query the data](https://github.com/DivanteLtd/vue-storefront/blob/develop/doc/data/ElasticSearch%20Queries.md). + +**bodybuilder** queries are still supported by our backward-compatibility mode so if you've used bodybuilder in your theme, it's fine as long as you're using the `api` mode for the backend queries. + +The **legacy queries** using bodybuilder will still work - and [here is an example](https://github.com/pkarw/vue-storefront/blob/28feb8e5dc30ec216353ef87a859212379901c57/src/extensions/template/index.js#L36). + +You can also use direct **ApolloQuery** GraphQL queries thanks to `vue-apollo` support. Please find the example [in here](https://github.com/DivanteLtd/vue-storefront/blob/4cbf866ca93f917b04461d3ae139a2d26ddf552a/src/themes/default/components/core/blocks/SearchPanel/SearchPanel.gql.vue#L21). diff --git a/docs/guide/basics/project-structure.md b/docs/guide/basics/project-structure.md index 0373f53aa3..c18ec38eaf 100644 --- a/docs/guide/basics/project-structure.md +++ b/docs/guide/basics/project-structure.md @@ -31,7 +31,7 @@ Below you can find the Vue Storefront project structure with explanations and co - `router` - Core Vue Router setup. The definition of routes happens in `{themeroot}/index.js` - `scripts` - Core scripts like app installer, extension installer etc. - `service-worker` - Core service worker. It's merged with `sw-precache` data from `build` and `{theme}/service-worker-ext.js` - - `store` - Core Vuex stores (related: [Working with Vuex](../data/vuex.md), [Working with data](../core-themes/data.md)) + - `store` - Core Vuex stores (related: [Working with Vuex](../vuex/introduction.md), [Working with data](../core-themes/data.md)) - `src` - Main project folder containing Vue Storefront core and themes. This is your app playground so you can modify this folder. - `extensions` - Custom extensions made for Vue Storefront like integration with MailChimp or support for Google Analytics) (see: [Working with extensions](../core-themes/extensions.md)) diff --git a/docs/guide/basics/recipes.md b/docs/guide/basics/recipes.md index 298ee42064..9ad0d2e959 100644 --- a/docs/guide/basics/recipes.md +++ b/docs/guide/basics/recipes.md @@ -216,7 +216,7 @@ It's done via Database Tool schema changes. Please follow the instructions from Unfortunately, Magento extensions are not compliant with any PWA available solution yet. So if you would like to integrate some existing extensions, the simplest way is to: - expose the data via some Magento2 REST api endpoints; -- consume the endpoints in the VS using Vuex stores; [read more](../data/vuex.md) about Vuex in Vue Storefront; +- consume the endpoints in the VS using Vuex stores; [read more](../vuex/introduction.md) about Vuex in Vue Storefront; - implement the UI in VS If the extensions are not playing with the User Interface, probably they will work with VS out of the box, as we're using the standard Magento2 API calls for the integration part. diff --git a/docs/guide/data/ssr-cache.md b/docs/guide/basics/ssr-cache.md similarity index 56% rename from docs/guide/data/ssr-cache.md rename to docs/guide/basics/ssr-cache.md index 238ef5f9d0..e7a32ca95a 100644 --- a/docs/guide/data/ssr-cache.md +++ b/docs/guide/basics/ssr-cache.md @@ -1,6 +1,6 @@ # SSR Cache -Vue Storefront generates the Server Side rendered pages to improve the SEO results. In the latest version of Vue Storefront we've added the Output cache option (disabled by default) to improve the performance. +Vue Storefront generates the Server Side rendered pages to improve the SEO results. In the latest version of Vue Storefront we've added the Output cache option (disabled by default) to improve performance. The output cache is set by the following `config/local.json` variables: @@ -23,7 +23,7 @@ The output cache is set by the following `config/local.json` variables: ## Dynamic tags -The dynamic tags config uption: `useOutputCacheTaging` - if set to true, Vue Storefront is generating the special HTTP Header `X-VS-Cache-Tags` +The dynamic tags config option: `useOutputCacheTaging` - if set to `true`, Vue Storefront is generating the special HTTP Header `X-VS-Cache-Tags` ```js res.setHeader('X-VS-Cache-Tags', cacheTags); @@ -35,24 +35,31 @@ Cache tags are assigned regarding the products and categories which are used on X-VS-Cache-Tags: P1852 P198 C20 ``` -The tags can be used to invalidate the Varnish cache if You're using it. [Read more on that](https://www.drupal.org/docs/8/api/cache-api/cache-tags-varnish). +The tags can be used to invalidate the Varnish cache if you're using it. [Read more on that](https://www.drupal.org/docs/8/api/cache-api/cache-tags-varnish). ## Redis -If both `useOutputCache` and `useOutputCacheTagging` options are set to `true` - Vue Storefront is using Output Cache stored in Redis (configured in the `redis` section of the config file). Cache is tagged with Dynamic tags and can be invalidated using special webhook: +If both `useOutputCache` and `useOutputCacheTagging` options are set to `true` - Vue Storefront is using Output Cache stored in Redis (configured in the `redis` section of the config file). Cache is tagged with Dynamic tags and can be invalidated using a special webhook: Example call to clear all pages containing specific product and category: -`curl http://localhost:3000/invalidate?tag=P1852,C20` + +```bash +curl http://localhost:3000/invalidate?tag=P1852,C20 +``` Example call to clear all product, category and home pages: -`curl http://localhost:3000/invalidate?tag=product,category,home` -**WARNING:** -We strongly recommend You to NOT USE Output cache in the development mode. By using it You won't be able to refresh the UI changes after modyfing the Vue components etc. +```bash +curl http://localhost:3000/invalidate?tag=product,category,home +``` + +:::danger Warning +We strongly recommend you NOT TO USE Output cache in the development mode. By using it you won't be able to refresh the UI changes after modifying the Vue components etc. +::: ## CLI cache clear -You can manualy clear Redis cache for specific tags by running the following command: +You can manually clear Redis cache for specific tags by running the following command: ```bash npm run cache clear @@ -63,25 +70,27 @@ npm run cache clear -- --tag=* Available tag keys are set in the `config.server.availableCacheTags` (by default: `"product", "category", "home", "checkout", "page-not-found", "compare", "my-account", "P", "C"`) -**Dynamic cache invalidation:** Recent version of [mage2vuestorefront](https://github.com/DivanteLtd/mage2vuestorefront) do support output cache invalidation. Output cache is being tagged with the product and categories id (products and categories used on specific page). Mage2vuestorefront can invalidate cache of product and category pages if You set the following ENV variables: +**Dynamic cache invalidation:** Recent version of [mage2vuestorefront](https://github.com/DivanteLtd/mage2vuestorefront) supports output cache invalidation. Output cache is being tagged with the product and categories id (products and categories used on specific page). Mage2vuestorefront can invalidate cache of a product and category pages if you set the following ENV variables: ```bash export VS_INVALIDATE_CACHE_URL=http://localhost:3000/invalidate?key=SECRETKEY&tag= export VS_INVALIDATE_CACHE=1 ``` -**SECURITY NOTE:** Please note that `key=SECRETKEY` should be equal to `vue-storefront/config/local.json` value of `server.invalidateCacheKey` +:::warning Security note +Please note that `key=SECRETKEY` should be equal to `vue-storefront/config/local.json` value of `server.invalidateCacheKey` +::: ## Adding new types / cache tags -If You're adding new type of page (`core/pages`) and `config.server.useOutputCache=true` - You should also extend the `config.server.availableCacheTags` of new general purpose tag that will be connected with the URLs connected with this new page. +If you're adding a new type of page (`core/pages`) and `config.server.useOutputCache=true`, you should also extend the `config.server.availableCacheTags` of new general purpose tag that will be connected with the URLs connected with this new page. -After doing so, please add the `asyncData` method to Your page code assigning the right tag (please take a look at `core/pages/Home.js` for instance): +After doing so, please add the `asyncData` method to your page code assigning the right tag (please take a look at `core/pages/Home.js` for instance): ```js - asyncData ({ store, route }) { // this is for SSR purposes to prefetch data + asyncData ({ store, route, context }) { // this is for SSR purposes to prefetch data return new Promise((resolve, reject) => { - store.state.requestContext.outputCacheTags.add(`home`) + if (context) context.output.cacheTags.add(`home`) console.log('Entering asyncData for Home root ' + new Date()) EventBus.$emitFilter('home-after-load', { store: store, route: route }).then((results) => { return resolve() @@ -96,7 +105,7 @@ After doing so, please add the `asyncData` method to Your page code assigning th This line: ```js -store.state.requestContext.outputCacheTags.add(`home`); +if (context) context.output.cacheTags.add(`home`); ``` -... is in charge of assigning the specific tag with current HTTP request output. +is in charge of assigning the specific tag with current HTTP request output. diff --git a/docs/guide/components/category-page.md b/docs/guide/components/category-page.md new file mode 100644 index 0000000000..2d6fc94e2b --- /dev/null +++ b/docs/guide/components/category-page.md @@ -0,0 +1,44 @@ +# Core Category Page + +## Props + +No props + +## Data + +- `pagination` - an object that defines two settings: + - `perPage` of product items to load per page, currently set to 50 + - `offset` that probably defines which page has been last loaded, currently set to 0 and isn't changed anywhere. +- `enabled` - enables/disables paging. When it's disabled it lazy-loads other products on scroll +- `filters.available`, `filters.chosen` - a set of filters that user has defined on Category page - there we have available filters and chosen filter values +- `products` - computed property that returns a list of product items of current category from the Vuex store. +- `isCategoryEmpty` - computed property that returns `true` if product list of current category is empty. +- `category` - computed property that returns current category from the Vuex store. +- `categoryName` - category name. +- `categoryId` - category ID. +- `breadcrumbs` - breadcrumbs for the current category from the Vuex store. +- `productsCounter` - how many products are in the category. +- `lazyLoadProductsOnscroll` - allows lazy-loading more products on scroll, by default it's true + +## Methods + +- `fetchData ({ store, route })` - prepares query for fetching a list of products of the current category and dispatches `product/list` action that extracts that list. + + - `{ store, route }` - an object consisting of the Vuex store and global router references. + +- `validateRoute ({ store, route })` - this method is called whenever the global `$route` object changes its value. It dispatches `'category/single'` action to load current category object and then calls `fetchData` method to load a list of products that relate to this category. + - `{ store, route }` - an object consisting of the Vuex store and global router references. + +## Events + +### asyncData + +Since the app is using SSR, this method prefetches and resolves the asynchronous data before rendering happens and saves it to Vuex store. Asynchronous data for Category page is a list of all categories, category attributes and list of products for each category. + +### beforeMount + +`filter-changed-category` event listener is initialized. The event is fired when user selects custom filter value. + +### beforeDestroy + +`filter-changed-category`event listener is removed. diff --git a/docs/guide/components/home-page.md b/docs/guide/components/home-page.md new file mode 100644 index 0000000000..26ce59c3d6 --- /dev/null +++ b/docs/guide/components/home-page.md @@ -0,0 +1,25 @@ +# Core Home Page + +:::tip Note +Core page has almost zero functionality, everything is in theme component, which definitely needs be replaced to core. +::: + +## Props + +No props + +## Data + +`rootCategories` category list to be used for your own custom home page + +## Methods + +No methods + +## Events + +`home-after-load` event can be used to populate the vuex `store` with additional data required by SSR. + +### beforeMount + +Clears Vuex store entries that define current category by dispatching `category/reset` action. diff --git a/docs/guide/components/modal.md b/docs/guide/components/modal.md new file mode 100644 index 0000000000..327972fcc6 --- /dev/null +++ b/docs/guide/components/modal.md @@ -0,0 +1,39 @@ +# Modal component + +Simple modal component. Visibility of modal container is based on internal state `isVisible`. We can set this state by `$emit` event on global `$bus` event. + +## Basic usage + +### Component markup + +```html + +
Lorem Ipsum is simply dummy text of the printing and typesetting industry.
+
+``` + +### Available events: + +```html + + + +``` + +### Available props + +| Prop | Type | Required | Default | Description | +| ----- | ------ | -------- | ------- | --------------------- | +| name | String | true | | Unique name of modal | +| delay | Number | false | 300 | Timeout to show modal | + +### Available Methods + +| Method | Argument | Description | +| ------ | -------------- | ---------------------------------------------------------- | +| toggle | state: Boolean | Manually toggles a modal | +| close | | Alias for manually hides a modal. Helpful for Close button | + +### Styles + +Core component doesn't have css styles. If you want to see an example of our implementation please look [here](https://github.com/DivanteLtd/vue-storefront/blob/master/src/themes/default/components/core/Modal.vue) diff --git a/docs/guide/components/product.md b/docs/guide/components/product.md new file mode 100644 index 0000000000..97e08d8611 --- /dev/null +++ b/docs/guide/components/product.md @@ -0,0 +1,106 @@ +# Core Product Page + +## Props + +No props + +## Data + +- `loading` - if `true` indicates the product is currently being loaded from the backend. +- `favorite` - an object that defines 1) if the current product is in the list of favorite products and 2) name of an icon that will be shown to indicate its status in relation to being in the list of favorite products. +- `compare` - defines if the current product is in compare list. +- `product` - a computed property that represents current product that is shown on the page. Initially gets its value from `product/productCurrent` Vuex store getter. Includes all the options like size and color that user sets on the page. +- `originalProduct` - a computed property that represents current product in its initial state. Gets its value from `product/productOriginal` Vuex store getter. +- `parentProduct` - a computed property that represents current product parent product, if any. Gets its value from `product/productParent` Vuex store getter. +- `attributesByCode` - a computed property that returns the list of all product attributes by their code. Gets its value from `attribute/attributeListByCode` Vuex store getter. +- `attributesById` - a computed property that returns the list of all product attributes by their id. Gets its value from `attribute/attributeListById` Vuex store getter. **This prop is not used anywhere**. +- `breadcrumbs` - a computed property that represents breadcrumbs for the current product. Gets its value from `product/breadcrumbs` Vuex store getter. +- `configuration` - a computed property that represents an object that shows which attributes (like size and color) are chosen on the product. Gets its value from `product/currentConfiguration` Vuex store getter. +- `options` - a computed property that represents an object that shows what attributes (like size and color) with what values are available on the product. Gets its value from `product/currentOptions` Vuex store getter. +- `category` - a computed property representing a category object of the current product. Gets its value from `category/current` Vuex store getter. +- `productName` - a computed property that represents a product name. Gets its value from `category/current` Vuex store getter. +- `productId` - a computed property representing a product id. Gets its value from `category/current` Vuex store getter. +- `isOnCompare` - a computed property that checks if a given product is in compare list. +- `image` - a computed property that defines an image (thumbnail) that will be shown on the page and its size. +- `customAttributes` - this is a subset of `attributesByCode` list of attributes that the current product has. + +## Methods + +### Unbound methods + +#### filterChanged (filterOption) + +Sets attributes on the product according to what the user has chosen on the page. Dispatches `product/configure` action. + +:::tip Note +This method is called when 'filter-changed-product' event is triggered, but it's not triggered anywhere in the code. +::: + +_Parameters_ + +- `filterOption` - an object that represents an attribute that have changed on the product. + +#### fetchData (store, route) + +Fetches current product data from the backend by dispatching `product/single` action. Also dispatches several other actions to get breadcrumbs, product attributes, variants for configurable product, also to set sub-products if the product is grouped. + +_Parameters_ + +- `store` - Vuex store +- `route` - global router object + +#### loadData ({ store, route }) + +Dispatches `product/reset` action that sets current product to original product, nullifies all the configuration and options, then calls `fetchData` method to load current product data. + +_Parameters_ + +- `{store, route}` - an object that consists of references to Vuex store and global router object. + +#### stateCheck + +If current product has a parent, redirects to a parent product page. Then checks if the current product is in the wishlist or in the compare list, sets `favorite` and `compare` props accordingly. + +_Parameters_ +No parameters + +### Bound methods + +#### validateRoute + +This method is called whenever the global `$route` object changes its value. Calls `loadData` and `stateCheck` methods. + +_Parameters_ +No parameters + +#### addToList + +Adds the current product to the compare by dispatching `compare/addItem` action accordingly. + +_Parameters_ + +- `list` - compare + +#### removeFromList + +Removes the current product from the compare by dispatching `compare/removeItem` action accordingly. + +_Parameters_ + +- `list` - compare + +## Hooks + +### asyncData + +Since the app is using SSR, this method prefetches and resolves the asynchronous data before rendering happens and saves it to Vuex store. On Product page this is done by calling `loadData` method. + +The `asyncData` fires the `product-after-load` event which can be used to populate the Vuex SSR store for additional data regarding the product. + +### beforeMount + +Calls `stateCheck` method. Defines `product-after-priceupdate` event listener which, if triggered, dispatches `product/setCurrent` action that sets current product object in Vuex store. Also defines `filter-changed-product` event listener which, if triggered, calls `filterChanged` method. **Currently 'filter-changed-product' event is not triggered anywhere.** + +### beforeDestroy + +Removes event listeners that were defined in `beforeMount` hook. diff --git a/docs/guide/core-themes/core-components-api.md b/docs/guide/core-themes/core-components-api.md new file mode 100644 index 0000000000..08f55759d3 --- /dev/null +++ b/docs/guide/core-themes/core-components-api.md @@ -0,0 +1,627 @@ +# Core components API + +:::warning Note +Temporary file for core components API. List of props, public methods and data that are available to use via mixin insertion. +In case of injectable components (like modal) or the ones triggered by Vuex actions you should write them down also. Feel free to write some description/new api proposal for each documented component. +::: + +## AddToCart + +This component represents a single button that when pressed adds a product to cart. Right now this component is used on Product page only. + +### Props + +- `product` - An instance of a product + +### Data + +No data + +### Methods + +- `addToCart (product)` - Dispatches 'cart/addItem' action and passes a product instance as a parameter. + +#### Parameters + +_product_ - An instance of a product + +## BaseCheckbox + +This component represents a checkbox with label and validation. + +### Props + +- `id` - id for a checkbox input and a label +- `validation.condition` - vuelidate if statement +- `validation.text` - validation error text +- `disabled` - boolean prop to disable checkbox input + +### Data + +No data + +### Methods + +No methods + +## BaseInput + +This component represents an input with validation. + +### Props + +- `type` - input type +- `name` - input name +- `placeholder` - input placeholder +- `autocomplete` - input autocomplete +- `focus` - boolean prop that defines if this input is autofocused +- `validation.condition` - vuelidate if statement +- `validation.text` - validation error text +- `validations` - array of validation objects to apply multiple validations + +### Data + +- `passType` - copy of password type for toggle password visibility +- `iconActive` - boolean prop that defines if visibility button is enabled +- `icon` - default material icon name for visibility button + +### Methods + +- `togglePassType` - toggle password visibility and icon name + +## Breadcrumbs + +This component represents a hierarchy of the current page in relation to the application structure. It is used in Product, Category, My Account and Compare pages. + +### Props + +- `routes` - An array of route objects, each representing name and path to a parent page. +- `activeRoute` - A name of the current page + +### Data + +No data + +### Methods + +No methods + +## ColorSelector + +This components represents a button that is used for visualizing different options, specifically product filters. It is used on category's Sidebar and Product pages. + +### Props + +- `label` - label that is shown on the component button. +- `id` - identifier that unifies an option among others in array. +- `code` - a name of an option that the component is being used to represent (currently 'color'). +- `context` - a name of an entity that the component belongs to (currently one of 'category' or 'product'). + +### Data + +- `active` - boolean prop that defines if button is pressed and is active. + +### Methods + +- `switchFilter (id, label)` - triggers `filter-changed-` event, where context is a value of _context_ prop. + +#### Parameters + +_id_ - same as _id_ prop. +_label_ - same as _label_ prop. + +### Hooks + +#### beforeMount + +If current route's name is not 'product' defines 2 event listeners. First one is `filter-reset` that sets _active_ prop to false. Second is `filter-changed-`, where context is a value of _context_ prop. This event listener toggles the value of _active_ prop depending on which instance of ColorSelector component was passed to it as a parameter. + +#### beforeDestroy + +Removes event listeners defined in `beforeMount` hook. + +## Loader + +This component is used for visualizing loading process, when something is happening in the background. It is currently used when account is being registered, password is being reset and user is logging in. + +### Props + +No props + +### Data + +- `message` - A message that is shown while loading process is on. +- `isVisible` - Computed property which is equal to UI-store's _loader_ property. This prop defines whether to show or hide the spinner. + +### Methods + +- `show (message = null)` - Sets message property and calls UI-store mutation 'ui/setLoader', which causes a spinner to show up. + +#### Parameters + +_message_ - A text that is shown when loading process is on. + +- `hide` - Calls UI-store mutation 'ui/setLoader', which hides a spinner. + +#### Parameters + +No parameters + +### Hooks + +#### Mounted + +Two listeners are defined: +`'notification-progress-start'` - calls _show_ method. +`'notification-progress-stop'` - calls _hide_ method. + +## Logo + +This component is intended to serve an image of an application logo and to navigate the user to a Home page when the logo is pressed. + +### Props + +No props + +### Data + +No data + +### Methods + +No methods + +## Newsletter popup (should be using modal) + +Shows popup modal window where user can enter his/her newsletter subscription preferences, currently only email address. This component is used in a Footer page. + +### Props + +No props + +### Data + +No data + +### Methods + +- `closeNewsletter` - Closes newsletter popup modal window by calling UI-store mutation 'ui/setNewsletterPopup'. + +## Notification + +Shows notifications after some actions being performed in shop. There are four types of notifications: success, error, warning and info. + +### Props + +No props + +### Data + +- `notifications` - An Array of notifications that should be currently displayed + +### Methods + +- `action(action, id)` - Performs an `action` defined as String on notification with passed `id` - usually the action is `close`. Actions are defined in Notification component. Current Notification object schema: + +```json +{ + // Choose one + "type": "info/success/error/warning", + "title": "Lorem ipsum", + "action1": { + "label": "OK", + "action": "close" + }, + // Optional param + "action2": { + "label": "NO", + "action": "close" + }, + // Optional param, if its empty TTL is 5s + "timeToLive": 10 +} +``` + +### Events + +- `('notification', notificationObject)` - takes notification object and adds it to Notification array, then the new notification is displayed + +## OfflineBadge + +When there's no active internet connection, shows notification about getting offline at the bottom of the screen. + +### Props + +No props + +### Data + +- `isOnline` - defines if there is an active internet connection + +### Methods + +No methods + +### Hooks + +#### Mounted + +Sets isOnline data property and defines two event listeners: + +- `'online'` - sets _isOnline_ data property to true. +- `'offline'` - sets _isOnline_ data property to false. + +## Overlay + +This component is used to shadow parts of the screen that are left after opening modal windows, like WishList or Cart. + +### Props + +- `isVisible` - computed property that is equal to _overlay_ property of UI-store. Defines whether to shadow parts of the screen or not. + +### Data + +No data + +### Methods + +- `close` - calls UI-store mutation 'ui/setOverlay' and sets its _overlay_ property to _false_. + +## PriceSelector + +Represents one of the options on Category page. Shows price range and allows user to choose one of the ranges. + +### Props + +- `content` - text that shows the price range +- `id` - unique identifier of the option +- `code` - options' code, equals to 'price' +- `from` - minimum value of the price range +- `to` - maximum value of the price range +- `context` - a name of an entity that the component belongs to (currently 'category') + +### Data + +- `active` - boolean prop that defines if button is pressed and is active. + +### Methods + +- `switchFilter (id, label)` - triggers `'filter-changed-'` event, where context is a value of _context_ prop. + +#### Parameters + +_id_ - same as _id_ prop. +_label_ - same as _label_ prop. + +### Hooks + +#### beforeMount + +Defines 2 event listeners. First one is `filter-reset` that sets _active_ prop to false. Second is `filter-changed-`, where context is a value of _context_ prop. This event listener toggles the value of _active_ prop depending on which instance of PriceSelector component was passed to it as a parameter. + +#### beforeDestroy + +Removes event listeners defined in `beforeMoun`t hook. + +## ProductAttribute + +Shows attributes that a specific product has. Used on Product Page. + +### Props + +- `product` - reference to product that the attribute belongs to +- `attribute` - attribute itself +- `emptyPlaceholder` - a string that is shown if an attribute has no value + +### Data + +- `label` - name of an attribute +- `value` - attribute's value(-s) + +### Methods + +No methods + +### Hooks + +#### beforeMount + +Extracts attribute's label and value(-s) from _product_ and _attribute_ properties. + +## ProductLinks + +If product is grouped (which means it consists of several products) this component shows list of compound products. Used on Product page. + +### Props + +- `products` - array of compound products of a given product + +### Data + +No data + +### Methods + +No methods + +## ProductListing + +Shows given array of products on a page in a given number of columns. Used on Category and Home pages, and also on Related block. + +### Props + +- `product` - array of products to show +- `columns` - number of columns to display on a page. Each product is displayed with ProductTile component. + +### Data + +No data + +### Methods + +No methods + +## ProductSlider + +Shows product tiles slider. Used in Collection component in _default_ theme. + +### Props + +- `title` - a title of a slider +- `products` - an array of products to show in a slider +- `config` - and object that defines configuration of a slider, like number of tiles to show on a page, pagination and looping. + +### Data + +No data + +### Methods + +No methods + +## ProductTile + +Shows a product in a compact way when several products are shown on one page. Used in many places, such as Home page, Search panel, 404 page and so on. + +### Props + +- `product` - a specific product +- `thumbnail` - a computed property that represents a smaller image for the product to show in this component. _The size of an image is hard-coded in this property, it might be better to keep dimensions in a config file._ + +### Data + +No data + +### Methods + +No methods + +## SizeSelector + +Represents one of the options of a product, namely product's size. Used on Category and Product pages. + +### Props + +- `label` - a string that represents the size +- `id` - unique identifier of the size +- `code` - a code name of an option, which is 'size' +- `context` - a name of an entity that the component belongs to (currently one of 'category' or 'product') + +### Data + +`active` - boolean prop that defines if button is pressed and is active. + +### Methods + +- `switchFilter (id, label)` - triggers `filter-changed-` event, where context is a value of _context_ prop. + +#### Parameters + +_id_ - same as _id_ prop. +_label_ - same as _label_ prop. + +### Hooks + +#### beforeMount + +Defines 2 event listeners. First one is `filter-reset` that sets _active_ prop to false. Second is `filter-changed-`, where context is a value of _context_ prop. This event listener toggles the value of _active_ prop depending on which instance of SizeSelector component was passed to it as a parameter. + +#### beforeDestroy + +Removes event listeners defined in `beforeMount` hook. + +## Tooltip + +Shows an informational icon and hint when focused on that icon. Used on My Account and Checkout pages. + +### Props + +No props + +### Data + +No data + +### Methods + +No methods + +## ValidationError + +This was supposed to show a validation error message, but is not used anywhere. _Has to be deleted_ + +### Props + +- `message` - a text that explains the error + +### Data + +No data + +### Methods + +No methods + +# Core pages + +## Category + +Category page has been refactored (1.0RC) to the new core proposal and the [docs has been moved here](../components/category-page.md). + +## Checkout + +### Props + +No props + +### Data + +- `stockCheckCompleted` - a boolean prop that shows if all products in cart (if any) have been checked for availability (whether they are in stock or not). +- `stockCheckOK` - a boolean prop that shows if all products in cart are in stock. +- `orderPlaced` - a boolean prop that is set to true after `order-after-placed` event has been triggered, defining a successful placement of an order. +- `activeSection` - an object that consists of 4 boolean props: _personalDetails_, _shipping_, _payment_ and _orderReview_, - that define which section of Checkout page is currently active. At any point of time only one section can be active. +- `order` - an order object, that consists of all necessary order information that will be sent to the backend to place it. +- `personalDetails` - an object that contains personal details part of the Checkout page. +- `shipping` - an object that contains shipping details part of the Checkout page. +- `payment` - an object that contains payment details part of the Checkout page. +- `orderReview` - _this prop is not used_ +- `cartSummary` - this prop is supposed to be filled after `checkout.cartSummary` event has been triggered. _But this event is not triggered anywhere, therefore this prop currently has no usage._ +- `validationResults` - an object that keeps validation result of 3 child components: Personal Details, Shipping Details and Payment Details. _Currently all the validation happens within those 3 child components and there's no need to store the result in a parent component. This prop is redundant._ +- `userId` - this new user ID is returned by child OrderReview component if a user registers a new account at checkout. It is then sent to the backend to bind an order to the user. +- `isValid` - this boolean computed property defines if an order can be placed. If there's any validation error within any child component or _stockCheckOK_ prop is not true, this returns false and an order won't be placed. + +### Methods + +- `checkConnection (status)` - checks if there's an active internet connection. If not, fires a notification. + +#### Parameters + +_status_ - a boolean parameter that defines if there's an active internet connection. + +- `activateSection (sectionToActivate)` - sets _sectionToActivate_ named section in _activeSection_ prop object to true and all others to false. + +#### Parameters + +_sectionToActivate_ - a name of a section that needs to be activated. + +- `prepareOrder ()` - returns an order object that will be sent to the backend. + +- `placeOrder ()` - if _isValid_ prop is true dispatches `'checkout/placeOrder'` action which will place the order, otherwise fires a notification about existence of validation errors. + +- `savePersonalDetails ()` - dispatches `'checkout/savePersonalDetails'` action which will save checkout personal details information (from _personalDetails_ prop) to the Vuex store. + +- `saveShippingDetails ()` - dispatches `'checkout/saveShippingDetails'` action which will save checkout shipping details information (from _shipping_ prop) to the Vuex store. + +- `savePaymentDetails ()` - dispatches `'checkout/savePaymentDetails'` action which will save checkout payment details information (from _payment_ prop) to the Vuex store. + +### Hooks + +#### created + +Defines several event listeners to communicate with child components. + +- **'network.status'** event listener receives internet connection status and calls _checkConnection_ method. +- **'checkout.personalDetails'** event listener receives personal details information from PersonalDetails child component and activates the next section of the Checkout page (which is shipping details). +- **'checkout.shipping'** event listener receives shipping details information from Shipping child component and activates next section of the Checkout page (which is payment details). +- **'checkout.payment'** event listener receives payment details information from Payment child component and activates next section of the Checkout page (which is order review). +- **'checkout.cartSummary'** - _this event listener is not called anywhere._ +- **'checkout.placeOrder'** event listener is called by OrderReview child component. It has an optional _userId_ parameter that is passed to it in case user registers a new account at the checkout. With or without _userId_ this event listener calls _placeOrder_ method. +- **'checkout.edit'** event listener activates a section of Checkout page, name of which is passed to it in a parameter. +- **'order-after-placed'** event listener sets _orderPlaced_ prop to true. + +#### beforeMount + +Checks if cart is not empty. If it is, then a notification is fired. Otherwise, sets promises that will check availability of the products from the cart and if they are all in stock. + +#### destroyed + +Removes all event listeners that were previously defined in `created` hook. + +## Compare + +### Props + +`title` - title of the Compare page + +### Data + +- `attributesByCode` - a computed property that returns the list of all product attributes by their code. Gets its value from `'attribute/attributeListByCode'` Vuex store getter. +- `attributesById` - a computed property that return the list of all product attributes by their Id. Gets its value from `'attribute/attributeListById'` Vuex store getter. _This prop is not used anywhere._ +- `items` - returns the list of products that were chosen for comparison from Vuex store. +- `all_comparable_attributes` - returns the subset of attributes from `attributesByCode` prop that have `is_comparable` property set to true. + +### Methods + +- `removeFromCompare (product)` - removes a given product from the compare list by dispatching `'compare/removeItem'` action. + +#### Parameters + +_product_ - a specific product to be removed. + +### Hooks + +#### created + +Dispatches `'compare/load'` action that loads the list of products to compare from localStorage into Vuex store. Also dispatches `'attribute/list'` action that loads all product attributes that have `is_user_defined` property set to true into Vuex store. + +## Home + +Home page has been refactored to the new core proposal (1.0RC) and the [docs has been moved](../components/home-page.md). + +## MyAccount + +### Props + +- `activeBlock` - currently active block displayed in component (i.e. newsletter preferences, shipping data or orders history) + +### Data + +- `navigation` - an object that contains names of sections of MyAccount page and anchor links to them. + +### Methods + +`notify (title)` - this is a temporary method that notifies user if he presses on a link of a section that is not yet implemented. + +### Hooks + +#### created + +Defines several event listeners to communicate with child components. + +- **'myAccount.updateUser'** event listener receives filled out data from child components and dispatches `'user/update'` action to update user profile. It's called from PersonalDetails and ShippingDetails child components. +- **'myAccount.changePassword'** event listener receives updated authentication data from PersonalDetails child component and dispatches `'user/changePassword'` action. +- **'myAccount.updatePreferences'** event listener receives user's updated newsletter subscription preferences from MyNewsletter child component and updates them by dispatching `'user/updatePreferences'` action. + +#### mounted + +Checks if there's a user token in localStorage. If not, redirects user to Home page. + +#### destroyed + +Removes all event listeners that were previously defined in `created` hook. + +## PageNotFound + +404 page + +### Props + +No props + +### Data + +No data + +### Methods + +No methods + +### Hooks + +#### asyncData + +Since the app is using SSR, this method prefetches and resolves the asynchronous data before rendering happens and saves it to Vuex store. Asynchronous data for PageNotFound page is a list of 8 random products that are called Bestsellers. + +## Product + +Product page has been refactored to the new core proposal (1.0RC) and the [docs has been moved](../components/product.md). diff --git a/docs/guide/core-themes/core-components.md b/docs/guide/core-themes/core-components.md index 5fa744b1f8..8809ffc9c2 100644 --- a/docs/guide/core-themes/core-components.md +++ b/docs/guide/core-themes/core-components.md @@ -1,3 +1,90 @@ -# Vue Storefront component types +# Working with core components -_Work in progress_ +## Vue Storefront component types + +In Vue Storefront there are two types of components: + +- **Core components** (`core/components`) - In core components we implemented all basic business logic for e-commerce shop, so you don't need to write it from scratch by yourself. You can make use of them in your themes where all you need to do is styling and creating the HTML markup. Every core component provides an interface to interact with. This interface can be extended or overwritten in your theme if you need it. Core components should be injected to themes as mixins. They contain only business logic - HTML markup and styling should be done in themes. + +- **Theme components** (`src/themes/{theme_name}/components`) - the theme component is what you really see in the app. They can inherit business logic from core components or be created as theme-specific components. All CSS and HTML should be placed in theme. A good practice is to create theme components that inherit from specific core components with the same name and in the same path (e.g components inheriting from (`core/components/ProductTile.js`) should be placed (`src/themes/{theme_name}/components/core/ProductTile.vue`) but it's not obligatory and you can structure your theme in any way you want. + +## Using core components in your theme + +### For components + +Inheritance by itself is done by [vue mixins](https://vuejs.org/v2/guide/mixins.html) with default merging strategy. + +To inherit from core component: + +1. **Create new component in your theme** + +2. **Import the core component that you want to include:** + +```js +import YourCoreComponent from '@vue-storefront/core/components/YourCoreComponent'; +``` + +3. **Add core components mixin to your newly created theme component:** + +```js +export default { + ... + mixins: [YourCoreComponent] +} +``` + +From now you can access and override all methods, data and components from core component like it was declared in your own theme component. + +### For pages + +Inheritance in pages works exactly like in other components. The only difference is the importing alias. Instead of `core/components` we need to start with `core/pages` alias + +```js +import YourCorePage from '@vue-storefront/core/pages/YourCorePage' + +export default { + ... + mixins: [YourCorePage] +} +``` + +Core pages are placed in `core/pages` folder. + +## Working with core components + +First of all: **override core components only when you're adding features to the core**. The correct approach for using core components in your theme is thinking of them as an external API. You can inherit the functionalities and extend them in theme but never change it in a core. + +**When you're modifying the core component never change the component's API** (data and methods exposed by component for themes). Such changes would break the themes using this core component. + +### The core component folders structure + +- `core/components` - Components that can be used across whole project should be placed in the root of this folder. +- `core/components/blocks` - All other components specific to pages (e.g Home, Category), other components (e.g Header, Footer) or functionalities (e.g Auth). + +### Rules to follow when creating new core components + +1. Use `.js` files for core mixins instead of `.vue` files +2. Put only theme-agnostic business logic in core components. + +## Core components docs + +:::tip Note +Please keep in mind we are still working on these docs +::: + +### Pages + +- [Home](../components/home-page.md) - [`Home.vue`](https://github.com/DivanteLtd/vue-storefront/blob/master/core/pages/Home.vue) +- [Category](../components/category-page.md) - [`Category.vue`](https://github.com/DivanteLtd/vue-storefront/blob/master/core/pages/Category.vue) +- [Product](../components/product.md) - [`Product.vue`](https://github.com/DivanteLtd/vue-storefront/blob/master/core/pages/Product.vue) +- ... + +### Components + +- [Modal](../components/modal.md) - [`Modal.vue`](https://github.com/DivanteLtd/vue-storefront/blob/master/core/components/Modal.vue) +- ... + +## Related + +- [Working with themes](themes.md) +- [Creating themes in Vue Storefront - Part 1 ('Using Vue Storefront core in your theme' section)](https://medium.com/@frakowski/developing-themes-in-vue-storefront-backend-agnostic-ecommerce-pwa-frontend-part-1-72ea3c939593) diff --git a/docs/guide/core-themes/data.md b/docs/guide/core-themes/data.md deleted file mode 100644 index 931e933b6a..0000000000 --- a/docs/guide/core-themes/data.md +++ /dev/null @@ -1,3 +0,0 @@ -# Working with data - -_Work in progress_ diff --git a/docs/guide/core-themes/layouts.md b/docs/guide/core-themes/layouts.md new file mode 100644 index 0000000000..af13de3e20 --- /dev/null +++ b/docs/guide/core-themes/layouts.md @@ -0,0 +1,187 @@ +# Layouts and advanced output operations + +Starting from version 1.4.0 Vue Storefront allows you to switch the html templates and layouts dynamically in the SSR mode. + +This feature can be very useful for non-standard rendering scenarios like: + +- generating the XML output, +- generating the AMPHTML pages, +- generating widgets without `` section + +## How it works + +Before 1.4.0 Vue Storefront generated the output by mix of: + +- taking the base HTML template `src/index.template.html`, +- rendering the `src/themes/default/App.vue` root component, +- injecting the Vue SSR output into the template + adding CSS styles, script references etc. [Read more on Vue SSR Styles and Scripts injection](https://ssr.vuejs.org/guide/build-config.html#client-config) + +This mode is still in place and it's enabled by default. +What we've changed is **you can now select which html template + layout your app is routing in per-route manner**. + +## Changelog + +The changes we've introduced: + +- now distinct routes can set `context.output.template` in `asyncData` method. By doing so you can skip using `dist/index.html` (which contains typical HTML5 elements - like ``). This is important when we're going to generate either AMPHTML pages (that cannot contain any ` +``` + +The key part is: + +```js +contextserver.response.setHeader('Content-Type', 'text/xml'); +context.output.template = ''; +``` + +These two statements: + +- set the HTTP header (by accessing ExpressJS response object - `contextserver.response`. There is also `contextserver.request` and `context.app` - the ExpressJS application)- set `output.template` to none which will cause to skip the HTML template rendering at all. + +### Switching off layout + injecting dynamic content + +Example URL: `http://localhost:3000/append-prepend.html` + +Route setup to switch the Vue layout: + +```js + { path: '/append-prepend.html', component: NoLayoutAppendPrependExample, meta: { layout: 'empty' } }, +``` + +Vue component to render the XML: + +```js + + + +``` + +The key part is: + +```js +context.output.template = ''; +context.output.append = context => { + return '
This content has been dynamically appended
'; +}; +context.output.prepend = context => { + return '
this content has been dynamically prepended
'; +}; +``` + +These two statements: + +- set `output.template` to none which will cause to skip the HTML template rendering at all. +- add the `output.append` and `output.prepend` methods to the server context. + +The output will be generated with this logic: + +```js +const contentPrepend = + typeof context.output.prepend === 'function' + ? context.output.prepend(context) + : ''; +const contentAppend = + typeof context.output.append === 'function' + ? context.output.append(context) + : ''; +output = contentPrepend + output + contentAppend; +``` + +Please note, that the `context` contains lot of interesting features you can use to control the CSS, SCRIPT and META injection. [Read more on Vue SSR Styles and Scripts injection](https://ssr.vuejs.org/guide/build-config.html#client-config) + +**Note: [The context object = Vue.prototype.$ssrContext](https://ssr.vuejs.org/guide/head.html)** diff --git a/docs/guide/core-themes/plugins.md b/docs/guide/core-themes/plugins.md index 5c9f14bb42..5eede70a50 100644 --- a/docs/guide/core-themes/plugins.md +++ b/docs/guide/core-themes/plugins.md @@ -1,3 +1,43 @@ # Working with plugins -_Work in progress_ +In Vue Storefront there are two types of plugins: + +- **Core plugins** - placed in `core/plugins` and available for any theme and extension. You shouldn't modify these plugins as they are part of upgradable core. +- **Theme plugins** - placed in `src/{theme}/plugins` and available only for specific theme + +Each of these plugins works and is registered like a normal Vue.js plugin. You can read about them [here](https://vuejs.org/v2/guide/plugins.html) + +## Core plugins + +Core plugins are exported in `core/plugins/index.js` file as JavaScript objects + +```js +export { EventBusPlugin, ConfigPlugin }; +``` + +and then registered in `core/app.js` + +```js +Object.keys(pluginsObject).forEach(function(key) { + Vue.use(pluginsObject[key]); +}); +``` + +Currently there are two core plugins: + +- **config** - This plugin is responsible for easy access to your storefront config. It can be accessed via `this.$config` alias +- **event-bus** - Global Event Bus that can be used in any place of the application via `this.$bus` alias. It also provides some functionalities for intercepting and modifying core events. + +## Theme plugins + +It's a good practice to register theme plugins under `{theme}/plugins` folder. + +```js +import Vuetify from 'vuetify'; +// import other plugins + +Vue.use(Vuetify); +// other plugins +``` + +If you want to make a custom plugin for your theme, you should create a directory for it in `src/{theme}/plugins` (eg. `src/{theme}/plugins/custom_plugin`) and register it in `src/{theme}/plugins/index.js` like a 3rd party plugin in example above. diff --git a/docs/guide/core-themes/service-workers.md b/docs/guide/core-themes/service-workers.md new file mode 100644 index 0000000000..80ddc4fe83 --- /dev/null +++ b/docs/guide/core-themes/service-workers.md @@ -0,0 +1,61 @@ +# Working with Service Workers + +We're using service workers for two main purposes: + +1. To cache out static and dynamic data feeds - to make them [available offline](https://developers.google.com/web/fundamentals/primers/service-workers/) +2. To run offline data sync using service workers + +To achieve the first point, we're using [sw-precache](https://github.com/GoogleChromeLabs/sw-precache) from Google and for the second - Vanilla JS with a little help from [sw-toolbox](https://www.google.pl/search?q=sw-toolbox&oq=sw-toolbox&aqs=chrome..69i57j69i60l3j0l2.1529j0j4&sourceid=chrome&ie=UTF-8) + +## Making things happen + +The service-worker source code for `vue-storefront` is pre-compiled with Babel presets and all is stored in additional theme-specific Service Worker in `src/{themename}/service-worker/index.js`. This file is attached to `service-worker.js` generated by `sw-toolbox`. + +After changing anything in `{themename}/service-worker/index.js`, despite you're in `yarn dev` auto reloading mode, you need to do two things: + +1. Recompile app (which regenerates service-worker): + `yarn build` + +2. Reload Service worker in Dev Tools (in Chrome - just click **"Unregister"** and reload the page, new SW will be installed). + +![How to work with service-workers in Chrome](/vue-storefront/chrome-dev-console.png) + +## Communication with the app + +Application can speak to service worker using the event bus - and only doing so. Please take a look at `/core/lib/sw.js` where we have following method: + +```js +export function postMessage(payload) { + if ('serviceWorker' in navigator && navigator.serviceWorker.controller) { + // check if it's properly installed + navigator.serviceWorker.controller.postMessage(payload); + return false; + } else { + // no service workers supported push the queue manualy + return true; + } +} +``` + +It allows you to send data to service worker. For example, when the order is placed (`/core/store/modules/checkout`): + +```js + /** + * Add order to sync. queue + * @param {Object} product data format for products is described in /doc/ElasticSearch data formats.md + */ + [types.CHECKOUT_PLACE_ORDER] (state, order) { + const ordersCollection = Vue.prototype.$db.ordersCollection + const orderId = entities.uniqueEntityId(order) // timestamp as a order id is not the best we can do but it's enough + order.id = orderId.toString() + order.transmited = false + order.created_at = new Date() + order.updated_at = new Date() + + ordersCollection.setItem(orderId.toString(), order).catch((reason) => { + console.error(reason) // it doesn't work on SSR + sw.postMessage({ config: config, command: types.CHECKOUT_PROCESS_QUEUE }) // process checkout queue + console.info('Order placed, orderId = ' + orderId) + }) // populate cache + }, +``` diff --git a/docs/guide/core-themes/themes.md b/docs/guide/core-themes/themes.md index 115f6064f3..816b3d3076 100644 --- a/docs/guide/core-themes/themes.md +++ b/docs/guide/core-themes/themes.md @@ -1,3 +1,58 @@ # Themes in Vue Storefront -_Work in progress_ +Vue Storefront allows you to quickly develop your own themes and use our core business logic. All e-commerce features are implemented in core, so you can easily develop fully working online shop only by writing HTML and CSS and inheriting the business logic from the core. Of course, you can easily modify and extend the core logic in your theme. + +You can read more about Vue Storefront core components and how to make use of them [here](core-components.md) + +All themes are located in `src/themes` folder and you can think about them as a separate Vue.js applications that are using Vue Storefront core for out-of-the-box features. + +## Switching themes + +To use any of the themes located in `src/themes`, just change the `theme` property in your config file to `name` property from package.json file sitting in your theme's root dir. Config files are located in `config` folder. You shouldn't make changes in `config/default.json`. Instead just copy the `default.json` file to the same folder, name it `local.json` and make changes there. + +## Creating your own themes + +There are two ways of creating your own VS theme + +1. Copying and modifying the default theme which is fully-styled and ready to work out of the box (it's the one that you can find on our demo) +2. Copying and modifying theme-starter which contains only data and no styling. It requires more work to have it production-ready (you need to style it from scratch) but if your designs are much different than our default theme you'd probably want to start with this one. + +To create your own theme just copy the `theme-starter` or `default` folder located in `src/themes` and change it's name to your new theme's name. Next change the name property in your theme `package.json` file. You can use this name in your config file to change the active theme. After adding new theme you need to run `yarn install` so lerna can detect a new theme. Now you can start development of your own theme for Vue Storefront! + +Only official themes tested and accepted by the community should be in a `master` branch. Please develop your own themes on separate branches and keep them updated with `master` to be sure it works with the newest core. + +## Important theme files + +Each theme is a separate Vue.js application with its own dependencies, which can make use of the core or even modify it. +Below you can find the list of files that are essential for your theme to work: + +- `extensions` - theme-specific extension (see [Working with extensions](extensions.md)) + - `index.js` - here you can register your theme-specific extensions +- `filters` - theme-specific filters (extends `core/filters`) + - `index.js` - here you can register your theme-specific filters +- `mixins` - theme-specific mixins (extends `core/mixins`) + - `index.js` - here you can register your theme-specific mixins +- `pages` - your shop pages +- `plugins` - theme-specific plugins (extends `core/plugins`, see [Working with plugins](plugins.md) +- `resource` - theme-specific resources (extends `core/resource`) +- `router` - theme router +- `store` - theme-specific stores (extends `core/store`) + - `ui-store.js` - here you can extend core `ui-store` + - `index.js` - here you can register theme-specific stores +- `App.vue` - theme's entry component +- `index.js` - theme initialization +- `package.json` - theme-specific dependencies +- `service-worker` + - `index.js` you can extend core service worker here (see [Working with Service Workers](service-workers.md) +- `webpack.config.js` - you can extend core webpack build in this file (extends `core/build/`, see [Working with webpack](webpack.md)) + +## Official Vue Storefront themes included with the template: + +- `default` - Default VS theme always with newest features. The easiest way to adopt VS in your shop is taking this one and modifying it to your needs (check [gogetgold.com](https://www.gogetgold.com/) as an example) +- `theme-starter` - boilerplate for developing VS themes. If you want to create new theme copy and rename this folder. +- `catalog` - VS catalog theme - currently in alpha + +## Related + +- [Working with components](core-components.md) +- [Creating themes in Vue Storefront — backend-agnostic eCommerce PWA frontend (part 1  - understanding Vue Storefront core)](https://medium.com/@frakowski/developing-themes-in-vue-storefront-backend-agnostic-ecommerce-pwa-frontend-part-1-72ea3c939593) diff --git a/docs/guide/core-themes/translations.md b/docs/guide/core-themes/translations.md index 32c8c1cc43..8fba800dbf 100644 --- a/docs/guide/core-themes/translations.md +++ b/docs/guide/core-themes/translations.md @@ -1,8 +1,8 @@ # Internationalization (i18n) of Vue Storefront -Vue Storefront allows you to translate the whole UI using powerful [vue-i18n](http://kazupon.github.io/vue-i18n/api/#methods) library. +Vue Storefront allows you to translate the whole UI using powerful [vue-i18n](http://kazupon.github.io/vue-i18n/) library. -Please be aware of i18n issues while writing your own themes/extensions and keep the i18n support in mind, especially when creating Pull Requests to the core +Please be aware of i18n issues while writing your own themes/extensions and keep the i18n support in mind, especially when creating Pull Requests to the core. ## Using i18n in code @@ -17,7 +17,7 @@ EventBus.$emit('notification', { }); ``` -If you're working with \*.vue components the matter is even simpler with Vue directive `$t`: +If you're working with `.vue` components the matter is even simpler with Vue directive `$t`: ```html @@ -25,18 +25,18 @@ If you're working with \*.vue components the matter is even simpler with Vue dir ``` -For all helper methods and directives along with available parameters please do check the [vue-i18n documentation](http://kazupon.github.io/vue-i18n/api/#methods). +For all helper methods and directives along with available parameters please do check the [vue-i18n documentation](http://kazupon.github.io/vue-i18n/introduction.html). ## Working with translations -Translations are provided in `resource/i18n/en-US.csv` file and can be extended / overriden in `theme/resource/i18n/en-US.csv` accordingly. +Translations are provided in `i18n/resource/i18n/en-US.csv` file and can be extended / overriden in `theme/resource/i18n/en-US.csv` accordingly. Here's an example of `en-US.csv` for `en-US` locale: ```csv "customMessage","You can define or override translation messages here." "welcomeMessage", "Welcome to Vue Storefront theme starter!", -"In case of any problems please take a look at the docs. If you havn't find what you were looking for in docs feel free to ask your question on our Slack", "In case of any problems please take a look at the docs. If you havn't find what you were looking for in docs feel free to ask your question on our Slack", +"In case of any problems please take a look at the docs. If you haven't find what you were looking for in docs feel free to ask your question on our Slack", "In case of any problems please take a look at the docs. If you haven't find what you were looking for in docs feel free to ask your question on our Slack", "Here are some links that can help you with developing your own theme", "Here are some links that can help you with developing your own theme", "Project structure", "Project structure", "Working with themes", "Working with themes", diff --git a/docs/guide/core-themes/ui-store.md b/docs/guide/core-themes/ui-store.md new file mode 100644 index 0000000000..00c48ec7d2 --- /dev/null +++ b/docs/guide/core-themes/ui-store.md @@ -0,0 +1,16 @@ +# Working with UI Store (Interface state) + +We are using Vuex to store the application interface state. The [ui-store file](https://github.com/DivanteLtd/vue-storefront/blob/master/core/store/modules/ui-store/index.ts) contains the information about the state of different pieces of UI like: Overlay visibility, Wishlist visibility etc. Of course you are not forced to make use of it in your theme but keep in mind that many of core components are using UI store. + +## State object + +- `sidebar` - visible/hidden state of sidebar menu (find: `SidebarMenu.vue`) +- `microcart` - visible/hidden state of microcart (find: `Microcart.vue`) +- `wishlist` - visible/hidden state of wishlist (find: `Wishlist.vue`) +- `searchpanel` - visible/hidden state of search panel (find: `SearchPanel.vue`) +- `newsletterPopup` - visible/hidden state of newsletter popup (_will be removed from Vuex store_) +- `overlay` - visible/hidden state of overlay (find: `Overlay.vue`) +- `loader` - visible/hidden state of loader (find: `Loader.vue`) +- `authElem` - component to be displayed at Auth popup (will be changed and moved only to this component) +- `checkoutMode` - determines whether user is in checkout or not - useful when you want to change some ui elements or behavior only on checkout (e.g. hide footer) +- `openMyAccount` - determines whether to redirect user to My Account page - used when user clicked on My Account link in the sidebar, but had to login first. After successful logging in user will be automatically redirected to My Account page. diff --git a/docs/guide/core-themes/webpack.md b/docs/guide/core-themes/webpack.md index 25425c0260..8a6b70d99e 100644 --- a/docs/guide/core-themes/webpack.md +++ b/docs/guide/core-themes/webpack.md @@ -1,3 +1,64 @@ -# Working with Webpack +# Working with webpack -_Work in progress_ +To make Vue Storefront so fast and developer friendly we use webpack under the hood. We need it to transpile assets, handle `.vue` files, process all styles and make our code a little bit more maintainable with linting provided by eslint. With that you don't need to worry about configuring it by hand to start working on Vue Storefront or to build your own theme for it. However, when you want to tweak it to your special needs there is also a possibility to do that with extendable webpack configuration for each theme. + +## Core webpack build + +All build scripts used by core of the Vue Storefront are available in `core/build` directory. If you want to improve our build or add support for new cases you will probably only need to change files there and sometimes update `package.json`. + +Base config for client and server is set up in `webpack.base.config.js`. This configuration is then merged with specific client and server configs in `webpack.client.config.js` and `webpack.server.config.js`. + +For development mode (`yarn dev`) `dev-server.js` file is used to run previously mentioned config files (`webpack.client.config.js`, `webpack.server.config.js`) with custom config provided by the theme. We use `webpack-dev-middleware` and `webpack-hot-middleware` to make website development as fast and easy as possible. + +In `vue-loader.config.js` the whole configuration for `vue-loader` is stored. If there is a need to change style processing for single file components, you can set it up in this file (if you want to extend the Vue Storefront core). + +To build a production version of Vue Storefront `webpack.prod.client.config.js` and `webpack.prod.server.config.js` are used with a `build` script. In these files our base configuration is merged with theme specific extended config. + +## Extending core build in themes + +Vue Storefront follows technique popularized by [next.js](https://github.com/zeit/next.js/) and [Nuxt](https://nuxtjs.org/) for extending webpack config. For each theme you can configure `webpack.config.js` file that will allow you to have an access to base configuration and customize it for your needs without changing core build files. + +### Example + +Below is a simple example that adds `webpack-bundle-analyzer` to check generated webpack bundles. In addition to analyzer `json5-loader` is used to handle JSON5 files `json5-loader` in project. + +```js +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') + .BundleAnalyzerPlugin; + +module.exports = function(config, { isClient, isDev }) { + let configLoaders; + if (isClient) { + configLoaders = config[0].module.rules; + config[0].plugins.push( + new BundleAnalyzerPlugin({ + openAnalyzer: false, + statsFilename: 'test', + generateStatsFile: true, + analyzerMode: 'static', + }), + ); + } else { + configLoaders = config.module.rules; + } + configLoaders.push({ + test: /\.json5$/, + loader: 'json5-loader', + }); + return config; +}; +``` + +This file should export a function that returns a complete configuration. + +This function is executed with 2 arguments. First one is the complete core Vue Storefront webpack configuration. Second is an object that has properties: `isClient` and `isDev`. + +Option `isClient` indicates that the configuration is for client bundle. + +Option `isDev` is set to `true` if Vue Storefront runs in development mode. + +In case of client build (`isClient == true`) config argument is an array with 2 elements. First array element is the client configuration and the second one is used to generate a service worker file. + +For server build (`isClient == false`) config argument is an standard webpack configuration object. + +All loaders and plugins used in extended configuration will be fetched from theme `node_modules` directory, so make sure you have it saved in theme `package.json` file. diff --git a/docs/guide/data/data-migrations.md b/docs/guide/data/data-migrations.md new file mode 100644 index 0000000000..063ef1a69d --- /dev/null +++ b/docs/guide/data/data-migrations.md @@ -0,0 +1,3 @@ +# Data Migrations for ElacticSearch + +_Work in progress_ diff --git a/docs/guide/data/data.md b/docs/guide/data/data.md index 566882f8f1..714bccec64 100644 --- a/docs/guide/data/data.md +++ b/docs/guide/data/data.md @@ -1,3 +1,384 @@ -# Data +# Working with data -_Work in progress_ +Vue storefront uses two primary data sources: + +1. IndexedDb/WebSQL data store in the browser - using [localForage](https://github.com/localForage/localForage) +2. Server data source via [vue-storefront-api](https://github.com/DivanteLtd/vue-storefront-api) - which API is compliant with ElasticSearch (regarding product catalog) + +## Local data store + +You can access localForage repositories thru `Vue.prototype.$db` object anywhere in the code BUT all data-related operations SHOULD be placed in Vuex stores. + +Details on localForage API can be found [here](http://localforage.github.io/localForage/) + +We basically have following data stores accessible in the browser (`/core/store/index.ts`): + +```js +Vue.prototype.$db = { + ordersCollection: new UniversalStorage( + localForage.createInstance({ + name: 'shop', + storeName: 'orders', + }), + ), + + categoriesCollection: new UniversalStorage( + localForage.createInstance({ + name: 'shop', + storeName: 'categories', + }), + ), + + attributesCollection: new UniversalStorage( + localForage.createInstance({ + name: 'shop', + storeName: 'attributes', + }), + ), + + cartsCollection: new UniversalStorage( + localForage.createInstance({ + name: 'shop', + storeName: 'carts', + }), + ), + + elasticCacheCollection: new UniversalStorage( + localForage.createInstance({ + name: 'shop', + storeName: 'elasticCache', + }), + ), + + productsCollection: new UniversalStorage( + localForage.createInstance({ + name: 'shop', + storeName: 'products', + }), + ), + + claimsCollection: new UniversalStorage( + localForage.createInstance({ + name: 'shop', + storeName: 'claims', + }), + ), + + wishlistCollection: new UniversalStorage( + localForage.createInstance({ + name: 'shop', + storeName: 'wishlist', + }), + ), + + compareCollection: new UniversalStorage( + localForage.createInstance({ + name: 'shop', + storeName: 'compare', + }), + ), + + usersCollection: new UniversalStorage( + localForage.createInstance({ + name: 'shop', + storeName: 'user', + }), + ), + + syncTaskCollection: new UniversalStorage( + localForage.createInstance({ + name: 'shop', + storeName: 'syncTasks', + }), + ), + + checkoutFieldsCollection: new UniversalStorage( + localForage.createInstance({ + name: 'shop', + storeName: 'checkoutFieldValues', + }), + ), +}; +``` + +## Example Vuex store + +Here you have an example on how the Vuex store should be constructed. Please notice the _Ajv data validation_: + +```js +import * as types from '../mutation-types'; +import { ValidationError } from '@vue-storefront/store/lib/exceptions'; +import * as entities from '../../lib/entities'; +import * as sw from '@vue-storefront/core/lib/sw'; +import config from '../../config'; +const Ajv = require('ajv'); // json validator + +// initial state +const state = { + checkoutQueue: [], // queue of orders to be sent to the server +}; + +const getters = {}; + +// actions +const actions = { + /** + * Place order - send it to service worker queue + * @param {Object} commit method + * @param {Object} order order data to be send + */ + placeOrder({ commit }, order) { + const ajv = new Ajv(); + const validate = ajv.compile( + require('core/store/modules/order/order.schema.json'), + ); + + if (!validate(order)) { + // schema validation of upcoming order + throw new ValidationError(validate.errors); + } + commit(types.CHECKOUT_PLACE_ORDER, order); + }, +}; + +// mutations +const mutations = { + /** + * Add order to sync. queue + * @param {Object} product data format for products is described in /doc/ElasticSearch data formats.md + */ + [types.CHECKOUT_PLACE_ORDER](state, order) { + const ordersCollection = Vue.prototype.$db.ordersCollection; + const orderId = entities.uniqueEntityId(order); // timestamp as a order id is not the best we can do but it's enough + order.order_id = orderId.toString(); + order.transmited = false; + order.created_at = new Date(); + order.updated_at = new Date(); + + ordersCollection + .setItem(orderId.toString(), order) + .catch(reason => { + console.debug(reason); // it doesn't work on SSR + }) + .then(resp => { + sw.postMessage({ + config: config, + command: types.CHECKOUT_PROCESS_QUEUE, + }); // process checkout queue + console.debug('Order placed, orderId = ' + orderId); + }); // populate cache + }, + /** + * Add order to sync. queue + * @param {Object} queue + */ + [types.CHECKOUT_LOAD_QUEUE](state, queue) { + state.checkoutQueue = queue; + console.debug( + 'Order queue loaded, queue size is: ' + state.checkoutQueue.length, + ); + }, +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; +``` + +## Data formats & validation + +Data formats for vue-storefront and vue-storefront-api are the same JSON files. There is [Ajv validator](https://github.com/epoberezkin/ajv) used for the validation. + +The convention is that schemas are stored under `/core/store/modules//.schema.json` - for example [Order schema](https://github.com/DivanteLtd/vue-storefront/blob/master/core/store/modules/order/order.schema.json). + +Objects validation is rather straightforward: + +```js +const Ajv = require('ajv'); // json validator +const ajv = new Ajv(); +const validate = ajv.compile( + require('core/store/modules/order/order.schema.json'), +); + +if (!validate(order)) { + // schema validation of upcoming order + throw new ValidationError(validate.errors); +} +``` + +Validation errors format: + +```json +[ + { + "keyword": "additionalProperties", + "dataPath": "", + "schemaPath": "#/additionalProperties", + "params": { "additionalProperty": "id" }, + "message": "should NOT have additional properties" + } +] +``` + +### Orders + +`Orders` repository stores all orders transmitted and _to be transmitted_ (aka order queue) used by service worker. + +![Orders data format as seen on Developers Tools](/vue-storefront/orders-localstorage.png) + +Here you have a [validation schema for order](https://github.com/DivanteLtd/vue-storefront/blob/master/core/store/modules/order/order.schema.json): + +```json +{ + "order_id": "123456789", + "created_at": "2017-09-28 12:00:00", + "updated_at": "2017-09-28 12:00:00", + "transmited_at": "2017-09-28 12:00:00", + "transmited": false, + "products": [ + { + "sku": "product_dynamic_1", + "qty": 1, + "name": "Product one", + "price": 19, + "product_type": "simple" + }, + { + "sku": "product_dynamic_2", + "qty": 1, + "name": "Product two", + "price": 54, + "product_type": "simple" + } + ], + "addressInformation": { + "shippingAddress": { + "region": "MH", + "region_id": 0, + "country_id": "PL", + "street": ["Street name line no 1", "Street name line no 2"], + "company": "Company name", + "telephone": "123123123", + "postcode": "00123", + "city": "Cityname", + "firstname": "John ", + "lastname": "Doe", + "email": "john@doe.com", + "region_code": "MH", + "sameAsBilling": 1 + }, + "billingAddress": { + "region": "MH", + "region_id": 0, + "country_id": "PL", + "street": ["Street name line no 1", "Street name line no 2"], + "company": "abc", + "telephone": "1111111", + "postcode": "00123", + "city": "Mumbai", + "firstname": "Sameer", + "lastname": "Sawant", + "email": "john@doe.com", + "prefix": "address_", + "region_code": "MH" + }, + "shipping_method_code": "flatrate", + "shipping_carrier_code": "flatrate", + "payment_method_code": "cashondelivery", + "payment_method_additional": {} // Payment Method Payload (eg, stripe token) + } +} +``` + +### Categories + +`Categories` is a hash organized by category 'slug' (for example for category with name = 'Example category', slug is 'example-category') + +![Categories data format as seen on Developers Tools](/vue-storefront/categories-localstorage.png) + +If category do have any child categories - you have an access to them via `children_data` property. + +```json +{ + "id": 13, + "parent_id": 11, + "name": "Bottoms", + "is_active": true, + "position": 2, + "level": 3, + "product_count": 0, + "children_data": [ + { + "id": 18, + "parent_id": 13, + "name": "Pants", + "is_active": true, + "position": 1, + "level": 4, + "product_count": 156, + "children_data": [] + }, + { + "id": 19, + "parent_id": 13, + "name": "Shorts", + "is_active": true, + "position": 2, + "level": 4, + "product_count": 148, + "children_data": [] + } + ], + "tsk": 1505573191094 +} +``` + +### Carts + +`Carts` is a store for a shopping cart with a default key `current-cart` representing a current shopping cart. +Cart object is an array consisting of Products with an additional field `qty` in case when 2+ items are ordered. + +![Carts data format as seen on Developers Tools](/vue-storefront/cart-localstorage.png) + +```json +[ + { + "id": 26, + "qty": 5, + "sku": "24-WG081-blue", + "name": "Sprite Stasis Ball 55 cm", + "attribute_set_id": 12, + "price": 23, + "status": 1, + "visibility": 1, + "type_id": "simple", + "created_at": "2017-09-16 13:46:48", + "updated_at": "2017-09-16 13:46:48", + "extension_attributes": [], + "product_links": [], + "tier_prices": [], + "custom_attributes": null, + "category": [], + "tsk": 1505573582376, + "description": "

The Sprite Stasis Ball gives you the toned abs, sides, and back you want by amping up your core workout. With bright colors and a burst-resistant design, it's a must-have for every hard-core exercise addict. Use for abdominal conditioning, balance training, yoga, or even physical therapy.

  • Durable, burst-resistant design.
  • Hand pump included.
", + "image": "/l/u/luma-stability-ball.jpg", + "small_image": "/l/u/luma-stability-ball.jpg", + "thumbnail": "/l/u/luma-stability-ball.jpg", + "color": "50", + "options_container": "container2", + "required_options": "0", + "has_options": "0", + "url_key": "sprite-stasis-ball-55-cm-blue", + "tax_class_id": "2", + "activity": "8,11", + "material": "44", + "gender": "80,81,82,83,84", + "category_gear": "87", + "size": "91" + } +] +``` diff --git a/docs/guide/data/database-tool.md b/docs/guide/data/database-tool.md index aed420caa4..7a140f01ed 100644 --- a/docs/guide/data/database-tool.md +++ b/docs/guide/data/database-tool.md @@ -1,11 +1,12 @@ # Database tool -Vue Storefront gets it's all data from [vue-storefront-api](https://github.com/DivanteLtd/vue-storefront-api) endpoints, operating on top of Elastic Search data store. +Vue Storefront gets all its data from [vue-storefront-api](https://github.com/DivanteLtd/vue-storefront-api) endpoints, operating on top of Elastic Search data store. + +If you installed the project using `npm run installer` command then, the database has been set up, data imported from demo-dump and everything should be just fine. -If You installed the project using `npm run installer` command then, the database has been set up, data imported from demo-dump and everything should be just fine. After some more extensive data operations - like custom imports using [mage2vuestorefront](https://github.com/DivanteLtd/mage2vuestorefront) or [magento1-vsbridge](https://github.com/DivanteLtd/magento1-vsbridge) there is a need to re-index the ElasticSearch and setup the proper metadata for fields. -The main reason You know You must reindex the database is kind of the following error You get from vue-storefront console: +The main reason you know you must reindex the database is kind of the following error you get from the `vue-storefront` console: ```json Error: {"root_cause":[{"type":"illegal_argument_exception","reason":"Fielddata is disabled on text fields by default. Set fielddata=true on [created_at] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead."}],"type":"search_phase_execution_exception","reason":"all shards failed","phase":"query","grouped":true,"failed_shards":[{"shard":0,"index":"vue_storefront_catalog_1521776807","node":"xIOeZW2lTwaprGXh6YLyCA","reason":{"type":"illegal_argument_exception","reason":"Fielddata is disabled on text fields by default. Set fielddata=true on [created_at] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead."}}]} @@ -13,34 +14,46 @@ Error: {"root_cause":[{"type":"illegal_argument_exception","reason":"Fielddata i In this case there is a db tool inside your local `vue-storefront-api` installation to the rescue. -## Re-indexing existing database +## Re-indexing an existing database Please go to `vue-storefront-api` directory and run: -`npm run db rebuild` + +```bash +npm run db rebuild` +``` This command will: - reindex your currently set (in the `config/local.json` config file) elastic search index to temp-one, - put the right elastic search mappings on top of the temp index, - drop the original index, -- create the alias with original name to the temp one - so You can use original name without any reference chcanges. +- create the alias with original name to the temp one - so You can use original name without any reference changes. You can specify different (than this set in `config/local.json`) index name by running: -`npm run db rebuild -- --indexName=custom_index_name` + +```bash +npm run db rebuild -- --indexName=custom_index_name +``` ## Creating the new index If you like to create new, empty index please run: -`npm run db new` + +```bash +npm run db new +``` This tool will drop your current index and create new, empty one with all the metafields set. You can specify different (than this set in `config/local.json`) index name by running: -`npm run db rebuild -- --indexName=custom_index_name` -## Chaning the index structure / adding new fields / chaning the types +```bash +npm run db rebuild -- --indexName=custom_index_name +``` + +## Changing the index structure / adding new fields / changing the types -If You like to extenend the ElasticSearch data structures or map some particular field types. For example after getting kind of this error: +If you like to extend the ElasticSearch data structures or map some particular field types, for example after getting kind of this error: ``` [{"type":"illegal_argument_exception","reason":"Fielddata is disabled on text fields by default. Set fielddata=true on [created_at] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead."}] @@ -52,7 +65,10 @@ Please do change the ES schema by modifying: - [config/elastic.schema.attribute.extension.json](https://github.com/DivanteLtd/vue-storefront-api/blob/master/config/elastic.schema.attribute.extension.json) - [config/elastic.schema.taxrate.extension.json](https://github.com/DivanteLtd/vue-storefront-api/blob/master/config/elastic.schema.taxrate.extension.json) -The format is compliant with ES DSL for schema modifications: https://www.elastic.co/blog/found-elasticsearch-mapping-introduction +The format is compliant with [ES DSL for schema modifications](https://www.elastic.co/blog/found-elasticsearch-mapping-introduction) After the changes please do run the following indexing command: -`npm run db rebuild` + +```bash +npm run db rebuild +``` diff --git a/docs/guide/data/elastic-queries.md b/docs/guide/data/elastic-queries.md new file mode 100644 index 0000000000..d74c2985d0 --- /dev/null +++ b/docs/guide/data/elastic-queries.md @@ -0,0 +1,51 @@ +# ElasticSearch Queries + +## Getting data from ElasticSearch + +VueStorefront stores most of the catalog data within the ElasticSearch data store. Please have a look at our architecture diagram: + +![Architecture diagram](/vue-storefront/Vue-storefront-architecture.png). + +To properly access ElasticSearch data, you should implement a specific Vuex action. Here is an example of [vuex action for getting the data](https://github.com/DivanteLtd/vue-storefront/blob/c954b96f6633a201e10bed1d2e4c0def1aeb3071/core/store/modules/category.js#L38) : + +```js +import { quickSearchByQuery } from '../../lib/search' + + /** + * Load categories within specified parent + * @param {Object} commit promise + * @param {Object} parent parent category + */ + list (context, { parent = null, onlyActive = true, onlyNotEmpty = false, size = 4000, start = 0, sort = 'position:asc', includeFields = config.entities.optimize ? config.entities.category.includeFields : null }) { + const commit = context.commit + + let searchQuery = new SearchQuery() + if (parent && typeof parent !== 'undefined') { + searchQuery = searchQuery.applyFilter({key: 'parent_id', value: {'eq': parent.id}}) + } + + if (onlyActive === true) { + searchQuery = searchQuery.applyFilter({key: 'is_active', value: {'eq': true}}) + } + + if (onlyNotEmpty === true) { + searchQuery = searchQuery.applyFilter({key: 'product_count', value: {'gt': 0}}) + } + + if (!context.state.list | context.state.list.length === 0) { + return quickSearchByQuery({ entityType: 'category', query: searchQuery, sort: sort, size: size, start: start, includeFields: includeFields }).then(function (resp) { + commit(types.CATEGORY_UPD_CATEGORIES, resp) + EventBus.$emit('category-after-list', { query: searchQuery, sort: sort, size: size, start: start, list: resp }) + return resp + }).catch(function (err) { + console.error(err) + }) + } +``` + +As You may see we're using [quickSearchByQuery](https://github.com/DivanteLtd/vue-storefront/blob/c954b96f6633a201e10bed1d2e4c0def1aeb3071/core/lib/search/search.js#L60) for executing Search. This method is pretty interesting because: + +- it uses the `searchQuery` query object which has an ability to apply filters in common way +- it does cache the received data into `localForage` collection named `elasticCache`; the next call with the same queryObject will return the data directly from the browser storage, not hiting the server. + +We do not build elasticsearch query on this step more. We use search layer object containing all necessary filters and search text. ES query builds using the powerful bodybuilder package right before sending Elasticsearch request. Please take a look at the [reference docs for more options](https://github.com/danpaz/bodybuilder). diff --git a/docs/guide/data/elasticsearch.md b/docs/guide/data/elasticsearch.md new file mode 100644 index 0000000000..3d44231ce4 --- /dev/null +++ b/docs/guide/data/elasticsearch.md @@ -0,0 +1,1356 @@ +# ElasticSearch data formats + +The service is using ElasticSearch data format compliant with ElasticSuite for Magento 1.x/2.x from [Smile](https://github.com/Smile-SA/smile-magento-elasticsearch). + +## Product type + +The product data format is combined form of the following Magento2 REST API calls: + +- [catalogProductRepositoryV1GetListGet](http://devdocs.magento.com/swagger/#!/catalogProductRepositoryV1/catalogProductRepositoryV1GetListGet) +- [catalogInventoryStockRegistryV1GetStockItemBySkuGet](http://devdocs.magento.com/swagger/#!/catalogInventoryStockRegistryV1/catalogInventoryStockRegistryV1GetStockItemBySkuGet) +- [configurableProductLinkManagementV1GetChildrenGet](http://devdocs.magento.com/swagger/#!/configurableProductLinkManagementV1/configurableProductLinkManagementV1GetChildrenGet) +- [configurableProductOptionRepositoryV1GetListGet](http://devdocs.magento.com/swagger/#!/configurableProductOptionRepositoryV1/configurableProductOptionRepositoryV1GetListGet) + +```json + { + "_index": "storefront_catalog", + "_type": "product", + "_id": 19, + "_score": 1, + "_source": { + "id": 19, + "sku": "24-UG05", + "name": "Go-Get'r Pushup Grips", + "attribute_set_id": 11, + "price": 19, + "status": 1, + "visibility": 4, + "type_id": "simple", + "created_at": "2017-10-31 00:07:05", + "updated_at": "2017-10-31 00:07:05", + "extension_attributes": [], + "product_links": [], + "tier_prices": [], + "custom_attributes": null, + "category": [ + { + "category_id": 2, + "name": "Default Category" + }, + { + "category_id": 3, + "name": "Gear" + }, + { + "category_id": 5, + "name": "Fitness Equipment" + }, + { + "category_id": 7, + "name": "Collections" + }, + { + "category_id": 8, + "name": "New Luma Yoga Collection" + } + ], + "description": "

The Go-Get'r Pushup Grips safely provide the extra range of motion you need for a deep-dip routine targeting core, shoulder, chest and arm strength. Do fewer pushups using more energy, getting better results faster than the standard floor-level technique yield.

\n
    \n
  • Durable foam grips.
  • \n
  • Supportive base.
  • \n
", + "image": "/u/g/ug05-gr-0.jpg", + "small_image": "/u/g/ug05-gr-0.jpg", + "thumbnail": "/u/g/ug05-gr-0.jpg", + "options_container": "container2", + "required_options": 0, + "has_options": 0, + "url_key": "go-get-r-pushup-grips", + "tax_class_id": 2, + "activity": "16,11", + "material": "44,45", + "gender": "80,81,84", + "category_gear": "87", + "erin_recommends": "1", + "new": "1", + "pattern": "195", + "eco_collection": "1", + "msrp_display_actual_price_type": 0, + "climate": "202,204,205,208,210", + "performance_fabric": "0", + "sale": "1", + "children_data": [], + "configurable_options": [ + { + "attribute_id": 93, + "values": [ + { + "value_index": 49 + }, + { + "value_index": 52 + }, + { + "value_index": 56 + } + ], + "product_id": 19, + "id": 3, + "label": "Color", + "position": 0 + }, + { + "attribute_id": 157, + "values": [ + { + "value_index": 167 + }, + { + "value_index": 168 + }, + { + "value_index": 169 + }, + { + "value_index": 170 + }, + { + "value_index": 171 + } + ], + "product_id": 19, + "id": 2, + "label": "Size", + "position": 0 + } + ], + "configurable_children": [ + { + "price": 52, + "name": "Chaz Kangeroo Hoodie-XS-Black", + "sku": "MH01-XS-Black", + "custom_attributes": [ + { + "value": "0", + "attribute_code": "required_options" + }, + { + "value": "0", + "attribute_code": "has_options" + }, + { + "value": "2", + "attribute_code": "tax_class_id" + }, + { + "value": [ + "15", + "36", + "2" + ], + "attribute_code": "category_ids" + }, + { + "value": "167", + "attribute_code": "size" + }, + { + "value": "49", + "attribute_code": "color" + }, + { + "value": "/m/h/mh01-black_main.jpg", + "attribute_code": "image" + }, + { + "value": "/m/h/mh01-black_main.jpg", + "attribute_code": "small_image" + }, + { + "value": "/m/h/mh01-black_main.jpg", + "attribute_code": "thumbnail" + }, + { + "value": "chaz-kangeroo-hoodie-xs-black", + "attribute_code": "url_key" + }, + { + "value": "0", + "attribute_code": "msrp_display_actual_price_type" + } + ] + }, + { + "price": 52, + "name": "Chaz Kangeroo Hoodie-XS-Gray", + "sku": "MH01-XS-Gray", + "custom_attributes": [ + { + "value": "0", + "attribute_code": "required_options" + }, + { + "value": "0", + "attribute_code": "has_options" + }, + { + "value": "2", + "attribute_code": "tax_class_id" + }, + { + "value": [ + "15", + "36", + "2" + ], + "attribute_code": "category_ids" + }, + { + "value": "167", + "attribute_code": "size" + }, + { + "value": "52", + "attribute_code": "color" + }, + { + "value": "/m/h/mh01-gray_main.jpg", + "attribute_code": "image" + }, + { + "value": "/m/h/mh01-gray_main.jpg", + "attribute_code": "small_image" + }, + { + "value": "/m/h/mh01-gray_main.jpg", + "attribute_code": "thumbnail" + }, + { + "value": "chaz-kangeroo-hoodie-xs-gray", + "attribute_code": "url_key" + }, + { + "value": "0", + "attribute_code": "msrp_display_actual_price_type" + } + ] + }, + { + "price": 52, + "name": "Chaz Kangeroo Hoodie-XS-Orange", + "sku": "MH01-XS-Orange", + "custom_attributes": [ + { + "value": "0", + "attribute_code": "required_options" + }, + { + "value": "0", + "attribute_code": "has_options" + }, + { + "value": "2", + "attribute_code": "tax_class_id" + }, + { + "value": [ + "15", + "36", + "2" + ], + "attribute_code": "category_ids" + }, + { + "value": "167", + "attribute_code": "size" + }, + { + "value": "56", + "attribute_code": "color" + }, + { + "value": "/m/h/mh01-orange_main.jpg", + "attribute_code": "image" + }, + { + "value": "/m/h/mh01-orange_main.jpg", + "attribute_code": "small_image" + }, + { + "value": "/m/h/mh01-orange_main.jpg", + "attribute_code": "thumbnail" + }, + { + "value": "chaz-kangeroo-hoodie-xs-orange", + "attribute_code": "url_key" + }, + { + "value": "0", + "attribute_code": "msrp_display_actual_price_type" + } + ] + }, + { + "price": 52, + "name": "Chaz Kangeroo Hoodie-S-Black", + "sku": "MH01-S-Black", + "custom_attributes": [ + { + "value": "0", + "attribute_code": "required_options" + }, + { + "value": "0", + "attribute_code": "has_options" + }, + { + "value": "2", + "attribute_code": "tax_class_id" + }, + { + "value": [ + "15", + "36", + "2" + ], + "attribute_code": "category_ids" + }, + { + "value": "168", + "attribute_code": "size" + }, + { + "value": "49", + "attribute_code": "color" + }, + { + "value": "/m/h/mh01-black_main.jpg", + "attribute_code": "image" + }, + { + "value": "/m/h/mh01-black_main.jpg", + "attribute_code": "small_image" + }, + { + "value": "/m/h/mh01-black_main.jpg", + "attribute_code": "thumbnail" + }, + { + "value": "chaz-kangeroo-hoodie-s-black", + "attribute_code": "url_key" + }, + { + "value": "0", + "attribute_code": "msrp_display_actual_price_type" + } + ] + }, + { + "price": 52, + "name": "Chaz Kangeroo Hoodie-S-Gray", + "sku": "MH01-S-Gray", + "custom_attributes": [ + { + "value": "0", + "attribute_code": "required_options" + }, + { + "value": "0", + "attribute_code": "has_options" + }, + { + "value": "2", + "attribute_code": "tax_class_id" + }, + { + "value": [ + "15", + "36", + "2" + ], + "attribute_code": "category_ids" + }, + { + "value": "168", + "attribute_code": "size" + }, + { + "value": "52", + "attribute_code": "color" + }, + { + "value": "/m/h/mh01-gray_main.jpg", + "attribute_code": "image" + }, + { + "value": "/m/h/mh01-gray_main.jpg", + "attribute_code": "small_image" + }, + { + "value": "/m/h/mh01-gray_main.jpg", + "attribute_code": "thumbnail" + }, + { + "value": "chaz-kangeroo-hoodie-s-gray", + "attribute_code": "url_key" + }, + { + "value": "0", + "attribute_code": "msrp_display_actual_price_type" + } + ] + }, + { + "price": 52, + "name": "Chaz Kangeroo Hoodie-S-Orange", + "sku": "MH01-S-Orange", + "custom_attributes": [ + { + "value": "0", + "attribute_code": "required_options" + }, + { + "value": "0", + "attribute_code": "has_options" + }, + { + "value": "2", + "attribute_code": "tax_class_id" + }, + { + "value": [ + "15", + "36", + "2" + ], + "attribute_code": "category_ids" + }, + { + "value": "168", + "attribute_code": "size" + }, + { + "value": "56", + "attribute_code": "color" + }, + { + "value": "/m/h/mh01-orange_main.jpg", + "attribute_code": "image" + }, + { + "value": "/m/h/mh01-orange_main.jpg", + "attribute_code": "small_image" + }, + { + "value": "/m/h/mh01-orange_main.jpg", + "attribute_code": "thumbnail" + }, + { + "value": "chaz-kangeroo-hoodie-s-orange", + "attribute_code": "url_key" + }, + { + "value": "0", + "attribute_code": "msrp_display_actual_price_type" + } + ] + }, + { + "price": 52, + "name": "Chaz Kangeroo Hoodie-M-Black", + "sku": "MH01-M-Black", + "custom_attributes": [ + { + "value": "0", + "attribute_code": "required_options" + }, + { + "value": "0", + "attribute_code": "has_options" + }, + { + "value": "2", + "attribute_code": "tax_class_id" + }, + { + "value": [ + "15", + "36", + "2" + ], + "attribute_code": "category_ids" + }, + { + "value": "169", + "attribute_code": "size" + }, + { + "value": "49", + "attribute_code": "color" + }, + { + "value": "/m/h/mh01-black_main.jpg", + "attribute_code": "image" + }, + { + "value": "/m/h/mh01-black_main.jpg", + "attribute_code": "small_image" + }, + { + "value": "/m/h/mh01-black_main.jpg", + "attribute_code": "thumbnail" + }, + { + "value": "chaz-kangeroo-hoodie-m-black", + "attribute_code": "url_key" + }, + { + "value": "0", + "attribute_code": "msrp_display_actual_price_type" + } + ] + }, + { + "price": 52, + "name": "Chaz Kangeroo Hoodie-M-Gray", + "sku": "MH01-M-Gray", + "custom_attributes": [ + { + "value": "0", + "attribute_code": "required_options" + }, + { + "value": "0", + "attribute_code": "has_options" + }, + { + "value": "2", + "attribute_code": "tax_class_id" + }, + { + "value": [ + "15", + "36", + "2" + ], + "attribute_code": "category_ids" + }, + { + "value": "169", + "attribute_code": "size" + }, + { + "value": "52", + "attribute_code": "color" + }, + { + "value": "/m/h/mh01-gray_main.jpg", + "attribute_code": "image" + }, + { + "value": "/m/h/mh01-gray_main.jpg", + "attribute_code": "small_image" + }, + { + "value": "/m/h/mh01-gray_main.jpg", + "attribute_code": "thumbnail" + }, + { + "value": "chaz-kangeroo-hoodie-m-gray", + "attribute_code": "url_key" + }, + { + "value": "0", + "attribute_code": "msrp_display_actual_price_type" + } + ] + }, + { + "price": 52, + "name": "Chaz Kangeroo Hoodie-M-Orange", + "sku": "MH01-M-Orange", + "custom_attributes": [ + { + "value": "0", + "attribute_code": "required_options" + }, + { + "value": "0", + "attribute_code": "has_options" + }, + { + "value": "2", + "attribute_code": "tax_class_id" + }, + { + "value": [ + "15", + "36", + "2" + ], + "attribute_code": "category_ids" + }, + { + "value": "169", + "attribute_code": "size" + }, + { + "value": "56", + "attribute_code": "color" + }, + { + "value": "/m/h/mh01-orange_main.jpg", + "attribute_code": "image" + }, + { + "value": "/m/h/mh01-orange_main.jpg", + "attribute_code": "small_image" + }, + { + "value": "/m/h/mh01-orange_main.jpg", + "attribute_code": "thumbnail" + }, + { + "value": "chaz-kangeroo-hoodie-m-orange", + "attribute_code": "url_key" + }, + { + "value": "0", + "attribute_code": "msrp_display_actual_price_type" + } + ] + }, + { + "price": 52, + "name": "Chaz Kangeroo Hoodie-L-Black", + "sku": "MH01-L-Black", + "custom_attributes": [ + { + "value": "0", + "attribute_code": "required_options" + }, + { + "value": "0", + "attribute_code": "has_options" + }, + { + "value": "2", + "attribute_code": "tax_class_id" + }, + { + "value": [ + "15", + "36", + "2" + ], + "attribute_code": "category_ids" + }, + { + "value": "170", + "attribute_code": "size" + }, + { + "value": "49", + "attribute_code": "color" + }, + { + "value": "/m/h/mh01-black_main.jpg", + "attribute_code": "image" + }, + { + "value": "/m/h/mh01-black_main.jpg", + "attribute_code": "small_image" + }, + { + "value": "/m/h/mh01-black_main.jpg", + "attribute_code": "thumbnail" + }, + { + "value": "chaz-kangeroo-hoodie-l-black", + "attribute_code": "url_key" + }, + { + "value": "0", + "attribute_code": "msrp_display_actual_price_type" + } + ] + }, + { + "price": 52, + "name": "Chaz Kangeroo Hoodie-L-Gray", + "sku": "MH01-L-Gray", + "custom_attributes": [ + { + "value": "0", + "attribute_code": "required_options" + }, + { + "value": "0", + "attribute_code": "has_options" + }, + { + "value": "2", + "attribute_code": "tax_class_id" + }, + { + "value": [ + "15", + "36", + "2" + ], + "attribute_code": "category_ids" + }, + { + "value": "170", + "attribute_code": "size" + }, + { + "value": "52", + "attribute_code": "color" + }, + { + "value": "/m/h/mh01-gray_main.jpg", + "attribute_code": "image" + }, + { + "value": "/m/h/mh01-gray_main.jpg", + "attribute_code": "small_image" + }, + { + "value": "/m/h/mh01-gray_main.jpg", + "attribute_code": "thumbnail" + }, + { + "value": "chaz-kangeroo-hoodie-l-gray", + "attribute_code": "url_key" + }, + { + "value": "0", + "attribute_code": "msrp_display_actual_price_type" + } + ] + }, + { + "price": 52, + "name": "Chaz Kangeroo Hoodie-L-Orange", + "sku": "MH01-L-Orange", + "custom_attributes": [ + { + "value": "0", + "attribute_code": "required_options" + }, + { + "value": "0", + "attribute_code": "has_options" + }, + { + "value": "2", + "attribute_code": "tax_class_id" + }, + { + "value": [ + "15", + "36", + "2" + ], + "attribute_code": "category_ids" + }, + { + "value": "170", + "attribute_code": "size" + }, + { + "value": "56", + "attribute_code": "color" + }, + { + "value": "/m/h/mh01-orange_main.jpg", + "attribute_code": "image" + }, + { + "value": "/m/h/mh01-orange_main.jpg", + "attribute_code": "small_image" + }, + { + "value": "/m/h/mh01-orange_main.jpg", + "attribute_code": "thumbnail" + }, + { + "value": "chaz-kangeroo-hoodie-l-orange", + "attribute_code": "url_key" + }, + { + "value": "0", + "attribute_code": "msrp_display_actual_price_type" + } + ] + }, + { + "price": 52, + "name": "Chaz Kangeroo Hoodie-XL-Black", + "sku": "MH01-XL-Black", + "custom_attributes": [ + { + "value": "0", + "attribute_code": "required_options" + }, + { + "value": "0", + "attribute_code": "has_options" + }, + { + "value": "2", + "attribute_code": "tax_class_id" + }, + { + "value": [ + "15", + "36", + "2" + ], + "attribute_code": "category_ids" + }, + { + "value": "171", + "attribute_code": "size" + }, + { + "value": "49", + "attribute_code": "color" + }, + { + "value": "/m/h/mh01-black_main.jpg", + "attribute_code": "image" + }, + { + "value": "/m/h/mh01-black_main.jpg", + "attribute_code": "small_image" + }, + { + "value": "/m/h/mh01-black_main.jpg", + "attribute_code": "thumbnail" + }, + { + "value": "chaz-kangeroo-hoodie-xl-black", + "attribute_code": "url_key" + }, + { + "value": "0", + "attribute_code": "msrp_display_actual_price_type" + } + ] + }, + { + "price": 52, + "name": "Chaz Kangeroo Hoodie-XL-Gray", + "sku": "MH01-XL-Gray", + "custom_attributes": [ + { + "value": "0", + "attribute_code": "required_options" + }, + { + "value": "0", + "attribute_code": "has_options" + }, + { + "value": "2", + "attribute_code": "tax_class_id" + }, + { + "value": [ + "15", + "36", + "2" + ], + "attribute_code": "category_ids" + }, + { + "value": "171", + "attribute_code": "size" + }, + { + "value": "52", + "attribute_code": "color" + }, + { + "value": "/m/h/mh01-gray_main.jpg", + "attribute_code": "image" + }, + { + "value": "/m/h/mh01-gray_main.jpg", + "attribute_code": "small_image" + }, + { + "value": "/m/h/mh01-gray_main.jpg", + "attribute_code": "thumbnail" + }, + { + "value": "chaz-kangeroo-hoodie-xl-gray", + "attribute_code": "url_key" + }, + { + "value": "0", + "attribute_code": "msrp_display_actual_price_type" + } + ] + }, + { + "price": 52, + "name": "Chaz Kangeroo Hoodie-XL-Orange", + "sku": "MH01-XL-Orange", + "custom_attributes": [ + { + "value": "0", + "attribute_code": "required_options" + }, + { + "value": "0", + "attribute_code": "has_options" + }, + { + "value": "2", + "attribute_code": "tax_class_id" + }, + { + "value": [ + "15", + "36", + "2" + ], + "attribute_code": "category_ids" + }, + { + "value": "171", + "attribute_code": "size" + }, + { + "value": "56", + "attribute_code": "color" + }, + { + "value": "/m/h/mh01-orange_main.jpg", + "attribute_code": "image" + }, + { + "value": "/m/h/mh01-orange_main.jpg", + "attribute_code": "small_image" + }, + { + "value": "/m/h/mh01-orange_main.jpg", + "attribute_code": "thumbnail" + }, + { + "value": "chaz-kangeroo-hoodie-xl-orange", + "attribute_code": "url_key" + }, + { + "value": "0", + "attribute_code": "msrp_display_actual_price_type" + } + ] + } + ], + "category_ids": [ + "3", + "7", + "5", + "8" + ], + "stock": { + "min_sale_qty": 1, + "qty_increments": 0, + "stock_status_changed_auto": 0, + "is_in_stock": true, + "show_default_notification_message": false, + "use_config_max_sale_qty": true, + "product_id": 19, + "use_config_qty_increments": true, + "notify_stock_qty": 1, + "manage_stock": true, + "item_id": 19, + "min_qty": 0, + "use_config_min_qty": true, + "use_config_notify_stock_qty": true, + "stock_id": 1, + "use_config_backorders": true, + "max_sale_qty": 10000, + "backorders": 0, + "qty": 100, + "use_config_enable_qty_inc": true, + "is_decimal_divided": false, + "enable_qty_increments": false, + "is_qty_decimal": false, + "use_config_manage_stock": true, + "low_stock_date": null, + "use_config_min_sale_qty": 1 + } + } + } + ] + } +``` + +## Category type + +The proposed data format is a result of: + +- [catalogCategoryListV1GetListGet](http://devdocs.magento.com/swagger/#!/catalogCategoryListV1/catalogCategoryListV1GetListGet) + +```json +{ + "_index": "storefront_catalog", + "_type": "category", + "_id": "22", + "_score": 1, + "_source": { + "id": 22, + "parent_id": 20, + "name": "Bottoms", + "is_active": true, + "position": 2, + "level": 3, + "product_count": 0, + "children_data": [ + { + "is_active": true, + "level": 4, + "parent_id": 22, + "product_count": 91, + "name": "Pants", + "id": 27, + "position": 1, + "children_data": [] + }, + { + "is_active": true, + "level": 4, + "parent_id": 22, + "product_count": 137, + "name": "Shorts", + "id": 28, + "position": 2, + "children_data": [] + } + ], + "tsk": 1509551138285 + } +} +``` + +## Attribute type + +The data format here is a result of: + +- [catalogProductAttributeRepositoryV1GetListGet](http://devdocs.magento.com/swagger/#!/catalogProductAttributeRepositoryV1/catalogProductAttributeRepositoryV1GetListGet) + +```json +{ + "_index": "storefront_catalog", + "_type": "attribute", + "_id": "79", + "_score": 1, + "_source": { + "is_wysiwyg_enabled": false, + "is_html_allowed_on_front": false, + "used_for_sort_by": false, + "is_filterable": false, + "is_filterable_in_search": false, + "is_used_in_grid": true, + "is_visible_in_grid": false, + "is_filterable_in_grid": false, + "position": 0, + "apply_to": ["simple", "virtual", "bundle", "downloadable", "configurable"], + "is_searchable": "0", + "is_visible_in_advanced_search": "0", + "is_comparable": "0", + "is_used_for_promo_rules": "0", + "is_visible_on_front": "0", + "used_in_product_listing": "1", + "is_visible": true, + "scope": "website", + "attribute_id": 79, + "attribute_code": "special_from_date", + "frontend_input": "date", + "entity_type_id": "4", + "is_required": false, + "options": [], + "is_user_defined": false, + "default_frontend_label": "Special Price From Date", + "frontend_labels": null, + "backend_type": "datetime", + "backend_model": "Magento\\Catalog\\Model\\Attribute\\Backend\\Startdate", + "is_unique": "0", + "validation_rules": [], + "id": 79, + "tsk": 1510353353440 + } +} +``` + +## TaxRule type + +The suggested data format is a combined result of: + +- [taxTaxRuleRepositoryV1GetListGet](http://devdocs.magento.com/swagger/#!/taxTaxRuleRepositoryV1/taxTaxRuleRepositoryV1GetListGet) +- [taxTaxRateRepositoryV1GetGet](http://devdocs.magento.com/swagger/#!/taxTaxRateRepositoryV1/taxTaxRateRepositoryV1GetGet) + +```json +{ + "id": 2, + "code": "Poland", + "priority": 0, + "position": 0, + "customer_tax_class_ids": [3], + "product_tax_class_ids": [2], + "tax_rate_ids": [4], + "calculate_subtotal": false, + "rates": [ + { + "id": 4, + "tax_country_id": "PL", + "tax_region_id": 0, + "tax_postcode": "*", + "rate": 23, + "code": "VAT23%", + "titles": [] + } + ], + "tsk": 1510603185144 +} +``` + +# Example ElasticSearch queries + +ElasticSearch is the main data store and [elasticsearch-js library](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html) is used for accesing the data store. We're alsos using [bodybuilder module](https://www.npmjs.com/package/bodybuilder) for easier ES query building. + +## Product search + +```json + starting request { + "method": "POST", + "path": "/vue_storefront_catalog/product/_search", + "body": { + "query": { + "bool": { + "filter": { + "bool": { + "must": [ + { + "range": { + "visibility": { + "gte": 3, + "lte": 4 + } + } + }, + { + "terms": { + "category.category_id": [ + 20, + 21, + 23 + ] + } + } + ], + "should": [ + { + "bool": { + "must": [ + { + "match": { + "color": 53 + } + }, + { + "match": { + "size": 173 + } + }, + { + "match": { + "type_id": "simple" + } + } + ] + } + }, + { + "bool": { + "must": [ + { + "match": { + "color_options": 53 + } + }, + { + "match": { + "size_options": 173 + } + }, + { + "match": { + "type_id": "configurable" + } + } + ] + } + } + ] + } + }, + "must": { + "range": { + "price": { + "gt": 0 + } + } + } + } + }, + "aggs": { + "agg_terms_color": { + "terms": { + "field": "color" + } + }, + "agg_terms_color_options": { + "terms": { + "field": "color_options" + } + }, + "agg_terms_size": { + "terms": { + "field": "size" + } + }, + "agg_terms_size_options": { + "terms": { + "field": "size_options" + } + }, + "agg_terms_price": { + "terms": { + "field": "price" + } + }, + "agg_range_price": { + "range": { + "field": "price", + "ranges": [ + { + "from": 0, + "to": 50 + }, + { + "from": 50, + "to": 100 + }, + { + "from": 100, + "to": 150 + }, + { + "from": 150 + } + ] + } + } + } + }, + "query": { + "size": 18, + "from": 0, + "sort": "" + } + } +``` + +## List categories + +```json + starting request { + "method": "POST", + "path": "/vue_storefront_catalog/category/_search", + "body": { + "query": { + "bool": { + "filter": { + "term": { + "is_active": true + } + } + } + } + }, + "query": { + "size": 50, + "from": 0, + "sort": "position:asc" + } + } +``` + +## Get attributes for filters (on category page) + +```json + starting request { + "method": "POST", + "path": "/vue_storefront_catalog/attribute/_search", + "body": { + "query": { + "bool": { + "filter": { + "bool": { + "should": [ + { + "term": { + "attribute_id": "93" + } + }, + { + "term": { + "attribute_id": "141" + } + } + ] + } + } + } + } + }, + "query": { + "size": 50, + "from": 0, + "sort": "" + } + } +``` + +## Get attributes for product page + +```json + starting request { + "method": "POST", + "path": "/vue_storefront_catalog/attribute/_search", + "body": { + "query": { + "bool": { + "filter": { + "bool": { + "should": [ + { + "term": { + "is_user_defined": true + } + } + ] + } + } + } + } + }, + "query": { + "size": 50, + "from": 0, + "sort": "" + } + } +``` diff --git a/docs/guide/data/entity-types.md b/docs/guide/data/entity-types.md new file mode 100644 index 0000000000..69238298cd --- /dev/null +++ b/docs/guide/data/entity-types.md @@ -0,0 +1,110 @@ +# Data Entity Types + +Vue-storefront uses multiple data entity types to cover the whole scope of storefront. +Default entity types are + +- Product +- Category +- Attribute +- Taxrule + +These entity types were hardcoded and there were no ability to easy use another custom entity type required for customization. + +Now Vue-storefront has a new logic to work with entities in the data fetching prospective: Entity Types. + +Each search adapter should register an entity type to cover a search feature. Default API and new GraphQL search adapters are updated to register all required existing entity types. But developer can also inject custom entity types to work with some other custom entity type data (eg to get list of offline stores or something else). + +To use it, an internal GraphQL server should be updated with adding a corresponding resolver for the new entity type. Also, you can use some other external GraphQL server that already have implemented a resolver for this entity type. + +To register such an entity type, you should use `searchAdapter.registerEntityTypeByQuer`y` method like shown in the example below: + +```js +const factory = new SearchAdapterFactory(); +let searchAdapter = factory.getSearchAdapter('graphql'); +searchAdapter.registerEntityTypeByQuery('testentity', { + url: 'http://localhost:8080/graphql/', + query: require('./queries/testentity.gql'), + queryProcessor: query => { + // function that can modify the query each time before it's being executed + return query; + }, + resultPorcessor: (resp, start, size) => { + if (resp === null) { + throw new Error('Invalid graphQl result - null not exepcted'); + } + if (resp.hasOwnProperty('data')) { + return processESResponseType(resp.data.testentity, start, size); + } else { + 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'", + ); + } + } + }, +}); +``` + +Sample extension `sample-custom-entity-graphql` was added to illustrate how it can be used. It injects a custom entity type `testentity` and set a custom GraphQL server URL (it is the same as a default api host in the example just because a resolver for this `testentity` was added there for testing. But please notice it was removed there) + +To test a sample extension with resolver you can add GraphqQL schema file and resolver file in the separate `src/graphql/elastcisearch/testentity` folder in the Vue Storefront API. + +`schema.graphqls` file: + +```graphql +type Query { + testentity(filter: TestInput): ESResponse +} +input TestInput + @doc( + description: "TaxRuleInput specifies the tax rules information to search" + ) { + id: FilterTypeInput + @doc(description: "An ID that uniquely identifies the tax rule") + code: FilterTypeInput + @doc( + description: "The unique identifier for an tax rule. This value should be in lowercase letters without spaces." + ) + priority: FilterTypeInput @doc(description: "Priority of the tax rule") + position: FilterTypeInput @doc(description: "Position of the tax rule") + customer_tax_class_ids: FilterTypeInput + @doc(description: "Cunstomer tax class ids of the tax rule") + product_tax_class_ids: FilterTypeInput + @doc(description: "Products tax class ids of the tax rule") + tax_rate_ids: FilterTypeInput + @doc(description: "Tax rates ids of the tax rule") + calculate_subtotal: FilterTypeInput + @doc(description: "Calculating subtotals of the tax rule") + rates: FilterTypeInput @doc(description: "Rates of the tax rule") +} +``` + +Resolver file `resolver.js`: + +```js +import config from 'config'; +import client from '../client'; +import { buildQuery } from '../queryBuilder'; + +async function testentity(filter) { + let query = buildQuery({ filter, pageSize: 150, type: 'taxrule' }); + + const response = await client.search({ + index: config.elasticsearch.indices[0], + type: config.elasticsearch.indexTypes[4], + body: query, + }); + + return response; +} + +const resolver = { + Query: { + testentity: (_, { filter }) => testentity(filter), + }, +}; + +export default resolver; +``` diff --git a/docs/guide/data/vuex.md b/docs/guide/data/vuex.md deleted file mode 100644 index 342630fe56..0000000000 --- a/docs/guide/data/vuex.md +++ /dev/null @@ -1,3 +0,0 @@ -# Working with Vuex - -_Work in progress_ diff --git a/docs/guide/core-themes/extensions.md b/docs/guide/extensions/extensions.md similarity index 100% rename from docs/guide/core-themes/extensions.md rename to docs/guide/extensions/extensions.md diff --git a/docs/guide/installation/magento.md b/docs/guide/installation/magento.md index 09490cb394..6e803cc3b7 100644 --- a/docs/guide/installation/magento.md +++ b/docs/guide/installation/magento.md @@ -1,15 +1,5 @@ # Integration with Magento 2 -## Integrating Magento2 with your local instance - -As a first step, you need to to install [mage2vuestorefront ](https://github.com/DivanteLtd/mage2vuestorefront): - -```bash -git clone https://github.com/DivanteLtd/mage2vuestorefront.git mage2vs -cd mage2vs/src -yarn install -``` - The tool is using Magento2 API via OAuth authorization, so you need to prepare Magento Integration access at first. Go to your Magento2 admin panel and click: _System -> Integrations_ ![Magento Admin Panel](/vue-storefront/magento_1.png) @@ -26,6 +16,55 @@ In the result you’ll click _Activate_ and get some oauth access tokens: ![Magento tokens](/vue-storefront/magento_3.png) +## Integrating Magento2 with your local instance + +### Fast integration + +Magento2 data import is now integrated into `vue-storefront-api` for simplicity. It's still managed by the [mage2vuestorefront](https://github.com/DivanteLtd/mage2vuestorefront) - added as a dependency to `vue-storefront-api`. + +After setting the `config.magento2.api` section using yours Magento2 oauth credentials: + +```json + "magento2": { + "url": "http://magento2.demo-1.xyz.com", + "imgUrl": "http://localhost:8080/media/catalog/product", + "assetPath": "/../var/magento2-sample-data/pub/media", + "magentoUserName": "", + "magentoUserPassword": "", + "httpUserName": "", + "httpUserPassword": "", + "api": { + "url": "http://demo-magento2.vuestorefront.io/rest", + "consumerKey": "byv3730rhoulpopcq64don8ukb8lf2gq", + "consumerSecret": "u9q4fcobv7vfx9td80oupa6uhexc27rb", + "accessToken": "040xx3qy7s0j28o3q0exrfop579cy20m", + "accessTokenSecret": "7qunl3p505rubmr7u1ijt7odyialnih9" + } + }, +``` + +You can run the following command to execute the full import of all the Products, Categories and other important stuff to your Elastic Search instance: + +```bash +yarn mage2vs import +``` + +... or in multistore setup you can run the same command with specified `store-code` parameter + +```bash + yarn mage2vs import --store-code=de +``` + +### Manual integration + +As a first step, you need to to install [mage2vuestorefront ](https://github.com/DivanteLtd/mage2vuestorefront): + +```bash +git clone https://github.com/DivanteLtd/mage2vuestorefront.git mage2vs +cd mage2vs/src +yarn install +``` + Now please edit the `src/config.js` file in your `mage2vuestorefront` directory to set the following section: ```js @@ -42,7 +81,7 @@ As you can see, you can override the defaults by ENV variables as well. The rest of config.js entries points out to your `vue-storefront-api` based Docker and Redis instances which are required by `mage2nosql` to work. -To import all the Products, Categories and other important stuff to your Elastic Search instance you should run the following commands (the sequence of commands is important  -  as for example `node cli.js categories` populates Redis cache for the further use of `node cli.js` products and so on) +To make the full import, you should run the following commands (the sequence of commands is important  -  as for example `node cli.js categories` populates Redis cache for the further use of `node cli.js` products and so on) ```bash node cli.js taxrule diff --git a/docs/guide/installation/windows.md b/docs/guide/installation/windows.md index 10785d541a..6e05637fb9 100644 --- a/docs/guide/installation/windows.md +++ b/docs/guide/installation/windows.md @@ -39,6 +39,8 @@ docker-compose up This step can take some minutes. +Note: If it appears that docker-compose is hanging, try opening a new terminal and continue to the next step using that terminal. Allow docker-compose to continue running in the background. + 6. Restore products database and run latest migrations ```bash diff --git a/docs/guide/vuex/introduction.md b/docs/guide/vuex/introduction.md new file mode 100644 index 0000000000..bff68e850d --- /dev/null +++ b/docs/guide/vuex/introduction.md @@ -0,0 +1,60 @@ +# Introduction + +All data processing and remote requests should be managed by Vuex data stores. Core module contains more than [10 default data stores](https://github.com/DivanteLtd/vue-storefront/tree/master/core/store/modules) and can be easily extended by [store extensions](../extensions/extensions.md). +You can modify the existing store actions by responding to events. Events are specified in the docs below and could be found in the [core module](https://github.com/DivanteLtd/vue-storefront/tree/master/core), where `EventBus.$emit` has been mostly used for Vuex Actions. + +**You should put all the REST calls, Elasticsearch data queries inside the Vuex Actions**. This is our default design pattern for managing the data. + +## Vuex modules + +- [Product](Product%20Store.md) +- [Category](Category%20Store.md) +- [Cart](Cart%20Store.md) +- [Checkout](Checkout%20Store.md) +- [Order](Order%20Store.md) +- [Stock](Stock%20Store.md) +- [Sync](Sync%20Store.md) +- [User](User%20Store.md) +- [Attribute](Attribute%20Store.md) +- [UI Store]() + +## Override existing core modules + +Existing core modules can be overridden in themes store. Just import any core store modules and override it using `extendStore()` utility method like the example given below in `themes/default/store/ui-store.js`. + +``` +import coreStore from '@vue-storefront/store/modules/ui-store' +import { extendStore } from '@vue-storefront/core/lib/themes' + +const state = { + // override state of core ui module... +} + +const mutations = { + // override mutations of core ui module... +} + +const actions = { + // override actions of core ui module... +} + +export default extendStore(coreStore, { + state, + mutations, + actions +}) +``` + +And then import it in `themes/default/store/index.js` + +``` +import ui from './ui-store' + +export default { + ui +} +``` + +## Related + +[Working with data](data.md) diff --git a/docs/guide/vuex/product-store.md b/docs/guide/vuex/product-store.md new file mode 100644 index 0000000000..be7ba5ffc0 --- /dev/null +++ b/docs/guide/vuex/product-store.md @@ -0,0 +1 @@ +# Product diff --git a/src/themes/default/assets/android-icon-144x144.png b/src/themes/default/assets/android-icon-144x144.png index c966d2d898..c9e52ac054 100644 Binary files a/src/themes/default/assets/android-icon-144x144.png and b/src/themes/default/assets/android-icon-144x144.png differ diff --git a/src/themes/default/assets/android-icon-168x168.png b/src/themes/default/assets/android-icon-168x168.png index 5a8d707207..aa88da2a1c 100644 Binary files a/src/themes/default/assets/android-icon-168x168.png and b/src/themes/default/assets/android-icon-168x168.png differ diff --git a/src/themes/default/assets/android-icon-192x192.png b/src/themes/default/assets/android-icon-192x192.png index 6a760f6c5a..684955c9d2 100644 Binary files a/src/themes/default/assets/android-icon-192x192.png and b/src/themes/default/assets/android-icon-192x192.png differ diff --git a/src/themes/default/assets/android-icon-48x48.png b/src/themes/default/assets/android-icon-48x48.png index 71863317d0..aadd50e932 100644 Binary files a/src/themes/default/assets/android-icon-48x48.png and b/src/themes/default/assets/android-icon-48x48.png differ diff --git a/src/themes/default/assets/android-icon-512x512.png b/src/themes/default/assets/android-icon-512x512.png index 26620e281c..b04137be4f 100644 Binary files a/src/themes/default/assets/android-icon-512x512.png and b/src/themes/default/assets/android-icon-512x512.png differ diff --git a/src/themes/default/assets/android-icon-72x72.png b/src/themes/default/assets/android-icon-72x72.png index 248531a165..a52539ce3d 100644 Binary files a/src/themes/default/assets/android-icon-72x72.png and b/src/themes/default/assets/android-icon-72x72.png differ diff --git a/src/themes/default/assets/android-icon-96x96.png b/src/themes/default/assets/android-icon-96x96.png index 9b3e593f59..cad8888d51 100644 Binary files a/src/themes/default/assets/android-icon-96x96.png and b/src/themes/default/assets/android-icon-96x96.png differ diff --git a/src/themes/default/assets/apple-touch-icon.png b/src/themes/default/assets/apple-touch-icon.png index 375a05b4c7..d7d9883c2f 100644 Binary files a/src/themes/default/assets/apple-touch-icon.png and b/src/themes/default/assets/apple-touch-icon.png differ diff --git a/src/themes/default/assets/apple_splash_1125.png b/src/themes/default/assets/apple_splash_1125.png index 0004950935..a652e7ba49 100644 Binary files a/src/themes/default/assets/apple_splash_1125.png and b/src/themes/default/assets/apple_splash_1125.png differ diff --git a/src/themes/default/assets/apple_splash_1242.png b/src/themes/default/assets/apple_splash_1242.png index f3ae838e9d..fe83551d4e 100644 Binary files a/src/themes/default/assets/apple_splash_1242.png and b/src/themes/default/assets/apple_splash_1242.png differ diff --git a/src/themes/default/assets/apple_splash_1536.png b/src/themes/default/assets/apple_splash_1536.png index e3183f5b01..45e1ad20aa 100644 Binary files a/src/themes/default/assets/apple_splash_1536.png and b/src/themes/default/assets/apple_splash_1536.png differ diff --git a/src/themes/default/assets/apple_splash_1668.png b/src/themes/default/assets/apple_splash_1668.png index b84b4c2ca2..e1a3f1a9d2 100644 Binary files a/src/themes/default/assets/apple_splash_1668.png and b/src/themes/default/assets/apple_splash_1668.png differ diff --git a/src/themes/default/assets/apple_splash_2048.png b/src/themes/default/assets/apple_splash_2048.png index de0cafd3dd..34d4ef2c0c 100644 Binary files a/src/themes/default/assets/apple_splash_2048.png and b/src/themes/default/assets/apple_splash_2048.png differ diff --git a/src/themes/default/assets/apple_splash_640.png b/src/themes/default/assets/apple_splash_640.png index 8a9d21ced3..92d222ee97 100644 Binary files a/src/themes/default/assets/apple_splash_640.png and b/src/themes/default/assets/apple_splash_640.png differ diff --git a/src/themes/default/assets/apple_splash_750.png b/src/themes/default/assets/apple_splash_750.png index e1fb70068d..445e473ee8 100644 Binary files a/src/themes/default/assets/apple_splash_750.png and b/src/themes/default/assets/apple_splash_750.png differ diff --git a/src/themes/default/assets/logo.png b/src/themes/default/assets/logo.png index 5b4623915e..63222d49ef 100644 Binary files a/src/themes/default/assets/logo.png and b/src/themes/default/assets/logo.png differ diff --git a/src/themes/default/assets/placeholder.jpg b/src/themes/default/assets/placeholder.jpg index f151b7fdb3..2f3d6a9011 100644 Binary files a/src/themes/default/assets/placeholder.jpg and b/src/themes/default/assets/placeholder.jpg differ diff --git a/src/themes/default/components/core/AddToCart.vue b/src/themes/default/components/core/AddToCart.vue index 2ff67f5006..65d80d4e36 100644 --- a/src/themes/default/components/core/AddToCart.vue +++ b/src/themes/default/components/core/AddToCart.vue @@ -23,7 +23,7 @@ export default { return formatProductMessages(product.errors) !== '' } }, - created () { + beforeMount () { this.$bus.$on('product-after-removevariant', this.onAfterRemovedVariant) }, beforeDestroy () { diff --git a/src/themes/default/components/core/Notification.vue b/src/themes/default/components/core/Notification.vue index e61646f4b3..495ccfb5bb 100644 --- a/src/themes/default/components/core/Notification.vue +++ b/src/themes/default/components/core/Notification.vue @@ -20,7 +20,7 @@ :class="`border-${notification.type}`" id="notificationAction1" data-testid="notificationAction1" - @click="action(notification.action1.action, index)" + @click="action(notification.action1.action, index, notification)" > {{ notification.action1.label }} @@ -28,7 +28,7 @@ class="py10 px20 pointer weight-400 notification-action uppercase" id="notificationAction2" data-testid="notificationAction2" - @click="action(notification.action2.action, index)" + @click="action(notification.action2.action, index, notification)" v-if="notification.action2" > {{ notification.action2.label }} diff --git a/src/themes/default/components/core/blocks/Checkout/ThankYouPage.vue b/src/themes/default/components/core/blocks/Checkout/ThankYouPage.vue index f727d85396..232b75b89c 100644 --- a/src/themes/default/components/core/blocks/Checkout/ThankYouPage.vue +++ b/src/themes/default/components/core/blocks/Checkout/ThankYouPage.vue @@ -113,12 +113,31 @@ export default { subject: this.$t('What we can improve?'), emailText: this.feedback }, - this.notifyResult + this.notifySuccess, + this.notifyFailure ) }, - notifyResult (type, message) { + notifySuccess (message) { this.$bus.$emit('notification', { - type, + type: 'success', + message, + action1: { label: this.$t('OK'), action: 'close' } + }) + if (this.$store.state.config.mailer.sendConfirmation) { + this.sendEmail( + { + sourceAddress: this.$store.state.config.mailer.contactAddress, + targetAddress: this.$store.state.checkout.personalDetails.emailAddress, + subject: this.$t('Confirmation of receival'), + emailText: this.$t(`Dear customer,\n\nWe have received your letter.\nThank you for your feedback!`), + confirmation: true + } + ) + } + }, + notifyFailure (message) { + this.$bus.$emit('notification', { + type: 'error', message, action1: { label: this.$t('OK'), action: 'close' } }) diff --git a/src/themes/default/components/core/blocks/Product/Related.vue b/src/themes/default/components/core/blocks/Product/Related.vue index ce6290ef24..e5d490e3ce 100644 --- a/src/themes/default/components/core/blocks/Product/Related.vue +++ b/src/themes/default/components/core/blocks/Product/Related.vue @@ -39,13 +39,15 @@ export default { components: { ProductListing }, - created () { + beforeMount () { this.$bus.$on('product-after-load', this.refreshList) if (store.state.config.usePriceTiers) { this.$bus.$on('user-after-loggedin', this.refreshList) this.$bus.$on('user-after-logout', this.refreshList) } + + this.refreshList() }, beforeDestroy () { if (store.state.config.usePriceTiers) { @@ -56,9 +58,6 @@ export default { destroyed () { this.$bus.$off('product-after-load', this.refreshList) }, - beforeMount () { - this.refreshList() - }, methods: { refreshList () { let sku = this.productLinks diff --git a/src/themes/default/components/core/blocks/SearchPanel/SearchPanel.vue b/src/themes/default/components/core/blocks/SearchPanel/SearchPanel.vue index bfb4e03932..5bbc25cdbb 100644 --- a/src/themes/default/components/core/blocks/SearchPanel/SearchPanel.vue +++ b/src/themes/default/components/core/blocks/SearchPanel/SearchPanel.vue @@ -4,50 +4,59 @@ :class="{ active: isOpen }" data-testid="searchPanel" > -
-
- - close - -
-
-
- - + + close +
-
- - -
- {{ $t('No results were found.') }} +
+
+
+ +
+ + search + + +
- -
-
- - +
+
+ + +
+ {{ $t('No results were found.') }} +
+
+
+
+ + +
@@ -85,46 +94,113 @@ export default { transition: transform 300ms $motion-main; overflow-y: auto; overflow-x: hidden; + .close-icon-row { + display: flex; + justify-content: flex-end; + } + + .container { + padding-left: 40px; + padding-right: 40px; + } + + .row { + margin-left: -15px; + margin-right: -15px; + } + + .col-md-12 { + padding-left: 15px; + padding-right: 15px; + } + + .product-listing { + padding-top: 30px; + } + + .product { + box-sizing: border-box; + width: 33.33%; + padding-left: 15px; + padding-right: 15px; + } &.active { transform: translateX(0) } - .product { - width: 30%; - padding: 10px; + .close-icon { + padding: 18px 8px; } - input { - width: calc(100% - 40px); + .search-input-group { + display: flex; + border-bottom: 1px solid #bdbdbd; + } + + .search-icon { + width: 60px; + height: 60px; + display: flex; + align-items: center; + justify-content: center; + } + + .search-panel-input { + border: none; + width: 100%; + padding-bottom: 0px; + padding-top: 0px; + outline: 0; + height: 60px; } .no-results { top: 80px; - width: calc(100% - 40px); + width: 100%; } -} -i { - opacity: 0.6; -} + i { + opacity: 0.6; + } -i:hover { - opacity: 1; -} + i:hover { + opacity: 1; + } -.close-button { - background: #fff; + .close-button { + background: #fff; + } } -@media only screen and (max-width:50em) { - .searchpanel .product { - width: 50%; - box-sizing: border-box; - } - button { - width: 100%; - margin-bottom: 15px; +@media only screen and (max-width:575.98px) { + .searchpanel { + + .container { + padding-left: 30px; + padding-right: 30px; + } + + .row { + margin-right: -10px; + margin-left: -10px; + } + + .col-md-12 { + padding-left: 10px; + padding-right: 10px; + } + + .product { + width: 50%; + padding-left: 10px; + padding-right: 10px; + } + + button { + width: 100%; + margin-bottom: 15px; + } } } diff --git a/src/themes/default/package.json b/src/themes/default/package.json index 1bf6b46b28..ce01de3ef4 100644 --- a/src/themes/default/package.json +++ b/src/themes/default/package.json @@ -1,6 +1,6 @@ { "name": "@vue-storefront/theme-default", - "version": "1.4.1", + "version": "1.4.2", "description": "Default theme for Vue Storefront", "main": "index.js", "scripts": {