Browse Source

Merge branch 'master' into next

Michael Bromley 5 years ago
parent
commit
4b905a6cf6
36 changed files with 1102 additions and 131 deletions
  1. 23 0
      CHANGELOG.md
  2. 1 1
      lerna.json
  3. 3 3
      packages/admin-ui-plugin/package.json
  4. 1 0
      packages/admin-ui-plugin/src/constants.ts
  5. 2 2
      packages/admin-ui/package.json
  6. 10 7
      packages/admin-ui/src/lib/catalog/src/components/collection-detail/collection-detail.component.ts
  7. 52 3
      packages/admin-ui/src/lib/catalog/src/components/collection-tree/array-to-tree.spec.ts
  8. 20 2
      packages/admin-ui/src/lib/catalog/src/components/collection-tree/array-to-tree.ts
  9. 22 5
      packages/admin-ui/src/lib/catalog/src/components/collection-tree/collection-tree-node.component.ts
  10. 1 2
      packages/admin-ui/src/lib/catalog/src/components/collection-tree/collection-tree.component.ts
  11. 7 5
      packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts
  12. 50 43
      packages/admin-ui/src/lib/catalog/src/components/product-variants-editor/product-variants-editor.component.ts
  13. 1 1
      packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.ts
  14. 1 1
      packages/admin-ui/src/lib/core/src/common/version.ts
  15. 3 0
      packages/admin-ui/src/lib/core/src/components/user-menu/user-menu.component.scss
  16. 5 0
      packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts
  17. 8 4
      packages/admin-ui/src/lib/core/src/shared/components/order-state-label/order-state-label.component.ts
  18. 2 8
      packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.html
  19. 1 0
      packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.ts
  20. 2 2
      packages/admin-ui/src/lib/settings/src/components/payment-method-detail/payment-method-detail.component.html
  21. 705 0
      packages/admin-ui/src/lib/static/i18n-messages/cs.json
  22. 1 1
      packages/admin-ui/src/lib/static/vendure-ui-config.json
  23. 3 3
      packages/asset-server-plugin/package.json
  24. 1 1
      packages/common/package.json
  25. 105 1
      packages/core/e2e/shop-order.e2e-spec.ts
  26. 2 2
      packages/core/package.json
  27. 13 1
      packages/core/src/service/services/order.service.ts
  28. 3 3
      packages/create/package.json
  29. 9 9
      packages/dev-server/package.json
  30. 3 3
      packages/elasticsearch-plugin/package.json
  31. 3 3
      packages/email-plugin/package.json
  32. 8 0
      packages/email-plugin/src/event-handler.ts
  33. 18 0
      packages/email-plugin/src/plugin.spec.ts
  34. 6 8
      packages/email-plugin/src/plugin.ts
  35. 3 3
      packages/testing/package.json
  36. 4 4
      packages/ui-devkit/package.json

+ 23 - 0
CHANGELOG.md

