diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cde415515a..9247c0dbe5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,12 +21,12 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v1.4.1 with: node-version: '13.x' - name: Install - uses: bahmutov/npm-install@v1 + uses: bahmutov/npm-install@v1.1.0 - name: Get repository name, and branch name run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3c9e0b894e..a43e0f65a7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v1.4.1 with: node-version: '13.x' @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v1.4.1 with: node-version: '13.x' @@ -69,7 +69,7 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v1.4.1 with: node-version: '13.x' @@ -82,7 +82,7 @@ jobs: run: rsync -r ./vue-storefront-workspace/template/vue-storefront/ ./ - name: Install - uses: bahmutov/npm-install@v1 + uses: bahmutov/npm-install@v1.1.0 - name: Build run: yarn build diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..2e81d78770 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,8 @@ +[submodule "src/modules/vsf-cache-nginx"] + path = src/modules/vsf-cache-nginx + url = https://github.com/new-fantastic/vsf-cache-nginx.git + branch = master +[submodule "src/modules/vsf-cache-varnish"] + path = src/modules/vsf-cache-varnish + url = https://github.com/new-fantastic/vsf-cache-varnish.git + branch = master diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eff44ea75..3ae774a0be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.12.0-rc1] - UNRELEASED ### Added +- Allow parent_ids field on product as an alternative to urlpath based breadcrumb navigation (#4219) - Pass the original item_id when updating/deleting a cart entry @carlokok (#4218) - Separating endpoints for CSR/SSR - @Fifciu (#2861) - Added short hands for version and help flags - @jamesgeorge007 (#3946) @@ -26,6 +27,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Product Page Schema implementation as JSON-LD - @Michal-Dziedzinski (#3704) - Add `/cache-version.json` route to get current cache version - Built-in module for detecting device type based on UserAgent with SSR support - @Fifciu +- Update to `storefront-query-builder` version `1.0.0` - @cewald (#4234) +- Move generating files from webpack config to script @gibkigonzo (#4236) +- Add correct type matching to `getConfigurationMatchLevel` - @cewald (#4241) +- Support `useSpecificImagePaths` with `useExactUrlsNoProxy` - @cewald (#4243) +- Adds module which handles cache invalidation for Fastly. - @gibkigonzo (#4096) +- Add vsf-cache-nginx and vsf-cache-varnish modules - @gibkigonzo (#4096) ### Fixed @@ -45,6 +52,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed `cart/isVirtualCart` to return `false` when cart is empty - @haelbichalex(#4182) - Use `setProductGallery` in `product/setCurrent` to use logic of the action - @cewald (#4153) - Use same data format in getConfigurationMatchLevel - @gibkigonzo (#4208) +- removed possible memory leak in ssr - @resubaka (#4247) ### Changed / Improved @@ -59,6 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removed `product/loadConfigurableAttributes` calls - @andrzejewsky, @gibkigonzo (#3336) - Disable `mapFallback` url by default - @gibkigonzo(#4092) - Include token in pricing sync - @carlokok (#4156) +- Move 'graphql' search adapter from core to src (deprecated) - @gibkigonzo (#4214) ## [1.11.2] - 2020.03.10 diff --git a/config/default.json b/config/default.json index 38307b860a..22165085eb 100644 --- a/config/default.json +++ b/config/default.json @@ -522,5 +522,14 @@ "enableMapFallbackUrl": false, "endpoint": "/api/url", "map_endpoint": "/api/url/map" + }, + "fastly": { + "enabled":false + }, + "nginx": { + "enabled":false + }, + "varnish": { + "enabled":false } } diff --git a/core/helpers/index.ts b/core/helpers/index.ts index e278d39ccd..9fd9f2ab38 100644 --- a/core/helpers/index.ts +++ b/core/helpers/index.ts @@ -47,14 +47,14 @@ export function slugify (text) { * @returns {string} */ export function getThumbnailPath (relativeUrl: string, width: number = 0, height: number = 0, pathType: string = 'product'): string { + if (config.images.useSpecificImagePaths) { + const path = config.images.paths[pathType] !== undefined ? config.images.paths[pathType] : '' + relativeUrl = path + relativeUrl + } + if (config.images.useExactUrlsNoProxy) { - return coreHooksExecutors.afterProductThumbnailPathGenerate({ path: relativeUrl, sizeX: width, sizeY: height }).path // this is exact url mode + return coreHooksExecutors.afterProductThumbnailPathGenerate({ path: relativeUrl, sizeX: width, sizeY: height, pathType }).path // this is exact url mode } else { - if (config.images.useSpecificImagePaths) { - const path = config.images.paths[pathType] !== undefined ? config.images.paths[pathType] : '' - relativeUrl = path + relativeUrl - } - let resultUrl if (relativeUrl && (relativeUrl.indexOf('://') > 0 || relativeUrl.indexOf('?') > 0 || relativeUrl.indexOf('&') > 0)) relativeUrl = encodeURIComponent(relativeUrl) // proxyUrl is not a url base path but contains {{url}} parameters and so on to use the relativeUrl as a template value and then do the image proxy opertions @@ -69,7 +69,7 @@ export function getThumbnailPath (relativeUrl: string, width: number = 0, height } const path = relativeUrl && relativeUrl.indexOf('no_selection') < 0 ? resultUrl : config.images.productPlaceholder || '' - return coreHooksExecutors.afterProductThumbnailPathGenerate({ path, sizeX: width, sizeY: height }).path + return coreHooksExecutors.afterProductThumbnailPathGenerate({ path, sizeX: width, sizeY: height, pathType }).path } } diff --git a/core/hooks.ts b/core/hooks.ts index 8e0c083685..733309fcfc 100644 --- a/core/hooks.ts +++ b/core/hooks.ts @@ -23,7 +23,7 @@ const { const { hook: afterProductThumbnailPathGeneratedHook, executor: afterProductThumbnailPathGeneratedExecutor -} = createMutatorHook<{ path: string, sizeX: number, sizeY: number }, { path: string }>() +} = createMutatorHook<{ path: string, pathType: string, sizeX: number, sizeY: number }, { path: string }>() /** Only for internal usage in core */ const coreHooksExecutors = { diff --git a/core/lib/search/adapter/api-search-query/searchAdapter.ts b/core/lib/search/adapter/api-search-query/searchAdapter.ts index 931dd31d19..45242903b2 100644 --- a/core/lib/search/adapter/api-search-query/searchAdapter.ts +++ b/core/lib/search/adapter/api-search-query/searchAdapter.ts @@ -42,6 +42,11 @@ export class SearchAdapter { if (Request.hasOwnProperty('groupToken') && Request.groupToken !== null) { rawQueryObject['groupToken'] = Request.groupToken } + if (Request.sort) { + const [ field, options ] = Request.sort.split(':') + rawQueryObject.applySort({ field, options }) + delete Request.sort + } const storeView = (Request.store === null) ? currentStoreView() : await prepareStoreView(Request.store) Request.index = storeView.elasticsearch.index diff --git a/core/modules/catalog-next/store/category/actions.ts b/core/modules/catalog-next/store/category/actions.ts index c0ea72a6f0..9b702eb108 100644 --- a/core/modules/catalog-next/store/category/actions.ts +++ b/core/modules/catalog-next/store/category/actions.ts @@ -156,7 +156,7 @@ const actions: ActionTree = { Vue.prototype.$cacheTags.add(`C${category.id}`) }) } - const notFoundCategories = searchedIds.filter(categoryId => !categories.some(cat => cat.id === parseInt(categoryId))) + const notFoundCategories = searchedIds.filter(categoryId => !categories.some(cat => cat.id === parseInt(categoryId) || cat.id === categoryId)) commit(types.CATEGORY_ADD_CATEGORIES, categories) commit(types.CATEGORY_ADD_NOT_FOUND_CATEGORY_IDS, notFoundCategories) @@ -216,7 +216,7 @@ const actions: ActionTree = { }, async loadCategoryBreadcrumbs ({ dispatch, getters }, { category, currentRouteName, omitCurrent = false }) { if (!category) return - const categoryHierarchyIds = _prepareCategoryPathIds(category) // getters.getCategoriesHierarchyMap.find(categoryMapping => categoryMapping.includes(category.id)) + const categoryHierarchyIds = category.parent_ids ? [...category.parent_ids, category.id] : _prepareCategoryPathIds(category) // getters.getCategoriesHierarchyMap.find(categoryMapping => categoryMapping.includes(category.id)) const categoryFilters = Object.assign({ 'id': categoryHierarchyIds }, cloneDeep(config.entities.category.breadcrumbFilterFields)) const categories = await dispatch('loadCategories', { filters: categoryFilters, reloadAll: Object.keys(config.entities.category.breadcrumbFilterFields).length > 0 }) const sorted = [] diff --git a/docker/vue-storefront/vue-storefront.sh b/docker/vue-storefront/vue-storefront.sh index a6db7c8673..920b5c52ba 100755 --- a/docker/vue-storefront/vue-storefront.sh +++ b/docker/vue-storefront/vue-storefront.sh @@ -3,7 +3,7 @@ set -e yarn install || exit $? -yarn build:client && yarn build:server && yarn build:sw || exit $? +yarn generate-files && yarn build:client && yarn build:server && yarn build:sw || exit $? if [ "$VS_ENV" = 'dev' ]; then yarn dev diff --git a/docs/guide/cookbook/module.md b/docs/guide/cookbook/module.md index 3ec015afef..45f50decd2 100644 --- a/docs/guide/cookbook/module.md +++ b/docs/guide/cookbook/module.md @@ -678,10 +678,98 @@ It's hands down no-brainer to bootstrap a module _manually_ because the skeleton ### 2. Recipe ### 3. Peep into the kitchen (what happens internally) ### 4. Chef's secret (protip) -
-
-## 6. Anti-patterns & Common pitfalls +## 6. Extend Elasticsearch request body using `storefront-query-builder` + +If you're using the new [`storefront-query-builder`](https://github.com/DivanteLtd/storefront-query-builder) and the `api-search-query` search-adapter ([introduced with v1.1.12](/guide/upgrade-notes/#_1-11-1-12)) it is now possible to extend it by new filters, or even overwrite a existing filter, to customize your Elasticsearch request-bodies. + +So, this way you can add custom Elasticsearch queries to the query-chain and still use the notation of `SearchQuery` in the Vue Storefront. + +> **Note:** This will only work from `storefront-query-builder` version `1.0.0` and `vue-storefront` version `1.12.2`. + +### Usecases + +One usecases where this feature would come in handy is for example if you like to add complex queries on multiple points in your source code. Using the following technique you can just add a custom filter to your `SearchQuery` in a single line inside your VSF source-code using the `query.applyFilter(...)` method and then add the complex logic into your custom-filter inside the API. + +### Registering a new filter + +The `vue-storefront-api` will only try to load filters that are registered in the configs. The extension/module, that contains the filter, must be enabled and the new filter module-classes needs to be registered in its extension config inside the `catalogFilter` array. The filter files must be located inside `filter/catalog/` of your module folder. + +For example: If you have a module called `extend-catalog` with a filter called `StockFilter`, the file path to filter would be `src/api/extensions/extend-catalog/filter/catalog/StockFilter.ts` and the config would look like: +``` +{ + "registeredExtensions": [ "extend-catalog" ], + "extensions": { + "extend-catalog": { + "catalogFilter": [ "StockFilter" ] + } + } +} +``` + +### Filter module-class properties + +The filter can contain four different properties. Followed a short explaination, what they are doing. + +* `check` – This method checks the condition that be must matched to execute the filter. The first valid filter is executed – all afterwards are ignored. +* `priority` – This is the priority in which the filters are going to be called. The sort is lower to higher. +* `mutator` – The mutator method is in charge of prehandling the filter value, to e.g. set defaults or check and change the type. +* `filter` – This method contains the query logic we wan't to add and mutates the `bodybuilder` query-chain. + +### Example + +Lets assume we like to add a possibility to add a default set of product-attribute filters we can apply to each `SearchQuery` without repeating ourselfs in source-code. So, for example, it should filter for two `color`'s and a specific `cut` to supply a filter for spring-coloured short's we implement at several places in our VSF. + +#### Changes in `vue-storefront` repository + +The query in the VSF code would look like this (that's it on the VSF side): +```js +import { SearchQuery } from 'storefront-query-builder' +import { quickSearchByQuery } from '@vue-storefront/core/lib/search' + +//... + +const query = new SearchQuery() +query.applyFilter({ key: 'spring-shorts', value: 'male', scope: 'default' }) +const products = await dispatch('product/list', { query, size: 5 }) +``` + +#### Changes in `vue-storefront-api` repository + +In the `vue-storefront-api` we are going to add the real filter/query magic. +There is already an example module called `example-custom-filter` which we are going to use for our filter. + +As you look inside its module folder `src/api/extensions/example-custom-filter/`, you will find a child folder `filter/catalog/` with all existing custom filters for this module. Inside this folder we are going to duplicate the existing `SampleFilter.ts` into another one called `SpringShorts.ts` – this is our new custom filter module-class. + +This file needs to be registered in the config JSON to let the API know that there is a new custom filter inside our extension. +Therefore you open your `default.json` or specific config JSON file and add our new filename `SpringShorts` to the config node `extensions.example-custom-filter.catalogFilter` array. + +Our `SpringShorts.ts` contains an object that contains [four properties](#filter-module-class-properties): `priority`, `check`, `filter`, `mutator`. We don't need a `mutator` nor `priority`, so we can remove these lines. `check` and `filter` needs to be changed to fulfill our needs. So, this is how our filter finally looks like: + +```js +import { FilterInterface } from 'storefront-query-builder' + +const filter: FilterInterface = { + check: ({ attribute }) => attribute === 'spring-shorts', + filter ({ value, attribute, operator, queryChain }) { + return queryChain + .filter('terms', 'pants', [ 'shorts' ]) + .filter('terms', 'cut', [ 1, 2 ]) + .filter('terms', 'color', [ 3, 4 ]) + .filter('terms', 'gender', [ value ]) + } +} + +export default filter +``` + +Inside `check` we tell the filter to just be applied if the attribute is named exactly `spring-shorts`. + +Inside `filter` we extend the Elasticsearch query-chain by our desired filters, using the `bodybuilder` library syntax. + +That's it, now we are able to filter by a complex query in only one line inside VSF. + +## 7. Anti-patterns & Common pitfalls ### 1. Preparation ### 2. Recipe @@ -699,7 +787,7 @@ _[INSERT VIDEO HERE]_

