Michael Bromley преди 3 години
родител
ревизия
5f3a4fb741
променени са 100 файла, в които са добавени 2227 реда и са изтрити 579 реда
  1. 2 6
      docs/content/developer-guide/customizing-models.md
  2. 1 0
      docs/content/developer-guide/deployment.md
  3. 15 3
      docs/content/developer-guide/importing-product-data.md
  4. 1 0
      packages/admin-ui-plugin/src/plugin.ts
  5. 34 34
      packages/admin-ui/i18n-coverage.json
  6. 2 2
      packages/admin-ui/src/lib/catalog/src/catalog.module.ts
  7. 0 0
      packages/admin-ui/src/lib/catalog/src/components/assets/assets.component.html
  8. 0 0
      packages/admin-ui/src/lib/catalog/src/components/assets/assets.component.scss
  9. 8 25
      packages/admin-ui/src/lib/catalog/src/components/assets/assets.component.ts
  10. 3 2
      packages/admin-ui/src/lib/catalog/src/components/collection-detail/collection-detail.component.html
  11. 3 2
      packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.html
  12. 2 0
      packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts
  13. 3 2
      packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.html
  14. 1 1
      packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.ts
  15. 1 1
      packages/admin-ui/src/lib/catalog/src/public_api.ts
  16. 103 5
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  17. 195 256
      packages/admin-ui/src/lib/core/src/common/introspection-result.ts
  18. 8 0
      packages/admin-ui/src/lib/core/src/data/definitions/customer-definitions.ts
  19. 1 0
      packages/admin-ui/src/lib/core/src/data/definitions/order-definitions.ts
  20. 10 1
      packages/admin-ui/src/lib/core/src/data/providers/customer-data.service.ts
  21. 3 1
      packages/admin-ui/src/lib/core/src/data/providers/promotion-data.service.ts
  22. 2 0
      packages/admin-ui/src/lib/core/src/public_api.ts
  23. 6 1
      packages/admin-ui/src/lib/core/src/shared/components/asset-gallery/asset-gallery.component.html
  24. 17 0
      packages/admin-ui/src/lib/core/src/shared/components/asset-preview-links/asset-preview-links.component.html
  25. 3 0
      packages/admin-ui/src/lib/core/src/shared/components/asset-preview-links/asset-preview-links.component.scss
  26. 14 0
      packages/admin-ui/src/lib/core/src/shared/components/asset-preview-links/asset-preview-links.component.ts
  27. 11 9
      packages/admin-ui/src/lib/core/src/shared/components/asset-preview/asset-preview.component.html
  28. 7 1
      packages/admin-ui/src/lib/core/src/shared/components/asset-preview/asset-preview.component.scss
  29. 11 3
      packages/admin-ui/src/lib/core/src/shared/components/object-tree/object-tree.component.ts
  30. 35 0
      packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/relation-form-input/generic/relation-generic-input.component.html
  31. 4 0
      packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/relation-form-input/generic/relation-generic-input.component.scss
  32. 47 0
      packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/relation-form-input/generic/relation-generic-input.component.ts
  33. 5 1
      packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/relation-form-input/relation-form-input.component.html
  34. 4 0
      packages/admin-ui/src/lib/core/src/shared/shared.module.ts
  35. 10 0
      packages/admin-ui/src/lib/customer/src/components/address-card/address-card.component.html
  36. 6 0
      packages/admin-ui/src/lib/customer/src/components/address-card/address-card.component.ts
  37. 3 1
      packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.html
  38. 4 0
      packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.scss
  39. 41 16
      packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.ts
  40. 2 9
      packages/admin-ui/src/lib/customer/src/components/customer-list/customer-list.component.html
  41. 12 7
      packages/admin-ui/src/lib/customer/src/components/customer-list/customer-list.component.ts
  42. 28 1
      packages/admin-ui/src/lib/order/src/components/cancel-order-dialog/cancel-order-dialog.component.html
  43. 3 0
      packages/admin-ui/src/lib/order/src/components/cancel-order-dialog/cancel-order-dialog.component.scss
  44. 39 3
      packages/admin-ui/src/lib/order/src/components/cancel-order-dialog/cancel-order-dialog.component.ts
  45. 1 1
      packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.ts
  46. 22 0
      packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.html
  47. 42 5
      packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.ts
  48. 3 0
      packages/admin-ui/src/lib/order/src/components/order-history/order-history.component.html
  49. 4 2
      packages/admin-ui/src/lib/order/src/components/order-table/order-table.component.html
  50. 2 2
      packages/admin-ui/src/lib/order/src/components/refund-order-dialog/refund-order-dialog.component.html
  51. 6 0
      packages/admin-ui/src/lib/order/src/components/refund-order-dialog/refund-order-dialog.component.scss
  52. 8 2
      packages/admin-ui/src/lib/order/src/components/refund-order-dialog/refund-order-dialog.component.ts
  53. 7 1
      packages/admin-ui/src/lib/static/i18n-messages/cs.json
  54. 7 1
      packages/admin-ui/src/lib/static/i18n-messages/de.json
  55. 9 3
      packages/admin-ui/src/lib/static/i18n-messages/en.json
  56. 7 1
      packages/admin-ui/src/lib/static/i18n-messages/es.json
  57. 7 1
      packages/admin-ui/src/lib/static/i18n-messages/fr.json
  58. 7 1
      packages/admin-ui/src/lib/static/i18n-messages/it.json
  59. 7 1
      packages/admin-ui/src/lib/static/i18n-messages/pl.json
  60. 7 1
      packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json
  61. 7 1
      packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json
  62. 7 1
      packages/admin-ui/src/lib/static/i18n-messages/ru.json
  63. 7 1
      packages/admin-ui/src/lib/static/i18n-messages/uk.json
  64. 7 1
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json
  65. 7 1
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json
  66. 25 10
      packages/admin-ui/src/lib/static/vendure-ui-config.json
  67. 59 1
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  68. 52 7
      packages/common/src/generated-shop-types.ts
  69. 61 3
      packages/common/src/generated-types.ts
  70. 10 2
      packages/common/src/shared-types.ts
  71. 1 16
      packages/core/e2e/collection.e2e-spec.ts
  72. 34 0
      packages/core/e2e/custom-fields.e2e-spec.ts
  73. 18 0
      packages/core/e2e/customer.e2e-spec.ts
  74. 3 0
      packages/core/e2e/fixtures/product-import-channel.csv
  75. 112 26
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  76. 67 7
      packages/core/e2e/graphql/generated-e2e-shop-types.ts
  77. 16 0
      packages/core/e2e/graphql/shared-definitions.ts
  78. 9 0
      packages/core/e2e/graphql/shop-definitions.ts
  79. 31 2
      packages/core/e2e/order-merge.e2e-spec.ts
  80. 257 4
      packages/core/e2e/order-modification.e2e-spec.ts
  81. 50 1
      packages/core/e2e/order-process.e2e-spec.ts
  82. 25 1
      packages/core/e2e/order.e2e-spec.ts
  83. 194 0
      packages/core/e2e/populate.e2e-spec.ts
  84. 78 0
      packages/core/e2e/shop-auth.e2e-spec.ts
  85. 0 2
      packages/core/src/api/api.module.ts
  86. 3 1
      packages/core/src/api/common/request-context.ts
  87. 1 0
      packages/core/src/api/config/configure-graphql-module.ts
  88. 30 16
      packages/core/src/api/config/graphql-custom-fields.ts
  89. 1 1
      packages/core/src/api/middleware/auth-guard.ts
  90. 4 0
      packages/core/src/api/schema/admin-api/customer.api.graphql
  91. 6 0
      packages/core/src/api/schema/admin-api/order.api.graphql
  92. 1 0
      packages/core/src/api/schema/common/common-enums.graphql
  93. 22 0
      packages/core/src/api/schema/common/common-error-results.graphql
  94. 25 0
      packages/core/src/api/schema/common/common-types.graphql
  95. 7 29
      packages/core/src/api/schema/shop-api/shop-error-results.graphql
  96. 4 2
      packages/core/src/api/schema/shop-api/shop.api.graphql
  97. 82 25
      packages/core/src/cli/populate.ts
  98. 35 1
      packages/core/src/common/error/generated-graphql-admin-errors.ts
  99. 12 1
      packages/core/src/common/error/generated-graphql-shop-errors.ts
  100. 1 1
      packages/core/src/common/finite-state-machine/finite-state-machine.ts

+ 2 - 6
docs/content/developer-guide/customizing-models.md