@@ -1,3 +1,26 @@
+## <small>0.16.2 (2020-10-22)</small>
+
+
+#### Fixes
+
+* **admin-ui** Auto-fill Product & Collection slugs in other languages ([9393d04](https://github.com/vendure-ecommerce/vendure/commit/9393d04)), closes [#522](https://github.com/vendure-ecommerce/vendure/issues/522)
+* **admin-ui** Correct display of args input in PaymentMethodDetail ([3f7627e](https://github.com/vendure-ecommerce/vendure/commit/3f7627e)), closes [#489](https://github.com/vendure-ecommerce/vendure/issues/489)
+* **admin-ui** Fix collection list "expand all" behaviour when toggling ([c77af2b](https://github.com/vendure-ecommerce/vendure/commit/c77af2b)), closes [#513](https://github.com/vendure-ecommerce/vendure/issues/513)
+* **admin-ui** Fix display of existing variants in ProductVariantEditor ([ca538b8](https://github.com/vendure-ecommerce/vendure/commit/ca538b8)), closes [#521](https://github.com/vendure-ecommerce/vendure/issues/521)
+* **admin-ui** Preserve expanded state on moving collections ([8d028cf](https://github.com/vendure-ecommerce/vendure/commit/8d028cf)), closes [#515](https://github.com/vendure-ecommerce/vendure/issues/515)
+* **core** Add missing events to export (fulfillment, logout) ([04a49bf](https://github.com/vendure-ecommerce/vendure/commit/04a49bf))
+* **core** Correctly de-duplicate OrderLines with empty custom fields ([ef99c22](https://github.com/vendure-ecommerce/vendure/commit/ef99c22)), closes [#512](https://github.com/vendure-ecommerce/vendure/issues/512)
+* **email-plugin** Only call `loadData()` function after filters run ([e22db7e](https://github.com/vendure-ecommerce/vendure/commit/e22db7e)), closes [#518](https://github.com/vendure-ecommerce/vendure/issues/518)
+
+#### Features
+
+* **admin-ui** Add Czech translations ([89ee826](https://github.com/vendure-ecommerce/vendure/commit/89ee826))
+* **admin-ui** Enable filtering by custom Order states in list view ([76d2d56](https://github.com/vendure-ecommerce/vendure/commit/76d2d56))
+* **core** Add custom error result on AuthenticationStrategy ([d3ddb96](https://github.com/vendure-ecommerce/vendure/commit/d3ddb96)), closes [#499](https://github.com/vendure-ecommerce/vendure/issues/499)
+* **core** Add NotVerifiedError to AuthenticationResult ([ee39263](https://github.com/vendure-ecommerce/vendure/commit/ee39263)), closes [#500](https://github.com/vendure-ecommerce/vendure/issues/500)
+* **core** Add support for better-sqlite3 driver to DefaultSearchPlugin ([7a71fbe](https://github.com/vendure-ecommerce/vendure/commit/7a71fbe)), closes [#505](https://github.com/vendure-ecommerce/vendure/issues/505)
+* **create** Use better-sqlite3 driver for improved sqlite perf ([dfd4f36](https://github.com/vendure-ecommerce/vendure/commit/dfd4f36)), closes [#505](https://github.com/vendure-ecommerce/vendure/issues/505)
+
 ## <small>0.16.1 (2020-10-15)</small>
 
 

+ 1 - 1
lerna.json

@@ -2,7 +2,7 @@
   "packages": [
     "packages/*"
   ],
-  "version": "0.16.1",
+  "version": "0.16.2",
   "npmClient": "yarn",
   "useWorkspaces": true,
   "command": {

+ 3 - 3
packages/admin-ui-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/admin-ui-plugin",
-  "version": "0.16.1",
+  "version": "0.16.2",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
   "files": [
@@ -19,8 +19,8 @@
   "devDependencies": {
     "@types/express": "^4.17.8",
     "@types/fs-extra": "^9.0.1",
-    "@vendure/common": "^0.16.1",
-    "@vendure/core": "^0.16.1",
+    "@vendure/common": "^0.16.2",
+    "@vendure/core": "^0.16.2",
     "express": "^4.17.1",
     "rimraf": "^3.0.2",
     "typescript": "4.0.3"

+ 1 - 0
packages/admin-ui-plugin/src/constants.ts

@@ -13,4 +13,5 @@ export const defaultAvailableLanguages = [
     LanguageCode.zh_Hans,
     LanguageCode.zh_Hant,
     LanguageCode.pt_BR,
+    LanguageCode.cs,
 ];

+ 2 - 2
packages/admin-ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/admin-ui",
-  "version": "0.16.1",
+  "version": "0.16.2",
   "license": "MIT",
   "scripts": {
     "ng": "ng",
@@ -36,7 +36,7 @@
     "@ng-select/ng-select": "^5.0.3",
     "@ngx-translate/core": "^13.0.0",
     "@ngx-translate/http-loader": "^6.0.0",
-    "@vendure/common": "^0.16.1",
+    "@vendure/common": "^0.16.2",
     "@webcomponents/custom-elements": "^1.2.4",
     "apollo-angular": "^2.0.4",
     "apollo-upload-client": "^12.1.0",

+ 10 - 7
packages/admin-ui/src/lib/catalog/src/components/collection-detail/collection-detail.component.ts

@@ -40,7 +40,8 @@ import { CollectionContentsComponent } from '../collection-contents/collection-c
     styleUrls: ['./collection-detail.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class CollectionDetailComponent extends BaseDetailComponent<Collection.Fragment>
+export class CollectionDetailComponent
+    extends BaseDetailComponent<Collection.Fragment>
     implements OnInit, OnDestroy {
     customFields: CustomFieldConfig[];
     detailForm: FormGroup;
@@ -97,17 +98,19 @@ export class CollectionDetailComponent extends BaseDetailComponent<Collection.Fr
     }
 
     /**
-     * If creating a new product, automatically generate the slug based on the collection name.
+     * If creating a new Collection, automatically generate the slug based on the collection name.
      */
     updateSlug(nameValue: string) {
-        this.isNew$.pipe(take(1)).subscribe(isNew => {
-            if (isNew) {
+        combineLatest(this.entity$, this.languageCode$)
+            .pipe(take(1))
+            .subscribe(([entity, languageCode]) => {
                 const slugControl = this.detailForm.get(['slug']);
-                if (slugControl && slugControl.pristine) {
+                const currentTranslation = entity.translations.find(t => t.languageCode === languageCode);
+                const currentSlugIsEmpty = !currentTranslation || !currentTranslation.slug;
+                if (slugControl && slugControl.pristine && currentSlugIsEmpty) {
                     slugControl.setValue(normalizeString(`${nameValue}`, '-'));
                 }
-            }
-        });
+            });
     }
 
     addFilter(collectionFilter: ConfigurableOperation) {

+ 52 - 3
packages/admin-ui/src/lib/catalog/src/components/collection-tree/array-to-tree.spec.ts

@@ -1,11 +1,17 @@
-import { arrayToTree, HasParent } from './array-to-tree';
+import { arrayToTree, HasParent, RootNode, TreeNode } from './array-to-tree';
 
 describe('arrayToTree()', () => {
     it('preserves ordering', () => {
-        const result1 = arrayToTree([{ id: '13', parent: { id: '1' } }, { id: '12', parent: { id: '1' } }]);
+        const result1 = arrayToTree([
+            { id: '13', parent: { id: '1' } },
+            { id: '12', parent: { id: '1' } },
+        ]);
         expect(result1.children.map(i => i.id)).toEqual(['13', '12']);
 
-        const result2 = arrayToTree([{ id: '12', parent: { id: '1' } }, { id: '13', parent: { id: '1' } }]);
+        const result2 = arrayToTree([
+            { id: '12', parent: { id: '1' } },
+            { id: '13', parent: { id: '1' } },
+        ]);
         expect(result2.children.map(i => i.id)).toEqual(['12', '13']);
     });
 
@@ -48,4 +54,47 @@ describe('arrayToTree()', () => {
             ],
         });
     });
+
+    it('preserves expanded state from existing RootNode', () => {
+        const existing: RootNode<TreeNode<any>> = {
+            id: '1',
+            children: [
+                {
+                    id: '12',
+                    parent: { id: '1' },
+                    expanded: false,
+                    children: [
+                        {
+                            id: '121',
+                            parent: { id: '12' },
+                            expanded: false,
+                            children: [{ id: '1211', expanded: false, parent: { id: '121' }, children: [] }],
+                        },
+                    ],
+                },
+                {
+                    id: '13',
+                    parent: { id: '1' },
+                    expanded: true,
+                    children: [
+                        { id: '132', expanded: true, parent: { id: '13' }, children: [] },
+                        { id: '131', expanded: false, parent: { id: '13' }, children: [] },
+                    ],
+                },
+            ],
+        };
+
+        const input: HasParent[] = [
+            { id: '12', parent: { id: '1' } },
+            { id: '13', parent: { id: '1' } },
+            { id: '132', parent: { id: '13' } },
+            { id: '131', parent: { id: '13' } },
+            { id: '1211', parent: { id: '121' } },
+            { id: '121', parent: { id: '12' } },
+        ];
+
+        const result = arrayToTree(input, existing);
+
+        expect(result).toEqual(existing);
+    });
 });

+ 20 - 2
packages/admin-ui/src/lib/catalog/src/components/collection-tree/array-to-tree.ts

@@ -6,9 +6,10 @@ export type RootNode<T extends HasParent> = { id?: string; children: Array<TreeN
  * Builds a tree from an array of nodes which have a parent.
  * Based on https://stackoverflow.com/a/31247960/772859, modified to preserve ordering.
  */
-export function arrayToTree<T extends HasParent>(nodes: T[]): RootNode<T> {
+export function arrayToTree<T extends HasParent>(nodes: T[], currentState?: RootNode<T>): RootNode<T> {
     const topLevelNodes: Array<TreeNode<T>> = [];
     const mappedArr: { [id: string]: TreeNode<T> } = {};
+    const currentStateMap = treeToMap(currentState);
 
     // First map the nodes of the array to an object -> create a hash table.
     for (const node of nodes) {
@@ -18,7 +19,7 @@ export function arrayToTree<T extends HasParent>(nodes: T[]): RootNode<T> {
     for (const id of nodes.map(n => n.id)) {
         if (mappedArr.hasOwnProperty(id)) {
             const mappedElem = mappedArr[id];
-            mappedElem.expanded = false;
+            mappedElem.expanded = currentStateMap.get(id)?.expanded ?? false;
             const parent = mappedElem.parent;
             if (!parent) {
                 continue;
@@ -40,3 +41,20 @@ export function arrayToTree<T extends HasParent>(nodes: T[]): RootNode<T> {
     const rootId = topLevelNodes.length ? topLevelNodes[0].parent!.id : undefined;
     return { id: rootId, children: topLevelNodes };
 }
+
+/**
+ * Converts an existing tree (as generated by the arrayToTree function) into a flat
+ * Map. This is used to persist certain states (e.g. `expanded`) when re-building the
+ * tree.
+ */
+function treeToMap<T extends HasParent>(tree?: RootNode<T>): Map<string, TreeNode<T>> {
+    const nodeMap = new Map<string, TreeNode<T>>();
+    function visit(node: TreeNode<T>) {
+        nodeMap.set(node.id, node);
+        node.children.forEach(visit);
+    }
+    if (tree) {
+        visit(tree as TreeNode<T>);
+    }
+    return nodeMap;
+}

+ 22 - 5
packages/admin-ui/src/lib/catalog/src/components/collection-tree/collection-tree-node.component.ts

@@ -1,10 +1,18 @@
 import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
-import { ChangeDetectionStrategy, Component, Input, OnInit, Optional, SkipSelf } from '@angular/core';
-import { Observable } from 'rxjs';
-import { map, shareReplay } from 'rxjs/operators';
-
+import {
+    ChangeDetectionStrategy,
+    Component,
+    Input,
+    OnChanges,
+    OnInit,
+    Optional,
+    SimpleChanges,
+    SkipSelf,
+} from '@angular/core';
 import { Permission } from '@vendure/admin-ui/core';
 import { DataService } from '@vendure/admin-ui/core';
+import { Observable } from 'rxjs';
+import { map, shareReplay } from 'rxjs/operators';
 
 import { RootNode, TreeNode } from './array-to-tree';
 import { CollectionPartial, CollectionTreeComponent } from './collection-tree.component';
@@ -15,7 +23,7 @@ import { CollectionPartial, CollectionTreeComponent } from './collection-tree.co
     styleUrls: ['./collection-tree-node.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class CollectionTreeNodeComponent implements OnInit {
+export class CollectionTreeNodeComponent implements OnInit, OnChanges {
     depth = 0;
     parentName: string;
     @Input() collectionTree: TreeNode<CollectionPartial>;
@@ -44,6 +52,15 @@ export class CollectionTreeNodeComponent implements OnInit {
         this.hasDeletePermission$ = permissions$.pipe(map(perms => perms.includes(Permission.DeleteCatalog)));
     }
 
+    ngOnChanges(changes: SimpleChanges) {
+        const expandAllChange = changes['expandAll'];
+        if (expandAllChange) {
+            if (expandAllChange.previousValue === true && expandAllChange.currentValue === false) {
+                this.collectionTree.children.forEach(c => (c.expanded = false));
+            }
+        }
+    }
+
     trackByFn(index: number, item: CollectionPartial) {
         return item.id;
     }

+ 1 - 2
packages/admin-ui/src/lib/catalog/src/components/collection-tree/collection-tree.component.ts

@@ -8,7 +8,6 @@ import {
     Output,
     SimpleChanges,
 } from '@angular/core';
-
 import { Collection } from '@vendure/admin-ui/core';
 
 import { arrayToTree, HasParent, RootNode } from './array-to-tree';
@@ -32,7 +31,7 @@ export class CollectionTreeComponent implements OnChanges {
 
     ngOnChanges(changes: SimpleChanges) {
         if ('collections' in changes && this.collections) {
-            this.collectionTree = arrayToTree(this.collections);
+            this.collectionTree = arrayToTree(this.collections, this.collectionTree);
         }
     }
 

+ 7 - 5
packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts

@@ -263,14 +263,16 @@ export class ProductDetailComponent
      * If creating a new product, automatically generate the slug based on the product name.
      */
     updateSlug(nameValue: string) {
-        this.isNew$.pipe(take(1)).subscribe(isNew => {
-            if (isNew) {
+        combineLatest(this.entity$, this.languageCode$)
+            .pipe(take(1))
+            .subscribe(([entity, languageCode]) => {
                 const slugControl = this.detailForm.get(['product', 'slug']);
-                if (slugControl && slugControl.pristine) {
+                const currentTranslation = entity.translations.find(t => t.languageCode === languageCode);
+                const currentSlugIsEmpty = !currentTranslation || !currentTranslation.slug;
+                if (slugControl && slugControl.pristine && currentSlugIsEmpty) {
                     slugControl.setValue(normalizeString(`${nameValue}`, '-'));
                 }
-            }
-        });
+            });
     }
 
     selectProductFacetValue() {

+ 50 - 43
packages/admin-ui/src/lib/catalog/src/components/product-variants-editor/product-variants-editor.component.ts

@@ -75,7 +75,7 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
         this.initOptionsAndVariants();
         this.languageCode =
             (this.route.snapshot.paramMap.get('lang') as LanguageCode) || getDefaultUiLanguage();
-        this.dataService.settings.getActiveChannel().single$.subscribe((data) => {
+        this.dataService.settings.getActiveChannel().single$.subscribe(data => {
             this.currencyCode = data.activeChannel.currencyCode;
         });
     }
@@ -90,13 +90,13 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
     }
 
     getVariantsToAdd() {
-        return Object.values(this.variantFormValues).filter((v) => !v.existing && v.enabled);
+        return Object.values(this.variantFormValues).filter(v => !v.existing && v.enabled);
     }
 
     getVariantName(variant: GeneratedVariant) {
         return variant.options.length === 0
             ? _('catalog.default-variant')
-            : variant.options.map((o) => o.name).join(' ');
+            : variant.options.map(o => o.name).join(' ');
     }
 
     addOption() {
@@ -108,23 +108,23 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
     }
 
     generateVariants() {
-        const groups = this.optionGroups.map((g) => g.values);
+        const groups = this.optionGroups.map(g => g.values);
         const previousVariants = this.variants;
         this.variants = groups.length
             ? generateAllCombinations(groups).map((options, i) => ({
                   isDefault: this.product.variants.length === 1 && i === 0,
-                  id: options.map((o) => o.name).join('|'),
+                  id: this.generateOptionsId(options),
                   options,
               }))
             : [{ isDefault: true, id: DEFAULT_VARIANT_CODE, options: [] }];
 
-        this.variants.forEach((variant) => {
+        this.variants.forEach(variant => {
             if (!this.variantFormValues[variant.id]) {
                 const prototype = this.getVariantPrototype(variant, previousVariants);
                 this.variantFormValues[variant.id] = {
                     enabled: false,
                     existing: false,
-                    options: variant.options.map((o) => o.name),
+                    options: variant.options.map(o => o.name),
                     price: prototype.price,
                     sku: prototype.sku,
                     stock: prototype.stock,
@@ -144,11 +144,11 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
         if (variant.isDefault) {
             return this.variantFormValues[DEFAULT_VARIANT_CODE];
         }
-        const variantsWithSimilarOptions = previousVariants.filter((v) =>
-            variant.options.map((o) => o.name).filter((name) => v.options.map((o) => o.name).includes(name)),
+        const variantsWithSimilarOptions = previousVariants.filter(v =>
+            variant.options.map(o => o.name).filter(name => v.options.map(o => o.name).includes(name)),
         );
         if (variantsWithSimilarOptions.length) {
-            return this.variantFormValues[variantsWithSimilarOptions[0].options.map((o) => o.name).join('|')];
+            return this.variantFormValues[this.generateOptionsId(variantsWithSimilarOptions[0].options)];
         }
         return {
             sku: '',
@@ -167,7 +167,7 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
                 ],
             })
             .pipe(
-                switchMap((response) =>
+                switchMap(response =>
                     response ? this.productDetailService.deleteProductVariant(id, this.product.id) : EMPTY,
                 ),
                 switchMap(() => this.reFetchProduct(null)),
@@ -179,7 +179,7 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
                     });
                     this.initOptionsAndVariants();
                 },
-                (err) => {
+                err => {
                     this.notificationService.error(_('common.notify-delete-error'), {
                         entity: 'ProductVariant',
                     });
@@ -189,8 +189,8 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
 
     save() {
         const newOptionGroups = this.optionGroups
-            .filter((og) => og.isNew)
-            .map((og) => ({
+            .filter(og => og.isNew)
+            .map(og => ({
                 name: og.name,
                 values: [],
             }));
@@ -200,15 +200,15 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
                 mergeMap(() =>
                     this.productDetailService.createProductOptionGroups(newOptionGroups, this.languageCode),
                 ),
-                mergeMap((createdOptionGroups) => this.addOptionGroupsToProduct(createdOptionGroups)),
-                mergeMap((createdOptionGroups) => this.addNewOptionsToGroups(createdOptionGroups)),
-                mergeMap((groupsIds) => this.fetchOptionGroups(groupsIds)),
-                mergeMap((groups) => this.createNewProductVariants(groups)),
-                mergeMap((res) => this.deleteDefaultVariant(res.createProductVariants)),
-                mergeMap((variants) => this.reFetchProduct(variants)),
+                mergeMap(createdOptionGroups => this.addOptionGroupsToProduct(createdOptionGroups)),
+                mergeMap(createdOptionGroups => this.addNewOptionsToGroups(createdOptionGroups)),
+                mergeMap(groupsIds => this.fetchOptionGroups(groupsIds)),
+                mergeMap(groups => this.createNewProductVariants(groups)),
+                mergeMap(res => this.deleteDefaultVariant(res.createProductVariants)),
+                mergeMap(variants => this.reFetchProduct(variants)),
             )
             .subscribe({
-                next: (variants) => {
+                next: variants => {
                     this.formValueChanged = false;
                     this.notificationService.success(_('catalog.created-new-variants-success'), {
                         count: variants.length,
@@ -230,7 +230,7 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
                     ],
                 })
                 .pipe(
-                    mergeMap((res) => {
+                    mergeMap(res => {
                         return res === true ? of(true) : EMPTY;
                     }),
                 );
@@ -244,7 +244,7 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
     ): Observable<CreateProductOptionGroup.CreateProductOptionGroup[]> {
         if (createdOptionGroups.length) {
             return forkJoin(
-                createdOptionGroups.map((optionGroup) => {
+                createdOptionGroups.map(optionGroup => {
                     return this.dataService.product.addOptionGroupToProduct({
                         productId: this.product.id,
                         optionGroupId: optionGroup.id,
@@ -260,15 +260,15 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
         createdOptionGroups: CreateProductOptionGroup.CreateProductOptionGroup[],
     ): Observable<string[]> {
         const newOptions: CreateProductOptionInput[] = this.optionGroups
-            .map((og) => {
-                const createdGroup = createdOptionGroups.find((cog) => cog.name === og.name);
+            .map(og => {
+                const createdGroup = createdOptionGroups.find(cog => cog.name === og.name);
                 const productOptionGroupId = createdGroup ? createdGroup.id : og.id;
                 if (!productOptionGroupId) {
                     throw new Error('Could not get a productOptionGroupId');
                 }
                 return og.values
-                    .filter((v) => !v.locked)
-                    .map((v) => ({
+                    .filter(v => !v.locked)
+                    .map(v => ({
                         productOptionGroupId,
                         code: normalizeString(v.name, '-'),
                         translations: [{ name: v.name, languageCode: this.languageCode }],
@@ -277,12 +277,12 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
             .reduce((flat, options) => [...flat, ...options], []);
 
         const allGroupIds = [
-            ...createdOptionGroups.map((g) => g.id),
-            ...this.optionGroups.map((g) => g.id).filter(notNullOrUndefined),
+            ...createdOptionGroups.map(g => g.id),
+            ...this.optionGroups.map(g => g.id).filter(notNullOrUndefined),
         ];
 
         if (newOptions.length) {
-            return forkJoin(newOptions.map((input) => this.dataService.product.addOptionToGroup(input))).pipe(
+            return forkJoin(newOptions.map(input => this.dataService.product.addOptionToGroup(input))).pipe(
                 map(() => allGroupIds),
             );
         } else {
@@ -292,10 +292,10 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
 
     private fetchOptionGroups(groupsIds: string[]): Observable<ProductOptionGroupWithOptionsFragment[]> {
         return forkJoin(
-            groupsIds.map((id) =>
+            groupsIds.map(id =>
                 this.dataService.product
                     .getProductOptionGroup(id)
-                    .mapSingle((data) => data.productOptionGroup)
+                    .mapSingle(data => data.productOptionGroup)
                     .pipe(filter(notNullOrUndefined)),
             ),
         );
@@ -304,18 +304,18 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
     private createNewProductVariants(groups: ProductOptionGroupWithOptionsFragment[]) {
         const options = groups
             .filter(notNullOrUndefined)
-            .map((og) => og.options)
+            .map(og => og.options)
             .reduce((flat, o) => [...flat, ...o], []);
         const variants = Object.values(this.variantFormValues)
-            .filter((v) => v.enabled && !v.existing)
-            .map((v) => ({
+            .filter(v => v.enabled && !v.existing)
+            .map(v => ({
                 price: v.price,
                 sku: v.sku,
                 stock: v.stock,
                 optionIds: v.options
-                    .map((name) => options.find((o) => o.name === name))
+                    .map(name => options.find(o => o.name === name))
                     .filter(notNullOrUndefined)
-                    .map((o) => o.id),
+                    .map(o => o.id),
             }));
         return this.productDetailService.createProductVariants(
             this.product,
@@ -350,17 +350,17 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
     private initOptionsAndVariants() {
         this.route.data
             .pipe(
-                switchMap((data) => data.entity as Observable<GetProductVariantOptions.Product>),
+                switchMap(data => data.entity as Observable<GetProductVariantOptions.Product>),
                 take(1),
             )
-            .subscribe((product) => {
+            .subscribe(product => {
                 this.product = product;
-                this.optionGroups = product.optionGroups.map((og) => {
+                this.optionGroups = product.optionGroups.map(og => {
                     return {
                         id: og.id,
                         isNew: false,
                         name: og.name,
-                        values: og.options.map((o) => ({
+                        values: og.options.map(o => ({
                             id: o.id,
                             name: o.name,
                             locked: true,
@@ -376,14 +376,14 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
         variants: GetProductVariantOptions.Variants[],
     ): { [id: string]: VariantInfo } {
         return variants.reduce((all, v) => {
-            const id = v.options.length ? v.options.map((o) => o.name).join('|') : DEFAULT_VARIANT_CODE;
+            const id = v.options.length ? this.generateOptionsId(v.options) : DEFAULT_VARIANT_CODE;
             return {
                 ...all,
                 [id]: {
                     productVariantId: v.id,
                     enabled: true,
                     existing: true,
-                    options: v.options.map((o) => o.name),
+                    options: v.options.map(o => o.name),
                     sku: v.sku,
                     price: v.price,
                     stock: v.stockOnHand,
@@ -391,4 +391,11 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
             };
         }, {});
     }
+
+    private generateOptionsId(options: GeneratedVariant['options']): string {
+        return options
+            .map(o => o.name)
+            .sort()
+            .join('|');
+    }
 }

+ 1 - 1
packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.ts

@@ -17,7 +17,7 @@ export function getConfigArgValue(value: any) {
 }
 
 export function encodeConfigArgValue(value: any): string {
-    return Array.isArray(value) ? JSON.stringify(value) : value.toString();
+    return Array.isArray(value) ? JSON.stringify(value) : (value ?? '').toString();
 }
 
 /**

+ 1 - 1
packages/admin-ui/src/lib/core/src/common/version.ts

@@ -1,2 +1,2 @@
 // Auto-generated by the set-version.js script.
-export const ADMIN_UI_VERSION = '0.16.1';
+export const ADMIN_UI_VERSION = '0.16.2';

+ 3 - 0
packages/admin-ui/src/lib/core/src/components/user-menu/user-menu.component.scss

@@ -10,6 +10,9 @@
 .user-name {
     color: $color-grey-200;
     margin-right: 12px;
+    @media screen and (max-width: $breakpoint-small) {
+        display: none;
+    }
 }
 
 .trigger clr-icon {

+ 5 - 0
packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts

@@ -423,6 +423,11 @@ export const GLOBAL_SETTINGS_FRAGMENT = gql`
         availableLanguages
         trackInventory
         outOfStockThreshold
+        serverConfig {
+            orderProcess {
+                name
+            }
+        }
     }
 `;
 

+ 8 - 4
packages/admin-ui/src/lib/core/src/shared/components/order-state-label/order-state-label.component.ts

@@ -11,16 +11,20 @@ export class OrderStateLabelComponent {
 
     get chipColorType() {
         switch (this.state) {
+            case 'AddingItems':
+            case 'ArrangingPayment':
+                return '';
+            case 'Delivered':
+                return 'success';
+            case 'Cancelled':
+                return 'error';
             case 'PaymentAuthorized':
             case 'PaymentSettled':
             case 'PartiallyDelivered':
             case 'PartiallyShipped':
             case 'Shipped':
+            default:
                 return 'warning';
-            case 'Delivered':
-                return 'success';
-            case 'Cancelled':
-                return 'error';
         }
     }
 }

+ 2 - 8
packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.html

@@ -3,15 +3,9 @@
         <div class="search-form">
             <select clrSelect name="state" [formControl]="stateFilter">
                 <option value="all">{{ 'order.state-all-orders' | translate }}</option>
-                <option value="AddingItems">{{ 'order.state-adding-items' | translate }}</option>
-                <option value="ArrangingPayment">{{ 'order.state-arranging-payment' | translate }}</option>
-                <option value="PaymentAuthorized">{{ 'order.state-payment-authorized' | translate }}</option>
-                <option value="PaymentSettled">{{ 'order.state-payment-settled' | translate }}</option>
-                <option value="PartiallyDelivered">
-                    {{ 'order.state-partially-delivered' | translate }}
+                <option *ngFor="let orderState of (orderStates$ | async)" [value]="orderState">
+                    {{ orderState | orderStateI18nToken | translate }}
                 </option>
-                <option value="Delivered">{{ 'order.state-delivered' | translate }}</option>
-                <option value="Cancelled">{{ 'order.state-cancelled' | translate }}</option>
             </select>
             <input
                 type="text"

+ 1 - 0
packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.ts

@@ -17,6 +17,7 @@ export class OrderListComponent extends BaseListComponent<GetOrderList.Query, Ge
     implements OnInit {
     searchTerm = new FormControl('');
     stateFilter = new FormControl('all');
+    orderStates$ = this.dataService.settings.getGlobalSettings().mapSingle(data => data.globalSettings.serverConfig.orderProcess.map(item => item.name));
 
     constructor(private dataService: DataService, router: Router, route: ActivatedRoute) {
         super(router, route);

+ 2 - 2
packages/admin-ui/src/lib/settings/src/components/payment-method-detail/payment-method-detail.component.html

@@ -54,9 +54,9 @@
         >
             <div class="clr-col">
                 <label>{{ 'settings.payment-method-config-options' | translate }}</label>
-                <section class="form-block" *ngFor="let arg of paymentMethod.configArgs; index as i">
+                <section class="form-block" *ngFor="let arg of paymentMethod.definition.args; index as i">
                     <vdr-form-field
-                        [label]="arg.name"
+                        [label]="arg.label || arg.name"
                         [for]="arg.name"
                     >
                         <vdr-dynamic-form-input

+ 705 - 0
packages/admin-ui/src/lib/static/i18n-messages/cs.json

@@ -0,0 +1,705 @@
+{
+  "admin": {
+    "create-new-administrator": "Vytvořit nového administrátora"
+  },
+  "asset": {
+    "add-asset": "Přidat médium",
+    "add-asset-with-count": "Přidat {count, plural, 0 {médium} =1 {médium} few {{count} média} other {{count} médií}}",
+    "assets-selected-count": "{ count } médií vybráno",
+    "dimensions": "Rozměry",
+    "focal-point": "Ohnisko",
+    "notify-create-assets-success": "{count, plural, =1 {Vytvořeno} few {Vytvořena} other {Vytvořeno}} {count, plural, =1 {médium} few {{count} média} other {{count} médií}}",
+    "original-asset-size": "Originální velikost",
+    "preview": "Náhled",
+    "remove-asset": "Smazat médium",
+    "search-asset-name": "Vyhledat médium dle jména",
+    "select-assets": "Vybrat média",
+    "set-as-featured-asset": "Zvýraznit médium",
+    "set-focal-point": "Nastavit ohnisko",
+    "source-file": "Zdrojový soubor",
+    "unset-focal-point": "Odebrat",
+    "update-focal-point": "Aktualizovat ohnisko",
+    "update-focal-point-error": "Chyba při aktualizaci ohniska",
+    "update-focal-point-success": "Ohnisko aktualizováno",
+    "upload-assets": "Nahrát média",
+    "uploading": "Nahrávání..."
+  },
+  "breadcrumb": {
+    "administrators": "Administrátoři",
+    "assets": "Média",
+    "channels": "Kanály",
+    "collections": "Kolekce",
+    "countries": "Země",
+    "customer-groups": "Cílové skupiny",
+    "customers": "Zákazníci",
+    "dashboard": "Nástěnka",
+    "facets": "Atributy",
+    "global-settings": "Všeobecné nastavení",
+    "job-queue": "Fronta úloh",
+    "manage-variants": "Správa variant",
+    "orders": "Objednávky",
+    "payment-methods": "Platební metody",
+    "products": "Produkty",
+    "promotions": "Propagace",
+    "roles": "Role",
+    "shipping-methods": "Dopravní metody",
+    "system-status": "Status systému",
+    "tax-categories": "Daňové kategorie",
+    "tax-rates": "Daňové sazby",
+    "zones": "Zóny"
+  },
+  "catalog": {
+    "add-facet-value": "Přidat hodnotu atributu",
+    "add-facets": "Přidat atribut",
+    "add-option": "Přidat možnost",
+    "assign-product-to-channel-success": "Produkt byl úspěšně přiřazen do \"{ channel }\"",
+    "assign-products-to-channel": "Přiřadit produkty do kanálu",
+    "assign-to-channel": "Přiřadit do kanálu",
+    "assign-to-named-channel": "Přiřadit do { channelCode }",
+    "channel-price-preview": "Náhled ceny v kanálu",
+    "collection-contents": "Obsah kolekce",
+    "confirm-adding-options-delete-default-body": "Přidáním vlastností k tomuto produktu způsobí vymazání nýnější výchozí varianty. Chcete pokračovat?",
+    "confirm-adding-options-delete-default-title": "Smazat výchozí variantu?",
+    "confirm-delete-administrator": "Smazat administrátora?",
+    "confirm-delete-assets": "Smazat {count} {count, plural, one {médium} few {média} other {médií}}?",
+    "confirm-delete-channel": "Smazat kanál?",
+    "confirm-delete-collection": "Smazat kolekci?",
+    "confirm-delete-collection-and-children-body": "Deleting this collection will also delete all child collections",
+    "confirm-delete-country": "Smazat zemi?",
+    "confirm-delete-customer": "Smazat zákazníka?",
+    "confirm-delete-facet": "Smazat atribut?",
+    "confirm-delete-facet-value": "Smazat hodnotu atributu?",
+    "confirm-delete-product": "Smazat produkt?",
+    "confirm-delete-product-variant": "Smazat variantu produktu?",
+    "confirm-delete-promotion": "Smazat propagaci?",
+    "confirm-delete-shipping-method": "Smazat dopravní metodu?",
+    "confirm-delete-zone": "Smazat zónu?",
+    "create-new-collection": "Vytvořit kolekci",
+    "create-new-facet": "Vytvořit nový atribut",
+    "create-new-product": "Nový produkt",
+    "created-new-variants-success": "Successfully created {count} new {count, plural, one {variant} other {variants}}",
+    "default-variant": "Výchozí varianta",
+    "delete-default-variant": "Smazat výchozí variantu",
+    "display-variant-cards": "Zobrazit jako karty",
+    "display-variant-table": "Zobrazit jako tabulku",
+    "drop-files-to-upload": "Přetáhněte soubory k nahrávání",
+    "expand-all-collections": "Rozevřít všechny kolekce",
+    "facet-values": "Hodnoty atributů",
+    "filter-by-name": "Filtrovat dle jména",
+    "filter-by-name-or-sku": "Filtrovat dle jména nebo SKU",
+    "filters": "Filtry",
+    "group-by-product": "Seskupovat varianty",
+    "manage-variants": "Správa variant",
+    "move-down": "Posunout dolů",
+    "move-to": "Posunout",
+    "move-up": "Posunout nahoru",
+    "no-channel-selected": "Žádný kanál nevybrán",
+    "no-featured-asset": "Žádné zvýrazněné médium",
+    "no-selection": "Žádný výběr",
+    "notify-remove-product-from-channel-error": "Produkt se nepovedlo odebrat z kanálu",
+    "notify-remove-product-from-channel-success": "Produkt byl úspěšně odebrán z kanálu",
+    "option": "Volba",
+    "option-name": "Jméno volby",
+    "option-values": "Hodnoty volby",
+    "price": "Cena",
+    "price-conversion-factor": "Přepočítávací koeficient ceny",
+    "price-in-channel": "Cena v { channel }",
+    "price-includes-tax-at": "Včetně daně { rate }%",
+    "price-with-tax-in-default-zone": "Včetně { rate }% daně: { price }",
+    "private": "Soukromý",
+    "product-details": "Detaily produktu",
+    "product-name": "Jméno produktu",
+    "product-variants": "Varianty produktu",
+    "public": "Veřejný",
+    "rebuild-search-index": "Regenerovat vyhledávací index",
+    "reindex-error": "Při regeneraci vyhledávacího indexu došlo k chybě",
+    "reindex-successful": "Zaindexováno: {count, plural, one {varianta produktu} other {{count} variant produktu}} během {time}ms",
+    "reindexing": "Regenerovat vyhledávací index",
+    "remove-from-channel": "Odebrat z kanálu",
+    "remove-option": "Odebrat volbu",
+    "remove-product-from-channel": "Odebrat produkt z kanálu",
+    "search-for-term": "Hledat výraz",
+    "search-product-name-or-code": "Hledat produkt dle jména, nebo kódu",
+    "sku": "SKU",
+    "slug": "Odkaz",
+    "slug-pattern-error": "Špatný formát odkazu",
+    "stock-on-hand": "Sklad",
+    "tax-category": "Skupina daní",
+    "taxes": "Daně",
+    "track-inventory": "Dopočítávat sklad",
+    "update-product-option": "Update product option",
+    "values": "Hodnoty",
+    "variant": "Varianta",
+    "view-contents": "Zobrazit obsah",
+    "visibility": "Viditelnost"
+  },
+  "common": {
+    "ID": "ID",
+    "actions": "Akce",
+    "add-item-to-list": "Přidat položku do seznamu",
+    "add-new-variants": "Přidat {count, plural, one {variantu} few {{count} varianty} other {{count} variant}}",
+    "add-note": "Přidat poznámku",
+    "available-languages": "Dostupné jazyky",
+    "cancel": "Zrušit",
+    "cancel-navigation": "Zrušit navigaci",
+    "channel": "Kanál",
+    "channels": "Kanály",
+    "code": "Kód",
+    "collapse-entries": "Schovat vstupy",
+    "confirm": "Potvrdit",
+    "confirm-delete-note": "Smazat poznámku?",
+    "confirm-navigation": "Potvrdit navigaci",
+    "create": "Vytvořit",
+    "created-at": "Vytvořeno",
+    "custom-fields": "Extra pole",
+    "default-channel": "Výchozí kanál",
+    "default-language": "Výchozí jazyk",
+    "delete": "Smazat",
+    "description": "Popis",
+    "details": "Detaily",
+    "disabled": "Vypnuto",
+    "discard-changes": "Zrušit změny",
+    "display-custom-fields": "Zobrazit extra pole",
+    "edit": "Upravit",
+    "edit-field": "Upravit pole",
+    "edit-note": "Upravit poznámku",
+    "enabled": "Zapnuto",
+    "expand-entries": "Otevřít vstupy",
+    "extension-running-in-separate-window": "Rozšíření běží v novém okně",
+    "guest": "Host",
+    "hide-custom-fields": "Skrýt extra pole",
+    "items-per-page-option": "{ count } na stránku",
+    "language": "Jazyk",
+    "launch-extension": "Spustit rozšíření",
+    "live-update": "Živé aktualizace",
+    "log-out": "Odhlásit",
+    "login": "Přihlásit",
+    "more": "Více...",
+    "name": "jméno",
+    "no-results": "Žádné výsledky",
+    "not-set": "Nenastaveno",
+    "notify-create-error": "Vyskytla se chyba, nebylo vytvořeno: { entity }",
+    "notify-create-success": "Vytvořeno: { entity }",
+    "notify-delete-error": "Vyskytla se chyba, nebylo smazáno: { entity }",
+    "notify-delete-success": "Smazáno: { entity }",
+    "notify-save-changes-error": "Vyskytla se chyba, nebylo možné uložit změny",
+    "notify-saved-changes": "Změny uloženy",
+    "notify-update-error": "Vyskytla se chyba, nebylo aktualizováno: { entity }",
+    "notify-update-success": "Aktualizováno: { entity }",
+    "open": "Otevřít",
+    "password": "Heslo",
+    "price": "Cena",
+    "price-with-tax": "Cena s daní",
+    "private": "Soukromé",
+    "public": "Veřejné",
+    "remember-me": "Zapamatovat",
+    "remove": "Smazat",
+    "remove-item-from-list": "Odebrat položku ze seznamu",
+    "results-count": "{ count } {count, plural, one {výsledek} other {výsledků/y}}",
+    "select": "Vybrat...",
+    "select-display-language": "Vyberte jazyk",
+    "select-today": "Vybrat dnešní datum",
+    "there-are-unsaved-changes": "Provedené změny nebyly uloženy. Přechod na jinou stránku způsobí ztrátu těchto změn.",
+    "update": "Aktualizovat",
+    "updated-at": "Aktualizováno",
+    "username": "Uživatelské jméno",
+    "view-next-month": "Další měsíc",
+    "view-previous-month": "Předchozí měsíc",
+    "with-selected": "S vybranými..."
+  },
+  "customer": {
+    "add-customer-to-group": "Add customer to group",
+    "add-customer-to-groups-with-count": "Add customer to {count, plural, one {1 group} other {{count} groups}}",
+    "add-customers-to-group": "Add customers to group",
+    "add-customers-to-group-success": "Added {customerCount, plural, one {1 customer} other {{customerCount} customers}} to \"{ groupName }\"",
+    "add-customers-to-group-with-count": "Add {count, plural, one {1 customer} other {{count} customers}}",
+    "add-customers-to-group-with-name": "Add customers to \"{ groupName }\"",
+    "addresses": "Adresy",
+    "city": "Město",
+    "confirm-delete-customer-group": "Delete customer group?",
+    "confirm-remove-customer-from-group": "Remove customer from group?",
+    "country": "Země",
+    "create-customer-group": "Create customer group",
+    "create-new-address": "Create new address",
+    "create-new-customer": "Create new customer",
+    "create-new-customer-group": "Create new customer group",
+    "customer-groups": "Skupiny zákazníka",
+    "customer-history": "Historie zákazníka",
+    "customer-type": "Typ zákazníka",
+    "default-billing-address": "Výchozí fakturační",
+    "default-shipping-address": "Výchozí dodací",
+    "email-address": "E-mailová adresa",
+    "email-verification-sent": "Ověřovací email byl zaslán na e-mail { emailAddress }",
+    "first-name": "Křestní jméno",
+    "full-name": "Celé jméno",
+    "guest": "Host",
+    "history-customer-added-to-group": "Zákazník přidán do skupiny \"{ groupName }\"",
+    "history-customer-address-created": "Adresa vytvořena",
+    "history-customer-address-deleted": "Adresa smazána",
+    "history-customer-address-updated": "Adresa aktualizována",
+    "history-customer-detail-updated": "Detaily zákazníka aktualizovány",
+    "history-customer-email-update-requested": "Požadavek na změnu e-mailové adresy",
+    "history-customer-email-update-verified": "Změna e-mailové adresy potvrzena",
+    "history-customer-password-reset-requested": "Požadavek na změnu hesla",
+    "history-customer-password-reset-verified": "Změna hesla potvrzena",
+    "history-customer-password-updated": "Heslo aktualizováno",
+    "history-customer-registered": "Zákazník registrován",
+    "history-customer-removed-from-group": "Zákazník odebrán ze skupiny \"{ groupName }\"",
+    "history-customer-verified": "Zákazník ověřen",
+    "history-using-external-auth-strategy": "za použití { strategy }",
+    "history-using-native-auth-strategy": "za použití e-mailové adresy",
+    "last-login": "Poslední přihlášení",
+    "last-name": "Příjmení",
+    "name": "Jméno",
+    "new-email-address": "Nová e-mailová adresa",
+    "no-orders-placed": "Žádné objednávky",
+    "not-a-member-of-any-groups": "Tento zákazník není členem žádné skupiny",
+    "old-email-address": "Stará e-mailová adresa",
+    "orders": "Objednávky",
+    "password": "Heslo",
+    "phone-number": "Telefon",
+    "postal-code": "PSČ",
+    "province": "Kraj",
+    "registered": "Registrován",
+    "remove-customers-from-group-success": "Odebrán: {customerCount, plural, one {1 zákazník} other {{customerCount} zákazníci/zákazníků}} z \"{ groupName }\"",
+    "remove-from-group": "Odebrat ze skupiny",
+    "search-customers-by-email": "Hledat podle e-mailové adresy",
+    "set-as-default-billing-address": "Nastavit jako výchozí fakturační adresu",
+    "set-as-default-shipping-address": "Nastavit jako výchozí dodací adresu",
+    "street-line-1": "Adresa",
+    "street-line-2": "Upřesnění adresy",
+    "title": "Titulek",
+    "update-customer-group": "Aktualizovat zákaznickou skupinu",
+    "verified": "Ověřený",
+    "view-group-members": "Zobrazit členy skupiny"
+  },
+  "datetime": {
+    "ago-days": "před {count, plural, one {1 dnem} other {{count} dny}}",
+    "ago-hours": "před {count, plural, one {1 hod} other {{count} hod}}",
+    "ago-minutes": "před {count, plural, one {1 min} other {{count} min}}",
+    "ago-seconds": "{count, plural, =0 {teď} one {1 sek} other {před {count} sek}}",
+    "ago-years": "před {count, plural, one {1 year} other {{count} years}}",
+    "duration-milliseconds": "{ms}ms",
+    "duration-minutes:seconds": "{m}:{s}m",
+    "duration-seconds": "{s}s",
+    "month-apr": "Duben",
+    "month-aug": "Srpen",
+    "month-dec": "Prosinec",
+    "month-feb": "Únor",
+    "month-jan": "Leden",
+    "month-jul": "Červenec",
+    "month-jun": "Červen",
+    "month-mar": "Březen",
+    "month-may": "Květen",
+    "month-nov": "Listopad",
+    "month-oct": "Říjen",
+    "month-sep": "Září",
+    "time": "Čas",
+    "weekday-fr": "Pá",
+    "weekday-mo": "Po",
+    "weekday-sa": "So",
+    "weekday-su": "Ne",
+    "weekday-th": "Čt",
+    "weekday-tu": "Út",
+    "weekday-we": "St"
+  },
+  "editor": {
+    "image-alt": "Popisek (alt)",
+    "image-src": "Zdroj",
+    "image-title": "Titulek",
+    "insert-image": "Vložit obrázek",
+    "link-href": "Odkaz",
+    "link-title": "Odkaz titulky",
+    "remove-link": "Odebrat odkaz",
+    "set-link": "Nastavit odkaz"
+  },
+  "error": {
+    "403-forbidden": "Neautorizovaný přístup k \"{ path }\". Buďto nemáte oprávnění, nebo Vaše relace vypršela.",
+    "could-not-connect-to-server": "Nelze se připojit k Vendure serveru na { url }",
+    "facet-value-form-values-do-not-match": "Počet hodnot ve formuláři atributu nesouhlasí k aktuálním počtem hodnot",
+    "health-check-failed": "Kontrala stavu systému selhala",
+    "no-default-shipping-zone-set": "Tento kanál nemá výchozí dodací zónu. To může způsobovat chyby při výpočtu poštovného.",
+    "no-default-tax-zone-set": "Tento kanál nemá výchozí daňovou zónu, to může způsobovat chyby při výpočtu cen. Prosím vytvořte, nebo vyberte zónu.",
+    "product-variant-form-values-do-not-match": "Počet variant ve formuláři neodpovídá aktuálnímu počtu variant"
+  },
+  "lang": {
+    "af": "Afrikaans",
+    "ak": "Akan",
+    "am": "Amharic",
+    "ar": "Arabic",
+    "as": "Assamese",
+    "az": "Azerbaijani",
+    "be": "Belarusian",
+    "bg": "Bulgarian",
+    "bm": "Bambara",
+    "bn": "Bangla",
+    "bo": "Tibetan",
+    "br": "Breton",
+    "bs": "Bosnian",
+    "ca": "Catalan",
+    "ce": "Chechen",
+    "co": "Corsican",
+    "cs": "Czech",
+    "cu": "Church Slavic",
+    "cy": "Welsh",
+    "da": "Danish",
+    "de": "German",
+    "de_AT": "Austrian German",
+    "de_CH": "Swiss High German",
+    "dz": "Dzongkha",
+    "ee": "Ewe",
+    "el": "Greek",
+    "en": "English",
+    "en_AU": "Australian English",
+    "en_CA": "Canadian English",
+    "en_GB": "British English",
+    "en_US": "American English",
+    "eo": "Esperanto",
+    "es": "Spanish",
+    "es_ES": "European Spanish",
+    "es_MX": "Mexican Spanish",
+    "et": "Estonian",
+    "eu": "Basque",
+    "fa": "Persian",
+    "fa_AF": "Dari",
+    "ff": "Fulah",
+    "fi": "Finnish",
+    "fo": "Faroese",
+    "fr": "French",
+    "fr_CA": "Canadian French",
+    "fr_CH": "Swiss French",
+    "fy": "Western Frisian",
+    "ga": "Irish",
+    "gd": "Scottish Gaelic",
+    "gl": "Galician",
+    "gu": "Gujarati",
+    "gv": "Manx",
+    "ha": "Hausa",
+    "he": "Hebrew",
+    "hi": "Hindi",
+    "hr": "Croatian",
+    "ht": "Haitian Creole",
+    "hu": "Hungarian",
+    "hy": "Armenian",
+    "ia": "Interlingua",
+    "id": "Indonesian",
+    "ig": "Igbo",
+    "ii": "Sichuan Yi",
+    "is": "Icelandic",
+    "it": "Italian",
+    "ja": "Japanese",
+    "jv": "Javanese",
+    "ka": "Georgian",
+    "ki": "Kikuyu",
+    "kk": "Kazakh",
+    "kl": "Kalaallisut",
+    "km": "Khmer",
+    "kn": "Kannada",
+    "ko": "Korean",
+    "ks": "Kashmiri",
+    "ku": "Kurdish",
+    "kw": "Cornish",
+    "ky": "Kyrgyz",
+    "la": "Latin",
+    "lb": "Luxembourgish",
+    "lg": "Ganda",
+    "ln": "Lingala",
+    "lo": "Lao",
+    "lt": "Lithuanian",
+    "lu": "Luba-Katanga",
+    "lv": "Latvian",
+    "mg": "Malagasy",
+    "mi": "Maori",
+    "mk": "Macedonian",
+    "ml": "Malayalam",
+    "mn": "Mongolian",
+    "mr": "Marathi",
+    "ms": "Malay",
+    "mt": "Maltese",
+    "my": "Burmese",
+    "nb": "Norwegian Bokmål",
+    "nd": "North Ndebele",
+    "ne": "Nepali",
+    "nl": "Dutch",
+    "nl_BE": "Flemish",
+    "nn": "Norwegian Nynorsk",
+    "ny": "Nyanja",
+    "om": "Oromo",
+    "or": "Odia",
+    "os": "Ossetic",
+    "pa": "Punjabi",
+    "pl": "Polish",
+    "ps": "Pashto",
+    "pt": "Portuguese",
+    "pt_BR": "Brazilian Portuguese",
+    "pt_PT": "European Portuguese",
+    "qu": "Quechua",
+    "rm": "Romansh",
+    "rn": "Rundi",
+    "ro": "Romanian",
+    "ro_MD": "Moldavian",
+    "ru": "Russian",
+    "rw": "Kinyarwanda",
+    "sa": "Sanskrit",
+    "sd": "Sindhi",
+    "se": "Northern Sami",
+    "sg": "Sango",
+    "si": "Sinhala",
+    "sk": "Slovak",
+    "sl": "Slovenian",
+    "sm": "Samoan",
+    "sn": "Shona",
+    "so": "Somali",
+    "sq": "Albanian",
+    "sr": "Serbian",
+    "st": "Southern Sotho",
+    "su": "Sundanese",
+    "sv": "Swedish",
+    "sw": "Swahili",
+    "sw_CD": "Congo Swahili",
+    "ta": "Tamil",
+    "te": "Telugu",
+    "tg": "Tajik",
+    "th": "Thai",
+    "ti": "Tigrinya",
+    "tk": "Turkmen",
+    "to": "Tongan",
+    "tr": "Turkish",
+    "tt": "Tatar",
+    "ug": "Uyghur",
+    "uk": "Ukrainian",
+    "ur": "Urdu",
+    "uz": "Uzbek",
+    "vi": "Vietnamese",
+    "vo": "Volapük",
+    "wo": "Wolof",
+    "xh": "Xhosa",
+    "yi": "Yiddish",
+    "yo": "Yoruba",
+    "zh": "Chinese",
+    "zh_Hans": "Simplified Chinese",
+    "zh_Hant": "Traditional Chinese",
+    "zu": "Zulu"
+  },
+  "marketing": {
+    "actions": "Akce",
+    "add-action": "Přidat akci",
+    "add-condition": "Přidat podmínku",
+    "conditions": "Podmínky",
+    "coupon-code": "Kód kupónu",
+    "create-new-promotion": "Nová propagace",
+    "ends-at": "Končí",
+    "per-customer-limit": "Limit za zákazníka",
+    "starts-at": "Začíná"
+  },
+  "nav": {
+    "administrators": "Administrátoři",
+    "assets": "Média",
+    "catalog": "Katalog",
+    "channels": "Kanály",
+    "collections": "Kolekce",
+    "countries": "Země",
+    "customer-groups": "Cílové skupiny",
+    "customers": "Zákazníci",
+    "facets": "Atributy",
+    "global-settings": "Globální nastavení",
+    "job-queue": "Fronta úloh",
+    "marketing": "Marketing",
+    "orders": "Objednávky",
+    "payment-methods": "Platební metody",
+    "products": "Produkty",
+    "promotions": "Propagace",
+    "roles": "Role",
+    "sales": "Prodeje",
+    "settings": "Nastavení",
+    "shipping-methods": "Dopravní metody",
+    "system": "Systém",
+    "system-status": "Status systému",
+    "tax-categories": "Daňové kategorie",
+    "tax-rates": "Daňové sazby",
+    "zones": "Zóny"
+  },
+  "order": {
+    "add-note": "Přidat poznámku",
+    "amount": "Částka",
+    "billing-address": "Fakturační adresa",
+    "cancel": "Zrušit",
+    "cancel-fulfillment": "Zrušit zpracování",
+    "cancel-order": "Zrušit objednávku",
+    "cancel-reason-customer-request": "Požadavek zákazníka",
+    "cancel-reason-not-available": "Neuveden",
+    "cancel-selected-items": "Zrušit vybrané položky",
+    "cancellation-reason": "Důvod zrušení",
+    "cancelled-order-success": "Objednávka úspěšně zrušena",
+    "contents": "Obsah",
+    "create-fulfillment": "Zpracovat",
+    "create-fulfillment-success": "Zpracováno",
+    "customer": "Zákazník",
+    "fulfill": "Zpracovat",
+    "fulfill-order": "Zpracovat objednávku",
+    "fulfillment": "Zpracování",
+    "fulfillment-method": "Metoda zpracování",
+    "history-coupon-code-applied": "Využitý kupón",
+    "history-coupon-code-removed": "Kupón odstraněn",
+    "history-fulfillment-created": "Zpracování vytvořeno",
+    "history-fulfillment-delivered": "Zpracování doručeno",
+    "history-fulfillment-shipped": "Zpracování expendováno",
+    "history-fulfillment-transition": "Stav zpracování z {from} na {to}",
+    "history-items-cancelled": "{count} {count, plural, one {položka} other {položky}} zrušeny",
+    "history-order-cancelled": "Objednávka zrušena",
+    "history-order-created": "Objednávka vytvořena",
+    "history-order-fulfilled": "Objednávka zpracována",
+    "history-order-transition": "Order transitioned from {from} to {to}",
+    "history-payment-settled": "Platba vyřízena",
+    "history-payment-transition": "Platba #{id} proběhla od {from} pro {to}",
+    "history-refund-transition": "Refundace #{id} proběhla od {from} pro {to}",
+    "item-count": "{count} {count, plural, one {položka} other {položky}}",
+    "line-fulfillment-all": "Všechny položky zpracovány",
+    "line-fulfillment-none": "Žádné položky zpracovány",
+    "line-fulfillment-partial": "{ count } z { total } položek zpracováno",
+    "net-price": "Čistá cena",
+    "note-is-private": "Interní poznámka",
+    "note-only-visible-to-administrators": "Pouze pro adminy",
+    "note-visible-to-customer": "Pro adminy i zákazníka",
+    "order-history": "Historie objednávky",
+    "order-state-diagram": "Přehled stavu objednávky",
+    "payment": "Platba",
+    "payment-amount": "Částka platby",
+    "payment-metadata": "Data platby",
+    "payment-method": "Platební metoda",
+    "payment-state": "Stav",
+    "payment-to-refund": "Platba k refundaci",
+    "product-name": "Název produktu",
+    "product-sku": "SKU",
+    "promotions-applied": "Promotions applied",
+    "quantity": "Množství",
+    "refund": "Refundace",
+    "refund-adjustment": "Úprava",
+    "refund-and-cancel-order": "Refundovat a zrušit objednávku",
+    "refund-metadata": "Data refundace",
+    "refund-order": "Refundace objednávky",
+    "refund-order-success": "Successfully refunded order",
+    "refund-reason": "Důvod refundace",
+    "refund-reason-customer-request": "Požadavek zákazníka",
+    "refund-reason-not-available": "Neuveden",
+    "refund-reason-required": "Uvedení důvodu refundace je povinné",
+    "refund-shipping": "Refundovat poštovné",
+    "refund-total": "Refundovat celkem",
+    "refund-total-error": "Částka refundace musí být mezi {min} a {max}",
+    "refund-with-amount": "Refundovat {amount}",
+    "refunded-count": "{count} {count, plural, one {položka} other {položky}} refundovány",
+    "return-to-stock": "Vrátit do skladu",
+    "search-by-order-code": "Hledat na základě kódu objednávky",
+    "set-fulfillment-state": "Označit jako {state}",
+    "settle-payment": "Vyřízení platby",
+    "settle-payment-error": "Nelze vyřídit platbu",
+    "settle-payment-success": "Platba úspěšně vypořádana",
+    "settle-refund": "Vyřízení refundace",
+    "settle-refund-manual-instructions": "Po manuálním vyřízení refundace ({method}), zadejte ID transakce.",
+    "settle-refund-success": "Úspěšně vypořádana refundace",
+    "shipping": "Dodání",
+    "shipping-address": "Dodací adresa",
+    "shipping-method": "Dodací metoda",
+    "state": "Stav",
+    "state-adding-items": "Košík",
+    "state-all-orders": "Všechny objednávky",
+    "state-arranging-payment": "Zřizování platby",
+    "state-cancelled": "Zrušeno",
+    "state-delivered": "Doručeno",
+    "state-partially-delivered": "Částečně doručeno",
+    "state-partially-shipped": "Částečně expedováno",
+    "state-payment-authorized": "Platba autorizovaná",
+    "state-payment-settled": "Platba vypořádána",
+    "state-shipped": "Expedováno",
+    "sub-total": "Mezisoučet",
+    "successfully-updated-fulfillment": "Zpracování aktualizováno",
+    "total": "Celkem",
+    "tracking-code": "Kód sledování zásilky",
+    "transaction-id": "ID transakce",
+    "transition-to-state": "Změna stavu { state }",
+    "transitioned-to-state-success": "Stav úspěšně změněn na { state }",
+    "unfulfilled": "Nevyřízeno",
+    "unit-price": "Cena za kus"
+  },
+  "settings": {
+    "add-countries-to-zone": "Přidat země do { zoneName }",
+    "add-countries-to-zone-success": "Přidáno: { countryCount } {countryCount, plural, one {země} other {země}} do zóny \"{ zoneName }\"",
+    "add-products-to-test-order": "Přidat produkty do testovací objednávky",
+    "administrator": "Administrátor",
+    "catalog": "Katalog",
+    "channel": "Kanál",
+    "channel-token": "Token kanálu",
+    "confirm-delete-role": "Smazat roli?",
+    "confirm-delete-tax-category": "Smazat daňovou kategorii?",
+    "confirm-delete-tax-rate": "Smazat daňovou sazbu?",
+    "create": "Vytváření",
+    "create-new-channel": "Vytvořit kanál",
+    "create-new-country": "Vytvořit zemi",
+    "create-new-role": "Vytvořit roli",
+    "create-new-shipping-method": "Vytvořit dodací metodu",
+    "create-new-tax-category": "Vytvořit daňovou kategorii",
+    "create-new-tax-rate": "Vytvořit daňovou sazbu",
+    "create-new-zone": "Vytvořit zónu",
+    "create-zone": "Vytvořit zónu",
+    "currency": "Měna",
+    "customer": "Zákazník",
+    "default-role-label": "Toto je výchozí role a nemůže být změněna.",
+    "default-shipping-zone": "Výchozí dodací zóna",
+    "default-tax-zone": "Výchozí daňová zóna",
+    "delete": "Mazání",
+    "eligible": "Způsobilé",
+    "email-address": "E-mailová adresa",
+    "filter-by-member-name": "Filtrovat dle země",
+    "first-name": "Jméno",
+    "last-name": "Příjmení",
+    "no-eligible-shipping-methods": "Nezpůsobilé pro žádnou dodací metodu",
+    "order": "Objednávka",
+    "password": "Heslo",
+    "payment-method-config-options": "Konfigurace platební metody",
+    "permissions": "Oprávnění",
+    "prices-include-tax": "Zadávané ceny jsou včetně daně pro výchozí zónu",
+    "promotion": "Propagace",
+    "rate": "Sazba",
+    "read": "Čtení",
+    "remove-countries-from-zone-success": "Odebráno: { countryCount } {countryCount, plural, one {země} other {země}} ze zóny \"{ zoneName }\"",
+    "remove-from-zone": "Odebrat ze zóny",
+    "roles": "Role",
+    "search-by-product-name-or-sku": "Hledat dle názvu nebo SKU produktu",
+    "search-country-by-name": "Vyhledat zemi dle jména",
+    "section": "Sekce",
+    "settings": "Nastavení",
+    "shipping-calculator": "Kalkulátor poštovného",
+    "shipping-eligibility-checker": "Test způsobilosti k dodací metodě",
+    "shipping-method": "Dodací metoda",
+    "tax-category": "Daňová kategorie",
+    "tax-rate": "Daňová sazba",
+    "test-address": "Testovací adresa",
+    "test-order": "Testovací objednávka",
+    "test-result": "Výsledek testu",
+    "test-shipping-method": "Testovat dodací metodu",
+    "test-shipping-methods": "Testovat dodací metody",
+    "track-inventory-default": "Běžně dopočítavat sklad",
+    "update": "Úpravy",
+    "update-zone": "Aktualizovat zónu",
+    "view-zone-members": "Zobrazit členy",
+    "zone": "Zóna"
+  },
+  "system": {
+    "all-job-queues": "Všechny fronty úloh",
+    "health-all-systems-up": "Všechny systémy běží",
+    "health-error": "Chyba: jeden nebo více systému neběží!",
+    "health-last-checked": "Poslední kontrola",
+    "health-message": "Zpráva",
+    "health-refresh": "Obnovit",
+    "health-status": "Stav",
+    "health-status-down": "Neběží",
+    "health-status-up": "Běží",
+    "hide-settled-jobs": "Skrýt vyřízené úlohy",
+    "job-data": "Data úlohy",
+    "job-duration": "Doba",
+    "job-error": "Chyba úlohy",
+    "job-queue-name": "Jméno fronty",
+    "job-result": "Výsledek úlohy",
+    "job-state": "Stav úlohy"
+  }
+}

+ 1 - 1
packages/admin-ui/src/lib/static/vendure-ui-config.json

@@ -5,5 +5,5 @@
   "tokenMethod": "bearer",
   "authTokenHeaderKey": "vendure-auth-token",
   "defaultLanguage": "en",
-  "availableLanguages": ["en", "es", "zh_Hant", "zh_Hans", "pl", "de", "pt_BR"]
+  "availableLanguages": ["en", "es", "zh_Hant", "zh_Hans", "pl", "de", "pt_BR", "cs"]
 }

+ 3 - 3
packages/asset-server-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/asset-server-plugin",
-  "version": "0.16.1",
+  "version": "0.16.2",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
   "files": [
@@ -22,8 +22,8 @@
     "@types/fs-extra": "^9.0.1",
     "@types/node-fetch": "^2.5.7",
     "@types/sharp": "^0.26.0",
-    "@vendure/common": "^0.16.1",
-    "@vendure/core": "^0.16.1",
+    "@vendure/common": "^0.16.2",
+    "@vendure/core": "^0.16.2",
     "aws-sdk": "^2.766.0",
     "express": "^4.17.1",
     "node-fetch": "^2.6.1",

+ 1 - 1
packages/common/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/common",
-  "version": "0.16.1",
+  "version": "0.16.2",
   "main": "index.js",
   "license": "MIT",
   "scripts": {

+ 105 - 1
packages/core/e2e/shop-order.e2e-spec.ts

@@ -6,7 +6,7 @@ import gql from 'graphql-tag';
 import path from 'path';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
-import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
+import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
 
 import {
     testErrorPaymentMethod,
@@ -77,6 +77,7 @@ import {
     SET_SHIPPING_METHOD,
     TEST_ORDER_FRAGMENT,
     TRANSITION_TO_STATE,
+    UPDATED_ORDER_FRAGMENT,
 } from './graphql/shop-definitions';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
@@ -92,6 +93,7 @@ describe('Shop orders', () => {
             },
             customFields: {
                 Order: [{ name: 'giftWrap', type: 'boolean', defaultValue: false }],
+                OrderLine: [{ name: 'notes', type: 'string' }],
             },
             orderOptions: {
                 orderItemsLimit: 99,
@@ -212,6 +214,87 @@ describe('Shop orders', () => {
             expect(addItemToOrder!.lines[0].quantity).toBe(3);
         });
 
+        it('addItemToOrder with equal customFields adds quantity to the existing OrderLine', async () => {
+            const { addItemToOrder: add1 } = await shopClient.query<AddItemToOrder.Mutation>(
+                ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
+                {
+                    productVariantId: 'T_2',
+                    quantity: 1,
+                    customFields: {
+                        notes: 'note1',
+                    },
+                },
+            );
+            orderResultGuard.assertSuccess(add1);
+            expect(add1!.lines.length).toBe(2);
+            expect(add1!.lines[1].quantity).toBe(1);
+
+            const { addItemToOrder: add2 } = await shopClient.query<AddItemToOrder.Mutation>(
+                ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
+                {
+                    productVariantId: 'T_2',
+                    quantity: 1,
+                    customFields: {
+                        notes: 'note1',
+                    },
+                },
+            );
+            orderResultGuard.assertSuccess(add2);
+            expect(add2!.lines.length).toBe(2);
+            expect(add2!.lines[1].quantity).toBe(2);
+
+            await shopClient.query<RemoveItemFromOrder.Mutation, RemoveItemFromOrder.Variables>(
+                REMOVE_ITEM_FROM_ORDER,
+                {
+                    orderLineId: add2!.lines[1].id,
+                },
+            );
+        });
+
+        it('addItemToOrder with different customFields adds quantity to a new OrderLine', async () => {
+            const { addItemToOrder: add1 } = await shopClient.query<AddItemToOrder.Mutation>(
+                ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
+                {
+                    productVariantId: 'T_3',
+                    quantity: 1,
+                    customFields: {
+                        notes: 'note2',
+                    },
+                },
+            );
+            orderResultGuard.assertSuccess(add1);
+            expect(add1!.lines.length).toBe(2);
+            expect(add1!.lines[1].quantity).toBe(1);
+
+            const { addItemToOrder: add2 } = await shopClient.query<AddItemToOrder.Mutation>(
+                ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
+                {
+                    productVariantId: 'T_3',
+                    quantity: 1,
+                    customFields: {
+                        notes: 'note3',
+                    },
+                },
+            );
+            orderResultGuard.assertSuccess(add2);
+            expect(add2!.lines.length).toBe(3);
+            expect(add2!.lines[1].quantity).toBe(1);
+            expect(add2!.lines[2].quantity).toBe(1);
+
+            await shopClient.query<RemoveItemFromOrder.Mutation, RemoveItemFromOrder.Variables>(
+                REMOVE_ITEM_FROM_ORDER,
+                {
+                    orderLineId: add2!.lines[1].id,
+                },
+            );
+            await shopClient.query<RemoveItemFromOrder.Mutation, RemoveItemFromOrder.Variables>(
+                REMOVE_ITEM_FROM_ORDER,
+                {
+                    orderLineId: add2!.lines[2].id,
+                },
+            );
+        });
+
         it('addItemToOrder errors when going beyond orderItemsLimit', async () => {
             const { addItemToOrder } = await shopClient.query<
                 AddItemToOrder.Mutation,
@@ -1317,3 +1400,24 @@ const SET_ORDER_CUSTOM_FIELDS = gql`
         }
     }
 `;
+
+export const ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS = gql`
+    mutation AddItemToOrderWithCustomFields(
+        $productVariantId: ID!
+        $quantity: Int!
+        $customFields: OrderLineCustomFieldsInput
+    ) {
+        addItemToOrder(
+            productVariantId: $productVariantId
+            quantity: $quantity
+            customFields: $customFields
+        ) {
+            ...UpdatedOrder
+            ... on ErrorResult {
+                errorCode
+                message
+            }
+        }
+    }
+    ${UPDATED_ORDER_FRAGMENT}
+`;

+ 2 - 2
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/core",
-  "version": "0.16.1",
+  "version": "0.16.2",
   "description": "A modern, headless ecommerce framework",
   "repository": {
     "type": "git",
@@ -48,7 +48,7 @@
     "@nestjs/testing": "7.4.4",
     "@nestjs/typeorm": "7.1.3",
     "@types/fs-extra": "^9.0.1",
-    "@vendure/common": "^0.16.1",
+    "@vendure/common": "^0.16.2",
     "apollo-server-express": "2.18.1",
     "bcrypt": "^5.0.0",
     "body-parser": "^1.19.0",

+ 13 - 1
packages/core/src/service/services/order.service.ts

@@ -320,7 +320,7 @@ export class OrderService {
         let orderLine = order.lines.find(line => {
             return (
                 idsAreEqual(line.productVariant.id, productVariantId) &&
-                JSON.stringify(line.customFields) === JSON.stringify(customFields)
+                this.customFieldsAreEqual(customFields, line.customFields)
             );
         });
 
@@ -1128,6 +1128,18 @@ export class OrderService {
         });
     }
 
+    private customFieldsAreEqual(
+        inputCustomFields: { [key: string]: any } | null | undefined,
+        existingCustomFields?: { [key: string]: any },
+    ): boolean {
+        if (inputCustomFields == null && typeof existingCustomFields === 'object') {
+            // A null value for an OrderLine customFields input is the equivalent
+            // of every property of an existing customFields object being null.
+            return Object.values(existingCustomFields).every(v => v === null);
+        }
+        return JSON.stringify(inputCustomFields) === JSON.stringify(existingCustomFields);
+    }
+
     private async getOrdersAndItemsFromLines(
         ctx: RequestContext,
         orderLinesInput: OrderLineInput[],

+ 3 - 3
packages/create/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/create",
-  "version": "0.16.1",
+  "version": "0.16.2",
   "license": "MIT",
   "bin": {
     "create": "./index.js"
@@ -26,13 +26,13 @@
     "@types/handlebars": "^4.1.0",
     "@types/listr": "^0.14.2",
     "@types/semver": "^6.2.2",
-    "@vendure/core": "^0.16.1",
+    "@vendure/core": "^0.16.2",
     "rimraf": "^3.0.2",
     "ts-node": "^9.0.0",
     "typescript": "4.0.3"
   },
   "dependencies": {
-    "@vendure/common": "^0.16.1",
+    "@vendure/common": "^0.16.2",
     "chalk": "^4.1.0",
     "commander": "^6.1.0",
     "cross-spawn": "^7.0.3",

+ 9 - 9
packages/dev-server/package.json

@@ -1,6 +1,6 @@
 {
   "name": "dev-server",
-  "version": "0.16.1",
+  "version": "0.16.2",
   "main": "index.js",
   "license": "MIT",
   "private": true,
@@ -14,18 +14,18 @@
     "load-test:100k": "node -r ts-node/register load-testing/run-load-test.ts 100000"
   },
   "dependencies": {
-    "@vendure/admin-ui-plugin": "^0.16.1",
-    "@vendure/asset-server-plugin": "^0.16.1",
-    "@vendure/common": "^0.16.1",
-    "@vendure/core": "^0.16.1",
-    "@vendure/elasticsearch-plugin": "^0.16.1",
-    "@vendure/email-plugin": "^0.16.1",
+    "@vendure/admin-ui-plugin": "^0.16.2",
+    "@vendure/asset-server-plugin": "^0.16.2",
+    "@vendure/common": "^0.16.2",
+    "@vendure/core": "^0.16.2",
+    "@vendure/elasticsearch-plugin": "^0.16.2",
+    "@vendure/email-plugin": "^0.16.2",
     "typescript": "4.0.3"
   },
   "devDependencies": {
     "@types/csv-stringify": "^3.1.0",
-    "@vendure/testing": "^0.16.1",
-    "@vendure/ui-devkit": "^0.16.1",
+    "@vendure/testing": "^0.16.2",
+    "@vendure/ui-devkit": "^0.16.2",
     "concurrently": "^5.0.0",
     "csv-stringify": "^5.3.3"
   }

+ 3 - 3
packages/elasticsearch-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/elasticsearch-plugin",
-  "version": "0.16.1",
+  "version": "0.16.2",
   "license": "MIT",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
@@ -22,8 +22,8 @@
     "deepmerge": "^4.2.2"
   },
   "devDependencies": {
-    "@vendure/common": "^0.16.1",
-    "@vendure/core": "^0.16.1",
+    "@vendure/common": "^0.16.2",
+    "@vendure/core": "^0.16.2",
     "rimraf": "^3.0.2",
     "typescript": "4.0.3"
   }

+ 3 - 3
packages/email-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/email-plugin",
-  "version": "0.16.1",
+  "version": "0.16.2",
   "license": "MIT",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
@@ -33,8 +33,8 @@
     "@types/handlebars": "^4.1.0",
     "@types/mjml": "^4.0.4",
     "@types/nodemailer": "^6.4.0",
-    "@vendure/common": "^0.16.1",
-    "@vendure/core": "^0.16.1",
+    "@vendure/common": "^0.16.2",
+    "@vendure/core": "^0.16.2",
     "rimraf": "^3.0.2",
     "typescript": "4.0.3"
   }

+ 8 - 0
packages/email-plugin/src/event-handler.ts

@@ -1,5 +1,6 @@
 import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { Type } from '@vendure/common/lib/shared-types';
+import { Injector } from '@vendure/core';
 
 import { EmailEventListener, EmailTemplateConfig, SetTemplateVarsFn } from './event-listener';
 import { EventWithAsyncData, EventWithContext, IntermediateEmailDetails, LoadDataFn } from './types';
@@ -169,12 +170,19 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
     async handle(
         event: Event,
         globals: { [key: string]: any } = {},
+        injector: Injector,
     ): Promise<IntermediateEmailDetails | undefined> {
         for (const filterFn of this.filterFns) {
             if (!filterFn(event)) {
                 return;
             }
         }
+        if (this instanceof EmailEventHandlerWithAsyncData) {
+            (event as EventWithAsyncData<Event, any>).data = await this._loadDataFn({
+                event,
+                injector,
+            });
+        }
         if (!this.setRecipientFn) {
             throw new Error(
                 `No setRecipientFn has been defined. ` +

+ 18 - 0
packages/email-plugin/src/plugin.spec.ts

@@ -421,6 +421,24 @@ describe('EmailPlugin', () => {
             expect(onSend.mock.calls[0][0].from).toBe('"test from" <noreply@test.com>');
             expect(onSend.mock.calls[0][0].recipient).toBe('test@test.com');
         });
+
+        it('only executes for filtered events', async () => {
+            let callCount = 0;
+            const handler = new EmailEventListener('test')
+                .on(MockEvent)
+                .filter(event => event.shouldSend === true)
+                .loadData(async ({ injector }) => {
+                    callCount++;
+                });
+
+            await initPluginWithHandlers([handler]);
+
+            eventBus.publish(new MockEvent(RequestContext.empty(), false));
+            eventBus.publish(new MockEvent(RequestContext.empty(), true));
+            await pause();
+
+            expect(callCount).toBe(1);
+        });
     });
 
     describe('orderConfirmationHandler', () => {

+ 6 - 8
packages/email-plugin/src/plugin.ts

@@ -233,14 +233,12 @@ export class EmailPlugin implements OnVendureBootstrap, OnVendureClose {
         Logger.debug(`Handling event "${handler.type}"`, 'EmailPlugin');
         const { type } = handler;
         try {
-            if (handler instanceof EmailEventHandlerWithAsyncData) {
-                const injector = new Injector(this.moduleRef);
-                (event as EventWithAsyncData<EventWithContext, any>).data = await handler._loadDataFn({
-                    event,
-                    injector,
-                });
-            }
-            const result = await handler.handle(event as any, EmailPlugin.options.globalTemplateVars);
+            const injector = new Injector(this.moduleRef);
+            const result = await handler.handle(
+                event as any,
+                EmailPlugin.options.globalTemplateVars,
+                injector,
+            );
             if (!result) {
                 return;
             }

+ 3 - 3
packages/testing/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/testing",
-  "version": "0.16.1",
+  "version": "0.16.2",
   "description": "End-to-end testing tools for Vendure projects",
   "keywords": [
     "vendure",
@@ -33,7 +33,7 @@
   },
   "dependencies": {
     "@types/node-fetch": "^2.5.4",
-    "@vendure/common": "^0.16.1",
+    "@vendure/common": "^0.16.2",
     "faker": "^4.1.0",
     "form-data": "^3.0.0",
     "graphql": "15.3.0",
@@ -44,7 +44,7 @@
   "devDependencies": {
     "@types/mysql": "^2.15.15",
     "@types/pg": "^7.14.5",
-    "@vendure/core": "^0.16.1",
+    "@vendure/core": "^0.16.2",
     "mysql": "^2.18.1",
     "pg": "^8.4.0",
     "rimraf": "^3.0.0",

+ 4 - 4
packages/ui-devkit/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/ui-devkit",
-  "version": "0.16.1",
+  "version": "0.16.2",
   "description": "A library for authoring Vendure Admin UI extensions",
   "keywords": [
     "vendure",
@@ -39,8 +39,8 @@
     "@angular/cli": "^10.1.4",
     "@angular/compiler": "^10.1.4",
     "@angular/compiler-cli": "^10.1.4",
-    "@vendure/admin-ui": "^0.16.1",
-    "@vendure/common": "^0.16.1",
+    "@vendure/admin-ui": "^0.16.2",
+    "@vendure/common": "^0.16.2",
     "chalk": "^4.1.0",
     "chokidar": "^3.4.2",
     "fs-extra": "^9.0.1",
@@ -51,7 +51,7 @@
     "@rollup/plugin-node-resolve": "^9.0.0",
     "@types/fs-extra": "^9.0.1",
     "@types/glob": "^7.1.3",
-    "@vendure/core": "^0.16.1",
+    "@vendure/core": "^0.16.2",
     "rimraf": "^3.0.2",
     "rollup": "^2.28.2",
     "rollup-plugin-terser": "^7.0.2",