-## 7. Building a module from A to Z in an iteration +## 8. Building a module from A to Z in an iteration ### 1. Preparation @@ -709,7 +797,7 @@ _[INSERT VIDEO HERE]_

-## 8. Deprecated legacy of Modules +## 9. Deprecated legacy of Modules In this recipe, we will take a review of how to deal with modules in an old fashioned way , just in case you really need it. ### 1. Preparation @@ -720,7 +808,7 @@ In this recipe, we will take a review of how to deal with modules in an old fash
-## 9. Converting old modules to new modules +## 10. Converting old modules to new modules There are useful modules out there already developed in the old way. ### 1. Preparation diff --git a/package.json b/package.json index 146b526c27..64f7fc3578 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "@babel/core": "^7.8.6", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/polyfill": "^7.8.3", - "@babel/preset-env": "^7.8.6", + "@babel/preset-env": "^7.9.0", "@types/jest": "^25.1.3", "@types/node": "^13.7.7", "@typescript-eslint/eslint-plugin": "^1.7.1-alpha.17", diff --git a/packages/cli/vue-storefront b/packages/cli/vue-storefront deleted file mode 160000 index 3af5a1a152..0000000000 --- a/packages/cli/vue-storefront +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3af5a1a152489f7335aff49c9f1d29d92a45da2f diff --git a/src/modules/client.ts b/src/modules/client.ts index ad725d1ebb..11e1ab293b 100644 --- a/src/modules/client.ts +++ b/src/modules/client.ts @@ -24,6 +24,7 @@ import { IcmaaExtendedNewsletterModule } from 'icmaa-newsletter' import { IcmaaExtendedReviewRoutes } from 'icmaa-review' import { IcmaaCategoryModule } from 'icmaa-category' import { IcmaaCategoryExtrasModule } from 'icmaa-category-extras' +import { IcmaaCdnModule } from 'icmaa-cdn' import { IcmaaCmsModule } from 'icmaa-cms' import { IcmaaFormsModule } from 'icmaa-forms' import { IcmaaTeaserModule } from 'icmaa-teaser' @@ -70,6 +71,7 @@ export function registerClientModules () { registerModule(IcmaaExtendedReviewRoutes) registerModule(IcmaaCategoryModule) registerModule(IcmaaCategoryExtrasModule) + registerModule(IcmaaCdnModule) registerModule(IcmaaCmsModule) registerModule(IcmaaFormsModule) registerModule(IcmaaTeaserModule) diff --git a/src/modules/fastly/README.md b/src/modules/fastly/README.md new file mode 100644 index 0000000000..50293a731d --- /dev/null +++ b/src/modules/fastly/README.md @@ -0,0 +1,24 @@ +# VSF Cache Fastly +This module extends default caching docs/guide/basics/ssr-cache.md to allow using fastly as cache provider. + +## How to install +Add to config: +```json +"fastly": { + "enabled": true, + "serviceId": "xyz", // (https://docs.fastly.com/en/guides/finding-and-managing-your-account-info#finding-your-service-id) + "token": "xyz" // fastly api token (https://docs.fastly.com/api/auth#tokens) +} +``` + +Change those values in `server` section: +```json +"useOutputCacheTagging": true, +"useOutputCache": true +``` + +## How to purge cache? +Open: +``` +http://localhost:3000/invalidate?key=aeSu7aip&tag=home +``` diff --git a/src/modules/fastly/server.ts b/src/modules/fastly/server.ts new file mode 100644 index 0000000000..474c5525a6 --- /dev/null +++ b/src/modules/fastly/server.ts @@ -0,0 +1,39 @@ +import { serverHooks } from '@vue-storefront/core/server/hooks' +import fetch from 'isomorphic-fetch' +import config from 'config' + +const chunk = require('lodash/chunk') + +serverHooks.beforeOutputRenderedResponse(({ output, res, context }) => { + if (!config.get('fastly.enabled')) { + return output + } + + const tagsArray = Array.from(context.output.cacheTags) + const cacheTags = tagsArray.join(' ') + res.setHeader('Surrogate-Key', cacheTags) + + return output +}) + +serverHooks.beforeCacheInvalidated(async ({ tags }) => { + if (!config.get('fastly.enabled') || !config.get('server.useOutputCache') || !config.get('server.useOutputCacheTagging')) { + return + } + + console.log('Invalidating Fastly Surrogate-Key') + const tagsChunks = chunk(tags.filter((tag) => + config.server.availableCacheTags.indexOf(tag) >= 0 || + config.server.availableCacheTags.find(t => tag.indexOf(t) === 0) + ), 256) // we can send maximum 256 keys per request, more info https://docs.fastly.com/api/purge#purge_db35b293f8a724717fcf25628d713583 + + for (const tagsChunk of tagsChunks) { + const response = await fetch(`https://api.fastly.com/service/${config.get('fastly.serviceId')}/purge`, { + method: 'POST', + headers: { 'Fastly-Key': config.get('fastly.token') }, + body: JSON.stringify({ surrogate_keys: tagsChunk }) + }) + const text = await response.text() + console.log(text) + } +}) diff --git a/src/modules/icmaa-catalog/store/category/actions/index.ts b/src/modules/icmaa-catalog/store/category/actions/index.ts index fd68e25a40..821d0b5f80 100644 --- a/src/modules/icmaa-catalog/store/category/actions/index.ts +++ b/src/modules/icmaa-catalog/store/category/actions/index.ts @@ -36,7 +36,7 @@ const actions: ActionTree = { // Add our custom category filter // @see DivanteLtd/vue-storefront#4111 - filterQr.applyFilter({ key: 'stock', value: '', scope: 'default' }) + filterQr.applyFilter({ key: 'stock', value: '' }) if (!searchQuery.sort) { filterQr.applySort({ field: 'is_in_sale', options: { 'missing': '_first' } }) } @@ -78,7 +78,7 @@ const actions: ActionTree = { // Add our custom category filter // @see DivanteLtd/vue-storefront#4111 - filterQr.applyFilter({ key: 'stock', value: '', scope: 'default' }) + filterQr.applyFilter({ key: 'stock', value: '' }) if (!searchQuery.sort) { filterQr.applySort({ field: 'is_in_sale', options: { 'missing': '_first' } }) } diff --git a/src/modules/icmaa-category/store/actions.ts b/src/modules/icmaa-category/store/actions.ts index 3aad5e6eb4..4c8734f2b0 100644 --- a/src/modules/icmaa-category/store/actions.ts +++ b/src/modules/icmaa-category/store/actions.ts @@ -48,7 +48,7 @@ const actions: ActionTree = { let query = new SearchQuery() query - .applyFilter({ key: 'stock', value: '', scope: 'default' }) + .applyFilter({ key: 'stock', value: '' }) .applyFilter({ key: 'visibility', value: { in: [2, 3, 4] } }) .applyFilter({ key: 'status', value: { in: [0, 1] } }) .applyFilter({ key: 'category_ids', value: { in: [categoryId] } }) diff --git a/src/modules/icmaa-cdn/.gitignore b/src/modules/icmaa-cdn/.gitignore new file mode 100644 index 0000000000..04c01ba7ba --- /dev/null +++ b/src/modules/icmaa-cdn/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ \ No newline at end of file diff --git a/src/modules/icmaa-cdn/README.md b/src/modules/icmaa-cdn/README.md new file mode 100644 index 0000000000..dd1daebc95 --- /dev/null +++ b/src/modules/icmaa-cdn/README.md @@ -0,0 +1,37 @@ +# `icmaa-cdn` module + +Load custom image provider to support external CDN services. + +## Configs + +* Run `yarn` to install modules dependencies. + They are defined in templates `package.json`. + +* `config.images.useExactUrlsNoProxy` must be `true` / enabled + +* Add the following configs to your `config/local.json`: + ``` + "images": { + "useExactUrlsNoProxy": true + }, + "icmaa_cdn": { + "provider": "scalecommerce", + "scalecommerce": { + "baseUrl": "https://www.base-url.com/", + "quality": 85 + } + } + ``` + +## Add a new provider + +If you wan't to add another CDN provider, you can add a new modules classes inside the `provider/NewProvider.ts`. + +You also need to add the dynamic import variable to the `providers` array in the `index.ts` like: +`newprovider: () => import(/* webpackChunkName: "vsf-icmaa-cdn-newprovider" */ './provider/NewProvider')` + +Then add your desired configs under the `icmaa_cdn` configs path. + +## Todo + +[ ] ... diff --git a/src/modules/icmaa-cdn/index.ts b/src/modules/icmaa-cdn/index.ts new file mode 100644 index 0000000000..2d49328719 --- /dev/null +++ b/src/modules/icmaa-cdn/index.ts @@ -0,0 +1,21 @@ +import { StorefrontModule } from '@vue-storefront/core/lib/modules' +import { coreHooks } from '@vue-storefront/core/hooks' +import { Logger } from '@vue-storefront/core/lib/logger' + +import { ImakeHook } from './types/HookTypes' + +const providers: { [provider: string]: () => Promise } = { + scalecommerce: () => import(/* webpackChunkName: "vsf-icmaa-cdn-scalecommerce" */ './provider/ScaleCommerce') +} + +export const IcmaaCdnModule: StorefrontModule = function ({ store, appConfig, router }) { + const cdn = appConfig.icmaa_cdn && appConfig.icmaa_cdn.provider && appConfig.icmaa_cdn.provider !== '' ? appConfig.icmaa_cdn.provider : false + if (cdn && appConfig.images.useExactUrlsNoProxy && providers.hasOwnProperty(cdn)) { + const provider = providers[cdn]() + provider.then(c => { + coreHooks.afterProductThumbnailPathGenerate(c.default) + }).catch(e => { + Logger.error('Could not load provider:', 'icmaa-cdn', cdn)() + }) + } +} diff --git a/src/modules/icmaa-cdn/package.json b/src/modules/icmaa-cdn/package.json new file mode 100644 index 0000000000..01d4d41429 --- /dev/null +++ b/src/modules/icmaa-cdn/package.json @@ -0,0 +1,14 @@ +{ + "name": "icmaa-cdn", + "version": "1.0.0", + "author": "cewald and contributors", + "main": "index.js", + "license": "MIT", + "private": true, + "devDependencies": { + "@vue-storefront/core": "^1.11.1" + }, + "peerDependencies": { + "@vue-storefront/core": "^1.11.1" + } +} diff --git a/src/modules/icmaa-cdn/provider/ScaleCommerce.ts b/src/modules/icmaa-cdn/provider/ScaleCommerce.ts new file mode 100644 index 0000000000..9cda414905 --- /dev/null +++ b/src/modules/icmaa-cdn/provider/ScaleCommerce.ts @@ -0,0 +1,19 @@ +import config from 'config' +import { ImageHookProperties, ImageHookReturn } from '../types/HookTypes' + +const afterProductThumbnailPathGenerate = ({ path, sizeX, sizeY }: ImageHookProperties): ImageHookReturn => { + let { baseUrl, quality } = config.icmaa_cdn['scalecommerce'] + + baseUrl = baseUrl.replace(/\/*$/, '') + path = path.replace(/^\/*/, '') + + if (sizeX && sizeY && sizeX > 0 && sizeY > 0) { + path = `${baseUrl}/${sizeX}x${sizeY}x${quality}/media/${path}` + } else { + path = `${baseUrl}/media/${path}` + } + + return { path } +} + +export default afterProductThumbnailPathGenerate diff --git a/src/modules/icmaa-cdn/types/HookTypes.ts b/src/modules/icmaa-cdn/types/HookTypes.ts new file mode 100644 index 0000000000..09dd7b3edf --- /dev/null +++ b/src/modules/icmaa-cdn/types/HookTypes.ts @@ -0,0 +1,14 @@ +export interface ImageHookProperties { + path: string, + pathType: string, + sizeX: number, + sizeY: number +} + +export interface ImageHookReturn { + path: string +} + +export interface ImakeHook { + default(params: ImageHookProperties): ImageHookReturn +} diff --git a/src/modules/icmaa-cms/components/Block.vue b/src/modules/icmaa-cms/components/Block.vue index 078fba0704..85f3428c32 100644 --- a/src/modules/icmaa-cms/components/Block.vue +++ b/src/modules/icmaa-cms/components/Block.vue @@ -1,5 +1,5 @@ diff --git a/src/themes/icmaa-imp/components/core/blocks/SearchPanel/SearchPanel.vue b/src/themes/icmaa-imp/components/core/blocks/SearchPanel/SearchPanel.vue index 7e72b2a825..8f4b67652e 100644 --- a/src/themes/icmaa-imp/components/core/blocks/SearchPanel/SearchPanel.vue +++ b/src/themes/icmaa-imp/components/core/blocks/SearchPanel/SearchPanel.vue @@ -12,7 +12,7 @@ {{ $t(getNoResultsMessage) }} - +
@@ -39,7 +39,7 @@ import i18n from '@vue-storefront/i18n' import { mapGetters } from 'vuex' import { required, minLength } from 'vuelidate/lib/validators' import { disableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock' -import { prepareQuickSearchQuery } from '@vue-storefront/core/modules/catalog/queries/searchPanel' +import { SearchQuery } from 'storefront-query-builder' import { Logger } from '@vue-storefront/core/lib/logger' import debounce from 'lodash-es/debounce' import uniq from 'lodash-es/uniq' @@ -66,6 +66,7 @@ export default { searchString: '', searchAlias: '', products: [], + categoryAggs: [], size: 12, start: 0, placeholder: i18n.t('Type what you are looking for...'), @@ -90,18 +91,9 @@ export default { } return productList }, - categoryFilters () { - const categoriesMap = {} - this.products.forEach(product => { - [...product.category].forEach(category => { - categoriesMap[category.category_id] = category - }) - }) - return Object.keys(categoriesMap).map(categoryId => categoriesMap[categoryId]) - }, categories () { const splitChars = [' ', '-', ','] - return this.categoryFilters.filter(category => { + return this.categoryAggs.filter(category => { let searchStrings = [] const strings = [this.searchString, this.searchAlias] strings.forEach(s => splitChars.forEach(c => searchStrings.push(...s.split(c).filter(s => s.length >= 3)))) @@ -126,7 +118,7 @@ export default { } }, watch: { - categories () { + categoryAggs () { this.selectedCategoryIds = [] } }, @@ -158,18 +150,20 @@ export default { }, search: debounce(async function () { if (!this.$v.searchString.$invalid) { - let query = prepareQuickSearchQuery( - this.searchAlias = await this.getAlias(this.searchString) - ) + this.searchAlias = await this.getAlias(this.searchString) + let query = this.prepareQuickSearchQuery(this.searchAlias) this.start = 0 this.moreProducts = true this.loadingProducts = true this.$store.dispatch('product/list', { query, start: this.start, configuration: {}, size: this.size, updateState: false }).then(resp => { - this.products = resp.items + const { items, aggregations } = resp + this.products = items this.start += this.size - this.emptyResults = resp.items.length < 1 + this.emptyResults = items.length < 1 this.loadingProducts = false + + this.populateCategoryAggregations(aggregations) }).catch((err) => { Logger.error(err, 'components-search')() }) @@ -180,15 +174,16 @@ export default { }, 350), async loadMoreProducts () { if (!this.$v.searchString.$invalid) { - let query = prepareQuickSearchQuery(await this.getAlias(this.searchString)) + let query = this.prepareQuickSearchQuery(await this.getAlias(this.searchString), true) this.loadingProducts = true this.$store.dispatch('product/list', { query, start: this.start, size: this.size, updateState: false }).then((resp) => { - let page = Math.floor(resp.total / this.size) - let exceeed = resp.total - this.size * page - if (resp.start === resp.total - exceeed) { + const { items, aggregations, total, start } = resp + let page = Math.floor(total / this.size) + let exceeed = total - this.size * page + if (start === total - exceeed) { this.moreProducts = false } - this.products = this.products.concat(resp.items) + this.products = this.products.concat(items) this.start += this.size this.emptyResults = this.products.length < 1 this.loadingProducts = false @@ -201,6 +196,30 @@ export default { this.emptyResults = true } }, + prepareQuickSearchQuery (value, plain = false) { + let searchQuery = new SearchQuery() + + const searchFilterKey = plain ? 'search-text-plain' : 'search-text' + searchQuery = searchQuery + .applyFilter({ key: searchFilterKey, value }) + .applyFilter({ key: 'stock', value: '' }) + .applyFilter({ key: 'visibility', value: {'in': [3, 4]} }) + .applyFilter({ key: 'status', value: {'in': [0, 1]} }) + + return searchQuery + }, + populateCategoryAggregations (aggr) { + // This is a massive nested aggregation object which we crawl and collect all + // available categories of all results not just those who are on results page + this.categoryAggs = [] + if (aggr.categories_found && aggr.categories_found.doc_count > 0) { + const { categories_found } = aggr + const categories = categories_found.categories.buckets + categories.forEach(bucket => { + this.categoryAggs.push(bucket.hits.hits.hits[0]._source.category) + }) + } + }, closeSidebar () { this.$store.dispatch('ui/setSearchpanel', false) } diff --git a/src/themes/icmaa-imp/mixins/product/optionsMixin.ts b/src/themes/icmaa-imp/mixins/product/optionsMixin.ts index 3b4b9c7723..4af25a5a10 100644 --- a/src/themes/icmaa-imp/mixins/product/optionsMixin.ts +++ b/src/themes/icmaa-imp/mixins/product/optionsMixin.ts @@ -27,10 +27,11 @@ export default { }, sortedProductOptions () { return cloneDeep(this.product.configurable_options).map(o => { + const attribute = Object.assign({ options: [] }, this.getAttributeListByCode[o.attribute_code]) + const { options } = attribute + // Sort by attributes value `sort_order` parameter o.values = o.values.sort((a, b) => { - const attribute = this.getAttributeListByCode[o.attribute_code] - const { options } = attribute const aValue = a.value_index const bValue = b.value_index diff --git a/src/themes/icmaa-imp/pages/Home.vue b/src/themes/icmaa-imp/pages/Home.vue index ae71a54710..2f91f13536 100755 --- a/src/themes/icmaa-imp/pages/Home.vue +++ b/src/themes/icmaa-imp/pages/Home.vue @@ -15,6 +15,7 @@ + @@ -26,13 +27,15 @@ import LazyHydrate from 'vue-lazy-hydration' import Teaser from 'theme/components/core/blocks/Teaser/Teaser' import LogoLine from 'theme/components/core/blocks/CategoryExtras/LogoLineBlock' import ProductListingWidget from 'icmaa-category/components/core/ProductListingWidget' +import CmsBlock from 'icmaa-cms/components/Block' export default { components: { LazyHydrate, Teaser, LogoLine, - ProductListingWidget + ProductListingWidget, + CmsBlock }, mounted () { if (!this.isLoggedIn && localStorage.getItem('redirect')) { diff --git a/yarn.lock b/yarn.lock index fff9316ea2..ffc5deea83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -192,6 +192,15 @@ "@babel/template" "^7.8.3" "@babel/types" "^7.8.3" +"@babel/helper-function-name@^7.9.5": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz#2b53820d35275120e1874a82e5aabe1376920a5c" + integrity sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw== + dependencies: + "@babel/helper-get-function-arity" "^7.8.3" + "@babel/template" "^7.8.3" + "@babel/types" "^7.9.5" + "@babel/helper-get-function-arity@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5" @@ -306,6 +315,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz#ad53562a7fc29b3b9a91bbf7d10397fd146346ed" integrity sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw== +"@babel/helper-validator-identifier@^7.9.5": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80" + integrity sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g== + "@babel/helper-wrap-function@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz#9dbdb2bb55ef14aaa01fe8c99b629bd5352d8610" @@ -427,6 +441,15 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-object-rest-spread" "^7.8.0" +"@babel/plugin-proposal-object-rest-spread@^7.9.5": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.5.tgz#3fd65911306d8746014ec0d0cf78f0e39a149116" + integrity sha512-VP2oXvAf7KCYTthbUHwBlewbl1Iq059f6seJGsxMizaCdgHIeczOr7FBqELhSqfkIl04Fi8okzWzl63UKbQmmg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.0" + "@babel/plugin-transform-parameters" "^7.9.5" + "@babel/plugin-proposal-optional-catch-binding@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.8.3.tgz#9dee96ab1650eed88646ae9734ca167ac4a9c5c9" @@ -610,6 +633,20 @@ "@babel/helper-split-export-declaration" "^7.8.3" globals "^11.1.0" +"@babel/plugin-transform-classes@^7.9.5": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.9.5.tgz#800597ddb8aefc2c293ed27459c1fcc935a26c2c" + integrity sha512-x2kZoIuLC//O5iA7PEvecB105o7TLzZo8ofBVhP79N+DO3jaX+KYfww9TQcfBEZD0nikNyYcGB1IKtRq36rdmg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.8.3" + "@babel/helper-define-map" "^7.8.3" + "@babel/helper-function-name" "^7.9.5" + "@babel/helper-optimise-call-expression" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-replace-supers" "^7.8.6" + "@babel/helper-split-export-declaration" "^7.8.3" + globals "^11.1.0" + "@babel/plugin-transform-computed-properties@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.8.3.tgz#96d0d28b7f7ce4eb5b120bb2e0e943343c86f81b" @@ -624,6 +661,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" +"@babel/plugin-transform-destructuring@^7.9.5": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.9.5.tgz#72c97cf5f38604aea3abf3b935b0e17b1db76a50" + integrity sha512-j3OEsGel8nHL/iusv/mRd5fYZ3DrOxWC82x0ogmdN/vHfAP4MYw+AFKYanzWlktNwikKvlzUV//afBW5FTp17Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-transform-dotall-regex@^7.4.4", "@babel/plugin-transform-dotall-regex@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz#c3c6ec5ee6125c6993c5cbca20dc8621a9ea7a6e" @@ -796,6 +840,14 @@ "@babel/helper-get-function-arity" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" +"@babel/plugin-transform-parameters@^7.9.5": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.9.5.tgz#173b265746f5e15b2afe527eeda65b73623a0795" + integrity sha512-0+1FhHnMfj6lIIhVvS4KGQJeuhe1GI//h5uptK4PvLt+BGBxsoUJbd3/IW002yk//6sZPlFgsG1hY6OHLcy6kA== + dependencies: + "@babel/helper-get-function-arity" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-transform-property-literals@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.8.3.tgz#33194300d8539c1ed28c62ad5087ba3807b98263" @@ -953,7 +1005,7 @@ levenary "^1.1.1" semver "^5.5.0" -"@babel/preset-env@^7.8.4", "@babel/preset-env@^7.8.6": +"@babel/preset-env@^7.8.4": version "7.8.6" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.8.6.tgz#2a0773b08589ecba4995fc71b1965e4f531af40b" integrity sha512-M5u8llV9DIVXBFB/ArIpqJuvXpO+ymxcJ6e8ZAmzeK3sQeBNOD1y+rHvHCGG4TlEmsNpIrdecsHGHT8ZCoOSJg== @@ -1016,6 +1068,72 @@ levenary "^1.1.1" semver "^5.5.0" +"@babel/preset-env@^7.9.0": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.9.5.tgz#8ddc76039bc45b774b19e2fc548f6807d8a8919f" + integrity sha512-eWGYeADTlPJH+wq1F0wNfPbVS1w1wtmMJiYk55Td5Yu28AsdR9AsC97sZ0Qq8fHqQuslVSIYSGJMcblr345GfQ== + dependencies: + "@babel/compat-data" "^7.9.0" + "@babel/helper-compilation-targets" "^7.8.7" + "@babel/helper-module-imports" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-proposal-async-generator-functions" "^7.8.3" + "@babel/plugin-proposal-dynamic-import" "^7.8.3" + "@babel/plugin-proposal-json-strings" "^7.8.3" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-proposal-numeric-separator" "^7.8.3" + "@babel/plugin-proposal-object-rest-spread" "^7.9.5" + "@babel/plugin-proposal-optional-catch-binding" "^7.8.3" + "@babel/plugin-proposal-optional-chaining" "^7.9.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.8.3" + "@babel/plugin-syntax-async-generators" "^7.8.0" + "@babel/plugin-syntax-dynamic-import" "^7.8.0" + "@babel/plugin-syntax-json-strings" "^7.8.0" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" + "@babel/plugin-syntax-numeric-separator" "^7.8.0" + "@babel/plugin-syntax-object-rest-spread" "^7.8.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" + "@babel/plugin-syntax-optional-chaining" "^7.8.0" + "@babel/plugin-syntax-top-level-await" "^7.8.3" + "@babel/plugin-transform-arrow-functions" "^7.8.3" + "@babel/plugin-transform-async-to-generator" "^7.8.3" + "@babel/plugin-transform-block-scoped-functions" "^7.8.3" + "@babel/plugin-transform-block-scoping" "^7.8.3" + "@babel/plugin-transform-classes" "^7.9.5" + "@babel/plugin-transform-computed-properties" "^7.8.3" + "@babel/plugin-transform-destructuring" "^7.9.5" + "@babel/plugin-transform-dotall-regex" "^7.8.3" + "@babel/plugin-transform-duplicate-keys" "^7.8.3" + "@babel/plugin-transform-exponentiation-operator" "^7.8.3" + "@babel/plugin-transform-for-of" "^7.9.0" + "@babel/plugin-transform-function-name" "^7.8.3" + "@babel/plugin-transform-literals" "^7.8.3" + "@babel/plugin-transform-member-expression-literals" "^7.8.3" + "@babel/plugin-transform-modules-amd" "^7.9.0" + "@babel/plugin-transform-modules-commonjs" "^7.9.0" + "@babel/plugin-transform-modules-systemjs" "^7.9.0" + "@babel/plugin-transform-modules-umd" "^7.9.0" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.8.3" + "@babel/plugin-transform-new-target" "^7.8.3" + "@babel/plugin-transform-object-super" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.9.5" + "@babel/plugin-transform-property-literals" "^7.8.3" + "@babel/plugin-transform-regenerator" "^7.8.7" + "@babel/plugin-transform-reserved-words" "^7.8.3" + "@babel/plugin-transform-shorthand-properties" "^7.8.3" + "@babel/plugin-transform-spread" "^7.8.3" + "@babel/plugin-transform-sticky-regex" "^7.8.3" + "@babel/plugin-transform-template-literals" "^7.8.3" + "@babel/plugin-transform-typeof-symbol" "^7.8.4" + "@babel/plugin-transform-unicode-regex" "^7.8.3" + "@babel/preset-modules" "^0.1.3" + "@babel/types" "^7.9.5" + browserslist "^4.9.1" + core-js-compat "^3.6.2" + invariant "^2.2.2" + levenary "^1.1.1" + semver "^5.5.0" + "@babel/preset-modules@^0.1.3": version "0.1.3" resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.3.tgz#13242b53b5ef8c883c3cf7dddd55b36ce80fbc72" @@ -1098,6 +1216,15 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@babel/types@^7.9.5": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.5.tgz#89231f82915a8a566a703b3b20133f73da6b9444" + integrity sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg== + dependencies: + "@babel/helper-validator-identifier" "^7.9.5" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"