@@ -401,11 +401,7 @@ mutation {
 {{% alert %}}
 **UI for relation type**
 
-The Admin UI app has built-in selection components for "relation" custom fields which reference certain common entity types, such as Asset, Product, ProductVariant and Customer. If you are relating to an entity not covered by the built-in selection components, you will instead see the message:
+The Admin UI app has built-in selection components for "relation" custom fields which reference certain common entity types, such as Asset, Product, ProductVariant and Customer. If you are relating to an entity not covered by the built-in selection components, you will see a generic relation component which allows you to manually enter the ID of the entity you wish to select.
 
-```text
-No input component configured for "<entity>" type
-```
-
-In this case, you will need to create a UI extension which defines a custom field control for that custom field. You can read more about this in the [custom form input guide]({{< relref "custom-form-inputs" >}})
+If the generic selector is not suitable, or is you wish to replace one of the built-in selector components, you can create a UI extension which defines a custom field control for that custom field. You can read more about this in the [custom form input guide]({{< relref "custom-form-inputs" >}})
 {{< /alert >}}

+ 1 - 0
docs/content/developer-guide/deployment.md

@@ -39,6 +39,7 @@ and you should expect to see `UTC` or `Etc/UTC`.
 For a production Vendure server, there are a few security-related points to consider when deploying:
 
 * Set the [Superadmin credentials]({{< relref "auth-options" >}}#superadmincredentials) to something other than the default.
+* Disable introspection in the [ApiOptions]({{ relref "api-options" }}#introspection) (this option is available in v1.5+).
 * Consider taking steps to harden your GraphQL APIs against DOS attacks. Use the [ApiOptions]({{< relref "api-options" >}}) to set up appropriate Express middleware for things like [request timeouts](https://github.com/expressjs/express/issues/3330) and [rate limits](https://www.npmjs.com/package/express-rate-limit). A tool such as [graphql-query-complexity](https://github.com/slicknode/graphql-query-complexity) can be used to mitigate resource-intensive GraphQL queries. 
 * You may wish to restrict the Admin API to only be accessed from trusted IPs. This could be achieved for instance by configuring an nginx reverse proxy that sits in front of the Vendure server.
 * By default, Vendure uses auto-increment integer IDs as entity primary keys. While easier to work with in development, sequential primary keys can leak information such as the number of orders or customers in the system. For this reason you should consider using the [UuidIdStrategy]({{< relref "entity-id-strategy" >}}#uuididstrategy) for production.

+ 15 - 3
docs/content/developer-guide/importing-product-data.md

@@ -178,6 +178,7 @@ export const initialData: InitialData = {
 
 ## Populating The Server
 
+### The `populate()` function
 The `@vendure/core` package exposes a [`populate()` function]({{< relref "populate" >}}) which can be used along with the data formats described above to populate your Vendure server:
 
 ```TypeScript
@@ -203,7 +204,9 @@ populate(
   () => bootstrap(config),
   initialData,
   productsCsvFile,
-)
+  'my-channel-token' // optional - used to assign imported 
+)                    // entities to the specified Channel
+
 .then(app => {
   return app.close();
 })
@@ -216,7 +219,16 @@ populate(
 );
 ```
 
-{{< alert >}}
+### Custom populate scripts
+
 If you require more control over how your data is being imported - for example if you also need to import data into custom entities - you can create your own CLI script to do this: see [Stand-Alone CLI Scripts]({{< relref "stand-alone-scripts" >}}).
-{{< /alert >}} 
 
+In your script you can make use of the internal parse and import services:
+
+* [Importer]({{< relref "importer" >}})
+* [ImportParser]({{< relref "import-parser" >}})
+* [FastImporterService]({{< relref "fast-importer-service" >}})
+* [AssetImporter]({{< relref "asset-importer" >}})
+* [Populator]({{< relref "populator" >}})
+
+Using these specialized import services is preferable to using the normal service-layer services (ProductService, ProductVariantService etc.) for bulk imports. This is because these import services are optimized for bulk imports (they omit unnecessary checks, use optimized SQL queries) and also do not publish events when creating new entities.

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

@@ -246,6 +246,7 @@ export class AdminUiPlugin implements NestModule {
                 'hideVersion',
                 AdminUiPlugin.options.adminUiConfig?.hideVersion || false,
             ),
+            cancellationReasons: propOrDefault('cancellationReasons', undefined),
         };
     }
 

+ 34 - 34
packages/admin-ui/i18n-coverage.json

@@ -1,71 +1,71 @@
 {
-  "generatedOn": "2021-12-01T14:27:05.793Z",
-  "lastCommit": "8f218daf16959f6c8aa6f1d0262aeb124bd4214b",
+  "generatedOn": "2022-03-14T12:52:29.280Z",
+  "lastCommit": "1ab01194344fd5310fa4d7cd487c22de826d2744",
   "translationStatus": {
     "cs": {
-      "tokenCount": 634,
+      "tokenCount": 640,
       "translatedCount": 591,
-      "percentage": 93
+      "percentage": 92
     },
     "de": {
-      "tokenCount": 634,
+      "tokenCount": 640,
       "translatedCount": 570,
-      "percentage": 90
+      "percentage": 89
     },
     "en": {
-      "tokenCount": 634,
-      "translatedCount": 628,
-      "percentage": 99
+      "tokenCount": 640,
+      "translatedCount": 640,
+      "percentage": 100
     },
     "es": {
-      "tokenCount": 634,
+      "tokenCount": 640,
       "translatedCount": 623,
-      "percentage": 98
+      "percentage": 97
     },
     "fr": {
-      "tokenCount": 634,
+      "tokenCount": 640,
       "translatedCount": 613,
-      "percentage": 97
+      "percentage": 96
     },
     "it": {
-      "tokenCount": 634,
+      "tokenCount": 640,
       "translatedCount": 621,
-      "percentage": 98
+      "percentage": 97
     },
     "pl": {
-      "tokenCount": 634,
+      "tokenCount": 640,
       "translatedCount": 405,
-      "percentage": 64
+      "percentage": 63
     },
     "pt_BR": {
-      "tokenCount": 634,
-      "translatedCount": 588,
-      "percentage": 93
+      "tokenCount": 640,
+      "translatedCount": 589,
+      "percentage": 92
     },
     "pt_PT": {
-      "tokenCount": 634,
-      "translatedCount": 622,
-      "percentage": 98
+      "tokenCount": 640,
+      "translatedCount": 634,
+      "percentage": 99
     },
     "ru": {
-      "tokenCount": 634,
-      "translatedCount": 621,
-      "percentage": 98
+      "tokenCount": 640,
+      "translatedCount": 620,
+      "percentage": 97
     },
     "uk": {
-      "tokenCount": 634,
-      "translatedCount": 621,
-      "percentage": 98
+      "tokenCount": 640,
+      "translatedCount": 620,
+      "percentage": 97
     },
     "zh_Hans": {
-      "tokenCount": 634,
-      "translatedCount": 558,
-      "percentage": 88
+      "tokenCount": 640,
+      "translatedCount": 557,
+      "percentage": 87
     },
     "zh_Hant": {
-      "tokenCount": 634,
+      "tokenCount": 640,
       "translatedCount": 385,
-      "percentage": 61
+      "percentage": 60
     }
   }
 }

+ 2 - 2
packages/admin-ui/src/lib/catalog/src/catalog.module.ts

@@ -6,6 +6,7 @@ import { catalogRoutes } from './catalog.routes';
 import { ApplyFacetDialogComponent } from './components/apply-facet-dialog/apply-facet-dialog.component';
 import { AssetDetailComponent } from './components/asset-detail/asset-detail.component';
 import { AssetListComponent } from './components/asset-list/asset-list.component';
+import { AssetsComponent } from './components/assets/assets.component';
 import { AssignProductsToChannelDialogComponent } from './components/assign-products-to-channel-dialog/assign-products-to-channel-dialog.component';
 import { CollectionContentsComponent } from './components/collection-contents/collection-contents.component';
 import { CollectionDetailComponent } from './components/collection-detail/collection-detail.component';
@@ -17,7 +18,6 @@ import { FacetDetailComponent } from './components/facet-detail/facet-detail.com
 import { FacetListComponent } from './components/facet-list/facet-list.component';
 import { GenerateProductVariantsComponent } from './components/generate-product-variants/generate-product-variants.component';
 import { OptionValueInputComponent } from './components/option-value-input/option-value-input.component';
-import { ProductAssetsComponent } from './components/product-assets/product-assets.component';
 import { ProductDetailComponent } from './components/product-detail/product-detail.component';
 import { ProductListComponent } from './components/product-list/product-list.component';
 import { ProductOptionsEditorComponent } from './components/product-options-editor/product-options-editor.component';
@@ -37,7 +37,7 @@ const CATALOG_COMPONENTS = [
     ProductVariantsListComponent,
     ApplyFacetDialogComponent,
     AssetListComponent,
-    ProductAssetsComponent,
+    AssetsComponent,
     VariantPriceDetailComponent,
     CollectionListComponent,
     CollectionDetailComponent,

+ 0 - 0
packages/admin-ui/src/lib/catalog/src/components/product-assets/product-assets.component.html → packages/admin-ui/src/lib/catalog/src/components/assets/assets.component.html


+ 0 - 0
packages/admin-ui/src/lib/catalog/src/components/product-assets/product-assets.component.scss → packages/admin-ui/src/lib/catalog/src/components/assets/assets.component.scss


+ 8 - 25
packages/admin-ui/src/lib/catalog/src/components/product-assets/product-assets.component.ts → packages/admin-ui/src/lib/catalog/src/components/assets/assets.component.ts

@@ -1,5 +1,4 @@
 import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
-import { ViewportRuler } from '@angular/cdk/overlay';
 import {
     ChangeDetectionStrategy,
     ChangeDetectorRef,
@@ -7,7 +6,6 @@ import {
     EventEmitter,
     HostBinding,
     Input,
-    Optional,
     Output,
 } from '@angular/core';
 import {
@@ -19,27 +17,25 @@ import {
 } from '@vendure/admin-ui/core';
 import { unique } from '@vendure/common/lib/unique';
 
-import { CollectionDetailComponent } from '../collection-detail/collection-detail.component';
-
 export interface AssetChange {
     assets: Asset[];
     featuredAsset: Asset | undefined;
 }
 
 /**
- * A component which displays the Assets associated with a product, and allows assets to be removed and
+ * A component which displays the Assets, and allows assets to be removed and
  * added, and for the featured asset to be set.
  *
  * Note: rather complex code for drag drop is due to a limitation of the default CDK implementation
  * which is addressed by a work-around from here: https://github.com/angular/components/issues/13372#issuecomment-483998378
  */
 @Component({
-    selector: 'vdr-product-assets',
-    templateUrl: './product-assets.component.html',
-    styleUrls: ['./product-assets.component.scss'],
+    selector: 'vdr-assets',
+    templateUrl: './assets.component.html',
+    styleUrls: ['./assets.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class ProductAssetsComponent {
+export class AssetsComponent {
     @Input('assets') set assetsSetter(val: Asset[]) {
         // create a new non-readonly array of assets
         this.assets = (val || []).slice();
@@ -53,23 +49,10 @@ export class ProductAssetsComponent {
 
     public assets: Asset[] = [];
 
-    private readonly updateCollectionPermissions = [Permission.UpdateCatalog, Permission.UpdateCollection];
-    private readonly updateProductPermissions = [Permission.UpdateCatalog, Permission.UpdateProduct];
-
-    get updatePermissions(): Permission[] {
-        if (this.collectionDetailComponent) {
-            return this.updateCollectionPermissions;
-        } else {
-            return this.updateProductPermissions;
-        }
-    }
+    @Input()
+    updatePermissions: string | string[] | Permission | Permission[];
 
-    constructor(
-        private modalService: ModalService,
-        private changeDetector: ChangeDetectorRef,
-        private viewportRuler: ViewportRuler,
-        @Optional() private collectionDetailComponent?: CollectionDetailComponent,
-    ) {}
+    constructor(private modalService: ModalService, private changeDetector: ChangeDetectorRef) {}
 
     selectAssets() {
         this.modalService

+ 3 - 2
packages/admin-ui/src/lib/catalog/src/components/collection-detail/collection-detail.component.html

@@ -93,11 +93,12 @@
             ></vdr-custom-detail-component-host>
         </div>
         <div class="clr-col-md-auto">
-            <vdr-product-assets
+            <vdr-assets
                 [assets]="category.assets"
                 [featuredAsset]="category.featuredAsset"
+                [updatePermissions]="updatePermission"
                 (change)="assetChanges = $event"
-            ></vdr-product-assets>
+            ></vdr-assets>
         </div>
     </div>
     <div class="clr-row" formArrayName="filters">

+ 3 - 2
packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.html

@@ -145,11 +145,12 @@
                         </section>
                     </div>
                     <div class="clr-col-md-auto">
-                        <vdr-product-assets
+                        <vdr-assets
                             [assets]="assetChanges.assets || product.assets"
                             [featuredAsset]="assetChanges.featuredAsset || product.featuredAsset"
+                            [updatePermissions]="updatePermissions"
                             (change)="assetChanges = $event"
-                        ></vdr-product-assets>
+                        ></vdr-assets>
                         <div class="facets">
                             <vdr-facet-value-chip
                                 *ngFor="let facetValue of facetValues$ | async"

+ 2 - 0
packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts

@@ -19,6 +19,7 @@ import {
     LogicalOperator,
     ModalService,
     NotificationService,
+    Permission,
     ProductDetail,
     ProductVariant,
     ServerConfigService,
@@ -123,6 +124,7 @@ export class ProductDetailComponent
     // Used to store all ProductVariants which have been loaded.
     // It is needed when saving changes to variants.
     private productVariantMap = new Map<string, ProductVariant.Fragment>();
+    public readonly updatePermissions = [Permission.UpdateCatalog, Permission.UpdateProduct];
 
     constructor(
         route: ActivatedRoute,

+ 3 - 2
packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.html

@@ -44,14 +44,15 @@
             <div class="card-block">
                 <div class="variant-body">
                     <div class="assets">
-                        <vdr-product-assets
+                        <vdr-assets
                             [compact]="true"
                             [assets]="pendingAssetChanges[variant.id]?.assets || variant.assets"
                             [featuredAsset]="
                                 pendingAssetChanges[variant.id]?.featuredAsset || variant.featuredAsset
                             "
+                            [updatePermissions]="updatePermission"
                             (change)="onAssetChange(variant.id, $event)"
-                        ></vdr-product-assets>
+                        ></vdr-assets>
                     </div>
                     <div class="variant-form-inputs">
                         <div class="standard-fields">

+ 1 - 1
packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.ts

@@ -33,7 +33,7 @@ import { PaginationInstance } from 'ngx-pagination';
 import { Subscription } from 'rxjs';
 import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators';
 
-import { AssetChange } from '../product-assets/product-assets.component';
+import { AssetChange } from '../assets/assets.component';
 import {
     PaginationConfig,
     SelectedAssets,

+ 1 - 1
packages/admin-ui/src/lib/catalog/src/public_api.ts

@@ -4,6 +4,7 @@ export * from './catalog.routes';
 export * from './components/apply-facet-dialog/apply-facet-dialog.component';
 export * from './components/asset-detail/asset-detail.component';
 export * from './components/asset-list/asset-list.component';
+export * from './components/assets/assets.component';
 export * from './components/assign-products-to-channel-dialog/assign-products-to-channel-dialog.component';
 export * from './components/collection-contents/collection-contents.component';
 export * from './components/collection-detail/collection-detail.component';
@@ -16,7 +17,6 @@ export * from './components/facet-detail/facet-detail.component';
 export * from './components/facet-list/facet-list.component';
 export * from './components/generate-product-variants/generate-product-variants.component';
 export * from './components/option-value-input/option-value-input.component';
-export * from './components/product-assets/product-assets.component';
 export * from './components/product-detail/product-detail.component';
 export * from './components/product-list/product-list.component';
 export * from './components/product-options-editor/product-options-editor.component';

+ 103 - 5
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -74,7 +74,8 @@ export type Adjustment = {
 
 export enum AdjustmentType {
   PROMOTION = 'PROMOTION',
-  DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION'
+  DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
+  OTHER = 'OTHER'
 }
 
 export type Administrator = Node & {
@@ -276,6 +277,11 @@ export type BooleanCustomFieldConfig = CustomField & {
   ui?: Maybe<Scalars['JSON']>;
 };
 
+/** Operators for filtering on a list of Boolean fields */
+export type BooleanListOperators = {
+  inList: Scalars['Boolean'];
+};
+
 /** Operators for filtering on a Boolean field */
 export type BooleanOperators = {
   eq?: Maybe<Scalars['Boolean']>;
@@ -294,6 +300,8 @@ export type CancelOrderInput = {
   orderId: Scalars['ID'];
   /** Optionally specify which OrderLines to cancel. If not provided, all OrderLines will be cancelled */
   lines?: Maybe<Array<OrderLineInput>>;
+  /** Specify whether the shipping charges should also be cancelled. Defaults to false */
+  cancelShipping?: Maybe<Scalars['Boolean']>;
   reason?: Maybe<Scalars['String']>;
 };
 
@@ -552,6 +560,31 @@ export type CountryTranslationInput = {
   customFields?: Maybe<Scalars['JSON']>;
 };
 
+/** Returned if the provided coupon code is invalid */
+export type CouponCodeExpiredError = ErrorResult & {
+  __typename?: 'CouponCodeExpiredError';
+  errorCode: ErrorCode;
+  message: Scalars['String'];
+  couponCode: Scalars['String'];
+};
+
+/** Returned if the provided coupon code is invalid */
+export type CouponCodeInvalidError = ErrorResult & {
+  __typename?: 'CouponCodeInvalidError';
+  errorCode: ErrorCode;
+  message: Scalars['String'];
+  couponCode: Scalars['String'];
+};
+
+/** Returned if the provided coupon code is invalid */
+export type CouponCodeLimitError = ErrorResult & {
+  __typename?: 'CouponCodeLimitError';
+  errorCode: ErrorCode;
+  message: Scalars['String'];
+  couponCode: Scalars['String'];
+  limit: Scalars['Int'];
+};
+
 export type CreateAddressInput = {
   fullName?: Maybe<Scalars['String']>;
   company?: Maybe<Scalars['String']>;
@@ -1201,6 +1234,7 @@ export type CustomerOrdersArgs = {
 };
 
 export type CustomerFilterParameter = {
+  postalCode?: Maybe<StringOperators>;
   id?: Maybe<IdOperators>;
   createdAt?: Maybe<DateOperators>;
   updatedAt?: Maybe<DateOperators>;
@@ -1289,6 +1323,11 @@ export type CustomerSortParameter = {
   emailAddress?: Maybe<SortOrder>;
 };
 
+/** Operators for filtering on a list of Date fields */
+export type DateListOperators = {
+  inList: Scalars['DateTime'];
+};
+
 /** Operators for filtering on a DateTime field */
 export type DateOperators = {
   eq?: Maybe<Scalars['DateTime']>;
@@ -1405,7 +1444,10 @@ export enum ErrorCode {
   EMAIL_ADDRESS_CONFLICT_ERROR = 'EMAIL_ADDRESS_CONFLICT_ERROR',
   ORDER_LIMIT_ERROR = 'ORDER_LIMIT_ERROR',
   NEGATIVE_QUANTITY_ERROR = 'NEGATIVE_QUANTITY_ERROR',
-  INSUFFICIENT_STOCK_ERROR = 'INSUFFICIENT_STOCK_ERROR'
+  INSUFFICIENT_STOCK_ERROR = 'INSUFFICIENT_STOCK_ERROR',
+  COUPON_CODE_INVALID_ERROR = 'COUPON_CODE_INVALID_ERROR',
+  COUPON_CODE_EXPIRED_ERROR = 'COUPON_CODE_EXPIRED_ERROR',
+  COUPON_CODE_LIMIT_ERROR = 'COUPON_CODE_LIMIT_ERROR'
 }
 
 export type ErrorResult = {
@@ -1665,6 +1707,11 @@ export enum HistoryEntryType {
   ORDER_MODIFIED = 'ORDER_MODIFIED'
 }
 
+/** Operators for filtering on a list of ID fields */
+export type IdListOperators = {
+  inList: Scalars['ID'];
+};
+
 /** Operators for filtering on an ID field */
 export type IdOperators = {
   eq?: Maybe<Scalars['String']>;
@@ -2234,6 +2281,7 @@ export type ModifyOrderInput = {
   note?: Maybe<Scalars['String']>;
   refund?: Maybe<AdministratorRefundInput>;
   options?: Maybe<ModifyOrderOptions>;
+  couponCodes?: Maybe<Array<Scalars['String']>>;
 };
 
 export type ModifyOrderOptions = {
@@ -2241,7 +2289,7 @@ export type ModifyOrderOptions = {
   recalculateShipping?: Maybe<Scalars['Boolean']>;
 };
 
-export type ModifyOrderResult = Order | NoChangesSpecifiedError | OrderModificationStateError | PaymentMethodMissingError | RefundPaymentIdMissingError | OrderLimitError | NegativeQuantityError | InsufficientStockError;
+export type ModifyOrderResult = Order | NoChangesSpecifiedError | OrderModificationStateError | PaymentMethodMissingError | RefundPaymentIdMissingError | OrderLimitError | NegativeQuantityError | InsufficientStockError | CouponCodeExpiredError | CouponCodeInvalidError | CouponCodeLimitError;
 
 export type MoveCollectionInput = {
   collectionId: Scalars['ID'];
@@ -3101,6 +3149,11 @@ export type NothingToRefundError = ErrorResult & {
   message: Scalars['String'];
 };
 
+/** Operators for filtering on a list of Number fields */
+export type NumberListOperators = {
+  inList: Scalars['Float'];
+};
+
 /** Operators for filtering on a Int or Float field */
 export type NumberOperators = {
   eq?: Maybe<Scalars['Float']>;
@@ -4772,6 +4825,11 @@ export type StringFieldOption = {
   label?: Maybe<Array<LocalizedString>>;
 };
 
+/** Operators for filtering on a list of String fields */
+export type StringListOperators = {
+  inList: Scalars['String'];
+};
+
 /** Operators for filtering on a String field */
 export type StringOperators = {
   eq?: Maybe<Scalars['String']>;
@@ -5864,6 +5922,16 @@ export type UpdateCustomerAddressMutation = { updateCustomerAddress: (
     & AddressFragment
   ) };
 
+export type DeleteCustomerAddressMutationVariables = Exact<{
+  id: Scalars['ID'];
+}>;
+
+
+export type DeleteCustomerAddressMutation = { deleteCustomerAddress: (
+    { __typename?: 'Success' }
+    & Pick<Success, 'success'>
+  ) };
+
 export type CreateCustomerGroupMutationVariables = Exact<{
   input: CreateCustomerGroupInput;
 }>;
@@ -6175,7 +6243,7 @@ export type OrderLineFragment = (
 
 export type OrderDetailFragment = (
   { __typename?: 'Order' }
-  & Pick<Order, 'id' | 'createdAt' | 'updatedAt' | 'code' | 'state' | 'nextStates' | 'active' | 'subTotal' | 'subTotalWithTax' | 'total' | 'totalWithTax' | 'currencyCode' | 'shipping' | 'shippingWithTax'>
+  & Pick<Order, 'id' | 'createdAt' | 'updatedAt' | 'code' | 'state' | 'nextStates' | 'active' | 'couponCodes' | 'subTotal' | 'subTotalWithTax' | 'total' | 'totalWithTax' | 'currencyCode' | 'shipping' | 'shippingWithTax'>
   & { customer?: Maybe<(
     { __typename?: 'Customer' }
     & Pick<Customer, 'id' | 'firstName' | 'lastName'>
@@ -6539,6 +6607,15 @@ export type ModifyOrderMutation = { modifyOrder: (
   ) | (
     { __typename?: 'InsufficientStockError' }
     & ErrorResult_InsufficientStockError_Fragment
+  ) | (
+    { __typename?: 'CouponCodeExpiredError' }
+    & ErrorResult_CouponCodeExpiredError_Fragment
+  ) | (
+    { __typename?: 'CouponCodeInvalidError' }
+    & ErrorResult_CouponCodeInvalidError_Fragment
+  ) | (
+    { __typename?: 'CouponCodeLimitError' }
+    & ErrorResult_CouponCodeLimitError_Fragment
   ) };
 
 export type AddManualPaymentMutationVariables = Exact<{
@@ -8747,6 +8824,21 @@ type ErrorResult_ChannelDefaultLanguageError_Fragment = (
   & Pick<ChannelDefaultLanguageError, 'errorCode' | 'message'>
 );
 
+type ErrorResult_CouponCodeExpiredError_Fragment = (
+  { __typename?: 'CouponCodeExpiredError' }
+  & Pick<CouponCodeExpiredError, 'errorCode' | 'message'>
+);
+
+type ErrorResult_CouponCodeInvalidError_Fragment = (
+  { __typename?: 'CouponCodeInvalidError' }
+  & Pick<CouponCodeInvalidError, 'errorCode' | 'message'>
+);
+
+type ErrorResult_CouponCodeLimitError_Fragment = (
+  { __typename?: 'CouponCodeLimitError' }
+  & Pick<CouponCodeLimitError, 'errorCode' | 'message'>
+);
+
 type ErrorResult_CreateFulfillmentError_Fragment = (
   { __typename?: 'CreateFulfillmentError' }
   & Pick<CreateFulfillmentError, 'errorCode' | 'message'>
@@ -8897,7 +8989,7 @@ type ErrorResult_SettlePaymentError_Fragment = (
   & Pick<SettlePaymentError, 'errorCode' | 'message'>
 );
 
-export type ErrorResultFragment = ErrorResult_AlreadyRefundedError_Fragment | ErrorResult_CancelActiveOrderError_Fragment | ErrorResult_ChannelDefaultLanguageError_Fragment | ErrorResult_CreateFulfillmentError_Fragment | ErrorResult_EmailAddressConflictError_Fragment | ErrorResult_EmptyOrderLineSelectionError_Fragment | ErrorResult_FulfillmentStateTransitionError_Fragment | ErrorResult_InsufficientStockError_Fragment | ErrorResult_InsufficientStockOnHandError_Fragment | ErrorResult_InvalidCredentialsError_Fragment | ErrorResult_InvalidFulfillmentHandlerError_Fragment | ErrorResult_ItemsAlreadyFulfilledError_Fragment | ErrorResult_LanguageNotAvailableError_Fragment | ErrorResult_ManualPaymentStateError_Fragment | ErrorResult_MimeTypeError_Fragment | ErrorResult_MissingConditionsError_Fragment | ErrorResult_MultipleOrderError_Fragment | ErrorResult_NativeAuthStrategyError_Fragment | ErrorResult_NegativeQuantityError_Fragment | ErrorResult_NoChangesSpecifiedError_Fragment | ErrorResult_NothingToRefundError_Fragment | ErrorResult_OrderLimitError_Fragment | ErrorResult_OrderModificationStateError_Fragment | ErrorResult_OrderStateTransitionError_Fragment | ErrorResult_PaymentMethodMissingError_Fragment | ErrorResult_PaymentOrderMismatchError_Fragment | ErrorResult_PaymentStateTransitionError_Fragment | ErrorResult_ProductOptionInUseError_Fragment | ErrorResult_QuantityTooGreatError_Fragment | ErrorResult_RefundOrderStateError_Fragment | ErrorResult_RefundPaymentIdMissingError_Fragment | ErrorResult_RefundStateTransitionError_Fragment | ErrorResult_SettlePaymentError_Fragment;
+export type ErrorResultFragment = ErrorResult_AlreadyRefundedError_Fragment | ErrorResult_CancelActiveOrderError_Fragment | ErrorResult_ChannelDefaultLanguageError_Fragment | ErrorResult_CouponCodeExpiredError_Fragment | ErrorResult_CouponCodeInvalidError_Fragment | ErrorResult_CouponCodeLimitError_Fragment | ErrorResult_CreateFulfillmentError_Fragment | ErrorResult_EmailAddressConflictError_Fragment | ErrorResult_EmptyOrderLineSelectionError_Fragment | ErrorResult_FulfillmentStateTransitionError_Fragment | ErrorResult_InsufficientStockError_Fragment | ErrorResult_InsufficientStockOnHandError_Fragment | ErrorResult_InvalidCredentialsError_Fragment | ErrorResult_InvalidFulfillmentHandlerError_Fragment | ErrorResult_ItemsAlreadyFulfilledError_Fragment | ErrorResult_LanguageNotAvailableError_Fragment | ErrorResult_ManualPaymentStateError_Fragment | ErrorResult_MimeTypeError_Fragment | ErrorResult_MissingConditionsError_Fragment | ErrorResult_MultipleOrderError_Fragment | ErrorResult_NativeAuthStrategyError_Fragment | ErrorResult_NegativeQuantityError_Fragment | ErrorResult_NoChangesSpecifiedError_Fragment | ErrorResult_NothingToRefundError_Fragment | ErrorResult_OrderLimitError_Fragment | ErrorResult_OrderModificationStateError_Fragment | ErrorResult_OrderStateTransitionError_Fragment | ErrorResult_PaymentMethodMissingError_Fragment | ErrorResult_PaymentOrderMismatchError_Fragment | ErrorResult_PaymentStateTransitionError_Fragment | ErrorResult_ProductOptionInUseError_Fragment | ErrorResult_QuantityTooGreatError_Fragment | ErrorResult_RefundOrderStateError_Fragment | ErrorResult_RefundPaymentIdMissingError_Fragment | ErrorResult_RefundStateTransitionError_Fragment | ErrorResult_SettlePaymentError_Fragment;
 
 export type ShippingMethodFragment = (
   { __typename?: 'ShippingMethod' }
@@ -9337,6 +9429,12 @@ export namespace UpdateCustomerAddress {
   export type UpdateCustomerAddress = (NonNullable<UpdateCustomerAddressMutation['updateCustomerAddress']>);
 }
 
+export namespace DeleteCustomerAddress {
+  export type Variables = DeleteCustomerAddressMutationVariables;
+  export type Mutation = DeleteCustomerAddressMutation;
+  export type DeleteCustomerAddress = (NonNullable<DeleteCustomerAddressMutation['deleteCustomerAddress']>);
+}
+
 export namespace CreateCustomerGroup {
   export type Variables = CreateCustomerGroupMutationVariables;
   export type Mutation = CreateCustomerGroupMutation;

+ 195 - 256
packages/admin-ui/src/lib/core/src/common/introspection-result.ts

@@ -1,259 +1,198 @@
 // tslint:disable
 
-      export interface PossibleTypesResultData {
-        possibleTypes: {
-          [key: string]: string[]
-        }
-      }
-      const result: PossibleTypesResultData = {
-  "possibleTypes": {
-    "AddFulfillmentToOrderResult": [
-      "Fulfillment",
-      "EmptyOrderLineSelectionError",
-      "ItemsAlreadyFulfilledError",
-      "InsufficientStockOnHandError",
-      "InvalidFulfillmentHandlerError",
-      "FulfillmentStateTransitionError",
-      "CreateFulfillmentError"
-    ],
-    "AddManualPaymentToOrderResult": [
-      "Order",
-      "ManualPaymentStateError"
-    ],
-    "AuthenticationResult": [
-      "CurrentUser",
-      "InvalidCredentialsError"
-    ],
-    "CancelOrderResult": [
-      "Order",
-      "EmptyOrderLineSelectionError",
-      "QuantityTooGreatError",
-      "MultipleOrderError",
-      "CancelActiveOrderError",
-      "OrderStateTransitionError"
-    ],
-    "CreateAssetResult": [
-      "Asset",
-      "MimeTypeError"
-    ],
-    "CreateChannelResult": [
-      "Channel",
-      "LanguageNotAvailableError"
-    ],
-    "CreateCustomerResult": [
-      "Customer",
-      "EmailAddressConflictError"
-    ],
-    "CreatePromotionResult": [
-      "Promotion",
-      "MissingConditionsError"
-    ],
-    "CustomField": [
-      "BooleanCustomFieldConfig",
-      "DateTimeCustomFieldConfig",
-      "FloatCustomFieldConfig",
-      "IntCustomFieldConfig",
-      "LocaleStringCustomFieldConfig",
-      "RelationCustomFieldConfig",
-      "StringCustomFieldConfig",
-      "TextCustomFieldConfig"
-    ],
-    "CustomFieldConfig": [
-      "StringCustomFieldConfig",
-      "LocaleStringCustomFieldConfig",
-      "IntCustomFieldConfig",
-      "FloatCustomFieldConfig",
-      "BooleanCustomFieldConfig",
-      "DateTimeCustomFieldConfig",
-      "RelationCustomFieldConfig",
-      "TextCustomFieldConfig"
-    ],
-    "ErrorResult": [
-      "AlreadyRefundedError",
-      "CancelActiveOrderError",
-      "ChannelDefaultLanguageError",
-      "CreateFulfillmentError",
-      "EmailAddressConflictError",
-      "EmptyOrderLineSelectionError",
-      "FulfillmentStateTransitionError",
-      "InsufficientStockError",
-      "InsufficientStockOnHandError",
-      "InvalidCredentialsError",
-      "InvalidFulfillmentHandlerError",
-      "ItemsAlreadyFulfilledError",
-      "LanguageNotAvailableError",
-      "ManualPaymentStateError",
-      "MimeTypeError",
-      "MissingConditionsError",
-      "MultipleOrderError",
-      "NativeAuthStrategyError",
-      "NegativeQuantityError",
-      "NoChangesSpecifiedError",
-      "NothingToRefundError",
-      "OrderLimitError",
-      "OrderModificationStateError",
-      "OrderStateTransitionError",
-      "PaymentMethodMissingError",
-      "PaymentOrderMismatchError",
-      "PaymentStateTransitionError",
-      "ProductOptionInUseError",
-      "QuantityTooGreatError",
-      "RefundOrderStateError",
-      "RefundPaymentIdMissingError",
-      "RefundStateTransitionError",
-      "SettlePaymentError"
-    ],
-    "ModifyOrderResult": [
-      "Order",
-      "NoChangesSpecifiedError",
-      "OrderModificationStateError",
-      "PaymentMethodMissingError",
-      "RefundPaymentIdMissingError",
-      "OrderLimitError",
-      "NegativeQuantityError",
-      "InsufficientStockError"
-    ],
-    "NativeAuthenticationResult": [
-      "CurrentUser",
-      "InvalidCredentialsError",
-      "NativeAuthStrategyError"
-    ],
-    "Node": [
-      "Address",
-      "Administrator",
-      "Allocation",
-      "Asset",
-      "AuthenticationMethod",
-      "Cancellation",
-      "Channel",
-      "Collection",
-      "Country",
-      "Customer",
-      "CustomerGroup",
-      "Facet",
-      "FacetValue",
-      "Fulfillment",
-      "HistoryEntry",
-      "Job",
-      "Order",
-      "OrderItem",
-      "OrderLine",
-      "OrderModification",
-      "Payment",
-      "PaymentMethod",
-      "Product",
-      "ProductOption",
-      "ProductOptionGroup",
-      "ProductVariant",
-      "Promotion",
-      "Refund",
-      "Release",
-      "Return",
-      "Role",
-      "Sale",
-      "ShippingMethod",
-      "StockAdjustment",
-      "Surcharge",
-      "Tag",
-      "TaxCategory",
-      "TaxRate",
-      "User",
-      "Zone"
-    ],
-    "PaginatedList": [
-      "AdministratorList",
-      "AssetList",
-      "CollectionList",
-      "CountryList",
-      "CustomerGroupList",
-      "CustomerList",
-      "FacetList",
-      "HistoryEntryList",
-      "JobList",
-      "OrderList",
-      "PaymentMethodList",
-      "ProductList",
-      "ProductVariantList",
-      "PromotionList",
-      "RoleList",
-      "ShippingMethodList",
-      "TagList",
-      "TaxRateList"
-    ],
-    "RefundOrderResult": [
-      "Refund",
-      "QuantityTooGreatError",
-      "NothingToRefundError",
-      "OrderStateTransitionError",
-      "MultipleOrderError",
-      "PaymentOrderMismatchError",
-      "RefundOrderStateError",
-      "AlreadyRefundedError",
-      "RefundStateTransitionError"
-    ],
-    "RemoveOptionGroupFromProductResult": [
-      "Product",
-      "ProductOptionInUseError"
-    ],
-    "SearchResultPrice": [
-      "PriceRange",
-      "SinglePrice"
-    ],
-    "SettlePaymentResult": [
-      "Payment",
-      "SettlePaymentError",
-      "PaymentStateTransitionError",
-      "OrderStateTransitionError"
-    ],
-    "SettleRefundResult": [
-      "Refund",
-      "RefundStateTransitionError"
-    ],
-    "StockMovement": [
-      "Allocation",
-      "Cancellation",
-      "Release",
-      "Return",
-      "Sale",
-      "StockAdjustment"
-    ],
-    "StockMovementItem": [
-      "StockAdjustment",
-      "Allocation",
-      "Sale",
-      "Cancellation",
-      "Return",
-      "Release"
-    ],
-    "TransitionFulfillmentToStateResult": [
-      "Fulfillment",
-      "FulfillmentStateTransitionError"
-    ],
-    "TransitionOrderToStateResult": [
-      "Order",
-      "OrderStateTransitionError"
-    ],
-    "TransitionPaymentToStateResult": [
-      "Payment",
-      "PaymentStateTransitionError"
-    ],
-    "UpdateChannelResult": [
-      "Channel",
-      "LanguageNotAvailableError"
-    ],
-    "UpdateCustomerResult": [
-      "Customer",
-      "EmailAddressConflictError"
-    ],
-    "UpdateGlobalSettingsResult": [
-      "GlobalSettings",
-      "ChannelDefaultLanguageError"
-    ],
-    "UpdatePromotionResult": [
-      "Promotion",
-      "MissingConditionsError"
-    ]
-  }
+export interface PossibleTypesResultData {
+    possibleTypes: {
+        [key: string]: string[];
+    };
+}
+const result: PossibleTypesResultData = {
+    possibleTypes: {
+        AddFulfillmentToOrderResult: [
+            'Fulfillment',
+            'EmptyOrderLineSelectionError',
+            'ItemsAlreadyFulfilledError',
+            'InsufficientStockOnHandError',
+            'InvalidFulfillmentHandlerError',
+            'FulfillmentStateTransitionError',
+            'CreateFulfillmentError',
+        ],
+        AddManualPaymentToOrderResult: ['Order', 'ManualPaymentStateError'],
+        AuthenticationResult: ['CurrentUser', 'InvalidCredentialsError'],
+        CancelOrderResult: [
+            'Order',
+            'EmptyOrderLineSelectionError',
+            'QuantityTooGreatError',
+            'MultipleOrderError',
+            'CancelActiveOrderError',
+            'OrderStateTransitionError',
+        ],
+        CreateAssetResult: ['Asset', 'MimeTypeError'],
+        CreateChannelResult: ['Channel', 'LanguageNotAvailableError'],
+        CreateCustomerResult: ['Customer', 'EmailAddressConflictError'],
+        CreatePromotionResult: ['Promotion', 'MissingConditionsError'],
+        CustomField: [
+            'BooleanCustomFieldConfig',
+            'DateTimeCustomFieldConfig',
+            'FloatCustomFieldConfig',
+            'IntCustomFieldConfig',
+            'LocaleStringCustomFieldConfig',
+            'RelationCustomFieldConfig',
+            'StringCustomFieldConfig',
+            'TextCustomFieldConfig',
+        ],
+        CustomFieldConfig: [
+            'StringCustomFieldConfig',
+            'LocaleStringCustomFieldConfig',
+            'IntCustomFieldConfig',
+            'FloatCustomFieldConfig',
+            'BooleanCustomFieldConfig',
+            'DateTimeCustomFieldConfig',
+            'RelationCustomFieldConfig',
+            'TextCustomFieldConfig',
+        ],
+        ErrorResult: [
+            'AlreadyRefundedError',
+            'CancelActiveOrderError',
+            'ChannelDefaultLanguageError',
+            'CouponCodeExpiredError',
+            'CouponCodeInvalidError',
+            'CouponCodeLimitError',
+            'CreateFulfillmentError',
+            'EmailAddressConflictError',
+            'EmptyOrderLineSelectionError',
+            'FulfillmentStateTransitionError',
+            'InsufficientStockError',
+            'InsufficientStockOnHandError',
+            'InvalidCredentialsError',
+            'InvalidFulfillmentHandlerError',
+            'ItemsAlreadyFulfilledError',
+            'LanguageNotAvailableError',
+            'ManualPaymentStateError',
+            'MimeTypeError',
+            'MissingConditionsError',
+            'MultipleOrderError',
+            'NativeAuthStrategyError',
+            'NegativeQuantityError',
+            'NoChangesSpecifiedError',
+            'NothingToRefundError',
+            'OrderLimitError',
+            'OrderModificationStateError',
+            'OrderStateTransitionError',
+            'PaymentMethodMissingError',
+            'PaymentOrderMismatchError',
+            'PaymentStateTransitionError',
+            'ProductOptionInUseError',
+            'QuantityTooGreatError',
+            'RefundOrderStateError',
+            'RefundPaymentIdMissingError',
+            'RefundStateTransitionError',
+            'SettlePaymentError',
+        ],
+        ModifyOrderResult: [
+            'Order',
+            'NoChangesSpecifiedError',
+            'OrderModificationStateError',
+            'PaymentMethodMissingError',
+            'RefundPaymentIdMissingError',
+            'OrderLimitError',
+            'NegativeQuantityError',
+            'InsufficientStockError',
+            'CouponCodeExpiredError',
+            'CouponCodeInvalidError',
+            'CouponCodeLimitError',
+        ],
+        NativeAuthenticationResult: ['CurrentUser', 'InvalidCredentialsError', 'NativeAuthStrategyError'],
+        Node: [
+            'Address',
+            'Administrator',
+            'Allocation',
+            'Asset',
+            'AuthenticationMethod',
+            'Cancellation',
+            'Channel',
+            'Collection',
+            'Country',
+            'Customer',
+            'CustomerGroup',
+            'Facet',
+            'FacetValue',
+            'Fulfillment',
+            'HistoryEntry',
+            'Job',
+            'Order',
+            'OrderItem',
+            'OrderLine',
+            'OrderModification',
+            'Payment',
+            'PaymentMethod',
+            'Product',
+            'ProductOption',
+            'ProductOptionGroup',
+            'ProductVariant',
+            'Promotion',
+            'Refund',
+            'Release',
+            'Return',
+            'Role',
+            'Sale',
+            'ShippingMethod',
+            'StockAdjustment',
+            'Surcharge',
+            'Tag',
+            'TaxCategory',
+            'TaxRate',
+            'User',
+            'Zone',
+        ],
+        PaginatedList: [
+            'AdministratorList',
+            'AssetList',
+            'CollectionList',
+            'CountryList',
+            'CustomerGroupList',
+            'CustomerList',
+            'FacetList',
+            'HistoryEntryList',
+            'JobList',
+            'OrderList',
+            'PaymentMethodList',
+            'ProductList',
+            'ProductVariantList',
+            'PromotionList',
+            'RoleList',
+            'ShippingMethodList',
+            'TagList',
+            'TaxRateList',
+        ],
+        RefundOrderResult: [
+            'Refund',
+            'QuantityTooGreatError',
+            'NothingToRefundError',
+            'OrderStateTransitionError',
+            'MultipleOrderError',
+            'PaymentOrderMismatchError',
+            'RefundOrderStateError',
+            'AlreadyRefundedError',
+            'RefundStateTransitionError',
+        ],
+        RemoveOptionGroupFromProductResult: ['Product', 'ProductOptionInUseError'],
+        SearchResultPrice: ['PriceRange', 'SinglePrice'],
+        SettlePaymentResult: [
+            'Payment',
+            'SettlePaymentError',
+            'PaymentStateTransitionError',
+            'OrderStateTransitionError',
+        ],
+        SettleRefundResult: ['Refund', 'RefundStateTransitionError'],
+        StockMovement: ['Allocation', 'Cancellation', 'Release', 'Return', 'Sale', 'StockAdjustment'],
+        StockMovementItem: ['StockAdjustment', 'Allocation', 'Sale', 'Cancellation', 'Return', 'Release'],
+        TransitionFulfillmentToStateResult: ['Fulfillment', 'FulfillmentStateTransitionError'],
+        TransitionOrderToStateResult: ['Order', 'OrderStateTransitionError'],
+        TransitionPaymentToStateResult: ['Payment', 'PaymentStateTransitionError'],
+        UpdateChannelResult: ['Channel', 'LanguageNotAvailableError'],
+        UpdateCustomerResult: ['Customer', 'EmailAddressConflictError'],
+        UpdateGlobalSettingsResult: ['GlobalSettings', 'ChannelDefaultLanguageError'],
+        UpdatePromotionResult: ['Promotion', 'MissingConditionsError'],
+    },
 };
-      export default result;
-    
+export default result;

+ 8 - 0
packages/admin-ui/src/lib/core/src/data/definitions/customer-definitions.ts

@@ -151,6 +151,14 @@ export const UPDATE_CUSTOMER_ADDRESS = gql`
     ${ADDRESS_FRAGMENT}
 `;
 
+export const DELETE_CUSTOMER_ADDRESS = gql`
+    mutation DeleteCustomerAddress($id: ID!) {
+        deleteCustomerAddress(id: $id) {
+            success
+        }
+    }
+`;
+
 export const CREATE_CUSTOMER_GROUP = gql`
     mutation CreateCustomerGroup($input: CreateCustomerGroupInput!) {
         createCustomerGroup(input: $input) {

+ 1 - 0
packages/admin-ui/src/lib/core/src/data/definitions/order-definitions.ts

@@ -127,6 +127,7 @@ export const ORDER_DETAIL_FRAGMENT = gql`
         state
         nextStates
         active
+        couponCodes
         customer {
             id
             firstName

+ 10 - 1
packages/admin-ui/src/lib/core/src/data/providers/customer-data.service.ts

@@ -10,6 +10,7 @@ import {
     CustomerGroupListOptions,
     CustomerListOptions,
     DeleteCustomer,
+    DeleteCustomerAddress,
     DeleteCustomerGroup,
     DeleteCustomerNote,
     GetCustomer,
@@ -36,11 +37,12 @@ import {
     CREATE_CUSTOMER_ADDRESS,
     CREATE_CUSTOMER_GROUP,
     DELETE_CUSTOMER,
+    DELETE_CUSTOMER_ADDRESS,
     DELETE_CUSTOMER_GROUP,
     DELETE_CUSTOMER_NOTE,
     GET_CUSTOMER,
-    GET_CUSTOMER_GROUP_WITH_CUSTOMERS,
     GET_CUSTOMER_GROUPS,
+    GET_CUSTOMER_GROUP_WITH_CUSTOMERS,
     GET_CUSTOMER_HISTORY,
     GET_CUSTOMER_LIST,
     REMOVE_CUSTOMERS_FROM_GROUP,
@@ -129,6 +131,13 @@ export class CustomerDataService {
         );
     }
 
+    deleteCustomerAddress(id: string) {
+        return this.baseDataService.mutate<DeleteCustomerAddress.Mutation, DeleteCustomerAddress.Variables>(
+            DELETE_CUSTOMER_ADDRESS,
+            { id },
+        );
+    }
+
     createCustomerGroup(input: CreateCustomerGroupInput) {
         return this.baseDataService.mutate<CreateCustomerGroup.Mutation, CreateCustomerGroup.Variables>(
             CREATE_CUSTOMER_GROUP,

+ 3 - 1
packages/admin-ui/src/lib/core/src/data/providers/promotion-data.service.ts

@@ -5,6 +5,7 @@ import {
     GetAdjustmentOperations,
     GetPromotion,
     GetPromotionList,
+    PromotionFilterParameter,
     UpdatePromotion,
     UpdatePromotionInput,
 } from '../../common/generated-types';
@@ -22,13 +23,14 @@ import { BaseDataService } from './base-data.service';
 export class PromotionDataService {
     constructor(private baseDataService: BaseDataService) {}
 
-    getPromotions(take: number = 10, skip: number = 0) {
+    getPromotions(take: number = 10, skip: number = 0, filter?: PromotionFilterParameter) {
         return this.baseDataService.query<GetPromotionList.Query, GetPromotionList.Variables>(
             GET_PROMOTION_LIST,
             {
                 options: {
                     take,
                     skip,
+                    filter,
                 },
             },
         );

+ 2 - 0
packages/admin-ui/src/lib/core/src/public_api.ts

@@ -98,6 +98,7 @@ export * from './shared/components/asset-gallery/asset-gallery.component';
 export * from './shared/components/asset-picker-dialog/asset-picker-dialog.component';
 export * from './shared/components/asset-preview/asset-preview.component';
 export * from './shared/components/asset-preview-dialog/asset-preview-dialog.component';
+export * from './shared/components/asset-preview-links/asset-preview-links.component';
 export * from './shared/components/asset-search-input/asset-search-input.component';
 export * from './shared/components/channel-assignment-control/channel-assignment-control.component';
 export * from './shared/components/channel-badge/channel-badge.component';
@@ -185,6 +186,7 @@ export * from './shared/dynamic-form-inputs/product-selector-form-input/product-
 export * from './shared/dynamic-form-inputs/register-dynamic-input-components';
 export * from './shared/dynamic-form-inputs/relation-form-input/asset/relation-asset-input.component';
 export * from './shared/dynamic-form-inputs/relation-form-input/customer/relation-customer-input.component';
+export * from './shared/dynamic-form-inputs/relation-form-input/generic/relation-generic-input.component';
 export * from './shared/dynamic-form-inputs/relation-form-input/product/relation-product-input.component';
 export * from './shared/dynamic-form-inputs/relation-form-input/product-variant/relation-product-variant-input.component';
 export * from './shared/dynamic-form-inputs/relation-form-input/relation-card/relation-card.component';

+ 6 - 1
packages/admin-ui/src/lib/core/src/shared/components/asset-gallery/asset-gallery.component.html

@@ -37,12 +37,17 @@
             <div>{{ 'asset.original-asset-size' | translate }}: {{ lastSelected().fileSize | filesize }}</div>
 
             <ng-container *ngIf="selection.length === 1">
-                <vdr-chip *ngFor="let tag of lastSelected().tags" [colorFrom]="tag.value"><clr-icon shape="tag" class="mr2"></clr-icon> {{ tag.value }}</vdr-chip>
+                <vdr-chip *ngFor="let tag of lastSelected().tags" [colorFrom]="tag.value"
+                    ><clr-icon shape="tag" class="mr2"></clr-icon> {{ tag.value }}</vdr-chip
+                >
                 <div>
                     <button (click)="previewAsset(lastSelected())" class="btn btn-link">
                         <clr-icon shape="eye"></clr-icon> {{ 'asset.preview' | translate }}
                     </button>
                 </div>
+                <div>
+                    <vdr-asset-preview-links class="" [asset]="lastSelected()"></vdr-asset-preview-links>
+                </div>
                 <div>
                     <a [routerLink]="['./', lastSelected().id]" class="btn btn-link">
                         <clr-icon shape="pencil"></clr-icon> {{ 'common.edit' | translate }}

+ 17 - 0
packages/admin-ui/src/lib/core/src/shared/components/asset-preview-links/asset-preview-links.component.html

@@ -0,0 +1,17 @@
+<vdr-dropdown>
+    <button class="btn btn-link" vdrDropdownTrigger>
+        <clr-icon shape="link"></clr-icon> {{ 'catalog.asset-preview-links' | translate }}<clr-icon shape="caret" dir="down"></clr-icon>
+    </button>
+    <vdr-dropdown-menu vdrPosition="bottom-left">
+        <a
+            *ngFor="let size of sizes"
+            [href]="asset | assetPreview: size"
+            [title]="asset | assetPreview: size"
+            target="_blank"
+            class="asset-preview-link"
+            vdrDropdownItem
+        >
+            <vdr-chip><clr-icon shape="link"></clr-icon> {{ 'asset.preview' | translate }}: {{ size }}</vdr-chip>
+        </a>
+    </vdr-dropdown-menu></vdr-dropdown
+>

+ 3 - 0
packages/admin-ui/src/lib/core/src/shared/components/asset-preview-links/asset-preview-links.component.scss

@@ -0,0 +1,3 @@
+.asset-preview-link {
+    font-size: 12px;
+}

+ 14 - 0
packages/admin-ui/src/lib/core/src/shared/components/asset-preview-links/asset-preview-links.component.ts

@@ -0,0 +1,14 @@
+import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
+
+import { AssetLike } from '../asset-gallery/asset-gallery.component';
+
+@Component({
+    selector: 'vdr-asset-preview-links',
+    templateUrl: './asset-preview-links.component.html',
+    styleUrls: ['./asset-preview-links.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class AssetPreviewLinksComponent {
+    @Input() asset: AssetLike;
+    sizes = ['tiny', 'thumb', 'small', 'medium', 'large', 'full'];
+}

+ 11 - 9
packages/admin-ui/src/lib/core/src/shared/components/asset-preview/asset-preview.component.html

@@ -124,13 +124,15 @@
         </clr-select-container>
         <div class="asset-detail">{{ width }} x {{ height }}</div>
     </div>
-    <a
-        *ngIf="!editable"
-        class="btn btn-link btn-sm"
-        [routerLink]="['/catalog', 'assets', asset.id]"
-        (click)="editClick.emit()"
-    >
-        <clr-icon shape="edit"></clr-icon>
-        {{ 'common.edit' | translate }}
-    </a>
+    <vdr-asset-preview-links class="mb4" [asset]="asset"></vdr-asset-preview-links>
+    <div *ngIf="!editable" class="edit-button-wrapper">
+        <a
+            class="btn btn-link btn-sm"
+            [routerLink]="['/catalog', 'assets', asset.id]"
+            (click)="editClick.emit()"
+        >
+            <clr-icon shape="edit"></clr-icon>
+            {{ 'common.edit' | translate }}
+        </a>
+    </div>
 </div>

+ 7 - 1
packages/admin-ui/src/lib/core/src/shared/components/asset-preview/asset-preview.component.scss

@@ -58,6 +58,7 @@
 
     ::ng-deep .clr-control-container {
         width: 100%;
+
         .clr-input {
             width: 100%;
         }
@@ -76,9 +77,14 @@
     .preview-select {
         display: flex;
         align-items: center;
-        margin-bottom: 12px;
         clr-select-container {
             margin-right: 12px;
         }
     }
 }
+
+.edit-button-wrapper {
+    padding-top: 6px;
+    border-top: 1px solid var(--color-component-border-100);
+    text-align: center;
+}

+ 11 - 3
packages/admin-ui/src/lib/core/src/shared/components/object-tree/object-tree.component.ts

@@ -1,4 +1,12 @@
-import { ChangeDetectionStrategy, Component, Input, OnInit, Optional, SkipSelf } from '@angular/core';
+import {
+    ChangeDetectionStrategy,
+    Component,
+    Input,
+    OnChanges,
+    OnInit,
+    Optional,
+    SkipSelf,
+} from '@angular/core';
 
 /**
  * @description
@@ -17,7 +25,7 @@ import { ChangeDetectionStrategy, Component, Input, OnInit, Optional, SkipSelf }
     styleUrls: ['./object-tree.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class ObjectTreeComponent implements OnInit {
+export class ObjectTreeComponent implements OnChanges {
     @Input() value: { [key: string]: any } | string;
     @Input() isArrayItem = false;
     depth: number;
@@ -32,7 +40,7 @@ export class ObjectTreeComponent implements OnInit {
         }
     }
 
-    ngOnInit() {
+    ngOnChanges() {
         this.entries = this.getEntries(this.value);
         this.expanded = this.depth === 0 || this.isArrayItem;
         this.valueIsArray = Object.keys(this.value).every(v => Number.isInteger(+v));

+ 35 - 0
packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/relation-form-input/generic/relation-generic-input.component.html

@@ -0,0 +1,35 @@
+<vdr-relation-card
+    (select)="selectRelationId()"
+    (remove)="remove()"
+    placeholderIcon="objects"
+    [entity]="parentFormControl.value"
+    [selectLabel]="'common.select-relation-id' | translate"
+    [removable]="!config.list"
+    [readonly]="readonly"
+>
+    {{ parentFormControl.value | json }}
+    <ng-template vdrRelationCardPreview>
+        <div class="placeholder">
+            <clr-icon shape="objects" size="50"></clr-icon>
+        </div>
+    </ng-template>
+    <ng-template vdrRelationCardDetail let-entity="entity">
+        <div class="">
+            {{ config.entity }}: <strong>{{ entity.id }}</strong>
+        </div>
+        <vdr-object-tree [value]="{ properties: parentFormControl.value }"></vdr-object-tree>
+    </ng-template>
+</vdr-relation-card>
+
+<ng-template #selector let-select="select">
+    <div class="id-select-wrapper">
+        <clr-input-container>
+            <input [(ngModel)]="relationId" type="text" clrInput [readonly]="readonly" />
+        </clr-input-container>
+        <div>
+            <button class="btn btn-primary m0" (click)="select(relationId)">
+                {{ 'common.confirm' | translate }}
+            </button>
+        </div>
+    </div>
+</ng-template>

+ 4 - 0
packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/relation-form-input/generic/relation-generic-input.component.scss

@@ -0,0 +1,4 @@
+.id-select-wrapper {
+    display: flex;
+    align-items: flex-end;
+}

+ 47 - 0
packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/relation-form-input/generic/relation-generic-input.component.ts

@@ -0,0 +1,47 @@
+import { ChangeDetectionStrategy, Component, Input, TemplateRef, ViewChild } from '@angular/core';
+import { FormControl } from '@angular/forms';
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
+
+import { RelationCustomFieldConfig } from '../../../../common/generated-types';
+import { ModalService } from '../../../../providers/modal/modal.service';
+import { RelationSelectorDialogComponent } from '../relation-selector-dialog/relation-selector-dialog.component';
+
+@Component({
+    selector: 'vdr-relation-generic-input',
+    templateUrl: './relation-generic-input.component.html',
+    styleUrls: ['./relation-generic-input.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class RelationGenericInputComponent {
+    @Input() readonly: boolean;
+    @Input() parentFormControl: FormControl;
+    @Input() config: RelationCustomFieldConfig;
+    relationId: string;
+
+    @ViewChild('selector') template: TemplateRef<any>;
+
+    constructor(private modalService: ModalService) {}
+
+    selectRelationId() {
+        this.modalService
+            .fromComponent(RelationSelectorDialogComponent, {
+                size: 'md',
+                closable: true,
+                locals: {
+                    title: _('common.select-relation-id'),
+                    selectorTemplate: this.template,
+                },
+            })
+            .subscribe(result => {
+                if (result) {
+                    this.parentFormControl.setValue({ id: result });
+                    this.parentFormControl.markAsDirty();
+                }
+            });
+    }
+
+    remove() {
+        this.parentFormControl.setValue(null);
+        this.parentFormControl.markAsDirty();
+    }
+}

+ 5 - 1
packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/relation-form-input/relation-form-input.component.html

@@ -24,6 +24,10 @@
         [readonly]="readonly"
     ></vdr-relation-product-variant-input>
     <ng-template ngSwitchDefault>
-        No input component configured for "{{ config.entity }}" type
+        <vdr-relation-generic-input
+            [parentFormControl]="formControl"
+               [config]="config"
+               [readonly]="readonly"
+        ></vdr-relation-generic-input>
     </ng-template>
 </div>

+ 4 - 0
packages/admin-ui/src/lib/core/src/shared/shared.module.ts

@@ -27,6 +27,7 @@ import { AssetFileInputComponent } from './components/asset-file-input/asset-fil
 import { AssetGalleryComponent } from './components/asset-gallery/asset-gallery.component';
 import { AssetPickerDialogComponent } from './components/asset-picker-dialog/asset-picker-dialog.component';
 import { AssetPreviewDialogComponent } from './components/asset-preview-dialog/asset-preview-dialog.component';
+import { AssetPreviewLinksComponent } from './components/asset-preview-links/asset-preview-links.component';
 import { AssetPreviewComponent } from './components/asset-preview/asset-preview.component';
 import { AssetSearchInputComponent } from './components/asset-search-input/asset-search-input.component';
 import { ChannelAssignmentControlComponent } from './components/channel-assignment-control/channel-assignment-control.component';
@@ -97,6 +98,7 @@ import { PasswordFormInputComponent } from './dynamic-form-inputs/password-form-
 import { ProductSelectorFormInputComponent } from './dynamic-form-inputs/product-selector-form-input/product-selector-form-input.component';
 import { RelationAssetInputComponent } from './dynamic-form-inputs/relation-form-input/asset/relation-asset-input.component';
 import { RelationCustomerInputComponent } from './dynamic-form-inputs/relation-form-input/customer/relation-customer-input.component';
+import { RelationGenericInputComponent } from './dynamic-form-inputs/relation-form-input/generic/relation-generic-input.component';
 import { RelationProductVariantInputComponent } from './dynamic-form-inputs/relation-form-input/product-variant/relation-product-variant-input.component';
 import { RelationProductInputComponent } from './dynamic-form-inputs/relation-form-input/product/relation-product-input.component';
 import {
@@ -231,6 +233,7 @@ const DECLARATIONS = [
     TabbedCustomFieldsComponent,
     UiExtensionPointComponent,
     CustomDetailComponentHostComponent,
+    AssetPreviewLinksComponent,
 ];
 
 const DYNAMIC_FORM_INPUTS = [
@@ -251,6 +254,7 @@ const DYNAMIC_FORM_INPUTS = [
     RelationCardPreviewDirective,
     RelationCardDetailDirective,
     RelationSelectorDialogComponent,
+    RelationGenericInputComponent,
     TextareaFormInputComponent,
     RichTextFormInputComponent,
     JsonEditorFormInputComponent,

+ 10 - 0
packages/admin-ui/src/lib/customer/src/components/address-card/address-card.component.html

@@ -48,6 +48,16 @@
                     >
                         {{ 'customer.set-as-default-billing-address' | translate }}
                     </button>
+                    <div class="dropdown-divider"></div>
+                    <button
+                        type="button"
+                        class="delete-button"
+                        (click)="delete()"
+                        vdrDropdownItem
+                    >
+                        <clr-icon shape="trash" class="is-danger"></clr-icon>
+                        {{ 'common.delete' | translate }}
+                    </button>
                 </vdr-dropdown-menu>
             </vdr-dropdown>
         </ng-container>

+ 6 - 0
packages/admin-ui/src/lib/customer/src/components/address-card/address-card.component.ts

@@ -31,6 +31,7 @@ export class AddressCardComponent implements OnInit, OnChanges {
     @Input() editable = true;
     @Output() setAsDefaultShipping = new EventEmitter<string>();
     @Output() setAsDefaultBilling = new EventEmitter<string>();
+    @Output() deleteAddress = new EventEmitter<string>();
     private dataDependenciesPopulated = new BehaviorSubject<boolean>(false);
 
     constructor(private modalService: ModalService, private changeDetector: ChangeDetectorRef) {}
@@ -75,6 +76,11 @@ export class AddressCardComponent implements OnInit, OnChanges {
         this.addressForm.markAsDirty();
     }
 
+    delete() {
+        this.deleteAddress.emit(this.addressForm.value.id);
+        this.addressForm.markAsDirty();
+    }
+
     editAddress() {
         this.modalService
             .fromComponent(AddressDetailDialogComponent, {

+ 3 - 1
packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.html

@@ -118,14 +118,16 @@
         <h3>{{ 'customer.addresses' | translate }}</h3>
         <vdr-address-card
             *ngFor="let addressForm of getAddressFormControls()"
+            [class.to-delete]="addressesToDeleteIds.has(addressForm.value.id)"
             [availableCountries]="availableCountries$ | async"
             [isDefaultBilling]="defaultBillingAddressId === addressForm.value.id"
             [isDefaultShipping]="defaultShippingAddressId === addressForm.value.id"
             [addressForm]="addressForm"
             [customFields]="addressCustomFields"
-            [editable]="['UpdateCustomer'] | hasPermission"
+            [editable]="(['UpdateCustomer'] | hasPermission) && !addressesToDeleteIds.has(addressForm.value.id)"
             (setAsDefaultBilling)="setDefaultBillingAddressId($event)"
             (setAsDefaultShipping)="setDefaultShippingAddressId($event)"
+            (deleteAddress)="toggleDeleteAddress($event)"
         ></vdr-address-card>
         <button class="btn btn-secondary" (click)="addAddress()" *vdrIfPermissions="'UpdateCustomer'">
             <clr-icon shape="plus"></clr-icon>

+ 4 - 0
packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.scss

@@ -3,3 +3,7 @@
     margin-left: 6px;
     color: var(--color-grey-500);
 }
+
+.to-delete {
+    opacity: 0.5;
+}

+ 41 - 16
packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.ts

@@ -11,6 +11,7 @@ import {
     Customer,
     CustomFieldConfig,
     DataService,
+    DeleteCustomerAddress,
     EditNoteDialogComponent,
     GetAvailableCountries,
     GetCustomer,
@@ -27,7 +28,7 @@ import {
     UpdateCustomerInput,
     UpdateCustomerMutation,
 } from '@vendure/admin-ui/core';
-import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
+import { assertNever, notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { EMPTY, forkJoin, from, Observable, Subject } from 'rxjs';
 import {
     concatMap,
@@ -65,6 +66,7 @@ export class CustomerDetailComponent
     fetchHistory = new Subject<void>();
     defaultShippingAddressId: string;
     defaultBillingAddressId: string;
+    addressesToDeleteIds = new Set<string>();
     addressDefaultsUpdated = false;
     ordersPerPage = 10;
     currentOrdersPage = 1;
@@ -144,6 +146,14 @@ export class CustomerDetailComponent
         this.addressDefaultsUpdated = true;
     }
 
+    toggleDeleteAddress(id: string) {
+        if (this.addressesToDeleteIds.has(id)) {
+            this.addressesToDeleteIds.delete(id);
+        } else {
+            this.addressesToDeleteIds.add(id);
+        }
+    }
+
     addAddress() {
         const addressFormArray = this.detailForm.get('addresses') as FormArray;
         const newAddress = this.formBuilder.group({
@@ -231,6 +241,7 @@ export class CustomerDetailComponent
                             | UpdateCustomer.UpdateCustomer
                             | CreateCustomerAddress.CreateCustomerAddress
                             | UpdateCustomerAddress.UpdateCustomerAddress
+                            | DeleteCustomerAddress.DeleteCustomerAddress
                         >
                     > = [];
                     const customerForm = this.detailForm.get('customer');
@@ -278,14 +289,22 @@ export class CustomerDetailComponent
                                             .pipe(map(res => res.createCustomerAddress)),
                                     );
                                 } else {
-                                    saveOperations.push(
-                                        this.dataService.customer
-                                            .updateCustomerAddress({
-                                                ...input,
-                                                id: address.id,
-                                            })
-                                            .pipe(map(res => res.updateCustomerAddress)),
-                                    );
+                                    if (this.addressesToDeleteIds.has(address.id)) {
+                                        saveOperations.push(
+                                            this.dataService.customer
+                                                .deleteCustomerAddress(address.id)
+                                                .pipe(map(res => res.deleteCustomerAddress)),
+                                        );
+                                    } else {
+                                        saveOperations.push(
+                                            this.dataService.customer
+                                                .updateCustomerAddress({
+                                                    ...input,
+                                                    id: address.id,
+                                                })
+                                                .pipe(map(res => res.updateCustomerAddress)),
+                                        );
+                                    }
                                 }
                             }
                         }
@@ -295,17 +314,23 @@ export class CustomerDetailComponent
             )
             .subscribe(
                 data => {
+                    let notified = false;
                     for (const result of data) {
                         switch (result.__typename) {
                             case 'Customer':
                             case 'Address':
-                                this.notificationService.success(_('common.notify-update-success'), {
-                                    entity: 'Customer',
-                                });
-                                this.detailForm.markAsPristine();
-                                this.addressDefaultsUpdated = false;
-                                this.changeDetector.markForCheck();
-                                this.fetchHistory.next();
+                            case 'Success':
+                                if (!notified) {
+                                    this.notificationService.success(_('common.notify-update-success'), {
+                                        entity: 'Customer',
+                                    });
+                                    notified = true;
+                                    this.detailForm.markAsPristine();
+                                    this.addressDefaultsUpdated = false;
+                                    this.changeDetector.markForCheck();
+                                    this.fetchHistory.next();
+                                    this.dataService.customer.getCustomer(this.id).single$.subscribe();
+                                }
                                 break;
                             case 'EmailAddressConflictError':
                                 this.notificationService.error(result.message);

+ 2 - 9
packages/admin-ui/src/lib/customer/src/components/customer-list/customer-list.component.html

@@ -3,15 +3,8 @@
         <input
             type="text"
             name="emailSearchTerm"
-            [formControl]="emailSearchTerm"
-            [placeholder]="'customer.search-customers-by-email' | translate"
-            class="search-input ml3"
-        />
-        <input
-            type="text"
-            name="lastNameSearchTerm"
-            [formControl]="lastNameSearchTerm"
-            [placeholder]="'customer.search-customers-by-last-name' | translate"
+            [formControl]="searchTerm"
+            [placeholder]="'customer.search-customers-by-email-last-name-postal-code' | translate"
             class="search-input ml3"
         />
     </vdr-ab-left>

+ 12 - 7
packages/admin-ui/src/lib/customer/src/components/customer-list/customer-list.component.ts

@@ -6,11 +6,12 @@ import {
     BaseListComponent,
     DataService,
     GetCustomerList,
+    LogicalOperator,
     ModalService,
     NotificationService,
 } from '@vendure/admin-ui/core';
 import { SortOrder } from '@vendure/common/lib/generated-shop-types';
-import { EMPTY, merge } from 'rxjs';
+import { EMPTY } from 'rxjs';
 import { debounceTime, filter, switchMap, takeUntil } from 'rxjs/operators';
 
 @Component({
@@ -20,9 +21,9 @@ import { debounceTime, filter, switchMap, takeUntil } from 'rxjs/operators';
 })
 export class CustomerListComponent
     extends BaseListComponent<GetCustomerList.Query, GetCustomerList.Items>
-    implements OnInit {
-    emailSearchTerm = new FormControl('');
-    lastNameSearchTerm = new FormControl('');
+    implements OnInit
+{
+    searchTerm = new FormControl('');
     constructor(
         private dataService: DataService,
         router: Router,
@@ -40,12 +41,16 @@ export class CustomerListComponent
                     take,
                     filter: {
                         emailAddress: {
-                            contains: this.emailSearchTerm.value,
+                            contains: this.searchTerm.value,
                         },
                         lastName: {
-                            contains: this.lastNameSearchTerm.value,
+                            contains: this.searchTerm.value,
+                        },
+                        postalCode: {
+                            contains: this.searchTerm.value,
                         },
                     },
+                    filterOperator: LogicalOperator.OR,
                     sort: {
                         createdAt: SortOrder.DESC,
                     },
@@ -56,7 +61,7 @@ export class CustomerListComponent
 
     ngOnInit() {
         super.ngOnInit();
-        merge(this.emailSearchTerm.valueChanges, this.lastNameSearchTerm.valueChanges)
+        this.searchTerm.valueChanges
             .pipe(
                 filter(value => 2 < value.length || value.length === 0),
                 debounceTime(250),

+ 28 - 1
packages/admin-ui/src/lib/order/src/components/cancel-order-dialog/cancel-order-dialog.component.html

@@ -16,10 +16,11 @@
             <tr
                 *ngFor="let line of order.lines"
                 class="order-line"
+                [class.is-disabled]="cancelAll"
                 [class.is-cancelled]="line.quantity === 0"
             >
                 <td class="align-middle thumb">
-                    <img [src]="line.featuredAsset | assetPreview:'tiny'" />
+                    <img [src]="line.featuredAsset | assetPreview: 'tiny'" />
                 </td>
                 <td class="align-middle name">{{ line.productVariant.name }}</td>
                 <td class="align-middle sku">{{ line.productVariant.sku }}</td>
@@ -31,6 +32,8 @@
                     <input
                         *ngIf="line.quantity > 0 && !order.active; else nonEditable"
                         [(ngModel)]="lineQuantities[line.id]"
+                        (input)="checkIfAllSelected()"
+                        [disabled]="cancelAll"
                         type="number"
                         [max]="line.quantity"
                         min="0"
@@ -41,6 +44,30 @@
         </table>
     </div>
     <div class="cancellation-details">
+        <ng-container *ngIf="order.active !== true">
+            <clr-radio-wrapper>
+                <input
+                    type="radio"
+                    clrRadio
+                    [value]="true"
+                    [(ngModel)]="cancelAll"
+                    name="options"
+                    (ngModelChange)="radioChanged()"
+                />
+                <label>{{ 'order.cancel-entire-order' | translate }}</label>
+            </clr-radio-wrapper>
+            <clr-radio-wrapper>
+                <input
+                    type="radio"
+                    clrRadio
+                    [value]="false"
+                    [(ngModel)]="cancelAll"
+                    name="options"
+                    (ngModelChange)="radioChanged()"
+                />
+                <label>{{ 'order.cancel-specified-items' | translate }}</label>
+            </clr-radio-wrapper>
+        </ng-container>
         <label class="clr-control-label">{{ 'order.cancellation-reason' | translate }}</label>
         <ng-select
             [items]="reasons"

+ 3 - 0
packages/admin-ui/src/lib/order/src/components/cancel-order-dialog/cancel-order-dialog.component.scss

@@ -33,4 +33,7 @@
         text-decoration: line-through;
         background-color: var(--color-component-bg-200);
     }
+    .is-disabled td, .is-disabled td input {
+        background-color: var(--color-component-bg-200);
+    }
 }

+ 39 - 3
packages/admin-ui/src/lib/order/src/components/cancel-order-dialog/cancel-order-dialog.component.ts

@@ -1,6 +1,13 @@
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import { CancelOrderInput, Dialog, I18nService, OrderDetailFragment, OrderLineInput } from '@vendure/admin-ui/core';
+import {
+    CancelOrderInput,
+    Dialog,
+    getAppConfig,
+    I18nService,
+    OrderDetailFragment,
+    OrderLineInput,
+} from '@vendure/admin-ui/core';
 
 @Component({
     selector: 'vdr-cancel-order-dialog',
@@ -10,10 +17,14 @@ import { CancelOrderInput, Dialog, I18nService, OrderDetailFragment, OrderLineIn
 })
 export class CancelOrderDialogComponent implements OnInit, Dialog<CancelOrderInput> {
     order: OrderDetailFragment;
+    cancelAll = true;
     resolveWith: (result?: CancelOrderInput) => void;
     reason: string;
     lineQuantities: { [lineId: string]: number } = {};
-    reasons: string[] = [_('order.cancel-reason-customer-request'), _('order.cancel-reason-not-available')];
+    reasons: string[] = getAppConfig().cancellationReasons ?? [
+        _('order.cancel-reason-customer-request'),
+        _('order.cancel-reason-not-available'),
+    ];
 
     get selectionCount(): number {
         return Object.values(this.lineQuantities).reduce((sum, n) => sum + n, 0);
@@ -25,15 +36,40 @@ export class CancelOrderDialogComponent implements OnInit, Dialog<CancelOrderInp
 
     ngOnInit() {
         this.lineQuantities = this.order.lines.reduce((result, line) => {
-            return { ...result, [line.id]: 0 };
+            return { ...result, [line.id]: line.quantity };
         }, {});
     }
 
+    radioChanged() {
+        if (this.cancelAll) {
+            for (const line of this.order.lines) {
+                this.lineQuantities[line.id] = line.quantity;
+            }
+        } else {
+            for (const line of this.order.lines) {
+                this.lineQuantities[line.id] = 0;
+            }
+        }
+    }
+
+    checkIfAllSelected() {
+        for (const [lineId, quantity] of Object.entries(this.lineQuantities)) {
+            const quantityInOrder = this.order.lines.find(line => line.id === lineId)?.quantity;
+            if (quantityInOrder && quantity < quantityInOrder) {
+                return;
+            }
+        }
+        // If we got here, all of the selected quantities are equal to the order
+        // line quantities, i.e. everything is selected.
+        this.cancelAll = true;
+    }
+
     select() {
         this.resolveWith({
             orderId: this.order.id,
             lines: this.getLineInputs(),
             reason: this.reason,
+            cancelShipping: this.cancelAll,
         });
     }
 

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

@@ -231,7 +231,7 @@ export class OrderDetailComponent
     canAddFulfillment(order: OrderDetail.Fragment): boolean {
         const allItemsFulfilled = order.lines
             .reduce((items, line) => [...items, ...line.items], [] as OrderLineFragment['items'])
-            .every(item => !!item.fulfillment);
+            .every(item => !!item.fulfillment || item.cancelled);
         return (
             !allItemsFulfilled &&
             !this.hasUnsettledModifications(order) &&

+ 22 - 0
packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.html

@@ -224,6 +224,28 @@
                         </button>
                     </clr-accordion-content>
                 </clr-accordion-panel>
+                <clr-accordion-panel>
+                    <clr-accordion-title>{{ 'order.set-coupon-codes' | translate }}</clr-accordion-title>
+                    <clr-accordion-content *clrIfExpanded>
+                        <ng-select
+                            [items]="availableCouponCodes$ | async"
+                            appendTo="body"
+                            bindLabel="code"
+                            bindValue="code"
+                            [addTag]="false"
+                            [multiple]="true"
+                            [hideSelected]="true"
+                            [minTermLength]="2"
+                            typeToSearchText=""
+                            [typeahead]="couponCodeInput$"
+                            [formControl]="couponCodesControl"
+                        >
+                            <ng-template ng-option-tmp let-item="item">
+                                <vdr-chip>{{ item.code }}</vdr-chip> {{ item.promotionName }}
+                            </ng-template>
+                        </ng-select>
+                    </clr-accordion-content>
+                </clr-accordion-panel>
 
                 <clr-accordion-panel>
                     <clr-accordion-title>{{ 'order.add-surcharge' | translate }}</clr-accordion-title>

+ 42 - 5
packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.ts

@@ -22,8 +22,16 @@ import {
     SurchargeInput,
 } from '@vendure/admin-ui/core';
 import { assertNever, notNullOrUndefined } from '@vendure/common/lib/shared-utils';
-import { EMPTY, Observable, of } from 'rxjs';
-import { mapTo, shareReplay, switchMap, takeUntil } from 'rxjs/operators';
+import { concat, EMPTY, Observable, of, Subject } from 'rxjs';
+import {
+    distinctUntilChanged,
+    map,
+    mapTo,
+    shareReplay,
+    startWith,
+    switchMap,
+    takeUntil,
+} from 'rxjs/operators';
 
 import { OrderTransitionService } from '../../providers/order-transition.service';
 import {
@@ -54,10 +62,14 @@ type ModifyOrderData = Omit<ModifyOrderInput, 'addItems' | 'adjustOrderLines'> &
 })
 export class OrderEditorComponent
     extends BaseDetailComponent<OrderDetail.Fragment>
-    implements OnInit, OnDestroy {
+    implements OnInit, OnDestroy
+{
     availableCountries$: Observable<GetAvailableCountries.Items[]>;
+    availableCouponCodes$: Observable<Array<{ code: string; promotionName: string }>>;
+    couponCodeInput$ = new Subject<string>();
     addressCustomFields: CustomFieldConfig[];
     detailForm = new FormGroup({});
+    couponCodesControl = new FormControl();
     orderLineCustomFieldsFormArray: FormArray;
     addItemCustomFieldsFormArray: FormArray;
     addItemCustomFieldsForm: FormGroup;
@@ -114,10 +126,14 @@ export class OrderEditorComponent
 
     ngOnInit(): void {
         this.init();
+        this.dataService.promotion.getPromotions();
         this.addressCustomFields = this.getCustomFieldConfig('Address');
         this.modifyOrderInput.orderId = this.route.snapshot.paramMap.get('id') as string;
         this.orderLineCustomFields = this.getCustomFieldConfig('OrderLine');
         this.entity$.pipe(takeUntil(this.destroy$)).subscribe(order => {
+            if (order.couponCodes.length) {
+                this.couponCodesControl.setValue(order.couponCodes);
+            }
             this.surchargeForm = new FormGroup({
                 description: new FormControl('', Validators.required),
                 sku: new FormControl(''),
@@ -178,6 +194,22 @@ export class OrderEditorComponent
                 this.orderLineCustomFieldsFormArray.push(formGroup);
             }
         });
+        this.availableCouponCodes$ = concat(
+            this.couponCodeInput$.pipe(
+                distinctUntilChanged(),
+                switchMap(
+                    term =>
+                        this.dataService.promotion.getPromotions(10, 0, {
+                            couponCode: { contains: term },
+                        }).single$,
+                ),
+                map(({ promotions }) =>
+                    // tslint:disable-next-line:no-non-null-assertion
+                    promotions.items.map(p => ({ code: p.couponCode!, promotionName: p.name })),
+                ),
+                startWith([]),
+            ),
+        );
         this.addItemCustomFieldsFormArray = new FormArray([]);
         this.addItemCustomFieldsForm = new FormGroup({});
         for (const customField of this.orderLineCustomFields) {
@@ -219,7 +251,8 @@ export class OrderEditorComponent
             !!surcharges?.length ||
             !!adjustOrderLines?.length ||
             (this.shippingAddressForm.dirty && this.shippingAddressForm.valid) ||
-            (this.billingAddressForm.dirty && this.billingAddressForm.valid)
+            (this.billingAddressForm.dirty && this.billingAddressForm.valid) ||
+            this.couponCodesControl.dirty
         );
     }
 
@@ -352,6 +385,7 @@ export class OrderEditorComponent
                 ? { updateShippingAddress: this.shippingAddressForm.value }
                 : {}),
             dryRun: true,
+            couponCodes: this.couponCodesControl.dirty ? this.couponCodesControl.value : undefined,
             note: this.note ?? '',
             options: {
                 recalculateShipping: this.recalculateShipping,
@@ -380,7 +414,10 @@ export class OrderEditorComponent
                         case 'OrderLimitError':
                         case 'OrderModificationStateError':
                         case 'PaymentMethodMissingError':
-                        case 'RefundPaymentIdMissingError': {
+                        case 'RefundPaymentIdMissingError':
+                        case 'CouponCodeLimitError':
+                        case 'CouponCodeExpiredError':
+                        case 'CouponCodeInvalidError': {
                             this.notificationService.error(modifyOrder.message);
                             return of(false as const);
                         }

+ 3 - 0
packages/admin-ui/src/lib/order/src/components/order-history/order-history.component.html

@@ -106,6 +106,9 @@
                     <vdr-labeled-data [label]="'order.contents' | translate">
                         <vdr-simple-item-list [items]="items"></vdr-simple-item-list>
                     </vdr-labeled-data>
+                    <vdr-labeled-data [label]="'order.shipping-cancelled' | translate">
+                        {{ entry.data.shippingCancelled }}
+                    </vdr-labeled-data>
                 </vdr-history-entry-detail>
             </ng-container>
             <ng-container *ngSwitchCase="type.ORDER_FULFILLMENT">

+ 4 - 2
packages/admin-ui/src/lib/order/src/components/order-table/order-table.component.html

@@ -84,7 +84,8 @@
                 </div>
             </td>
         </tr>
-        <tr class="order-adjustment" *ngFor="let discount of order.discounts">
+        <ng-container *ngFor="let discount of order.discounts">
+            <tr class="order-adjustment" *ngIf="discount.type !== 'OTHER'">
             <td colspan="5" class="left clr-align-middle">
                 <a [routerLink]="getPromotionLink(discount)">{{ discount.description }}</a>
                 <vdr-chip *ngIf="getCouponCodeForAdjustment(order, discount) as couponCode">{{
@@ -97,7 +98,8 @@
                     {{ discount.amount | localeCurrency: order.currencyCode }}
                 </div>
             </td>
-        </tr>
+            </tr>
+        </ng-container>
         <tr class="sub-total">
             <td class="left clr-align-middle">{{ 'order.sub-total' | translate }}</td>
             <td colspan="4"></td>

+ 2 - 2
packages/admin-ui/src/lib/order/src/components/refund-order-dialog/refund-order-dialog.component.html

@@ -53,7 +53,7 @@
                         </ng-container>
                     </div>
                 </td>
-                <td class="align-middle fulfil">
+                <td class="align-middle quantity-col">
                     <input
                         *ngIf="lineCanBeRefundedOrCancelled(line)"
                         [(ngModel)]="lineQuantities[line.id].quantity"
@@ -88,7 +88,7 @@
             </tr>
         </table>
     </div>
-    <div class="refund-details mt4">
+    <div class="refund-details mt4" [class.faded]="!isRefunding() && !isCancelling()">
         <div>
             <label class="clr-control-label">{{ 'order.refund-cancellation-reason' | translate }}</label>
             <ng-select

+ 6 - 0
packages/admin-ui/src/lib/order/src/components/refund-order-dialog/refund-order-dialog.component.scss

@@ -18,6 +18,9 @@
         color: var(--color-grey-300);
     }
 }
+.quantity-col {
+    background-color: var(--color-warning-100);
+}
 .cancel-checkbox-wrapper {
     display: flex;
     align-items: center;
@@ -31,6 +34,9 @@ clr-checkbox-wrapper {
 .refund-details {
     display: flex;
     justify-content: space-between;
+    &.faded {
+        opacity: 0.5;
+    }
 }
 .totals {
     margin-top: 48px;

+ 8 - 2
packages/admin-ui/src/lib/order/src/components/refund-order-dialog/refund-order-dialog.component.ts

@@ -3,6 +3,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
     CancelOrderInput,
     Dialog,
+    getAppConfig,
     I18nService,
     OrderDetail,
     OrderDetailFragment,
@@ -20,7 +21,8 @@ type SelectionLine = { quantity: number; refund: boolean; cancel: boolean };
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class RefundOrderDialogComponent
-    implements OnInit, Dialog<{ cancel: CancelOrderInput; refund: RefundOrderInput }> {
+    implements OnInit, Dialog<{ cancel: CancelOrderInput; refund: RefundOrderInput }>
+{
     order: OrderDetailFragment;
     resolveWith: (result?: { cancel: CancelOrderInput; refund: RefundOrderInput }) => void;
     reason: string;
@@ -29,7 +31,10 @@ export class RefundOrderDialogComponent
     lineQuantities: { [lineId: string]: SelectionLine } = {};
     refundShipping = false;
     adjustment = 0;
-    reasons: string[] = [_('order.refund-reason-customer-request'), _('order.refund-reason-not-available')];
+    reasons = getAppConfig().cancellationReasons ?? [
+        _('order.refund-reason-customer-request'),
+        _('order.refund-reason-not-available'),
+    ];
 
     constructor(private i18nService: I18nService) {
         this.reasons = this.reasons.map(r => this.i18nService.translate(r));
@@ -147,6 +152,7 @@ export class RefundOrderDialogComponent
                     lines: cancelLines,
                     orderId: this.order.id,
                     reason: this.reason,
+                    cancelShipping: this.refundShipping,
                 },
             });
         }

+ 7 - 1
packages/admin-ui/src/lib/static/i18n-messages/cs.json

@@ -55,6 +55,7 @@
     "add-facet-value": "Přidat hodnotu atributu",
     "add-facets": "Přidat atribut",
     "add-option": "Přidat možnost",
+    "asset-preview-links": "",
     "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",
@@ -238,6 +239,7 @@
     "sample-formatting": "",
     "select": "Vybrat...",
     "select-display-language": "Vyberte jazyk",
+    "select-relation-id": "",
     "select-today": "Vybrat dnešní datum",
     "set-language": "",
     "short-date": "",
@@ -309,7 +311,7 @@
     "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",
-    "search-customers-by-last-name": "",
+    "search-customers-by-email-last-name-postal-code": "",
     "select-customer": "",
     "set-as-default-billing-address": "Nastavit jako výchozí fakturační adresu",
     "set-as-default-shipping-address": "Nastavit jako výchozí dodací adresu",
@@ -436,6 +438,7 @@
     "arrange-additional-payment": "",
     "billing-address": "Fakturační adresa",
     "cancel": "Zrušit",
+    "cancel-entire-order": "",
     "cancel-fulfillment": "Zrušit zpracování",
     "cancel-modification": "Zrušit úpravy",
     "cancel-order": "Zrušit objednávku",
@@ -443,6 +446,7 @@
     "cancel-reason-customer-request": "Požadavek zákazníka",
     "cancel-reason-not-available": "Neuveden",
     "cancel-selected-items": "Zrušit vybrané položky",
+    "cancel-specified-items": "",
     "cancellation-reason": "Důvod zrušení",
     "cancelled-order-success": "Objednávka úspěšně zrušena",
     "confirm-modifications": "Potvrdit úpravy",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "",
     "search-by-order-code": "Hledat na základě kódu objednávky",
     "select-state": "Vyberte stav",
+    "set-coupon-codes": "",
     "set-fulfillment-state": "Označit jako {state}",
     "settle-payment": "Vypořádání platby",
     "settle-payment-error": "Nelze vyřídit platbu",
@@ -547,6 +552,7 @@
     "settle-refund-success": "Úspěšně vypořádana refundace",
     "shipping": "Dodání",
     "shipping-address": "Dodací adresa",
+    "shipping-cancelled": "",
     "shipping-method": "Dodací metoda",
     "state": "Stav",
     "sub-total": "Mezisoučet",

+ 7 - 1
packages/admin-ui/src/lib/static/i18n-messages/de.json

@@ -55,6 +55,7 @@
     "add-facet-value": "Facettenwert hinzufügen",
     "add-facets": "Facetten hinzufügen",
     "add-option": "Option hinzufügen",
+    "asset-preview-links": "",
     "assign-product-to-channel-success": "Produkt erfolgreich an \"{ channel }\" zugewiesen",
     "assign-products-to-channel": "Produkte dem Kanal zuweisen",
     "assign-to-channel": "Zuweisung an Kanal",
@@ -238,6 +239,7 @@
     "sample-formatting": "",
     "select": "Auswählen...",
     "select-display-language": "Anzeigesprache wählen",
+    "select-relation-id": "",
     "select-today": "Heute auswählen",
     "set-language": "",
     "short-date": "",
@@ -309,7 +311,7 @@
     "remove-customers-from-group-success": "{customerCount, plural, one {Kunde} other {{customerCount} Kunden}} aus \"{ groupName }\" entfernt",
     "remove-from-group": "Aus dieser Gruppe entfernen",
     "search-customers-by-email": "Suche nach E-Mail-Adresse",
-    "search-customers-by-last-name": "",
+    "search-customers-by-email-last-name-postal-code": "",
     "select-customer": "Kunde auswählen",
     "set-as-default-billing-address": "Als Standard-Rechnungsadresse festlegen",
     "set-as-default-shipping-address": "Als Standard-Versandadresse festlegen",
@@ -436,6 +438,7 @@
     "arrange-additional-payment": "Zusätzliche Zahlung veranlassen",
     "billing-address": "Rechnungsadresse",
     "cancel": "Abbrechen",
+    "cancel-entire-order": "",
     "cancel-fulfillment": "Ausführung abbrechen",
     "cancel-modification": "Änderungen verwerfen",
     "cancel-order": "Bestellung stornieren",
@@ -443,6 +446,7 @@
     "cancel-reason-customer-request": "Kundenanfrage",
     "cancel-reason-not-available": "Nicht verfügbar",
     "cancel-selected-items": "Auswahl aufheben",
+    "cancel-specified-items": "",
     "cancellation-reason": "Stornierungsgrund",
     "cancelled-order-success": "Bestellung erfolgreich storniert",
     "confirm-modifications": "Änderungen bestätigen",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "",
     "search-by-order-code": "Suche nach Bestellnummer",
     "select-state": "Status auswählen",
+    "set-coupon-codes": "",
     "set-fulfillment-state": "Abwicklungsstatus wählen",
     "settle-payment": "Zahlung durchführen",
     "settle-payment-error": "Die Zahlung konnte nicht durchgeführt werden",
@@ -547,6 +552,7 @@
     "settle-refund-success": "Rückzahlung erfolgreich durchgeführt",
     "shipping": "Versand",
     "shipping-address": "Lieferadresse",
+    "shipping-cancelled": "",
     "shipping-method": "Versandart",
     "state": "Status",
     "sub-total": "Zwischensumme",

+ 9 - 3
packages/admin-ui/src/lib/static/i18n-messages/en.json

@@ -55,6 +55,7 @@
     "add-facet-value": "Add facet value",
     "add-facets": "Add facets",
     "add-option": "Add option",
+    "asset-preview-links": "Asset preview links",
     "assign-product-to-channel-success": "Successfully assigned product to \"{ channel }\"",
     "assign-products-to-channel": "Assign products to channel",
     "assign-to-channel": "Assign to channel",
@@ -238,6 +239,7 @@
     "sample-formatting": "Sample formatting",
     "select": "Select...",
     "select-display-language": "Select display language",
+    "select-relation-id": "Select relation ID",
     "select-today": "Select today",
     "set-language": "Set language",
     "short-date": "Short date",
@@ -309,7 +311,7 @@
     "remove-customers-from-group-success": "Removed {customerCount, plural, one {1 customer} other {{customerCount} customers}} from \"{ groupName }\"",
     "remove-from-group": "Remove from this group",
     "search-customers-by-email": "Search by email address",
-    "search-customers-by-last-name": "Search by last name",
+    "search-customers-by-email-last-name-postal-code": "Search by email / last name / postal code",
     "select-customer": "Select customer",
     "set-as-default-billing-address": "Set as default billing",
     "set-as-default-shipping-address": "Set as default shipping",
@@ -436,13 +438,15 @@
     "arrange-additional-payment": "Arrange additional payment",
     "billing-address": "Billing address",
     "cancel": "Cancel",
+    "cancel-entire-order": "Cancel entire order",
     "cancel-fulfillment": "Cancel fulfillment",
     "cancel-modification": "Cancel modification",
-    "cancel-order": "Cancel order",
+    "cancel-order": "Cancel order or items",
     "cancel-payment": "Cancel payment",
     "cancel-reason-customer-request": "Customer request",
     "cancel-reason-not-available": "Not available",
     "cancel-selected-items": "Cancel selected items",
+    "cancel-specified-items": "Cancel specified items",
     "cancellation-reason": "Cancellation reason",
     "cancelled-order-success": "Successfully cancelled order",
     "confirm-modifications": "Confirm modifications",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "Search by customer last name",
     "search-by-order-code": "Search by order code",
     "select-state": "Select state",
+    "set-coupon-codes": "Set coupon codes",
     "set-fulfillment-state": "Mark as {state}",
     "settle-payment": "Settle payment",
     "settle-payment-error": "Could not settle payment",
@@ -547,6 +552,7 @@
     "settle-refund-success": "Successfully settled refund",
     "shipping": "Shipping",
     "shipping-address": "Shipping address",
+    "shipping-cancelled": "Shipping cancelled",
     "shipping-method": "Shipping method",
     "state": "State",
     "sub-total": "Sub total",
@@ -665,4 +671,4 @@
     "job-result": "Job result",
     "job-state": "Job state"
   }
-}
+}

+ 7 - 1
packages/admin-ui/src/lib/static/i18n-messages/es.json

@@ -55,6 +55,7 @@
     "add-facet-value": "Añadir valor de faceta",
     "add-facets": "Añadir facetas",
     "add-option": "Añadir opción",
+    "asset-preview-links": "",
     "assign-product-to-channel-success": "Producto asignado a \"{ channel }\" con éxito",
     "assign-products-to-channel": "Asignar productos a canal de ventas",
     "assign-to-channel": "Asignar a canal de ventas",
@@ -238,6 +239,7 @@
     "sample-formatting": "",
     "select": "Seleccionar...",
     "select-display-language": "Seleccionar idioma de interfaz",
+    "select-relation-id": "",
     "select-today": "Hoy",
     "set-language": "",
     "short-date": "",
@@ -309,7 +311,7 @@
     "remove-customers-from-group-success": "{customerCount, plural, one {1 cliente} other {{customerCount} clientes}} {customerCount, plural, one {eliminado} other {eliminados}} de \"{ groupName }\"",
     "remove-from-group": "Eliminar del grupo",
     "search-customers-by-email": "Buscar por correo electrónico",
-    "search-customers-by-last-name": "Buscar por apellido",
+    "search-customers-by-email-last-name-postal-code": "Buscar por apellido / correo electrónico / Código postal",
     "select-customer": "Seleccionar cliente",
     "set-as-default-billing-address": "Fijar como dirección de facturación por defecto",
     "set-as-default-shipping-address": "Fijar como dirección de envío por defecto",
@@ -436,6 +438,7 @@
     "arrange-additional-payment": "Ordenar pagos adicionales",
     "billing-address": "Dirección de facturación",
     "cancel": "Cancelar",
+    "cancel-entire-order": "",
     "cancel-fulfillment": "Cancelar orden",
     "cancel-modification": "Cancelar modificación",
     "cancel-order": "Cancelar pedido",
@@ -443,6 +446,7 @@
     "cancel-reason-customer-request": "Solicitud del cliente",
     "cancel-reason-not-available": "No disponible",
     "cancel-selected-items": "Cancelar artículos seleccionados",
+    "cancel-specified-items": "",
     "cancellation-reason": "Motivo de la cancelación",
     "cancelled-order-success": "Pedido cancelado con éxito",
     "confirm-modifications": "Confirmar modificaciones",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "Buscar por el apellido del cliente",
     "search-by-order-code": "Buscar por el código de pedido",
     "select-state": "Seleccionar estado",
+    "set-coupon-codes": "",
     "set-fulfillment-state": "Fijar como {state}",
     "settle-payment": "Liquidar pago",
     "settle-payment-error": "No pudo liquidar el pago",
@@ -547,6 +552,7 @@
     "settle-refund-success": "Reembolso liquidado con éxito",
     "shipping": "Envío",
     "shipping-address": "Dirección de envío",
+    "shipping-cancelled": "",
     "shipping-method": "Método de envío",
     "state": "Estado",
     "sub-total": "Sub total",

+ 7 - 1
packages/admin-ui/src/lib/static/i18n-messages/fr.json

@@ -55,6 +55,7 @@
     "add-facet-value": "Ajout valeur du composant",
     "add-facets": "Ajout composant",
     "add-option": "Ajout option",
+    "asset-preview-links": "",
     "assign-product-to-channel-success": "produit attribué au canal \"{ channel }\"",
     "assign-products-to-channel": "Attribuer les produits au canal",
     "assign-to-channel": "Attribuer au canal",
@@ -238,6 +239,7 @@
     "sample-formatting": "",
     "select": "Selectionner...",
     "select-display-language": "Choisir la langue d'affichage",
+    "select-relation-id": "",
     "select-today": "Choisir aujourd'hui",
     "set-language": "",
     "short-date": "",
@@ -309,7 +311,7 @@
     "remove-customers-from-group-success": "Retrait {customerCount, plural, one {d'un client} other {de {customerCount} clients}} de \"{ groupName }\"",
     "remove-from-group": "Retirer de ce groupe",
     "search-customers-by-email": "Chercher par adresse email",
-    "search-customers-by-last-name": "Rechercher par nom",
+    "search-customers-by-email-last-name-postal-code": "Rechercher par nom / adresse email / code postal",
     "select-customer": "Sélectionner client",
     "set-as-default-billing-address": "Etablir en tant qu'adresse de facturation par défaut",
     "set-as-default-shipping-address": "Etablir en tant qu'adresse de livraison par défaut",
@@ -436,6 +438,7 @@
     "arrange-additional-payment": "Arranger un paiment additionnel",
     "billing-address": "Adresse de facturation",
     "cancel": "Annuler",
+    "cancel-entire-order": "",
     "cancel-fulfillment": "Annuler préparation",
     "cancel-modification": "Annuler la modification",
     "cancel-order": "Annuler la commande",
@@ -443,6 +446,7 @@
     "cancel-reason-customer-request": "Demande du client",
     "cancel-reason-not-available": "Pas disponible",
     "cancel-selected-items": "Annuler les articles selectionnés",
+    "cancel-specified-items": "",
     "cancellation-reason": "Raison de l'annulation",
     "cancelled-order-success": "Commande annulée",
     "confirm-modifications": "Confirmer les modifications",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "Rechercher par nom du client",
     "search-by-order-code": "Rehercher par numéro de commande",
     "select-state": "Sélectionner un état",
+    "set-coupon-codes": "",
     "set-fulfillment-state": "Marquer {state}",
     "settle-payment": "Régler le paiement",
     "settle-payment-error": "Règlement du paiement échoué",
@@ -547,6 +552,7 @@
     "settle-refund-success": "Remboursement réglé",
     "shipping": "Expédition",
     "shipping-address": "Adresse de livraison",
+    "shipping-cancelled": "",
     "shipping-method": "Mode de livraison",
     "state": "Etat",
     "sub-total": "Sous total",

+ 7 - 1
packages/admin-ui/src/lib/static/i18n-messages/it.json

@@ -55,6 +55,7 @@
     "add-facet-value": "Aggiungi valore attributo",
     "add-facets": "Aggiungi attributi",
     "add-option": "Aggiungi opzione",
+    "asset-preview-links": "",
     "assign-product-to-channel-success": "Prodotto assegnato correttamente a \"{ channel }\"",
     "assign-products-to-channel": "Assegna prodotto al canale",
     "assign-to-channel": "Assegna a un canale",
@@ -238,6 +239,7 @@
     "sample-formatting": "",
     "select": "Seleziona...",
     "select-display-language": "Seleziona lingua",
+    "select-relation-id": "",
     "select-today": "Seleziona oggi",
     "set-language": "",
     "short-date": "",
@@ -309,7 +311,7 @@
     "remove-customers-from-group-success": "Ho rimosso {customerCount, plural, one {1 cuclientestomer} other {{customerCount} clienti}} from \"{ groupName }\"",
     "remove-from-group": "Rimuovi da questo gruppo",
     "search-customers-by-email": "Cerca per indirizzo email",
-    "search-customers-by-last-name": "Cerca per cognome",
+    "search-customers-by-email-last-name-postal-code": "Cerca per cognome / indirizzo email / CAP",
     "select-customer": "Seleziona cliente",
     "set-as-default-billing-address": "Imposta come indirizzo di fatturazione predefinito",
     "set-as-default-shipping-address": "Imposta come indirizzo di spedizione predefinito",
@@ -436,6 +438,7 @@
     "arrange-additional-payment": "Effettua un ulteriore pagamento",
     "billing-address": "Indirizzo di fatturazione",
     "cancel": "Annulla",
+    "cancel-entire-order": "",
     "cancel-fulfillment": "Annulla consegna",
     "cancel-modification": "Annulla modifica",
     "cancel-order": "Annulla ordine",
@@ -443,6 +446,7 @@
     "cancel-reason-customer-request": "Richiesta del cliente",
     "cancel-reason-not-available": "Non disponibile",
     "cancel-selected-items": "Cancella prodotti selezionati",
+    "cancel-specified-items": "",
     "cancellation-reason": "Motivo dell'annullamento",
     "cancelled-order-success": "Ordine cancellato con successo",
     "confirm-modifications": "Conferma modifiche",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "Cerca per cognome cliente",
     "search-by-order-code": "Cerca per codice ordine",
     "select-state": "Seleziona stato",
+    "set-coupon-codes": "",
     "set-fulfillment-state": "Segna come {state}",
     "settle-payment": "Incassa pagamento",
     "settle-payment-error": "Non è stato possibile incassare il pagamento",
@@ -547,6 +552,7 @@
     "settle-refund-success": "Rimborso effettuato con successo",
     "shipping": "Spedizione",
     "shipping-address": "Indirizzo di spedizione",
+    "shipping-cancelled": "",
     "shipping-method": "Metodo di spedizione",
     "state": "Stato",
     "sub-total": "Sub totale",

+ 7 - 1
packages/admin-ui/src/lib/static/i18n-messages/pl.json

@@ -55,6 +55,7 @@
     "add-facet-value": "Dodaj nazwe faseta",
     "add-facets": "Dodaj faset",
     "add-option": "Dodaj opcje",
+    "asset-preview-links": "",
     "assign-product-to-channel-success": "Pomyślnie przypisano produkt do \"{ channel }\"",
     "assign-products-to-channel": "Przypisz produkt do kanału",
     "assign-to-channel": "Przypisz do kanału",
@@ -238,6 +239,7 @@
     "sample-formatting": "",
     "select": "Wybrano...",
     "select-display-language": "Wybierz język",
+    "select-relation-id": "",
     "select-today": "Wybierz dzisiaj",
     "set-language": "",
     "short-date": "",
@@ -309,7 +311,7 @@
     "remove-customers-from-group-success": "",
     "remove-from-group": "",
     "search-customers-by-email": "Szukaj przez email",
-    "search-customers-by-last-name": "",
+    "search-customers-by-email-last-name-postal-code": "",
     "select-customer": "",
     "set-as-default-billing-address": "Ustaw jako domyślny adres rozliczeniowy",
     "set-as-default-shipping-address": "Ustaw jako domyślny adres wysyłki",
@@ -436,6 +438,7 @@
     "arrange-additional-payment": "",
     "billing-address": "",
     "cancel": "Anuluj",
+    "cancel-entire-order": "",
     "cancel-fulfillment": "",
     "cancel-modification": "",
     "cancel-order": "Anuluj zamówienie",
@@ -443,6 +446,7 @@
     "cancel-reason-customer-request": "Prośba klienta",
     "cancel-reason-not-available": "Niedostępny",
     "cancel-selected-items": "Anuluj zaznaczone",
+    "cancel-specified-items": "",
     "cancellation-reason": "Powód anulowania",
     "cancelled-order-success": "Pomyślnie anulowano zamówienie",
     "confirm-modifications": "",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "",
     "search-by-order-code": "Szukaj po numerze zamówienia",
     "select-state": "",
+    "set-coupon-codes": "",
     "set-fulfillment-state": "",
     "settle-payment": "Rozlicz płatność",
     "settle-payment-error": "Nie można rozliczyć płatności",
@@ -547,6 +552,7 @@
     "settle-refund-success": "Pomyślnie rozliczono zwrot",
     "shipping": "Wysyłka",
     "shipping-address": "Adres wysyłki",
+    "shipping-cancelled": "",
     "shipping-method": "Metoda wysyłki",
     "state": "Status",
     "sub-total": "Sub total",

+ 7 - 1
packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json

@@ -55,6 +55,7 @@
     "add-facet-value": "Adiciona valor para etiqueta",
     "add-facets": "Adiciona etiqueta",
     "add-option": "Adiciona opção",
+    "asset-preview-links": "",
     "assign-product-to-channel-success": "Produto atribuído com sucesso a \"{ channel }\"",
     "assign-products-to-channel": "Atribuir produtos ao canal",
     "assign-to-channel": "Atribuir ao canal",
@@ -238,6 +239,7 @@
     "sample-formatting": "",
     "select": "Selecione...",
     "select-display-language": "Selecionar idioma de exibição",
+    "select-relation-id": "",
     "select-today": "Selecione hoje",
     "set-language": "",
     "short-date": "",
@@ -309,7 +311,7 @@
     "remove-customers-from-group-success": "Excluído {customerCount, plural, one {1 customer} other {{customerCount} customers}} from \"{ groupName }\"",
     "remove-from-group": "Excluir deste grupo",
     "search-customers-by-email": "Busca por email",
-    "search-customers-by-last-name": "",
+    "search-customers-by-email-last-name-postal-code": "Busca por email / sobrenome / cep",
     "select-customer": "",
     "set-as-default-billing-address": "Definir como cobrança padrão",
     "set-as-default-shipping-address": "Definir como remessa padrão",
@@ -436,6 +438,7 @@
     "arrange-additional-payment": "",
     "billing-address": "Endereço de cobrança",
     "cancel": "Cancelar",
+    "cancel-entire-order": "",
     "cancel-fulfillment": "Cancelar cumprimento",
     "cancel-modification": "Cancelar modificação",
     "cancel-order": "Cancelar Pedido",
@@ -443,6 +446,7 @@
     "cancel-reason-customer-request": "Pedido do cliente",
     "cancel-reason-not-available": "Não disponível",
     "cancel-selected-items": "Cancelar itens selecionados",
+    "cancel-specified-items": "",
     "cancellation-reason": "Motivo do cancelamento",
     "cancelled-order-success": "Pedido cancelado com sucesso",
     "confirm-modifications": "Confirmar modificações",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "",
     "search-by-order-code": "Buscar por código do pedido",
     "select-state": "Selecionar estado",
+    "set-coupon-codes": "",
     "set-fulfillment-state": "Marcar como {state}",
     "settle-payment": "Liquidar pagamento",
     "settle-payment-error": "Não posso liquidar pagamento",
@@ -547,6 +552,7 @@
     "settle-refund-success": "Reembolso liquidado com sucesso",
     "shipping": "Envio",
     "shipping-address": "Endereço de envio",
+    "shipping-cancelled": "",
     "shipping-method": "Método de envio",
     "state": "Estado",
     "sub-total": "Subtotal",

+ 7 - 1
packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json

@@ -55,6 +55,7 @@
     "add-facet-value": "Adicionar novo valor",
     "add-facets": "Adicionar etiqueta",
     "add-option": "Adicionar opção",
+    "asset-preview-links": "",
     "assign-product-to-channel-success": "Produto atribuído com sucesso a \"{ channel }\"",
     "assign-products-to-channel": "Atribuir produtos ao canal",
     "assign-to-channel": "Atribuir ao canal",
@@ -238,6 +239,7 @@
     "sample-formatting": "Formatação de amostra",
     "select": "Seleccione...",
     "select-display-language": "Seleccionar idioma",
+    "select-relation-id": "",
     "select-today": "Seleccione a data de hoje",
     "set-language": "Definir idioma",
     "short-date": "Data abreviada",
@@ -309,7 +311,7 @@
     "remove-customers-from-group-success": "{customerCount, plural, one {1 cliente eliminado} other {{customerCount} clientes eliminados}} do grupo \"{ groupName }\"",
     "remove-from-group": "Eliminar deste grupo",
     "search-customers-by-email": "Pesquisar por email",
-    "search-customers-by-last-name": "Pesquisar pelo apelido",
+    "search-customers-by-email-last-name-postal-code": "Pesquisar pelo apelido / email / código postal",
     "select-customer": "Seleccione o cliente",
     "set-as-default-billing-address": "Definir como morada de faturação padrão",
     "set-as-default-shipping-address": "Definir como morada de entrega padrão",
@@ -436,6 +438,7 @@
     "arrange-additional-payment": "Configurar pagamento adicional",
     "billing-address": "Morada de faturação",
     "cancel": "Cancelar",
+    "cancel-entire-order": "",
     "cancel-fulfillment": "Cancelar entrega",
     "cancel-modification": "Cancelar modificação",
     "cancel-order": "Cancelar encomenda",
@@ -443,6 +446,7 @@
     "cancel-reason-customer-request": "Encomenda do cliente",
     "cancel-reason-not-available": "Não disponível",
     "cancel-selected-items": "Cancelar itens seleccionados",
+    "cancel-specified-items": "",
     "cancellation-reason": "Motivo do cancelamento",
     "cancelled-order-success": "Encomenda cancelada com sucesso",
     "confirm-modifications": "Confirmar modificações",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "Pesquisar pelo apelido do cliente",
     "search-by-order-code": "Pesqusiar pelo código da encomenda",
     "select-state": "Seleccionar estado",
+    "set-coupon-codes": "",
     "set-fulfillment-state": "Marcar como {state}",
     "settle-payment": "Liquidar pagamento",
     "settle-payment-error": "Não foi possível liquidar o pagamento",
@@ -547,6 +552,7 @@
     "settle-refund-success": "Reembolso liquidado com sucesso",
     "shipping": "Envio",
     "shipping-address": "Morada de entrega",
+    "shipping-cancelled": "",
     "shipping-method": "Método de envio",
     "state": "Estado",
     "sub-total": "Subtotal",

+ 7 - 1
packages/admin-ui/src/lib/static/i18n-messages/ru.json

@@ -55,6 +55,7 @@
     "add-facet-value": "Добавить значение тега",
     "add-facets": "Добавить тег",
     "add-option": "Добавить опции",
+    "asset-preview-links": "",
     "assign-product-to-channel-success": "Товар успешно добавлен в канал \"{ channel }\"",
     "assign-products-to-channel": "Добавить товары в канал",
     "assign-to-channel": "Добавить в канал",
@@ -238,6 +239,7 @@
     "sample-formatting": "",
     "select": "Выбрать...",
     "select-display-language": "Выберите язык отображения",
+    "select-relation-id": "",
     "select-today": "Выберите сегодня",
     "set-language": "",
     "short-date": "",
@@ -309,7 +311,7 @@
     "remove-customers-from-group-success": "Удален {customerCount, plural, one {1 клиент} other {{customerCount} клиентов}} из \"{ groupName }\"",
     "remove-from-group": "Удалить из этой группы",
     "search-customers-by-email": "Поиск по адресу электронной почты",
-    "search-customers-by-last-name": "Поиск по фамилии",
+    "search-customers-by-email-last-name-postal-code": "",
     "select-customer": "Выберите клиента",
     "set-as-default-billing-address": "Установить как биллинг по умолчанию",
     "set-as-default-shipping-address": "Установить как доставку по умолчанию",
@@ -436,6 +438,7 @@
     "arrange-additional-payment": "Организовать доплату",
     "billing-address": "Платежный адрес",
     "cancel": "Отмена",
+    "cancel-entire-order": "",
     "cancel-fulfillment": "Отменить выполнение",
     "cancel-modification": "Отменить изменение",
     "cancel-order": "Отменить заказ",
@@ -443,6 +446,7 @@
     "cancel-reason-customer-request": "Запрос клиента",
     "cancel-reason-not-available": "Недоступен",
     "cancel-selected-items": "Отменить выбранные позиции",
+    "cancel-specified-items": "",
     "cancellation-reason": "Причина отмены",
     "cancelled-order-success": "Успешно отмененный заказ",
     "confirm-modifications": "Подтвердите изменения",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "Поиск по фамилии клиента",
     "search-by-order-code": "Поиск по коду заказа",
     "select-state": "Выберите состояние",
+    "set-coupon-codes": "",
     "set-fulfillment-state": "Отметить как {state}",
     "settle-payment": "Расчет платежа",
     "settle-payment-error": "Не удалось провести оплату",
@@ -547,6 +552,7 @@
     "settle-refund-success": "Возврат успешно осуществлен",
     "shipping": "Доставка",
     "shipping-address": "Адрес доставки",
+    "shipping-cancelled": "",
     "shipping-method": "Способ доставки",
     "state": "Состояние",
     "sub-total": "Промежуточный итог",

+ 7 - 1
packages/admin-ui/src/lib/static/i18n-messages/uk.json

@@ -55,6 +55,7 @@
     "add-facet-value": "Додати значення тегу",
     "add-facets": "Додати тег",
     "add-option": "Додати опцію",
+    "asset-preview-links": "",
     "assign-product-to-channel-success": "Товар успішно доданий в канал \"{ channel }\"",
     "assign-products-to-channel": "Додати товари в канал",
     "assign-to-channel": "Додати в канал",
@@ -238,6 +239,7 @@
     "sample-formatting": "",
     "select": "Вибрати...",
     "select-display-language": "Виберіть мову відображення",
+    "select-relation-id": "",
     "select-today": "Виберіть сьогодні",
     "set-language": "",
     "short-date": "",
@@ -309,7 +311,7 @@
     "remove-customers-from-group-success": "Видалено {customerCount, plural, one {1 клієнт} other {{customerCount} клієнтів}} из \"{ groupName }\"",
     "remove-from-group": "Вилучити з цієї групи",
     "search-customers-by-email": "Пошук за адресою електронної пошти",
-    "search-customers-by-last-name": "Пошук за прізвищем",
+    "search-customers-by-email-last-name-postal-code": "",
     "select-customer": "Виберіть клієнта",
     "set-as-default-billing-address": "Встановити як білінг за замовчуванням",
     "set-as-default-shipping-address": "Встановити як доставку за замовчуванням",
@@ -436,6 +438,7 @@
     "arrange-additional-payment": "Організувати доплату",
     "billing-address": "Платіжна адреса",
     "cancel": "Скасування",
+    "cancel-entire-order": "",
     "cancel-fulfillment": "Скасувати виконання",
     "cancel-modification": "Скасувати зміну",
     "cancel-order": "Скасувати замовлення",
@@ -443,6 +446,7 @@
     "cancel-reason-customer-request": "Запит клієнта",
     "cancel-reason-not-available": "Недоступний",
     "cancel-selected-items": "Скасувати вибрані позиції",
+    "cancel-specified-items": "",
     "cancellation-reason": "Причина скасування",
     "cancelled-order-success": "Успішно скасоване замовлення",
     "confirm-modifications": "Підтвердіть зміни",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "Пошук за прізвищем клієнта",
     "search-by-order-code": "Пошук по коду замовлення",
     "select-state": "Виберіть стан",
+    "set-coupon-codes": "",
     "set-fulfillment-state": "Помітити як {state}",
     "settle-payment": "Розрахунок платежу",
     "settle-payment-error": "Не вдалося провести оплату",
@@ -547,6 +552,7 @@
     "settle-refund-success": "Повернення успішно здійснено",
     "shipping": "Доставка",
     "shipping-address": "Адреса доставки",
+    "shipping-cancelled": "",
     "shipping-method": "Спосіб доставки",
     "state": "Стан",
     "sub-total": "Проміжний підсумок",

+ 7 - 1
packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json

@@ -55,6 +55,7 @@
     "add-facet-value": "添加特征值",
     "add-facets": "添加特征",
     "add-option": "添加规格组",
+    "asset-preview-links": "",
     "assign-product-to-channel-success": "成功将产品添加至销售渠道\"{ channel }\"",
     "assign-products-to-channel": "分配产品到销售渠道",
     "assign-to-channel": "分配至销售渠道",
@@ -238,6 +239,7 @@
     "sample-formatting": "",
     "select": "选择...",
     "select-display-language": "选择显示语言",
+    "select-relation-id": "",
     "select-today": "选择今天",
     "set-language": "",
     "short-date": "",
@@ -309,7 +311,7 @@
     "remove-customers-from-group-success": "成功从分组\"{ groupName }\"中移除{customerCount}个客户",
     "remove-from-group": "从分组中移除",
     "search-customers-by-email": "输入要搜索的客户邮件地址",
-    "search-customers-by-last-name": "按姓搜索",
+    "search-customers-by-email-last-name-postal-code": "",
     "select-customer": "选择客户",
     "set-as-default-billing-address": "设置为默认账单地址",
     "set-as-default-shipping-address": "设置为默认邮寄地址",
@@ -436,6 +438,7 @@
     "arrange-additional-payment": "添加额外付款",
     "billing-address": "账单地址",
     "cancel": "取消",
+    "cancel-entire-order": "",
     "cancel-fulfillment": "取消发货",
     "cancel-modification": "取消修改",
     "cancel-order": "取消订单",
@@ -443,6 +446,7 @@
     "cancel-reason-customer-request": "客户要求",
     "cancel-reason-not-available": "产品无库存",
     "cancel-selected-items": "取消已选",
+    "cancel-specified-items": "",
     "cancellation-reason": "取消原因",
     "cancelled-order-success": "订单成功取消",
     "confirm-modifications": "确认修改",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "",
     "search-by-order-code": "输入要搜索的订单编号",
     "select-state": "",
+    "set-coupon-codes": "",
     "set-fulfillment-state": "",
     "settle-payment": "结算付款",
     "settle-payment-error": "结算付款失败",
@@ -547,6 +552,7 @@
     "settle-refund-success": "结算退款成功",
     "shipping": "运费",
     "shipping-address": "配送地址",
+    "shipping-cancelled": "",
     "shipping-method": "配送方式",
     "state": "状态",
     "sub-total": "小计金额",

+ 7 - 1
packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json

@@ -55,6 +55,7 @@
     "add-facet-value": "新增特徵值",
     "add-facets": "新增特徵",
     "add-option": "新增規格選項",
+    "asset-preview-links": "",
     "assign-product-to-channel-success": "成功將產品新增至渠道\"{ channel }\"",
     "assign-products-to-channel": "分配產品到渠道",
     "assign-to-channel": "分配至渠道",
@@ -238,6 +239,7 @@
     "sample-formatting": "",
     "select": "選擇...",
     "select-display-language": "選擇顯示語言",
+    "select-relation-id": "",
     "select-today": "選擇今天",
     "set-language": "",
     "short-date": "",
@@ -309,7 +311,7 @@
     "remove-customers-from-group-success": "",
     "remove-from-group": "",
     "search-customers-by-email": "輸入要搜索的客户電郵地址",
-    "search-customers-by-last-name": "",
+    "search-customers-by-email-last-name-postal-code": "",
     "select-customer": "",
     "set-as-default-billing-address": "設定為默認賬單地址",
     "set-as-default-shipping-address": "設定為默認郵寄地址",
@@ -436,6 +438,7 @@
     "arrange-additional-payment": "",
     "billing-address": "",
     "cancel": "取消",
+    "cancel-entire-order": "",
     "cancel-fulfillment": "",
     "cancel-modification": "",
     "cancel-order": "取消訂單",
@@ -443,6 +446,7 @@
     "cancel-reason-customer-request": "客户要求",
     "cancel-reason-not-available": "產品無庫存",
     "cancel-selected-items": "取消已選",
+    "cancel-specified-items": "",
     "cancellation-reason": "取消原因",
     "cancelled-order-success": "訂單取消成功",
     "confirm-modifications": "",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "",
     "search-by-order-code": "輸入要搜索的訂單編號",
     "select-state": "",
+    "set-coupon-codes": "",
     "set-fulfillment-state": "",
     "settle-payment": "結算付款",
     "settle-payment-error": "結算付款失敗",
@@ -547,6 +552,7 @@
     "settle-refund-success": "結算退款成功",
     "shipping": "運費",
     "shipping-address": "配送地址",
+    "shipping-cancelled": "",
     "shipping-method": "配送方式",
     "state": "狀態",
     "sub-total": "小計金額",

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

@@ -1,12 +1,27 @@
 {
-  "apiHost": "http://localhost",
-  "apiPort": "3000",
-  "adminApiPath": "admin-api",
-  "tokenMethod": "bearer",
-  "authTokenHeaderKey": "vendure-auth-token",
-  "defaultLanguage": "en",
-  "availableLanguages": ["en", "es", "zh_Hant", "zh_Hans", "pl", "de", "pt_BR", "pt_PT", "cs", "fr", "ru", "uk", "it"],
-  "brand": "",
-  "hideVendureBranding": false,
-  "hideVersion": false
+    "apiHost": "http://localhost",
+    "apiPort": "3000",
+    "adminApiPath": "admin-api",
+    "tokenMethod": "bearer",
+    "authTokenHeaderKey": "vendure-auth-token",
+    "defaultLanguage": "en",
+    "availableLanguages": [
+        "en",
+        "es",
+        "zh_Hant",
+        "zh_Hans",
+        "pl",
+        "de",
+        "pt_BR",
+        "pt_PT",
+        "cs",
+        "fr",
+        "ru",
+        "uk",
+        "it"
+    ],
+    "brand": "",
+    "hideVendureBranding": false,
+    "hideVersion": false,
+    "cancellationReasons": ["order.cancel-reason-customer-request", "order.cancel-reason-not-available"]
 }

+ 59 - 1
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -79,6 +79,7 @@ export type Adjustment = {
 export enum AdjustmentType {
     PROMOTION = 'PROMOTION',
     DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
+    OTHER = 'OTHER',
 }
 
 export type Administrator = Node & {
@@ -273,6 +274,11 @@ export type BooleanCustomFieldConfig = CustomField & {
     ui?: Maybe<Scalars['JSON']>;
 };
 
+/** Operators for filtering on a list of Boolean fields */
+export type BooleanListOperators = {
+    inList: Scalars['Boolean'];
+};
+
 /** Operators for filtering on a Boolean field */
 export type BooleanOperators = {
     eq?: Maybe<Scalars['Boolean']>;
@@ -290,6 +296,8 @@ export type CancelOrderInput = {
     orderId: Scalars['ID'];
     /** Optionally specify which OrderLines to cancel. If not provided, all OrderLines will be cancelled */
     lines?: Maybe<Array<OrderLineInput>>;
+    /** Specify whether the shipping charges should also be cancelled. Defaults to false */
+    cancelShipping?: Maybe<Scalars['Boolean']>;
     reason?: Maybe<Scalars['String']>;
 };
 
@@ -538,6 +546,28 @@ export type CountryTranslationInput = {
     customFields?: Maybe<Scalars['JSON']>;
 };
 
+/** Returned if the provided coupon code is invalid */
+export type CouponCodeExpiredError = ErrorResult & {
+    errorCode: ErrorCode;
+    message: Scalars['String'];
+    couponCode: Scalars['String'];
+};
+
+/** Returned if the provided coupon code is invalid */
+export type CouponCodeInvalidError = ErrorResult & {
+    errorCode: ErrorCode;
+    message: Scalars['String'];
+    couponCode: Scalars['String'];
+};
+
+/** Returned if the provided coupon code is invalid */
+export type CouponCodeLimitError = ErrorResult & {
+    errorCode: ErrorCode;
+    message: Scalars['String'];
+    couponCode: Scalars['String'];
+    limit: Scalars['Int'];
+};
+
 export type CreateAddressInput = {
     fullName?: Maybe<Scalars['String']>;
     company?: Maybe<Scalars['String']>;
@@ -1181,6 +1211,7 @@ export type CustomerOrdersArgs = {
 };
 
 export type CustomerFilterParameter = {
+    postalCode?: Maybe<StringOperators>;
     id?: Maybe<IdOperators>;
     createdAt?: Maybe<DateOperators>;
     updatedAt?: Maybe<DateOperators>;
@@ -1265,6 +1296,11 @@ export type CustomerSortParameter = {
     emailAddress?: Maybe<SortOrder>;
 };
 
+/** Operators for filtering on a list of Date fields */
+export type DateListOperators = {
+    inList: Scalars['DateTime'];
+};
+
 /** Operators for filtering on a DateTime field */
 export type DateOperators = {
     eq?: Maybe<Scalars['DateTime']>;
@@ -1376,6 +1412,9 @@ export enum ErrorCode {
     ORDER_LIMIT_ERROR = 'ORDER_LIMIT_ERROR',
     NEGATIVE_QUANTITY_ERROR = 'NEGATIVE_QUANTITY_ERROR',
     INSUFFICIENT_STOCK_ERROR = 'INSUFFICIENT_STOCK_ERROR',
+    COUPON_CODE_INVALID_ERROR = 'COUPON_CODE_INVALID_ERROR',
+    COUPON_CODE_EXPIRED_ERROR = 'COUPON_CODE_EXPIRED_ERROR',
+    COUPON_CODE_LIMIT_ERROR = 'COUPON_CODE_LIMIT_ERROR',
 }
 
 export type ErrorResult = {
@@ -1623,6 +1662,11 @@ export enum HistoryEntryType {
     ORDER_MODIFIED = 'ORDER_MODIFIED',
 }
 
+/** Operators for filtering on a list of ID fields */
+export type IdListOperators = {
+    inList: Scalars['ID'];
+};
+
 /** Operators for filtering on an ID field */
 export type IdOperators = {
     eq?: Maybe<Scalars['String']>;
@@ -2174,6 +2218,7 @@ export type ModifyOrderInput = {
     note?: Maybe<Scalars['String']>;
     refund?: Maybe<AdministratorRefundInput>;
     options?: Maybe<ModifyOrderOptions>;
+    couponCodes?: Maybe<Array<Scalars['String']>>;
 };
 
 export type ModifyOrderOptions = {
@@ -2189,7 +2234,10 @@ export type ModifyOrderResult =
     | RefundPaymentIdMissingError
     | OrderLimitError
     | NegativeQuantityError
-    | InsufficientStockError;
+    | InsufficientStockError
+    | CouponCodeExpiredError
+    | CouponCodeInvalidError
+    | CouponCodeLimitError;
 
 export type MoveCollectionInput = {
     collectionId: Scalars['ID'];
@@ -2882,6 +2930,11 @@ export type NothingToRefundError = ErrorResult & {
     message: Scalars['String'];
 };
 
+/** Operators for filtering on a list of Number fields */
+export type NumberListOperators = {
+    inList: Scalars['Float'];
+};
+
 /** Operators for filtering on a Int or Float field */
 export type NumberOperators = {
     eq?: Maybe<Scalars['Float']>;
@@ -4458,6 +4511,11 @@ export type StringFieldOption = {
     label?: Maybe<Array<LocalizedString>>;
 };
 
+/** Operators for filtering on a list of String fields */
+export type StringListOperators = {
+    inList: Scalars['String'];
+};
+
 /** Operators for filtering on a String field */
 export type StringOperators = {
     eq?: Maybe<Scalars['String']>;

+ 52 - 7
packages/common/src/generated-shop-types.ts

@@ -59,6 +59,7 @@ export type Adjustment = {
 export enum AdjustmentType {
     PROMOTION = 'PROMOTION',
     DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
+    OTHER = 'OTHER',
 }
 
 /** Returned when attempting to set the Customer for an Order when already logged in. */
@@ -130,6 +131,11 @@ export type BooleanCustomFieldConfig = CustomField & {
     ui?: Maybe<Scalars['JSON']>;
 };
 
+/** Operators for filtering on a list of Boolean fields */
+export type BooleanListOperators = {
+    inList: Scalars['Boolean'];
+};
+
 /** Operators for filtering on a Boolean field */
 export type BooleanOperators = {
     eq?: Maybe<Scalars['Boolean']>;
@@ -803,6 +809,11 @@ export type CustomerSortParameter = {
     emailAddress?: Maybe<SortOrder>;
 };
 
+/** Operators for filtering on a list of Date fields */
+export type DateListOperators = {
+    inList: Scalars['DateTime'];
+};
+
 /** Operators for filtering on a DateTime field */
 export type DateOperators = {
     eq?: Maybe<Scalars['DateTime']>;
@@ -874,17 +885,18 @@ export enum ErrorCode {
     ORDER_LIMIT_ERROR = 'ORDER_LIMIT_ERROR',
     NEGATIVE_QUANTITY_ERROR = 'NEGATIVE_QUANTITY_ERROR',
     INSUFFICIENT_STOCK_ERROR = 'INSUFFICIENT_STOCK_ERROR',
+    COUPON_CODE_INVALID_ERROR = 'COUPON_CODE_INVALID_ERROR',
+    COUPON_CODE_EXPIRED_ERROR = 'COUPON_CODE_EXPIRED_ERROR',
+    COUPON_CODE_LIMIT_ERROR = 'COUPON_CODE_LIMIT_ERROR',
     ORDER_MODIFICATION_ERROR = 'ORDER_MODIFICATION_ERROR',
     INELIGIBLE_SHIPPING_METHOD_ERROR = 'INELIGIBLE_SHIPPING_METHOD_ERROR',
     ORDER_PAYMENT_STATE_ERROR = 'ORDER_PAYMENT_STATE_ERROR',
     INELIGIBLE_PAYMENT_METHOD_ERROR = 'INELIGIBLE_PAYMENT_METHOD_ERROR',
     PAYMENT_FAILED_ERROR = 'PAYMENT_FAILED_ERROR',
     PAYMENT_DECLINED_ERROR = 'PAYMENT_DECLINED_ERROR',
-    COUPON_CODE_INVALID_ERROR = 'COUPON_CODE_INVALID_ERROR',
-    COUPON_CODE_EXPIRED_ERROR = 'COUPON_CODE_EXPIRED_ERROR',
-    COUPON_CODE_LIMIT_ERROR = 'COUPON_CODE_LIMIT_ERROR',
     ALREADY_LOGGED_IN_ERROR = 'ALREADY_LOGGED_IN_ERROR',
     MISSING_PASSWORD_ERROR = 'MISSING_PASSWORD_ERROR',
+    PASSWORD_VALIDATION_ERROR = 'PASSWORD_VALIDATION_ERROR',
     PASSWORD_ALREADY_SET_ERROR = 'PASSWORD_ALREADY_SET_ERROR',
     VERIFICATION_TOKEN_INVALID_ERROR = 'VERIFICATION_TOKEN_INVALID_ERROR',
     VERIFICATION_TOKEN_EXPIRED_ERROR = 'VERIFICATION_TOKEN_EXPIRED_ERROR',
@@ -1106,6 +1118,11 @@ export enum HistoryEntryType {
     ORDER_MODIFIED = 'ORDER_MODIFIED',
 }
 
+/** Operators for filtering on a list of ID fields */
+export type IdListOperators = {
+    inList: Scalars['ID'];
+};
+
 /** Operators for filtering on an ID field */
 export type IdOperators = {
     eq?: Maybe<Scalars['String']>;
@@ -1605,7 +1622,7 @@ export type Mutation = {
     /**
      * Verify a Customer email address with the token sent to that address. Only applicable if `authOptions.requireVerification` is set to true.
      *
-     * If the Customer was not registered with a password in the `registerCustomerAccount` mutation, the a password _must_ be
+     * If the Customer was not registered with a password in the `registerCustomerAccount` mutation, the password _must_ be
      * provided here.
      */
     verifyCustomerAccount: VerifyCustomerAccountResult;
@@ -1791,6 +1808,11 @@ export type NotVerifiedError = ErrorResult & {
     message: Scalars['String'];
 };
 
+/** Operators for filtering on a list of Number fields */
+export type NumberListOperators = {
+    inList: Scalars['Float'];
+};
+
 /** Operators for filtering on a Int or Float field */
 export type NumberOperators = {
     eq?: Maybe<Scalars['Float']>;
@@ -2113,6 +2135,14 @@ export type PasswordResetTokenInvalidError = ErrorResult & {
     message: Scalars['String'];
 };
 
+/** Returned when attempting to register or verify a customer account where the given password fails password validation. */
+export type PasswordValidationError = ErrorResult & {
+    __typename?: 'PasswordValidationError';
+    errorCode: ErrorCode;
+    message: Scalars['String'];
+    validationErrorMessage: Scalars['String'];
+};
+
 export type Payment = Node & {
     __typename?: 'Payment';
     id: Scalars['ID'];
@@ -2695,7 +2725,11 @@ export type Refund = Node & {
     metadata?: Maybe<Scalars['JSON']>;
 };
 
-export type RegisterCustomerAccountResult = Success | MissingPasswordError | NativeAuthStrategyError;
+export type RegisterCustomerAccountResult =
+    | Success
+    | MissingPasswordError
+    | PasswordValidationError
+    | NativeAuthStrategyError;
 
 export type RegisterCustomerInput = {
     emailAddress: Scalars['String'];
@@ -2735,6 +2769,7 @@ export type ResetPasswordResult =
     | CurrentUser
     | PasswordResetTokenInvalidError
     | PasswordResetTokenExpiredError
+    | PasswordValidationError
     | NativeAuthStrategyError
     | NotVerifiedError;
 
@@ -2799,7 +2834,7 @@ export type SearchResult = {
     facetValueIds: Array<Scalars['ID']>;
     /** An array of ids of the Collections in which this result appears */
     collectionIds: Array<Scalars['ID']>;
-    /** A relevence score for the result. Differs between database implementations */
+    /** A relevance score for the result. Differs between database implementations */
     score: Scalars['Float'];
 };
 
@@ -2917,6 +2952,11 @@ export type StringFieldOption = {
     label?: Maybe<Array<LocalizedString>>;
 };
 
+/** Operators for filtering on a list of String fields */
+export type StringListOperators = {
+    inList: Scalars['String'];
+};
+
 /** Operators for filtering on a String field */
 export type StringOperators = {
     eq?: Maybe<Scalars['String']>;
@@ -3042,7 +3082,11 @@ export type UpdateCustomerInput = {
     customFields?: Maybe<Scalars['JSON']>;
 };
 
-export type UpdateCustomerPasswordResult = Success | InvalidCredentialsError | NativeAuthStrategyError;
+export type UpdateCustomerPasswordResult =
+    | Success
+    | InvalidCredentialsError
+    | PasswordValidationError
+    | NativeAuthStrategyError;
 
 export type UpdateOrderInput = {
     customFields?: Maybe<Scalars['JSON']>;
@@ -3093,6 +3137,7 @@ export type VerifyCustomerAccountResult =
     | VerificationTokenInvalidError
     | VerificationTokenExpiredError
     | MissingPasswordError
+    | PasswordValidationError
     | PasswordAlreadySetError
     | NativeAuthStrategyError;
 

+ 61 - 3
packages/common/src/generated-types.ts

@@ -73,7 +73,8 @@ export type Adjustment = {
 
 export enum AdjustmentType {
   PROMOTION = 'PROMOTION',
-  DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION'
+  DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
+  OTHER = 'OTHER'
 }
 
 export type Administrator = Node & {
@@ -275,6 +276,11 @@ export type BooleanCustomFieldConfig = CustomField & {
   ui?: Maybe<Scalars['JSON']>;
 };
 
+/** Operators for filtering on a list of Boolean fields */
+export type BooleanListOperators = {
+  inList: Scalars['Boolean'];
+};
+
 /** Operators for filtering on a Boolean field */
 export type BooleanOperators = {
   eq?: Maybe<Scalars['Boolean']>;
@@ -293,6 +299,8 @@ export type CancelOrderInput = {
   orderId: Scalars['ID'];
   /** Optionally specify which OrderLines to cancel. If not provided, all OrderLines will be cancelled */
   lines?: Maybe<Array<OrderLineInput>>;
+  /** Specify whether the shipping charges should also be cancelled. Defaults to false */
+  cancelShipping?: Maybe<Scalars['Boolean']>;
   reason?: Maybe<Scalars['String']>;
 };
 
@@ -551,6 +559,31 @@ export type CountryTranslationInput = {
   customFields?: Maybe<Scalars['JSON']>;
 };
 
+/** Returned if the provided coupon code is invalid */
+export type CouponCodeExpiredError = ErrorResult & {
+  __typename?: 'CouponCodeExpiredError';
+  errorCode: ErrorCode;
+  message: Scalars['String'];
+  couponCode: Scalars['String'];
+};
+
+/** Returned if the provided coupon code is invalid */
+export type CouponCodeInvalidError = ErrorResult & {
+  __typename?: 'CouponCodeInvalidError';
+  errorCode: ErrorCode;
+  message: Scalars['String'];
+  couponCode: Scalars['String'];
+};
+
+/** Returned if the provided coupon code is invalid */
+export type CouponCodeLimitError = ErrorResult & {
+  __typename?: 'CouponCodeLimitError';
+  errorCode: ErrorCode;
+  message: Scalars['String'];
+  couponCode: Scalars['String'];
+  limit: Scalars['Int'];
+};
+
 export type CreateAddressInput = {
   fullName?: Maybe<Scalars['String']>;
   company?: Maybe<Scalars['String']>;
@@ -1193,6 +1226,7 @@ export type CustomerOrdersArgs = {
 };
 
 export type CustomerFilterParameter = {
+  postalCode?: Maybe<StringOperators>;
   id?: Maybe<IdOperators>;
   createdAt?: Maybe<DateOperators>;
   updatedAt?: Maybe<DateOperators>;
@@ -1281,6 +1315,11 @@ export type CustomerSortParameter = {
   emailAddress?: Maybe<SortOrder>;
 };
 
+/** Operators for filtering on a list of Date fields */
+export type DateListOperators = {
+  inList: Scalars['DateTime'];
+};
+
 /** Operators for filtering on a DateTime field */
 export type DateOperators = {
   eq?: Maybe<Scalars['DateTime']>;
@@ -1397,7 +1436,10 @@ export enum ErrorCode {
   EMAIL_ADDRESS_CONFLICT_ERROR = 'EMAIL_ADDRESS_CONFLICT_ERROR',
   ORDER_LIMIT_ERROR = 'ORDER_LIMIT_ERROR',
   NEGATIVE_QUANTITY_ERROR = 'NEGATIVE_QUANTITY_ERROR',
-  INSUFFICIENT_STOCK_ERROR = 'INSUFFICIENT_STOCK_ERROR'
+  INSUFFICIENT_STOCK_ERROR = 'INSUFFICIENT_STOCK_ERROR',
+  COUPON_CODE_INVALID_ERROR = 'COUPON_CODE_INVALID_ERROR',
+  COUPON_CODE_EXPIRED_ERROR = 'COUPON_CODE_EXPIRED_ERROR',
+  COUPON_CODE_LIMIT_ERROR = 'COUPON_CODE_LIMIT_ERROR'
 }
 
 export type ErrorResult = {
@@ -1657,6 +1699,11 @@ export enum HistoryEntryType {
   ORDER_MODIFIED = 'ORDER_MODIFIED'
 }
 
+/** Operators for filtering on a list of ID fields */
+export type IdListOperators = {
+  inList: Scalars['ID'];
+};
+
 /** Operators for filtering on an ID field */
 export type IdOperators = {
   eq?: Maybe<Scalars['String']>;
@@ -2226,6 +2273,7 @@ export type ModifyOrderInput = {
   note?: Maybe<Scalars['String']>;
   refund?: Maybe<AdministratorRefundInput>;
   options?: Maybe<ModifyOrderOptions>;
+  couponCodes?: Maybe<Array<Scalars['String']>>;
 };
 
 export type ModifyOrderOptions = {
@@ -2233,7 +2281,7 @@ export type ModifyOrderOptions = {
   recalculateShipping?: Maybe<Scalars['Boolean']>;
 };
 
-export type ModifyOrderResult = Order | NoChangesSpecifiedError | OrderModificationStateError | PaymentMethodMissingError | RefundPaymentIdMissingError | OrderLimitError | NegativeQuantityError | InsufficientStockError;
+export type ModifyOrderResult = Order | NoChangesSpecifiedError | OrderModificationStateError | PaymentMethodMissingError | RefundPaymentIdMissingError | OrderLimitError | NegativeQuantityError | InsufficientStockError | CouponCodeExpiredError | CouponCodeInvalidError | CouponCodeLimitError;
 
 export type MoveCollectionInput = {
   collectionId: Scalars['ID'];
@@ -3037,6 +3085,11 @@ export type NothingToRefundError = ErrorResult & {
   message: Scalars['String'];
 };
 
+/** Operators for filtering on a list of Number fields */
+export type NumberListOperators = {
+  inList: Scalars['Float'];
+};
+
 /** Operators for filtering on a Int or Float field */
 export type NumberOperators = {
   eq?: Maybe<Scalars['Float']>;
@@ -4704,6 +4757,11 @@ export type StringFieldOption = {
   label?: Maybe<Array<LocalizedString>>;
 };
 
+/** Operators for filtering on a list of String fields */
+export type StringListOperators = {
+  inList: Scalars['String'];
+};
+
 /** Operators for filtering on a String field */
 export type StringOperators = {
   eq?: Maybe<Scalars['String']>;

+ 10 - 2
packages/common/src/shared-types.ts

@@ -264,8 +264,6 @@ export interface AdminUiConfig {
     /**
      * @description
      * An array of languages for which translations exist for the Admin UI.
-     *
-     * @default [LanguageCode.en, LanguageCode.es]
      */
     availableLanguages: LanguageCode[];
     /**
@@ -295,6 +293,16 @@ export interface AdminUiConfig {
      * @default false
      */
     hideVersion?: boolean;
+    /**
+     * @description
+     * Allows you to provide default reasons for a refund or cancellation. This will be used in the
+     * refund/cancel dialog. The values can be literal strings (e.g. "Not in stock") or translation
+     * tokens (see [Adding Admin UI Translations](/docs/plugins/extending-the-admin-ui/adding-ui-translations/)).
+     *
+     * @since 1.5.0
+     * @default ['order.cancel-reason-customer-request', 'order.cancel-reason-not-available']
+     */
+    cancellationReasons?: string[];
 }
 
 /**

+ 1 - 16
packages/core/e2e/collection.e2e-spec.ts

@@ -48,6 +48,7 @@ import {
     DELETE_PRODUCT,
     DELETE_PRODUCT_VARIANT,
     GET_ASSET_LIST,
+    GET_COLLECTIONS,
     UPDATE_COLLECTION,
     UPDATE_PRODUCT,
     UPDATE_PRODUCT_VARIANTS,
@@ -1809,22 +1810,6 @@ const GET_FACET_VALUES = gql`
     ${FACET_VALUE_FRAGMENT}
 `;
 
-const GET_COLLECTIONS = gql`
-    query GetCollections {
-        collections {
-            items {
-                id
-                name
-                position
-                parent {
-                    id
-                    name
-                }
-            }
-        }
-    }
-`;
-
 const GET_COLLECTION_PRODUCT_VARIANTS = gql`
     query GetCollectionProducts($id: ID!) {
         collection(id: $id) {

+ 34 - 0
packages/core/e2e/custom-fields.e2e-spec.ts

@@ -772,6 +772,40 @@ describe('Custom fields', () => {
             expect(products.totalItems).toBe(1);
         });
 
+        it('can filter by custom list fields', async () => {
+            const { products: result1 } = await adminClient.query(gql`
+                query {
+                    products(options: { filter: { intListWithValidation: { inList: 42 } } }) {
+                        totalItems
+                    }
+                }
+            `);
+
+            expect(result1.totalItems).toBe(1);
+            const { products: result2 } = await adminClient.query(gql`
+                query {
+                    products(options: { filter: { intListWithValidation: { inList: 43 } } }) {
+                        totalItems
+                    }
+                }
+            `);
+
+            expect(result2.totalItems).toBe(0);
+        });
+
+        it(
+            'cannot sort by custom list fields',
+            assertThrowsWithMessage(async () => {
+                await adminClient.query(gql`
+                    query {
+                        products(options: { sort: { intListWithValidation: ASC } }) {
+                            totalItems
+                        }
+                    }
+                `);
+            }, `Field "intListWithValidation" is not defined by type "ProductSortParameter".`),
+        );
+
         it(
             'cannot filter by internal field in Admin API',
             assertThrowsWithMessage(async () => {

+ 18 - 0
packages/core/e2e/customer.e2e-spec.ts

@@ -112,6 +112,24 @@ describe('Customer resolver', () => {
         thirdCustomer = result.customers.items[2];
     });
 
+    it('customers list filter by postalCode', async () => {
+        const result = await adminClient.query<GetCustomerList.Query, GetCustomerList.Variables>(
+            GET_CUSTOMER_LIST,
+            {
+                options: {
+                    filter: {
+                        postalCode: {
+                            eq: 'NU9 0PW',
+                        },
+                    },
+                },
+            },
+        );
+
+        expect(result.customers.items.length).toBe(1);
+        expect(result.customers.items[0].emailAddress).toBe('eliezer56@yahoo.com');
+    });
+
     it('customer resolver resolves User', async () => {
         const { customer } = await adminClient.query<
             GetCustomerWithUser.Query,

+ 3 - 0
packages/core/e2e/fixtures/product-import-channel.csv

@@ -0,0 +1,3 @@
+name                   ,slug                   ,description                         ,assets                              ,facets                           ,optionGroups ,optionValues    ,sku   ,price,taxCategory,stockOnHand,trackInventory,variantAssets  ,variantFacets          ,product:pageType,variant:weight,product:owner,product:keywords           ,product:localName
+Model Hand             ,model-hand             ,For when you want to draw a hand    ,vincent-botta-736919-unsplash.jpg   ,Material:wood                    ,size         ,Small           ,MHS   ,15.45 ,standard   ,0          ,false         ,               ,                      ,default         ,100           ,"{""id"": 1}",paper|stretching|watercolor,localModelHand
+                       ,                       ,                                    ,                                    ,                                 ,             ,Large           ,MHL   ,19.95 ,standard   ,0          ,false         ,               ,                      ,                ,100           ,"{""id"": 1}",                           ,

+ 112 - 26
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -79,6 +79,7 @@ export type Adjustment = {
 export enum AdjustmentType {
     PROMOTION = 'PROMOTION',
     DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
+    OTHER = 'OTHER',
 }
 
 export type Administrator = Node & {
@@ -273,6 +274,11 @@ export type BooleanCustomFieldConfig = CustomField & {
     ui?: Maybe<Scalars['JSON']>;
 };
 
+/** Operators for filtering on a list of Boolean fields */
+export type BooleanListOperators = {
+    inList: Scalars['Boolean'];
+};
+
 /** Operators for filtering on a Boolean field */
 export type BooleanOperators = {
     eq?: Maybe<Scalars['Boolean']>;
@@ -290,6 +296,8 @@ export type CancelOrderInput = {
     orderId: Scalars['ID'];
     /** Optionally specify which OrderLines to cancel. If not provided, all OrderLines will be cancelled */
     lines?: Maybe<Array<OrderLineInput>>;
+    /** Specify whether the shipping charges should also be cancelled. Defaults to false */
+    cancelShipping?: Maybe<Scalars['Boolean']>;
     reason?: Maybe<Scalars['String']>;
 };
 
@@ -538,6 +546,28 @@ export type CountryTranslationInput = {
     customFields?: Maybe<Scalars['JSON']>;
 };
 
+/** Returned if the provided coupon code is invalid */
+export type CouponCodeExpiredError = ErrorResult & {
+    errorCode: ErrorCode;
+    message: Scalars['String'];
+    couponCode: Scalars['String'];
+};
+
+/** Returned if the provided coupon code is invalid */
+export type CouponCodeInvalidError = ErrorResult & {
+    errorCode: ErrorCode;
+    message: Scalars['String'];
+    couponCode: Scalars['String'];
+};
+
+/** Returned if the provided coupon code is invalid */
+export type CouponCodeLimitError = ErrorResult & {
+    errorCode: ErrorCode;
+    message: Scalars['String'];
+    couponCode: Scalars['String'];
+    limit: Scalars['Int'];
+};
+
 export type CreateAddressInput = {
     fullName?: Maybe<Scalars['String']>;
     company?: Maybe<Scalars['String']>;
@@ -1181,6 +1211,7 @@ export type CustomerOrdersArgs = {
 };
 
 export type CustomerFilterParameter = {
+    postalCode?: Maybe<StringOperators>;
     id?: Maybe<IdOperators>;
     createdAt?: Maybe<DateOperators>;
     updatedAt?: Maybe<DateOperators>;
@@ -1265,6 +1296,11 @@ export type CustomerSortParameter = {
     emailAddress?: Maybe<SortOrder>;
 };
 
+/** Operators for filtering on a list of Date fields */
+export type DateListOperators = {
+    inList: Scalars['DateTime'];
+};
+
 /** Operators for filtering on a DateTime field */
 export type DateOperators = {
     eq?: Maybe<Scalars['DateTime']>;
@@ -1376,6 +1412,9 @@ export enum ErrorCode {
     ORDER_LIMIT_ERROR = 'ORDER_LIMIT_ERROR',
     NEGATIVE_QUANTITY_ERROR = 'NEGATIVE_QUANTITY_ERROR',
     INSUFFICIENT_STOCK_ERROR = 'INSUFFICIENT_STOCK_ERROR',
+    COUPON_CODE_INVALID_ERROR = 'COUPON_CODE_INVALID_ERROR',
+    COUPON_CODE_EXPIRED_ERROR = 'COUPON_CODE_EXPIRED_ERROR',
+    COUPON_CODE_LIMIT_ERROR = 'COUPON_CODE_LIMIT_ERROR',
 }
 
 export type ErrorResult = {
@@ -1623,6 +1662,11 @@ export enum HistoryEntryType {
     ORDER_MODIFIED = 'ORDER_MODIFIED',
 }
 
+/** Operators for filtering on a list of ID fields */
+export type IdListOperators = {
+    inList: Scalars['ID'];
+};
+
 /** Operators for filtering on an ID field */
 export type IdOperators = {
     eq?: Maybe<Scalars['String']>;
@@ -2174,6 +2218,7 @@ export type ModifyOrderInput = {
     note?: Maybe<Scalars['String']>;
     refund?: Maybe<AdministratorRefundInput>;
     options?: Maybe<ModifyOrderOptions>;
+    couponCodes?: Maybe<Array<Scalars['String']>>;
 };
 
 export type ModifyOrderOptions = {
@@ -2189,7 +2234,10 @@ export type ModifyOrderResult =
     | RefundPaymentIdMissingError
     | OrderLimitError
     | NegativeQuantityError
-    | InsufficientStockError;
+    | InsufficientStockError
+    | CouponCodeExpiredError
+    | CouponCodeInvalidError
+    | CouponCodeLimitError;
 
 export type MoveCollectionInput = {
     collectionId: Scalars['ID'];
@@ -2882,6 +2930,11 @@ export type NothingToRefundError = ErrorResult & {
     message: Scalars['String'];
 };
 
+/** Operators for filtering on a list of Number fields */
+export type NumberListOperators = {
+    inList: Scalars['Float'];
+};
+
 /** Operators for filtering on a Int or Float field */
 export type NumberOperators = {
     eq?: Maybe<Scalars['Float']>;
@@ -4458,6 +4511,11 @@ export type StringFieldOption = {
     label?: Maybe<Array<LocalizedString>>;
 };
 
+/** Operators for filtering on a list of String fields */
+export type StringListOperators = {
+    inList: Scalars['String'];
+};
+
 /** Operators for filtering on a String field */
 export type StringOperators = {
     eq?: Maybe<Scalars['String']>;
@@ -5093,16 +5151,6 @@ export type GetFacetValuesQueryVariables = Exact<{ [key: string]: never }>;
 
 export type GetFacetValuesQuery = { facets: { items: Array<{ values: Array<FacetValueFragment> }> } };
 
-export type GetCollectionsQueryVariables = Exact<{ [key: string]: never }>;
-
-export type GetCollectionsQuery = {
-    collections: {
-        items: Array<
-            Pick<Collection, 'id' | 'name' | 'position'> & { parent?: Maybe<Pick<Collection, 'id' | 'name'>> }
-        >;
-    };
-};
-
 export type GetCollectionProductsQueryVariables = Exact<{
     id: Scalars['ID'];
 }>;
@@ -6408,6 +6456,16 @@ export type GetShippingMethodListQuery = {
     shippingMethods: Pick<ShippingMethodList, 'totalItems'> & { items: Array<ShippingMethodFragment> };
 };
 
+export type GetCollectionsQueryVariables = Exact<{ [key: string]: never }>;
+
+export type GetCollectionsQuery = {
+    collections: {
+        items: Array<
+            Pick<Collection, 'id' | 'name' | 'position'> & { parent?: Maybe<Pick<Collection, 'id' | 'name'>> }
+        >;
+    };
+};
+
 export type CancelJobMutationVariables = Exact<{
     id: Scalars['ID'];
 }>;
@@ -6430,12 +6488,28 @@ export type GetFulfillmentHandlersQuery = {
     >;
 };
 
-export type OrderWithModificationsFragment = Pick<Order, 'id' | 'state' | 'total' | 'totalWithTax'> & {
+export type OrderWithModificationsFragment = Pick<
+    Order,
+    | 'id'
+    | 'state'
+    | 'subTotal'
+    | 'subTotalWithTax'
+    | 'shipping'
+    | 'shippingWithTax'
+    | 'total'
+    | 'totalWithTax'
+> & {
     lines: Array<
         Pick<
             OrderLine,
-            'id' | 'quantity' | 'linePrice' | 'linePriceWithTax' | 'discountedLinePriceWithTax'
+            | 'id'
+            | 'quantity'
+            | 'linePrice'
+            | 'linePriceWithTax'
+            | 'discountedLinePriceWithTax'
+            | 'proratedLinePriceWithTax'
         > & {
+            discounts: Array<Pick<Discount, 'description' | 'amountWithTax'>>;
             productVariant: Pick<ProductVariant, 'id' | 'name'>;
             items: Array<Pick<OrderItem, 'id' | 'createdAt' | 'updatedAt' | 'cancelled' | 'unitPrice'>>;
         }
@@ -6456,6 +6530,8 @@ export type OrderWithModificationsFragment = Pick<Order, 'id' | 'state' | 'total
             refund?: Maybe<Pick<Refund, 'id' | 'state' | 'total' | 'paymentId'>>;
         }
     >;
+    promotions: Array<Pick<Promotion, 'id' | 'name' | 'couponCode'>>;
+    discounts: Array<Pick<Discount, 'description' | 'adjustmentSource' | 'amount' | 'amountWithTax'>>;
     shippingAddress?: Maybe<
         Pick<OrderAddress, 'streetLine1' | 'city' | 'postalCode' | 'province' | 'countryCode' | 'country'>
     >;
@@ -6483,7 +6559,10 @@ export type ModifyOrderMutation = {
         | Pick<RefundPaymentIdMissingError, 'errorCode' | 'message'>
         | Pick<OrderLimitError, 'errorCode' | 'message'>
         | Pick<NegativeQuantityError, 'errorCode' | 'message'>
-        | Pick<InsufficientStockError, 'errorCode' | 'message'>;
+        | Pick<InsufficientStockError, 'errorCode' | 'message'>
+        | Pick<CouponCodeExpiredError, 'errorCode' | 'message'>
+        | Pick<CouponCodeInvalidError, 'errorCode' | 'message'>
+        | Pick<CouponCodeLimitError, 'errorCode' | 'message'>;
 };
 
 export type AddManualPaymentMutationVariables = Exact<{
@@ -7343,18 +7422,6 @@ export namespace GetFacetValues {
     >;
 }
 
-export namespace GetCollections {
-    export type Variables = GetCollectionsQueryVariables;
-    export type Query = GetCollectionsQuery;
-    export type Collections = NonNullable<GetCollectionsQuery['collections']>;
-    export type Items = NonNullable<
-        NonNullable<NonNullable<GetCollectionsQuery['collections']>['items']>[number]
-    >;
-    export type Parent = NonNullable<
-        NonNullable<NonNullable<NonNullable<GetCollectionsQuery['collections']>['items']>[number]>['parent']
-    >;
-}
-
 export namespace GetCollectionProducts {
     export type Variables = GetCollectionProductsQueryVariables;
     export type Query = GetCollectionProductsQuery;
@@ -8772,6 +8839,18 @@ export namespace GetShippingMethodList {
     >;
 }
 
+export namespace GetCollections {
+    export type Variables = GetCollectionsQueryVariables;
+    export type Query = GetCollectionsQuery;
+    export type Collections = NonNullable<GetCollectionsQuery['collections']>;
+    export type Items = NonNullable<
+        NonNullable<NonNullable<GetCollectionsQuery['collections']>['items']>[number]
+    >;
+    export type Parent = NonNullable<
+        NonNullable<NonNullable<NonNullable<GetCollectionsQuery['collections']>['items']>[number]>['parent']
+    >;
+}
+
 export namespace CancelJob {
     export type Variables = CancelJobMutationVariables;
     export type Mutation = CancelJobMutation;
@@ -8800,6 +8879,11 @@ export namespace GetFulfillmentHandlers {
 export namespace OrderWithModifications {
     export type Fragment = OrderWithModificationsFragment;
     export type Lines = NonNullable<NonNullable<OrderWithModificationsFragment['lines']>[number]>;
+    export type Discounts = NonNullable<
+        NonNullable<
+            NonNullable<NonNullable<OrderWithModificationsFragment['lines']>[number]>['discounts']
+        >[number]
+    >;
     export type ProductVariant = NonNullable<
         NonNullable<NonNullable<OrderWithModificationsFragment['lines']>[number]>['productVariant']
     >;
@@ -8834,6 +8918,8 @@ export namespace OrderWithModifications {
     export type Refund = NonNullable<
         NonNullable<NonNullable<OrderWithModificationsFragment['modifications']>[number]>['refund']
     >;
+    export type Promotions = NonNullable<NonNullable<OrderWithModificationsFragment['promotions']>[number]>;
+    export type _Discounts = NonNullable<NonNullable<OrderWithModificationsFragment['discounts']>[number]>;
     export type ShippingAddress = NonNullable<OrderWithModificationsFragment['shippingAddress']>;
     export type BillingAddress = NonNullable<OrderWithModificationsFragment['billingAddress']>;
 }

+ 67 - 7
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -57,6 +57,7 @@ export type Adjustment = {
 export enum AdjustmentType {
     PROMOTION = 'PROMOTION',
     DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
+    OTHER = 'OTHER',
 }
 
 /** Returned when attempting to set the Customer for an Order when already logged in. */
@@ -123,6 +124,11 @@ export type BooleanCustomFieldConfig = CustomField & {
     ui?: Maybe<Scalars['JSON']>;
 };
 
+/** Operators for filtering on a list of Boolean fields */
+export type BooleanListOperators = {
+    inList: Scalars['Boolean'];
+};
+
 /** Operators for filtering on a Boolean field */
 export type BooleanOperators = {
     eq?: Maybe<Scalars['Boolean']>;
@@ -774,6 +780,11 @@ export type CustomerSortParameter = {
     emailAddress?: Maybe<SortOrder>;
 };
 
+/** Operators for filtering on a list of Date fields */
+export type DateListOperators = {
+    inList: Scalars['DateTime'];
+};
+
 /** Operators for filtering on a DateTime field */
 export type DateOperators = {
     eq?: Maybe<Scalars['DateTime']>;
@@ -841,17 +852,18 @@ export enum ErrorCode {
     ORDER_LIMIT_ERROR = 'ORDER_LIMIT_ERROR',
     NEGATIVE_QUANTITY_ERROR = 'NEGATIVE_QUANTITY_ERROR',
     INSUFFICIENT_STOCK_ERROR = 'INSUFFICIENT_STOCK_ERROR',
+    COUPON_CODE_INVALID_ERROR = 'COUPON_CODE_INVALID_ERROR',
+    COUPON_CODE_EXPIRED_ERROR = 'COUPON_CODE_EXPIRED_ERROR',
+    COUPON_CODE_LIMIT_ERROR = 'COUPON_CODE_LIMIT_ERROR',
     ORDER_MODIFICATION_ERROR = 'ORDER_MODIFICATION_ERROR',
     INELIGIBLE_SHIPPING_METHOD_ERROR = 'INELIGIBLE_SHIPPING_METHOD_ERROR',
     ORDER_PAYMENT_STATE_ERROR = 'ORDER_PAYMENT_STATE_ERROR',
     INELIGIBLE_PAYMENT_METHOD_ERROR = 'INELIGIBLE_PAYMENT_METHOD_ERROR',
     PAYMENT_FAILED_ERROR = 'PAYMENT_FAILED_ERROR',
     PAYMENT_DECLINED_ERROR = 'PAYMENT_DECLINED_ERROR',
-    COUPON_CODE_INVALID_ERROR = 'COUPON_CODE_INVALID_ERROR',
-    COUPON_CODE_EXPIRED_ERROR = 'COUPON_CODE_EXPIRED_ERROR',
-    COUPON_CODE_LIMIT_ERROR = 'COUPON_CODE_LIMIT_ERROR',
     ALREADY_LOGGED_IN_ERROR = 'ALREADY_LOGGED_IN_ERROR',
     MISSING_PASSWORD_ERROR = 'MISSING_PASSWORD_ERROR',
+    PASSWORD_VALIDATION_ERROR = 'PASSWORD_VALIDATION_ERROR',
     PASSWORD_ALREADY_SET_ERROR = 'PASSWORD_ALREADY_SET_ERROR',
     VERIFICATION_TOKEN_INVALID_ERROR = 'VERIFICATION_TOKEN_INVALID_ERROR',
     VERIFICATION_TOKEN_EXPIRED_ERROR = 'VERIFICATION_TOKEN_EXPIRED_ERROR',
@@ -1063,6 +1075,11 @@ export enum HistoryEntryType {
     ORDER_MODIFIED = 'ORDER_MODIFIED',
 }
 
+/** Operators for filtering on a list of ID fields */
+export type IdListOperators = {
+    inList: Scalars['ID'];
+};
+
 /** Operators for filtering on an ID field */
 export type IdOperators = {
     eq?: Maybe<Scalars['String']>;
@@ -1551,7 +1568,7 @@ export type Mutation = {
     /**
      * Verify a Customer email address with the token sent to that address. Only applicable if `authOptions.requireVerification` is set to true.
      *
-     * If the Customer was not registered with a password in the `registerCustomerAccount` mutation, the a password _must_ be
+     * If the Customer was not registered with a password in the `registerCustomerAccount` mutation, the password _must_ be
      * provided here.
      */
     verifyCustomerAccount: VerifyCustomerAccountResult;
@@ -1733,6 +1750,11 @@ export type NotVerifiedError = ErrorResult & {
     message: Scalars['String'];
 };
 
+/** Operators for filtering on a list of Number fields */
+export type NumberListOperators = {
+    inList: Scalars['Float'];
+};
+
 /** Operators for filtering on a Int or Float field */
 export type NumberOperators = {
     eq?: Maybe<Scalars['Float']>;
@@ -2042,6 +2064,13 @@ export type PasswordResetTokenInvalidError = ErrorResult & {
     message: Scalars['String'];
 };
 
+/** Returned when attempting to register or verify a customer account where the given password fails password validation. */
+export type PasswordValidationError = ErrorResult & {
+    errorCode: ErrorCode;
+    message: Scalars['String'];
+    validationErrorMessage: Scalars['String'];
+};
+
 export type Payment = Node & {
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
@@ -2604,7 +2633,11 @@ export type Refund = Node & {
     metadata?: Maybe<Scalars['JSON']>;
 };
 
-export type RegisterCustomerAccountResult = Success | MissingPasswordError | NativeAuthStrategyError;
+export type RegisterCustomerAccountResult =
+    | Success
+    | MissingPasswordError
+    | PasswordValidationError
+    | NativeAuthStrategyError;
 
 export type RegisterCustomerInput = {
     emailAddress: Scalars['String'];
@@ -2643,6 +2676,7 @@ export type ResetPasswordResult =
     | CurrentUser
     | PasswordResetTokenInvalidError
     | PasswordResetTokenExpiredError
+    | PasswordValidationError
     | NativeAuthStrategyError
     | NotVerifiedError;
 
@@ -2702,7 +2736,7 @@ export type SearchResult = {
     facetValueIds: Array<Scalars['ID']>;
     /** An array of ids of the Collections in which this result appears */
     collectionIds: Array<Scalars['ID']>;
-    /** A relevence score for the result. Differs between database implementations */
+    /** A relevance score for the result. Differs between database implementations */
     score: Scalars['Float'];
 };
 
@@ -2811,6 +2845,11 @@ export type StringFieldOption = {
     label?: Maybe<Array<LocalizedString>>;
 };
 
+/** Operators for filtering on a list of String fields */
+export type StringListOperators = {
+    inList: Scalars['String'];
+};
+
 /** Operators for filtering on a String field */
 export type StringOperators = {
     eq?: Maybe<Scalars['String']>;
@@ -2927,7 +2966,11 @@ export type UpdateCustomerInput = {
     customFields?: Maybe<Scalars['JSON']>;
 };
 
-export type UpdateCustomerPasswordResult = Success | InvalidCredentialsError | NativeAuthStrategyError;
+export type UpdateCustomerPasswordResult =
+    | Success
+    | InvalidCredentialsError
+    | PasswordValidationError
+    | NativeAuthStrategyError;
 
 export type UpdateOrderInput = {
     customFields?: Maybe<Scalars['JSON']>;
@@ -2975,6 +3018,7 @@ export type VerifyCustomerAccountResult =
     | VerificationTokenInvalidError
     | VerificationTokenExpiredError
     | MissingPasswordError
+    | PasswordValidationError
     | PasswordAlreadySetError
     | NativeAuthStrategyError;
 
@@ -3090,6 +3134,7 @@ export type RegisterMutation = {
     registerCustomerAccount:
         | Pick<Success, 'success'>
         | Pick<MissingPasswordError, 'errorCode' | 'message'>
+        | Pick<PasswordValidationError, 'errorCode' | 'message' | 'validationErrorMessage'>
         | Pick<NativeAuthStrategyError, 'errorCode' | 'message'>;
 };
 
@@ -3108,6 +3153,7 @@ export type VerifyMutation = {
         | Pick<VerificationTokenInvalidError, 'errorCode' | 'message'>
         | Pick<VerificationTokenExpiredError, 'errorCode' | 'message'>
         | Pick<MissingPasswordError, 'errorCode' | 'message'>
+        | Pick<PasswordValidationError, 'errorCode' | 'message' | 'validationErrorMessage'>
         | Pick<PasswordAlreadySetError, 'errorCode' | 'message'>
         | Pick<NativeAuthStrategyError, 'errorCode' | 'message'>;
 };
@@ -3142,6 +3188,7 @@ export type ResetPasswordMutation = {
         | CurrentUserShopFragment
         | Pick<PasswordResetTokenInvalidError, 'errorCode' | 'message'>
         | Pick<PasswordResetTokenExpiredError, 'errorCode' | 'message'>
+        | Pick<PasswordValidationError, 'errorCode' | 'message' | 'validationErrorMessage'>
         | Pick<NativeAuthStrategyError, 'errorCode' | 'message'>
         | Pick<NotVerifiedError, 'errorCode' | 'message'>;
 };
@@ -3212,6 +3259,7 @@ export type UpdatePasswordMutation = {
     updateCustomerPassword:
         | Pick<Success, 'success'>
         | Pick<InvalidCredentialsError, 'errorCode' | 'message'>
+        | Pick<PasswordValidationError, 'errorCode' | 'message'>
         | Pick<NativeAuthStrategyError, 'errorCode' | 'message'>;
 };
 
@@ -3626,6 +3674,10 @@ export namespace Register {
         NonNullable<RegisterMutation['registerCustomerAccount']>,
         { __typename?: 'ErrorResult' }
     >;
+    export type PasswordValidationErrorInlineFragment = DiscriminateUnion<
+        NonNullable<RegisterMutation['registerCustomerAccount']>,
+        { __typename?: 'PasswordValidationError' }
+    >;
 }
 
 export namespace CurrentUserShop {
@@ -3641,6 +3693,10 @@ export namespace Verify {
         NonNullable<VerifyMutation['verifyCustomerAccount']>,
         { __typename?: 'ErrorResult' }
     >;
+    export type PasswordValidationErrorInlineFragment = DiscriminateUnion<
+        NonNullable<VerifyMutation['verifyCustomerAccount']>,
+        { __typename?: 'PasswordValidationError' }
+    >;
 }
 
 export namespace RefreshToken {
@@ -3681,6 +3737,10 @@ export namespace ResetPassword {
         NonNullable<ResetPasswordMutation['resetPassword']>,
         { __typename?: 'ErrorResult' }
     >;
+    export type PasswordValidationErrorInlineFragment = DiscriminateUnion<
+        NonNullable<ResetPasswordMutation['resetPassword']>,
+        { __typename?: 'PasswordValidationError' }
+    >;
 }
 
 export namespace RequestUpdateEmailAddress {

+ 16 - 0
packages/core/e2e/graphql/shared-definitions.ts

@@ -933,3 +933,19 @@ export const GET_SHIPPING_METHOD_LIST = gql`
     }
     ${SHIPPING_METHOD_FRAGMENT}
 `;
+
+export const GET_COLLECTIONS = gql`
+    query GetCollections {
+        collections {
+            items {
+                id
+                name
+                position
+                parent {
+                    id
+                    name
+                }
+            }
+        }
+    }
+`;

+ 9 - 0
packages/core/e2e/graphql/shop-definitions.ts

@@ -154,6 +154,9 @@ export const REGISTER_ACCOUNT = gql`
                 errorCode
                 message
             }
+            ... on PasswordValidationError {
+                validationErrorMessage
+            }
         }
     }
 `;
@@ -178,6 +181,9 @@ export const VERIFY_EMAIL = gql`
                 errorCode
                 message
             }
+            ... on PasswordValidationError {
+                validationErrorMessage
+            }
         }
     }
     ${CURRENT_USER_FRAGMENT}
@@ -217,6 +223,9 @@ export const RESET_PASSWORD = gql`
                 errorCode
                 message
             }
+            ... on PasswordValidationError {
+                validationErrorMessage
+            }
         }
     }
     ${CURRENT_USER_FRAGMENT}

+ 31 - 2
packages/core/e2e/order-merge.e2e-spec.ts

@@ -17,14 +17,22 @@ import path from 'path';
 import { initialData } from '../../../e2e-common/e2e-initial-data';
 import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
 
-import { AttemptLogin, GetCustomerList } from './graphql/generated-e2e-admin-types';
+import {
+    AttemptLogin,
+    AttemptLoginMutation,
+    AttemptLoginMutationVariables,
+    GetCustomerList,
+} from './graphql/generated-e2e-admin-types';
 import {
     AddItemToOrder,
+    AddItemToOrderMutation,
+    GetActiveOrderPaymentsQuery,
+    GetNextOrderStatesQuery,
     TestOrderFragmentFragment,
     UpdatedOrderFragment,
 } from './graphql/generated-e2e-shop-types';
 import { ATTEMPT_LOGIN, GET_CUSTOMER_LIST } from './graphql/shared-definitions';
-import { TEST_ORDER_FRAGMENT } from './graphql/shop-definitions';
+import { GET_ACTIVE_ORDER_PAYMENTS, GET_NEXT_STATES, TEST_ORDER_FRAGMENT } from './graphql/shop-definitions';
 import { sortById } from './utils/test-order-utils';
 
 /**
@@ -225,6 +233,27 @@ describe('Order merging', () => {
             })),
         ).toEqual([{ productVariantId: 'T_8', quantity: 1 }]);
     });
+
+    // https://github.com/vendure-ecommerce/vendure/issues/1454
+    it('does not throw FK error when merging with a cart with an existing session', async () => {
+        await shopClient.asUserWithCredentials(customers[7].emailAddress, 'test');
+        // Create an Order linked with the current session
+        const { nextOrderStates } = await shopClient.query<GetNextOrderStatesQuery>(GET_NEXT_STATES);
+
+        // unset last auth token to simulate a guest user in a different browser
+        shopClient.setAuthToken('');
+        await shopClient.query<AddItemToOrderMutation, AddItemToOrderWithCustomFields>(
+            ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
+            { productVariantId: '1', quantity: 2 },
+        );
+
+        const { login } = await shopClient.query<AttemptLoginMutation, AttemptLoginMutationVariables>(
+            ATTEMPT_LOGIN,
+            { username: customers[7].emailAddress, password: 'test' },
+        );
+
+        expect(login.id).toBe(customers[7].user?.id);
+    });
 });
 
 export const ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS = gql`

+ 257 - 4
packages/core/e2e/order-modification.e2e-spec.ts

@@ -1,10 +1,15 @@
 /* tslint:disable:no-non-null-assertion */
 import { omit } from '@vendure/common/lib/omit';
 import { pick } from '@vendure/common/lib/pick';
+import { summate } from '@vendure/common/lib/shared-utils';
 import {
     defaultShippingCalculator,
     defaultShippingEligibilityChecker,
+    freeShipping,
+    manualFulfillmentHandler,
     mergeConfig,
+    orderFixedDiscount,
+    orderPercentageDiscount,
     productsPercentageDiscount,
     ShippingCalculator,
 } from '@vendure/core';
@@ -14,9 +19,6 @@ import path from 'path';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
 import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
-import { manualFulfillmentHandler } from '../src/config/fulfillment/manual-fulfillment-handler';
-import { orderFixedDiscount } from '../src/config/promotion/actions/order-fixed-discount-action';
-import { defaultPromotionActions } from '../src/config/promotion/index';
 
 import {
     failsToSettlePaymentMethod,
@@ -28,16 +30,22 @@ import {
     AdminTransition,
     CreateFulfillment,
     CreatePromotion,
+    CreatePromotionMutation,
+    CreatePromotionMutationVariables,
     CreateShippingMethod,
     ErrorCode,
     GetOrder,
     GetOrderHistory,
     GetOrderWithModifications,
+    GetOrderWithModificationsQuery,
+    GetOrderWithModificationsQueryVariables,
     GetStockMovement,
     GlobalFlag,
     HistoryEntryType,
     LanguageCode,
     ModifyOrder,
+    ModifyOrderMutation,
+    ModifyOrderMutationVariables,
     OrderFragment,
     OrderWithLinesFragment,
     OrderWithModificationsFragment,
@@ -60,7 +68,6 @@ import {
     CREATE_SHIPPING_METHOD,
     GET_ORDER,
     GET_ORDER_HISTORY,
-    GET_PRODUCT_WITH_VARIANTS,
     GET_STOCK_MOVEMENT,
     UPDATE_CHANNEL,
     UPDATE_PRODUCT_VARIANTS,
@@ -1455,6 +1462,7 @@ describe('Order modification', () => {
                 originalTotalWithTax - order.lines[0].proratedUnitPriceWithTax,
             );
             expect(modifyOrder.payments![0].refunds![0].total).toBe(order.lines[0].proratedUnitPriceWithTax);
+            expect(modifyOrder.totalWithTax).toBe(getOrderPaymentsTotalWithRefunds(modifyOrder));
         });
     });
 
@@ -1736,6 +1744,227 @@ describe('Order modification', () => {
         });
     });
 
+    describe('couponCode handling', () => {
+        const CODE_50PC_OFF = '50PC';
+        const CODE_FREE_SHIPPING = 'FREESHIP';
+        let order: TestOrderWithPaymentsFragment;
+        beforeAll(async () => {
+            await adminClient.query<CreatePromotionMutation, CreatePromotionMutationVariables>(
+                CREATE_PROMOTION,
+                {
+                    input: {
+                        name: '50% off',
+                        couponCode: CODE_50PC_OFF,
+                        enabled: true,
+                        conditions: [],
+                        actions: [
+                            {
+                                code: orderPercentageDiscount.code,
+                                arguments: [{ name: 'discount', value: '50' }],
+                            },
+                        ],
+                    },
+                },
+            );
+            await adminClient.query<CreatePromotionMutation, CreatePromotionMutationVariables>(
+                CREATE_PROMOTION,
+                {
+                    input: {
+                        name: 'Free shipping',
+                        couponCode: CODE_FREE_SHIPPING,
+                        enabled: true,
+                        conditions: [],
+                        actions: [{ code: freeShipping.code, arguments: [] }],
+                    },
+                },
+            );
+
+            // create an order and check out
+            await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test');
+            await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
+                productVariantId: 'T_1',
+                quantity: 1,
+                customFields: {
+                    color: 'green',
+                },
+            } as any);
+            await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
+                productVariantId: 'T_4',
+                quantity: 2,
+            });
+            await proceedToArrangingPayment(shopClient);
+            const result = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
+            orderGuard.assertSuccess(result);
+            order = result;
+            const result2 = await adminTransitionOrderToState(order.id, 'Modifying');
+            orderGuard.assertSuccess(result2);
+            expect(result2.state).toBe('Modifying');
+        });
+
+        it('invalid coupon code returns ErrorResult', async () => {
+            const { modifyOrder } = await adminClient.query<
+                ModifyOrderMutation,
+                ModifyOrderMutationVariables
+            >(MODIFY_ORDER, {
+                input: {
+                    dryRun: false,
+                    orderId: order.id,
+                    couponCodes: ['BAD_CODE'],
+                },
+            });
+            orderGuard.assertErrorResult(modifyOrder);
+            expect(modifyOrder.message).toBe('Coupon code "BAD_CODE" is not valid');
+        });
+
+        it('valid coupon code applies Promotion', async () => {
+            const { modifyOrder } = await adminClient.query<
+                ModifyOrderMutation,
+                ModifyOrderMutationVariables
+            >(MODIFY_ORDER, {
+                input: {
+                    dryRun: false,
+                    orderId: order.id,
+                    refund: {
+                        paymentId: order.payments![0].id,
+                    },
+                    couponCodes: [CODE_50PC_OFF],
+                },
+            });
+            orderGuard.assertSuccess(modifyOrder);
+            expect(modifyOrder.subTotalWithTax).toBe(order.subTotalWithTax * 0.5);
+        });
+
+        it('adds order.discounts', async () => {
+            const { order: orderWithModifications } = await adminClient.query<
+                GetOrderWithModificationsQuery,
+                GetOrderWithModificationsQueryVariables
+            >(GET_ORDER_WITH_MODIFICATIONS, { id: order.id });
+            expect(orderWithModifications?.discounts.length).toBe(1);
+            expect(orderWithModifications?.discounts[0].description).toBe('50% off');
+        });
+
+        it('adds order.promotions', async () => {
+            const { order: orderWithModifications } = await adminClient.query<
+                GetOrderWithModificationsQuery,
+                GetOrderWithModificationsQueryVariables
+            >(GET_ORDER_WITH_MODIFICATIONS, { id: order.id });
+            expect(orderWithModifications?.promotions.length).toBe(1);
+            expect(orderWithModifications?.promotions[0].name).toBe('50% off');
+        });
+
+        it('creates correct refund amount', async () => {
+            const { order: orderWithModifications } = await adminClient.query<
+                GetOrderWithModificationsQuery,
+                GetOrderWithModificationsQueryVariables
+            >(GET_ORDER_WITH_MODIFICATIONS, { id: order.id });
+            expect(orderWithModifications?.payments![0].refunds.length).toBe(1);
+            expect(orderWithModifications!.totalWithTax).toBe(
+                getOrderPaymentsTotalWithRefunds(orderWithModifications!),
+            );
+            expect(orderWithModifications?.payments![0].refunds[0].total).toBe(
+                order.totalWithTax - orderWithModifications!.totalWithTax,
+            );
+        });
+
+        it('creates history entry for applying couponCode', async () => {
+            const { order: history } = await adminClient.query<
+                GetOrderHistory.Query,
+                GetOrderHistory.Variables
+            >(GET_ORDER_HISTORY, {
+                id: order.id,
+                options: { filter: { type: { eq: HistoryEntryType.ORDER_COUPON_APPLIED } } },
+            });
+            orderGuard.assertSuccess(history);
+
+            expect(history.history.items.length).toBe(1);
+            expect(pick(history.history.items[0]!, ['type', 'data'])).toEqual({
+                type: HistoryEntryType.ORDER_COUPON_APPLIED,
+                data: { couponCode: CODE_50PC_OFF, promotionId: 'T_4' },
+            });
+        });
+
+        it('removes coupon code', async () => {
+            const { modifyOrder } = await adminClient.query<
+                ModifyOrderMutation,
+                ModifyOrderMutationVariables
+            >(MODIFY_ORDER, {
+                input: {
+                    dryRun: false,
+                    orderId: order.id,
+                    couponCodes: [],
+                },
+            });
+            orderGuard.assertSuccess(modifyOrder);
+            expect(modifyOrder.subTotalWithTax).toBe(order.subTotalWithTax);
+        });
+
+        it('removes order.discounts', async () => {
+            const { order: orderWithModifications } = await adminClient.query<
+                GetOrderWithModificationsQuery,
+                GetOrderWithModificationsQueryVariables
+            >(GET_ORDER_WITH_MODIFICATIONS, { id: order.id });
+            expect(orderWithModifications?.discounts.length).toBe(0);
+        });
+
+        it('removes order.promotions', async () => {
+            const { order: orderWithModifications } = await adminClient.query<
+                GetOrderWithModificationsQuery,
+                GetOrderWithModificationsQueryVariables
+            >(GET_ORDER_WITH_MODIFICATIONS, { id: order.id });
+            expect(orderWithModifications?.promotions.length).toBe(0);
+        });
+
+        it('creates history entry for removing couponCode', async () => {
+            const { order: history } = await adminClient.query<
+                GetOrderHistory.Query,
+                GetOrderHistory.Variables
+            >(GET_ORDER_HISTORY, {
+                id: order.id,
+                options: { filter: { type: { eq: HistoryEntryType.ORDER_COUPON_REMOVED } } },
+            });
+            orderGuard.assertSuccess(history);
+
+            expect(history.history.items.length).toBe(1);
+            expect(pick(history.history.items[0]!, ['type', 'data'])).toEqual({
+                type: HistoryEntryType.ORDER_COUPON_REMOVED,
+                data: { couponCode: CODE_50PC_OFF },
+            });
+        });
+
+        it('correct refund for free shipping couponCode', async () => {
+            await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
+                productVariantId: 'T_1',
+                quantity: 1,
+            } as any);
+            await proceedToArrangingPayment(shopClient);
+            const result = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
+            orderGuard.assertSuccess(result);
+            const order2 = result;
+            const shippingWithTax = order2.shippingWithTax;
+            const result2 = await adminTransitionOrderToState(order2.id, 'Modifying');
+            orderGuard.assertSuccess(result2);
+            expect(result2.state).toBe('Modifying');
+
+            const { modifyOrder } = await adminClient.query<
+                ModifyOrderMutation,
+                ModifyOrderMutationVariables
+            >(MODIFY_ORDER, {
+                input: {
+                    dryRun: false,
+                    orderId: order2.id,
+                    refund: {
+                        paymentId: order2.payments![0].id,
+                    },
+                    couponCodes: [CODE_FREE_SHIPPING],
+                },
+            });
+            orderGuard.assertSuccess(modifyOrder);
+            expect(modifyOrder.shippingWithTax).toBe(0);
+            expect(modifyOrder!.totalWithTax).toBe(getOrderPaymentsTotalWithRefunds(modifyOrder!));
+            expect(modifyOrder.payments![0].refunds[0].total).toBe(shippingWithTax);
+        });
+    });
+
     async function adminTransitionOrderToState(id: string, state: string) {
         const result = await adminClient.query<AdminTransition.Mutation, AdminTransition.Variables>(
             ADMIN_TRANSITION_TO_STATE,
@@ -1798,12 +2027,20 @@ describe('Order modification', () => {
         await adminTransitionOrderToState(order.id, 'Modifying');
         return order;
     }
+
+    function getOrderPaymentsTotalWithRefunds(_order: OrderWithModificationsFragment) {
+        return _order.payments?.reduce((sum, p) => sum + p.amount - summate(p?.refunds, 'total'), 0) ?? 0;
+    }
 });
 
 export const ORDER_WITH_MODIFICATION_FRAGMENT = gql`
     fragment OrderWithModifications on Order {
         id
         state
+        subTotal
+        subTotalWithTax
+        shipping
+        shippingWithTax
         total
         totalWithTax
         lines {
@@ -1812,6 +2049,11 @@ export const ORDER_WITH_MODIFICATION_FRAGMENT = gql`
             linePrice
             linePriceWithTax
             discountedLinePriceWithTax
+            proratedLinePriceWithTax
+            discounts {
+                description
+                amountWithTax
+            }
             productVariant {
                 id
                 name
@@ -1870,6 +2112,17 @@ export const ORDER_WITH_MODIFICATION_FRAGMENT = gql`
                 paymentId
             }
         }
+        promotions {
+            id
+            name
+            couponCode
+        }
+        discounts {
+            description
+            adjustmentSource
+            amount
+            amountWithTax
+        }
         shippingAddress {
             streetLine1
             city

+ 50 - 1
packages/core/e2e/order-process.e2e-spec.ts

@@ -18,6 +18,8 @@ import {
     SetShippingMethod,
     TestOrderFragmentFragment,
     TransitionToState,
+    TransitionToStateMutation,
+    TransitionToStateMutationVariables,
 } from './graphql/generated-e2e-shop-types';
 import { ADMIN_TRANSITION_TO_STATE, GET_ORDER } from './graphql/shared-definitions';
 import {
@@ -40,7 +42,7 @@ const transitionErrorSpy = jest.fn();
 
 describe('Order process', () => {
     const VALIDATION_ERROR_MESSAGE = 'Customer must have a company email address';
-    const customOrderProcess: CustomOrderProcess<'ValidatingCustomer'> = {
+    const customOrderProcess: CustomOrderProcess<'ValidatingCustomer' | 'PaymentProcessing'> = {
         init(injector) {
             initSpy(injector.get(TransactionalConnection).rawConnection.name);
         },
@@ -52,6 +54,12 @@ describe('Order process', () => {
             ValidatingCustomer: {
                 to: ['ArrangingPayment', 'AddingItems'],
             },
+            ArrangingPayment: {
+                to: ['PaymentProcessing'],
+            },
+            PaymentProcessing: {
+                to: ['PaymentAuthorized', 'PaymentSettled'],
+            },
         },
         onTransitionStart(fromState, toState, data) {
             transitionStartSpy(fromState, toState, data);
@@ -261,6 +269,47 @@ describe('Order process', () => {
                 'AddingItems',
             ]);
         });
+
+        // https://github.com/vendure-ecommerce/vendure/issues/963
+        it('allows addPaymentToOrder from a custom state', async () => {
+            await shopClient.query<SetShippingMethod.Mutation, SetShippingMethod.Variables>(
+                SET_SHIPPING_METHOD,
+                { id: 'T_1' },
+            );
+            const result0 = await shopClient.query<
+                TransitionToStateMutation,
+                TransitionToStateMutationVariables
+            >(TRANSITION_TO_STATE, {
+                state: 'ValidatingCustomer',
+            });
+            orderErrorGuard.assertSuccess(result0.transitionOrderToState);
+            const result1 = await shopClient.query<
+                TransitionToStateMutation,
+                TransitionToStateMutationVariables
+            >(TRANSITION_TO_STATE, {
+                state: 'ArrangingPayment',
+            });
+            orderErrorGuard.assertSuccess(result1.transitionOrderToState);
+            const result2 = await shopClient.query<
+                TransitionToStateMutation,
+                TransitionToStateMutationVariables
+            >(TRANSITION_TO_STATE, {
+                state: 'PaymentProcessing',
+            });
+            orderErrorGuard.assertSuccess(result2.transitionOrderToState);
+            expect(result2.transitionOrderToState.state).toBe('PaymentProcessing');
+            const { addPaymentToOrder } = await shopClient.query<
+                AddPaymentToOrder.Mutation,
+                AddPaymentToOrder.Variables
+            >(ADD_PAYMENT, {
+                input: {
+                    method: testSuccessfulPaymentMethod.code,
+                    metadata: {},
+                },
+            });
+            orderErrorGuard.assertSuccess(addPaymentToOrder);
+            expect(addPaymentToOrder.state).toBe('PaymentSettled');
+        });
     });
 
     describe('Admin API transition constraints', () => {

+ 25 - 1
packages/core/e2e/order.e2e-spec.ts

@@ -1055,7 +1055,7 @@ describe('Orders resolver', () => {
             await assertNoStockMovementsCreated(testOrder.product.id);
         });
 
-        it('cancel from PaymentAuthorized state', async () => {
+        it('cancel from PaymentAuthorized state with cancelShipping: true', async () => {
             const testOrder = await createTestOrder(
                 adminClient,
                 shopClient,
@@ -1087,6 +1087,7 @@ describe('Orders resolver', () => {
                 {
                     input: {
                         orderId: testOrder.orderId,
+                        cancelShipping: true,
                     },
                 },
             );
@@ -1107,6 +1108,8 @@ describe('Orders resolver', () => {
             });
             expect(order2!.active).toBe(false);
             expect(order2!.state).toBe('Cancelled');
+            expect(order2!.totalWithTax).toBe(0);
+            expect(order2!.shippingWithTax).toBe(0);
 
             const result2 = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
                 GET_STOCK_MOVEMENT,
@@ -1339,6 +1342,7 @@ describe('Orders resolver', () => {
                     orderId,
                     lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
                     reason: 'cancel reason 2',
+                    cancelShipping: true,
                 },
             });
 
@@ -1346,6 +1350,8 @@ describe('Orders resolver', () => {
                 id: orderId,
             });
             expect(order2!.state).toBe('Cancelled');
+            expect(order2!.shippingWithTax).toBe(0);
+            expect(order2!.totalWithTax).toBe(0);
 
             const result = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
                 GET_STOCK_MOVEMENT,
@@ -1367,6 +1373,22 @@ describe('Orders resolver', () => {
             ]);
         });
 
+        it('cancelled OrderLine.unitPrice is not zero', async () => {
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: orderId,
+            });
+
+            expect(order?.lines[0].unitPrice).toEqual(order?.lines[0].items[0].unitPrice);
+        });
+
+        it('cancelled OrderLine.unitPrice is not zero', async () => {
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: orderId,
+            });
+
+            expect(order?.lines[0].unitPrice).toEqual(order?.lines[0].items[0].unitPrice);
+        });
+
         it('order history contains expected entries', async () => {
             const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(
                 GET_ORDER_HISTORY,
@@ -1412,6 +1434,7 @@ describe('Orders resolver', () => {
                     data: {
                         orderItemIds: ['T_13'],
                         reason: 'cancel reason 1',
+                        shippingCancelled: false,
                     },
                 },
                 {
@@ -1419,6 +1442,7 @@ describe('Orders resolver', () => {
                     data: {
                         orderItemIds: ['T_14'],
                         reason: 'cancel reason 2',
+                        shippingCancelled: true,
                     },
                 },
                 {

+ 194 - 0
packages/core/e2e/populate.e2e-spec.ts

@@ -0,0 +1,194 @@
+import { INestApplication } from '@nestjs/common';
+import { DefaultLogger, User } from '@vendure/core';
+import { populate } from '@vendure/core/cli';
+import { createTestEnvironment, E2E_DEFAULT_CHANNEL_TOKEN } from '@vendure/testing';
+import path from 'path';
+
+import { initialData } from '../../../e2e-common/e2e-initial-data';
+import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
+import { InitialData } from '../src/index';
+
+import {
+    ChannelFragment,
+    CreateChannelMutation,
+    CreateChannelMutationVariables,
+    CurrencyCode,
+    GetAssetListQuery,
+    GetCollectionsQuery,
+    GetProductListQuery,
+    LanguageCode,
+} from './graphql/generated-e2e-admin-types';
+import {
+    CREATE_CHANNEL,
+    GET_ASSET_LIST,
+    GET_COLLECTIONS,
+    GET_PRODUCT_LIST,
+} from './graphql/shared-definitions';
+
+describe('populate() function', () => {
+    let channel2: ChannelFragment;
+    const { server, adminClient } = createTestEnvironment({
+        ...testConfig(),
+        // logger: new DefaultLogger(),
+        customFields: {
+            Product: [
+                { type: 'string', name: 'pageType' },
+                {
+                    name: 'owner',
+                    public: true,
+                    nullable: true,
+                    type: 'relation',
+                    entity: User,
+                    eager: true,
+                },
+                {
+                    name: 'keywords',
+                    public: true,
+                    nullable: true,
+                    type: 'string',
+                    list: true,
+                },
+                {
+                    name: 'localName',
+                    type: 'localeString',
+                },
+            ],
+            ProductVariant: [{ type: 'int', name: 'weight' }],
+        },
+    });
+
+    beforeAll(async () => {
+        await server.init({
+            initialData: { ...initialData, collections: [] },
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-empty.csv'),
+            customerCount: 1,
+        });
+        await adminClient.asSuperAdmin();
+        const { createChannel } = await adminClient.query<
+            CreateChannelMutation,
+            CreateChannelMutationVariables
+        >(CREATE_CHANNEL, {
+            input: {
+                code: 'Channel 2',
+                token: 'channel-2',
+                currencyCode: CurrencyCode.EUR,
+                defaultLanguageCode: LanguageCode.en,
+                defaultShippingZoneId: 'T_1',
+                defaultTaxZoneId: 'T_2',
+                pricesIncludeTax: true,
+            },
+        });
+        channel2 = createChannel as ChannelFragment;
+        await server.destroy();
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    describe('populating default channel', () => {
+        let app: INestApplication;
+
+        beforeAll(async () => {
+            const initialDataForPopulate: InitialData = {
+                defaultLanguage: initialData.defaultLanguage,
+                defaultZone: initialData.defaultZone,
+                taxRates: [],
+                shippingMethods: [],
+                paymentMethods: [],
+                countries: [],
+                collections: [{ name: 'Collection 1', filters: [] }],
+            };
+            const csvFile = path.join(__dirname, 'fixtures', 'product-import.csv');
+            app = await populate(
+                async () => {
+                    await server.bootstrap();
+                    return server.app;
+                },
+                initialDataForPopulate,
+                csvFile,
+            );
+        }, TEST_SETUP_TIMEOUT_MS);
+
+        afterAll(async () => {
+            await app.close();
+        });
+
+        it('populates products', async () => {
+            await adminClient.asSuperAdmin();
+            const { products } = await adminClient.query<GetProductListQuery>(GET_PRODUCT_LIST);
+            expect(products.totalItems).toBe(4);
+            expect(products.items.map(i => i.name).sort()).toEqual([
+                'Artists Smock',
+                'Giotto Mega Pencils',
+                'Mabef M/02 Studio Easel',
+                'Perfect Paper Stretcher',
+            ]);
+        });
+
+        it('populates assets', async () => {
+            const { assets } = await adminClient.query<GetAssetListQuery>(GET_ASSET_LIST);
+            expect(assets.items.map(i => i.name).sort()).toEqual([
+                'box-of-12.jpg',
+                'box-of-8.jpg',
+                'pps1.jpg',
+                'pps2.jpg',
+            ]);
+        });
+
+        it('populates collections', async () => {
+            const { collections } = await adminClient.query<GetCollectionsQuery>(GET_COLLECTIONS);
+            expect(collections.items.map(i => i.name).sort()).toEqual(['Collection 1']);
+        });
+    });
+
+    describe('populating a non-default channel', () => {
+        let app: INestApplication;
+        beforeAll(async () => {
+            const initialDataForPopulate: InitialData = {
+                defaultLanguage: initialData.defaultLanguage,
+                defaultZone: initialData.defaultZone,
+                taxRates: [],
+                shippingMethods: [],
+                paymentMethods: [],
+                countries: [],
+                collections: [{ name: 'Collection 2', filters: [] }],
+            };
+            const csvFile = path.join(__dirname, 'fixtures', 'product-import-channel.csv');
+
+            app = await populate(
+                async () => {
+                    await server.bootstrap();
+                    return server.app;
+                },
+                initialDataForPopulate,
+                csvFile,
+                channel2.token,
+            );
+        });
+
+        afterAll(async () => {
+            await app.close();
+        });
+
+        it('populates products', async () => {
+            await adminClient.asSuperAdmin();
+            await adminClient.setChannelToken(channel2.token);
+            const { products } = await adminClient.query<GetProductListQuery>(GET_PRODUCT_LIST);
+            expect(products.totalItems).toBe(1);
+            expect(products.items.map(i => i.name).sort()).toEqual(['Model Hand']);
+        });
+
+        it('populates assets', async () => {
+            const { assets } = await adminClient.query<GetAssetListQuery>(GET_ASSET_LIST);
+            expect(assets.items.map(i => i.name).sort()).toEqual(['vincent-botta-736919-unsplash.jpg']);
+        });
+
+        it('populates collections', async () => {
+            const { collections } = await adminClient.query<GetCollectionsQuery>(GET_COLLECTIONS);
+            expect(collections.items.map(i => i.name).sort()).toEqual(['Collection 2']);
+        });
+
+        it('product also assigned to default channel', async () => {
+            await adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
+            const { products } = await adminClient.query<GetProductListQuery>(GET_PRODUCT_LIST);
+            expect(products.items.map(i => i.name).includes('Model Hand')).toBe(true);
+        });
+    });
+});

+ 78 - 0
packages/core/e2e/shop-auth.e2e-spec.ts

@@ -10,6 +10,8 @@ import {
     IdentifierChangeRequestEvent,
     mergeConfig,
     PasswordResetEvent,
+    PasswordValidationStrategy,
+    RequestContext,
     VendurePlugin,
 } from '@vendure/core';
 import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
@@ -19,6 +21,7 @@ import path from 'path';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
 import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
+import { PasswordValidationError } from '../src/common/error/generated-graphql-shop-errors';
 
 import {
     CreateAdministrator,
@@ -94,10 +97,29 @@ const currentUserErrorGuard: ErrorResultGuard<CurrentUserShopFragment> = createE
     input => input.identifier != null,
 );
 
+class TestPasswordValidationStrategy implements PasswordValidationStrategy {
+    validate(ctx: RequestContext, password: string): boolean | string {
+        if (password === 'test') {
+            // allow the default seed data password
+            return true;
+        }
+        if (password.length < 8) {
+            return 'Password must be more than 8 characters';
+        }
+        if (password === '12345678') {
+            return `Don't use 12345678!`;
+        }
+        return true;
+    }
+}
+
 describe('Shop auth & accounts', () => {
     const { server, adminClient, shopClient } = createTestEnvironment(
         mergeConfig(testConfig(), {
             plugins: [TestEmailPlugin as any],
+            authOptions: {
+                passwordValidationStrategy: new TestPasswordValidationStrategy(),
+            },
         }),
     );
 
@@ -276,6 +298,23 @@ describe('Shop auth & accounts', () => {
             expect(verifyCustomerAccount.errorCode).toBe(ErrorCode.MISSING_PASSWORD_ERROR);
         });
 
+        it('verification fails with invalid password', async () => {
+            const { verifyCustomerAccount } = await shopClient.query<Verify.Mutation, Verify.Variables>(
+                VERIFY_EMAIL,
+                {
+                    token: verificationToken,
+                    password: '2short',
+                },
+            );
+            currentUserErrorGuard.assertErrorResult(verifyCustomerAccount);
+
+            expect(verifyCustomerAccount.message).toBe(`Password is invalid`);
+            expect((verifyCustomerAccount as PasswordValidationError).validationErrorMessage).toBe(
+                `Password must be more than 8 characters`,
+            );
+            expect(verifyCustomerAccount.errorCode).toBe(ErrorCode.PASSWORD_VALIDATION_ERROR);
+        });
+
         it('verification succeeds with password and correct token', async () => {
             const { verifyCustomerAccount } = await shopClient.query<Verify.Mutation, Verify.Variables>(
                 VERIFY_EMAIL,
@@ -361,6 +400,28 @@ describe('Shop auth & accounts', () => {
         const emailAddress = 'test2@test.com';
         let verificationToken: string;
 
+        it('registerCustomerAccount fails with invalid password', async () => {
+            const input: RegisterCustomerInput = {
+                firstName: 'Lu',
+                lastName: 'Tester',
+                phoneNumber: '443324',
+                emailAddress,
+                password: '12345678',
+            };
+            const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
+                REGISTER_ACCOUNT,
+                {
+                    input,
+                },
+            );
+            successErrorGuard.assertErrorResult(registerCustomerAccount);
+            expect(registerCustomerAccount.errorCode).toBe(ErrorCode.PASSWORD_VALIDATION_ERROR);
+            expect(registerCustomerAccount.message).toBe(`Password is invalid`);
+            expect((registerCustomerAccount as PasswordValidationError).validationErrorMessage).toBe(
+                `Don't use 12345678!`,
+            );
+        });
+
         it('register a new account with password', async () => {
             const verificationTokenPromise = getVerificationTokenPromise();
             const input: RegisterCustomerInput = {
@@ -497,6 +558,23 @@ describe('Shop auth & accounts', () => {
             expect(resetPassword.errorCode).toBe(ErrorCode.PASSWORD_RESET_TOKEN_INVALID_ERROR);
         });
 
+        it('resetPassword fails with invalid password', async () => {
+            const { resetPassword } = await shopClient.query<ResetPassword.Mutation, ResetPassword.Variables>(
+                RESET_PASSWORD,
+                {
+                    token: passwordResetToken,
+                    password: '2short',
+                },
+            );
+            currentUserErrorGuard.assertErrorResult(resetPassword);
+
+            expect(resetPassword.message).toBe(`Password is invalid`);
+            expect((resetPassword as PasswordValidationError).validationErrorMessage).toBe(
+                `Password must be more than 8 characters`,
+            );
+            expect(resetPassword.errorCode).toBe(ErrorCode.PASSWORD_VALIDATION_ERROR);
+        });
+
         it('resetPassword works with valid token', async () => {
             const { resetPassword } = await shopClient.query<ResetPassword.Mutation, ResetPassword.Variables>(
                 RESET_PASSWORD,

+ 0 - 2
packages/core/src/api/api.module.ts

@@ -10,7 +10,6 @@ import { I18nModule } from '../i18n/i18n.module';
 import { ServiceModule } from '../service/service.module';
 
 import { AdminApiModule, ApiSharedModule, ShopApiModule } from './api-internal-modules';
-import { RequestContextService } from './common/request-context.service';
 import { configureGraphQLModule } from './config/configure-graphql-module';
 import { AuthGuard } from './middleware/auth-guard';
 import { ExceptionLoggerFilter } from './middleware/exception-logger.filter';
@@ -52,7 +51,6 @@ import { ValidateCustomFieldsInterceptor } from './middleware/validate-custom-fi
         })),
     ],
     providers: [
-        RequestContextService,
         {
             provide: APP_GUARD,
             useClass: AuthGuard,

+ 3 - 1
packages/core/src/api/common/request-context.ts

@@ -79,7 +79,9 @@ export class RequestContext {
      * @description
      * Creates an "empty" RequestContext object. This is only intended to be used
      * when a service method must be called outside the normal request-response
-     * cycle, e.g. when programmatically populating data.
+     * cycle, e.g. when programmatically populating data. Usually a better alternative
+     * is to use the {@link RequestContextService} `create()` method, which allows more control
+     * over the resulting RequestContext object.
      */
     static empty(): RequestContext {
         return new RequestContext({

+ 1 - 0
packages/core/src/api/config/configure-graphql-module.ts

@@ -119,6 +119,7 @@ async function createGraphQLOptions(
             ...configService.apiOptions.apolloServerPlugins,
         ],
         validationRules: options.validationRules,
+        introspection: configService.apiOptions.introspection ?? true,
     } as GqlModuleOptions;
 
     /**

+ 30 - 16
packages/core/src/api/config/graphql-custom-fields.ts

@@ -63,7 +63,7 @@ export function addGraphQLCustomFields(
             if (customEntityFields.length) {
                 customFieldTypeDefs += `
                     type ${entityName}CustomFields {
-                        ${mapToFields(customEntityFields, getGraphQlType)}
+                        ${mapToFields(customEntityFields, wrapListType(getGraphQlType))}
                     }
 
                     extend type ${entityName} {
@@ -82,7 +82,7 @@ export function addGraphQLCustomFields(
         if (localeStringFields.length && schema.getType(`${entityName}Translation`)) {
             customFieldTypeDefs += `
                     type ${entityName}TranslationCustomFields {
-                         ${mapToFields(localeStringFields, getGraphQlType)}
+                         ${mapToFields(localeStringFields, wrapListType(getGraphQlType))}
                     }
 
                     extend type ${entityName}Translation {
@@ -97,7 +97,7 @@ export function addGraphQLCustomFields(
                     input Create${entityName}CustomFieldsInput {
                        ${mapToFields(
                            writeableNonLocaleStringFields,
-                           getGraphQlInputType,
+                           wrapListType(getGraphQlInputType),
                            getGraphQlInputName,
                        )}
                     }
@@ -121,7 +121,7 @@ export function addGraphQLCustomFields(
                     input Update${entityName}CustomFieldsInput {
                        ${mapToFields(
                            writeableNonLocaleStringFields,
-                           getGraphQlInputType,
+                           wrapListType(getGraphQlInputType),
                            getGraphQlInputName,
                        )}
                     }
@@ -139,10 +139,13 @@ export function addGraphQLCustomFields(
             }
         }
 
-        if (customEntityFields.length && schema.getType(`${entityName}SortParameter`)) {
+        const customEntityNonListFields = customEntityFields.filter(f => f.list !== true);
+        if (customEntityNonListFields.length && schema.getType(`${entityName}SortParameter`)) {
+            // Sorting list fields makes no sense, so we only add "sort" fields
+            // to non-list fields.
             customFieldTypeDefs += `
                     extend input ${entityName}SortParameter {
-                         ${mapToFields(customEntityFields, () => 'SortOrder')}
+                         ${mapToFields(customEntityNonListFields, () => 'SortOrder')}
                     }
                 `;
         }
@@ -166,7 +169,7 @@ export function addGraphQLCustomFields(
                     if (writeableLocaleStringFields.length) {
                         customFieldTypeDefs += `
                             input ${inputName}CustomFields {
-                                ${mapToFields(writeableLocaleStringFields, getGraphQlType)}
+                                ${mapToFields(writeableLocaleStringFields, wrapListType(getGraphQlType))}
                             }
 
                             extend input ${inputName} {
@@ -275,7 +278,7 @@ export function addRegisterCustomerCustomFieldsInput(
     }
     const customFieldTypeDefs = `
         input RegisterCustomerCustomFieldsInput {
-            ${mapToFields(publicWritableCustomFields, getGraphQlInputType, getGraphQlInputName)}
+            ${mapToFields(publicWritableCustomFields, wrapListType(getGraphQlInputType), getGraphQlInputName)}
         }
 
         extend input RegisterCustomerInput {
@@ -444,13 +447,12 @@ function mapToFields(
 ): string {
     const res = fieldDefs
         .map(field => {
-            const primitiveType = typeFn(field);
-            if (!primitiveType) {
+            const type = typeFn(field);
+            if (!type) {
                 return;
             }
-            const finalType = field.list ? `[${primitiveType}!]` : primitiveType;
             const name = nameFn ? nameFn(field) : field.name;
-            return `${name}: ${finalType}`;
+            return `${name}: ${type}`;
         })
         .filter(x => x != null);
     return res.join('\n');
@@ -459,16 +461,16 @@ function mapToFields(
 function getFilterOperator(config: CustomFieldConfig): string | undefined {
     switch (config.type) {
         case 'datetime':
-            return 'DateOperators';
+            return config.list ? 'DateListOperators' : 'DateOperators';
         case 'string':
         case 'localeString':
         case 'text':
-            return 'StringOperators';
+            return config.list ? 'StringListOperators' : 'StringOperators';
         case 'boolean':
-            return 'BooleanOperators';
+            return config.list ? 'BooleanListOperators' : 'BooleanOperators';
         case 'int':
         case 'float':
-            return 'NumberOperators';
+            return config.list ? 'NumberListOperators' : 'NumberOperators';
         case 'relation':
             return undefined;
         default:
@@ -481,6 +483,18 @@ function getGraphQlInputType(config: CustomFieldConfig): string {
     return config.type === 'relation' ? `ID` : getGraphQlType(config);
 }
 
+function wrapListType(
+    getTypeFn: (def: CustomFieldConfig) => string | undefined,
+): (def: CustomFieldConfig) => string | undefined {
+    return (def: CustomFieldConfig) => {
+        const type = getTypeFn(def);
+        if (!type) {
+            return;
+        }
+        return def.list ? `[${type}!]` : type;
+    };
+}
+
 function getGraphQlType(config: CustomFieldConfig): string {
     switch (config.type) {
         case 'string':

+ 1 - 1
packages/core/src/api/middleware/auth-guard.ts

@@ -10,13 +10,13 @@ import { ConfigService } from '../../config/config.service';
 import { LogLevel } from '../../config/logger/vendure-logger';
 import { CachedSession } from '../../config/session-cache/session-cache-strategy';
 import { Customer } from '../../entity/customer/customer.entity';
+import { RequestContextService } from '../../service/helpers/request-context/request-context.service';
 import { ChannelService } from '../../service/services/channel.service';
 import { CustomerService } from '../../service/services/customer.service';
 import { SessionService } from '../../service/services/session.service';
 import { extractSessionToken } from '../common/extract-session-token';
 import { parseContext } from '../common/parse-context';
 import { RequestContext } from '../common/request-context';
-import { RequestContextService } from '../common/request-context.service';
 import { setSessionToken } from '../common/set-session-token';
 import { PERMISSIONS_METADATA_KEY } from '../decorators/allow.decorator';
 

+ 4 - 0
packages/core/src/api/schema/admin-api/customer.api.graphql

@@ -36,6 +36,10 @@ input UpdateCustomerInput {
     emailAddress: String
 }
 
+input CustomerFilterParameter {
+    postalCode: StringOperators
+}
+
 # generated by generateListOptions function
 input CustomerListOptions
 

+ 6 - 0
packages/core/src/api/schema/admin-api/order.api.graphql

@@ -63,6 +63,8 @@ input CancelOrderInput {
     orderId: ID!
     "Optionally specify which OrderLines to cancel. If not provided, all OrderLines will be cancelled"
     lines: [OrderLineInput!]
+    "Specify whether the shipping charges should also be cancelled. Defaults to false"
+    cancelShipping: Boolean
     reason: String
 }
 
@@ -134,6 +136,7 @@ input ModifyOrderInput {
     note: String
     refund: AdministratorRefundInput
     options: ModifyOrderOptions
+    couponCodes: [String!]
 }
 
 input AddItemInput {
@@ -365,4 +368,7 @@ union ModifyOrderResult =
     | OrderLimitError
     | NegativeQuantityError
     | InsufficientStockError
+    | CouponCodeExpiredError
+    | CouponCodeInvalidError
+    | CouponCodeLimitError
 union AddManualPaymentToOrderResult = Order | ManualPaymentStateError

+ 1 - 0
packages/core/src/api/schema/common/common-enums.graphql

@@ -7,6 +7,7 @@ enum GlobalFlag {
 enum AdjustmentType {
     PROMOTION
     DISTRIBUTED_ORDER_PROMOTION
+    OTHER
 }
 
 enum DeletionResult {

+ 22 - 0
packages/core/src/api/schema/common/common-error-results.graphql

@@ -46,3 +46,25 @@ type InsufficientStockError implements ErrorResult {
     quantityAvailable: Int!
     order: Order!
 }
+
+"Returned if the provided coupon code is invalid"
+type CouponCodeInvalidError implements ErrorResult {
+    errorCode: ErrorCode!
+    message: String!
+    couponCode: String!
+}
+
+"Returned if the provided coupon code is invalid"
+type CouponCodeExpiredError implements ErrorResult {
+    errorCode: ErrorCode!
+    message: String!
+    couponCode: String!
+}
+
+"Returned if the provided coupon code is invalid"
+type CouponCodeLimitError implements ErrorResult {
+    errorCode: ErrorCode!
+    message: String!
+    couponCode: String!
+    limit: Int!
+}

+ 25 - 0
packages/core/src/api/schema/common/common-types.graphql

@@ -125,6 +125,31 @@ input DateOperators {
     between: DateRange
 }
 
+"Operators for filtering on a list of String fields"
+input StringListOperators {
+    inList: String!
+}
+
+"Operators for filtering on a list of Number fields"
+input NumberListOperators {
+    inList: Float!
+}
+
+"Operators for filtering on a list of Boolean fields"
+input BooleanListOperators {
+    inList: Boolean!
+}
+
+"Operators for filtering on a list of ID fields"
+input IDListOperators {
+    inList: ID!
+}
+
+"Operators for filtering on a list of Date fields"
+input DateListOperators {
+    inList: DateTime!
+}
+
 """
 Used to construct boolean expressions for filtering search results
 by FacetValue ID. Examples:

+ 7 - 29
packages/core/src/api/schema/shop-api/shop-error-results.graphql

@@ -37,35 +37,6 @@ type PaymentDeclinedError implements ErrorResult {
     paymentErrorMessage: String!
 }
 
-"Returned if the provided coupon code is invalid"
-type CouponCodeInvalidError implements ErrorResult {
-    errorCode: ErrorCode!
-    message: String!
-    couponCode: String!
-}
-
-"Returned if the provided coupon code is invalid"
-type CouponCodeInvalidError implements ErrorResult {
-    errorCode: ErrorCode!
-    message: String!
-    couponCode: String!
-}
-
-"Returned if the provided coupon code is invalid"
-type CouponCodeExpiredError implements ErrorResult {
-    errorCode: ErrorCode!
-    message: String!
-    couponCode: String!
-}
-
-"Returned if the provided coupon code is invalid"
-type CouponCodeLimitError implements ErrorResult {
-    errorCode: ErrorCode!
-    message: String!
-    couponCode: String!
-    limit: Int!
-}
-
 "Returned when attempting to set the Customer for an Order when already logged in."
 type AlreadyLoggedInError implements ErrorResult {
     errorCode: ErrorCode!
@@ -78,6 +49,13 @@ type MissingPasswordError implements ErrorResult {
     message: String!
 }
 
+"Returned when attempting to register or verify a customer account where the given password fails password validation."
+type PasswordValidationError implements ErrorResult {
+    errorCode: ErrorCode!
+    message: String!
+    validationErrorMessage: String!
+}
+
 "Returned when attempting to verify a customer account with a password, when a password has already been set."
 type PasswordAlreadySetError implements ErrorResult {
     errorCode: ErrorCode!

+ 4 - 2
packages/core/src/api/schema/shop-api/shop.api.graphql

@@ -213,16 +213,17 @@ union AddPaymentToOrderResult =
     | NoActiveOrderError
 union TransitionOrderToStateResult = Order | OrderStateTransitionError
 union SetCustomerForOrderResult = Order | AlreadyLoggedInError | EmailAddressConflictError | NoActiveOrderError
-union RegisterCustomerAccountResult = Success | MissingPasswordError | NativeAuthStrategyError
+union RegisterCustomerAccountResult = Success | MissingPasswordError | PasswordValidationError | NativeAuthStrategyError
 union RefreshCustomerVerificationResult = Success | NativeAuthStrategyError
 union VerifyCustomerAccountResult =
       CurrentUser
     | VerificationTokenInvalidError
     | VerificationTokenExpiredError
     | MissingPasswordError
+    | PasswordValidationError
     | PasswordAlreadySetError
     | NativeAuthStrategyError
-union UpdateCustomerPasswordResult = Success | InvalidCredentialsError | NativeAuthStrategyError
+union UpdateCustomerPasswordResult = Success | InvalidCredentialsError | PasswordValidationError | NativeAuthStrategyError
 union RequestUpdateCustomerEmailAddressResult =
       Success
     | InvalidCredentialsError
@@ -238,6 +239,7 @@ union ResetPasswordResult =
       CurrentUser
     | PasswordResetTokenInvalidError
     | PasswordResetTokenExpiredError
+    | PasswordValidationError
     | NativeAuthStrategyError
     | NotVerifiedError
 union NativeAuthenticationResult =

+ 82 - 25
packages/core/src/cli/populate.ts

@@ -2,13 +2,48 @@ import { INestApplicationContext } from '@nestjs/common';
 import fs from 'fs-extra';
 import path from 'path';
 
-import { logColored } from './cli-utils';
+const loggerCtx = 'Populate';
 
 // tslint:disable:no-console
 /**
  * @description
  * Populates the Vendure server with some initial data and (optionally) product data from
- * a supplied CSV file.
+ * a supplied CSV file. The format of the CSV file is described in the section
+ * [Importing Product Data](/docs/developer-guide/importing-product-data).
+ *
+ * If the `channelOrToken` argument is provided, all ChannelAware entities (Products, ProductVariants,
+ * Assets, ShippingMethods, PaymentMethods etc.) will be assigned to the specified Channel.
+ * The argument can be either a Channel object or a valid channel `token`.
+ *
+ * Internally the `populate()` function does the following:
+ *
+ * 1. Uses the {@link Populator} to populate the {@link InitialData}.
+ * 2. If `productsCsvPath` is provided, uses {@link Importer} to populate Product data.
+ * 3. Uses {@Populator} to populate collections specified in the {@link InitialData}.
+ *
+ * @example
+ * ```TypeScript
+ * import { bootstrap } from '\@vendure/core';
+ * import { populate } from '\@vendure/core/cli';
+ * import { config } from './vendure-config.ts'
+ * import { initialData } from './my-initial-data.ts';
+ *
+ * const productsCsvFile = path.join(__dirname, 'path/to/products.csv')
+ *
+ * populate(
+ *   () => bootstrap(config),
+ *   initialData,
+ *   productsCsvFile,
+ * )
+ * .then(app => app.close())
+ * .then(
+ *   () => process.exit(0),
+ *   err => {
+ *     console.log(err);
+ *     process.exit(1);
+ *   },
+ * );
+ * ```
  *
  * @docsCategory import-export
  */
@@ -16,70 +51,86 @@ export async function populate<T extends INestApplicationContext>(
     bootstrapFn: () => Promise<T | undefined>,
     initialDataPathOrObject: string | object,
     productsCsvPath?: string,
+    channelOrToken?: string | import('@vendure/core').Channel,
 ): Promise<T> {
     const app = await bootstrapFn();
     if (!app) {
         throw new Error('Could not bootstrap the Vendure app');
     }
+    let channel: import('@vendure/core').Channel | undefined;
+    const { ChannelService, Channel, Logger } = await import('@vendure/core');
+    if (typeof channelOrToken === 'string') {
+        channel = await app.get(ChannelService).getChannelFromToken(channelOrToken);
+        if (!channel) {
+            Logger.warn(
+                `Warning: channel with token "${channelOrToken}" was not found. Using default Channel instead.`,
+                loggerCtx,
+            );
+        }
+    } else if (channelOrToken instanceof Channel) {
+        channel = channelOrToken;
+    }
     const initialData: import('@vendure/core').InitialData =
         typeof initialDataPathOrObject === 'string'
             ? require(initialDataPathOrObject)
             : initialDataPathOrObject;
 
-    await populateInitialData(app, initialData, logColored);
+    await populateInitialData(app, initialData, channel);
 
     if (productsCsvPath) {
-        const importResult = await importProductsFromCsv(app, productsCsvPath, initialData.defaultLanguage);
+        const importResult = await importProductsFromCsv(
+            app,
+            productsCsvPath,
+            initialData.defaultLanguage,
+            channel,
+        );
         if (importResult.errors && importResult.errors.length) {
             const errorFile = path.join(process.cwd(), 'vendure-import-error.log');
-            console.log(
+            Logger.error(
                 `${importResult.errors.length} errors encountered when importing product data. See: ${errorFile}`,
+                loggerCtx,
             );
             await fs.writeFile(errorFile, importResult.errors.join('\n'));
         }
 
-        logColored(`\nImported ${importResult.imported} products`);
+        Logger.info(`Imported ${importResult.imported} products`, loggerCtx);
 
-        await populateCollections(app, initialData, logColored);
+        await populateCollections(app, initialData, channel);
     }
 
-    logColored('\nDone!');
+    Logger.info('Done!', loggerCtx);
     return app;
 }
 
 export async function populateInitialData(
     app: INestApplicationContext,
     initialData: import('@vendure/core').InitialData,
-    loggingFn?: (message: string) => void,
+    channel?: import('@vendure/core').Channel,
 ) {
-    const { Populator } = await import('@vendure/core');
+    const { Populator, Logger } = await import('@vendure/core');
     const populator = app.get(Populator);
     try {
-        await populator.populateInitialData(initialData);
-        if (typeof loggingFn === 'function') {
-            loggingFn(`Populated initial data`);
-        }
+        await populator.populateInitialData(initialData, channel);
+        Logger.info(`Populated initial data`, loggerCtx);
     } catch (err) {
-        console.log(err.message);
+        Logger.error(err.message, loggerCtx);
     }
 }
 
 export async function populateCollections(
     app: INestApplicationContext,
     initialData: import('@vendure/core').InitialData,
-    loggingFn?: (message: string) => void,
+    channel?: import('@vendure/core').Channel,
 ) {
-    const { Populator } = await import('@vendure/core');
+    const { Populator, Logger } = await import('@vendure/core');
     const populator = app.get(Populator);
     try {
         if (initialData.collections.length) {
-            await populator.populateCollections(initialData);
-            if (typeof loggingFn === 'function') {
-                loggingFn(`Created ${initialData.collections.length} Collections`);
-            }
+            await populator.populateCollections(initialData, channel);
+            Logger.info(`Created ${initialData.collections.length} Collections`, loggerCtx);
         }
     } catch (err) {
-        console.log(err.message);
+        Logger.info(err.message, loggerCtx);
     }
 }
 
@@ -87,10 +138,16 @@ export async function importProductsFromCsv(
     app: INestApplicationContext,
     productsCsvPath: string,
     languageCode: import('@vendure/core').LanguageCode,
+    channel?: import('@vendure/core').Channel,
 ): Promise<import('@vendure/core').ImportProgress> {
-    const { Importer } = await import('@vendure/core');
+    const { Importer, RequestContextService } = await import('@vendure/core');
     const importer = app.get(Importer);
+    const requestContextService = app.get(RequestContextService);
     const productData = await fs.readFile(productsCsvPath, 'utf-8');
-
-    return importer.parseAndImport(productData, languageCode, true).toPromise();
+    const ctx = await requestContextService.create({
+        apiType: 'admin',
+        languageCode,
+        channelOrToken: channel,
+    });
+    return importer.parseAndImport(productData, ctx, true).toPromise();
 }

+ 35 - 1
packages/core/src/common/error/generated-graphql-admin-errors.ts

@@ -52,6 +52,40 @@ export class ChannelDefaultLanguageError extends ErrorResult {
   }
 }
 
+export class CouponCodeExpiredError extends ErrorResult {
+  readonly __typename = 'CouponCodeExpiredError';
+  readonly errorCode = 'COUPON_CODE_EXPIRED_ERROR' as any;
+  readonly message = 'COUPON_CODE_EXPIRED_ERROR';
+  constructor(
+    public couponCode: Scalars['String'],
+  ) {
+    super();
+  }
+}
+
+export class CouponCodeInvalidError extends ErrorResult {
+  readonly __typename = 'CouponCodeInvalidError';
+  readonly errorCode = 'COUPON_CODE_INVALID_ERROR' as any;
+  readonly message = 'COUPON_CODE_INVALID_ERROR';
+  constructor(
+    public couponCode: Scalars['String'],
+  ) {
+    super();
+  }
+}
+
+export class CouponCodeLimitError extends ErrorResult {
+  readonly __typename = 'CouponCodeLimitError';
+  readonly errorCode = 'COUPON_CODE_LIMIT_ERROR' as any;
+  readonly message = 'COUPON_CODE_LIMIT_ERROR';
+  constructor(
+    public couponCode: Scalars['String'],
+    public limit: Scalars['Int'],
+  ) {
+    super();
+  }
+}
+
 export class CreateFulfillmentError extends ErrorResult {
   readonly __typename = 'CreateFulfillmentError';
   readonly errorCode = 'CREATE_FULFILLMENT_ERROR' as any;
@@ -380,7 +414,7 @@ export class SettlePaymentError extends ErrorResult {
 }
 
 
-const errorTypeNames = new Set(['AlreadyRefundedError', 'CancelActiveOrderError', 'ChannelDefaultLanguageError', 'CreateFulfillmentError', 'EmailAddressConflictError', 'EmptyOrderLineSelectionError', 'FulfillmentStateTransitionError', 'InsufficientStockError', 'InsufficientStockOnHandError', 'InvalidCredentialsError', 'InvalidFulfillmentHandlerError', 'ItemsAlreadyFulfilledError', 'LanguageNotAvailableError', 'ManualPaymentStateError', 'MimeTypeError', 'MissingConditionsError', 'MultipleOrderError', 'NativeAuthStrategyError', 'NegativeQuantityError', 'NoChangesSpecifiedError', 'NothingToRefundError', 'OrderLimitError', 'OrderModificationStateError', 'OrderStateTransitionError', 'PaymentMethodMissingError', 'PaymentOrderMismatchError', 'PaymentStateTransitionError', 'ProductOptionInUseError', 'QuantityTooGreatError', 'RefundOrderStateError', 'RefundPaymentIdMissingError', 'RefundStateTransitionError', 'SettlePaymentError']);
+const errorTypeNames = new Set(['AlreadyRefundedError', 'CancelActiveOrderError', 'ChannelDefaultLanguageError', 'CouponCodeExpiredError', 'CouponCodeInvalidError', 'CouponCodeLimitError', 'CreateFulfillmentError', 'EmailAddressConflictError', 'EmptyOrderLineSelectionError', 'FulfillmentStateTransitionError', 'InsufficientStockError', 'InsufficientStockOnHandError', 'InvalidCredentialsError', 'InvalidFulfillmentHandlerError', 'ItemsAlreadyFulfilledError', 'LanguageNotAvailableError', 'ManualPaymentStateError', 'MimeTypeError', 'MissingConditionsError', 'MultipleOrderError', 'NativeAuthStrategyError', 'NegativeQuantityError', 'NoChangesSpecifiedError', 'NothingToRefundError', 'OrderLimitError', 'OrderModificationStateError', 'OrderStateTransitionError', 'PaymentMethodMissingError', 'PaymentOrderMismatchError', 'PaymentStateTransitionError', 'ProductOptionInUseError', 'QuantityTooGreatError', 'RefundOrderStateError', 'RefundPaymentIdMissingError', 'RefundStateTransitionError', 'SettlePaymentError']);
 function isGraphQLError(input: any): input is import('@vendure/common/lib/generated-types').ErrorResult {
   return input instanceof ErrorResult || errorTypeNames.has(input.__typename);
 }

+ 12 - 1
packages/core/src/common/error/generated-graphql-shop-errors.ts

@@ -260,6 +260,17 @@ export class PasswordResetTokenInvalidError extends ErrorResult {
   }
 }
 
+export class PasswordValidationError extends ErrorResult {
+  readonly __typename = 'PasswordValidationError';
+  readonly errorCode = 'PASSWORD_VALIDATION_ERROR' as any;
+  readonly message = 'PASSWORD_VALIDATION_ERROR';
+  constructor(
+    public validationErrorMessage: Scalars['String'],
+  ) {
+    super();
+  }
+}
+
 export class PaymentDeclinedError extends ErrorResult {
   readonly __typename = 'PaymentDeclinedError';
   readonly errorCode = 'PAYMENT_DECLINED_ERROR' as any;
@@ -303,7 +314,7 @@ export class VerificationTokenInvalidError extends ErrorResult {
 }
 
 
-const errorTypeNames = new Set(['AlreadyLoggedInError', 'CouponCodeExpiredError', 'CouponCodeInvalidError', 'CouponCodeLimitError', 'EmailAddressConflictError', 'IdentifierChangeTokenExpiredError', 'IdentifierChangeTokenInvalidError', 'IneligiblePaymentMethodError', 'IneligibleShippingMethodError', 'InsufficientStockError', 'InvalidCredentialsError', 'MissingPasswordError', 'NativeAuthStrategyError', 'NegativeQuantityError', 'NoActiveOrderError', 'NotVerifiedError', 'OrderLimitError', 'OrderModificationError', 'OrderPaymentStateError', 'OrderStateTransitionError', 'PasswordAlreadySetError', 'PasswordResetTokenExpiredError', 'PasswordResetTokenInvalidError', 'PaymentDeclinedError', 'PaymentFailedError', 'VerificationTokenExpiredError', 'VerificationTokenInvalidError']);
+const errorTypeNames = new Set(['AlreadyLoggedInError', 'CouponCodeExpiredError', 'CouponCodeInvalidError', 'CouponCodeLimitError', 'EmailAddressConflictError', 'IdentifierChangeTokenExpiredError', 'IdentifierChangeTokenInvalidError', 'IneligiblePaymentMethodError', 'IneligibleShippingMethodError', 'InsufficientStockError', 'InvalidCredentialsError', 'MissingPasswordError', 'NativeAuthStrategyError', 'NegativeQuantityError', 'NoActiveOrderError', 'NotVerifiedError', 'OrderLimitError', 'OrderModificationError', 'OrderPaymentStateError', 'OrderStateTransitionError', 'PasswordAlreadySetError', 'PasswordResetTokenExpiredError', 'PasswordResetTokenInvalidError', 'PasswordValidationError', 'PaymentDeclinedError', 'PaymentFailedError', 'VerificationTokenExpiredError', 'VerificationTokenInvalidError']);
 function isGraphQLError(input: any): input is import('@vendure/common/lib/generated-types').ErrorResult {
   return input instanceof ErrorResult || errorTypeNames.has(input.__typename);
 }

+ 1 - 1
packages/core/src/common/finite-state-machine/finite-state-machine.ts

@@ -76,7 +76,7 @@ export class FSM<T extends string, Data = any> {
      * Returns an array of state to which the machine may transition from the current state.
      */
     getNextStates(): ReadonlyArray<T> {
-        return this.config.transitions[this._currentState].to;
+        return this.config.transitions[this._currentState]?.to ?? [];
     }
 
     /**

Някои файлове не бяха показани, защото твърде много файлове са промени