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 %}}
 {{% alert %}}
 **UI for relation type**
 **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 >}}
 {{< /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:
 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.
 * 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. 
 * 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.
 * 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.
 * 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
 ## 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:
 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
 ```TypeScript
@@ -203,7 +204,9 @@ populate(
   () => bootstrap(config),
   () => bootstrap(config),
   initialData,
   initialData,
   productsCsvFile,
   productsCsvFile,
-)
+  'my-channel-token' // optional - used to assign imported 
+)                    // entities to the specified Channel
+
 .then(app => {
 .then(app => {
   return app.close();
   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" >}}).
 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',
                 'hideVersion',
                 AdminUiPlugin.options.adminUiConfig?.hideVersion || false,
                 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": {
   "translationStatus": {
     "cs": {
     "cs": {
-      "tokenCount": 634,
+      "tokenCount": 640,
       "translatedCount": 591,
       "translatedCount": 591,
-      "percentage": 93
+      "percentage": 92
     },
     },
     "de": {
     "de": {
-      "tokenCount": 634,
+      "tokenCount": 640,
       "translatedCount": 570,
       "translatedCount": 570,
-      "percentage": 90
+      "percentage": 89
     },
     },
     "en": {
     "en": {
-      "tokenCount": 634,
-      "translatedCount": 628,
-      "percentage": 99
+      "tokenCount": 640,
+      "translatedCount": 640,
+      "percentage": 100
     },
     },
     "es": {
     "es": {
-      "tokenCount": 634,
+      "tokenCount": 640,
       "translatedCount": 623,
       "translatedCount": 623,
-      "percentage": 98
+      "percentage": 97
     },
     },
     "fr": {
     "fr": {
-      "tokenCount": 634,
+      "tokenCount": 640,
       "translatedCount": 613,
       "translatedCount": 613,
-      "percentage": 97
+      "percentage": 96
     },
     },
     "it": {
     "it": {
-      "tokenCount": 634,
+      "tokenCount": 640,
       "translatedCount": 621,
       "translatedCount": 621,
-      "percentage": 98
+      "percentage": 97
     },
     },
     "pl": {
     "pl": {
-      "tokenCount": 634,
+      "tokenCount": 640,
       "translatedCount": 405,
       "translatedCount": 405,
-      "percentage": 64
+      "percentage": 63
     },
     },
     "pt_BR": {
     "pt_BR": {
-      "tokenCount": 634,
-      "translatedCount": 588,
-      "percentage": 93
+      "tokenCount": 640,
+      "translatedCount": 589,
+      "percentage": 92
     },
     },
     "pt_PT": {
     "pt_PT": {
-      "tokenCount": 634,
-      "translatedCount": 622,
-      "percentage": 98
+      "tokenCount": 640,
+      "translatedCount": 634,
+      "percentage": 99
     },
     },
     "ru": {
     "ru": {
-      "tokenCount": 634,
-      "translatedCount": 621,
-      "percentage": 98
+      "tokenCount": 640,
+      "translatedCount": 620,
+      "percentage": 97
     },
     },
     "uk": {
     "uk": {
-      "tokenCount": 634,
-      "translatedCount": 621,
-      "percentage": 98
+      "tokenCount": 640,
+      "translatedCount": 620,
+      "percentage": 97
     },
     },
     "zh_Hans": {
     "zh_Hans": {
-      "tokenCount": 634,
-      "translatedCount": 558,
-      "percentage": 88
+      "tokenCount": 640,
+      "translatedCount": 557,
+      "percentage": 87
     },
     },
     "zh_Hant": {
     "zh_Hant": {
-      "tokenCount": 634,
+      "tokenCount": 640,
       "translatedCount": 385,
       "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 { ApplyFacetDialogComponent } from './components/apply-facet-dialog/apply-facet-dialog.component';
 import { AssetDetailComponent } from './components/asset-detail/asset-detail.component';
 import { AssetDetailComponent } from './components/asset-detail/asset-detail.component';
 import { AssetListComponent } from './components/asset-list/asset-list.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 { AssignProductsToChannelDialogComponent } from './components/assign-products-to-channel-dialog/assign-products-to-channel-dialog.component';
 import { CollectionContentsComponent } from './components/collection-contents/collection-contents.component';
 import { CollectionContentsComponent } from './components/collection-contents/collection-contents.component';
 import { CollectionDetailComponent } from './components/collection-detail/collection-detail.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 { FacetListComponent } from './components/facet-list/facet-list.component';
 import { GenerateProductVariantsComponent } from './components/generate-product-variants/generate-product-variants.component';
 import { GenerateProductVariantsComponent } from './components/generate-product-variants/generate-product-variants.component';
 import { OptionValueInputComponent } from './components/option-value-input/option-value-input.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 { ProductDetailComponent } from './components/product-detail/product-detail.component';
 import { ProductListComponent } from './components/product-list/product-list.component';
 import { ProductListComponent } from './components/product-list/product-list.component';
 import { ProductOptionsEditorComponent } from './components/product-options-editor/product-options-editor.component';
 import { ProductOptionsEditorComponent } from './components/product-options-editor/product-options-editor.component';
@@ -37,7 +37,7 @@ const CATALOG_COMPONENTS = [
     ProductVariantsListComponent,
     ProductVariantsListComponent,
     ApplyFacetDialogComponent,
     ApplyFacetDialogComponent,
     AssetListComponent,
     AssetListComponent,
-    ProductAssetsComponent,
+    AssetsComponent,
     VariantPriceDetailComponent,
     VariantPriceDetailComponent,
     CollectionListComponent,
     CollectionListComponent,
     CollectionDetailComponent,
     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 { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
-import { ViewportRuler } from '@angular/cdk/overlay';
 import {
 import {
     ChangeDetectionStrategy,
     ChangeDetectionStrategy,
     ChangeDetectorRef,
     ChangeDetectorRef,
@@ -7,7 +6,6 @@ import {
     EventEmitter,
     EventEmitter,
     HostBinding,
     HostBinding,
     Input,
     Input,
-    Optional,
     Output,
     Output,
 } from '@angular/core';
 } from '@angular/core';
 import {
 import {
@@ -19,27 +17,25 @@ import {
 } from '@vendure/admin-ui/core';
 } from '@vendure/admin-ui/core';
 import { unique } from '@vendure/common/lib/unique';
 import { unique } from '@vendure/common/lib/unique';
 
 
-import { CollectionDetailComponent } from '../collection-detail/collection-detail.component';
-
 export interface AssetChange {
 export interface AssetChange {
     assets: Asset[];
     assets: Asset[];
     featuredAsset: Asset | undefined;
     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.
  * 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
  * 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
  * which is addressed by a work-around from here: https://github.com/angular/components/issues/13372#issuecomment-483998378
  */
  */
 @Component({
 @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,
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 })
-export class ProductAssetsComponent {
+export class AssetsComponent {
     @Input('assets') set assetsSetter(val: Asset[]) {
     @Input('assets') set assetsSetter(val: Asset[]) {
         // create a new non-readonly array of assets
         // create a new non-readonly array of assets
         this.assets = (val || []).slice();
         this.assets = (val || []).slice();
@@ -53,23 +49,10 @@ export class ProductAssetsComponent {
 
 
     public assets: Asset[] = [];
     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() {
     selectAssets() {
         this.modalService
         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>
             ></vdr-custom-detail-component-host>
         </div>
         </div>
         <div class="clr-col-md-auto">
         <div class="clr-col-md-auto">
-            <vdr-product-assets
+            <vdr-assets
                 [assets]="category.assets"
                 [assets]="category.assets"
                 [featuredAsset]="category.featuredAsset"
                 [featuredAsset]="category.featuredAsset"
+                [updatePermissions]="updatePermission"
                 (change)="assetChanges = $event"
                 (change)="assetChanges = $event"
-            ></vdr-product-assets>
+            ></vdr-assets>
         </div>
         </div>
     </div>
     </div>
     <div class="clr-row" formArrayName="filters">
     <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>
                         </section>
                     </div>
                     </div>
                     <div class="clr-col-md-auto">
                     <div class="clr-col-md-auto">
-                        <vdr-product-assets
+                        <vdr-assets
                             [assets]="assetChanges.assets || product.assets"
                             [assets]="assetChanges.assets || product.assets"
                             [featuredAsset]="assetChanges.featuredAsset || product.featuredAsset"
                             [featuredAsset]="assetChanges.featuredAsset || product.featuredAsset"
+                            [updatePermissions]="updatePermissions"
                             (change)="assetChanges = $event"
                             (change)="assetChanges = $event"
-                        ></vdr-product-assets>
+                        ></vdr-assets>
                         <div class="facets">
                         <div class="facets">
                             <vdr-facet-value-chip
                             <vdr-facet-value-chip
                                 *ngFor="let facetValue of facetValues$ | async"
                                 *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,
     LogicalOperator,
     ModalService,
     ModalService,
     NotificationService,
     NotificationService,
+    Permission,
     ProductDetail,
     ProductDetail,
     ProductVariant,
     ProductVariant,
     ServerConfigService,
     ServerConfigService,
@@ -123,6 +124,7 @@ export class ProductDetailComponent
     // Used to store all ProductVariants which have been loaded.
     // Used to store all ProductVariants which have been loaded.
     // It is needed when saving changes to variants.
     // It is needed when saving changes to variants.
     private productVariantMap = new Map<string, ProductVariant.Fragment>();
     private productVariantMap = new Map<string, ProductVariant.Fragment>();
+    public readonly updatePermissions = [Permission.UpdateCatalog, Permission.UpdateProduct];
 
 
     constructor(
     constructor(
         route: ActivatedRoute,
         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="card-block">
                 <div class="variant-body">
                 <div class="variant-body">
                     <div class="assets">
                     <div class="assets">
-                        <vdr-product-assets
+                        <vdr-assets
                             [compact]="true"
                             [compact]="true"
                             [assets]="pendingAssetChanges[variant.id]?.assets || variant.assets"
                             [assets]="pendingAssetChanges[variant.id]?.assets || variant.assets"
                             [featuredAsset]="
                             [featuredAsset]="
                                 pendingAssetChanges[variant.id]?.featuredAsset || variant.featuredAsset
                                 pendingAssetChanges[variant.id]?.featuredAsset || variant.featuredAsset
                             "
                             "
+                            [updatePermissions]="updatePermission"
                             (change)="onAssetChange(variant.id, $event)"
                             (change)="onAssetChange(variant.id, $event)"
-                        ></vdr-product-assets>
+                        ></vdr-assets>
                     </div>
                     </div>
                     <div class="variant-form-inputs">
                     <div class="variant-form-inputs">
                         <div class="standard-fields">
                         <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 { Subscription } from 'rxjs';
 import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators';
 import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators';
 
 
-import { AssetChange } from '../product-assets/product-assets.component';
+import { AssetChange } from '../assets/assets.component';
 import {
 import {
     PaginationConfig,
     PaginationConfig,
     SelectedAssets,
     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/apply-facet-dialog/apply-facet-dialog.component';
 export * from './components/asset-detail/asset-detail.component';
 export * from './components/asset-detail/asset-detail.component';
 export * from './components/asset-list/asset-list.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/assign-products-to-channel-dialog/assign-products-to-channel-dialog.component';
 export * from './components/collection-contents/collection-contents.component';
 export * from './components/collection-contents/collection-contents.component';
 export * from './components/collection-detail/collection-detail.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/facet-list/facet-list.component';
 export * from './components/generate-product-variants/generate-product-variants.component';
 export * from './components/generate-product-variants/generate-product-variants.component';
 export * from './components/option-value-input/option-value-input.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-detail/product-detail.component';
 export * from './components/product-list/product-list.component';
 export * from './components/product-list/product-list.component';
 export * from './components/product-options-editor/product-options-editor.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 {
 export enum AdjustmentType {
   PROMOTION = 'PROMOTION',
   PROMOTION = 'PROMOTION',
-  DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION'
+  DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
+  OTHER = 'OTHER'
 }
 }
 
 
 export type Administrator = Node & {
 export type Administrator = Node & {
@@ -276,6 +277,11 @@ export type BooleanCustomFieldConfig = CustomField & {
   ui?: Maybe<Scalars['JSON']>;
   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 */
 /** Operators for filtering on a Boolean field */
 export type BooleanOperators = {
 export type BooleanOperators = {
   eq?: Maybe<Scalars['Boolean']>;
   eq?: Maybe<Scalars['Boolean']>;
@@ -294,6 +300,8 @@ export type CancelOrderInput = {
   orderId: Scalars['ID'];
   orderId: Scalars['ID'];
   /** Optionally specify which OrderLines to cancel. If not provided, all OrderLines will be cancelled */
   /** Optionally specify which OrderLines to cancel. If not provided, all OrderLines will be cancelled */
   lines?: Maybe<Array<OrderLineInput>>;
   lines?: Maybe<Array<OrderLineInput>>;
+  /** Specify whether the shipping charges should also be cancelled. Defaults to false */
+  cancelShipping?: Maybe<Scalars['Boolean']>;
   reason?: Maybe<Scalars['String']>;
   reason?: Maybe<Scalars['String']>;
 };
 };
 
 
@@ -552,6 +560,31 @@ export type CountryTranslationInput = {
   customFields?: Maybe<Scalars['JSON']>;
   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 = {
 export type CreateAddressInput = {
   fullName?: Maybe<Scalars['String']>;
   fullName?: Maybe<Scalars['String']>;
   company?: Maybe<Scalars['String']>;
   company?: Maybe<Scalars['String']>;
@@ -1201,6 +1234,7 @@ export type CustomerOrdersArgs = {
 };
 };
 
 
 export type CustomerFilterParameter = {
 export type CustomerFilterParameter = {
+  postalCode?: Maybe<StringOperators>;
   id?: Maybe<IdOperators>;
   id?: Maybe<IdOperators>;
   createdAt?: Maybe<DateOperators>;
   createdAt?: Maybe<DateOperators>;
   updatedAt?: Maybe<DateOperators>;
   updatedAt?: Maybe<DateOperators>;
@@ -1289,6 +1323,11 @@ export type CustomerSortParameter = {
   emailAddress?: Maybe<SortOrder>;
   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 */
 /** Operators for filtering on a DateTime field */
 export type DateOperators = {
 export type DateOperators = {
   eq?: Maybe<Scalars['DateTime']>;
   eq?: Maybe<Scalars['DateTime']>;
@@ -1405,7 +1444,10 @@ export enum ErrorCode {
   EMAIL_ADDRESS_CONFLICT_ERROR = 'EMAIL_ADDRESS_CONFLICT_ERROR',
   EMAIL_ADDRESS_CONFLICT_ERROR = 'EMAIL_ADDRESS_CONFLICT_ERROR',
   ORDER_LIMIT_ERROR = 'ORDER_LIMIT_ERROR',
   ORDER_LIMIT_ERROR = 'ORDER_LIMIT_ERROR',
   NEGATIVE_QUANTITY_ERROR = 'NEGATIVE_QUANTITY_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 = {
 export type ErrorResult = {
@@ -1665,6 +1707,11 @@ export enum HistoryEntryType {
   ORDER_MODIFIED = 'ORDER_MODIFIED'
   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 */
 /** Operators for filtering on an ID field */
 export type IdOperators = {
 export type IdOperators = {
   eq?: Maybe<Scalars['String']>;
   eq?: Maybe<Scalars['String']>;
@@ -2234,6 +2281,7 @@ export type ModifyOrderInput = {
   note?: Maybe<Scalars['String']>;
   note?: Maybe<Scalars['String']>;
   refund?: Maybe<AdministratorRefundInput>;
   refund?: Maybe<AdministratorRefundInput>;
   options?: Maybe<ModifyOrderOptions>;
   options?: Maybe<ModifyOrderOptions>;
+  couponCodes?: Maybe<Array<Scalars['String']>>;
 };
 };
 
 
 export type ModifyOrderOptions = {
 export type ModifyOrderOptions = {
@@ -2241,7 +2289,7 @@ export type ModifyOrderOptions = {
   recalculateShipping?: Maybe<Scalars['Boolean']>;
   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 = {
 export type MoveCollectionInput = {
   collectionId: Scalars['ID'];
   collectionId: Scalars['ID'];
@@ -3101,6 +3149,11 @@ export type NothingToRefundError = ErrorResult & {
   message: Scalars['String'];
   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 */
 /** Operators for filtering on a Int or Float field */
 export type NumberOperators = {
 export type NumberOperators = {
   eq?: Maybe<Scalars['Float']>;
   eq?: Maybe<Scalars['Float']>;
@@ -4772,6 +4825,11 @@ export type StringFieldOption = {
   label?: Maybe<Array<LocalizedString>>;
   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 */
 /** Operators for filtering on a String field */
 export type StringOperators = {
 export type StringOperators = {
   eq?: Maybe<Scalars['String']>;
   eq?: Maybe<Scalars['String']>;
@@ -5864,6 +5922,16 @@ export type UpdateCustomerAddressMutation = { updateCustomerAddress: (
     & AddressFragment
     & AddressFragment
   ) };
   ) };
 
 
+export type DeleteCustomerAddressMutationVariables = Exact<{
+  id: Scalars['ID'];
+}>;
+
+
+export type DeleteCustomerAddressMutation = { deleteCustomerAddress: (
+    { __typename?: 'Success' }
+    & Pick<Success, 'success'>
+  ) };
+
 export type CreateCustomerGroupMutationVariables = Exact<{
 export type CreateCustomerGroupMutationVariables = Exact<{
   input: CreateCustomerGroupInput;
   input: CreateCustomerGroupInput;
 }>;
 }>;
@@ -6175,7 +6243,7 @@ export type OrderLineFragment = (
 
 
 export type OrderDetailFragment = (
 export type OrderDetailFragment = (
   { __typename?: 'Order' }
   { __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<(
   & { customer?: Maybe<(
     { __typename?: 'Customer' }
     { __typename?: 'Customer' }
     & Pick<Customer, 'id' | 'firstName' | 'lastName'>
     & Pick<Customer, 'id' | 'firstName' | 'lastName'>
@@ -6539,6 +6607,15 @@ export type ModifyOrderMutation = { modifyOrder: (
   ) | (
   ) | (
     { __typename?: 'InsufficientStockError' }
     { __typename?: 'InsufficientStockError' }
     & ErrorResult_InsufficientStockError_Fragment
     & ErrorResult_InsufficientStockError_Fragment
+  ) | (
+    { __typename?: 'CouponCodeExpiredError' }
+    & ErrorResult_CouponCodeExpiredError_Fragment
+  ) | (
+    { __typename?: 'CouponCodeInvalidError' }
+    & ErrorResult_CouponCodeInvalidError_Fragment
+  ) | (
+    { __typename?: 'CouponCodeLimitError' }
+    & ErrorResult_CouponCodeLimitError_Fragment
   ) };
   ) };
 
 
 export type AddManualPaymentMutationVariables = Exact<{
 export type AddManualPaymentMutationVariables = Exact<{
@@ -8747,6 +8824,21 @@ type ErrorResult_ChannelDefaultLanguageError_Fragment = (
   & Pick<ChannelDefaultLanguageError, 'errorCode' | 'message'>
   & 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 = (
 type ErrorResult_CreateFulfillmentError_Fragment = (
   { __typename?: 'CreateFulfillmentError' }
   { __typename?: 'CreateFulfillmentError' }
   & Pick<CreateFulfillmentError, 'errorCode' | 'message'>
   & Pick<CreateFulfillmentError, 'errorCode' | 'message'>
@@ -8897,7 +8989,7 @@ type ErrorResult_SettlePaymentError_Fragment = (
   & Pick<SettlePaymentError, 'errorCode' | 'message'>
   & 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 = (
 export type ShippingMethodFragment = (
   { __typename?: 'ShippingMethod' }
   { __typename?: 'ShippingMethod' }
@@ -9337,6 +9429,12 @@ export namespace UpdateCustomerAddress {
   export type UpdateCustomerAddress = (NonNullable<UpdateCustomerAddressMutation['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 namespace CreateCustomerGroup {
   export type Variables = CreateCustomerGroupMutationVariables;
   export type Variables = CreateCustomerGroupMutationVariables;
   export type Mutation = CreateCustomerGroupMutation;
   export type Mutation = CreateCustomerGroupMutation;

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

@@ -1,259 +1,198 @@
 // tslint:disable
 // 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}
     ${ADDRESS_FRAGMENT}
 `;
 `;
 
 
+export const DELETE_CUSTOMER_ADDRESS = gql`
+    mutation DeleteCustomerAddress($id: ID!) {
+        deleteCustomerAddress(id: $id) {
+            success
+        }
+    }
+`;
+
 export const CREATE_CUSTOMER_GROUP = gql`
 export const CREATE_CUSTOMER_GROUP = gql`
     mutation CreateCustomerGroup($input: CreateCustomerGroupInput!) {
     mutation CreateCustomerGroup($input: CreateCustomerGroupInput!) {
         createCustomerGroup(input: $input) {
         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
         state
         nextStates
         nextStates
         active
         active
+        couponCodes
         customer {
         customer {
             id
             id
             firstName
             firstName

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

@@ -10,6 +10,7 @@ import {
     CustomerGroupListOptions,
     CustomerGroupListOptions,
     CustomerListOptions,
     CustomerListOptions,
     DeleteCustomer,
     DeleteCustomer,
+    DeleteCustomerAddress,
     DeleteCustomerGroup,
     DeleteCustomerGroup,
     DeleteCustomerNote,
     DeleteCustomerNote,
     GetCustomer,
     GetCustomer,
@@ -36,11 +37,12 @@ import {
     CREATE_CUSTOMER_ADDRESS,
     CREATE_CUSTOMER_ADDRESS,
     CREATE_CUSTOMER_GROUP,
     CREATE_CUSTOMER_GROUP,
     DELETE_CUSTOMER,
     DELETE_CUSTOMER,
+    DELETE_CUSTOMER_ADDRESS,
     DELETE_CUSTOMER_GROUP,
     DELETE_CUSTOMER_GROUP,
     DELETE_CUSTOMER_NOTE,
     DELETE_CUSTOMER_NOTE,
     GET_CUSTOMER,
     GET_CUSTOMER,
-    GET_CUSTOMER_GROUP_WITH_CUSTOMERS,
     GET_CUSTOMER_GROUPS,
     GET_CUSTOMER_GROUPS,
+    GET_CUSTOMER_GROUP_WITH_CUSTOMERS,
     GET_CUSTOMER_HISTORY,
     GET_CUSTOMER_HISTORY,
     GET_CUSTOMER_LIST,
     GET_CUSTOMER_LIST,
     REMOVE_CUSTOMERS_FROM_GROUP,
     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) {
     createCustomerGroup(input: CreateCustomerGroupInput) {
         return this.baseDataService.mutate<CreateCustomerGroup.Mutation, CreateCustomerGroup.Variables>(
         return this.baseDataService.mutate<CreateCustomerGroup.Mutation, CreateCustomerGroup.Variables>(
             CREATE_CUSTOMER_GROUP,
             CREATE_CUSTOMER_GROUP,

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

@@ -5,6 +5,7 @@ import {
     GetAdjustmentOperations,
     GetAdjustmentOperations,
     GetPromotion,
     GetPromotion,
     GetPromotionList,
     GetPromotionList,
+    PromotionFilterParameter,
     UpdatePromotion,
     UpdatePromotion,
     UpdatePromotionInput,
     UpdatePromotionInput,
 } from '../../common/generated-types';
 } from '../../common/generated-types';
@@ -22,13 +23,14 @@ import { BaseDataService } from './base-data.service';
 export class PromotionDataService {
 export class PromotionDataService {
     constructor(private baseDataService: BaseDataService) {}
     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>(
         return this.baseDataService.query<GetPromotionList.Query, GetPromotionList.Variables>(
             GET_PROMOTION_LIST,
             GET_PROMOTION_LIST,
             {
             {
                 options: {
                 options: {
                     take,
                     take,
                     skip,
                     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-picker-dialog/asset-picker-dialog.component';
 export * from './shared/components/asset-preview/asset-preview.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-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/asset-search-input/asset-search-input.component';
 export * from './shared/components/channel-assignment-control/channel-assignment-control.component';
 export * from './shared/components/channel-assignment-control/channel-assignment-control.component';
 export * from './shared/components/channel-badge/channel-badge.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/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/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/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/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/product-variant/relation-product-variant-input.component';
 export * from './shared/dynamic-form-inputs/relation-form-input/relation-card/relation-card.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>
             <div>{{ 'asset.original-asset-size' | translate }}: {{ lastSelected().fileSize | filesize }}</div>
 
 
             <ng-container *ngIf="selection.length === 1">
             <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>
                 <div>
                     <button (click)="previewAsset(lastSelected())" class="btn btn-link">
                     <button (click)="previewAsset(lastSelected())" class="btn btn-link">
                         <clr-icon shape="eye"></clr-icon> {{ 'asset.preview' | translate }}
                         <clr-icon shape="eye"></clr-icon> {{ 'asset.preview' | translate }}
                     </button>
                     </button>
                 </div>
                 </div>
+                <div>
+                    <vdr-asset-preview-links class="" [asset]="lastSelected()"></vdr-asset-preview-links>
+                </div>
                 <div>
                 <div>
                     <a [routerLink]="['./', lastSelected().id]" class="btn btn-link">
                     <a [routerLink]="['./', lastSelected().id]" class="btn btn-link">
                         <clr-icon shape="pencil"></clr-icon> {{ 'common.edit' | translate }}
                         <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>
         </clr-select-container>
         <div class="asset-detail">{{ width }} x {{ height }}</div>
         <div class="asset-detail">{{ width }} x {{ height }}</div>
     </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>
 </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 {
     ::ng-deep .clr-control-container {
         width: 100%;
         width: 100%;
+
         .clr-input {
         .clr-input {
             width: 100%;
             width: 100%;
         }
         }
@@ -76,9 +77,14 @@
     .preview-select {
     .preview-select {
         display: flex;
         display: flex;
         align-items: center;
         align-items: center;
-        margin-bottom: 12px;
         clr-select-container {
         clr-select-container {
             margin-right: 12px;
             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
  * @description
@@ -17,7 +25,7 @@ import { ChangeDetectionStrategy, Component, Input, OnInit, Optional, SkipSelf }
     styleUrls: ['./object-tree.component.scss'],
     styleUrls: ['./object-tree.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 })
-export class ObjectTreeComponent implements OnInit {
+export class ObjectTreeComponent implements OnChanges {
     @Input() value: { [key: string]: any } | string;
     @Input() value: { [key: string]: any } | string;
     @Input() isArrayItem = false;
     @Input() isArrayItem = false;
     depth: number;
     depth: number;
@@ -32,7 +40,7 @@ export class ObjectTreeComponent implements OnInit {
         }
         }
     }
     }
 
 
-    ngOnInit() {
+    ngOnChanges() {
         this.entries = this.getEntries(this.value);
         this.entries = this.getEntries(this.value);
         this.expanded = this.depth === 0 || this.isArrayItem;
         this.expanded = this.depth === 0 || this.isArrayItem;
         this.valueIsArray = Object.keys(this.value).every(v => Number.isInteger(+v));
         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"
         [readonly]="readonly"
     ></vdr-relation-product-variant-input>
     ></vdr-relation-product-variant-input>
     <ng-template ngSwitchDefault>
     <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>
     </ng-template>
 </div>
 </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 { AssetGalleryComponent } from './components/asset-gallery/asset-gallery.component';
 import { AssetPickerDialogComponent } from './components/asset-picker-dialog/asset-picker-dialog.component';
 import { AssetPickerDialogComponent } from './components/asset-picker-dialog/asset-picker-dialog.component';
 import { AssetPreviewDialogComponent } from './components/asset-preview-dialog/asset-preview-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 { AssetPreviewComponent } from './components/asset-preview/asset-preview.component';
 import { AssetSearchInputComponent } from './components/asset-search-input/asset-search-input.component';
 import { AssetSearchInputComponent } from './components/asset-search-input/asset-search-input.component';
 import { ChannelAssignmentControlComponent } from './components/channel-assignment-control/channel-assignment-control.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 { 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 { 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 { 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 { 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 { RelationProductInputComponent } from './dynamic-form-inputs/relation-form-input/product/relation-product-input.component';
 import {
 import {
@@ -231,6 +233,7 @@ const DECLARATIONS = [
     TabbedCustomFieldsComponent,
     TabbedCustomFieldsComponent,
     UiExtensionPointComponent,
     UiExtensionPointComponent,
     CustomDetailComponentHostComponent,
     CustomDetailComponentHostComponent,
+    AssetPreviewLinksComponent,
 ];
 ];
 
 
 const DYNAMIC_FORM_INPUTS = [
 const DYNAMIC_FORM_INPUTS = [
@@ -251,6 +254,7 @@ const DYNAMIC_FORM_INPUTS = [
     RelationCardPreviewDirective,
     RelationCardPreviewDirective,
     RelationCardDetailDirective,
     RelationCardDetailDirective,
     RelationSelectorDialogComponent,
     RelationSelectorDialogComponent,
+    RelationGenericInputComponent,
     TextareaFormInputComponent,
     TextareaFormInputComponent,
     RichTextFormInputComponent,
     RichTextFormInputComponent,
     JsonEditorFormInputComponent,
     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 }}
                         {{ 'customer.set-as-default-billing-address' | translate }}
                     </button>
                     </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-menu>
             </vdr-dropdown>
             </vdr-dropdown>
         </ng-container>
         </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;
     @Input() editable = true;
     @Output() setAsDefaultShipping = new EventEmitter<string>();
     @Output() setAsDefaultShipping = new EventEmitter<string>();
     @Output() setAsDefaultBilling = new EventEmitter<string>();
     @Output() setAsDefaultBilling = new EventEmitter<string>();
+    @Output() deleteAddress = new EventEmitter<string>();
     private dataDependenciesPopulated = new BehaviorSubject<boolean>(false);
     private dataDependenciesPopulated = new BehaviorSubject<boolean>(false);
 
 
     constructor(private modalService: ModalService, private changeDetector: ChangeDetectorRef) {}
     constructor(private modalService: ModalService, private changeDetector: ChangeDetectorRef) {}
@@ -75,6 +76,11 @@ export class AddressCardComponent implements OnInit, OnChanges {
         this.addressForm.markAsDirty();
         this.addressForm.markAsDirty();
     }
     }
 
 
+    delete() {
+        this.deleteAddress.emit(this.addressForm.value.id);
+        this.addressForm.markAsDirty();
+    }
+
     editAddress() {
     editAddress() {
         this.modalService
         this.modalService
             .fromComponent(AddressDetailDialogComponent, {
             .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>
         <h3>{{ 'customer.addresses' | translate }}</h3>
         <vdr-address-card
         <vdr-address-card
             *ngFor="let addressForm of getAddressFormControls()"
             *ngFor="let addressForm of getAddressFormControls()"
+            [class.to-delete]="addressesToDeleteIds.has(addressForm.value.id)"
             [availableCountries]="availableCountries$ | async"
             [availableCountries]="availableCountries$ | async"
             [isDefaultBilling]="defaultBillingAddressId === addressForm.value.id"
             [isDefaultBilling]="defaultBillingAddressId === addressForm.value.id"
             [isDefaultShipping]="defaultShippingAddressId === addressForm.value.id"
             [isDefaultShipping]="defaultShippingAddressId === addressForm.value.id"
             [addressForm]="addressForm"
             [addressForm]="addressForm"
             [customFields]="addressCustomFields"
             [customFields]="addressCustomFields"
-            [editable]="['UpdateCustomer'] | hasPermission"
+            [editable]="(['UpdateCustomer'] | hasPermission) && !addressesToDeleteIds.has(addressForm.value.id)"
             (setAsDefaultBilling)="setDefaultBillingAddressId($event)"
             (setAsDefaultBilling)="setDefaultBillingAddressId($event)"
             (setAsDefaultShipping)="setDefaultShippingAddressId($event)"
             (setAsDefaultShipping)="setDefaultShippingAddressId($event)"
+            (deleteAddress)="toggleDeleteAddress($event)"
         ></vdr-address-card>
         ></vdr-address-card>
         <button class="btn btn-secondary" (click)="addAddress()" *vdrIfPermissions="'UpdateCustomer'">
         <button class="btn btn-secondary" (click)="addAddress()" *vdrIfPermissions="'UpdateCustomer'">
             <clr-icon shape="plus"></clr-icon>
             <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;
     margin-left: 6px;
     color: var(--color-grey-500);
     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,
     Customer,
     CustomFieldConfig,
     CustomFieldConfig,
     DataService,
     DataService,
+    DeleteCustomerAddress,
     EditNoteDialogComponent,
     EditNoteDialogComponent,
     GetAvailableCountries,
     GetAvailableCountries,
     GetCustomer,
     GetCustomer,
@@ -27,7 +28,7 @@ import {
     UpdateCustomerInput,
     UpdateCustomerInput,
     UpdateCustomerMutation,
     UpdateCustomerMutation,
 } from '@vendure/admin-ui/core';
 } 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 { EMPTY, forkJoin, from, Observable, Subject } from 'rxjs';
 import {
 import {
     concatMap,
     concatMap,
@@ -65,6 +66,7 @@ export class CustomerDetailComponent
     fetchHistory = new Subject<void>();
     fetchHistory = new Subject<void>();
     defaultShippingAddressId: string;
     defaultShippingAddressId: string;
     defaultBillingAddressId: string;
     defaultBillingAddressId: string;
+    addressesToDeleteIds = new Set<string>();
     addressDefaultsUpdated = false;
     addressDefaultsUpdated = false;
     ordersPerPage = 10;
     ordersPerPage = 10;
     currentOrdersPage = 1;
     currentOrdersPage = 1;
@@ -144,6 +146,14 @@ export class CustomerDetailComponent
         this.addressDefaultsUpdated = true;
         this.addressDefaultsUpdated = true;
     }
     }
 
 
+    toggleDeleteAddress(id: string) {
+        if (this.addressesToDeleteIds.has(id)) {
+            this.addressesToDeleteIds.delete(id);
+        } else {
+            this.addressesToDeleteIds.add(id);
+        }
+    }
+
     addAddress() {
     addAddress() {
         const addressFormArray = this.detailForm.get('addresses') as FormArray;
         const addressFormArray = this.detailForm.get('addresses') as FormArray;
         const newAddress = this.formBuilder.group({
         const newAddress = this.formBuilder.group({
@@ -231,6 +241,7 @@ export class CustomerDetailComponent
                             | UpdateCustomer.UpdateCustomer
                             | UpdateCustomer.UpdateCustomer
                             | CreateCustomerAddress.CreateCustomerAddress
                             | CreateCustomerAddress.CreateCustomerAddress
                             | UpdateCustomerAddress.UpdateCustomerAddress
                             | UpdateCustomerAddress.UpdateCustomerAddress
+                            | DeleteCustomerAddress.DeleteCustomerAddress
                         >
                         >
                     > = [];
                     > = [];
                     const customerForm = this.detailForm.get('customer');
                     const customerForm = this.detailForm.get('customer');
@@ -278,14 +289,22 @@ export class CustomerDetailComponent
                                             .pipe(map(res => res.createCustomerAddress)),
                                             .pipe(map(res => res.createCustomerAddress)),
                                     );
                                     );
                                 } else {
                                 } 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(
             .subscribe(
                 data => {
                 data => {
+                    let notified = false;
                     for (const result of data) {
                     for (const result of data) {
                         switch (result.__typename) {
                         switch (result.__typename) {
                             case 'Customer':
                             case 'Customer':
                             case 'Address':
                             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;
                                 break;
                             case 'EmailAddressConflictError':
                             case 'EmailAddressConflictError':
                                 this.notificationService.error(result.message);
                                 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
         <input
             type="text"
             type="text"
             name="emailSearchTerm"
             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"
             class="search-input ml3"
         />
         />
     </vdr-ab-left>
     </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,
     BaseListComponent,
     DataService,
     DataService,
     GetCustomerList,
     GetCustomerList,
+    LogicalOperator,
     ModalService,
     ModalService,
     NotificationService,
     NotificationService,
 } from '@vendure/admin-ui/core';
 } from '@vendure/admin-ui/core';
 import { SortOrder } from '@vendure/common/lib/generated-shop-types';
 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';
 import { debounceTime, filter, switchMap, takeUntil } from 'rxjs/operators';
 
 
 @Component({
 @Component({
@@ -20,9 +21,9 @@ import { debounceTime, filter, switchMap, takeUntil } from 'rxjs/operators';
 })
 })
 export class CustomerListComponent
 export class CustomerListComponent
     extends BaseListComponent<GetCustomerList.Query, GetCustomerList.Items>
     extends BaseListComponent<GetCustomerList.Query, GetCustomerList.Items>
-    implements OnInit {
-    emailSearchTerm = new FormControl('');
-    lastNameSearchTerm = new FormControl('');
+    implements OnInit
+{
+    searchTerm = new FormControl('');
     constructor(
     constructor(
         private dataService: DataService,
         private dataService: DataService,
         router: Router,
         router: Router,
@@ -40,12 +41,16 @@ export class CustomerListComponent
                     take,
                     take,
                     filter: {
                     filter: {
                         emailAddress: {
                         emailAddress: {
-                            contains: this.emailSearchTerm.value,
+                            contains: this.searchTerm.value,
                         },
                         },
                         lastName: {
                         lastName: {
-                            contains: this.lastNameSearchTerm.value,
+                            contains: this.searchTerm.value,
+                        },
+                        postalCode: {
+                            contains: this.searchTerm.value,
                         },
                         },
                     },
                     },
+                    filterOperator: LogicalOperator.OR,
                     sort: {
                     sort: {
                         createdAt: SortOrder.DESC,
                         createdAt: SortOrder.DESC,
                     },
                     },
@@ -56,7 +61,7 @@ export class CustomerListComponent
 
 
     ngOnInit() {
     ngOnInit() {
         super.ngOnInit();
         super.ngOnInit();
-        merge(this.emailSearchTerm.valueChanges, this.lastNameSearchTerm.valueChanges)
+        this.searchTerm.valueChanges
             .pipe(
             .pipe(
                 filter(value => 2 < value.length || value.length === 0),
                 filter(value => 2 < value.length || value.length === 0),
                 debounceTime(250),
                 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
             <tr
                 *ngFor="let line of order.lines"
                 *ngFor="let line of order.lines"
                 class="order-line"
                 class="order-line"
+                [class.is-disabled]="cancelAll"
                 [class.is-cancelled]="line.quantity === 0"
                 [class.is-cancelled]="line.quantity === 0"
             >
             >
                 <td class="align-middle thumb">
                 <td class="align-middle thumb">
-                    <img [src]="line.featuredAsset | assetPreview:'tiny'" />
+                    <img [src]="line.featuredAsset | assetPreview: 'tiny'" />
                 </td>
                 </td>
                 <td class="align-middle name">{{ line.productVariant.name }}</td>
                 <td class="align-middle name">{{ line.productVariant.name }}</td>
                 <td class="align-middle sku">{{ line.productVariant.sku }}</td>
                 <td class="align-middle sku">{{ line.productVariant.sku }}</td>
@@ -31,6 +32,8 @@
                     <input
                     <input
                         *ngIf="line.quantity > 0 && !order.active; else nonEditable"
                         *ngIf="line.quantity > 0 && !order.active; else nonEditable"
                         [(ngModel)]="lineQuantities[line.id]"
                         [(ngModel)]="lineQuantities[line.id]"
+                        (input)="checkIfAllSelected()"
+                        [disabled]="cancelAll"
                         type="number"
                         type="number"
                         [max]="line.quantity"
                         [max]="line.quantity"
                         min="0"
                         min="0"
@@ -41,6 +44,30 @@
         </table>
         </table>
     </div>
     </div>
     <div class="cancellation-details">
     <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>
         <label class="clr-control-label">{{ 'order.cancellation-reason' | translate }}</label>
         <ng-select
         <ng-select
             [items]="reasons"
             [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;
         text-decoration: line-through;
         background-color: var(--color-component-bg-200);
         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 { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 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({
 @Component({
     selector: 'vdr-cancel-order-dialog',
     selector: 'vdr-cancel-order-dialog',
@@ -10,10 +17,14 @@ import { CancelOrderInput, Dialog, I18nService, OrderDetailFragment, OrderLineIn
 })
 })
 export class CancelOrderDialogComponent implements OnInit, Dialog<CancelOrderInput> {
 export class CancelOrderDialogComponent implements OnInit, Dialog<CancelOrderInput> {
     order: OrderDetailFragment;
     order: OrderDetailFragment;
+    cancelAll = true;
     resolveWith: (result?: CancelOrderInput) => void;
     resolveWith: (result?: CancelOrderInput) => void;
     reason: string;
     reason: string;
     lineQuantities: { [lineId: string]: number } = {};
     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 {
     get selectionCount(): number {
         return Object.values(this.lineQuantities).reduce((sum, n) => sum + n, 0);
         return Object.values(this.lineQuantities).reduce((sum, n) => sum + n, 0);
@@ -25,15 +36,40 @@ export class CancelOrderDialogComponent implements OnInit, Dialog<CancelOrderInp
 
 
     ngOnInit() {
     ngOnInit() {
         this.lineQuantities = this.order.lines.reduce((result, line) => {
         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() {
     select() {
         this.resolveWith({
         this.resolveWith({
             orderId: this.order.id,
             orderId: this.order.id,
             lines: this.getLineInputs(),
             lines: this.getLineInputs(),
             reason: this.reason,
             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 {
     canAddFulfillment(order: OrderDetail.Fragment): boolean {
         const allItemsFulfilled = order.lines
         const allItemsFulfilled = order.lines
             .reduce((items, line) => [...items, ...line.items], [] as OrderLineFragment['items'])
             .reduce((items, line) => [...items, ...line.items], [] as OrderLineFragment['items'])
-            .every(item => !!item.fulfillment);
+            .every(item => !!item.fulfillment || item.cancelled);
         return (
         return (
             !allItemsFulfilled &&
             !allItemsFulfilled &&
             !this.hasUnsettledModifications(order) &&
             !this.hasUnsettledModifications(order) &&

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

@@ -224,6 +224,28 @@
                         </button>
                         </button>
                     </clr-accordion-content>
                     </clr-accordion-content>
                 </clr-accordion-panel>
                 </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-panel>
                     <clr-accordion-title>{{ 'order.add-surcharge' | translate }}</clr-accordion-title>
                     <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,
     SurchargeInput,
 } from '@vendure/admin-ui/core';
 } from '@vendure/admin-ui/core';
 import { assertNever, notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 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 { OrderTransitionService } from '../../providers/order-transition.service';
 import {
 import {
@@ -54,10 +62,14 @@ type ModifyOrderData = Omit<ModifyOrderInput, 'addItems' | 'adjustOrderLines'> &
 })
 })
 export class OrderEditorComponent
 export class OrderEditorComponent
     extends BaseDetailComponent<OrderDetail.Fragment>
     extends BaseDetailComponent<OrderDetail.Fragment>
-    implements OnInit, OnDestroy {
+    implements OnInit, OnDestroy
+{
     availableCountries$: Observable<GetAvailableCountries.Items[]>;
     availableCountries$: Observable<GetAvailableCountries.Items[]>;
+    availableCouponCodes$: Observable<Array<{ code: string; promotionName: string }>>;
+    couponCodeInput$ = new Subject<string>();
     addressCustomFields: CustomFieldConfig[];
     addressCustomFields: CustomFieldConfig[];
     detailForm = new FormGroup({});
     detailForm = new FormGroup({});
+    couponCodesControl = new FormControl();
     orderLineCustomFieldsFormArray: FormArray;
     orderLineCustomFieldsFormArray: FormArray;
     addItemCustomFieldsFormArray: FormArray;
     addItemCustomFieldsFormArray: FormArray;
     addItemCustomFieldsForm: FormGroup;
     addItemCustomFieldsForm: FormGroup;
@@ -114,10 +126,14 @@ export class OrderEditorComponent
 
 
     ngOnInit(): void {
     ngOnInit(): void {
         this.init();
         this.init();
+        this.dataService.promotion.getPromotions();
         this.addressCustomFields = this.getCustomFieldConfig('Address');
         this.addressCustomFields = this.getCustomFieldConfig('Address');
         this.modifyOrderInput.orderId = this.route.snapshot.paramMap.get('id') as string;
         this.modifyOrderInput.orderId = this.route.snapshot.paramMap.get('id') as string;
         this.orderLineCustomFields = this.getCustomFieldConfig('OrderLine');
         this.orderLineCustomFields = this.getCustomFieldConfig('OrderLine');
         this.entity$.pipe(takeUntil(this.destroy$)).subscribe(order => {
         this.entity$.pipe(takeUntil(this.destroy$)).subscribe(order => {
+            if (order.couponCodes.length) {
+                this.couponCodesControl.setValue(order.couponCodes);
+            }
             this.surchargeForm = new FormGroup({
             this.surchargeForm = new FormGroup({
                 description: new FormControl('', Validators.required),
                 description: new FormControl('', Validators.required),
                 sku: new FormControl(''),
                 sku: new FormControl(''),
@@ -178,6 +194,22 @@ export class OrderEditorComponent
                 this.orderLineCustomFieldsFormArray.push(formGroup);
                 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.addItemCustomFieldsFormArray = new FormArray([]);
         this.addItemCustomFieldsForm = new FormGroup({});
         this.addItemCustomFieldsForm = new FormGroup({});
         for (const customField of this.orderLineCustomFields) {
         for (const customField of this.orderLineCustomFields) {
@@ -219,7 +251,8 @@ export class OrderEditorComponent
             !!surcharges?.length ||
             !!surcharges?.length ||
             !!adjustOrderLines?.length ||
             !!adjustOrderLines?.length ||
             (this.shippingAddressForm.dirty && this.shippingAddressForm.valid) ||
             (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 }
                 ? { updateShippingAddress: this.shippingAddressForm.value }
                 : {}),
                 : {}),
             dryRun: true,
             dryRun: true,
+            couponCodes: this.couponCodesControl.dirty ? this.couponCodesControl.value : undefined,
             note: this.note ?? '',
             note: this.note ?? '',
             options: {
             options: {
                 recalculateShipping: this.recalculateShipping,
                 recalculateShipping: this.recalculateShipping,
@@ -380,7 +414,10 @@ export class OrderEditorComponent
                         case 'OrderLimitError':
                         case 'OrderLimitError':
                         case 'OrderModificationStateError':
                         case 'OrderModificationStateError':
                         case 'PaymentMethodMissingError':
                         case 'PaymentMethodMissingError':
-                        case 'RefundPaymentIdMissingError': {
+                        case 'RefundPaymentIdMissingError':
+                        case 'CouponCodeLimitError':
+                        case 'CouponCodeExpiredError':
+                        case 'CouponCodeInvalidError': {
                             this.notificationService.error(modifyOrder.message);
                             this.notificationService.error(modifyOrder.message);
                             return of(false as const);
                             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-labeled-data [label]="'order.contents' | translate">
                         <vdr-simple-item-list [items]="items"></vdr-simple-item-list>
                         <vdr-simple-item-list [items]="items"></vdr-simple-item-list>
                     </vdr-labeled-data>
                     </vdr-labeled-data>
+                    <vdr-labeled-data [label]="'order.shipping-cancelled' | translate">
+                        {{ entry.data.shippingCancelled }}
+                    </vdr-labeled-data>
                 </vdr-history-entry-detail>
                 </vdr-history-entry-detail>
             </ng-container>
             </ng-container>
             <ng-container *ngSwitchCase="type.ORDER_FULFILLMENT">
             <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>
                 </div>
             </td>
             </td>
         </tr>
         </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">
             <td colspan="5" class="left clr-align-middle">
                 <a [routerLink]="getPromotionLink(discount)">{{ discount.description }}</a>
                 <a [routerLink]="getPromotionLink(discount)">{{ discount.description }}</a>
                 <vdr-chip *ngIf="getCouponCodeForAdjustment(order, discount) as couponCode">{{
                 <vdr-chip *ngIf="getCouponCodeForAdjustment(order, discount) as couponCode">{{
@@ -97,7 +98,8 @@
                     {{ discount.amount | localeCurrency: order.currencyCode }}
                     {{ discount.amount | localeCurrency: order.currencyCode }}
                 </div>
                 </div>
             </td>
             </td>
-        </tr>
+            </tr>
+        </ng-container>
         <tr class="sub-total">
         <tr class="sub-total">
             <td class="left clr-align-middle">{{ 'order.sub-total' | translate }}</td>
             <td class="left clr-align-middle">{{ 'order.sub-total' | translate }}</td>
             <td colspan="4"></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>
                         </ng-container>
                     </div>
                     </div>
                 </td>
                 </td>
-                <td class="align-middle fulfil">
+                <td class="align-middle quantity-col">
                     <input
                     <input
                         *ngIf="lineCanBeRefundedOrCancelled(line)"
                         *ngIf="lineCanBeRefundedOrCancelled(line)"
                         [(ngModel)]="lineQuantities[line.id].quantity"
                         [(ngModel)]="lineQuantities[line.id].quantity"
@@ -88,7 +88,7 @@
             </tr>
             </tr>
         </table>
         </table>
     </div>
     </div>
-    <div class="refund-details mt4">
+    <div class="refund-details mt4" [class.faded]="!isRefunding() && !isCancelling()">
         <div>
         <div>
             <label class="clr-control-label">{{ 'order.refund-cancellation-reason' | translate }}</label>
             <label class="clr-control-label">{{ 'order.refund-cancellation-reason' | translate }}</label>
             <ng-select
             <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);
         color: var(--color-grey-300);
     }
     }
 }
 }
+.quantity-col {
+    background-color: var(--color-warning-100);
+}
 .cancel-checkbox-wrapper {
 .cancel-checkbox-wrapper {
     display: flex;
     display: flex;
     align-items: center;
     align-items: center;
@@ -31,6 +34,9 @@ clr-checkbox-wrapper {
 .refund-details {
 .refund-details {
     display: flex;
     display: flex;
     justify-content: space-between;
     justify-content: space-between;
+    &.faded {
+        opacity: 0.5;
+    }
 }
 }
 .totals {
 .totals {
     margin-top: 48px;
     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 {
 import {
     CancelOrderInput,
     CancelOrderInput,
     Dialog,
     Dialog,
+    getAppConfig,
     I18nService,
     I18nService,
     OrderDetail,
     OrderDetail,
     OrderDetailFragment,
     OrderDetailFragment,
@@ -20,7 +21,8 @@ type SelectionLine = { quantity: number; refund: boolean; cancel: boolean };
     changeDetection: ChangeDetectionStrategy.OnPush,
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 })
 export class RefundOrderDialogComponent
 export class RefundOrderDialogComponent
-    implements OnInit, Dialog<{ cancel: CancelOrderInput; refund: RefundOrderInput }> {
+    implements OnInit, Dialog<{ cancel: CancelOrderInput; refund: RefundOrderInput }>
+{
     order: OrderDetailFragment;
     order: OrderDetailFragment;
     resolveWith: (result?: { cancel: CancelOrderInput; refund: RefundOrderInput }) => void;
     resolveWith: (result?: { cancel: CancelOrderInput; refund: RefundOrderInput }) => void;
     reason: string;
     reason: string;
@@ -29,7 +31,10 @@ export class RefundOrderDialogComponent
     lineQuantities: { [lineId: string]: SelectionLine } = {};
     lineQuantities: { [lineId: string]: SelectionLine } = {};
     refundShipping = false;
     refundShipping = false;
     adjustment = 0;
     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) {
     constructor(private i18nService: I18nService) {
         this.reasons = this.reasons.map(r => this.i18nService.translate(r));
         this.reasons = this.reasons.map(r => this.i18nService.translate(r));
@@ -147,6 +152,7 @@ export class RefundOrderDialogComponent
                     lines: cancelLines,
                     lines: cancelLines,
                     orderId: this.order.id,
                     orderId: this.order.id,
                     reason: this.reason,
                     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-facet-value": "Přidat hodnotu atributu",
     "add-facets": "Přidat atribut",
     "add-facets": "Přidat atribut",
     "add-option": "Přidat možnost",
     "add-option": "Přidat možnost",
+    "asset-preview-links": "",
     "assign-product-to-channel-success": "Produkt byl úspěšně přiřazen do \"{ channel }\"",
     "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-products-to-channel": "Přiřadit produkty do kanálu",
     "assign-to-channel": "Přiřadit do kanálu",
     "assign-to-channel": "Přiřadit do kanálu",
@@ -238,6 +239,7 @@
     "sample-formatting": "",
     "sample-formatting": "",
     "select": "Vybrat...",
     "select": "Vybrat...",
     "select-display-language": "Vyberte jazyk",
     "select-display-language": "Vyberte jazyk",
+    "select-relation-id": "",
     "select-today": "Vybrat dnešní datum",
     "select-today": "Vybrat dnešní datum",
     "set-language": "",
     "set-language": "",
     "short-date": "",
     "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-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",
     "remove-from-group": "Odebrat ze skupiny",
     "search-customers-by-email": "Hledat podle e-mailové adresy",
     "search-customers-by-email": "Hledat podle e-mailové adresy",
-    "search-customers-by-last-name": "",
+    "search-customers-by-email-last-name-postal-code": "",
     "select-customer": "",
     "select-customer": "",
     "set-as-default-billing-address": "Nastavit jako výchozí fakturační adresu",
     "set-as-default-billing-address": "Nastavit jako výchozí fakturační adresu",
     "set-as-default-shipping-address": "Nastavit jako výchozí dodací adresu",
     "set-as-default-shipping-address": "Nastavit jako výchozí dodací adresu",
@@ -436,6 +438,7 @@
     "arrange-additional-payment": "",
     "arrange-additional-payment": "",
     "billing-address": "Fakturační adresa",
     "billing-address": "Fakturační adresa",
     "cancel": "Zrušit",
     "cancel": "Zrušit",
+    "cancel-entire-order": "",
     "cancel-fulfillment": "Zrušit zpracování",
     "cancel-fulfillment": "Zrušit zpracování",
     "cancel-modification": "Zrušit úpravy",
     "cancel-modification": "Zrušit úpravy",
     "cancel-order": "Zrušit objednávku",
     "cancel-order": "Zrušit objednávku",
@@ -443,6 +446,7 @@
     "cancel-reason-customer-request": "Požadavek zákazníka",
     "cancel-reason-customer-request": "Požadavek zákazníka",
     "cancel-reason-not-available": "Neuveden",
     "cancel-reason-not-available": "Neuveden",
     "cancel-selected-items": "Zrušit vybrané položky",
     "cancel-selected-items": "Zrušit vybrané položky",
+    "cancel-specified-items": "",
     "cancellation-reason": "Důvod zrušení",
     "cancellation-reason": "Důvod zrušení",
     "cancelled-order-success": "Objednávka úspěšně zrušena",
     "cancelled-order-success": "Objednávka úspěšně zrušena",
     "confirm-modifications": "Potvrdit úpravy",
     "confirm-modifications": "Potvrdit úpravy",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "",
     "search-by-customer-last-name": "",
     "search-by-order-code": "Hledat na základě kódu objednávky",
     "search-by-order-code": "Hledat na základě kódu objednávky",
     "select-state": "Vyberte stav",
     "select-state": "Vyberte stav",
+    "set-coupon-codes": "",
     "set-fulfillment-state": "Označit jako {state}",
     "set-fulfillment-state": "Označit jako {state}",
     "settle-payment": "Vypořádání platby",
     "settle-payment": "Vypořádání platby",
     "settle-payment-error": "Nelze vyřídit platbu",
     "settle-payment-error": "Nelze vyřídit platbu",
@@ -547,6 +552,7 @@
     "settle-refund-success": "Úspěšně vypořádana refundace",
     "settle-refund-success": "Úspěšně vypořádana refundace",
     "shipping": "Dodání",
     "shipping": "Dodání",
     "shipping-address": "Dodací adresa",
     "shipping-address": "Dodací adresa",
+    "shipping-cancelled": "",
     "shipping-method": "Dodací metoda",
     "shipping-method": "Dodací metoda",
     "state": "Stav",
     "state": "Stav",
     "sub-total": "Mezisoučet",
     "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-facet-value": "Facettenwert hinzufügen",
     "add-facets": "Facetten hinzufügen",
     "add-facets": "Facetten hinzufügen",
     "add-option": "Option hinzufügen",
     "add-option": "Option hinzufügen",
+    "asset-preview-links": "",
     "assign-product-to-channel-success": "Produkt erfolgreich an \"{ channel }\" zugewiesen",
     "assign-product-to-channel-success": "Produkt erfolgreich an \"{ channel }\" zugewiesen",
     "assign-products-to-channel": "Produkte dem Kanal zuweisen",
     "assign-products-to-channel": "Produkte dem Kanal zuweisen",
     "assign-to-channel": "Zuweisung an Kanal",
     "assign-to-channel": "Zuweisung an Kanal",
@@ -238,6 +239,7 @@
     "sample-formatting": "",
     "sample-formatting": "",
     "select": "Auswählen...",
     "select": "Auswählen...",
     "select-display-language": "Anzeigesprache wählen",
     "select-display-language": "Anzeigesprache wählen",
+    "select-relation-id": "",
     "select-today": "Heute auswählen",
     "select-today": "Heute auswählen",
     "set-language": "",
     "set-language": "",
     "short-date": "",
     "short-date": "",
@@ -309,7 +311,7 @@
     "remove-customers-from-group-success": "{customerCount, plural, one {Kunde} other {{customerCount} Kunden}} aus \"{ groupName }\" entfernt",
     "remove-customers-from-group-success": "{customerCount, plural, one {Kunde} other {{customerCount} Kunden}} aus \"{ groupName }\" entfernt",
     "remove-from-group": "Aus dieser Gruppe entfernen",
     "remove-from-group": "Aus dieser Gruppe entfernen",
     "search-customers-by-email": "Suche nach E-Mail-Adresse",
     "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",
     "select-customer": "Kunde auswählen",
     "set-as-default-billing-address": "Als Standard-Rechnungsadresse festlegen",
     "set-as-default-billing-address": "Als Standard-Rechnungsadresse festlegen",
     "set-as-default-shipping-address": "Als Standard-Versandadresse festlegen",
     "set-as-default-shipping-address": "Als Standard-Versandadresse festlegen",
@@ -436,6 +438,7 @@
     "arrange-additional-payment": "Zusätzliche Zahlung veranlassen",
     "arrange-additional-payment": "Zusätzliche Zahlung veranlassen",
     "billing-address": "Rechnungsadresse",
     "billing-address": "Rechnungsadresse",
     "cancel": "Abbrechen",
     "cancel": "Abbrechen",
+    "cancel-entire-order": "",
     "cancel-fulfillment": "Ausführung abbrechen",
     "cancel-fulfillment": "Ausführung abbrechen",
     "cancel-modification": "Änderungen verwerfen",
     "cancel-modification": "Änderungen verwerfen",
     "cancel-order": "Bestellung stornieren",
     "cancel-order": "Bestellung stornieren",
@@ -443,6 +446,7 @@
     "cancel-reason-customer-request": "Kundenanfrage",
     "cancel-reason-customer-request": "Kundenanfrage",
     "cancel-reason-not-available": "Nicht verfügbar",
     "cancel-reason-not-available": "Nicht verfügbar",
     "cancel-selected-items": "Auswahl aufheben",
     "cancel-selected-items": "Auswahl aufheben",
+    "cancel-specified-items": "",
     "cancellation-reason": "Stornierungsgrund",
     "cancellation-reason": "Stornierungsgrund",
     "cancelled-order-success": "Bestellung erfolgreich storniert",
     "cancelled-order-success": "Bestellung erfolgreich storniert",
     "confirm-modifications": "Änderungen bestätigen",
     "confirm-modifications": "Änderungen bestätigen",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "",
     "search-by-customer-last-name": "",
     "search-by-order-code": "Suche nach Bestellnummer",
     "search-by-order-code": "Suche nach Bestellnummer",
     "select-state": "Status auswählen",
     "select-state": "Status auswählen",
+    "set-coupon-codes": "",
     "set-fulfillment-state": "Abwicklungsstatus wählen",
     "set-fulfillment-state": "Abwicklungsstatus wählen",
     "settle-payment": "Zahlung durchführen",
     "settle-payment": "Zahlung durchführen",
     "settle-payment-error": "Die Zahlung konnte nicht durchgeführt werden",
     "settle-payment-error": "Die Zahlung konnte nicht durchgeführt werden",
@@ -547,6 +552,7 @@
     "settle-refund-success": "Rückzahlung erfolgreich durchgeführt",
     "settle-refund-success": "Rückzahlung erfolgreich durchgeführt",
     "shipping": "Versand",
     "shipping": "Versand",
     "shipping-address": "Lieferadresse",
     "shipping-address": "Lieferadresse",
+    "shipping-cancelled": "",
     "shipping-method": "Versandart",
     "shipping-method": "Versandart",
     "state": "Status",
     "state": "Status",
     "sub-total": "Zwischensumme",
     "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-facet-value": "Add facet value",
     "add-facets": "Add facets",
     "add-facets": "Add facets",
     "add-option": "Add option",
     "add-option": "Add option",
+    "asset-preview-links": "Asset preview links",
     "assign-product-to-channel-success": "Successfully assigned product to \"{ channel }\"",
     "assign-product-to-channel-success": "Successfully assigned product to \"{ channel }\"",
     "assign-products-to-channel": "Assign products to channel",
     "assign-products-to-channel": "Assign products to channel",
     "assign-to-channel": "Assign to channel",
     "assign-to-channel": "Assign to channel",
@@ -238,6 +239,7 @@
     "sample-formatting": "Sample formatting",
     "sample-formatting": "Sample formatting",
     "select": "Select...",
     "select": "Select...",
     "select-display-language": "Select display language",
     "select-display-language": "Select display language",
+    "select-relation-id": "Select relation ID",
     "select-today": "Select today",
     "select-today": "Select today",
     "set-language": "Set language",
     "set-language": "Set language",
     "short-date": "Short date",
     "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-customers-from-group-success": "Removed {customerCount, plural, one {1 customer} other {{customerCount} customers}} from \"{ groupName }\"",
     "remove-from-group": "Remove from this group",
     "remove-from-group": "Remove from this group",
     "search-customers-by-email": "Search by email address",
     "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",
     "select-customer": "Select customer",
     "set-as-default-billing-address": "Set as default billing",
     "set-as-default-billing-address": "Set as default billing",
     "set-as-default-shipping-address": "Set as default shipping",
     "set-as-default-shipping-address": "Set as default shipping",
@@ -436,13 +438,15 @@
     "arrange-additional-payment": "Arrange additional payment",
     "arrange-additional-payment": "Arrange additional payment",
     "billing-address": "Billing address",
     "billing-address": "Billing address",
     "cancel": "Cancel",
     "cancel": "Cancel",
+    "cancel-entire-order": "Cancel entire order",
     "cancel-fulfillment": "Cancel fulfillment",
     "cancel-fulfillment": "Cancel fulfillment",
     "cancel-modification": "Cancel modification",
     "cancel-modification": "Cancel modification",
-    "cancel-order": "Cancel order",
+    "cancel-order": "Cancel order or items",
     "cancel-payment": "Cancel payment",
     "cancel-payment": "Cancel payment",
     "cancel-reason-customer-request": "Customer request",
     "cancel-reason-customer-request": "Customer request",
     "cancel-reason-not-available": "Not available",
     "cancel-reason-not-available": "Not available",
     "cancel-selected-items": "Cancel selected items",
     "cancel-selected-items": "Cancel selected items",
+    "cancel-specified-items": "Cancel specified items",
     "cancellation-reason": "Cancellation reason",
     "cancellation-reason": "Cancellation reason",
     "cancelled-order-success": "Successfully cancelled order",
     "cancelled-order-success": "Successfully cancelled order",
     "confirm-modifications": "Confirm modifications",
     "confirm-modifications": "Confirm modifications",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "Search by customer last name",
     "search-by-customer-last-name": "Search by customer last name",
     "search-by-order-code": "Search by order code",
     "search-by-order-code": "Search by order code",
     "select-state": "Select state",
     "select-state": "Select state",
+    "set-coupon-codes": "Set coupon codes",
     "set-fulfillment-state": "Mark as {state}",
     "set-fulfillment-state": "Mark as {state}",
     "settle-payment": "Settle payment",
     "settle-payment": "Settle payment",
     "settle-payment-error": "Could not settle payment",
     "settle-payment-error": "Could not settle payment",
@@ -547,6 +552,7 @@
     "settle-refund-success": "Successfully settled refund",
     "settle-refund-success": "Successfully settled refund",
     "shipping": "Shipping",
     "shipping": "Shipping",
     "shipping-address": "Shipping address",
     "shipping-address": "Shipping address",
+    "shipping-cancelled": "Shipping cancelled",
     "shipping-method": "Shipping method",
     "shipping-method": "Shipping method",
     "state": "State",
     "state": "State",
     "sub-total": "Sub total",
     "sub-total": "Sub total",
@@ -665,4 +671,4 @@
     "job-result": "Job result",
     "job-result": "Job result",
     "job-state": "Job state"
     "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-facet-value": "Añadir valor de faceta",
     "add-facets": "Añadir facetas",
     "add-facets": "Añadir facetas",
     "add-option": "Añadir opción",
     "add-option": "Añadir opción",
+    "asset-preview-links": "",
     "assign-product-to-channel-success": "Producto asignado a \"{ channel }\" con éxito",
     "assign-product-to-channel-success": "Producto asignado a \"{ channel }\" con éxito",
     "assign-products-to-channel": "Asignar productos a canal de ventas",
     "assign-products-to-channel": "Asignar productos a canal de ventas",
     "assign-to-channel": "Asignar a canal de ventas",
     "assign-to-channel": "Asignar a canal de ventas",
@@ -238,6 +239,7 @@
     "sample-formatting": "",
     "sample-formatting": "",
     "select": "Seleccionar...",
     "select": "Seleccionar...",
     "select-display-language": "Seleccionar idioma de interfaz",
     "select-display-language": "Seleccionar idioma de interfaz",
+    "select-relation-id": "",
     "select-today": "Hoy",
     "select-today": "Hoy",
     "set-language": "",
     "set-language": "",
     "short-date": "",
     "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-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",
     "remove-from-group": "Eliminar del grupo",
     "search-customers-by-email": "Buscar por correo electrónico",
     "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",
     "select-customer": "Seleccionar cliente",
     "set-as-default-billing-address": "Fijar como dirección de facturación por defecto",
     "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",
     "set-as-default-shipping-address": "Fijar como dirección de envío por defecto",
@@ -436,6 +438,7 @@
     "arrange-additional-payment": "Ordenar pagos adicionales",
     "arrange-additional-payment": "Ordenar pagos adicionales",
     "billing-address": "Dirección de facturación",
     "billing-address": "Dirección de facturación",
     "cancel": "Cancelar",
     "cancel": "Cancelar",
+    "cancel-entire-order": "",
     "cancel-fulfillment": "Cancelar orden",
     "cancel-fulfillment": "Cancelar orden",
     "cancel-modification": "Cancelar modificación",
     "cancel-modification": "Cancelar modificación",
     "cancel-order": "Cancelar pedido",
     "cancel-order": "Cancelar pedido",
@@ -443,6 +446,7 @@
     "cancel-reason-customer-request": "Solicitud del cliente",
     "cancel-reason-customer-request": "Solicitud del cliente",
     "cancel-reason-not-available": "No disponible",
     "cancel-reason-not-available": "No disponible",
     "cancel-selected-items": "Cancelar artículos seleccionados",
     "cancel-selected-items": "Cancelar artículos seleccionados",
+    "cancel-specified-items": "",
     "cancellation-reason": "Motivo de la cancelación",
     "cancellation-reason": "Motivo de la cancelación",
     "cancelled-order-success": "Pedido cancelado con éxito",
     "cancelled-order-success": "Pedido cancelado con éxito",
     "confirm-modifications": "Confirmar modificaciones",
     "confirm-modifications": "Confirmar modificaciones",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "Buscar por el apellido del cliente",
     "search-by-customer-last-name": "Buscar por el apellido del cliente",
     "search-by-order-code": "Buscar por el código de pedido",
     "search-by-order-code": "Buscar por el código de pedido",
     "select-state": "Seleccionar estado",
     "select-state": "Seleccionar estado",
+    "set-coupon-codes": "",
     "set-fulfillment-state": "Fijar como {state}",
     "set-fulfillment-state": "Fijar como {state}",
     "settle-payment": "Liquidar pago",
     "settle-payment": "Liquidar pago",
     "settle-payment-error": "No pudo liquidar el pago",
     "settle-payment-error": "No pudo liquidar el pago",
@@ -547,6 +552,7 @@
     "settle-refund-success": "Reembolso liquidado con éxito",
     "settle-refund-success": "Reembolso liquidado con éxito",
     "shipping": "Envío",
     "shipping": "Envío",
     "shipping-address": "Dirección de envío",
     "shipping-address": "Dirección de envío",
+    "shipping-cancelled": "",
     "shipping-method": "Método de envío",
     "shipping-method": "Método de envío",
     "state": "Estado",
     "state": "Estado",
     "sub-total": "Sub total",
     "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-facet-value": "Ajout valeur du composant",
     "add-facets": "Ajout composant",
     "add-facets": "Ajout composant",
     "add-option": "Ajout option",
     "add-option": "Ajout option",
+    "asset-preview-links": "",
     "assign-product-to-channel-success": "produit attribué au canal \"{ channel }\"",
     "assign-product-to-channel-success": "produit attribué au canal \"{ channel }\"",
     "assign-products-to-channel": "Attribuer les produits au canal",
     "assign-products-to-channel": "Attribuer les produits au canal",
     "assign-to-channel": "Attribuer au canal",
     "assign-to-channel": "Attribuer au canal",
@@ -238,6 +239,7 @@
     "sample-formatting": "",
     "sample-formatting": "",
     "select": "Selectionner...",
     "select": "Selectionner...",
     "select-display-language": "Choisir la langue d'affichage",
     "select-display-language": "Choisir la langue d'affichage",
+    "select-relation-id": "",
     "select-today": "Choisir aujourd'hui",
     "select-today": "Choisir aujourd'hui",
     "set-language": "",
     "set-language": "",
     "short-date": "",
     "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-customers-from-group-success": "Retrait {customerCount, plural, one {d'un client} other {de {customerCount} clients}} de \"{ groupName }\"",
     "remove-from-group": "Retirer de ce groupe",
     "remove-from-group": "Retirer de ce groupe",
     "search-customers-by-email": "Chercher par adresse email",
     "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",
     "select-customer": "Sélectionner client",
     "set-as-default-billing-address": "Etablir en tant qu'adresse de facturation par défaut",
     "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",
     "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",
     "arrange-additional-payment": "Arranger un paiment additionnel",
     "billing-address": "Adresse de facturation",
     "billing-address": "Adresse de facturation",
     "cancel": "Annuler",
     "cancel": "Annuler",
+    "cancel-entire-order": "",
     "cancel-fulfillment": "Annuler préparation",
     "cancel-fulfillment": "Annuler préparation",
     "cancel-modification": "Annuler la modification",
     "cancel-modification": "Annuler la modification",
     "cancel-order": "Annuler la commande",
     "cancel-order": "Annuler la commande",
@@ -443,6 +446,7 @@
     "cancel-reason-customer-request": "Demande du client",
     "cancel-reason-customer-request": "Demande du client",
     "cancel-reason-not-available": "Pas disponible",
     "cancel-reason-not-available": "Pas disponible",
     "cancel-selected-items": "Annuler les articles selectionnés",
     "cancel-selected-items": "Annuler les articles selectionnés",
+    "cancel-specified-items": "",
     "cancellation-reason": "Raison de l'annulation",
     "cancellation-reason": "Raison de l'annulation",
     "cancelled-order-success": "Commande annulée",
     "cancelled-order-success": "Commande annulée",
     "confirm-modifications": "Confirmer les modifications",
     "confirm-modifications": "Confirmer les modifications",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "Rechercher par nom du client",
     "search-by-customer-last-name": "Rechercher par nom du client",
     "search-by-order-code": "Rehercher par numéro de commande",
     "search-by-order-code": "Rehercher par numéro de commande",
     "select-state": "Sélectionner un état",
     "select-state": "Sélectionner un état",
+    "set-coupon-codes": "",
     "set-fulfillment-state": "Marquer {state}",
     "set-fulfillment-state": "Marquer {state}",
     "settle-payment": "Régler le paiement",
     "settle-payment": "Régler le paiement",
     "settle-payment-error": "Règlement du paiement échoué",
     "settle-payment-error": "Règlement du paiement échoué",
@@ -547,6 +552,7 @@
     "settle-refund-success": "Remboursement réglé",
     "settle-refund-success": "Remboursement réglé",
     "shipping": "Expédition",
     "shipping": "Expédition",
     "shipping-address": "Adresse de livraison",
     "shipping-address": "Adresse de livraison",
+    "shipping-cancelled": "",
     "shipping-method": "Mode de livraison",
     "shipping-method": "Mode de livraison",
     "state": "Etat",
     "state": "Etat",
     "sub-total": "Sous total",
     "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-facet-value": "Aggiungi valore attributo",
     "add-facets": "Aggiungi attributi",
     "add-facets": "Aggiungi attributi",
     "add-option": "Aggiungi opzione",
     "add-option": "Aggiungi opzione",
+    "asset-preview-links": "",
     "assign-product-to-channel-success": "Prodotto assegnato correttamente a \"{ channel }\"",
     "assign-product-to-channel-success": "Prodotto assegnato correttamente a \"{ channel }\"",
     "assign-products-to-channel": "Assegna prodotto al canale",
     "assign-products-to-channel": "Assegna prodotto al canale",
     "assign-to-channel": "Assegna a un canale",
     "assign-to-channel": "Assegna a un canale",
@@ -238,6 +239,7 @@
     "sample-formatting": "",
     "sample-formatting": "",
     "select": "Seleziona...",
     "select": "Seleziona...",
     "select-display-language": "Seleziona lingua",
     "select-display-language": "Seleziona lingua",
+    "select-relation-id": "",
     "select-today": "Seleziona oggi",
     "select-today": "Seleziona oggi",
     "set-language": "",
     "set-language": "",
     "short-date": "",
     "short-date": "",
@@ -309,7 +311,7 @@
     "remove-customers-from-group-success": "Ho rimosso {customerCount, plural, one {1 cuclientestomer} other {{customerCount} clienti}} from \"{ groupName }\"",
     "remove-customers-from-group-success": "Ho rimosso {customerCount, plural, one {1 cuclientestomer} other {{customerCount} clienti}} from \"{ groupName }\"",
     "remove-from-group": "Rimuovi da questo gruppo",
     "remove-from-group": "Rimuovi da questo gruppo",
     "search-customers-by-email": "Cerca per indirizzo email",
     "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",
     "select-customer": "Seleziona cliente",
     "set-as-default-billing-address": "Imposta come indirizzo di fatturazione predefinito",
     "set-as-default-billing-address": "Imposta come indirizzo di fatturazione predefinito",
     "set-as-default-shipping-address": "Imposta come indirizzo di spedizione predefinito",
     "set-as-default-shipping-address": "Imposta come indirizzo di spedizione predefinito",
@@ -436,6 +438,7 @@
     "arrange-additional-payment": "Effettua un ulteriore pagamento",
     "arrange-additional-payment": "Effettua un ulteriore pagamento",
     "billing-address": "Indirizzo di fatturazione",
     "billing-address": "Indirizzo di fatturazione",
     "cancel": "Annulla",
     "cancel": "Annulla",
+    "cancel-entire-order": "",
     "cancel-fulfillment": "Annulla consegna",
     "cancel-fulfillment": "Annulla consegna",
     "cancel-modification": "Annulla modifica",
     "cancel-modification": "Annulla modifica",
     "cancel-order": "Annulla ordine",
     "cancel-order": "Annulla ordine",
@@ -443,6 +446,7 @@
     "cancel-reason-customer-request": "Richiesta del cliente",
     "cancel-reason-customer-request": "Richiesta del cliente",
     "cancel-reason-not-available": "Non disponibile",
     "cancel-reason-not-available": "Non disponibile",
     "cancel-selected-items": "Cancella prodotti selezionati",
     "cancel-selected-items": "Cancella prodotti selezionati",
+    "cancel-specified-items": "",
     "cancellation-reason": "Motivo dell'annullamento",
     "cancellation-reason": "Motivo dell'annullamento",
     "cancelled-order-success": "Ordine cancellato con successo",
     "cancelled-order-success": "Ordine cancellato con successo",
     "confirm-modifications": "Conferma modifiche",
     "confirm-modifications": "Conferma modifiche",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "Cerca per cognome cliente",
     "search-by-customer-last-name": "Cerca per cognome cliente",
     "search-by-order-code": "Cerca per codice ordine",
     "search-by-order-code": "Cerca per codice ordine",
     "select-state": "Seleziona stato",
     "select-state": "Seleziona stato",
+    "set-coupon-codes": "",
     "set-fulfillment-state": "Segna come {state}",
     "set-fulfillment-state": "Segna come {state}",
     "settle-payment": "Incassa pagamento",
     "settle-payment": "Incassa pagamento",
     "settle-payment-error": "Non è stato possibile incassare il pagamento",
     "settle-payment-error": "Non è stato possibile incassare il pagamento",
@@ -547,6 +552,7 @@
     "settle-refund-success": "Rimborso effettuato con successo",
     "settle-refund-success": "Rimborso effettuato con successo",
     "shipping": "Spedizione",
     "shipping": "Spedizione",
     "shipping-address": "Indirizzo di spedizione",
     "shipping-address": "Indirizzo di spedizione",
+    "shipping-cancelled": "",
     "shipping-method": "Metodo di spedizione",
     "shipping-method": "Metodo di spedizione",
     "state": "Stato",
     "state": "Stato",
     "sub-total": "Sub totale",
     "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-facet-value": "Dodaj nazwe faseta",
     "add-facets": "Dodaj faset",
     "add-facets": "Dodaj faset",
     "add-option": "Dodaj opcje",
     "add-option": "Dodaj opcje",
+    "asset-preview-links": "",
     "assign-product-to-channel-success": "Pomyślnie przypisano produkt do \"{ channel }\"",
     "assign-product-to-channel-success": "Pomyślnie przypisano produkt do \"{ channel }\"",
     "assign-products-to-channel": "Przypisz produkt do kanału",
     "assign-products-to-channel": "Przypisz produkt do kanału",
     "assign-to-channel": "Przypisz do kanału",
     "assign-to-channel": "Przypisz do kanału",
@@ -238,6 +239,7 @@
     "sample-formatting": "",
     "sample-formatting": "",
     "select": "Wybrano...",
     "select": "Wybrano...",
     "select-display-language": "Wybierz język",
     "select-display-language": "Wybierz język",
+    "select-relation-id": "",
     "select-today": "Wybierz dzisiaj",
     "select-today": "Wybierz dzisiaj",
     "set-language": "",
     "set-language": "",
     "short-date": "",
     "short-date": "",
@@ -309,7 +311,7 @@
     "remove-customers-from-group-success": "",
     "remove-customers-from-group-success": "",
     "remove-from-group": "",
     "remove-from-group": "",
     "search-customers-by-email": "Szukaj przez email",
     "search-customers-by-email": "Szukaj przez email",
-    "search-customers-by-last-name": "",
+    "search-customers-by-email-last-name-postal-code": "",
     "select-customer": "",
     "select-customer": "",
     "set-as-default-billing-address": "Ustaw jako domyślny adres rozliczeniowy",
     "set-as-default-billing-address": "Ustaw jako domyślny adres rozliczeniowy",
     "set-as-default-shipping-address": "Ustaw jako domyślny adres wysyłki",
     "set-as-default-shipping-address": "Ustaw jako domyślny adres wysyłki",
@@ -436,6 +438,7 @@
     "arrange-additional-payment": "",
     "arrange-additional-payment": "",
     "billing-address": "",
     "billing-address": "",
     "cancel": "Anuluj",
     "cancel": "Anuluj",
+    "cancel-entire-order": "",
     "cancel-fulfillment": "",
     "cancel-fulfillment": "",
     "cancel-modification": "",
     "cancel-modification": "",
     "cancel-order": "Anuluj zamówienie",
     "cancel-order": "Anuluj zamówienie",
@@ -443,6 +446,7 @@
     "cancel-reason-customer-request": "Prośba klienta",
     "cancel-reason-customer-request": "Prośba klienta",
     "cancel-reason-not-available": "Niedostępny",
     "cancel-reason-not-available": "Niedostępny",
     "cancel-selected-items": "Anuluj zaznaczone",
     "cancel-selected-items": "Anuluj zaznaczone",
+    "cancel-specified-items": "",
     "cancellation-reason": "Powód anulowania",
     "cancellation-reason": "Powód anulowania",
     "cancelled-order-success": "Pomyślnie anulowano zamówienie",
     "cancelled-order-success": "Pomyślnie anulowano zamówienie",
     "confirm-modifications": "",
     "confirm-modifications": "",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "",
     "search-by-customer-last-name": "",
     "search-by-order-code": "Szukaj po numerze zamówienia",
     "search-by-order-code": "Szukaj po numerze zamówienia",
     "select-state": "",
     "select-state": "",
+    "set-coupon-codes": "",
     "set-fulfillment-state": "",
     "set-fulfillment-state": "",
     "settle-payment": "Rozlicz płatność",
     "settle-payment": "Rozlicz płatność",
     "settle-payment-error": "Nie można rozliczyć płatności",
     "settle-payment-error": "Nie można rozliczyć płatności",
@@ -547,6 +552,7 @@
     "settle-refund-success": "Pomyślnie rozliczono zwrot",
     "settle-refund-success": "Pomyślnie rozliczono zwrot",
     "shipping": "Wysyłka",
     "shipping": "Wysyłka",
     "shipping-address": "Adres wysyłki",
     "shipping-address": "Adres wysyłki",
+    "shipping-cancelled": "",
     "shipping-method": "Metoda wysyłki",
     "shipping-method": "Metoda wysyłki",
     "state": "Status",
     "state": "Status",
     "sub-total": "Sub total",
     "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-facet-value": "Adiciona valor para etiqueta",
     "add-facets": "Adiciona etiqueta",
     "add-facets": "Adiciona etiqueta",
     "add-option": "Adiciona opção",
     "add-option": "Adiciona opção",
+    "asset-preview-links": "",
     "assign-product-to-channel-success": "Produto atribuído com sucesso a \"{ channel }\"",
     "assign-product-to-channel-success": "Produto atribuído com sucesso a \"{ channel }\"",
     "assign-products-to-channel": "Atribuir produtos ao canal",
     "assign-products-to-channel": "Atribuir produtos ao canal",
     "assign-to-channel": "Atribuir ao canal",
     "assign-to-channel": "Atribuir ao canal",
@@ -238,6 +239,7 @@
     "sample-formatting": "",
     "sample-formatting": "",
     "select": "Selecione...",
     "select": "Selecione...",
     "select-display-language": "Selecionar idioma de exibição",
     "select-display-language": "Selecionar idioma de exibição",
+    "select-relation-id": "",
     "select-today": "Selecione hoje",
     "select-today": "Selecione hoje",
     "set-language": "",
     "set-language": "",
     "short-date": "",
     "short-date": "",
@@ -309,7 +311,7 @@
     "remove-customers-from-group-success": "Excluído {customerCount, plural, one {1 customer} other {{customerCount} customers}} from \"{ groupName }\"",
     "remove-customers-from-group-success": "Excluído {customerCount, plural, one {1 customer} other {{customerCount} customers}} from \"{ groupName }\"",
     "remove-from-group": "Excluir deste grupo",
     "remove-from-group": "Excluir deste grupo",
     "search-customers-by-email": "Busca por email",
     "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": "",
     "select-customer": "",
     "set-as-default-billing-address": "Definir como cobrança padrão",
     "set-as-default-billing-address": "Definir como cobrança padrão",
     "set-as-default-shipping-address": "Definir como remessa padrão",
     "set-as-default-shipping-address": "Definir como remessa padrão",
@@ -436,6 +438,7 @@
     "arrange-additional-payment": "",
     "arrange-additional-payment": "",
     "billing-address": "Endereço de cobrança",
     "billing-address": "Endereço de cobrança",
     "cancel": "Cancelar",
     "cancel": "Cancelar",
+    "cancel-entire-order": "",
     "cancel-fulfillment": "Cancelar cumprimento",
     "cancel-fulfillment": "Cancelar cumprimento",
     "cancel-modification": "Cancelar modificação",
     "cancel-modification": "Cancelar modificação",
     "cancel-order": "Cancelar Pedido",
     "cancel-order": "Cancelar Pedido",
@@ -443,6 +446,7 @@
     "cancel-reason-customer-request": "Pedido do cliente",
     "cancel-reason-customer-request": "Pedido do cliente",
     "cancel-reason-not-available": "Não disponível",
     "cancel-reason-not-available": "Não disponível",
     "cancel-selected-items": "Cancelar itens selecionados",
     "cancel-selected-items": "Cancelar itens selecionados",
+    "cancel-specified-items": "",
     "cancellation-reason": "Motivo do cancelamento",
     "cancellation-reason": "Motivo do cancelamento",
     "cancelled-order-success": "Pedido cancelado com sucesso",
     "cancelled-order-success": "Pedido cancelado com sucesso",
     "confirm-modifications": "Confirmar modificações",
     "confirm-modifications": "Confirmar modificações",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "",
     "search-by-customer-last-name": "",
     "search-by-order-code": "Buscar por código do pedido",
     "search-by-order-code": "Buscar por código do pedido",
     "select-state": "Selecionar estado",
     "select-state": "Selecionar estado",
+    "set-coupon-codes": "",
     "set-fulfillment-state": "Marcar como {state}",
     "set-fulfillment-state": "Marcar como {state}",
     "settle-payment": "Liquidar pagamento",
     "settle-payment": "Liquidar pagamento",
     "settle-payment-error": "Não posso liquidar pagamento",
     "settle-payment-error": "Não posso liquidar pagamento",
@@ -547,6 +552,7 @@
     "settle-refund-success": "Reembolso liquidado com sucesso",
     "settle-refund-success": "Reembolso liquidado com sucesso",
     "shipping": "Envio",
     "shipping": "Envio",
     "shipping-address": "Endereço de envio",
     "shipping-address": "Endereço de envio",
+    "shipping-cancelled": "",
     "shipping-method": "Método de envio",
     "shipping-method": "Método de envio",
     "state": "Estado",
     "state": "Estado",
     "sub-total": "Subtotal",
     "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-facet-value": "Adicionar novo valor",
     "add-facets": "Adicionar etiqueta",
     "add-facets": "Adicionar etiqueta",
     "add-option": "Adicionar opção",
     "add-option": "Adicionar opção",
+    "asset-preview-links": "",
     "assign-product-to-channel-success": "Produto atribuído com sucesso a \"{ channel }\"",
     "assign-product-to-channel-success": "Produto atribuído com sucesso a \"{ channel }\"",
     "assign-products-to-channel": "Atribuir produtos ao canal",
     "assign-products-to-channel": "Atribuir produtos ao canal",
     "assign-to-channel": "Atribuir ao canal",
     "assign-to-channel": "Atribuir ao canal",
@@ -238,6 +239,7 @@
     "sample-formatting": "Formatação de amostra",
     "sample-formatting": "Formatação de amostra",
     "select": "Seleccione...",
     "select": "Seleccione...",
     "select-display-language": "Seleccionar idioma",
     "select-display-language": "Seleccionar idioma",
+    "select-relation-id": "",
     "select-today": "Seleccione a data de hoje",
     "select-today": "Seleccione a data de hoje",
     "set-language": "Definir idioma",
     "set-language": "Definir idioma",
     "short-date": "Data abreviada",
     "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-customers-from-group-success": "{customerCount, plural, one {1 cliente eliminado} other {{customerCount} clientes eliminados}} do grupo \"{ groupName }\"",
     "remove-from-group": "Eliminar deste grupo",
     "remove-from-group": "Eliminar deste grupo",
     "search-customers-by-email": "Pesquisar por email",
     "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",
     "select-customer": "Seleccione o cliente",
     "set-as-default-billing-address": "Definir como morada de faturação padrão",
     "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",
     "set-as-default-shipping-address": "Definir como morada de entrega padrão",
@@ -436,6 +438,7 @@
     "arrange-additional-payment": "Configurar pagamento adicional",
     "arrange-additional-payment": "Configurar pagamento adicional",
     "billing-address": "Morada de faturação",
     "billing-address": "Morada de faturação",
     "cancel": "Cancelar",
     "cancel": "Cancelar",
+    "cancel-entire-order": "",
     "cancel-fulfillment": "Cancelar entrega",
     "cancel-fulfillment": "Cancelar entrega",
     "cancel-modification": "Cancelar modificação",
     "cancel-modification": "Cancelar modificação",
     "cancel-order": "Cancelar encomenda",
     "cancel-order": "Cancelar encomenda",
@@ -443,6 +446,7 @@
     "cancel-reason-customer-request": "Encomenda do cliente",
     "cancel-reason-customer-request": "Encomenda do cliente",
     "cancel-reason-not-available": "Não disponível",
     "cancel-reason-not-available": "Não disponível",
     "cancel-selected-items": "Cancelar itens seleccionados",
     "cancel-selected-items": "Cancelar itens seleccionados",
+    "cancel-specified-items": "",
     "cancellation-reason": "Motivo do cancelamento",
     "cancellation-reason": "Motivo do cancelamento",
     "cancelled-order-success": "Encomenda cancelada com sucesso",
     "cancelled-order-success": "Encomenda cancelada com sucesso",
     "confirm-modifications": "Confirmar modificações",
     "confirm-modifications": "Confirmar modificações",
@@ -538,6 +542,7 @@
     "search-by-customer-last-name": "Pesquisar pelo apelido do cliente",
     "search-by-customer-last-name": "Pesquisar pelo apelido do cliente",
     "search-by-order-code": "Pesqusiar pelo código da encomenda",
     "search-by-order-code": "Pesqusiar pelo código da encomenda",
     "select-state": "Seleccionar estado",
     "select-state": "Seleccionar estado",
+    "set-coupon-codes": "",
     "set-fulfillment-state": "Marcar como {state}",
     "set-fulfillment-state": "Marcar como {state}",
     "settle-payment": "Liquidar pagamento",
     "settle-payment": "Liquidar pagamento",
     "settle-payment-error": "Não foi possível liquidar o pagamento",
     "settle-payment-error": "Não foi possível liquidar o pagamento",
@@ -547,6 +552,7 @@
     "settle-refund-success": "Reembolso liquidado com sucesso",
     "settle-refund-success": "Reembolso liquidado com sucesso",
     "shipping": "Envio",
     "shipping": "Envio",
     "shipping-address": "Morada de entrega",
     "shipping-address": "Morada de entrega",
+    "shipping-cancelled": "",
     "shipping-method": "Método de envio",
     "shipping-method": "Método de envio",
     "state": "Estado",
     "state": "Estado",
     "sub-total": "Subtotal",
     "sub-total": "Subtotal",

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

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

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

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

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

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

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

@@ -55,6 +55,7 @@
     "add-facet-value": "新增特徵值",
     "add-facet-value": "新增特徵值",
     "add-facets": "新增特徵",
     "add-facets": "新增特徵",
     "add-option": "新增規格選項",
     "add-option": "新增規格選項",
+    "asset-preview-links": "",
     "assign-product-to-channel-success": "成功將產品新增至渠道\"{ channel }\"",
     "assign-product-to-channel-success": "成功將產品新增至渠道\"{ 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-today": "選擇今天",
     "select-today": "選擇今天",
     "set-language": "",
     "set-language": "",
     "short-date": "",
     "short-date": "",
@@ -309,7 +311,7 @@
     "remove-customers-from-group-success": "",
     "remove-customers-from-group-success": "",
     "remove-from-group": "",
     "remove-from-group": "",
     "search-customers-by-email": "輸入要搜索的客户電郵地址",
     "search-customers-by-email": "輸入要搜索的客户電郵地址",
-    "search-customers-by-last-name": "",
+    "search-customers-by-email-last-name-postal-code": "",
     "select-customer": "",
     "select-customer": "",
     "set-as-default-billing-address": "設定為默認賬單地址",
     "set-as-default-billing-address": "設定為默認賬單地址",
     "set-as-default-shipping-address": "設定為默認郵寄地址",
     "set-as-default-shipping-address": "設定為默認郵寄地址",
@@ -436,6 +438,7 @@
     "arrange-additional-payment": "",
     "arrange-additional-payment": "",
     "billing-address": "",
     "billing-address": "",
     "cancel": "取消",
     "cancel": "取消",
+    "cancel-entire-order": "",
     "cancel-fulfillment": "",
     "cancel-fulfillment": "",
     "cancel-modification": "",
     "cancel-modification": "",
     "cancel-order": "取消訂單",
     "cancel-order": "取消訂單",
@@ -443,6 +446,7 @@
     "cancel-reason-customer-request": "客户要求",
     "cancel-reason-customer-request": "客户要求",
     "cancel-reason-not-available": "產品無庫存",
     "cancel-reason-not-available": "產品無庫存",
     "cancel-selected-items": "取消已選",
     "cancel-selected-items": "取消已選",
+    "cancel-specified-items": "",
     "cancellation-reason": "取消原因",
     "cancellation-reason": "取消原因",
     "cancelled-order-success": "訂單取消成功",
     "cancelled-order-success": "訂單取消成功",
     "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-fulfillment-state": "",
     "set-fulfillment-state": "",
     "settle-payment": "結算付款",
     "settle-payment": "結算付款",
     "settle-payment-error": "結算付款失敗",
     "settle-payment-error": "結算付款失敗",
@@ -547,6 +552,7 @@
     "settle-refund-success": "結算退款成功",
     "settle-refund-success": "結算退款成功",
     "shipping": "運費",
     "shipping": "運費",
     "shipping-address": "配送地址",
     "shipping-address": "配送地址",
+    "shipping-cancelled": "",
     "shipping-method": "配送方式",
     "shipping-method": "配送方式",
     "state": "狀態",
     "state": "狀態",
     "sub-total": "小計金額",
     "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 {
 export enum AdjustmentType {
     PROMOTION = 'PROMOTION',
     PROMOTION = 'PROMOTION',
     DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
     DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
+    OTHER = 'OTHER',
 }
 }
 
 
 export type Administrator = Node & {
 export type Administrator = Node & {
@@ -273,6 +274,11 @@ export type BooleanCustomFieldConfig = CustomField & {
     ui?: Maybe<Scalars['JSON']>;
     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 */
 /** Operators for filtering on a Boolean field */
 export type BooleanOperators = {
 export type BooleanOperators = {
     eq?: Maybe<Scalars['Boolean']>;
     eq?: Maybe<Scalars['Boolean']>;
@@ -290,6 +296,8 @@ export type CancelOrderInput = {
     orderId: Scalars['ID'];
     orderId: Scalars['ID'];
     /** Optionally specify which OrderLines to cancel. If not provided, all OrderLines will be cancelled */
     /** Optionally specify which OrderLines to cancel. If not provided, all OrderLines will be cancelled */
     lines?: Maybe<Array<OrderLineInput>>;
     lines?: Maybe<Array<OrderLineInput>>;
+    /** Specify whether the shipping charges should also be cancelled. Defaults to false */
+    cancelShipping?: Maybe<Scalars['Boolean']>;
     reason?: Maybe<Scalars['String']>;
     reason?: Maybe<Scalars['String']>;
 };
 };
 
 
@@ -538,6 +546,28 @@ export type CountryTranslationInput = {
     customFields?: Maybe<Scalars['JSON']>;
     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 = {
 export type CreateAddressInput = {
     fullName?: Maybe<Scalars['String']>;
     fullName?: Maybe<Scalars['String']>;
     company?: Maybe<Scalars['String']>;
     company?: Maybe<Scalars['String']>;
@@ -1181,6 +1211,7 @@ export type CustomerOrdersArgs = {
 };
 };
 
 
 export type CustomerFilterParameter = {
 export type CustomerFilterParameter = {
+    postalCode?: Maybe<StringOperators>;
     id?: Maybe<IdOperators>;
     id?: Maybe<IdOperators>;
     createdAt?: Maybe<DateOperators>;
     createdAt?: Maybe<DateOperators>;
     updatedAt?: Maybe<DateOperators>;
     updatedAt?: Maybe<DateOperators>;
@@ -1265,6 +1296,11 @@ export type CustomerSortParameter = {
     emailAddress?: Maybe<SortOrder>;
     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 */
 /** Operators for filtering on a DateTime field */
 export type DateOperators = {
 export type DateOperators = {
     eq?: Maybe<Scalars['DateTime']>;
     eq?: Maybe<Scalars['DateTime']>;
@@ -1376,6 +1412,9 @@ export enum ErrorCode {
     ORDER_LIMIT_ERROR = 'ORDER_LIMIT_ERROR',
     ORDER_LIMIT_ERROR = 'ORDER_LIMIT_ERROR',
     NEGATIVE_QUANTITY_ERROR = 'NEGATIVE_QUANTITY_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 = {
 export type ErrorResult = {
@@ -1623,6 +1662,11 @@ export enum HistoryEntryType {
     ORDER_MODIFIED = 'ORDER_MODIFIED',
     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 */
 /** Operators for filtering on an ID field */
 export type IdOperators = {
 export type IdOperators = {
     eq?: Maybe<Scalars['String']>;
     eq?: Maybe<Scalars['String']>;
@@ -2174,6 +2218,7 @@ export type ModifyOrderInput = {
     note?: Maybe<Scalars['String']>;
     note?: Maybe<Scalars['String']>;
     refund?: Maybe<AdministratorRefundInput>;
     refund?: Maybe<AdministratorRefundInput>;
     options?: Maybe<ModifyOrderOptions>;
     options?: Maybe<ModifyOrderOptions>;
+    couponCodes?: Maybe<Array<Scalars['String']>>;
 };
 };
 
 
 export type ModifyOrderOptions = {
 export type ModifyOrderOptions = {
@@ -2189,7 +2234,10 @@ export type ModifyOrderResult =
     | RefundPaymentIdMissingError
     | RefundPaymentIdMissingError
     | OrderLimitError
     | OrderLimitError
     | NegativeQuantityError
     | NegativeQuantityError
-    | InsufficientStockError;
+    | InsufficientStockError
+    | CouponCodeExpiredError
+    | CouponCodeInvalidError
+    | CouponCodeLimitError;
 
 
 export type MoveCollectionInput = {
 export type MoveCollectionInput = {
     collectionId: Scalars['ID'];
     collectionId: Scalars['ID'];
@@ -2882,6 +2930,11 @@ export type NothingToRefundError = ErrorResult & {
     message: Scalars['String'];
     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 */
 /** Operators for filtering on a Int or Float field */
 export type NumberOperators = {
 export type NumberOperators = {
     eq?: Maybe<Scalars['Float']>;
     eq?: Maybe<Scalars['Float']>;
@@ -4458,6 +4511,11 @@ export type StringFieldOption = {
     label?: Maybe<Array<LocalizedString>>;
     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 */
 /** Operators for filtering on a String field */
 export type StringOperators = {
 export type StringOperators = {
     eq?: Maybe<Scalars['String']>;
     eq?: Maybe<Scalars['String']>;

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

@@ -59,6 +59,7 @@ export type Adjustment = {
 export enum AdjustmentType {
 export enum AdjustmentType {
     PROMOTION = 'PROMOTION',
     PROMOTION = 'PROMOTION',
     DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
     DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
+    OTHER = 'OTHER',
 }
 }
 
 
 /** Returned when attempting to set the Customer for an Order when already logged in. */
 /** 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']>;
     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 */
 /** Operators for filtering on a Boolean field */
 export type BooleanOperators = {
 export type BooleanOperators = {
     eq?: Maybe<Scalars['Boolean']>;
     eq?: Maybe<Scalars['Boolean']>;
@@ -803,6 +809,11 @@ export type CustomerSortParameter = {
     emailAddress?: Maybe<SortOrder>;
     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 */
 /** Operators for filtering on a DateTime field */
 export type DateOperators = {
 export type DateOperators = {
     eq?: Maybe<Scalars['DateTime']>;
     eq?: Maybe<Scalars['DateTime']>;
@@ -874,17 +885,18 @@ export enum ErrorCode {
     ORDER_LIMIT_ERROR = 'ORDER_LIMIT_ERROR',
     ORDER_LIMIT_ERROR = 'ORDER_LIMIT_ERROR',
     NEGATIVE_QUANTITY_ERROR = 'NEGATIVE_QUANTITY_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',
     ORDER_MODIFICATION_ERROR = 'ORDER_MODIFICATION_ERROR',
     ORDER_MODIFICATION_ERROR = 'ORDER_MODIFICATION_ERROR',
     INELIGIBLE_SHIPPING_METHOD_ERROR = 'INELIGIBLE_SHIPPING_METHOD_ERROR',
     INELIGIBLE_SHIPPING_METHOD_ERROR = 'INELIGIBLE_SHIPPING_METHOD_ERROR',
     ORDER_PAYMENT_STATE_ERROR = 'ORDER_PAYMENT_STATE_ERROR',
     ORDER_PAYMENT_STATE_ERROR = 'ORDER_PAYMENT_STATE_ERROR',
     INELIGIBLE_PAYMENT_METHOD_ERROR = 'INELIGIBLE_PAYMENT_METHOD_ERROR',
     INELIGIBLE_PAYMENT_METHOD_ERROR = 'INELIGIBLE_PAYMENT_METHOD_ERROR',
     PAYMENT_FAILED_ERROR = 'PAYMENT_FAILED_ERROR',
     PAYMENT_FAILED_ERROR = 'PAYMENT_FAILED_ERROR',
     PAYMENT_DECLINED_ERROR = 'PAYMENT_DECLINED_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',
     ALREADY_LOGGED_IN_ERROR = 'ALREADY_LOGGED_IN_ERROR',
     MISSING_PASSWORD_ERROR = 'MISSING_PASSWORD_ERROR',
     MISSING_PASSWORD_ERROR = 'MISSING_PASSWORD_ERROR',
+    PASSWORD_VALIDATION_ERROR = 'PASSWORD_VALIDATION_ERROR',
     PASSWORD_ALREADY_SET_ERROR = 'PASSWORD_ALREADY_SET_ERROR',
     PASSWORD_ALREADY_SET_ERROR = 'PASSWORD_ALREADY_SET_ERROR',
     VERIFICATION_TOKEN_INVALID_ERROR = 'VERIFICATION_TOKEN_INVALID_ERROR',
     VERIFICATION_TOKEN_INVALID_ERROR = 'VERIFICATION_TOKEN_INVALID_ERROR',
     VERIFICATION_TOKEN_EXPIRED_ERROR = 'VERIFICATION_TOKEN_EXPIRED_ERROR',
     VERIFICATION_TOKEN_EXPIRED_ERROR = 'VERIFICATION_TOKEN_EXPIRED_ERROR',
@@ -1106,6 +1118,11 @@ export enum HistoryEntryType {
     ORDER_MODIFIED = 'ORDER_MODIFIED',
     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 */
 /** Operators for filtering on an ID field */
 export type IdOperators = {
 export type IdOperators = {
     eq?: Maybe<Scalars['String']>;
     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.
      * 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.
      * provided here.
      */
      */
     verifyCustomerAccount: VerifyCustomerAccountResult;
     verifyCustomerAccount: VerifyCustomerAccountResult;
@@ -1791,6 +1808,11 @@ export type NotVerifiedError = ErrorResult & {
     message: Scalars['String'];
     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 */
 /** Operators for filtering on a Int or Float field */
 export type NumberOperators = {
 export type NumberOperators = {
     eq?: Maybe<Scalars['Float']>;
     eq?: Maybe<Scalars['Float']>;
@@ -2113,6 +2135,14 @@ export type PasswordResetTokenInvalidError = ErrorResult & {
     message: Scalars['String'];
     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 & {
 export type Payment = Node & {
     __typename?: 'Payment';
     __typename?: 'Payment';
     id: Scalars['ID'];
     id: Scalars['ID'];
@@ -2695,7 +2725,11 @@ export type Refund = Node & {
     metadata?: Maybe<Scalars['JSON']>;
     metadata?: Maybe<Scalars['JSON']>;
 };
 };
 
 
-export type RegisterCustomerAccountResult = Success | MissingPasswordError | NativeAuthStrategyError;
+export type RegisterCustomerAccountResult =
+    | Success
+    | MissingPasswordError
+    | PasswordValidationError
+    | NativeAuthStrategyError;
 
 
 export type RegisterCustomerInput = {
 export type RegisterCustomerInput = {
     emailAddress: Scalars['String'];
     emailAddress: Scalars['String'];
@@ -2735,6 +2769,7 @@ export type ResetPasswordResult =
     | CurrentUser
     | CurrentUser
     | PasswordResetTokenInvalidError
     | PasswordResetTokenInvalidError
     | PasswordResetTokenExpiredError
     | PasswordResetTokenExpiredError
+    | PasswordValidationError
     | NativeAuthStrategyError
     | NativeAuthStrategyError
     | NotVerifiedError;
     | NotVerifiedError;
 
 
@@ -2799,7 +2834,7 @@ export type SearchResult = {
     facetValueIds: Array<Scalars['ID']>;
     facetValueIds: Array<Scalars['ID']>;
     /** An array of ids of the Collections in which this result appears */
     /** An array of ids of the Collections in which this result appears */
     collectionIds: Array<Scalars['ID']>;
     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'];
     score: Scalars['Float'];
 };
 };
 
 
@@ -2917,6 +2952,11 @@ export type StringFieldOption = {
     label?: Maybe<Array<LocalizedString>>;
     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 */
 /** Operators for filtering on a String field */
 export type StringOperators = {
 export type StringOperators = {
     eq?: Maybe<Scalars['String']>;
     eq?: Maybe<Scalars['String']>;
@@ -3042,7 +3082,11 @@ export type UpdateCustomerInput = {
     customFields?: Maybe<Scalars['JSON']>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 };
 
 
-export type UpdateCustomerPasswordResult = Success | InvalidCredentialsError | NativeAuthStrategyError;
+export type UpdateCustomerPasswordResult =
+    | Success
+    | InvalidCredentialsError
+    | PasswordValidationError
+    | NativeAuthStrategyError;
 
 
 export type UpdateOrderInput = {
 export type UpdateOrderInput = {
     customFields?: Maybe<Scalars['JSON']>;
     customFields?: Maybe<Scalars['JSON']>;
@@ -3093,6 +3137,7 @@ export type VerifyCustomerAccountResult =
     | VerificationTokenInvalidError
     | VerificationTokenInvalidError
     | VerificationTokenExpiredError
     | VerificationTokenExpiredError
     | MissingPasswordError
     | MissingPasswordError
+    | PasswordValidationError
     | PasswordAlreadySetError
     | PasswordAlreadySetError
     | NativeAuthStrategyError;
     | NativeAuthStrategyError;
 
 

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

@@ -73,7 +73,8 @@ export type Adjustment = {
 
 
 export enum AdjustmentType {
 export enum AdjustmentType {
   PROMOTION = 'PROMOTION',
   PROMOTION = 'PROMOTION',
-  DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION'
+  DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
+  OTHER = 'OTHER'
 }
 }
 
 
 export type Administrator = Node & {
 export type Administrator = Node & {
@@ -275,6 +276,11 @@ export type BooleanCustomFieldConfig = CustomField & {
   ui?: Maybe<Scalars['JSON']>;
   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 */
 /** Operators for filtering on a Boolean field */
 export type BooleanOperators = {
 export type BooleanOperators = {
   eq?: Maybe<Scalars['Boolean']>;
   eq?: Maybe<Scalars['Boolean']>;
@@ -293,6 +299,8 @@ export type CancelOrderInput = {
   orderId: Scalars['ID'];
   orderId: Scalars['ID'];
   /** Optionally specify which OrderLines to cancel. If not provided, all OrderLines will be cancelled */
   /** Optionally specify which OrderLines to cancel. If not provided, all OrderLines will be cancelled */
   lines?: Maybe<Array<OrderLineInput>>;
   lines?: Maybe<Array<OrderLineInput>>;
+  /** Specify whether the shipping charges should also be cancelled. Defaults to false */
+  cancelShipping?: Maybe<Scalars['Boolean']>;
   reason?: Maybe<Scalars['String']>;
   reason?: Maybe<Scalars['String']>;
 };
 };
 
 
@@ -551,6 +559,31 @@ export type CountryTranslationInput = {
   customFields?: Maybe<Scalars['JSON']>;
   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 = {
 export type CreateAddressInput = {
   fullName?: Maybe<Scalars['String']>;
   fullName?: Maybe<Scalars['String']>;
   company?: Maybe<Scalars['String']>;
   company?: Maybe<Scalars['String']>;
@@ -1193,6 +1226,7 @@ export type CustomerOrdersArgs = {
 };
 };
 
 
 export type CustomerFilterParameter = {
 export type CustomerFilterParameter = {
+  postalCode?: Maybe<StringOperators>;
   id?: Maybe<IdOperators>;
   id?: Maybe<IdOperators>;
   createdAt?: Maybe<DateOperators>;
   createdAt?: Maybe<DateOperators>;
   updatedAt?: Maybe<DateOperators>;
   updatedAt?: Maybe<DateOperators>;
@@ -1281,6 +1315,11 @@ export type CustomerSortParameter = {
   emailAddress?: Maybe<SortOrder>;
   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 */
 /** Operators for filtering on a DateTime field */
 export type DateOperators = {
 export type DateOperators = {
   eq?: Maybe<Scalars['DateTime']>;
   eq?: Maybe<Scalars['DateTime']>;
@@ -1397,7 +1436,10 @@ export enum ErrorCode {
   EMAIL_ADDRESS_CONFLICT_ERROR = 'EMAIL_ADDRESS_CONFLICT_ERROR',
   EMAIL_ADDRESS_CONFLICT_ERROR = 'EMAIL_ADDRESS_CONFLICT_ERROR',
   ORDER_LIMIT_ERROR = 'ORDER_LIMIT_ERROR',
   ORDER_LIMIT_ERROR = 'ORDER_LIMIT_ERROR',
   NEGATIVE_QUANTITY_ERROR = 'NEGATIVE_QUANTITY_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 = {
 export type ErrorResult = {
@@ -1657,6 +1699,11 @@ export enum HistoryEntryType {
   ORDER_MODIFIED = 'ORDER_MODIFIED'
   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 */
 /** Operators for filtering on an ID field */
 export type IdOperators = {
 export type IdOperators = {
   eq?: Maybe<Scalars['String']>;
   eq?: Maybe<Scalars['String']>;
@@ -2226,6 +2273,7 @@ export type ModifyOrderInput = {
   note?: Maybe<Scalars['String']>;
   note?: Maybe<Scalars['String']>;
   refund?: Maybe<AdministratorRefundInput>;
   refund?: Maybe<AdministratorRefundInput>;
   options?: Maybe<ModifyOrderOptions>;
   options?: Maybe<ModifyOrderOptions>;
+  couponCodes?: Maybe<Array<Scalars['String']>>;
 };
 };
 
 
 export type ModifyOrderOptions = {
 export type ModifyOrderOptions = {
@@ -2233,7 +2281,7 @@ export type ModifyOrderOptions = {
   recalculateShipping?: Maybe<Scalars['Boolean']>;
   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 = {
 export type MoveCollectionInput = {
   collectionId: Scalars['ID'];
   collectionId: Scalars['ID'];
@@ -3037,6 +3085,11 @@ export type NothingToRefundError = ErrorResult & {
   message: Scalars['String'];
   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 */
 /** Operators for filtering on a Int or Float field */
 export type NumberOperators = {
 export type NumberOperators = {
   eq?: Maybe<Scalars['Float']>;
   eq?: Maybe<Scalars['Float']>;
@@ -4704,6 +4757,11 @@ export type StringFieldOption = {
   label?: Maybe<Array<LocalizedString>>;
   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 */
 /** Operators for filtering on a String field */
 export type StringOperators = {
 export type StringOperators = {
   eq?: Maybe<Scalars['String']>;
   eq?: Maybe<Scalars['String']>;

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

@@ -264,8 +264,6 @@ export interface AdminUiConfig {
     /**
     /**
      * @description
      * @description
      * An array of languages for which translations exist for the Admin UI.
      * An array of languages for which translations exist for the Admin UI.
-     *
-     * @default [LanguageCode.en, LanguageCode.es]
      */
      */
     availableLanguages: LanguageCode[];
     availableLanguages: LanguageCode[];
     /**
     /**
@@ -295,6 +293,16 @@ export interface AdminUiConfig {
      * @default false
      * @default false
      */
      */
     hideVersion?: boolean;
     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,
     DELETE_PRODUCT_VARIANT,
     DELETE_PRODUCT_VARIANT,
     GET_ASSET_LIST,
     GET_ASSET_LIST,
+    GET_COLLECTIONS,
     UPDATE_COLLECTION,
     UPDATE_COLLECTION,
     UPDATE_PRODUCT,
     UPDATE_PRODUCT,
     UPDATE_PRODUCT_VARIANTS,
     UPDATE_PRODUCT_VARIANTS,
@@ -1809,22 +1810,6 @@ const GET_FACET_VALUES = gql`
     ${FACET_VALUE_FRAGMENT}
     ${FACET_VALUE_FRAGMENT}
 `;
 `;
 
 
-const GET_COLLECTIONS = gql`
-    query GetCollections {
-        collections {
-            items {
-                id
-                name
-                position
-                parent {
-                    id
-                    name
-                }
-            }
-        }
-    }
-`;
-
 const GET_COLLECTION_PRODUCT_VARIANTS = gql`
 const GET_COLLECTION_PRODUCT_VARIANTS = gql`
     query GetCollectionProducts($id: ID!) {
     query GetCollectionProducts($id: ID!) {
         collection(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);
             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(
         it(
             'cannot filter by internal field in Admin API',
             'cannot filter by internal field in Admin API',
             assertThrowsWithMessage(async () => {
             assertThrowsWithMessage(async () => {

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

@@ -112,6 +112,24 @@ describe('Customer resolver', () => {
         thirdCustomer = result.customers.items[2];
         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 () => {
     it('customer resolver resolves User', async () => {
         const { customer } = await adminClient.query<
         const { customer } = await adminClient.query<
             GetCustomerWithUser.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 {
 export enum AdjustmentType {
     PROMOTION = 'PROMOTION',
     PROMOTION = 'PROMOTION',
     DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
     DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
+    OTHER = 'OTHER',
 }
 }
 
 
 export type Administrator = Node & {
 export type Administrator = Node & {
@@ -273,6 +274,11 @@ export type BooleanCustomFieldConfig = CustomField & {
     ui?: Maybe<Scalars['JSON']>;
     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 */
 /** Operators for filtering on a Boolean field */
 export type BooleanOperators = {
 export type BooleanOperators = {
     eq?: Maybe<Scalars['Boolean']>;
     eq?: Maybe<Scalars['Boolean']>;
@@ -290,6 +296,8 @@ export type CancelOrderInput = {
     orderId: Scalars['ID'];
     orderId: Scalars['ID'];
     /** Optionally specify which OrderLines to cancel. If not provided, all OrderLines will be cancelled */
     /** Optionally specify which OrderLines to cancel. If not provided, all OrderLines will be cancelled */
     lines?: Maybe<Array<OrderLineInput>>;
     lines?: Maybe<Array<OrderLineInput>>;
+    /** Specify whether the shipping charges should also be cancelled. Defaults to false */
+    cancelShipping?: Maybe<Scalars['Boolean']>;
     reason?: Maybe<Scalars['String']>;
     reason?: Maybe<Scalars['String']>;
 };
 };
 
 
@@ -538,6 +546,28 @@ export type CountryTranslationInput = {
     customFields?: Maybe<Scalars['JSON']>;
     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 = {
 export type CreateAddressInput = {
     fullName?: Maybe<Scalars['String']>;
     fullName?: Maybe<Scalars['String']>;
     company?: Maybe<Scalars['String']>;
     company?: Maybe<Scalars['String']>;
@@ -1181,6 +1211,7 @@ export type CustomerOrdersArgs = {
 };
 };
 
 
 export type CustomerFilterParameter = {
 export type CustomerFilterParameter = {
+    postalCode?: Maybe<StringOperators>;
     id?: Maybe<IdOperators>;
     id?: Maybe<IdOperators>;
     createdAt?: Maybe<DateOperators>;
     createdAt?: Maybe<DateOperators>;
     updatedAt?: Maybe<DateOperators>;
     updatedAt?: Maybe<DateOperators>;
@@ -1265,6 +1296,11 @@ export type CustomerSortParameter = {
     emailAddress?: Maybe<SortOrder>;
     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 */
 /** Operators for filtering on a DateTime field */
 export type DateOperators = {
 export type DateOperators = {
     eq?: Maybe<Scalars['DateTime']>;
     eq?: Maybe<Scalars['DateTime']>;
@@ -1376,6 +1412,9 @@ export enum ErrorCode {
     ORDER_LIMIT_ERROR = 'ORDER_LIMIT_ERROR',
     ORDER_LIMIT_ERROR = 'ORDER_LIMIT_ERROR',
     NEGATIVE_QUANTITY_ERROR = 'NEGATIVE_QUANTITY_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 = {
 export type ErrorResult = {
@@ -1623,6 +1662,11 @@ export enum HistoryEntryType {
     ORDER_MODIFIED = 'ORDER_MODIFIED',
     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 */
 /** Operators for filtering on an ID field */
 export type IdOperators = {
 export type IdOperators = {
     eq?: Maybe<Scalars['String']>;
     eq?: Maybe<Scalars['String']>;
@@ -2174,6 +2218,7 @@ export type ModifyOrderInput = {
     note?: Maybe<Scalars['String']>;
     note?: Maybe<Scalars['String']>;
     refund?: Maybe<AdministratorRefundInput>;
     refund?: Maybe<AdministratorRefundInput>;
     options?: Maybe<ModifyOrderOptions>;
     options?: Maybe<ModifyOrderOptions>;
+    couponCodes?: Maybe<Array<Scalars['String']>>;
 };
 };
 
 
 export type ModifyOrderOptions = {
 export type ModifyOrderOptions = {
@@ -2189,7 +2234,10 @@ export type ModifyOrderResult =
     | RefundPaymentIdMissingError
     | RefundPaymentIdMissingError
     | OrderLimitError
     | OrderLimitError
     | NegativeQuantityError
     | NegativeQuantityError
-    | InsufficientStockError;
+    | InsufficientStockError
+    | CouponCodeExpiredError
+    | CouponCodeInvalidError
+    | CouponCodeLimitError;
 
 
 export type MoveCollectionInput = {
 export type MoveCollectionInput = {
     collectionId: Scalars['ID'];
     collectionId: Scalars['ID'];
@@ -2882,6 +2930,11 @@ export type NothingToRefundError = ErrorResult & {
     message: Scalars['String'];
     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 */
 /** Operators for filtering on a Int or Float field */
 export type NumberOperators = {
 export type NumberOperators = {
     eq?: Maybe<Scalars['Float']>;
     eq?: Maybe<Scalars['Float']>;
@@ -4458,6 +4511,11 @@ export type StringFieldOption = {
     label?: Maybe<Array<LocalizedString>>;
     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 */
 /** Operators for filtering on a String field */
 export type StringOperators = {
 export type StringOperators = {
     eq?: Maybe<Scalars['String']>;
     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 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<{
 export type GetCollectionProductsQueryVariables = Exact<{
     id: Scalars['ID'];
     id: Scalars['ID'];
 }>;
 }>;
@@ -6408,6 +6456,16 @@ export type GetShippingMethodListQuery = {
     shippingMethods: Pick<ShippingMethodList, 'totalItems'> & { items: Array<ShippingMethodFragment> };
     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<{
 export type CancelJobMutationVariables = Exact<{
     id: Scalars['ID'];
     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<
     lines: Array<
         Pick<
         Pick<
             OrderLine,
             OrderLine,
-            'id' | 'quantity' | 'linePrice' | 'linePriceWithTax' | 'discountedLinePriceWithTax'
+            | 'id'
+            | 'quantity'
+            | 'linePrice'
+            | 'linePriceWithTax'
+            | 'discountedLinePriceWithTax'
+            | 'proratedLinePriceWithTax'
         > & {
         > & {
+            discounts: Array<Pick<Discount, 'description' | 'amountWithTax'>>;
             productVariant: Pick<ProductVariant, 'id' | 'name'>;
             productVariant: Pick<ProductVariant, 'id' | 'name'>;
             items: Array<Pick<OrderItem, 'id' | 'createdAt' | 'updatedAt' | 'cancelled' | 'unitPrice'>>;
             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'>>;
             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<
     shippingAddress?: Maybe<
         Pick<OrderAddress, 'streetLine1' | 'city' | 'postalCode' | 'province' | 'countryCode' | 'country'>
         Pick<OrderAddress, 'streetLine1' | 'city' | 'postalCode' | 'province' | 'countryCode' | 'country'>
     >;
     >;
@@ -6483,7 +6559,10 @@ export type ModifyOrderMutation = {
         | Pick<RefundPaymentIdMissingError, 'errorCode' | 'message'>
         | Pick<RefundPaymentIdMissingError, 'errorCode' | 'message'>
         | Pick<OrderLimitError, 'errorCode' | 'message'>
         | Pick<OrderLimitError, 'errorCode' | 'message'>
         | Pick<NegativeQuantityError, '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<{
 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 namespace GetCollectionProducts {
     export type Variables = GetCollectionProductsQueryVariables;
     export type Variables = GetCollectionProductsQueryVariables;
     export type Query = GetCollectionProductsQuery;
     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 namespace CancelJob {
     export type Variables = CancelJobMutationVariables;
     export type Variables = CancelJobMutationVariables;
     export type Mutation = CancelJobMutation;
     export type Mutation = CancelJobMutation;
@@ -8800,6 +8879,11 @@ export namespace GetFulfillmentHandlers {
 export namespace OrderWithModifications {
 export namespace OrderWithModifications {
     export type Fragment = OrderWithModificationsFragment;
     export type Fragment = OrderWithModificationsFragment;
     export type Lines = NonNullable<NonNullable<OrderWithModificationsFragment['lines']>[number]>;
     export type Lines = NonNullable<NonNullable<OrderWithModificationsFragment['lines']>[number]>;
+    export type Discounts = NonNullable<
+        NonNullable<
+            NonNullable<NonNullable<OrderWithModificationsFragment['lines']>[number]>['discounts']
+        >[number]
+    >;
     export type ProductVariant = NonNullable<
     export type ProductVariant = NonNullable<
         NonNullable<NonNullable<OrderWithModificationsFragment['lines']>[number]>['productVariant']
         NonNullable<NonNullable<OrderWithModificationsFragment['lines']>[number]>['productVariant']
     >;
     >;
@@ -8834,6 +8918,8 @@ export namespace OrderWithModifications {
     export type Refund = NonNullable<
     export type Refund = NonNullable<
         NonNullable<NonNullable<OrderWithModificationsFragment['modifications']>[number]>['refund']
         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 ShippingAddress = NonNullable<OrderWithModificationsFragment['shippingAddress']>;
     export type BillingAddress = NonNullable<OrderWithModificationsFragment['billingAddress']>;
     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 {
 export enum AdjustmentType {
     PROMOTION = 'PROMOTION',
     PROMOTION = 'PROMOTION',
     DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
     DISTRIBUTED_ORDER_PROMOTION = 'DISTRIBUTED_ORDER_PROMOTION',
+    OTHER = 'OTHER',
 }
 }
 
 
 /** Returned when attempting to set the Customer for an Order when already logged in. */
 /** 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']>;
     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 */
 /** Operators for filtering on a Boolean field */
 export type BooleanOperators = {
 export type BooleanOperators = {
     eq?: Maybe<Scalars['Boolean']>;
     eq?: Maybe<Scalars['Boolean']>;
@@ -774,6 +780,11 @@ export type CustomerSortParameter = {
     emailAddress?: Maybe<SortOrder>;
     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 */
 /** Operators for filtering on a DateTime field */
 export type DateOperators = {
 export type DateOperators = {
     eq?: Maybe<Scalars['DateTime']>;
     eq?: Maybe<Scalars['DateTime']>;
@@ -841,17 +852,18 @@ export enum ErrorCode {
     ORDER_LIMIT_ERROR = 'ORDER_LIMIT_ERROR',
     ORDER_LIMIT_ERROR = 'ORDER_LIMIT_ERROR',
     NEGATIVE_QUANTITY_ERROR = 'NEGATIVE_QUANTITY_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',
     ORDER_MODIFICATION_ERROR = 'ORDER_MODIFICATION_ERROR',
     ORDER_MODIFICATION_ERROR = 'ORDER_MODIFICATION_ERROR',
     INELIGIBLE_SHIPPING_METHOD_ERROR = 'INELIGIBLE_SHIPPING_METHOD_ERROR',
     INELIGIBLE_SHIPPING_METHOD_ERROR = 'INELIGIBLE_SHIPPING_METHOD_ERROR',
     ORDER_PAYMENT_STATE_ERROR = 'ORDER_PAYMENT_STATE_ERROR',
     ORDER_PAYMENT_STATE_ERROR = 'ORDER_PAYMENT_STATE_ERROR',
     INELIGIBLE_PAYMENT_METHOD_ERROR = 'INELIGIBLE_PAYMENT_METHOD_ERROR',
     INELIGIBLE_PAYMENT_METHOD_ERROR = 'INELIGIBLE_PAYMENT_METHOD_ERROR',
     PAYMENT_FAILED_ERROR = 'PAYMENT_FAILED_ERROR',
     PAYMENT_FAILED_ERROR = 'PAYMENT_FAILED_ERROR',
     PAYMENT_DECLINED_ERROR = 'PAYMENT_DECLINED_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',
     ALREADY_LOGGED_IN_ERROR = 'ALREADY_LOGGED_IN_ERROR',
     MISSING_PASSWORD_ERROR = 'MISSING_PASSWORD_ERROR',
     MISSING_PASSWORD_ERROR = 'MISSING_PASSWORD_ERROR',
+    PASSWORD_VALIDATION_ERROR = 'PASSWORD_VALIDATION_ERROR',
     PASSWORD_ALREADY_SET_ERROR = 'PASSWORD_ALREADY_SET_ERROR',
     PASSWORD_ALREADY_SET_ERROR = 'PASSWORD_ALREADY_SET_ERROR',
     VERIFICATION_TOKEN_INVALID_ERROR = 'VERIFICATION_TOKEN_INVALID_ERROR',
     VERIFICATION_TOKEN_INVALID_ERROR = 'VERIFICATION_TOKEN_INVALID_ERROR',
     VERIFICATION_TOKEN_EXPIRED_ERROR = 'VERIFICATION_TOKEN_EXPIRED_ERROR',
     VERIFICATION_TOKEN_EXPIRED_ERROR = 'VERIFICATION_TOKEN_EXPIRED_ERROR',
@@ -1063,6 +1075,11 @@ export enum HistoryEntryType {
     ORDER_MODIFIED = 'ORDER_MODIFIED',
     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 */
 /** Operators for filtering on an ID field */
 export type IdOperators = {
 export type IdOperators = {
     eq?: Maybe<Scalars['String']>;
     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.
      * 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.
      * provided here.
      */
      */
     verifyCustomerAccount: VerifyCustomerAccountResult;
     verifyCustomerAccount: VerifyCustomerAccountResult;
@@ -1733,6 +1750,11 @@ export type NotVerifiedError = ErrorResult & {
     message: Scalars['String'];
     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 */
 /** Operators for filtering on a Int or Float field */
 export type NumberOperators = {
 export type NumberOperators = {
     eq?: Maybe<Scalars['Float']>;
     eq?: Maybe<Scalars['Float']>;
@@ -2042,6 +2064,13 @@ export type PasswordResetTokenInvalidError = ErrorResult & {
     message: Scalars['String'];
     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 & {
 export type Payment = Node & {
     id: Scalars['ID'];
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     createdAt: Scalars['DateTime'];
@@ -2604,7 +2633,11 @@ export type Refund = Node & {
     metadata?: Maybe<Scalars['JSON']>;
     metadata?: Maybe<Scalars['JSON']>;
 };
 };
 
 
-export type RegisterCustomerAccountResult = Success | MissingPasswordError | NativeAuthStrategyError;
+export type RegisterCustomerAccountResult =
+    | Success
+    | MissingPasswordError
+    | PasswordValidationError
+    | NativeAuthStrategyError;
 
 
 export type RegisterCustomerInput = {
 export type RegisterCustomerInput = {
     emailAddress: Scalars['String'];
     emailAddress: Scalars['String'];
@@ -2643,6 +2676,7 @@ export type ResetPasswordResult =
     | CurrentUser
     | CurrentUser
     | PasswordResetTokenInvalidError
     | PasswordResetTokenInvalidError
     | PasswordResetTokenExpiredError
     | PasswordResetTokenExpiredError
+    | PasswordValidationError
     | NativeAuthStrategyError
     | NativeAuthStrategyError
     | NotVerifiedError;
     | NotVerifiedError;
 
 
@@ -2702,7 +2736,7 @@ export type SearchResult = {
     facetValueIds: Array<Scalars['ID']>;
     facetValueIds: Array<Scalars['ID']>;
     /** An array of ids of the Collections in which this result appears */
     /** An array of ids of the Collections in which this result appears */
     collectionIds: Array<Scalars['ID']>;
     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'];
     score: Scalars['Float'];
 };
 };
 
 
@@ -2811,6 +2845,11 @@ export type StringFieldOption = {
     label?: Maybe<Array<LocalizedString>>;
     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 */
 /** Operators for filtering on a String field */
 export type StringOperators = {
 export type StringOperators = {
     eq?: Maybe<Scalars['String']>;
     eq?: Maybe<Scalars['String']>;
@@ -2927,7 +2966,11 @@ export type UpdateCustomerInput = {
     customFields?: Maybe<Scalars['JSON']>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 };
 
 
-export type UpdateCustomerPasswordResult = Success | InvalidCredentialsError | NativeAuthStrategyError;
+export type UpdateCustomerPasswordResult =
+    | Success
+    | InvalidCredentialsError
+    | PasswordValidationError
+    | NativeAuthStrategyError;
 
 
 export type UpdateOrderInput = {
 export type UpdateOrderInput = {
     customFields?: Maybe<Scalars['JSON']>;
     customFields?: Maybe<Scalars['JSON']>;
@@ -2975,6 +3018,7 @@ export type VerifyCustomerAccountResult =
     | VerificationTokenInvalidError
     | VerificationTokenInvalidError
     | VerificationTokenExpiredError
     | VerificationTokenExpiredError
     | MissingPasswordError
     | MissingPasswordError
+    | PasswordValidationError
     | PasswordAlreadySetError
     | PasswordAlreadySetError
     | NativeAuthStrategyError;
     | NativeAuthStrategyError;
 
 
@@ -3090,6 +3134,7 @@ export type RegisterMutation = {
     registerCustomerAccount:
     registerCustomerAccount:
         | Pick<Success, 'success'>
         | Pick<Success, 'success'>
         | Pick<MissingPasswordError, 'errorCode' | 'message'>
         | Pick<MissingPasswordError, 'errorCode' | 'message'>
+        | Pick<PasswordValidationError, 'errorCode' | 'message' | 'validationErrorMessage'>
         | Pick<NativeAuthStrategyError, 'errorCode' | 'message'>;
         | Pick<NativeAuthStrategyError, 'errorCode' | 'message'>;
 };
 };
 
 
@@ -3108,6 +3153,7 @@ export type VerifyMutation = {
         | Pick<VerificationTokenInvalidError, 'errorCode' | 'message'>
         | Pick<VerificationTokenInvalidError, 'errorCode' | 'message'>
         | Pick<VerificationTokenExpiredError, 'errorCode' | 'message'>
         | Pick<VerificationTokenExpiredError, 'errorCode' | 'message'>
         | Pick<MissingPasswordError, 'errorCode' | 'message'>
         | Pick<MissingPasswordError, 'errorCode' | 'message'>
+        | Pick<PasswordValidationError, 'errorCode' | 'message' | 'validationErrorMessage'>
         | Pick<PasswordAlreadySetError, 'errorCode' | 'message'>
         | Pick<PasswordAlreadySetError, 'errorCode' | 'message'>
         | Pick<NativeAuthStrategyError, 'errorCode' | 'message'>;
         | Pick<NativeAuthStrategyError, 'errorCode' | 'message'>;
 };
 };
@@ -3142,6 +3188,7 @@ export type ResetPasswordMutation = {
         | CurrentUserShopFragment
         | CurrentUserShopFragment
         | Pick<PasswordResetTokenInvalidError, 'errorCode' | 'message'>
         | Pick<PasswordResetTokenInvalidError, 'errorCode' | 'message'>
         | Pick<PasswordResetTokenExpiredError, 'errorCode' | 'message'>
         | Pick<PasswordResetTokenExpiredError, 'errorCode' | 'message'>
+        | Pick<PasswordValidationError, 'errorCode' | 'message' | 'validationErrorMessage'>
         | Pick<NativeAuthStrategyError, 'errorCode' | 'message'>
         | Pick<NativeAuthStrategyError, 'errorCode' | 'message'>
         | Pick<NotVerifiedError, 'errorCode' | 'message'>;
         | Pick<NotVerifiedError, 'errorCode' | 'message'>;
 };
 };
@@ -3212,6 +3259,7 @@ export type UpdatePasswordMutation = {
     updateCustomerPassword:
     updateCustomerPassword:
         | Pick<Success, 'success'>
         | Pick<Success, 'success'>
         | Pick<InvalidCredentialsError, 'errorCode' | 'message'>
         | Pick<InvalidCredentialsError, 'errorCode' | 'message'>
+        | Pick<PasswordValidationError, 'errorCode' | 'message'>
         | Pick<NativeAuthStrategyError, 'errorCode' | 'message'>;
         | Pick<NativeAuthStrategyError, 'errorCode' | 'message'>;
 };
 };
 
 
@@ -3626,6 +3674,10 @@ export namespace Register {
         NonNullable<RegisterMutation['registerCustomerAccount']>,
         NonNullable<RegisterMutation['registerCustomerAccount']>,
         { __typename?: 'ErrorResult' }
         { __typename?: 'ErrorResult' }
     >;
     >;
+    export type PasswordValidationErrorInlineFragment = DiscriminateUnion<
+        NonNullable<RegisterMutation['registerCustomerAccount']>,
+        { __typename?: 'PasswordValidationError' }
+    >;
 }
 }
 
 
 export namespace CurrentUserShop {
 export namespace CurrentUserShop {
@@ -3641,6 +3693,10 @@ export namespace Verify {
         NonNullable<VerifyMutation['verifyCustomerAccount']>,
         NonNullable<VerifyMutation['verifyCustomerAccount']>,
         { __typename?: 'ErrorResult' }
         { __typename?: 'ErrorResult' }
     >;
     >;
+    export type PasswordValidationErrorInlineFragment = DiscriminateUnion<
+        NonNullable<VerifyMutation['verifyCustomerAccount']>,
+        { __typename?: 'PasswordValidationError' }
+    >;
 }
 }
 
 
 export namespace RefreshToken {
 export namespace RefreshToken {
@@ -3681,6 +3737,10 @@ export namespace ResetPassword {
         NonNullable<ResetPasswordMutation['resetPassword']>,
         NonNullable<ResetPasswordMutation['resetPassword']>,
         { __typename?: 'ErrorResult' }
         { __typename?: 'ErrorResult' }
     >;
     >;
+    export type PasswordValidationErrorInlineFragment = DiscriminateUnion<
+        NonNullable<ResetPasswordMutation['resetPassword']>,
+        { __typename?: 'PasswordValidationError' }
+    >;
 }
 }
 
 
 export namespace RequestUpdateEmailAddress {
 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}
     ${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
                 errorCode
                 message
                 message
             }
             }
+            ... on PasswordValidationError {
+                validationErrorMessage
+            }
         }
         }
     }
     }
 `;
 `;
@@ -178,6 +181,9 @@ export const VERIFY_EMAIL = gql`
                 errorCode
                 errorCode
                 message
                 message
             }
             }
+            ... on PasswordValidationError {
+                validationErrorMessage
+            }
         }
         }
     }
     }
     ${CURRENT_USER_FRAGMENT}
     ${CURRENT_USER_FRAGMENT}
@@ -217,6 +223,9 @@ export const RESET_PASSWORD = gql`
                 errorCode
                 errorCode
                 message
                 message
             }
             }
+            ... on PasswordValidationError {
+                validationErrorMessage
+            }
         }
         }
     }
     }
     ${CURRENT_USER_FRAGMENT}
     ${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 { initialData } from '../../../e2e-common/e2e-initial-data';
 import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
 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 {
 import {
     AddItemToOrder,
     AddItemToOrder,
+    AddItemToOrderMutation,
+    GetActiveOrderPaymentsQuery,
+    GetNextOrderStatesQuery,
     TestOrderFragmentFragment,
     TestOrderFragmentFragment,
     UpdatedOrderFragment,
     UpdatedOrderFragment,
 } from './graphql/generated-e2e-shop-types';
 } from './graphql/generated-e2e-shop-types';
 import { ATTEMPT_LOGIN, GET_CUSTOMER_LIST } from './graphql/shared-definitions';
 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';
 import { sortById } from './utils/test-order-utils';
 
 
 /**
 /**
@@ -225,6 +233,27 @@ describe('Order merging', () => {
             })),
             })),
         ).toEqual([{ productVariantId: 'T_8', quantity: 1 }]);
         ).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`
 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 */
 /* tslint:disable:no-non-null-assertion */
 import { omit } from '@vendure/common/lib/omit';
 import { omit } from '@vendure/common/lib/omit';
 import { pick } from '@vendure/common/lib/pick';
 import { pick } from '@vendure/common/lib/pick';
+import { summate } from '@vendure/common/lib/shared-utils';
 import {
 import {
     defaultShippingCalculator,
     defaultShippingCalculator,
     defaultShippingEligibilityChecker,
     defaultShippingEligibilityChecker,
+    freeShipping,
+    manualFulfillmentHandler,
     mergeConfig,
     mergeConfig,
+    orderFixedDiscount,
+    orderPercentageDiscount,
     productsPercentageDiscount,
     productsPercentageDiscount,
     ShippingCalculator,
     ShippingCalculator,
 } from '@vendure/core';
 } from '@vendure/core';
@@ -14,9 +19,6 @@ import path from 'path';
 
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
 import { initialData } from '../../../e2e-common/e2e-initial-data';
 import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
 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 {
 import {
     failsToSettlePaymentMethod,
     failsToSettlePaymentMethod,
@@ -28,16 +30,22 @@ import {
     AdminTransition,
     AdminTransition,
     CreateFulfillment,
     CreateFulfillment,
     CreatePromotion,
     CreatePromotion,
+    CreatePromotionMutation,
+    CreatePromotionMutationVariables,
     CreateShippingMethod,
     CreateShippingMethod,
     ErrorCode,
     ErrorCode,
     GetOrder,
     GetOrder,
     GetOrderHistory,
     GetOrderHistory,
     GetOrderWithModifications,
     GetOrderWithModifications,
+    GetOrderWithModificationsQuery,
+    GetOrderWithModificationsQueryVariables,
     GetStockMovement,
     GetStockMovement,
     GlobalFlag,
     GlobalFlag,
     HistoryEntryType,
     HistoryEntryType,
     LanguageCode,
     LanguageCode,
     ModifyOrder,
     ModifyOrder,
+    ModifyOrderMutation,
+    ModifyOrderMutationVariables,
     OrderFragment,
     OrderFragment,
     OrderWithLinesFragment,
     OrderWithLinesFragment,
     OrderWithModificationsFragment,
     OrderWithModificationsFragment,
@@ -60,7 +68,6 @@ import {
     CREATE_SHIPPING_METHOD,
     CREATE_SHIPPING_METHOD,
     GET_ORDER,
     GET_ORDER,
     GET_ORDER_HISTORY,
     GET_ORDER_HISTORY,
-    GET_PRODUCT_WITH_VARIANTS,
     GET_STOCK_MOVEMENT,
     GET_STOCK_MOVEMENT,
     UPDATE_CHANNEL,
     UPDATE_CHANNEL,
     UPDATE_PRODUCT_VARIANTS,
     UPDATE_PRODUCT_VARIANTS,
@@ -1455,6 +1462,7 @@ describe('Order modification', () => {
                 originalTotalWithTax - order.lines[0].proratedUnitPriceWithTax,
                 originalTotalWithTax - order.lines[0].proratedUnitPriceWithTax,
             );
             );
             expect(modifyOrder.payments![0].refunds![0].total).toBe(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) {
     async function adminTransitionOrderToState(id: string, state: string) {
         const result = await adminClient.query<AdminTransition.Mutation, AdminTransition.Variables>(
         const result = await adminClient.query<AdminTransition.Mutation, AdminTransition.Variables>(
             ADMIN_TRANSITION_TO_STATE,
             ADMIN_TRANSITION_TO_STATE,
@@ -1798,12 +2027,20 @@ describe('Order modification', () => {
         await adminTransitionOrderToState(order.id, 'Modifying');
         await adminTransitionOrderToState(order.id, 'Modifying');
         return order;
         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`
 export const ORDER_WITH_MODIFICATION_FRAGMENT = gql`
     fragment OrderWithModifications on Order {
     fragment OrderWithModifications on Order {
         id
         id
         state
         state
+        subTotal
+        subTotalWithTax
+        shipping
+        shippingWithTax
         total
         total
         totalWithTax
         totalWithTax
         lines {
         lines {
@@ -1812,6 +2049,11 @@ export const ORDER_WITH_MODIFICATION_FRAGMENT = gql`
             linePrice
             linePrice
             linePriceWithTax
             linePriceWithTax
             discountedLinePriceWithTax
             discountedLinePriceWithTax
+            proratedLinePriceWithTax
+            discounts {
+                description
+                amountWithTax
+            }
             productVariant {
             productVariant {
                 id
                 id
                 name
                 name
@@ -1870,6 +2112,17 @@ export const ORDER_WITH_MODIFICATION_FRAGMENT = gql`
                 paymentId
                 paymentId
             }
             }
         }
         }
+        promotions {
+            id
+            name
+            couponCode
+        }
+        discounts {
+            description
+            adjustmentSource
+            amount
+            amountWithTax
+        }
         shippingAddress {
         shippingAddress {
             streetLine1
             streetLine1
             city
             city

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

@@ -18,6 +18,8 @@ import {
     SetShippingMethod,
     SetShippingMethod,
     TestOrderFragmentFragment,
     TestOrderFragmentFragment,
     TransitionToState,
     TransitionToState,
+    TransitionToStateMutation,
+    TransitionToStateMutationVariables,
 } from './graphql/generated-e2e-shop-types';
 } from './graphql/generated-e2e-shop-types';
 import { ADMIN_TRANSITION_TO_STATE, GET_ORDER } from './graphql/shared-definitions';
 import { ADMIN_TRANSITION_TO_STATE, GET_ORDER } from './graphql/shared-definitions';
 import {
 import {
@@ -40,7 +42,7 @@ const transitionErrorSpy = jest.fn();
 
 
 describe('Order process', () => {
 describe('Order process', () => {
     const VALIDATION_ERROR_MESSAGE = 'Customer must have a company email address';
     const VALIDATION_ERROR_MESSAGE = 'Customer must have a company email address';
-    const customOrderProcess: CustomOrderProcess<'ValidatingCustomer'> = {
+    const customOrderProcess: CustomOrderProcess<'ValidatingCustomer' | 'PaymentProcessing'> = {
         init(injector) {
         init(injector) {
             initSpy(injector.get(TransactionalConnection).rawConnection.name);
             initSpy(injector.get(TransactionalConnection).rawConnection.name);
         },
         },
@@ -52,6 +54,12 @@ describe('Order process', () => {
             ValidatingCustomer: {
             ValidatingCustomer: {
                 to: ['ArrangingPayment', 'AddingItems'],
                 to: ['ArrangingPayment', 'AddingItems'],
             },
             },
+            ArrangingPayment: {
+                to: ['PaymentProcessing'],
+            },
+            PaymentProcessing: {
+                to: ['PaymentAuthorized', 'PaymentSettled'],
+            },
         },
         },
         onTransitionStart(fromState, toState, data) {
         onTransitionStart(fromState, toState, data) {
             transitionStartSpy(fromState, toState, data);
             transitionStartSpy(fromState, toState, data);
@@ -261,6 +269,47 @@ describe('Order process', () => {
                 'AddingItems',
                 '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', () => {
     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);
             await assertNoStockMovementsCreated(testOrder.product.id);
         });
         });
 
 
-        it('cancel from PaymentAuthorized state', async () => {
+        it('cancel from PaymentAuthorized state with cancelShipping: true', async () => {
             const testOrder = await createTestOrder(
             const testOrder = await createTestOrder(
                 adminClient,
                 adminClient,
                 shopClient,
                 shopClient,
@@ -1087,6 +1087,7 @@ describe('Orders resolver', () => {
                 {
                 {
                     input: {
                     input: {
                         orderId: testOrder.orderId,
                         orderId: testOrder.orderId,
+                        cancelShipping: true,
                     },
                     },
                 },
                 },
             );
             );
@@ -1107,6 +1108,8 @@ describe('Orders resolver', () => {
             });
             });
             expect(order2!.active).toBe(false);
             expect(order2!.active).toBe(false);
             expect(order2!.state).toBe('Cancelled');
             expect(order2!.state).toBe('Cancelled');
+            expect(order2!.totalWithTax).toBe(0);
+            expect(order2!.shippingWithTax).toBe(0);
 
 
             const result2 = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
             const result2 = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
                 GET_STOCK_MOVEMENT,
                 GET_STOCK_MOVEMENT,
@@ -1339,6 +1342,7 @@ describe('Orders resolver', () => {
                     orderId,
                     orderId,
                     lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
                     lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
                     reason: 'cancel reason 2',
                     reason: 'cancel reason 2',
+                    cancelShipping: true,
                 },
                 },
             });
             });
 
 
@@ -1346,6 +1350,8 @@ describe('Orders resolver', () => {
                 id: orderId,
                 id: orderId,
             });
             });
             expect(order2!.state).toBe('Cancelled');
             expect(order2!.state).toBe('Cancelled');
+            expect(order2!.shippingWithTax).toBe(0);
+            expect(order2!.totalWithTax).toBe(0);
 
 
             const result = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
             const result = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
                 GET_STOCK_MOVEMENT,
                 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 () => {
         it('order history contains expected entries', async () => {
             const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(
             const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(
                 GET_ORDER_HISTORY,
                 GET_ORDER_HISTORY,
@@ -1412,6 +1434,7 @@ describe('Orders resolver', () => {
                     data: {
                     data: {
                         orderItemIds: ['T_13'],
                         orderItemIds: ['T_13'],
                         reason: 'cancel reason 1',
                         reason: 'cancel reason 1',
+                        shippingCancelled: false,
                     },
                     },
                 },
                 },
                 {
                 {
@@ -1419,6 +1442,7 @@ describe('Orders resolver', () => {
                     data: {
                     data: {
                         orderItemIds: ['T_14'],
                         orderItemIds: ['T_14'],
                         reason: 'cancel reason 2',
                         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,
     IdentifierChangeRequestEvent,
     mergeConfig,
     mergeConfig,
     PasswordResetEvent,
     PasswordResetEvent,
+    PasswordValidationStrategy,
+    RequestContext,
     VendurePlugin,
     VendurePlugin,
 } from '@vendure/core';
 } from '@vendure/core';
 import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
@@ -19,6 +21,7 @@ import path from 'path';
 
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
 import { initialData } from '../../../e2e-common/e2e-initial-data';
 import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
 import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
+import { PasswordValidationError } from '../src/common/error/generated-graphql-shop-errors';
 
 
 import {
 import {
     CreateAdministrator,
     CreateAdministrator,
@@ -94,10 +97,29 @@ const currentUserErrorGuard: ErrorResultGuard<CurrentUserShopFragment> = createE
     input => input.identifier != null,
     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', () => {
 describe('Shop auth & accounts', () => {
     const { server, adminClient, shopClient } = createTestEnvironment(
     const { server, adminClient, shopClient } = createTestEnvironment(
         mergeConfig(testConfig(), {
         mergeConfig(testConfig(), {
             plugins: [TestEmailPlugin as any],
             plugins: [TestEmailPlugin as any],
+            authOptions: {
+                passwordValidationStrategy: new TestPasswordValidationStrategy(),
+            },
         }),
         }),
     );
     );
 
 
@@ -276,6 +298,23 @@ describe('Shop auth & accounts', () => {
             expect(verifyCustomerAccount.errorCode).toBe(ErrorCode.MISSING_PASSWORD_ERROR);
             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 () => {
         it('verification succeeds with password and correct token', async () => {
             const { verifyCustomerAccount } = await shopClient.query<Verify.Mutation, Verify.Variables>(
             const { verifyCustomerAccount } = await shopClient.query<Verify.Mutation, Verify.Variables>(
                 VERIFY_EMAIL,
                 VERIFY_EMAIL,
@@ -361,6 +400,28 @@ describe('Shop auth & accounts', () => {
         const emailAddress = 'test2@test.com';
         const emailAddress = 'test2@test.com';
         let verificationToken: string;
         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 () => {
         it('register a new account with password', async () => {
             const verificationTokenPromise = getVerificationTokenPromise();
             const verificationTokenPromise = getVerificationTokenPromise();
             const input: RegisterCustomerInput = {
             const input: RegisterCustomerInput = {
@@ -497,6 +558,23 @@ describe('Shop auth & accounts', () => {
             expect(resetPassword.errorCode).toBe(ErrorCode.PASSWORD_RESET_TOKEN_INVALID_ERROR);
             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 () => {
         it('resetPassword works with valid token', async () => {
             const { resetPassword } = await shopClient.query<ResetPassword.Mutation, ResetPassword.Variables>(
             const { resetPassword } = await shopClient.query<ResetPassword.Mutation, ResetPassword.Variables>(
                 RESET_PASSWORD,
                 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 { ServiceModule } from '../service/service.module';
 
 
 import { AdminApiModule, ApiSharedModule, ShopApiModule } from './api-internal-modules';
 import { AdminApiModule, ApiSharedModule, ShopApiModule } from './api-internal-modules';
-import { RequestContextService } from './common/request-context.service';
 import { configureGraphQLModule } from './config/configure-graphql-module';
 import { configureGraphQLModule } from './config/configure-graphql-module';
 import { AuthGuard } from './middleware/auth-guard';
 import { AuthGuard } from './middleware/auth-guard';
 import { ExceptionLoggerFilter } from './middleware/exception-logger.filter';
 import { ExceptionLoggerFilter } from './middleware/exception-logger.filter';
@@ -52,7 +51,6 @@ import { ValidateCustomFieldsInterceptor } from './middleware/validate-custom-fi
         })),
         })),
     ],
     ],
     providers: [
     providers: [
-        RequestContextService,
         {
         {
             provide: APP_GUARD,
             provide: APP_GUARD,
             useClass: AuthGuard,
             useClass: AuthGuard,

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

@@ -79,7 +79,9 @@ export class RequestContext {
      * @description
      * @description
      * Creates an "empty" RequestContext object. This is only intended to be used
      * Creates an "empty" RequestContext object. This is only intended to be used
      * when a service method must be called outside the normal request-response
      * 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 {
     static empty(): RequestContext {
         return new 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,
             ...configService.apiOptions.apolloServerPlugins,
         ],
         ],
         validationRules: options.validationRules,
         validationRules: options.validationRules,
+        introspection: configService.apiOptions.introspection ?? true,
     } as GqlModuleOptions;
     } as GqlModuleOptions;
 
 
     /**
     /**

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

@@ -63,7 +63,7 @@ export function addGraphQLCustomFields(
             if (customEntityFields.length) {
             if (customEntityFields.length) {
                 customFieldTypeDefs += `
                 customFieldTypeDefs += `
                     type ${entityName}CustomFields {
                     type ${entityName}CustomFields {
-                        ${mapToFields(customEntityFields, getGraphQlType)}
+                        ${mapToFields(customEntityFields, wrapListType(getGraphQlType))}
                     }
                     }
 
 
                     extend type ${entityName} {
                     extend type ${entityName} {
@@ -82,7 +82,7 @@ export function addGraphQLCustomFields(
         if (localeStringFields.length && schema.getType(`${entityName}Translation`)) {
         if (localeStringFields.length && schema.getType(`${entityName}Translation`)) {
             customFieldTypeDefs += `
             customFieldTypeDefs += `
                     type ${entityName}TranslationCustomFields {
                     type ${entityName}TranslationCustomFields {
-                         ${mapToFields(localeStringFields, getGraphQlType)}
+                         ${mapToFields(localeStringFields, wrapListType(getGraphQlType))}
                     }
                     }
 
 
                     extend type ${entityName}Translation {
                     extend type ${entityName}Translation {
@@ -97,7 +97,7 @@ export function addGraphQLCustomFields(
                     input Create${entityName}CustomFieldsInput {
                     input Create${entityName}CustomFieldsInput {
                        ${mapToFields(
                        ${mapToFields(
                            writeableNonLocaleStringFields,
                            writeableNonLocaleStringFields,
-                           getGraphQlInputType,
+                           wrapListType(getGraphQlInputType),
                            getGraphQlInputName,
                            getGraphQlInputName,
                        )}
                        )}
                     }
                     }
@@ -121,7 +121,7 @@ export function addGraphQLCustomFields(
                     input Update${entityName}CustomFieldsInput {
                     input Update${entityName}CustomFieldsInput {
                        ${mapToFields(
                        ${mapToFields(
                            writeableNonLocaleStringFields,
                            writeableNonLocaleStringFields,
-                           getGraphQlInputType,
+                           wrapListType(getGraphQlInputType),
                            getGraphQlInputName,
                            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 += `
             customFieldTypeDefs += `
                     extend input ${entityName}SortParameter {
                     extend input ${entityName}SortParameter {
-                         ${mapToFields(customEntityFields, () => 'SortOrder')}
+                         ${mapToFields(customEntityNonListFields, () => 'SortOrder')}
                     }
                     }
                 `;
                 `;
         }
         }
@@ -166,7 +169,7 @@ export function addGraphQLCustomFields(
                     if (writeableLocaleStringFields.length) {
                     if (writeableLocaleStringFields.length) {
                         customFieldTypeDefs += `
                         customFieldTypeDefs += `
                             input ${inputName}CustomFields {
                             input ${inputName}CustomFields {
-                                ${mapToFields(writeableLocaleStringFields, getGraphQlType)}
+                                ${mapToFields(writeableLocaleStringFields, wrapListType(getGraphQlType))}
                             }
                             }
 
 
                             extend input ${inputName} {
                             extend input ${inputName} {
@@ -275,7 +278,7 @@ export function addRegisterCustomerCustomFieldsInput(
     }
     }
     const customFieldTypeDefs = `
     const customFieldTypeDefs = `
         input RegisterCustomerCustomFieldsInput {
         input RegisterCustomerCustomFieldsInput {
-            ${mapToFields(publicWritableCustomFields, getGraphQlInputType, getGraphQlInputName)}
+            ${mapToFields(publicWritableCustomFields, wrapListType(getGraphQlInputType), getGraphQlInputName)}
         }
         }
 
 
         extend input RegisterCustomerInput {
         extend input RegisterCustomerInput {
@@ -444,13 +447,12 @@ function mapToFields(
 ): string {
 ): string {
     const res = fieldDefs
     const res = fieldDefs
         .map(field => {
         .map(field => {
-            const primitiveType = typeFn(field);
-            if (!primitiveType) {
+            const type = typeFn(field);
+            if (!type) {
                 return;
                 return;
             }
             }
-            const finalType = field.list ? `[${primitiveType}!]` : primitiveType;
             const name = nameFn ? nameFn(field) : field.name;
             const name = nameFn ? nameFn(field) : field.name;
-            return `${name}: ${finalType}`;
+            return `${name}: ${type}`;
         })
         })
         .filter(x => x != null);
         .filter(x => x != null);
     return res.join('\n');
     return res.join('\n');
@@ -459,16 +461,16 @@ function mapToFields(
 function getFilterOperator(config: CustomFieldConfig): string | undefined {
 function getFilterOperator(config: CustomFieldConfig): string | undefined {
     switch (config.type) {
     switch (config.type) {
         case 'datetime':
         case 'datetime':
-            return 'DateOperators';
+            return config.list ? 'DateListOperators' : 'DateOperators';
         case 'string':
         case 'string':
         case 'localeString':
         case 'localeString':
         case 'text':
         case 'text':
-            return 'StringOperators';
+            return config.list ? 'StringListOperators' : 'StringOperators';
         case 'boolean':
         case 'boolean':
-            return 'BooleanOperators';
+            return config.list ? 'BooleanListOperators' : 'BooleanOperators';
         case 'int':
         case 'int':
         case 'float':
         case 'float':
-            return 'NumberOperators';
+            return config.list ? 'NumberListOperators' : 'NumberOperators';
         case 'relation':
         case 'relation':
             return undefined;
             return undefined;
         default:
         default:
@@ -481,6 +483,18 @@ function getGraphQlInputType(config: CustomFieldConfig): string {
     return config.type === 'relation' ? `ID` : getGraphQlType(config);
     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 {
 function getGraphQlType(config: CustomFieldConfig): string {
     switch (config.type) {
     switch (config.type) {
         case 'string':
         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 { LogLevel } from '../../config/logger/vendure-logger';
 import { CachedSession } from '../../config/session-cache/session-cache-strategy';
 import { CachedSession } from '../../config/session-cache/session-cache-strategy';
 import { Customer } from '../../entity/customer/customer.entity';
 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 { ChannelService } from '../../service/services/channel.service';
 import { CustomerService } from '../../service/services/customer.service';
 import { CustomerService } from '../../service/services/customer.service';
 import { SessionService } from '../../service/services/session.service';
 import { SessionService } from '../../service/services/session.service';
 import { extractSessionToken } from '../common/extract-session-token';
 import { extractSessionToken } from '../common/extract-session-token';
 import { parseContext } from '../common/parse-context';
 import { parseContext } from '../common/parse-context';
 import { RequestContext } from '../common/request-context';
 import { RequestContext } from '../common/request-context';
-import { RequestContextService } from '../common/request-context.service';
 import { setSessionToken } from '../common/set-session-token';
 import { setSessionToken } from '../common/set-session-token';
 import { PERMISSIONS_METADATA_KEY } from '../decorators/allow.decorator';
 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
     emailAddress: String
 }
 }
 
 
+input CustomerFilterParameter {
+    postalCode: StringOperators
+}
+
 # generated by generateListOptions function
 # generated by generateListOptions function
 input CustomerListOptions
 input CustomerListOptions
 
 

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

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

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

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

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

@@ -46,3 +46,25 @@ type InsufficientStockError implements ErrorResult {
     quantityAvailable: Int!
     quantityAvailable: Int!
     order: Order!
     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
     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
 Used to construct boolean expressions for filtering search results
 by FacetValue ID. Examples:
 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!
     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."
 "Returned when attempting to set the Customer for an Order when already logged in."
 type AlreadyLoggedInError implements ErrorResult {
 type AlreadyLoggedInError implements ErrorResult {
     errorCode: ErrorCode!
     errorCode: ErrorCode!
@@ -78,6 +49,13 @@ type MissingPasswordError implements ErrorResult {
     message: String!
     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."
 "Returned when attempting to verify a customer account with a password, when a password has already been set."
 type PasswordAlreadySetError implements ErrorResult {
 type PasswordAlreadySetError implements ErrorResult {
     errorCode: ErrorCode!
     errorCode: ErrorCode!

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

@@ -213,16 +213,17 @@ union AddPaymentToOrderResult =
     | NoActiveOrderError
     | NoActiveOrderError
 union TransitionOrderToStateResult = Order | OrderStateTransitionError
 union TransitionOrderToStateResult = Order | OrderStateTransitionError
 union SetCustomerForOrderResult = Order | AlreadyLoggedInError | EmailAddressConflictError | NoActiveOrderError
 union SetCustomerForOrderResult = Order | AlreadyLoggedInError | EmailAddressConflictError | NoActiveOrderError
-union RegisterCustomerAccountResult = Success | MissingPasswordError | NativeAuthStrategyError
+union RegisterCustomerAccountResult = Success | MissingPasswordError | PasswordValidationError | NativeAuthStrategyError
 union RefreshCustomerVerificationResult = Success | NativeAuthStrategyError
 union RefreshCustomerVerificationResult = Success | NativeAuthStrategyError
 union VerifyCustomerAccountResult =
 union VerifyCustomerAccountResult =
       CurrentUser
       CurrentUser
     | VerificationTokenInvalidError
     | VerificationTokenInvalidError
     | VerificationTokenExpiredError
     | VerificationTokenExpiredError
     | MissingPasswordError
     | MissingPasswordError
+    | PasswordValidationError
     | PasswordAlreadySetError
     | PasswordAlreadySetError
     | NativeAuthStrategyError
     | NativeAuthStrategyError
-union UpdateCustomerPasswordResult = Success | InvalidCredentialsError | NativeAuthStrategyError
+union UpdateCustomerPasswordResult = Success | InvalidCredentialsError | PasswordValidationError | NativeAuthStrategyError
 union RequestUpdateCustomerEmailAddressResult =
 union RequestUpdateCustomerEmailAddressResult =
       Success
       Success
     | InvalidCredentialsError
     | InvalidCredentialsError
@@ -238,6 +239,7 @@ union ResetPasswordResult =
       CurrentUser
       CurrentUser
     | PasswordResetTokenInvalidError
     | PasswordResetTokenInvalidError
     | PasswordResetTokenExpiredError
     | PasswordResetTokenExpiredError
+    | PasswordValidationError
     | NativeAuthStrategyError
     | NativeAuthStrategyError
     | NotVerifiedError
     | NotVerifiedError
 union NativeAuthenticationResult =
 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 fs from 'fs-extra';
 import path from 'path';
 import path from 'path';
 
 
-import { logColored } from './cli-utils';
+const loggerCtx = 'Populate';
 
 
 // tslint:disable:no-console
 // tslint:disable:no-console
 /**
 /**
  * @description
  * @description
  * Populates the Vendure server with some initial data and (optionally) product data from
  * 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
  * @docsCategory import-export
  */
  */
@@ -16,70 +51,86 @@ export async function populate<T extends INestApplicationContext>(
     bootstrapFn: () => Promise<T | undefined>,
     bootstrapFn: () => Promise<T | undefined>,
     initialDataPathOrObject: string | object,
     initialDataPathOrObject: string | object,
     productsCsvPath?: string,
     productsCsvPath?: string,
+    channelOrToken?: string | import('@vendure/core').Channel,
 ): Promise<T> {
 ): Promise<T> {
     const app = await bootstrapFn();
     const app = await bootstrapFn();
     if (!app) {
     if (!app) {
         throw new Error('Could not bootstrap the Vendure 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 =
     const initialData: import('@vendure/core').InitialData =
         typeof initialDataPathOrObject === 'string'
         typeof initialDataPathOrObject === 'string'
             ? require(initialDataPathOrObject)
             ? require(initialDataPathOrObject)
             : initialDataPathOrObject;
             : initialDataPathOrObject;
 
 
-    await populateInitialData(app, initialData, logColored);
+    await populateInitialData(app, initialData, channel);
 
 
     if (productsCsvPath) {
     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) {
         if (importResult.errors && importResult.errors.length) {
             const errorFile = path.join(process.cwd(), 'vendure-import-error.log');
             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}`,
                 `${importResult.errors.length} errors encountered when importing product data. See: ${errorFile}`,
+                loggerCtx,
             );
             );
             await fs.writeFile(errorFile, importResult.errors.join('\n'));
             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;
     return app;
 }
 }
 
 
 export async function populateInitialData(
 export async function populateInitialData(
     app: INestApplicationContext,
     app: INestApplicationContext,
     initialData: import('@vendure/core').InitialData,
     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);
     const populator = app.get(Populator);
     try {
     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) {
     } catch (err) {
-        console.log(err.message);
+        Logger.error(err.message, loggerCtx);
     }
     }
 }
 }
 
 
 export async function populateCollections(
 export async function populateCollections(
     app: INestApplicationContext,
     app: INestApplicationContext,
     initialData: import('@vendure/core').InitialData,
     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);
     const populator = app.get(Populator);
     try {
     try {
         if (initialData.collections.length) {
         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) {
     } catch (err) {
-        console.log(err.message);
+        Logger.info(err.message, loggerCtx);
     }
     }
 }
 }
 
 
@@ -87,10 +138,16 @@ export async function importProductsFromCsv(
     app: INestApplicationContext,
     app: INestApplicationContext,
     productsCsvPath: string,
     productsCsvPath: string,
     languageCode: import('@vendure/core').LanguageCode,
     languageCode: import('@vendure/core').LanguageCode,
+    channel?: import('@vendure/core').Channel,
 ): Promise<import('@vendure/core').ImportProgress> {
 ): Promise<import('@vendure/core').ImportProgress> {
-    const { Importer } = await import('@vendure/core');
+    const { Importer, RequestContextService } = await import('@vendure/core');
     const importer = app.get(Importer);
     const importer = app.get(Importer);
+    const requestContextService = app.get(RequestContextService);
     const productData = await fs.readFile(productsCsvPath, 'utf-8');
     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 {
 export class CreateFulfillmentError extends ErrorResult {
   readonly __typename = 'CreateFulfillmentError';
   readonly __typename = 'CreateFulfillmentError';
   readonly errorCode = 'CREATE_FULFILLMENT_ERROR' as any;
   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 {
 function isGraphQLError(input: any): input is import('@vendure/common/lib/generated-types').ErrorResult {
   return input instanceof ErrorResult || errorTypeNames.has(input.__typename);
   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 {
 export class PaymentDeclinedError extends ErrorResult {
   readonly __typename = 'PaymentDeclinedError';
   readonly __typename = 'PaymentDeclinedError';
   readonly errorCode = 'PAYMENT_DECLINED_ERROR' as any;
   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 {
 function isGraphQLError(input: any): input is import('@vendure/common/lib/generated-types').ErrorResult {
   return input instanceof ErrorResult || errorTypeNames.has(input.__typename);
   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.
      * Returns an array of state to which the machine may transition from the current state.
      */
      */
     getNextStates(): ReadonlyArray<T> {
     getNextStates(): ReadonlyArray<T> {
-        return this.config.transitions[this._currentState].to;
+        return this.config.transitions[this._currentState]?.to ?? [];
     }
     }
 
 
     /**
     /**

Неке датотеке нису приказане због велике количине промена