Browse Source

Merge branch 'next'

Michael Bromley 5 years ago
parent
commit
9e7f51c869
100 changed files with 7392 additions and 7604 deletions
  1. 1 9
      .github/workflows/build_and_test.yml
  2. 2 0
      .prettierignore
  3. 5 0
      docs/README.md
  4. 128 0
      docs/content/docs/developer-guide/error-handling.md
  5. 10 1
      docs/content/docs/developer-guide/payment-integrations/index.md
  6. 24 2
      docs/content/docs/developer-guide/shipping.md
  7. 5 9
      docs/content/docs/plugins/plugin-examples.md
  8. BIN
      docs/content/docs/storefront/order-workflow/order_state_diagram.png
  9. 30 18
      docs/diagrams/order-state-diagram.puml
  10. 23 23
      package.json
  11. 6 6
      packages/admin-ui-plugin/package.json
  12. 2 1
      packages/admin-ui-plugin/src/constants.ts
  13. 1 1
      packages/admin-ui-plugin/src/plugin.ts
  14. 22 22
      packages/admin-ui/i18n-coverage.json
  15. 50 54
      packages/admin-ui/package.json
  16. 0 0
      packages/admin-ui/src/.browserslistrc
  17. 28 16
      packages/admin-ui/src/lib/catalog/src/components/asset-list/asset-list.component.ts
  18. 9 4
      packages/admin-ui/src/lib/catalog/src/components/product-assets/product-assets.component.ts
  19. 4 3
      packages/admin-ui/src/lib/core/src/common/base-list.component.ts
  20. 2853 2662
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  21. 147 276
      packages/admin-ui/src/lib/core/src/common/introspection-result.ts
  22. 1 1
      packages/admin-ui/src/lib/core/src/data/check-jobs-link.ts
  23. 8 8
      packages/admin-ui/src/lib/core/src/data/client-state/client-resolvers.ts
  24. 10 11
      packages/admin-ui/src/lib/core/src/data/data.module.ts
  25. 1 1
      packages/admin-ui/src/lib/core/src/data/definitions/administrator-definitions.ts
  26. 9 5
      packages/admin-ui/src/lib/core/src/data/definitions/auth-definitions.ts
  27. 16 1
      packages/admin-ui/src/lib/core/src/data/definitions/client-definitions.ts
  28. 1 1
      packages/admin-ui/src/lib/core/src/data/definitions/collection-definitions.ts
  29. 7 1
      packages/admin-ui/src/lib/core/src/data/definitions/customer-definitions.ts
  30. 1 1
      packages/admin-ui/src/lib/core/src/data/definitions/facet-definitions.ts
  31. 52 8
      packages/admin-ui/src/lib/core/src/data/definitions/order-definitions.ts
  32. 16 7
      packages/admin-ui/src/lib/core/src/data/definitions/product-definitions.ts
  33. 8 2
      packages/admin-ui/src/lib/core/src/data/definitions/promotion-definitions.ts
  34. 8 2
      packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts
  35. 8 1
      packages/admin-ui/src/lib/core/src/data/definitions/shared-definitions.ts
  36. 2 1
      packages/admin-ui/src/lib/core/src/data/definitions/shipping-definitions.ts
  37. 1 1
      packages/admin-ui/src/lib/core/src/data/omit-typename-link.ts
  38. 3 14
      packages/admin-ui/src/lib/core/src/data/providers/base-data.service.ts
  39. 3 3
      packages/admin-ui/src/lib/core/src/data/providers/data.service.ts
  40. 13 1
      packages/admin-ui/src/lib/core/src/data/providers/order-data.service.ts
  41. 3 3
      packages/admin-ui/src/lib/core/src/data/providers/settings-data.service.ts
  42. 1 1
      packages/admin-ui/src/lib/core/src/data/query-result.ts
  43. 1 1
      packages/admin-ui/src/lib/core/src/data/server-config.ts
  44. 20 8
      packages/admin-ui/src/lib/core/src/providers/auth/auth.service.ts
  45. 2 2
      packages/admin-ui/src/lib/core/src/shared/components/order-state-label/order-state-label.component.html
  46. 4 2
      packages/admin-ui/src/lib/core/src/shared/components/order-state-label/order-state-label.component.ts
  47. 0 1
      packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/menu/images.ts
  48. 0 1
      packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/menu/links.ts
  49. 0 1
      packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/menu/menu.ts
  50. 9 3
      packages/admin-ui/src/lib/core/src/shared/components/timeline-entry/timeline-entry.component.html
  51. 39 5
      packages/admin-ui/src/lib/core/src/shared/components/timeline-entry/timeline-entry.component.scss
  52. 18 1
      packages/admin-ui/src/lib/core/src/shared/components/timeline-entry/timeline-entry.component.ts
  53. 6 5
      packages/admin-ui/src/lib/core/src/shared/directives/if-directive-base.ts
  54. 4 2
      packages/admin-ui/src/lib/core/src/shared/pipes/order-state-i18n-token.pipe.ts
  55. 66 37
      packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.ts
  56. 7 0
      packages/admin-ui/src/lib/login/src/components/login/login.component.html
  57. 15 10
      packages/admin-ui/src/lib/login/src/components/login/login.component.ts
  58. 14 5
      packages/admin-ui/src/lib/marketing/src/components/promotion-detail/promotion-detail.component.ts
  59. 46 0
      packages/admin-ui/src/lib/order/src/components/fulfillment-card/fulfillment-card.component.html
  60. 12 0
      packages/admin-ui/src/lib/order/src/components/fulfillment-card/fulfillment-card.component.scss
  61. 39 0
      packages/admin-ui/src/lib/order/src/components/fulfillment-card/fulfillment-card.component.ts
  62. 0 1
      packages/admin-ui/src/lib/order/src/components/fulfillment-detail/fulfillment-detail.component.ts
  63. 4 0
      packages/admin-ui/src/lib/order/src/components/fulfillment-state-label/fulfillment-state-label.component.html
  64. 3 0
      packages/admin-ui/src/lib/order/src/components/fulfillment-state-label/fulfillment-state-label.component.scss
  65. 23 0
      packages/admin-ui/src/lib/order/src/components/fulfillment-state-label/fulfillment-state-label.component.ts
  66. 1 1
      packages/admin-ui/src/lib/order/src/components/line-fulfillment/line-fulfillment.component.html
  67. 6 10
      packages/admin-ui/src/lib/order/src/components/line-fulfillment/line-fulfillment.component.ts
  68. 15 19
      packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.html
  69. 0 5
      packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.scss
  70. 87 26
      packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.ts
  71. 41 12
      packages/admin-ui/src/lib/order/src/components/order-history/order-history.component.html
  72. 26 10
      packages/admin-ui/src/lib/order/src/components/order-history/order-history.component.ts
  73. 3 3
      packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.html
  74. 1 2
      packages/admin-ui/src/lib/order/src/components/order-payment-card/order-payment-card.component.ts
  75. 4 0
      packages/admin-ui/src/lib/order/src/order.module.ts
  76. 2 0
      packages/admin-ui/src/lib/order/src/public_api.ts
  77. 30 31
      packages/admin-ui/src/lib/settings/src/components/channel-detail/channel-detail.component.ts
  78. 18 16
      packages/admin-ui/src/lib/settings/src/components/global-settings/global-settings.component.ts
  79. 2 2
      packages/admin-ui/src/lib/settings/src/components/shipping-method-detail/shipping-method-detail.component.html
  80. 13 3
      packages/admin-ui/src/lib/static/i18n-messages/de.json
  81. 15 5
      packages/admin-ui/src/lib/static/i18n-messages/en.json
  82. 13 3
      packages/admin-ui/src/lib/static/i18n-messages/es.json
  83. 14 4
      packages/admin-ui/src/lib/static/i18n-messages/pl.json
  84. 14 4
      packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json
  85. 14 4
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json
  86. 14 4
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json
  87. 2 2
      packages/admin-ui/tsconfig.json
  88. 15 13
      packages/asset-server-plugin/e2e/asset-server-plugin.e2e-spec.ts
  89. 1623 1991
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  90. 11 11
      packages/asset-server-plugin/package.json
  91. 5 5
      packages/asset-server-plugin/src/plugin.ts
  92. 1 0
      packages/asset-server-plugin/src/s3-asset-storage-strategy.ts
  93. 2 2
      packages/common/package.json
  94. 469 413
      packages/common/src/generated-shop-types.ts
  95. 889 1616
      packages/common/src/generated-types.ts
  96. 10 13
      packages/common/src/omit.ts
  97. 59 42
      packages/core/e2e/asset.e2e-spec.ts
  98. 10 10
      packages/core/e2e/auth.e2e-spec.ts
  99. 44 22
      packages/core/e2e/authentication-strategy.e2e-spec.ts
  100. 65 34
      packages/core/e2e/channel.e2e-spec.ts

+ 1 - 9
.github/workflows/build_and_test.yml

@@ -1,14 +1,6 @@
 name: Build & Test
 name: Build & Test
 
 
-on:
-  push:
-    branches:
-    - master
-    - next
-  pull_request:
-    branches:
-    - master
-    - next
+on: push
 env:
 env:
   CI: true
   CI: true
   node: 12.x
   node: 12.x

+ 2 - 0
.prettierignore

@@ -1,3 +1,5 @@
 generated-types.ts
 generated-types.ts
 lazy-extensions.module.ts
 lazy-extensions.module.ts
 shared-extensions.module.ts
 shared-extensions.module.ts
+generated-graphql-shop-errors.ts
+generated-graphql-admin-errors.ts

+ 5 - 0
docs/README.md

@@ -49,6 +49,11 @@ This is required as its presence determines whether the declaration is extracted
 This optional tag can be used to group declarations together onto a single page. This is useful e.g. in the case of utility functions or
 This optional tag can be used to group declarations together onto a single page. This is useful e.g. in the case of utility functions or
 type aliases, which may be considered too trivial to get an entire page to themselves.
 type aliases, which may be considered too trivial to get an entire page to themselves.
 
 
+##### `@docsWeight`
+
+This optional tag can be used to define the order of definitions on a single page. By default, multiple definitions on a page are sorted alphabetically,
+but this sometimes leaves the "main" definition near the bottom. In this case, the `@docsWeight` tag can promote it to the top (0 is first).
+
 ##### `@description`
 ##### `@description`
 
 
 This tag specifies the text description of the declaration. It supports markdown, but should not be used for code blocks, which should be tagged with `@example` (see below). Links to other declarations can be made with the `{@link SomeOtherDeclaration}` syntax. Also applies to class/interface members.
 This tag specifies the text description of the declaration. It supports markdown, but should not be used for code blocks, which should be tagged with `@example` (see below). Links to other declarations can be made with the `{@link SomeOtherDeclaration}` syntax. Also applies to class/interface members.

+ 128 - 0
docs/content/docs/developer-guide/error-handling.md

@@ -0,0 +1,128 @@
+---
+title: "Error Handling"
+showtoc: true
+---
+
+# Error Handling
+
+Errors in Vendure can be divided into two categories:
+
+* Unexpected errors
+* Expected errors
+
+These two types have different meanings and are handled differently to one another.
+
+## Unexpected Errors
+
+This type of error occurs when something goes unexpectedly wrong during the processing of a request. Examples include internal server errors, database connectivity issues, lacking permissions for a resource, etc. In short, these are errors that are *not supposed to happen*.
+
+Internally, these situations are handled by throwing an Error:
+
+```TypeScript
+const customer = await this.findOneByUserId(ctx, user.id);
+// in this case, the customer *should always* be found, and if
+// not then something unknown has gone wrong...
+if (!customer) {
+    throw new InternalServerError('error.cannot-locate-customer-for-user');
+}
+```
+
+In the GraphQL APIs, these errors are returned in the standard `errors` array:
+
+```JSON
+{
+  "errors": [
+    {
+      "message": "You are not currently authorized to perform this action",
+      "locations": [
+        {
+          "line": 2,
+          "column": 2
+        }
+      ],
+      "path": [
+        "me"
+      ],
+      "extensions": {
+        "code": "FORBIDDEN"
+      }
+    }
+  ],
+  "data": {
+    "me": null
+  }
+}
+```
+So your client applications need a generic way of detecting and handling this kind of error. For example, many http client libraries support "response interceptors" which can be used to intercept all API responses and check the `errors` array. 
+
+## Expected errors (ErrorResults)
+
+This type of error represents a well-defined result of (typically) a GraphQL mutation which is not considered "successful". For example, when using the `applyCouponCode` mutation, the code may be invalid, or it may have expired. These are examples of "expected" errors and are named in Vendure "ErrorResults". These ErrorResults are encoded into the GraphQL schema itself.
+
+ErrorResults all implement the `ErrorResult` interface:
+
+```GraphQL
+interface ErrorResult {
+  errorCode: ErrorCode!
+  message: String!
+}
+```
+
+Some ErrorResults add other relevant fields to the type:
+
+```GraphQL
+"Returned if there is an error in transitioning the Order state"
+type OrderStateTransitionError implements ErrorResult {
+  errorCode: ErrorCode!
+  message: String!
+  transitionError: String!
+  fromState: String!
+  toState: String!
+}
+```
+
+Operations which may return ErrorResults use a GraphQL `union` as their return type:
+
+```GraphQL
+type Mutation {
+  "Applies the given coupon code to the active Order"
+  applyCouponCode(couponCode: String!): ApplyCouponCodeResult!
+}
+
+union ApplyCouponCodeResult = Order 
+  | CouponCodeExpiredError 
+  | CouponCodeInvalidError 
+  | CouponCodeLimitError
+``` 
+
+### Querying an ErrorResult union
+
+When performing an operation of a query or mutation which returns a union, you will need to use the [GraphQL conditional fragment](https://graphql.org/learn/schema/#union-types) to select the desired fields:
+
+```GraphQL
+mutation ApplyCoupon($code: String!) {
+  applyCouponCode(couponCode: $code) {
+    ...on Order {
+      id
+      couponCodes
+      total
+    }
+    # querying the ErrorResult fields
+    # "catches" all possible errors
+    ...on ErrorResult {
+      errorCode
+      message
+    }
+    # you can also specify particular fields
+    # if your client app needs that specific data
+    # as part of handling the error.
+    ...on CouponCodeLimitError {
+      limit
+    }
+  }
+}
+```
+
+This ensures that your client code is aware of and handles all the usual error cases.
+
+You can see all the ErrorResult types returned by the Shop API mutations in the [Shop API Mutations docs]({{< relref "/docs/graphql-api/shop/mutations" >}}). 

+ 10 - 1
docs/content/docs/developer-guide/payment-integrations/index.md

@@ -57,7 +57,16 @@ const myPaymentIntegration = new PaymentMethodHandler({
         amount: order.total,
         amount: order.total,
         state: 'Authorized' as const,
         state: 'Authorized' as const,
         transactionId: result.id.toString(),
         transactionId: result.id.toString(),
-        metadata: result.outcome,
+        metadata: {
+          cardInfo: result.cardInfo,
+          // Any metadata in the `public` field
+          // will be available in the Shop API,
+          // All other metadata is private and 
+          // only available in the Admin API.
+          public: {
+            referenceCode: result.publicId,
+          }
+        },
       };
       };
     } catch (err) {
     } catch (err) {
       return {
       return {

+ 24 - 2
docs/content/docs/developer-guide/shipping.md

@@ -1,8 +1,8 @@
 ---
 ---
-title: "Shipping"
+title: "Shipping & Fulfillment"
 showtoc: true
 showtoc: true
 ---
 ---
-# Shipping
+# Shipping & Fulfillment
 
 
 Shipping in Vendure is handled by [ShippingMethods]({{< relref "shipping-method" >}}). Multiple ShippingMethods can be set up and then your storefront can query [`eligibleShippingMethods`]({{< relref "/docs/graphql-api/shop/queries" >}}#eligibleshippingmethods) to find out which ones can be applied to the active order.
 Shipping in Vendure is handled by [ShippingMethods]({{< relref "shipping-method" >}}). Multiple ShippingMethods can be set up and then your storefront can query [`eligibleShippingMethods`]({{< relref "/docs/graphql-api/shop/queries" >}}#eligibleshippingmethods) to find out which ones can be applied to the active order.
 
 
@@ -124,3 +124,25 @@ export const config: VendureConfig = {
   }
   }
 }
 }
 ```
 ```
+
+## Fulfillments
+
+Fulfillments represent the actual shipping status of items in an order. Like Orders, Fulfillments are governed by a [finite state machine]({{< relref "fsm" >}}) and by default, a Fulfillment can be in one of the [following states]({{< relref "fulfillment-state" >}}):
+
+* `Pending` The Fulfillment has been created
+* `Shipped` The Fulfillment has been shipped
+* `Delivered` The Fulfillment has arrived with the customer
+* `Cancelled` The Fulfillment has been cancelled 
+
+These states cover the typical workflow for fulfilling orders. However, it is possible to customize the fulfillment workflow by defining a [CustomFulfillmentProcess]({{< relref "custom-fulfillment-process" >}}) and passing it to your VendureConfig:
+
+```TypeScript
+export const config: VendureConfig = {
+  // ...
+  shippingOptions: {
+    customFulfillmentProcess: [myCustomFulfillmentProcess],
+  },
+};
+```
+
+For a more detailed look at how custom processes are used, see the [Customizing The Order Process guide]({{< relref "customizing-the-order-process" >}}).

+ 5 - 9
docs/content/docs/plugins/plugin-examples.md

@@ -173,17 +173,15 @@ Also see the docs for [WorkerService]({{< relref "worker-service" >}}).
 
 
 ```TypeScript
 ```TypeScript
 // order-processing.controller.ts
 // order-processing.controller.ts
-import { asyncObservable, ID, Order } from '@vendure/core';
+import { asyncObservable, ID, Order, TransactionalConnection } from '@vendure/core';
 import { Controller } from '@nestjs/common';
 import { Controller } from '@nestjs/common';
 import { MessagePattern } from '@nestjs/microservices';
 import { MessagePattern } from '@nestjs/microservices';
-import { InjectConnection } from '@nestjs/typeorm';
-import { Connection } from 'typeorm';
 import { ProcessOrderMessage } from './process-order-message';
 import { ProcessOrderMessage } from './process-order-message';
 
 
 @Controller()
 @Controller()
 class OrderProcessingController {
 class OrderProcessingController {
 
 
-  constructor(@InjectConnection() private connection: Connection) {}
+  constructor(private connection: TransactionalConnection) {}
 
 
   @MessagePattern(ProcessOrderMessage.pattern)
   @MessagePattern(ProcessOrderMessage.pattern)
   async processOrder({ orderId }: ProcessOrderMessage['data']) {
   async processOrder({ orderId }: ProcessOrderMessage['data']) {
@@ -234,7 +232,7 @@ export class OrderAnalyticsPlugin implements OnVendureBootstrap {
    */
    */
   onVendureBootstrap() {
   onVendureBootstrap() {
     this.eventBus.ofType(OrderStateTransitionEvent).subscribe(event => {
     this.eventBus.ofType(OrderStateTransitionEvent).subscribe(event => {
-      if (event.toState === 'Fulfilled') {
+      if (event.toState === 'Delivered') {
         this.workerService.send(new ProcessOrderMessage({ orderId: event.order.id })).subscribe();
         this.workerService.send(new ProcessOrderMessage({ orderId: event.order.id })).subscribe();
       }
       }
     });
     });
@@ -272,9 +270,7 @@ The resolver just defines how to handle the new `addVideoToProduct` mutation, de
 ```TypeScript
 ```TypeScript
 // product-video.service.ts
 // product-video.service.ts
 import { Injectable, OnModuleInit } from '@nestjs/common';
 import { Injectable, OnModuleInit } from '@nestjs/common';
-import { InjectConnection } from '@nestjs/typeorm';
-import { Connection } from 'typeorm';
-import { JobQueue, JobQueueService, ID, Product } from '@vendure/core';
+import { JobQueue, JobQueueService, ID, Product, TransactionalConnection } from '@vendure/core';
 import { transcode } from 'third-party-video-sdk'; 
 import { transcode } from 'third-party-video-sdk'; 
 
 
 let jobQueue: JobQueue<{ productId: ID; videoUrl: string; }>;
 let jobQueue: JobQueue<{ productId: ID; videoUrl: string; }>;
@@ -283,7 +279,7 @@ let jobQueue: JobQueue<{ productId: ID; videoUrl: string; }>;
 class ProductVideoService implements OnModuleInit { 
 class ProductVideoService implements OnModuleInit { 
   
   
   constructor(private jobQueueService: JobQueueService, 
   constructor(private jobQueueService: JobQueueService, 
-              @InjectConnection() private connection: Connection) {}
+              private connection: TransactionalConnection) {}
 
 
   onModuleInit() {
   onModuleInit() {
     // This check ensures that only a single JobQueue is created, even if this
     // This check ensures that only a single JobQueue is created, even if this

BIN
docs/content/docs/storefront/order-workflow/order_state_diagram.png


+ 30 - 18
docs/diagrams/order-state-diagram.puml

@@ -16,7 +16,7 @@ state ShopAPI {
     }
     }
 }
 }
 
 
-state AdminAPI {
+state AdminAPI #f9c876 {
     state PaymentAuthorized {
     state PaymentAuthorized {
         PaymentAuthorized: The payment has been authorized by the
         PaymentAuthorized: The payment has been authorized by the
         PaymentAuthorized: payment provider.
         PaymentAuthorized: payment provider.
@@ -27,12 +27,20 @@ state AdminAPI {
         PaymentSettled: provider, i.e. the transaction is complete.
         PaymentSettled: provider, i.e. the transaction is complete.
     }
     }
 
 
-    state PartiallyFulfilled {
-        PartiallyFulfilled: One or more OrderItems have been dispatched to the Customer
+    state PartiallyShipped {
+        PartiallyShipped: Some, but not all, OrderItems have been shipped to the Customer
     }
     }
 
 
-    state Fulfilled #9d9 {
-        Fulfilled: All OrderItems have been dispatched to the Customer
+    state Shipped {
+        Shipped: All OrderItems have been shipped to the Customer
+    }
+
+    state PartiallyDelivered {
+        PartiallyDelivered: Some, but not all, OrderItems have arrived with the Customer
+    }
+
+    state Delivered #9d9 {
+        Delivered: All OrderItems have arrived with the Customer
     }
     }
 
 
 
 
@@ -41,24 +49,28 @@ state AdminAPI {
     }
     }
     Cancelled --> [*]
     Cancelled --> [*]
 
 
-    Fulfilled --> [*]
+    Delivered --> [*]
 }
 }
 
 
 
 
-AddingItems --> ArrangingPayment: transitionOrderToState
-ArrangingPayment --> AddingItems: transitionOrderToState
-ArrangingPayment --> PaymentAuthorized: addPaymentToOrder
-ArrangingPayment -----> PaymentSettled: addPaymentToOrder
-PaymentAuthorized --> PaymentSettled: settlePayment
-PaymentSettled --> Fulfilled: fulfillOrder
-PaymentSettled --> PartiallyFulfilled: fulfillOrder
-PartiallyFulfilled --> PartiallyFulfilled: fulfillOrder
-PartiallyFulfilled --> Fulfilled: fulfillOrder
+AddingItems -> ArrangingPayment: transitionOrderToState
+ArrangingPayment -> AddingItems: transitionOrderToState
+ArrangingPayment -> PaymentAuthorized: addPaymentToOrder
+ArrangingPayment --> PaymentSettled: addPaymentToOrder
+PaymentAuthorized -> PaymentSettled: settlePayment
+PaymentSettled -> Shipped: addFulfillmentToOrder, transitionFulfillmentToState
+PaymentSettled --> PartiallyShipped: addFulfillmentToOrder, transitionFulfillmentToState
+Shipped --> PartiallyDelivered: transitionFulfillmentToState
+Shipped -> Delivered: transitionFulfillmentToState
+PartiallyShipped --> Shipped: transitionFulfillmentToState
+PartiallyDelivered --> Delivered: transitionFulfillmentToState
 
 
 PaymentAuthorized --> Cancelled: cancelOrder
 PaymentAuthorized --> Cancelled: cancelOrder
-PaymentSettled --> Cancelled: cancelOrder
-PartiallyFulfilled --> Cancelled: cancelOrder
-Fulfilled --> Cancelled: cancelOrder
+PaymentSettled --> Cancelled
+Shipped --> Cancelled
+PartiallyShipped --> Cancelled
+PartiallyDelivered --> Cancelled
+Delivered --> Cancelled
 
 
 
 
 
 

+ 23 - 23
package.json

@@ -13,7 +13,7 @@
     "docs:watch": "concurrently --restart-tries 5 -n docgen,hugo,webpack -c green,blue,cyan \"yarn generate-graphql-docs && yarn generate-typescript-docs -w\" \"cd docs && hugo server\" \"cd docs && yarn webpack -w\"",
     "docs:watch": "concurrently --restart-tries 5 -n docgen,hugo,webpack -c green,blue,cyan \"yarn generate-graphql-docs && yarn generate-typescript-docs -w\" \"cd docs && hugo server\" \"cd docs && yarn webpack -w\"",
     "docs:build": "yarn generate-graphql-docs && yarn generate-typescript-docs && cd docs && yarn webpack --prod && node build.js && hugo",
     "docs:build": "yarn generate-graphql-docs && yarn generate-typescript-docs && cd docs && yarn webpack --prod && node build.js && hugo",
     "docs:deploy": "cd docs && yarn && cd .. && yarn docs:build",
     "docs:deploy": "cd docs && yarn && cd .. && yarn docs:build",
-    "codegen": "ts-node scripts/codegen/generate-graphql-types.ts",
+    "codegen": "tsc -p scripts/codegen/plugins && ts-node scripts/codegen/generate-graphql-types.ts",
     "generate-typescript-docs": "ts-node scripts/docs/generate-typescript-docs.ts",
     "generate-typescript-docs": "ts-node scripts/docs/generate-typescript-docs.ts",
     "generate-graphql-docs": "ts-node scripts/docs/generate-graphql-docs.ts --api=shop && ts-node scripts/docs/generate-graphql-docs.ts --api=admin",
     "generate-graphql-docs": "ts-node scripts/docs/generate-graphql-docs.ts --api=shop && ts-node scripts/docs/generate-graphql-docs.ts --api=admin",
     "version": "yarn check-imports && yarn build && yarn generate-changelog && git add CHANGELOG.md && git add */version.ts",
     "version": "yarn check-imports && yarn build && yarn generate-changelog && git add CHANGELOG.md && git add */version.ts",
@@ -27,33 +27,33 @@
     "publish-local": "lerna version --no-git-tag-version && cd scripts && ./publish-to-verdaccio.sh"
     "publish-local": "lerna version --no-git-tag-version && cd scripts && ./publish-to-verdaccio.sh"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@commitlint/cli": "^8.2.0",
-    "@commitlint/config-conventional": "^8.2.0",
-    "@graphql-codegen/add": "1.13.1",
-    "@graphql-codegen/cli": "1.13.1",
-    "@graphql-codegen/fragment-matcher": "1.13.1",
-    "@graphql-codegen/typescript": "1.13.1",
-    "@graphql-codegen/typescript-compatibility": "1.13.1",
-    "@graphql-codegen/typescript-operations": "1.13.1",
+    "@commitlint/cli": "^11.0.0",
+    "@commitlint/config-conventional": "^11.0.0",
+    "@graphql-codegen/add": "2.0.1",
+    "@graphql-codegen/cli": "1.17.8",
+    "@graphql-codegen/fragment-matcher": "1.17.8",
+    "@graphql-codegen/typescript": "1.17.9",
+    "@graphql-codegen/typescript-compatibility": "2.0.0",
+    "@graphql-codegen/typescript-operations": "1.17.8",
+    "@graphql-tools/schema": "^6.2.4",
     "@types/graphql": "^14.0.5",
     "@types/graphql": "^14.0.5",
-    "@types/jest": "^25.1.4",
+    "@types/jest": "^26.0.14",
     "@types/klaw-sync": "^6.0.0",
     "@types/klaw-sync": "^6.0.0",
     "@types/node": "^12.12.0",
     "@types/node": "^12.12.0",
-    "concurrently": "^5.0.0",
-    "conventional-changelog-core": "^4.0.3",
+    "concurrently": "^5.3.0",
+    "conventional-changelog-core": "^4.2.0",
     "find": "^0.3.0",
     "find": "^0.3.0",
-    "graphql": "^14.5.8",
-    "graphql-tools": "^4.0.0",
-    "husky": "^3.0.0",
-    "jest": "^25.2.1",
+    "graphql": "15.3.0",
+    "husky": "^4.3.0",
+    "jest": "^26.5.2",
     "klaw-sync": "^6.0.0",
     "klaw-sync": "^6.0.0",
-    "lerna": "^3.16.4",
-    "lint-staged": "^10.0.9",
-    "prettier": "^2.0.2",
-    "ts-jest": "^25.2.1",
-    "ts-node": "^8.4.1",
-    "tslint": "^5.11.0",
-    "typescript": "3.8.3"
+    "lerna": "^3.22.1",
+    "lint-staged": "^10.4.0",
+    "prettier": "^2.1.2",
+    "ts-jest": "^26.4.1",
+    "ts-node": "^9.0.0",
+    "tslint": "^6.1.3",
+    "typescript": "4.0.3"
   },
   },
   "resolutions": {
   "resolutions": {
     "npm-packlist": "1.1.12"
     "npm-packlist": "1.1.12"

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

@@ -17,15 +17,15 @@
     "access": "public"
     "access": "public"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@types/express": "^4.0.39",
-    "@types/fs-extra": "^8.0.1",
+    "@types/express": "^4.17.8",
+    "@types/fs-extra": "^9.0.1",
     "@vendure/common": "^0.15.0",
     "@vendure/common": "^0.15.0",
     "@vendure/core": "^0.15.2",
     "@vendure/core": "^0.15.2",
-    "express": "^4.16.4",
-    "rimraf": "^3.0.0",
-    "typescript": "3.8.3"
+    "express": "^4.17.1",
+    "rimraf": "^3.0.2",
+    "typescript": "4.0.3"
   },
   },
   "dependencies": {
   "dependencies": {
-    "fs-extra": "^9.0.0"
+    "fs-extra": "^9.0.1"
   }
   }
 }
 }

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

@@ -10,6 +10,7 @@ export const defaultAvailableLanguages = [
     LanguageCode.en,
     LanguageCode.en,
     LanguageCode.es,
     LanguageCode.es,
     LanguageCode.pl,
     LanguageCode.pl,
-    LanguageCode.zh,
+    LanguageCode.zh_Hans,
+    LanguageCode.zh_Hant,
     LanguageCode.pt_BR,
     LanguageCode.pt_BR,
 ];
 ];

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

@@ -21,7 +21,7 @@ import fs from 'fs-extra';
 import { Server } from 'http';
 import { Server } from 'http';
 import path from 'path';
 import path from 'path';
 
 
-import { defaultAvailableLanguages, defaultLanguage, DEFAULT_APP_PATH, loggerCtx } from './constants';
+import { DEFAULT_APP_PATH, defaultAvailableLanguages, defaultLanguage, loggerCtx } from './constants';
 
 
 /**
 /**
  * @description
  * @description

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

@@ -1,41 +1,41 @@
 {
 {
-  "generatedOn": "2020-08-26T08:49:50.552Z",
-  "lastCommit": "8881aee83b25f57edba537fbdcb85c77bf5f3c4e",
+  "generatedOn": "2020-10-05T10:27:57.372Z",
+  "lastCommit": "30dc63957a149d7175935cb31047cd46016b73bc",
   "translationStatus": {
   "translationStatus": {
     "de": {
     "de": {
-      "tokenCount": 662,
-      "translatedCount": 610,
-      "percentage": 92
+      "tokenCount": 672,
+      "translatedCount": 609,
+      "percentage": 91
     },
     },
     "en": {
     "en": {
-      "tokenCount": 662,
-      "translatedCount": 662,
+      "tokenCount": 672,
+      "translatedCount": 672,
       "percentage": 100
       "percentage": 100
     },
     },
     "es": {
     "es": {
-      "tokenCount": 662,
-      "translatedCount": 467,
-      "percentage": 71
+      "tokenCount": 672,
+      "translatedCount": 466,
+      "percentage": 69
     },
     },
     "pl": {
     "pl": {
-      "tokenCount": 662,
-      "translatedCount": 566,
-      "percentage": 85
+      "tokenCount": 672,
+      "translatedCount": 564,
+      "percentage": 84
     },
     },
     "pt_BR": {
     "pt_BR": {
-      "tokenCount": 662,
-      "translatedCount": 657,
-      "percentage": 99
+      "tokenCount": 672,
+      "translatedCount": 655,
+      "percentage": 97
     },
     },
     "zh_Hans": {
     "zh_Hans": {
-      "tokenCount": 662,
-      "translatedCount": 550,
-      "percentage": 83
+      "tokenCount": 672,
+      "translatedCount": 548,
+      "percentage": 82
     },
     },
     "zh_Hant": {
     "zh_Hant": {
-      "tokenCount": 662,
-      "translatedCount": 550,
-      "percentage": 83
+      "tokenCount": 672,
+      "translatedCount": 548,
+      "percentage": 82
     }
     }
   }
   }
 }
 }

+ 50 - 54
packages/admin-ui/package.json

@@ -18,82 +18,78 @@
     "directory": "package"
     "directory": "package"
   },
   },
   "dependencies": {
   "dependencies": {
-    "@angular/animations": "9.1.7",
-    "@angular/cdk": "9.2.3",
-    "@angular/common": "9.1.7",
-    "@angular/core": "9.1.7",
-    "@angular/forms": "9.1.7",
-    "@angular/language-service": "9.1.7",
-    "@angular/platform-browser": "9.1.7",
-    "@angular/platform-browser-dynamic": "9.1.7",
-    "@angular/router": "9.1.7",
+    "@angular/animations": "10.1.4",
+    "@angular/cdk": "10.2.4",
+    "@angular/common": "10.1.4",
+    "@angular/core": "10.1.4",
+    "@angular/forms": "10.1.4",
+    "@angular/language-service": "10.1.4",
+    "@angular/platform-browser": "10.1.4",
+    "@angular/platform-browser-dynamic": "10.1.4",
+    "@angular/router": "10.1.4",
+    "@apollo/client": "^3.0.0",
     "@biesbjerg/ngx-translate-extract-marker": "^1.0.0",
     "@biesbjerg/ngx-translate-extract-marker": "^1.0.0",
-    "@clr/angular": "^3.0.0",
-    "@clr/core": "^3.0.0",
-    "@clr/icons": "^3.0.0",
-    "@clr/ui": "^3.0.0",
-    "@ng-select/ng-select": "^3.7.2",
-    "@ngx-translate/core": "^12.1.2",
-    "@ngx-translate/http-loader": "^4.0.0",
+    "@clr/angular": "^4.0.3",
+    "@clr/core": "^4.0.3",
+    "@clr/icons": "^4.0.3",
+    "@clr/ui": "^4.0.3",
+    "@ng-select/ng-select": "^5.0.3",
+    "@ngx-translate/core": "^13.0.0",
+    "@ngx-translate/http-loader": "^6.0.0",
     "@vendure/common": "^0.15.0",
     "@vendure/common": "^0.15.0",
     "@webcomponents/custom-elements": "^1.2.4",
     "@webcomponents/custom-elements": "^1.2.4",
-    "apollo-angular": "^1.8.0",
-    "apollo-cache-inmemory": "^1.6.5",
-    "apollo-client": "^2.6.8",
-    "apollo-link": "^1.2.13",
-    "apollo-link-context": "^1.0.19",
+    "apollo-angular": "^2.0.4",
     "apollo-upload-client": "^12.1.0",
     "apollo-upload-client": "^12.1.0",
     "core-js": "^3.1.3",
     "core-js": "^3.1.3",
-    "dayjs": "^1.8.20",
-    "graphql": "^14.6.0",
-    "graphql-tag": "^2.10.3",
+    "dayjs": "^1.9.1",
+    "graphql": "15.3.0",
     "messageformat": "2.3.0",
     "messageformat": "2.3.0",
     "ngx-pagination": "^5.0.0",
     "ngx-pagination": "^5.0.0",
-    "ngx-translate-messageformat-compiler": "^4.6.0",
-    "prosemirror-commands": "^1.0.0",
-    "prosemirror-dropcursor": "^1.0.0",
-    "prosemirror-gapcursor": "^1.0.0",
-    "prosemirror-history": "^1.0.0",
-    "prosemirror-inputrules": "^1.0.0",
-    "prosemirror-keymap": "^1.0.0",
-    "prosemirror-menu": "^1.0.0",
+    "ngx-translate-messageformat-compiler": "^4.8.0",
+    "prosemirror-commands": "^1.1.4",
+    "prosemirror-dropcursor": "^1.3.2",
+    "prosemirror-gapcursor": "^1.1.5",
+    "prosemirror-history": "^1.1.3",
+    "prosemirror-inputrules": "^1.1.3",
+    "prosemirror-keymap": "^1.1.4",
+    "prosemirror-menu": "^1.1.4",
     "prosemirror-schema-basic": "^1.1.2",
     "prosemirror-schema-basic": "^1.1.2",
-    "prosemirror-schema-list": "^1.0.0",
-    "prosemirror-state": "^1.0.0",
-    "rxjs": "^6.5.4",
-    "tslib": "^1.10.0",
+    "prosemirror-schema-list": "^1.1.4",
+    "prosemirror-state": "^1.3.3",
+    "rxjs": "^6.6.3",
+    "tslib": "^2.0.0",
     "zone.js": "~0.10.2"
     "zone.js": "~0.10.2"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@angular-devkit/build-angular": "~0.901.6",
-    "@angular-devkit/build-ng-packagr": "~0.901.6",
-    "@angular/cli": "^9.1.6",
-    "@angular/compiler": "^9.1.7",
-    "@angular/compiler-cli": "^9.1.7",
+    "@angular-devkit/build-angular": "~0.1001.4",
+    "@angular-devkit/build-ng-packagr": "~0.1001.4",
+    "@angular/cli": "^10.1.4",
+    "@angular/compiler": "^10.1.4",
+    "@angular/compiler-cli": "^10.1.4",
     "@biesbjerg/ngx-translate-extract": "^6.0.3",
     "@biesbjerg/ngx-translate-extract": "^6.0.3",
     "@types/jasmine": "~3.5.10",
     "@types/jasmine": "~3.5.10",
     "@types/jasminewd2": "~2.0.6",
     "@types/jasminewd2": "~2.0.6",
     "@types/node": "^12.11.1",
     "@types/node": "^12.11.1",
-    "@types/prosemirror-commands": "^1.0.1",
-    "@types/prosemirror-menu": "^1.0.1",
-    "@types/prosemirror-state": "^1.2.3",
-    "@types/prosemirror-view": "^1.11.2",
+    "@types/prosemirror-commands": "^1.0.3",
+    "@types/prosemirror-menu": "^1.0.2",
+    "@types/prosemirror-state": "^1.2.5",
+    "@types/prosemirror-view": "1.15.1",
     "codelyzer": "^5.2.2",
     "codelyzer": "^5.2.2",
     "cross-spawn": "^7.0.3",
     "cross-spawn": "^7.0.3",
     "fs-extra": "^9.0.0",
     "fs-extra": "^9.0.0",
     "jasmine-core": "~3.5.0",
     "jasmine-core": "~3.5.0",
-    "jasmine-spec-reporter": "~5.0.1",
-    "karma": "~4.4.1",
+    "jasmine-spec-reporter": "~5.0.0",
+    "karma": "~5.0.0",
     "karma-chrome-launcher": "~3.1.0",
     "karma-chrome-launcher": "~3.1.0",
-    "karma-coverage-istanbul-reporter": "~2.1.1",
-    "karma-jasmine": "~3.1.1",
-    "karma-jasmine-html-reporter": "^1.5.3",
+    "karma-coverage-istanbul-reporter": "~3.0.2",
+    "karma-jasmine": "~4.0.0",
+    "karma-jasmine-html-reporter": "^1.5.0",
     "karma-mocha-reporter": "^2.2.5",
     "karma-mocha-reporter": "^2.2.5",
-    "ng-packagr": "^9.0.0",
-    "protractor": "~5.4.2",
+    "ng-packagr": "^10.1.0",
+    "protractor": "~7.0.0",
     "puppeteer": "^2.1.1",
     "puppeteer": "^2.1.1",
     "rimraf": "^3.0.0",
     "rimraf": "^3.0.0",
-    "tslint": "^6.1.0",
-    "typescript": "3.8.3"
+    "tslint": "~6.1.0",
+    "typescript": "4.0.3"
   }
   }
 }
 }

+ 0 - 0
packages/admin-ui/src/browserslist → packages/admin-ui/src/.browserslistrc


+ 28 - 16
packages/admin-ui/src/lib/catalog/src/components/asset-list/asset-list.component.ts

@@ -21,7 +21,8 @@ import { debounceTime, finalize, map, switchMap, takeUntil } from 'rxjs/operator
     templateUrl: './asset-list.component.html',
     templateUrl: './asset-list.component.html',
     styleUrls: ['./asset-list.component.scss'],
     styleUrls: ['./asset-list.component.scss'],
 })
 })
-export class AssetListComponent extends BaseListComponent<GetAssetList.Query, GetAssetList.Items>
+export class AssetListComponent
+    extends BaseListComponent<GetAssetList.Query, GetAssetList.Items>
     implements OnInit {
     implements OnInit {
     searchTerm = new FormControl('');
     searchTerm = new FormControl('');
     uploading = false;
     uploading = false;
@@ -37,7 +38,7 @@ export class AssetListComponent extends BaseListComponent<GetAssetList.Query, Ge
         super(router, route);
         super(router, route);
         super.setQueryFn(
         super.setQueryFn(
             (...args: any[]) => this.dataService.product.getAssetList(...args),
             (...args: any[]) => this.dataService.product.getAssetList(...args),
-            (data) => data.assets,
+            data => data.assets,
             (skip, take) => ({
             (skip, take) => ({
                 options: {
                 options: {
                     skip,
                     skip,
@@ -71,26 +72,39 @@ export class AssetListComponent extends BaseListComponent<GetAssetList.Query, Ge
             this.dataService.product
             this.dataService.product
                 .createAssets(files)
                 .createAssets(files)
                 .pipe(finalize(() => (this.uploading = false)))
                 .pipe(finalize(() => (this.uploading = false)))
-                .subscribe((res) => {
-                    super.refresh();
-                    this.notificationService.success(_('asset.notify-create-assets-success'), {
-                        count: files.length,
-                    });
+                .subscribe(({ createAssets }) => {
+                    let successCount = 0;
+                    for (const result of createAssets) {
+                        switch (result.__typename) {
+                            case 'Asset':
+                                successCount++;
+                                break;
+                            case 'MimeTypeError':
+                                this.notificationService.error(result.message);
+                                break;
+                        }
+                    }
+                    if (0 < successCount) {
+                        super.refresh();
+                        this.notificationService.success(_('asset.notify-create-assets-success'), {
+                            count: successCount,
+                        });
+                    }
                 });
                 });
         }
         }
     }
     }
 
 
     deleteAssets(assets: Asset[]) {
     deleteAssets(assets: Asset[]) {
-        this.showModalAndDelete(assets.map((a) => a.id))
+        this.showModalAndDelete(assets.map(a => a.id))
             .pipe(
             .pipe(
-                switchMap((response) => {
+                switchMap(response => {
                     if (response.result === DeletionResult.DELETED) {
                     if (response.result === DeletionResult.DELETED) {
                         return [true];
                         return [true];
                     } else {
                     } else {
                         return this.showModalAndDelete(
                         return this.showModalAndDelete(
-                            assets.map((a) => a.id),
+                            assets.map(a => a.id),
                             response.message || '',
                             response.message || '',
-                        ).pipe(map((r) => r.result === DeletionResult.DELETED));
+                        ).pipe(map(r => r.result === DeletionResult.DELETED));
                     }
                     }
                 }),
                 }),
             )
             )
@@ -101,7 +115,7 @@ export class AssetListComponent extends BaseListComponent<GetAssetList.Query, Ge
                     });
                     });
                     this.refresh();
                     this.refresh();
                 },
                 },
-                (err) => {
+                err => {
                     this.notificationService.error(_('common.notify-delete-error'), {
                     this.notificationService.error(_('common.notify-delete-error'), {
                         entity: 'Assets',
                         entity: 'Assets',
                     });
                     });
@@ -123,10 +137,8 @@ export class AssetListComponent extends BaseListComponent<GetAssetList.Query, Ge
                 ],
                 ],
             })
             })
             .pipe(
             .pipe(
-                switchMap((res) =>
-                    res ? this.dataService.product.deleteAssets(assetIds, !!message) : EMPTY,
-                ),
-                map((res) => res.deleteAssets),
+                switchMap(res => (res ? this.dataService.product.deleteAssets(assetIds, !!message) : EMPTY)),
+                map(res => res.deleteAssets),
             );
             );
     }
     }
 }
 }

+ 9 - 4
packages/admin-ui/src/lib/catalog/src/components/product-assets/product-assets.component.ts

@@ -11,7 +11,12 @@ import {
     Output,
     Output,
     ViewChild,
     ViewChild,
 } from '@angular/core';
 } from '@angular/core';
-import { Asset, AssetPickerDialogComponent, AssetPreviewDialogComponent, ModalService } from '@vendure/admin-ui/core';
+import {
+    Asset,
+    AssetPickerDialogComponent,
+    AssetPreviewDialogComponent,
+    ModalService,
+} from '@vendure/admin-ui/core';
 import { unique } from '@vendure/common/lib/unique';
 import { unique } from '@vendure/common/lib/unique';
 
 
 export interface AssetChange {
 export interface AssetChange {
@@ -189,13 +194,13 @@ export class ProductAssetsComponent implements AfterViewInit {
             );
             );
         }
         }
 
 
-        this.placeholder.enter(
-            drag,
+        this.placeholder._dropListRef.enter(
+            drag._dragRef,
             drag.element.nativeElement.offsetLeft,
             drag.element.nativeElement.offsetLeft,
             drag.element.nativeElement.offsetTop,
             drag.element.nativeElement.offsetTop,
         );
         );
         return false;
         return false;
-    }
+    };
 
 
     /** Determines the point of the page that was touched by the user. */
     /** Determines the point of the page that was touched by the user. */
     getPointerPositionOnPage(event: MouseEvent | TouchEvent) {
     getPointerPositionOnPage(event: MouseEvent | TouchEvent) {

+ 4 - 3
packages/admin-ui/src/lib/core/src/common/base-list.component.ts

@@ -1,4 +1,4 @@
-import { OnDestroy, OnInit } from '@angular/core';
+import { Directive, OnDestroy, OnInit } from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
 import { ActivatedRoute, Router } from '@angular/router';
 import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
 import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
 import { map, shareReplay, takeUntil } from 'rxjs/operators';
 import { map, shareReplay, takeUntil } from 'rxjs/operators';
@@ -10,9 +10,10 @@ export type MappingFn<T, R> = (result: R) => { items: T[]; totalItems: number };
 export type OnPageChangeFn<V> = (skip: number, take: number) => V;
 export type OnPageChangeFn<V> = (skip: number, take: number) => V;
 
 
 /**
 /**
- * This is a base class which implements the logic required to fetch and manipluate
+ * This is a base class which implements the logic required to fetch and manipulate
  * a list of data from a query which returns a PaginatedList type.
  * a list of data from a query which returns a PaginatedList type.
  */
  */
+@Directive()
 export class BaseListComponent<ResultType, ItemType, VariableType = any> implements OnInit, OnDestroy {
 export class BaseListComponent<ResultType, ItemType, VariableType = any> implements OnInit, OnDestroy {
     result$: Observable<ResultType>;
     result$: Observable<ResultType>;
     items$: Observable<ItemType[]>;
     items$: Observable<ItemType[]>;
@@ -24,7 +25,7 @@ export class BaseListComponent<ResultType, ItemType, VariableType = any> impleme
     private listQueryFn: ListQueryFn<ResultType>;
     private listQueryFn: ListQueryFn<ResultType>;
     private mappingFn: MappingFn<ItemType, ResultType>;
     private mappingFn: MappingFn<ItemType, ResultType>;
     private onPageChangeFn: OnPageChangeFn<VariableType> = (skip, take) =>
     private onPageChangeFn: OnPageChangeFn<VariableType> = (skip, take) =>
-        ({ options: { skip, take } } as any)
+        ({ options: { skip, take } } as any);
     private refresh$ = new BehaviorSubject<undefined>(undefined);
     private refresh$ = new BehaviorSubject<undefined>(undefined);
 
 
     constructor(protected router: Router, protected route: ActivatedRoute) {}
     constructor(protected router: Router, protected route: ActivatedRoute) {}

File diff suppressed because it is too large
+ 2853 - 2662
packages/admin-ui/src/lib/core/src/common/generated-types.ts


+ 147 - 276
packages/admin-ui/src/lib/core/src/common/introspection-result.ts

@@ -1,284 +1,155 @@
 // tslint:disable
 // tslint:disable
 
 
-export interface IntrospectionResultData {
-    __schema: {
-        types: {
-            kind: string;
-            name: string;
-            possibleTypes: {
-                name: string;
-            }[];
-        }[];
+export interface PossibleTypesResultData {
+    possibleTypes: {
+        [key: string]: string[];
     };
     };
 }
 }
-const result: IntrospectionResultData = {
-    __schema: {
-        types: [
-            {
-                kind: 'INTERFACE',
-                name: 'Node',
-                possibleTypes: [
-                    {
-                        name: 'Channel',
-                    },
-                    {
-                        name: 'Zone',
-                    },
-                    {
-                        name: 'Country',
-                    },
-                    {
-                        name: 'Administrator',
-                    },
-                    {
-                        name: 'User',
-                    },
-                    {
-                        name: 'Role',
-                    },
-                    {
-                        name: 'AuthenticationMethod',
-                    },
-                    {
-                        name: 'Asset',
-                    },
-                    {
-                        name: 'Collection',
-                    },
-                    {
-                        name: 'ProductVariant',
-                    },
-                    {
-                        name: 'StockAdjustment',
-                    },
-                    {
-                        name: 'Sale',
-                    },
-                    {
-                        name: 'OrderLine',
-                    },
-                    {
-                        name: 'OrderItem',
-                    },
-                    {
-                        name: 'Fulfillment',
-                    },
-                    {
-                        name: 'Order',
-                    },
-                    {
-                        name: 'Customer',
-                    },
-                    {
-                        name: 'CustomerGroup',
-                    },
-                    {
-                        name: 'HistoryEntry',
-                    },
-                    {
-                        name: 'Address',
-                    },
-                    {
-                        name: 'Promotion',
-                    },
-                    {
-                        name: 'Payment',
-                    },
-                    {
-                        name: 'Refund',
-                    },
-                    {
-                        name: 'ShippingMethod',
-                    },
-                    {
-                        name: 'Cancellation',
-                    },
-                    {
-                        name: 'Return',
-                    },
-                    {
-                        name: 'Product',
-                    },
-                    {
-                        name: 'ProductOptionGroup',
-                    },
-                    {
-                        name: 'ProductOption',
-                    },
-                    {
-                        name: 'FacetValue',
-                    },
-                    {
-                        name: 'Facet',
-                    },
-                    {
-                        name: 'TaxRate',
-                    },
-                    {
-                        name: 'TaxCategory',
-                    },
-                    {
-                        name: 'Job',
-                    },
-                    {
-                        name: 'PaymentMethod',
-                    },
-                ],
-            },
-            {
-                kind: 'INTERFACE',
-                name: 'PaginatedList',
-                possibleTypes: [
-                    {
-                        name: 'AdministratorList',
-                    },
-                    {
-                        name: 'AssetList',
-                    },
-                    {
-                        name: 'ProductVariantList',
-                    },
-                    {
-                        name: 'CustomerList',
-                    },
-                    {
-                        name: 'HistoryEntryList',
-                    },
-                    {
-                        name: 'OrderList',
-                    },
-                    {
-                        name: 'CollectionList',
-                    },
-                    {
-                        name: 'CountryList',
-                    },
-                    {
-                        name: 'CustomerGroupList',
-                    },
-                    {
-                        name: 'FacetList',
-                    },
-                    {
-                        name: 'JobList',
-                    },
-                    {
-                        name: 'PaymentMethodList',
-                    },
-                    {
-                        name: 'ProductList',
-                    },
-                    {
-                        name: 'PromotionList',
-                    },
-                    {
-                        name: 'RoleList',
-                    },
-                    {
-                        name: 'ShippingMethodList',
-                    },
-                    {
-                        name: 'TaxRateList',
-                    },
-                ],
-            },
-            {
-                kind: 'UNION',
-                name: 'StockMovementItem',
-                possibleTypes: [
-                    {
-                        name: 'StockAdjustment',
-                    },
-                    {
-                        name: 'Sale',
-                    },
-                    {
-                        name: 'Cancellation',
-                    },
-                    {
-                        name: 'Return',
-                    },
-                ],
-            },
-            {
-                kind: 'INTERFACE',
-                name: 'StockMovement',
-                possibleTypes: [
-                    {
-                        name: 'StockAdjustment',
-                    },
-                    {
-                        name: 'Sale',
-                    },
-                    {
-                        name: 'Cancellation',
-                    },
-                    {
-                        name: 'Return',
-                    },
-                ],
-            },
-            {
-                kind: 'UNION',
-                name: 'CustomFieldConfig',
-                possibleTypes: [
-                    {
-                        name: 'StringCustomFieldConfig',
-                    },
-                    {
-                        name: 'LocaleStringCustomFieldConfig',
-                    },
-                    {
-                        name: 'IntCustomFieldConfig',
-                    },
-                    {
-                        name: 'FloatCustomFieldConfig',
-                    },
-                    {
-                        name: 'BooleanCustomFieldConfig',
-                    },
-                    {
-                        name: 'DateTimeCustomFieldConfig',
-                    },
-                ],
-            },
-            {
-                kind: 'INTERFACE',
-                name: 'CustomField',
-                possibleTypes: [
-                    {
-                        name: 'StringCustomFieldConfig',
-                    },
-                    {
-                        name: 'LocaleStringCustomFieldConfig',
-                    },
-                    {
-                        name: 'IntCustomFieldConfig',
-                    },
-                    {
-                        name: 'FloatCustomFieldConfig',
-                    },
-                    {
-                        name: 'BooleanCustomFieldConfig',
-                    },
-                    {
-                        name: 'DateTimeCustomFieldConfig',
-                    },
-                ],
-            },
-            {
-                kind: 'UNION',
-                name: 'SearchResultPrice',
-                possibleTypes: [
-                    {
-                        name: 'PriceRange',
-                    },
-                    {
-                        name: 'SinglePrice',
-                    },
-                ],
-            },
+const result: PossibleTypesResultData = {
+    possibleTypes: {
+        CreateAssetResult: ['Asset', 'MimeTypeError'],
+        NativeAuthenticationResult: ['CurrentUser', 'InvalidCredentialsError', 'NativeAuthStrategyError'],
+        AuthenticationResult: ['CurrentUser', 'InvalidCredentialsError'],
+        CreateChannelResult: ['Channel', 'LanguageNotAvailableError'],
+        UpdateChannelResult: ['Channel', 'LanguageNotAvailableError'],
+        CreateCustomerResult: ['Customer', 'EmailAddressConflictError'],
+        UpdateCustomerResult: ['Customer', 'EmailAddressConflictError'],
+        UpdateGlobalSettingsResult: ['GlobalSettings', 'ChannelDefaultLanguageError'],
+        TransitionOrderToStateResult: ['Order', 'OrderStateTransitionError'],
+        SettlePaymentResult: [
+            'Payment',
+            'SettlePaymentError',
+            'PaymentStateTransitionError',
+            'OrderStateTransitionError',
         ],
         ],
+        AddFulfillmentToOrderResult: [
+            'Fulfillment',
+            'EmptyOrderLineSelectionError',
+            'ItemsAlreadyFulfilledError',
+        ],
+        CancelOrderResult: [
+            'Order',
+            'EmptyOrderLineSelectionError',
+            'QuantityTooGreatError',
+            'MultipleOrderError',
+            'CancelActiveOrderError',
+            'OrderStateTransitionError',
+        ],
+        RefundOrderResult: [
+            'Refund',
+            'QuantityTooGreatError',
+            'NothingToRefundError',
+            'OrderStateTransitionError',
+            'MultipleOrderError',
+            'PaymentOrderMismatchError',
+            'RefundOrderStateError',
+            'AlreadyRefundedError',
+            'RefundStateTransitionError',
+        ],
+        SettleRefundResult: ['Refund', 'RefundStateTransitionError'],
+        TransitionFulfillmentToStateResult: ['Fulfillment', 'FulfillmentStateTransitionError'],
+        RemoveOptionGroupFromProductResult: ['Product', 'ProductOptionInUseError'],
+        CreatePromotionResult: ['Promotion', 'MissingConditionsError'],
+        UpdatePromotionResult: ['Promotion', 'MissingConditionsError'],
+        PaginatedList: [
+            'CustomerGroupList',
+            'JobList',
+            'PaymentMethodList',
+            'AdministratorList',
+            'AssetList',
+            'CollectionList',
+            'ProductVariantList',
+            'CountryList',
+            'CustomerList',
+            'FacetList',
+            'HistoryEntryList',
+            'OrderList',
+            'ProductList',
+            'PromotionList',
+            'RoleList',
+            'ShippingMethodList',
+            'TaxRateList',
+        ],
+        Node: [
+            'Collection',
+            'Customer',
+            'Facet',
+            'Fulfillment',
+            'Job',
+            'Order',
+            'Product',
+            'ProductVariant',
+            'Address',
+            'Administrator',
+            'Asset',
+            'Channel',
+            'Country',
+            'CustomerGroup',
+            'FacetValue',
+            'HistoryEntry',
+            'OrderItem',
+            'OrderLine',
+            'Payment',
+            'Refund',
+            'PaymentMethod',
+            'ProductOptionGroup',
+            'ProductOption',
+            'Promotion',
+            'Role',
+            'ShippingMethod',
+            'StockAdjustment',
+            'Sale',
+            'Cancellation',
+            'Return',
+            'TaxCategory',
+            'TaxRate',
+            'User',
+            'AuthenticationMethod',
+            'Zone',
+        ],
+        ErrorResult: [
+            'MimeTypeError',
+            'LanguageNotAvailableError',
+            'ChannelDefaultLanguageError',
+            'SettlePaymentError',
+            'EmptyOrderLineSelectionError',
+            'ItemsAlreadyFulfilledError',
+            'MultipleOrderError',
+            'CancelActiveOrderError',
+            'PaymentOrderMismatchError',
+            'RefundOrderStateError',
+            'NothingToRefundError',
+            'AlreadyRefundedError',
+            'QuantityTooGreatError',
+            'RefundStateTransitionError',
+            'PaymentStateTransitionError',
+            'FulfillmentStateTransitionError',
+            'ProductOptionInUseError',
+            'MissingConditionsError',
+            'NativeAuthStrategyError',
+            'InvalidCredentialsError',
+            'OrderStateTransitionError',
+            'EmailAddressConflictError',
+        ],
+        CustomField: [
+            'StringCustomFieldConfig',
+            'LocaleStringCustomFieldConfig',
+            'IntCustomFieldConfig',
+            'FloatCustomFieldConfig',
+            'BooleanCustomFieldConfig',
+            'DateTimeCustomFieldConfig',
+        ],
+        CustomFieldConfig: [
+            'StringCustomFieldConfig',
+            'LocaleStringCustomFieldConfig',
+            'IntCustomFieldConfig',
+            'FloatCustomFieldConfig',
+            'BooleanCustomFieldConfig',
+            'DateTimeCustomFieldConfig',
+        ],
+        SearchResultPrice: ['PriceRange', 'SinglePrice'],
+        StockMovement: ['StockAdjustment', 'Sale', 'Cancellation', 'Return'],
+        StockMovementItem: ['StockAdjustment', 'Sale', 'Cancellation', 'Return'],
     },
     },
 };
 };
 export default result;
 export default result;

+ 1 - 1
packages/admin-ui/src/lib/core/src/data/check-jobs-link.ts

@@ -1,5 +1,5 @@
 import { Injector } from '@angular/core';
 import { Injector } from '@angular/core';
-import { ApolloLink, Operation } from 'apollo-link';
+import { ApolloLink, Operation } from '@apollo/client/core';
 
 
 import { JobQueueService } from '../providers/job-queue/job-queue.service';
 import { JobQueueService } from '../providers/job-queue/job-queue.service';
 
 

+ 8 - 8
packages/admin-ui/src/lib/core/src/data/client-state/client-resolvers.ts

@@ -1,4 +1,4 @@
-import { InMemoryCache } from 'apollo-cache-inmemory';
+import { InMemoryCache } from '@apollo/client/core';
 
 
 import {
 import {
     GetNetworkStatus,
     GetNetworkStatus,
@@ -11,7 +11,7 @@ import {
     UpdateUserChannels,
     UpdateUserChannels,
     UserStatus,
     UserStatus,
 } from '../../common/generated-types';
 } from '../../common/generated-types';
-import { GET_NEWTORK_STATUS, GET_USER_STATUS } from '../definitions/client-definitions';
+import { GET_NEWTORK_STATUS, GET_UI_STATE, GET_USER_STATUS } from '../definitions/client-definitions';
 
 
 export type ResolverContext = {
 export type ResolverContext = {
     cache: InMemoryCache;
     cache: InMemoryCache;
@@ -50,7 +50,7 @@ export const clientResolvers: ResolverDefinition = {
                     activeChannelId,
                     activeChannelId,
                 },
                 },
             };
             };
-            cache.writeData({ data });
+            cache.writeQuery({ query: GET_USER_STATUS, data });
             return data.userStatus;
             return data.userStatus;
         },
         },
         setAsLoggedOut: (_, args, { cache }): GetUserStatus.UserStatus => {
         setAsLoggedOut: (_, args, { cache }): GetUserStatus.UserStatus => {
@@ -65,7 +65,7 @@ export const clientResolvers: ResolverDefinition = {
                     activeChannelId: null,
                     activeChannelId: null,
                 },
                 },
             };
             };
-            cache.writeData({ data });
+            cache.writeQuery({ query: GET_USER_STATUS, data });
             return data.userStatus;
             return data.userStatus;
         },
         },
         setUiLanguage: (_, args: SetUiLanguage.Variables, { cache }): LanguageCode => {
         setUiLanguage: (_, args: SetUiLanguage.Variables, { cache }): LanguageCode => {
@@ -75,7 +75,7 @@ export const clientResolvers: ResolverDefinition = {
                     language: args.languageCode,
                     language: args.languageCode,
                 },
                 },
             };
             };
-            cache.writeData({ data });
+            cache.writeQuery({ query: GET_UI_STATE, data });
             return args.languageCode;
             return args.languageCode;
         },
         },
         setActiveChannel: (_, args: SetActiveChannel.Variables, { cache }): UserStatus => {
         setActiveChannel: (_, args: SetActiveChannel.Variables, { cache }): UserStatus => {
@@ -93,7 +93,7 @@ export const clientResolvers: ResolverDefinition = {
                     activeChannelId: activeChannel.id,
                     activeChannelId: activeChannel.id,
                 },
                 },
             };
             };
-            cache.writeData({ data });
+            cache.writeQuery({ query: GET_USER_STATUS, data });
             return { ...previous.userStatus, ...data.userStatus };
             return { ...previous.userStatus, ...data.userStatus };
         },
         },
         updateUserChannels: (_, args: UpdateUserChannels.Variables, { cache }): UserStatus => {
         updateUserChannels: (_, args: UpdateUserChannels.Variables, { cache }): UserStatus => {
@@ -105,7 +105,7 @@ export const clientResolvers: ResolverDefinition = {
                     channels: args.channels,
                     channels: args.channels,
                 },
                 },
             };
             };
-            cache.writeData({ data });
+            cache.writeQuery({ query: GET_USER_STATUS, data });
             return { ...previous.userStatus, ...data.userStatus };
             return { ...previous.userStatus, ...data.userStatus };
         },
         },
     },
     },
@@ -120,6 +120,6 @@ function updateRequestsInFlight(cache: InMemoryCache, increment: 1 | -1): number
             inFlightRequests,
             inFlightRequests,
         },
         },
     };
     };
-    cache.writeData({ data });
+    cache.writeQuery({ query: GET_NEWTORK_STATUS, data });
     return inFlightRequests;
     return inFlightRequests;
 }
 }

+ 10 - 11
packages/admin-ui/src/lib/core/src/data/data.module.ts

@@ -1,10 +1,9 @@
-import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
+import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
 import { APP_INITIALIZER, Injector, NgModule } from '@angular/core';
 import { APP_INITIALIZER, Injector, NgModule } from '@angular/core';
-import { ApolloModule, APOLLO_OPTIONS } from 'apollo-angular';
-import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
-import { ApolloClientOptions } from 'apollo-client';
-import { ApolloLink } from 'apollo-link';
-import { setContext } from 'apollo-link-context';
+import { ApolloClientOptions, InMemoryCache } from '@apollo/client/core';
+import { setContext } from '@apollo/client/link/context';
+import { ApolloLink } from '@apollo/client/link/core';
+import { APOLLO_OPTIONS } from 'apollo-angular';
 import { createUploadLink } from 'apollo-upload-client';
 import { createUploadLink } from 'apollo-upload-client';
 
 
 import { getAppConfig } from '../app.config';
 import { getAppConfig } from '../app.config';
@@ -14,6 +13,7 @@ import { LocalStorageService } from '../providers/local-storage/local-storage.se
 import { CheckJobsLink } from './check-jobs-link';
 import { CheckJobsLink } from './check-jobs-link';
 import { getClientDefaults } from './client-state/client-defaults';
 import { getClientDefaults } from './client-state/client-defaults';
 import { clientResolvers } from './client-state/client-resolvers';
 import { clientResolvers } from './client-state/client-resolvers';
+import { GET_CLIENT_STATE } from './definitions/client-definitions';
 import { OmitTypenameLink } from './omit-typename-link';
 import { OmitTypenameLink } from './omit-typename-link';
 import { BaseDataService } from './providers/base-data.service';
 import { BaseDataService } from './providers/base-data.service';
 import { DataService } from './providers/data.service';
 import { DataService } from './providers/data.service';
@@ -30,11 +30,10 @@ export function createApollo(
     const { adminApiPath, tokenMethod } = getAppConfig();
     const { adminApiPath, tokenMethod } = getAppConfig();
     const serverLocation = getServerLocation();
     const serverLocation = getServerLocation();
     const apolloCache = new InMemoryCache({
     const apolloCache = new InMemoryCache({
-        fragmentMatcher: new IntrospectionFragmentMatcher({
-            introspectionQueryResultData: introspectionResult,
-        }),
+        possibleTypes: introspectionResult.possibleTypes,
     });
     });
-    apolloCache.writeData({
+    apolloCache.writeQuery({
+        query: GET_CLIENT_STATE,
         data: getClientDefaults(localStorageService),
         data: getClientDefaults(localStorageService),
     });
     });
 
 
@@ -76,7 +75,7 @@ export function createApollo(
  * state via the apollo-link-state package.
  * state via the apollo-link-state package.
  */
  */
 @NgModule({
 @NgModule({
-    imports: [ApolloModule, HttpClientModule],
+    imports: [HttpClientModule],
     exports: [],
     exports: [],
     declarations: [],
     declarations: [],
     providers: [
     providers: [

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

@@ -1,4 +1,4 @@
-import gql from 'graphql-tag';
+import { gql } from 'apollo-angular';
 
 
 export const ROLE_FRAGMENT = gql`
 export const ROLE_FRAGMENT = gql`
     fragment Role on Role {
     fragment Role on Role {

+ 9 - 5
packages/admin-ui/src/lib/core/src/data/definitions/auth-definitions.ts

@@ -1,4 +1,6 @@
-import gql from 'graphql-tag';
+import { gql } from 'apollo-angular';
+
+import { ERROR_RESULT_FRAGMENT } from './shared-definitions';
 
 
 export const CURRENT_USER_FRAGMENT = gql`
 export const CURRENT_USER_FRAGMENT = gql`
     fragment CurrentUser on CurrentUser {
     fragment CurrentUser on CurrentUser {
@@ -16,17 +18,19 @@ export const CURRENT_USER_FRAGMENT = gql`
 export const ATTEMPT_LOGIN = gql`
 export const ATTEMPT_LOGIN = gql`
     mutation AttemptLogin($username: String!, $password: String!, $rememberMe: Boolean!) {
     mutation AttemptLogin($username: String!, $password: String!, $rememberMe: Boolean!) {
         login(username: $username, password: $password, rememberMe: $rememberMe) {
         login(username: $username, password: $password, rememberMe: $rememberMe) {
-            user {
-                ...CurrentUser
-            }
+            ...CurrentUser
+            ...ErrorResult
         }
         }
     }
     }
     ${CURRENT_USER_FRAGMENT}
     ${CURRENT_USER_FRAGMENT}
+    ${ERROR_RESULT_FRAGMENT}
 `;
 `;
 
 
 export const LOG_OUT = gql`
 export const LOG_OUT = gql`
     mutation LogOut {
     mutation LogOut {
-        logout
+        logout {
+            success
+        }
     }
     }
 `;
 `;
 
 

+ 16 - 1
packages/admin-ui/src/lib/core/src/data/definitions/client-definitions.ts

@@ -1,4 +1,4 @@
-import gql from 'graphql-tag';
+import { gql } from 'apollo-angular';
 
 
 export const REQUEST_STARTED = gql`
 export const REQUEST_STARTED = gql`
     mutation RequestStarted {
     mutation RequestStarted {
@@ -77,6 +77,21 @@ export const GET_UI_STATE = gql`
     }
     }
 `;
 `;
 
 
+export const GET_CLIENT_STATE = gql`
+    query GetClientState {
+        networkStatus @client {
+            inFlightRequests
+        }
+        userStatus @client {
+            ...UserStatus
+        }
+        uiState @client {
+            language
+        }
+    }
+    ${USER_STATUS_FRAGMENT}
+`;
+
 export const SET_ACTIVE_CHANNEL = gql`
 export const SET_ACTIVE_CHANNEL = gql`
     mutation SetActiveChannel($channelId: ID!) {
     mutation SetActiveChannel($channelId: ID!) {
         setActiveChannel(channelId: $channelId) @client {
         setActiveChannel(channelId: $channelId) @client {

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

@@ -1,4 +1,4 @@
-import gql from 'graphql-tag';
+import { gql } from 'apollo-angular';
 
 
 import { ASSET_FRAGMENT } from './product-definitions';
 import { ASSET_FRAGMENT } from './product-definitions';
 import { CONFIGURABLE_OPERATION_DEF_FRAGMENT, CONFIGURABLE_OPERATION_FRAGMENT } from './shared-definitions';
 import { CONFIGURABLE_OPERATION_DEF_FRAGMENT, CONFIGURABLE_OPERATION_FRAGMENT } from './shared-definitions';

+ 7 - 1
packages/admin-ui/src/lib/core/src/data/definitions/customer-definitions.ts

@@ -1,4 +1,6 @@
-import gql from 'graphql-tag';
+import { gql } from 'apollo-angular';
+
+import { ERROR_RESULT_FRAGMENT } from './shared-definitions';
 
 
 export const ADDRESS_FRAGMENT = gql`
 export const ADDRESS_FRAGMENT = gql`
     fragment Address on Address {
     fragment Address on Address {
@@ -95,18 +97,22 @@ export const CREATE_CUSTOMER = gql`
     mutation CreateCustomer($input: CreateCustomerInput!, $password: String) {
     mutation CreateCustomer($input: CreateCustomerInput!, $password: String) {
         createCustomer(input: $input, password: $password) {
         createCustomer(input: $input, password: $password) {
             ...Customer
             ...Customer
+            ...ErrorResult
         }
         }
     }
     }
     ${CUSTOMER_FRAGMENT}
     ${CUSTOMER_FRAGMENT}
+    ${ERROR_RESULT_FRAGMENT}
 `;
 `;
 
 
 export const UPDATE_CUSTOMER = gql`
 export const UPDATE_CUSTOMER = gql`
     mutation UpdateCustomer($input: UpdateCustomerInput!) {
     mutation UpdateCustomer($input: UpdateCustomerInput!) {
         updateCustomer(input: $input) {
         updateCustomer(input: $input) {
             ...Customer
             ...Customer
+            ...ErrorResult
         }
         }
     }
     }
     ${CUSTOMER_FRAGMENT}
     ${CUSTOMER_FRAGMENT}
+    ${ERROR_RESULT_FRAGMENT}
 `;
 `;
 
 
 export const DELETE_CUSTOMER = gql`
 export const DELETE_CUSTOMER = gql`

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

@@ -1,4 +1,4 @@
-import gql from 'graphql-tag';
+import { gql } from 'apollo-angular';
 
 
 export const FACET_VALUE_FRAGMENT = gql`
 export const FACET_VALUE_FRAGMENT = gql`
     fragment FacetValue on FacetValue {
     fragment FacetValue on FacetValue {

+ 52 - 8
packages/admin-ui/src/lib/core/src/data/definitions/order-definitions.ts

@@ -1,4 +1,6 @@
-import gql from 'graphql-tag';
+import { gql } from 'apollo-angular';
+
+import { ERROR_RESULT_FRAGMENT } from './shared-definitions';
 
 
 export const ADJUSTMENT_FRAGMENT = gql`
 export const ADJUSTMENT_FRAGMENT = gql`
     fragment Adjustment on Adjustment {
     fragment Adjustment on Adjustment {
@@ -56,6 +58,8 @@ export const ORDER_FRAGMENT = gql`
 export const FULFILLMENT_FRAGMENT = gql`
 export const FULFILLMENT_FRAGMENT = gql`
     fragment Fulfillment on Fulfillment {
     fragment Fulfillment on Fulfillment {
         id
         id
+        state
+        nextStates
         createdAt
         createdAt
         updatedAt
         updatedAt
         method
         method
@@ -197,50 +201,71 @@ export const GET_ORDER = gql`
 export const SETTLE_PAYMENT = gql`
 export const SETTLE_PAYMENT = gql`
     mutation SettlePayment($id: ID!) {
     mutation SettlePayment($id: ID!) {
         settlePayment(id: $id) {
         settlePayment(id: $id) {
-            id
-            transactionId
-            amount
-            method
-            state
-            metadata
+            ... on Payment {
+                id
+                transactionId
+                amount
+                method
+                state
+                metadata
+            }
+            ...ErrorResult
+            ... on SettlePaymentError {
+                paymentErrorMessage
+            }
+            ... on PaymentStateTransitionError {
+                transitionError
+            }
+            ... on OrderStateTransitionError {
+                transitionError
+            }
         }
         }
     }
     }
+    ${ERROR_RESULT_FRAGMENT}
 `;
 `;
 
 
 export const CREATE_FULFILLMENT = gql`
 export const CREATE_FULFILLMENT = gql`
     mutation CreateFulfillment($input: FulfillOrderInput!) {
     mutation CreateFulfillment($input: FulfillOrderInput!) {
-        fulfillOrder(input: $input) {
+        addFulfillmentToOrder(input: $input) {
             ...Fulfillment
             ...Fulfillment
+            ...ErrorResult
         }
         }
     }
     }
     ${FULFILLMENT_FRAGMENT}
     ${FULFILLMENT_FRAGMENT}
+    ${ERROR_RESULT_FRAGMENT}
 `;
 `;
 
 
 export const CANCEL_ORDER = gql`
 export const CANCEL_ORDER = gql`
     mutation CancelOrder($input: CancelOrderInput!) {
     mutation CancelOrder($input: CancelOrderInput!) {
         cancelOrder(input: $input) {
         cancelOrder(input: $input) {
             ...OrderDetail
             ...OrderDetail
+            ...ErrorResult
         }
         }
     }
     }
     ${ORDER_DETAIL_FRAGMENT}
     ${ORDER_DETAIL_FRAGMENT}
+    ${ERROR_RESULT_FRAGMENT}
 `;
 `;
 
 
 export const REFUND_ORDER = gql`
 export const REFUND_ORDER = gql`
     mutation RefundOrder($input: RefundOrderInput!) {
     mutation RefundOrder($input: RefundOrderInput!) {
         refundOrder(input: $input) {
         refundOrder(input: $input) {
             ...Refund
             ...Refund
+            ...ErrorResult
         }
         }
     }
     }
     ${REFUND_FRAGMENT}
     ${REFUND_FRAGMENT}
+    ${ERROR_RESULT_FRAGMENT}
 `;
 `;
 
 
 export const SETTLE_REFUND = gql`
 export const SETTLE_REFUND = gql`
     mutation SettleRefund($input: SettleRefundInput!) {
     mutation SettleRefund($input: SettleRefundInput!) {
         settleRefund(input: $input) {
         settleRefund(input: $input) {
             ...Refund
             ...Refund
+            ...ErrorResult
         }
         }
     }
     }
     ${REFUND_FRAGMENT}
     ${REFUND_FRAGMENT}
+    ${ERROR_RESULT_FRAGMENT}
 `;
 `;
 
 
 export const GET_ORDER_HISTORY = gql`
 export const GET_ORDER_HISTORY = gql`
@@ -297,9 +322,14 @@ export const TRANSITION_ORDER_TO_STATE = gql`
     mutation TransitionOrderToState($id: ID!, $state: String!) {
     mutation TransitionOrderToState($id: ID!, $state: String!) {
         transitionOrderToState(id: $id, state: $state) {
         transitionOrderToState(id: $id, state: $state) {
             ...Order
             ...Order
+            ...ErrorResult
+            ... on OrderStateTransitionError {
+                transitionError
+            }
         }
         }
     }
     }
     ${ORDER_FRAGMENT}
     ${ORDER_FRAGMENT}
+    ${ERROR_RESULT_FRAGMENT}
 `;
 `;
 
 
 export const UPDATE_ORDER_CUSTOM_FIELDS = gql`
 export const UPDATE_ORDER_CUSTOM_FIELDS = gql`
@@ -310,3 +340,17 @@ export const UPDATE_ORDER_CUSTOM_FIELDS = gql`
     }
     }
     ${ORDER_FRAGMENT}
     ${ORDER_FRAGMENT}
 `;
 `;
+
+export const TRANSITION_FULFILLMENT_TO_STATE = gql`
+    mutation TransitionFulfillmentToState($id: ID!, $state: String!) {
+        transitionFulfillmentToState(id: $id, state: $state) {
+            ...Fulfillment
+            ...ErrorResult
+            ... on FulfillmentStateTransitionError {
+                transitionError
+            }
+        }
+    }
+    ${FULFILLMENT_FRAGMENT}
+    ${ERROR_RESULT_FRAGMENT}
+`;

+ 16 - 7
packages/admin-ui/src/lib/core/src/data/definitions/product-definitions.ts

@@ -1,4 +1,6 @@
-import gql from 'graphql-tag';
+import { gql } from 'apollo-angular';
+
+import { ERROR_RESULT_FRAGMENT } from './shared-definitions';
 
 
 export const ASSET_FRAGMENT = gql`
 export const ASSET_FRAGMENT = gql`
     fragment Asset on Asset {
     fragment Asset on Asset {
@@ -274,23 +276,27 @@ export const ADD_OPTION_GROUP_TO_PRODUCT = gql`
 export const REMOVE_OPTION_GROUP_FROM_PRODUCT = gql`
 export const REMOVE_OPTION_GROUP_FROM_PRODUCT = gql`
     mutation RemoveOptionGroupFromProduct($productId: ID!, $optionGroupId: ID!) {
     mutation RemoveOptionGroupFromProduct($productId: ID!, $optionGroupId: ID!) {
         removeOptionGroupFromProduct(productId: $productId, optionGroupId: $optionGroupId) {
         removeOptionGroupFromProduct(productId: $productId, optionGroupId: $optionGroupId) {
-            id
-            createdAt
-            updatedAt
-            optionGroups {
+            ... on Product {
                 id
                 id
                 createdAt
                 createdAt
                 updatedAt
                 updatedAt
-                code
-                options {
+                optionGroups {
                     id
                     id
                     createdAt
                     createdAt
                     updatedAt
                     updatedAt
                     code
                     code
+                    options {
+                        id
+                        createdAt
+                        updatedAt
+                        code
+                    }
                 }
                 }
             }
             }
+            ...ErrorResult
         }
         }
     }
     }
+    ${ERROR_RESULT_FRAGMENT}
 `;
 `;
 
 
 export const GET_PRODUCT_WITH_VARIANTS = gql`
 export const GET_PRODUCT_WITH_VARIANTS = gql`
@@ -371,6 +377,9 @@ export const CREATE_ASSETS = gql`
     mutation CreateAssets($input: [CreateAssetInput!]!) {
     mutation CreateAssets($input: [CreateAssetInput!]!) {
         createAssets(input: $input) {
         createAssets(input: $input) {
             ...Asset
             ...Asset
+            ... on ErrorResult {
+                message
+            }
         }
         }
     }
     }
     ${ASSET_FRAGMENT}
     ${ASSET_FRAGMENT}

+ 8 - 2
packages/admin-ui/src/lib/core/src/data/definitions/promotion-definitions.ts

@@ -1,6 +1,10 @@
-import gql from 'graphql-tag';
+import { gql } from 'apollo-angular';
 
 
-import { CONFIGURABLE_OPERATION_DEF_FRAGMENT, CONFIGURABLE_OPERATION_FRAGMENT } from './shared-definitions';
+import {
+    CONFIGURABLE_OPERATION_DEF_FRAGMENT,
+    CONFIGURABLE_OPERATION_FRAGMENT,
+    ERROR_RESULT_FRAGMENT,
+} from './shared-definitions';
 
 
 export const PROMOTION_FRAGMENT = gql`
 export const PROMOTION_FRAGMENT = gql`
     fragment Promotion on Promotion {
     fragment Promotion on Promotion {
@@ -60,9 +64,11 @@ export const CREATE_PROMOTION = gql`
     mutation CreatePromotion($input: CreatePromotionInput!) {
     mutation CreatePromotion($input: CreatePromotionInput!) {
         createPromotion(input: $input) {
         createPromotion(input: $input) {
             ...Promotion
             ...Promotion
+            ...ErrorResult
         }
         }
     }
     }
     ${PROMOTION_FRAGMENT}
     ${PROMOTION_FRAGMENT}
+    ${ERROR_RESULT_FRAGMENT}
 `;
 `;
 
 
 export const UPDATE_PROMOTION = gql`
 export const UPDATE_PROMOTION = gql`

+ 8 - 2
packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts

@@ -1,6 +1,6 @@
-import gql from 'graphql-tag';
+import { gql } from 'apollo-angular';
 
 
-import { CONFIGURABLE_OPERATION_DEF_FRAGMENT } from './shared-definitions';
+import { CONFIGURABLE_OPERATION_DEF_FRAGMENT, ERROR_RESULT_FRAGMENT } from './shared-definitions';
 
 
 export const COUNTRY_FRAGMENT = gql`
 export const COUNTRY_FRAGMENT = gql`
     fragment Country on Country {
     fragment Country on Country {
@@ -342,18 +342,22 @@ export const CREATE_CHANNEL = gql`
     mutation CreateChannel($input: CreateChannelInput!) {
     mutation CreateChannel($input: CreateChannelInput!) {
         createChannel(input: $input) {
         createChannel(input: $input) {
             ...Channel
             ...Channel
+            ...ErrorResult
         }
         }
     }
     }
     ${CHANNEL_FRAGMENT}
     ${CHANNEL_FRAGMENT}
+    ${ERROR_RESULT_FRAGMENT}
 `;
 `;
 
 
 export const UPDATE_CHANNEL = gql`
 export const UPDATE_CHANNEL = gql`
     mutation UpdateChannel($input: UpdateChannelInput!) {
     mutation UpdateChannel($input: UpdateChannelInput!) {
         updateChannel(input: $input) {
         updateChannel(input: $input) {
             ...Channel
             ...Channel
+            ...ErrorResult
         }
         }
     }
     }
     ${CHANNEL_FRAGMENT}
     ${CHANNEL_FRAGMENT}
+    ${ERROR_RESULT_FRAGMENT}
 `;
 `;
 
 
 export const DELETE_CHANNEL = gql`
 export const DELETE_CHANNEL = gql`
@@ -433,9 +437,11 @@ export const UPDATE_GLOBAL_SETTINGS = gql`
     mutation UpdateGlobalSettings($input: UpdateGlobalSettingsInput!) {
     mutation UpdateGlobalSettings($input: UpdateGlobalSettingsInput!) {
         updateGlobalSettings(input: $input) {
         updateGlobalSettings(input: $input) {
             ...GlobalSettings
             ...GlobalSettings
+            ...ErrorResult
         }
         }
     }
     }
     ${GLOBAL_SETTINGS_FRAGMENT}
     ${GLOBAL_SETTINGS_FRAGMENT}
+    ${ERROR_RESULT_FRAGMENT}
 `;
 `;
 
 
 export const CUSTOM_FIELD_CONFIG_FRAGMENT = gql`
 export const CUSTOM_FIELD_CONFIG_FRAGMENT = gql`

+ 8 - 1
packages/admin-ui/src/lib/core/src/data/definitions/shared-definitions.ts

@@ -1,4 +1,4 @@
-import gql from 'graphql-tag';
+import { gql } from 'apollo-angular';
 
 
 export const CONFIGURABLE_OPERATION_FRAGMENT = gql`
 export const CONFIGURABLE_OPERATION_FRAGMENT = gql`
     fragment ConfigurableOperation on ConfigurableOperation {
     fragment ConfigurableOperation on ConfigurableOperation {
@@ -23,3 +23,10 @@ export const CONFIGURABLE_OPERATION_DEF_FRAGMENT = gql`
         description
         description
     }
     }
 `;
 `;
+
+export const ERROR_RESULT_FRAGMENT = gql`
+    fragment ErrorResult on ErrorResult {
+        errorCode
+        message
+    }
+`;

+ 2 - 1
packages/admin-ui/src/lib/core/src/data/definitions/shipping-definitions.ts

@@ -1,4 +1,4 @@
-import gql from 'graphql-tag';
+import { gql } from 'apollo-angular';
 
 
 import { CONFIGURABLE_OPERATION_DEF_FRAGMENT, CONFIGURABLE_OPERATION_FRAGMENT } from './shared-definitions';
 import { CONFIGURABLE_OPERATION_DEF_FRAGMENT, CONFIGURABLE_OPERATION_FRAGMENT } from './shared-definitions';
 
 
@@ -86,6 +86,7 @@ export const TEST_SHIPPING_METHOD = gql`
             quote {
             quote {
                 price
                 price
                 priceWithTax
                 priceWithTax
+                description
                 metadata
                 metadata
             }
             }
         }
         }

+ 1 - 1
packages/admin-ui/src/lib/core/src/data/omit-typename-link.ts

@@ -1,5 +1,5 @@
+import { ApolloLink } from '@apollo/client/core';
 import { omit } from '@vendure/common/lib/omit';
 import { omit } from '@vendure/common/lib/omit';
-import { ApolloLink } from 'apollo-link';
 
 
 /**
 /**
  * The "__typename" property added by Apollo Client causes errors when posting the entity
  * The "__typename" property added by Apollo Client causes errors when posting the entity

+ 3 - 14
packages/admin-ui/src/lib/core/src/data/providers/base-data.service.ts

@@ -1,9 +1,7 @@
 import { HttpClient } from '@angular/common/http';
 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 import { Injectable } from '@angular/core';
+import { DataProxy, MutationUpdaterFn, WatchQueryFetchPolicy } from '@apollo/client/core';
 import { Apollo } from 'apollo-angular';
 import { Apollo } from 'apollo-angular';
-import { DataProxy } from 'apollo-cache';
-import { WatchQueryFetchPolicy } from 'apollo-client';
-import { ExecutionResult } from 'apollo-link';
 import { DocumentNode } from 'graphql/language/ast';
 import { DocumentNode } from 'graphql/language/ast';
 import { Observable } from 'rxjs';
 import { Observable } from 'rxjs';
 import { map } from 'rxjs/operators';
 import { map } from 'rxjs/operators';
@@ -18,15 +16,6 @@ import {
     removeReadonlyCustomFields,
     removeReadonlyCustomFields,
 } from '../utils/remove-readonly-custom-fields';
 } from '../utils/remove-readonly-custom-fields';
 
 
-/**
- * Make the MutationUpdaterFn type-safe until this issue is resolved: https://github.com/apollographql/apollo-link/issues/616
- */
-export type TypedFetchResult<T = Record<string, any>> = ExecutionResult & {
-    context?: T;
-    data: T;
-};
-export type TypedMutationUpdateFn<T> = (proxy: DataProxy, mutationResult: TypedFetchResult<T>) => void;
-
 @Injectable()
 @Injectable()
 export class BaseDataService {
 export class BaseDataService {
     constructor(
     constructor(
@@ -64,7 +53,7 @@ export class BaseDataService {
     mutate<T, V = Record<string, any>>(
     mutate<T, V = Record<string, any>>(
         mutation: DocumentNode,
         mutation: DocumentNode,
         variables?: V,
         variables?: V,
-        update?: TypedMutationUpdateFn<T>,
+        update?: MutationUpdaterFn<T>,
     ): Observable<T> {
     ): Observable<T> {
         const withCustomFields = addCustomFields(mutation, this.customFields);
         const withCustomFields = addCustomFields(mutation, this.customFields);
         const withoutReadonlyFields = this.removeReadonlyCustomFieldsFromVariables(mutation, variables);
         const withoutReadonlyFields = this.removeReadonlyCustomFieldsFromVariables(mutation, variables);
@@ -73,7 +62,7 @@ export class BaseDataService {
             .mutate<T, V>({
             .mutate<T, V>({
                 mutation: withCustomFields,
                 mutation: withCustomFields,
                 variables: withoutReadonlyFields,
                 variables: withoutReadonlyFields,
-                update: update as any,
+                update,
             })
             })
             .pipe(map(result => result.data as T));
             .pipe(map(result => result.data as T));
     }
     }

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

@@ -1,5 +1,5 @@
 import { Injectable } from '@angular/core';
 import { Injectable } from '@angular/core';
-import { WatchQueryFetchPolicy } from 'apollo-client';
+import { MutationUpdaterFn, WatchQueryFetchPolicy } from '@apollo/client/core';
 import { DocumentNode } from 'graphql';
 import { DocumentNode } from 'graphql';
 import { Observable } from 'rxjs';
 import { Observable } from 'rxjs';
 
 
@@ -7,7 +7,7 @@ import { QueryResult } from '../query-result';
 
 
 import { AdministratorDataService } from './administrator-data.service';
 import { AdministratorDataService } from './administrator-data.service';
 import { AuthDataService } from './auth-data.service';
 import { AuthDataService } from './auth-data.service';
-import { BaseDataService, TypedMutationUpdateFn } from './base-data.service';
+import { BaseDataService } from './base-data.service';
 import { ClientDataService } from './client-data.service';
 import { ClientDataService } from './client-data.service';
 import { CollectionDataService } from './collection-data.service';
 import { CollectionDataService } from './collection-data.service';
 import { CustomerDataService } from './customer-data.service';
 import { CustomerDataService } from './customer-data.service';
@@ -63,7 +63,7 @@ export class DataService {
     mutate<T, V = Record<string, any>>(
     mutate<T, V = Record<string, any>>(
         mutation: DocumentNode,
         mutation: DocumentNode,
         variables?: V,
         variables?: V,
-        update?: TypedMutationUpdateFn<T>,
+        update?: MutationUpdaterFn<T>,
     ): Observable<T> {
     ): Observable<T> {
         return this.baseDataService.mutate(mutation, variables, update);
         return this.baseDataService.mutate(mutation, variables, update);
     }
     }

+ 13 - 1
packages/admin-ui/src/lib/core/src/data/providers/order-data.service.ts

@@ -15,6 +15,7 @@ import {
     SettlePayment,
     SettlePayment,
     SettleRefund,
     SettleRefund,
     SettleRefundInput,
     SettleRefundInput,
+    TransitionFulfillmentToState,
     TransitionOrderToState,
     TransitionOrderToState,
     UpdateOrderCustomFields,
     UpdateOrderCustomFields,
     UpdateOrderInput,
     UpdateOrderInput,
@@ -32,6 +33,7 @@ import {
     REFUND_ORDER,
     REFUND_ORDER,
     SETTLE_PAYMENT,
     SETTLE_PAYMENT,
     SETTLE_REFUND,
     SETTLE_REFUND,
+    TRANSITION_FULFILLMENT_TO_STATE,
     TRANSITION_ORDER_TO_STATE,
     TRANSITION_ORDER_TO_STATE,
     UPDATE_ORDER_CUSTOM_FIELDS,
     UPDATE_ORDER_CUSTOM_FIELDS,
     UPDATE_ORDER_NOTE,
     UPDATE_ORDER_NOTE,
@@ -71,7 +73,7 @@ export class OrderDataService {
         });
         });
     }
     }
 
 
-    createFullfillment(input: FulfillOrderInput) {
+    createFulfillment(input: FulfillOrderInput) {
         return this.baseDataService.mutate<CreateFulfillment.Mutation, CreateFulfillment.Variables>(
         return this.baseDataService.mutate<CreateFulfillment.Mutation, CreateFulfillment.Variables>(
             CREATE_FULFILLMENT,
             CREATE_FULFILLMENT,
             {
             {
@@ -80,6 +82,16 @@ export class OrderDataService {
         );
         );
     }
     }
 
 
+    transitionFulfillmentToState(id: string, state: string) {
+        return this.baseDataService.mutate<
+            TransitionFulfillmentToState.Mutation,
+            TransitionFulfillmentToState.Variables
+        >(TRANSITION_FULFILLMENT_TO_STATE, {
+            id,
+            state,
+        });
+    }
+
     cancelOrder(input: CancelOrderInput) {
     cancelOrder(input: CancelOrderInput) {
         return this.baseDataService.mutate<CancelOrder.Mutation, CancelOrder.Variables>(CANCEL_ORDER, {
         return this.baseDataService.mutate<CancelOrder.Mutation, CancelOrder.Variables>(CANCEL_ORDER, {
             input,
             input,

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

@@ -1,5 +1,5 @@
+import { FetchPolicy } from '@apollo/client/core';
 import { pick } from '@vendure/common/lib/pick';
 import { pick } from '@vendure/common/lib/pick';
-import { FetchPolicy } from 'apollo-client';
 
 
 import {
 import {
     AddMembersToZone,
     AddMembersToZone,
@@ -74,10 +74,10 @@ import {
     GET_COUNTRY,
     GET_COUNTRY,
     GET_COUNTRY_LIST,
     GET_COUNTRY_LIST,
     GET_GLOBAL_SETTINGS,
     GET_GLOBAL_SETTINGS,
-    GET_JOBS_BY_ID,
-    GET_JOBS_LIST,
     GET_JOB_INFO,
     GET_JOB_INFO,
     GET_JOB_QUEUE_LIST,
     GET_JOB_QUEUE_LIST,
+    GET_JOBS_BY_ID,
+    GET_JOBS_LIST,
     GET_PAYMENT_METHOD,
     GET_PAYMENT_METHOD,
     GET_PAYMENT_METHOD_LIST,
     GET_PAYMENT_METHOD_LIST,
     GET_TAX_CATEGORIES,
     GET_TAX_CATEGORIES,

+ 1 - 1
packages/admin-ui/src/lib/core/src/data/query-result.ts

@@ -1,6 +1,6 @@
+import { ApolloQueryResult, NetworkStatus } from '@apollo/client/core';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { Apollo, QueryRef } from 'apollo-angular';
 import { Apollo, QueryRef } from 'apollo-angular';
-import { ApolloQueryResult, NetworkStatus } from 'apollo-client';
 import { merge, Observable, Subject } from 'rxjs';
 import { merge, Observable, Subject } from 'rxjs';
 import { distinctUntilChanged, filter, finalize, map, skip, take, takeUntil, tap } from 'rxjs/operators';
 import { distinctUntilChanged, filter, finalize, map, skip, take, takeUntil, tap } from 'rxjs/operators';
 
 

+ 1 - 1
packages/admin-ui/src/lib/core/src/data/server-config.ts

@@ -1,5 +1,5 @@
 import { Injectable, Injector } from '@angular/core';
 import { Injectable, Injector } from '@angular/core';
-import gql from 'graphql-tag';
+import { gql } from 'apollo-angular';
 
 
 import {
 import {
     CustomFieldConfig,
     CustomFieldConfig,

+ 20 - 8
packages/admin-ui/src/lib/core/src/providers/auth/auth.service.ts

@@ -1,9 +1,14 @@
 import { Injectable } from '@angular/core';
 import { Injectable } from '@angular/core';
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import { Observable, of } from 'rxjs';
 import { Observable, of } from 'rxjs';
-import { catchError, mapTo, mergeMap, switchMap } from 'rxjs/operators';
+import { catchError, map, mapTo, mergeMap, switchMap } from 'rxjs/operators';
 
 
-import { CurrentUserChannel, CurrentUserFragment, SetAsLoggedIn } from '../../common/generated-types';
+import {
+    AttemptLogin,
+    CurrentUserChannel,
+    CurrentUserFragment,
+    SetAsLoggedIn,
+} from '../../common/generated-types';
 import { DataService } from '../../data/providers/data.service';
 import { DataService } from '../../data/providers/data.service';
 import { ServerConfigService } from '../../data/server-config';
 import { ServerConfigService } from '../../data/server-config';
 import { LocalStorageService } from '../local-storage/local-storage.service';
 import { LocalStorageService } from '../local-storage/local-storage.service';
@@ -25,15 +30,22 @@ export class AuthService {
      * Attempts to log in via the REST login endpoint and updates the app
      * Attempts to log in via the REST login endpoint and updates the app
      * state on success.
      * state on success.
      */
      */
-    logIn(username: string, password: string, rememberMe: boolean): Observable<SetAsLoggedIn.Mutation> {
+    logIn(username: string, password: string, rememberMe: boolean): Observable<AttemptLogin.Login> {
         return this.dataService.auth.attemptLogin(username, password, rememberMe).pipe(
         return this.dataService.auth.attemptLogin(username, password, rememberMe).pipe(
             switchMap(response => {
             switchMap(response => {
-                this.setChannelToken(response.login.user.channels);
-                return this.serverConfigService.getServerConfig().then(() => response.login.user);
+                if (response.login.__typename === 'CurrentUser') {
+                    this.setChannelToken(response.login.channels);
+                }
+                return this.serverConfigService.getServerConfig().then(() => response.login);
             }),
             }),
-            switchMap(user => {
-                const { id } = this.getActiveChannel(user.channels);
-                return this.dataService.client.loginSuccess(username, id, user.channels);
+            switchMap(login => {
+                if (login.__typename === 'CurrentUser') {
+                    const { id } = this.getActiveChannel(login.channels);
+                    return this.dataService.client
+                        .loginSuccess(username, id, login.channels)
+                        .pipe(map(() => login));
+                }
+                return of(login);
             }),
             }),
         );
         );
     }
     }

+ 2 - 2
packages/admin-ui/src/lib/core/src/shared/components/order-state-label/order-state-label.component.html

@@ -1,6 +1,6 @@
 <vdr-chip [ngClass]="state" [colorType]="chipColorType">
 <vdr-chip [ngClass]="state" [colorType]="chipColorType">
-    <clr-icon shape="success-standard" *ngIf="state === 'Fulfilled'" size="12"></clr-icon>
-    <clr-icon shape="success-standard" *ngIf="state === 'PartiallyFulfilled'" size="12"></clr-icon>
+    <clr-icon shape="success-standard" *ngIf="state === 'Delivered'" size="12"></clr-icon>
+    <clr-icon shape="success-standard" *ngIf="state === 'PartiallyDelivered'" size="12"></clr-icon>
     <clr-icon shape="ban" *ngIf="state === 'Cancelled'" size="12"></clr-icon>
     <clr-icon shape="ban" *ngIf="state === 'Cancelled'" size="12"></clr-icon>
     {{ state | orderStateI18nToken | translate }}
     {{ state | orderStateI18nToken | translate }}
     <ng-content></ng-content>
     <ng-content></ng-content>

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

@@ -13,9 +13,11 @@ export class OrderStateLabelComponent {
         switch (this.state) {
         switch (this.state) {
             case 'PaymentAuthorized':
             case 'PaymentAuthorized':
             case 'PaymentSettled':
             case 'PaymentSettled':
-            case 'PartiallyFulfilled':
+            case 'PartiallyDelivered':
+            case 'PartiallyShipped':
+            case 'Shipped':
                 return 'warning';
                 return 'warning';
-            case 'Fulfilled':
+            case 'Delivered':
                 return 'success';
                 return 'success';
             case 'Cancelled':
             case 'Cancelled':
                 return 'error';
                 return 'error';

+ 0 - 1
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/menu/images.ts

@@ -17,7 +17,6 @@ export function insertImageItem(nodeType: NodeType, modalService: ModalService)
         label: 'Image',
         label: 'Image',
         class: '',
         class: '',
         css: '',
         css: '',
-        execEvent: 'mousedown',
         enable(state: EditorState) {
         enable(state: EditorState) {
             return canInsert(state, nodeType);
             return canInsert(state, nodeType);
         },
         },

+ 0 - 1
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/menu/links.ts

@@ -30,7 +30,6 @@ export function linkItem(linkMark: MarkType, modalService: ModalService) {
         icon: icons.link,
         icon: icons.link,
         class: '',
         class: '',
         css: '',
         css: '',
-        execEvent: 'mousedown',
         active(state) {
         active(state) {
             return markActive(state, linkMark);
             return markActive(state, linkMark);
         },
         },

+ 0 - 1
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/menu/menu.ts

@@ -182,7 +182,6 @@ export function buildMenuItems(schema: Schema, modalService: ModalService) {
             label: 'Horizontal rule',
             label: 'Horizontal rule',
             class: '',
             class: '',
             css: '',
             css: '',
-            execEvent: 'mousedown',
             enable(state) {
             enable(state) {
                 return canInsert(state, hr);
                 return canInsert(state, hr);
             },
             },

+ 9 - 3
packages/admin-ui/src/lib/core/src/shared/components/timeline-entry/timeline-entry.component.html

@@ -1,8 +1,14 @@
-<div [ngClass]="displayType" [class.has-custom-icon]="!!iconShape" class="entry" [class.last]="isLast === true">
-    <div class="timeline">
+<div
+    [ngClass]="displayType"
+    [class.has-custom-icon]="!!iconShape"
+    class="entry"
+    [class.last]="isLast === true"
+    [class.collapsed]="collapsed"
+>
+    <div class="timeline" (click)="expandClick.emit()" [title]="timelineTitle | translate">
         <div class="custom-icon">
         <div class="custom-icon">
             <clr-icon
             <clr-icon
-                *ngIf="iconShape"
+                *ngIf="iconShape && !collapsed"
                 [attr.shape]="getIconShape()"
                 [attr.shape]="getIconShape()"
                 [ngClass]="getIconClass()"
                 [ngClass]="getIconClass()"
                 size="24"
                 size="24"

+ 39 - 5
packages/admin-ui/src/lib/core/src/shared/components/timeline-entry/timeline-entry.component.scss

@@ -1,18 +1,32 @@
 @import "variables";
 @import "variables";
 
 
+:host {
+    display: block;
+
+    &:first-of-type {
+        .timeline {
+            &:before {
+                border-left-color: $color-grey-100;
+            }
+        }
+        .entry-body {
+            max-height: initial;
+        }
+    }
+}
 .entry {
 .entry {
     display: flex;
     display: flex;
 }
 }
 .timeline {
 .timeline {
     border-left: 2px solid $color-primary-100;
     border-left: 2px solid $color-primary-100;
-    padding-bottom: 24px;
+    padding-bottom: 8px;
     position: relative;
     position: relative;
 
 
     &:before {
     &:before {
         content: '';
         content: '';
         position: absolute;
         position: absolute;
         width: 2px;
         width: 2px;
-        height: 10px;
+        height: 32px;
         left: -2px;
         left: -2px;
         border-left: 2px solid $color-primary-100;
         border-left: 2px solid $color-primary-100;
     }
     }
@@ -26,7 +40,9 @@
         border: 1px solid $color-grey-400;
         border: 1px solid $color-grey-400;
         position: absolute;
         position: absolute;
         left: -5px;
         left: -5px;
-        top: 4px;
+        top: 32px;
+        transition: top 0.2s;
+        cursor: pointer;
     }
     }
 
 
     .custom-icon {
     .custom-icon {
@@ -34,7 +50,7 @@
         width: 32px;
         width: 32px;
         height: 32px;
         height: 32px;
         left: -17px;
         left: -17px;
-        top: -4px;
+        top: 32px;
         align-items: center;
         align-items: center;
         justify-content: center;
         justify-content: center;
         border-radius: 50%;
         border-radius: 50%;
@@ -60,11 +76,14 @@
 
 
 .entry-body {
 .entry-body {
     flex: 1;
     flex: 1;
-    padding-bottom: 24px;
+    padding-top: 24px;
     padding-left: 12px;
     padding-left: 12px;
     line-height: 16px;
     line-height: 16px;
     margin-left: 12px;
     margin-left: 12px;
     color: $color-grey-500;
     color: $color-grey-500;
+    overflow: visible;
+    max-height: 100px;
+    transition: max-height 0.2s, padding-top 0.2s, opacity 0.2s 0.2s;
 }
 }
 
 
 .featured-entry ::ng-deep {
 .featured-entry ::ng-deep {
@@ -126,3 +145,18 @@
         border: 1px solid $color-warning-400;
         border: 1px solid $color-warning-400;
     }
     }
 }
 }
+
+.collapsed {
+    .entry-body {
+        max-height: 0;
+        overflow: hidden;
+        opacity: 0;
+        padding-top: 0;
+    }
+    .timeline {
+        border-left-color: transparent;
+    }
+    .timeline:after {
+        top: 0;
+    }
+}

+ 18 - 1
packages/admin-ui/src/lib/core/src/shared/components/timeline-entry/timeline-entry.component.ts

@@ -1,4 +1,13 @@
-import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
+import {
+    ChangeDetectionStrategy,
+    Component,
+    EventEmitter,
+    HostBinding,
+    Input,
+    OnInit,
+    Output,
+} from '@angular/core';
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 
 
 export type TimelineDisplayType = 'success' | 'error' | 'warning' | 'default' | 'muted';
 export type TimelineDisplayType = 'success' | 'error' | 'warning' | 'default' | 'muted';
 
 
@@ -15,6 +24,14 @@ export class TimelineEntryComponent {
     @Input() featured: boolean;
     @Input() featured: boolean;
     @Input() iconShape?: string | [string, string];
     @Input() iconShape?: string | [string, string];
     @Input() isLast?: boolean;
     @Input() isLast?: boolean;
+    @HostBinding('class.collapsed')
+    @Input()
+    collapsed = false;
+    @Output() expandClick = new EventEmitter();
+
+    get timelineTitle(): string {
+        return this.collapsed ? _('common.expand-entries') : _('common.collapse-entries');
+    }
 
 
     getIconShape() {
     getIconShape() {
         if (this.iconShape) {
         if (this.iconShape) {

+ 6 - 5
packages/admin-ui/src/lib/core/src/shared/directives/if-directive-base.ts

@@ -1,12 +1,13 @@
-import { EmbeddedViewRef, OnDestroy, OnInit, TemplateRef, ViewContainerRef } from '@angular/core';
-import { BehaviorSubject, combineLatest, Observable, Subject, Subscription } from 'rxjs';
-import { switchMap, take } from 'rxjs/operators';
-
-import { Permission } from '../../common/generated-types';
+import { Directive, EmbeddedViewRef, OnDestroy, OnInit, TemplateRef, ViewContainerRef } from '@angular/core';
+import { BehaviorSubject, Observable, Subscription } from 'rxjs';
+import { switchMap } from 'rxjs/operators';
 
 
 /**
 /**
  * A base class for implementing custom *ngIf-style structural directives based on custom conditions.
  * A base class for implementing custom *ngIf-style structural directives based on custom conditions.
+ *
+ * @dynamic
  */
  */
+@Directive()
 export class IfDirectiveBase<Args extends any[]> implements OnInit, OnDestroy {
 export class IfDirectiveBase<Args extends any[]> implements OnInit, OnDestroy {
     protected updateArgs$ = new BehaviorSubject<Args>([] as any);
     protected updateArgs$ = new BehaviorSubject<Args>([] as any);
     private readonly _thenTemplateRef: TemplateRef<any> | null = null;
     private readonly _thenTemplateRef: TemplateRef<any> | null = null;

+ 4 - 2
packages/admin-ui/src/lib/core/src/shared/pipes/order-state-i18n-token.pipe.ts

@@ -10,8 +10,10 @@ export class OrderStateI18nTokenPipe implements PipeTransform {
         ArrangingPayment: _('order.state-arranging-payment'),
         ArrangingPayment: _('order.state-arranging-payment'),
         PaymentAuthorized: _('order.state-payment-authorized'),
         PaymentAuthorized: _('order.state-payment-authorized'),
         PaymentSettled: _('order.state-payment-settled'),
         PaymentSettled: _('order.state-payment-settled'),
-        PartiallyFulfilled: _('order.state-partially-fulfilled'),
-        Fulfilled: _('order.state-fulfilled'),
+        PartiallyShipped: _('order.state-partially-shipped'),
+        Shipped: _('order.state-shipped'),
+        PartiallyDelivered: _('order.state-partially-delivered'),
+        Delivered: _('order.state-delivered'),
         Cancelled: _('order.state-cancelled'),
         Cancelled: _('order.state-cancelled'),
     };
     };
     transform<T extends unknown>(value: T): T {
     transform<T extends unknown>(value: T): T {

+ 66 - 37
packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.ts

@@ -5,6 +5,8 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
 import {
     BaseDetailComponent,
     BaseDetailComponent,
     CreateAddressInput,
     CreateAddressInput,
+    CreateCustomerAddress,
+    CreateCustomerAddressMutation,
     CreateCustomerInput,
     CreateCustomerInput,
     Customer,
     Customer,
     CustomFieldConfig,
     CustomFieldConfig,
@@ -19,7 +21,11 @@ import {
     NotificationService,
     NotificationService,
     ServerConfigService,
     ServerConfigService,
     SortOrder,
     SortOrder,
+    UpdateCustomer,
+    UpdateCustomerAddress,
+    UpdateCustomerAddressMutation,
     UpdateCustomerInput,
     UpdateCustomerInput,
+    UpdateCustomerMutation,
 } from '@vendure/admin-ui/core';
 } from '@vendure/admin-ui/core';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { EMPTY, forkJoin, from, Observable, Subject } from 'rxjs';
 import { EMPTY, forkJoin, from, Observable, Subject } from 'rxjs';
@@ -190,30 +196,31 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
             phoneNumber: formValue.phoneNumber,
             phoneNumber: formValue.phoneNumber,
             customFields,
             customFields,
         };
         };
-        this.dataService.customer.createCustomer(customer, formValue.password).subscribe(
-            data => {
-                this.notificationService.success(_('common.notify-create-success'), {
-                    entity: 'Customer',
-                });
-                if (data.createCustomer.emailAddress && !formValue.password) {
-                    this.notificationService.notify({
-                        message: _('customer.email-verification-sent'),
-                        translationVars: { emailAddress: formValue.emailAddress },
-                        type: 'info',
-                        duration: 10000,
-                    });
+        this.dataService.customer
+            .createCustomer(customer, formValue.password)
+            .subscribe(({ createCustomer }) => {
+                switch (createCustomer.__typename) {
+                    case 'Customer':
+                        this.notificationService.success(_('common.notify-create-success'), {
+                            entity: 'Customer',
+                        });
+                        if (createCustomer.emailAddress && !formValue.password) {
+                            this.notificationService.notify({
+                                message: _('customer.email-verification-sent'),
+                                translationVars: { emailAddress: formValue.emailAddress },
+                                type: 'info',
+                                duration: 10000,
+                            });
+                        }
+                        this.detailForm.markAsPristine();
+                        this.addressDefaultsUpdated = false;
+                        this.changeDetector.markForCheck();
+                        this.router.navigate(['../', createCustomer.id], { relativeTo: this.route });
+                        break;
+                    case 'EmailAddressConflictError':
+                        this.notificationService.error(createCustomer.message);
                 }
                 }
-                this.detailForm.markAsPristine();
-                this.addressDefaultsUpdated = false;
-                this.changeDetector.markForCheck();
-                this.router.navigate(['../', data.createCustomer.id], { relativeTo: this.route });
-            },
-            err => {
-                this.notificationService.error(_('common.notify-create-error'), {
-                    entity: 'Customer',
-                });
-            },
-        );
+            });
     }
     }
 
 
     save() {
     save() {
@@ -221,7 +228,11 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
             .pipe(
             .pipe(
                 take(1),
                 take(1),
                 mergeMap(({ id }) => {
                 mergeMap(({ id }) => {
-                    const saveOperations: Array<Observable<any>> = [];
+                    const saveOperations: Array<Observable<
+                        | UpdateCustomer.UpdateCustomer
+                        | CreateCustomerAddress.CreateCustomerAddress
+                        | UpdateCustomerAddress.UpdateCustomerAddress
+                    >> = [];
                     const customerForm = this.detailForm.get('customer');
                     const customerForm = this.detailForm.get('customer');
                     if (customerForm && customerForm.dirty) {
                     if (customerForm && customerForm.dirty) {
                         const formValue = customerForm.value;
                         const formValue = customerForm.value;
@@ -235,7 +246,11 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
                             phoneNumber: formValue.phoneNumber,
                             phoneNumber: formValue.phoneNumber,
                             customFields,
                             customFields,
                         };
                         };
-                        saveOperations.push(this.dataService.customer.updateCustomer(customer));
+                        saveOperations.push(
+                            this.dataService.customer
+                                .updateCustomer(customer)
+                                .pipe(map(res => res.updateCustomer)),
+                        );
                     }
                     }
                     const addressFormArray = this.detailForm.get('addresses') as FormArray;
                     const addressFormArray = this.detailForm.get('addresses') as FormArray;
                     if ((addressFormArray && addressFormArray.dirty) || this.addressDefaultsUpdated) {
                     if ((addressFormArray && addressFormArray.dirty) || this.addressDefaultsUpdated) {
@@ -258,14 +273,18 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
                                 };
                                 };
                                 if (!address.id) {
                                 if (!address.id) {
                                     saveOperations.push(
                                     saveOperations.push(
-                                        this.dataService.customer.createCustomerAddress(id, input),
+                                        this.dataService.customer
+                                            .createCustomerAddress(id, input)
+                                            .pipe(map(res => res.createCustomerAddress)),
                                     );
                                     );
                                 } else {
                                 } else {
                                     saveOperations.push(
                                     saveOperations.push(
-                                        this.dataService.customer.updateCustomerAddress({
-                                            ...input,
-                                            id: address.id,
-                                        }),
+                                        this.dataService.customer
+                                            .updateCustomerAddress({
+                                                ...input,
+                                                id: address.id,
+                                            })
+                                            .pipe(map(res => res.updateCustomerAddress)),
                                     );
                                     );
                                 }
                                 }
                             }
                             }
@@ -276,13 +295,23 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
             )
             )
             .subscribe(
             .subscribe(
                 data => {
                 data => {
-                    this.notificationService.success(_('common.notify-update-success'), {
-                        entity: 'Customer',
-                    });
-                    this.detailForm.markAsPristine();
-                    this.addressDefaultsUpdated = false;
-                    this.changeDetector.markForCheck();
-                    this.fetchHistory.next();
+                    for (const result of data) {
+                        switch (result.__typename) {
+                            case 'Customer':
+                            case 'Address':
+                                this.notificationService.success(_('common.notify-update-success'), {
+                                    entity: 'Customer',
+                                });
+                                this.detailForm.markAsPristine();
+                                this.addressDefaultsUpdated = false;
+                                this.changeDetector.markForCheck();
+                                this.fetchHistory.next();
+                                break;
+                            case 'EmailAddressConflictError':
+                                this.notificationService.error(result.message);
+                                break;
+                        }
+                    }
                 },
                 },
                 err => {
                 err => {
                     this.notificationService.error(_('common.notify-update-error'), {
                     this.notificationService.error(_('common.notify-update-error'), {

+ 7 - 0
packages/admin-ui/src/lib/login/src/components/login/login.component.html

@@ -36,6 +36,13 @@
             >
             >
                 {{ 'common.login' | translate }}
                 {{ 'common.login' | translate }}
             </button>
             </button>
+            <clr-alert [clrAlertType]="'danger'" *ngIf="errorMessage">
+                <clr-alert-item>
+                    <span class="alert-text">
+                        {{ errorMessage }}
+                    </span>
+                </clr-alert-item>
+            </clr-alert>
         </div>
         </div>
         <div class="version">vendure {{ version }}</div>
         <div class="version">vendure {{ version }}</div>
     </form>
     </form>

+ 15 - 10
packages/admin-ui/src/lib/login/src/components/login/login.component.ts

@@ -12,23 +12,28 @@ export class LoginComponent {
     password = '';
     password = '';
     rememberMe = false;
     rememberMe = false;
     version = ADMIN_UI_VERSION;
     version = ADMIN_UI_VERSION;
+    errorMessage: string | undefined;
 
 
     constructor(private authService: AuthService, private router: Router) {}
     constructor(private authService: AuthService, private router: Router) {}
 
 
     logIn(): void {
     logIn(): void {
-        this.authService.logIn(this.username, this.password, this.rememberMe).subscribe(
-            () => {
-                const redirect = this.getRedirectRoute();
-                this.router.navigateByUrl(redirect ? redirect : '/');
-            },
-            err => {
-                /* error handled by http interceptor */
-            },
-        );
+        this.errorMessage = undefined;
+        this.authService.logIn(this.username, this.password, this.rememberMe).subscribe(result => {
+            switch (result.__typename) {
+                case 'CurrentUser':
+                    const redirect = this.getRedirectRoute();
+                    this.router.navigateByUrl(redirect ? redirect : '/');
+                    break;
+                case 'InvalidCredentialsError':
+                case 'NativeAuthStrategyError':
+                    this.errorMessage = result.message;
+                    break;
+            }
+        });
     }
     }
 
 
     /**
     /**
-     * Attemps to read a redirect param from the current url and parse it into a
+     * Attempts to read a redirect param from the current url and parse it into a
      * route from which the user was redirected after a 401 error.
      * route from which the user was redirected after a 401 error.
      */
      */
     private getRedirectRoute(): string | undefined {
     private getRedirectRoute(): string | undefined {

+ 14 - 5
packages/admin-ui/src/lib/marketing/src/components/promotion-detail/promotion-detail.component.ts

@@ -138,11 +138,20 @@ export class PromotionDetailComponent extends BaseDetailComponent<Promotion.Frag
             actions: this.mapOperationsToInputs(this.actions, formValue.actions),
             actions: this.mapOperationsToInputs(this.actions, formValue.actions),
         };
         };
         this.dataService.promotion.createPromotion(input).subscribe(
         this.dataService.promotion.createPromotion(input).subscribe(
-            data => {
-                this.notificationService.success(_('common.notify-create-success'), { entity: 'Promotion' });
-                this.detailForm.markAsPristine();
-                this.changeDetector.markForCheck();
-                this.router.navigate(['../', data.createPromotion.id], { relativeTo: this.route });
+            ({ createPromotion }) => {
+                switch (createPromotion.__typename) {
+                    case 'Promotion':
+                        this.notificationService.success(_('common.notify-create-success'), {
+                            entity: 'Promotion',
+                        });
+                        this.detailForm.markAsPristine();
+                        this.changeDetector.markForCheck();
+                        this.router.navigate(['../', createPromotion.id], { relativeTo: this.route });
+                        break;
+                    case 'MissingConditionsError':
+                        this.notificationService.error(createPromotion.message);
+                        break;
+                }
             },
             },
             err => {
             err => {
                 this.notificationService.error(_('common.notify-create-error'), {
                 this.notificationService.error(_('common.notify-create-error'), {

+ 46 - 0
packages/admin-ui/src/lib/order/src/components/fulfillment-card/fulfillment-card.component.html

@@ -0,0 +1,46 @@
+<div class="card">
+    <div class="card-header fulfillment-header">
+        <div>{{ 'order.fulfillment' | translate }}</div>
+        <div class="fulfillment-state">
+            <vdr-fulfillment-state-label [state]="fulfillment?.state"></vdr-fulfillment-state-label>
+        </div>
+    </div>
+    <div class="card-block">
+        <vdr-fulfillment-detail
+            *ngIf="!!fulfillment"
+            [fulfillmentId]="fulfillment?.id"
+            [order]="order"
+        ></vdr-fulfillment-detail>
+    </div>
+    <div class="card-footer">
+        <ng-container *ngIf="nextSuggestedState() as suggestedState">
+            <button class="btn btn-sm btn-primary" (click)="transitionState.emit(suggestedState)">
+                {{ 'order.set-fulfillment-state' | translate: { state: suggestedState } }}
+            </button>
+        </ng-container>
+        <vdr-dropdown>
+            <button class="icon-button" vdrDropdownTrigger>
+                <clr-icon shape="ellipsis-vertical"></clr-icon>
+            </button>
+            <vdr-dropdown-menu vdrPosition="bottom-right">
+                <ng-container *ngFor="let nextState of nextOtherStates()">
+                    <button
+                        type="button"
+                        class="btn"
+                        vdrDropdownItem
+                        (click)="transitionState.emit(nextState)"
+                    >
+                        <ng-container *ngIf="nextState !== 'Cancelled'; else cancel">
+                            <clr-icon shape="step-forward-2"></clr-icon>
+                            {{ 'order.transition-to-state' | translate: { state: nextState } }}
+                        </ng-container>
+                        <ng-template #cancel>
+                            <clr-icon shape="error-standard" class="is-error"></clr-icon>
+                            {{ 'order.cancel-fulfillment' | translate }}
+                        </ng-template>
+                    </button>
+                </ng-container>
+            </vdr-dropdown-menu>
+        </vdr-dropdown>
+    </div>
+</div>

+ 12 - 0
packages/admin-ui/src/lib/order/src/components/fulfillment-card/fulfillment-card.component.scss

@@ -0,0 +1,12 @@
+
+.fulfillment-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.card-footer {
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+}

+ 39 - 0
packages/admin-ui/src/lib/order/src/components/fulfillment-card/fulfillment-card.component.ts

@@ -0,0 +1,39 @@
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
+import { Fulfillment, OrderDetail } from '@vendure/admin-ui/core';
+
+@Component({
+    selector: 'vdr-fulfillment-card',
+    templateUrl: './fulfillment-card.component.html',
+    styleUrls: ['./fulfillment-card.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class FulfillmentCardComponent {
+    @Input() fulfillment: Fulfillment.Fragment | undefined;
+    @Input() order: OrderDetail.Fragment;
+    @Output() transitionState = new EventEmitter<string>();
+
+    nextSuggestedState(): string | undefined {
+        if (!this.fulfillment) {
+            return;
+        }
+        const { nextStates } = this.fulfillment;
+        const namedStateOrDefault = (targetState: string) =>
+            nextStates.includes(targetState) ? targetState : nextStates[0];
+        switch (this.fulfillment?.state) {
+            case 'Pending':
+                return namedStateOrDefault('Shipped');
+            case 'Shipped':
+                return namedStateOrDefault('Delivered');
+            default:
+                return nextStates.find(s => s !== 'Cancelled');
+        }
+    }
+
+    nextOtherStates(): string[] {
+        if (!this.fulfillment) {
+            return [];
+        }
+        const suggested = this.nextSuggestedState();
+        return this.fulfillment.nextStates.filter(s => s !== suggested);
+    }
+}

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

@@ -1,5 +1,4 @@
 import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
 import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
-
 import { OrderDetail } from '@vendure/admin-ui/core';
 import { OrderDetail } from '@vendure/admin-ui/core';
 
 
 @Component({
 @Component({

+ 4 - 0
packages/admin-ui/src/lib/order/src/components/fulfillment-state-label/fulfillment-state-label.component.html

@@ -0,0 +1,4 @@
+<vdr-chip [title]="'order.payment-state' | translate" [colorType]="chipColorType">
+    <clr-icon shape="check-circle" *ngIf="state === 'Delivered'"></clr-icon>
+    {{ state }}
+</vdr-chip>

+ 3 - 0
packages/admin-ui/src/lib/order/src/components/fulfillment-state-label/fulfillment-state-label.component.scss

@@ -0,0 +1,3 @@
+:host {
+    font-size: 14px;
+}

+ 23 - 0
packages/admin-ui/src/lib/order/src/components/fulfillment-state-label/fulfillment-state-label.component.ts

@@ -0,0 +1,23 @@
+import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
+
+@Component({
+    selector: 'vdr-fulfillment-state-label',
+    templateUrl: './fulfillment-state-label.component.html',
+    styleUrls: ['./fulfillment-state-label.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class FulfillmentStateLabelComponent {
+    @Input() state: string;
+
+    get chipColorType() {
+        switch (this.state) {
+            case 'Pending':
+            case 'Shipped':
+                return 'warning';
+            case 'Delivered':
+                return 'success';
+            case 'Cancelled':
+                return 'error';
+        }
+    }
+}

+ 1 - 1
packages/admin-ui/src/lib/order/src/components/line-fulfillment/line-fulfillment.component.html

@@ -1,4 +1,4 @@
-<vdr-dropdown class="search-settings-menu" *ngIf="fulfilledCount || orderState === 'PartiallyFulfilled'">
+<vdr-dropdown class="search-settings-menu" *ngIf="fulfilledCount || orderState === 'PartiallyDelivered'">
     <button type="button" class="icon-button" vdrDropdownTrigger>
     <button type="button" class="icon-button" vdrDropdownTrigger>
         <clr-icon *ngIf="fulfillmentStatus === 'full'" class="item-fulfilled" shape="check-circle"></clr-icon>
         <clr-icon *ngIf="fulfillmentStatus === 'full'" class="item-fulfilled" shape="check-circle"></clr-icon>
         <clr-icon
         <clr-icon

+ 6 - 10
packages/admin-ui/src/lib/order/src/components/line-fulfillment/line-fulfillment.component.ts

@@ -1,7 +1,6 @@
 import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
 import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
-import { unique } from '@vendure/common/lib/unique';
-
 import { OrderDetail } from '@vendure/admin-ui/core';
 import { OrderDetail } from '@vendure/admin-ui/core';
+import { unique } from '@vendure/common/lib/unique';
 
 
 export type FulfillmentStatus = 'full' | 'partial' | 'none';
 export type FulfillmentStatus = 'full' | 'partial' | 'none';
 
 
@@ -20,7 +19,7 @@ export class LineFulfillmentComponent implements OnChanges {
 
 
     ngOnChanges(changes: SimpleChanges): void {
     ngOnChanges(changes: SimpleChanges): void {
         if (this.line) {
         if (this.line) {
-            this.fulfilledCount = this.getFulfilledCount(this.line);
+            this.fulfilledCount = this.getDeliveredCount(this.line);
             this.fulfillmentStatus = this.getFulfillmentStatus(this.fulfilledCount, this.line.items.length);
             this.fulfillmentStatus = this.getFulfillmentStatus(this.fulfilledCount, this.line.items.length);
             this.fulfillments = this.getFulfillments(this.line);
             this.fulfillments = this.getFulfillments(this.line);
         }
         }
@@ -29,7 +28,7 @@ export class LineFulfillmentComponent implements OnChanges {
     /**
     /**
      * Returns the number of items in an OrderLine which are fulfilled.
      * Returns the number of items in an OrderLine which are fulfilled.
      */
      */
-    private getFulfilledCount(line: OrderDetail.Lines): number {
+    private getDeliveredCount(line: OrderDetail.Lines): number {
         return line.items.reduce((sum, item) => sum + (item.fulfillment ? 1 : 0), 0);
         return line.items.reduce((sum, item) => sum + (item.fulfillment ? 1 : 0), 0);
     }
     }
 
 
@@ -57,12 +56,9 @@ export class LineFulfillmentComponent implements OnChanges {
                 }
                 }
             }
             }
         }
         }
-        const all = line.items.reduce(
-            (fulfillments, item) => {
-                return item.fulfillment ? [...fulfillments, item.fulfillment] : fulfillments;
-            },
-            [] as OrderDetail.Fulfillments[],
-        );
+        const all = line.items.reduce((fulfillments, item) => {
+            return item.fulfillment ? [...fulfillments, item.fulfillment] : fulfillments;
+        }, [] as OrderDetail.Fulfillments[]);
 
 
         return Object.entries(counts).map(([id, count]) => {
         return Object.entries(counts).map(([id, count]) => {
             return {
             return {

+ 15 - 19
packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.html

@@ -3,8 +3,12 @@
         <div class="flex clr-align-items-center">
         <div class="flex clr-align-items-center">
             <vdr-entity-info [entity]="entity$ | async"></vdr-entity-info>
             <vdr-entity-info [entity]="entity$ | async"></vdr-entity-info>
             <vdr-order-state-label [state]="order.state">
             <vdr-order-state-label [state]="order.state">
-                <button class="icon-button" (click)="openStateDiagram()" [title]="'order.order-state-diagram' | translate">
-                <clr-icon shape="list"></clr-icon>
+                <button
+                    class="icon-button"
+                    (click)="openStateDiagram()"
+                    [title]="'order.order-state-diagram' | translate"
+                >
+                    <clr-icon shape="list"></clr-icon>
                 </button>
                 </button>
             </vdr-order-state-label>
             </vdr-order-state-label>
         </div>
         </div>
@@ -15,7 +19,7 @@
         <button
         <button
             class="btn btn-primary"
             class="btn btn-primary"
             (click)="fulfillOrder()"
             (click)="fulfillOrder()"
-            [disabled]="order.state !== 'PaymentSettled' && order.state !== 'PartiallyFulfilled'"
+            [disabled]="!canAddFulfillment(order)"
         >
         >
             {{ 'order.fulfill-order' | translate }}
             {{ 'order.fulfill-order' | translate }}
         </button>
         </button>
@@ -258,28 +262,20 @@
                 </div>
                 </div>
             </div>
             </div>
             <ng-container *ngIf="order.payments && order.payments.length">
             <ng-container *ngIf="order.payments && order.payments.length">
-                <vdr-order-payment-detail
+                <vdr-order-payment-card
                     *ngFor="let payment of order.payments"
                     *ngFor="let payment of order.payments"
                     [currencyCode]="order.currencyCode"
                     [currencyCode]="order.currencyCode"
                     [payment]="payment"
                     [payment]="payment"
                     (settlePayment)="settlePayment($event)"
                     (settlePayment)="settlePayment($event)"
                     (settleRefund)="settleRefund($event)"
                     (settleRefund)="settleRefund($event)"
-                ></vdr-order-payment-detail>
+                ></vdr-order-payment-card>
             </ng-container>
             </ng-container>
-            <ng-container *ngIf="order.fulfillments && order.fulfillments.length">
-                <div class="card">
-                    <div class="card-header">
-                        {{ 'order.fulfillment' | translate }}
-                    </div>
-                    <div class="card-block">
-                        <div class="fulfillment-detail" *ngFor="let fulfillment of order.fulfillments">
-                            <vdr-fulfillment-detail
-                                [fulfillmentId]="fulfillment.id"
-                                [order]="order"
-                            ></vdr-fulfillment-detail>
-                        </div>
-                    </div>
-                </div>
+            <ng-container *ngFor="let fulfillment of order.fulfillments">
+                <vdr-fulfillment-card
+                    [fulfillment]="fulfillment"
+                    [order]="order"
+                    (transitionState)="transitionFulfillment(fulfillment.id, $event)"
+                ></vdr-fulfillment-card>
             </ng-container>
             </ng-container>
         </div>
         </div>
     </div>
     </div>

+ 0 - 5
packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.scss

@@ -67,8 +67,3 @@
         color: $color-grey-500;
         color: $color-grey-500;
     }
     }
 }
 }
-
-.fulfillment-detail:not(:last-of-type) {
-    border-bottom: 1px dashed $color-grey-300;
-    margin-bottom: 12px;
-}

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

@@ -9,11 +9,13 @@ import {
     DataService,
     DataService,
     EditNoteDialogComponent,
     EditNoteDialogComponent,
     GetOrderHistory,
     GetOrderHistory,
+    GetOrderQuery,
     HistoryEntry,
     HistoryEntry,
     ModalService,
     ModalService,
     NotificationService,
     NotificationService,
     Order,
     Order,
     OrderDetail,
     OrderDetail,
+    OrderLineFragment,
     ServerConfigService,
     ServerConfigService,
     SortOrder,
     SortOrder,
 } from '@vendure/admin-ui/core';
 } from '@vendure/admin-ui/core';
@@ -47,8 +49,8 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
         'ArrangingPayment',
         'ArrangingPayment',
         'PaymentAuthorized',
         'PaymentAuthorized',
         'PaymentSettled',
         'PaymentSettled',
-        'PartiallyFulfilled',
-        'Fulfilled',
+        'PartiallyDelivered',
+        'Delivered',
         'Cancelled',
         'Cancelled',
     ];
     ];
     constructor(
     constructor(
@@ -132,9 +134,15 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
     }
     }
 
 
     transitionToState(state: string) {
     transitionToState(state: string) {
-        this.dataService.order.transitionToState(this.id, state).subscribe(val => {
-            this.notificationService.success(_('order.transitioned-to-state-success'), { state });
-            this.fetchHistory.next();
+        this.dataService.order.transitionToState(this.id, state).subscribe(({ transitionOrderToState }) => {
+            switch (transitionOrderToState?.__typename) {
+                case 'Order':
+                    this.notificationService.success(_('order.transitioned-to-state-success'), { state });
+                    this.fetchHistory.next();
+                    break;
+                case 'OrderStateTransitionError':
+                    this.notificationService.error(transitionOrderToState.transitionError);
+            }
         });
         });
     }
     }
 
 
@@ -171,18 +179,34 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
 
 
     settlePayment(payment: OrderDetail.Payments) {
     settlePayment(payment: OrderDetail.Payments) {
         this.dataService.order.settlePayment(payment.id).subscribe(({ settlePayment }) => {
         this.dataService.order.settlePayment(payment.id).subscribe(({ settlePayment }) => {
-            if (settlePayment) {
-                if (settlePayment.state === 'Settled') {
-                    this.notificationService.success(_('order.settle-payment-success'));
-                } else {
-                    this.notificationService.error(_('order.settle-payment-error'));
-                }
-                this.dataService.order.getOrder(this.id).single$.subscribe();
-                this.fetchHistory.next();
+            switch (settlePayment.__typename) {
+                case 'Payment':
+                    if (settlePayment.state === 'Settled') {
+                        this.notificationService.success(_('order.settle-payment-success'));
+                    } else {
+                        this.notificationService.error(_('order.settle-payment-error'));
+                    }
+                    this.dataService.order.getOrder(this.id).single$.subscribe();
+                    this.fetchHistory.next();
+                    break;
+                case 'OrderStateTransitionError':
+                case 'PaymentStateTransitionError':
+                case 'SettlePaymentError':
+                    this.notificationService.error(settlePayment.message);
             }
             }
         });
         });
     }
     }
 
 
+    canAddFulfillment(order: OrderDetail.Fragment): boolean {
+        const allItemsFulfilled = order.lines
+            .reduce((items, line) => [...items, ...line.items], [] as OrderLineFragment['items'])
+            .every(item => !!item.fulfillment);
+        return (
+            !allItemsFulfilled &&
+            (order.nextStates.includes('Shipped') || order.nextStates.includes('PartiallyShipped'))
+        );
+    }
+
     fulfillOrder() {
     fulfillOrder() {
         this.entity$
         this.entity$
             .pipe(
             .pipe(
@@ -197,7 +221,7 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
                 }),
                 }),
                 switchMap(input => {
                 switchMap(input => {
                     if (input) {
                     if (input) {
-                        return this.dataService.order.createFullfillment(input);
+                        return this.dataService.order.createFulfillment(input);
                     } else {
                     } else {
                         return of(undefined);
                         return of(undefined);
                     }
                     }
@@ -211,6 +235,15 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
             });
             });
     }
     }
 
 
+    transitionFulfillment(id: string, state: string) {
+        this.dataService.order
+            .transitionFulfillmentToState(id, state)
+            .pipe(switchMap(result => this.refetchOrder(result)))
+            .subscribe(() => {
+                this.notificationService.success(_('order.successfully-updated-fulfillment'));
+            });
+    }
+
     cancelOrRefund(order: OrderDetail.Fragment) {
     cancelOrRefund(order: OrderDetail.Fragment) {
         const isRefundable = this.orderHasSettledPayments(order);
         const isRefundable = this.orderHasSettledPayments(order);
         if (order.state === 'PaymentAuthorized' || order.active === true || !isRefundable) {
         if (order.state === 'PaymentAuthorized' || order.active === true || !isRefundable) {
@@ -358,15 +391,32 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
                 switchMap(input => {
                 switchMap(input => {
                     if (input) {
                     if (input) {
                         return this.dataService.order.refundOrder(omit(input, ['cancel'])).pipe(
                         return this.dataService.order.refundOrder(omit(input, ['cancel'])).pipe(
-                            switchMap(result => {
-                                if (input.cancel.length) {
-                                    return this.dataService.order.cancelOrder({
-                                        orderId: this.id,
-                                        lines: input.cancel,
-                                        reason: input.reason,
-                                    });
-                                } else {
-                                    return of(result);
+                            switchMap(({ refundOrder }) => {
+                                switch (refundOrder.__typename) {
+                                    case 'Refund':
+                                        if (input.cancel.length) {
+                                            return this.dataService.order
+                                                .cancelOrder({
+                                                    orderId: this.id,
+                                                    lines: input.cancel,
+                                                    reason: input.reason,
+                                                })
+                                                .pipe(map(({ cancelOrder }) => cancelOrder));
+                                        } else {
+                                            return of(refundOrder);
+                                        }
+                                    case 'AlreadyRefundedError':
+                                    case 'OrderStateTransitionError':
+                                    case 'MultipleOrderError':
+                                    case 'NothingToRefundError':
+                                    case 'PaymentOrderMismatchError':
+                                    case 'QuantityTooGreatError':
+                                    case 'RefundOrderStateError':
+                                    case 'RefundStateTransitionError':
+                                        this.notificationService.error(refundOrder.message);
+                                    // tslint:disable-next-line:no-switch-case-fall-through
+                                    default:
+                                        return of(undefined);
                                 }
                                 }
                             }),
                             }),
                         );
                         );
@@ -374,16 +424,27 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
                         return of(undefined);
                         return of(undefined);
                     }
                     }
                 }),
                 }),
-                switchMap(result => this.refetchOrder(result)),
             )
             )
             .subscribe(result => {
             .subscribe(result => {
                 if (result) {
                 if (result) {
-                    this.notificationService.success(_('order.refund-order-success'));
+                    switch (result.__typename) {
+                        case 'Order':
+                        case 'Refund':
+                            this.refetchOrder(result).subscribe();
+                            this.notificationService.success(_('order.refund-order-success'));
+                            break;
+                        case 'QuantityTooGreatError':
+                        case 'MultipleOrderError':
+                        case 'OrderStateTransitionError':
+                        case 'CancelActiveOrderError':
+                        case 'EmptyOrderLineSelectionError':
+                            this.notificationService.error(result.message);
+                    }
                 }
                 }
             });
             });
     }
     }
 
 
-    private refetchOrder(result: object | undefined) {
+    private refetchOrder(result: object | undefined): Observable<GetOrderQuery | undefined> {
         this.fetchHistory.next();
         this.fetchHistory.next();
         if (result) {
         if (result) {
             return this.dataService.order.getOrder(this.id).single$;
             return this.dataService.order.getOrder(this.id).single$;

+ 41 - 12
packages/admin-ui/src/lib/order/src/components/order-history/order-history.component.html

@@ -1,6 +1,6 @@
 <h4>{{ 'order.order-history' | translate }}</h4>
 <h4>{{ 'order.order-history' | translate }}</h4>
-<div class="entry-list">
-    <vdr-timeline-entry iconShape="note" displayType="muted">
+<div class="entry-list" [class.expanded]="expanded">
+    <vdr-timeline-entry iconShape="note" displayType="muted" [featured]="true">
         <div class="note-entry">
         <div class="note-entry">
             <textarea [(ngModel)]="note" name="note" class="note"></textarea>
             <textarea [(ngModel)]="note" name="note" class="note"></textarea>
             <button class="btn btn-secondary" [disabled]="!note" (click)="addNoteToOrder()">
             <button class="btn btn-secondary" [disabled]="!note" (click)="addNoteToOrder()">
@@ -27,16 +27,18 @@
         [createdAt]="entry.createdAt"
         [createdAt]="entry.createdAt"
         [name]="getName(entry)"
         [name]="getName(entry)"
         [featured]="isFeatured(entry)"
         [featured]="isFeatured(entry)"
+        [collapsed]="!expanded && !isFeatured(entry)"
+        (expandClick)="expanded = !expanded"
     >
     >
         <ng-container [ngSwitch]="entry.type">
         <ng-container [ngSwitch]="entry.type">
             <ng-container *ngSwitchCase="type.ORDER_STATE_TRANSITION">
             <ng-container *ngSwitchCase="type.ORDER_STATE_TRANSITION">
-                <div class="title" *ngIf="entry.data.to === 'Fulfilled'">
+                <div class="title" *ngIf="entry.data.to === 'Delivered'">
                     {{ 'order.history-order-fulfilled' | translate }}
                     {{ 'order.history-order-fulfilled' | translate }}
                 </div>
                 </div>
                 <div class="title" *ngIf="entry.data.to === 'Cancelled'">
                 <div class="title" *ngIf="entry.data.to === 'Cancelled'">
                     {{ 'order.history-order-cancelled' | translate }}
                     {{ 'order.history-order-cancelled' | translate }}
                 </div>
                 </div>
-                <ng-template [ngIf]="entry.data.to !== 'Cancelled' && entry.data.to !== 'Fulfilled'">
+                <ng-template [ngIf]="entry.data.to !== 'Cancelled' && entry.data.to !== 'Delivered'">
                     {{
                     {{
                         'order.history-order-transition'
                         'order.history-order-transition'
                             | translate: { from: entry.data.from, to: entry.data.to }
                             | translate: { from: entry.data.from, to: entry.data.to }
@@ -60,7 +62,7 @@
                     {{
                     {{
                         'order.history-payment-transition'
                         'order.history-payment-transition'
                             | translate
                             | translate
-                                : { from: entry.data.from, to: entry.data.to, id: entry.data.paymentId }
+                                : { from: entry.data.from, to: entry.data.to, id: getPayment(entry)?.transactionId }
                     }}
                     }}
                 </ng-template>
                 </ng-template>
             </ng-container>
             </ng-container>
@@ -81,12 +83,35 @@
                     </vdr-labeled-data>
                     </vdr-labeled-data>
                 </vdr-history-entry-detail>
                 </vdr-history-entry-detail>
             </ng-container>
             </ng-container>
-            <ng-container *ngSwitchCase="type.ORDER_FULLFILLMENT">
-                <div class="title">
-                    {{ 'order.history-fulfillment-created' | translate }}
-                </div>
-                {{ 'order.tracking-code' | translate }}: {{ getFullfillment(entry)?.trackingCode }}
-                <vdr-history-entry-detail *ngIf="getFullfillment(entry) as fulfillment">
+            <ng-container *ngSwitchCase="type.ORDER_FULFILLMENT">
+                {{ 'order.history-fulfillment-created' | translate }}
+                <vdr-history-entry-detail *ngIf="getFulfillment(entry) as fulfillment">
+                    <vdr-fulfillment-detail
+                        [fulfillmentId]="fulfillment.id"
+                        [order]="order"
+                    ></vdr-fulfillment-detail>
+                </vdr-history-entry-detail>
+            </ng-container>
+            <ng-container *ngSwitchCase="type.ORDER_FULFILLMENT_TRANSITION">
+                <ng-container *ngIf="entry.data.to === 'Delivered'">
+                    <div class="title">
+                        {{ 'order.history-fulfillment-delivered' | translate }}
+                    </div>
+                    {{ 'order.tracking-code' | translate }}: {{ getFulfillment(entry)?.trackingCode }}
+                </ng-container>
+                <ng-container *ngIf="entry.data.to === 'Shipped'">
+                    <div class="title">
+                        {{ 'order.history-fulfillment-shipped' | translate }}
+                    </div>
+                    {{ 'order.tracking-code' | translate }}: {{ getFulfillment(entry)?.trackingCode }}
+                </ng-container>
+                <ng-container *ngIf="entry.data.to !== 'Delivered' && entry.data.to !== 'Shipped'">
+                    {{
+                        'order.history-fulfillment-transition'
+                            | translate: { from: entry.data.from, to: entry.data.to }
+                    }}
+                </ng-container>
+                <vdr-history-entry-detail *ngIf="getFulfillment(entry) as fulfillment">
                     <vdr-fulfillment-detail
                     <vdr-fulfillment-detail
                         [fulfillmentId]="fulfillment.id"
                         [fulfillmentId]="fulfillment.id"
                         [order]="order"
                         [order]="order"
@@ -149,5 +174,9 @@
             </ng-container>
             </ng-container>
         </ng-container>
         </ng-container>
     </vdr-timeline-entry>
     </vdr-timeline-entry>
-    <vdr-timeline-entry [isLast]="true"></vdr-timeline-entry>
+    <vdr-timeline-entry [isLast]="true" [createdAt]="order.createdAt" [featured]="true">
+        <div class="title">
+            {{ 'order.history-order-created' | translate }}
+        </div>
+    </vdr-timeline-entry>
 </div>
 </div>

+ 26 - 10
packages/admin-ui/src/lib/order/src/components/order-history/order-history.component.ts

@@ -22,17 +22,23 @@ export class OrderHistoryComponent {
     @Output() deleteNote = new EventEmitter<HistoryEntry>();
     @Output() deleteNote = new EventEmitter<HistoryEntry>();
     note = '';
     note = '';
     noteIsPrivate = true;
     noteIsPrivate = true;
+    expanded = false;
     readonly type = HistoryEntryType;
     readonly type = HistoryEntryType;
 
 
     getDisplayType(entry: GetOrderHistory.Items): TimelineDisplayType {
     getDisplayType(entry: GetOrderHistory.Items): TimelineDisplayType {
         if (entry.type === HistoryEntryType.ORDER_STATE_TRANSITION) {
         if (entry.type === HistoryEntryType.ORDER_STATE_TRANSITION) {
-            if (entry.data.to === 'Fulfilled') {
+            if (entry.data.to === 'Delivered') {
                 return 'success';
                 return 'success';
             }
             }
             if (entry.data.to === 'Cancelled') {
             if (entry.data.to === 'Cancelled') {
                 return 'error';
                 return 'error';
             }
             }
         }
         }
+        if (entry.type === HistoryEntryType.ORDER_FULFILLMENT_TRANSITION) {
+            if (entry.data.to === 'Delivered') {
+                return 'success';
+            }
+        }
         if (entry.type === HistoryEntryType.ORDER_PAYMENT_TRANSITION) {
         if (entry.type === HistoryEntryType.ORDER_PAYMENT_TRANSITION) {
             if (entry.data.to === 'Declined') {
             if (entry.data.to === 'Declined') {
                 return 'error';
                 return 'error';
@@ -49,7 +55,7 @@ export class OrderHistoryComponent {
 
 
     getTimelineIcon(entry: GetOrderHistory.Items) {
     getTimelineIcon(entry: GetOrderHistory.Items) {
         if (entry.type === HistoryEntryType.ORDER_STATE_TRANSITION) {
         if (entry.type === HistoryEntryType.ORDER_STATE_TRANSITION) {
-            if (entry.data.to === 'Fulfilled') {
+            if (entry.data.to === 'Delivered') {
                 return ['success-standard', 'is-solid'];
                 return ['success-standard', 'is-solid'];
             }
             }
             if (entry.data.to === 'Cancelled') {
             if (entry.data.to === 'Cancelled') {
@@ -62,8 +68,13 @@ export class OrderHistoryComponent {
         if (entry.type === HistoryEntryType.ORDER_NOTE) {
         if (entry.type === HistoryEntryType.ORDER_NOTE) {
             return 'note';
             return 'note';
         }
         }
-        if (entry.type === HistoryEntryType.ORDER_FULLFILLMENT) {
-            return 'truck';
+        if (entry.type === HistoryEntryType.ORDER_FULFILLMENT_TRANSITION) {
+            if (entry.data.to === 'Shipped') {
+                return 'truck';
+            }
+            if (entry.data.to === 'Delivered') {
+                return 'truck';
+            }
         }
         }
     }
     }
 
 
@@ -71,14 +82,15 @@ export class OrderHistoryComponent {
         switch (entry.type) {
         switch (entry.type) {
             case HistoryEntryType.ORDER_STATE_TRANSITION: {
             case HistoryEntryType.ORDER_STATE_TRANSITION: {
                 return (
                 return (
-                    entry.data.to === 'Fulfilled' ||
+                    entry.data.to === 'Delivered' ||
                     entry.data.to === 'Cancelled' ||
                     entry.data.to === 'Cancelled' ||
                     entry.data.to === 'Settled'
                     entry.data.to === 'Settled'
                 );
                 );
             }
             }
             case HistoryEntryType.ORDER_PAYMENT_TRANSITION:
             case HistoryEntryType.ORDER_PAYMENT_TRANSITION:
                 return entry.data.to === 'Settled';
                 return entry.data.to === 'Settled';
-            case HistoryEntryType.ORDER_FULLFILLMENT:
+            case HistoryEntryType.ORDER_FULFILLMENT_TRANSITION:
+                return entry.data.to === 'Delivered' || entry.data.to === 'Shipped';
             case HistoryEntryType.ORDER_NOTE:
             case HistoryEntryType.ORDER_NOTE:
                 return true;
                 return true;
             default:
             default:
@@ -86,15 +98,19 @@ export class OrderHistoryComponent {
         }
         }
     }
     }
 
 
-    getFullfillment(entry: GetOrderHistory.Items): OrderDetail.Fulfillments | undefined {
-        if (entry.type === HistoryEntryType.ORDER_FULLFILLMENT && this.order.fulfillments) {
-            return this.order.fulfillments.find((f) => f.id === entry.data.fulfillmentId);
+    getFulfillment(entry: GetOrderHistory.Items): OrderDetail.Fulfillments | undefined {
+        if (
+            (entry.type === HistoryEntryType.ORDER_FULFILLMENT ||
+                entry.type === HistoryEntryType.ORDER_FULFILLMENT_TRANSITION) &&
+            this.order.fulfillments
+        ) {
+            return this.order.fulfillments.find(f => f.id === entry.data.fulfillmentId);
         }
         }
     }
     }
 
 
     getPayment(entry: GetOrderHistory.Items): OrderDetail.Payments | undefined {
     getPayment(entry: GetOrderHistory.Items): OrderDetail.Payments | undefined {
         if (entry.type === HistoryEntryType.ORDER_PAYMENT_TRANSITION && this.order.payments) {
         if (entry.type === HistoryEntryType.ORDER_PAYMENT_TRANSITION && this.order.payments) {
-            return this.order.payments.find((p) => p.id === entry.data.paymentId);
+            return this.order.payments.find(p => p.id === entry.data.paymentId);
         }
         }
     }
     }
 
 

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

@@ -7,10 +7,10 @@
                 <option value="ArrangingPayment">{{ 'order.state-arranging-payment' | translate }}</option>
                 <option value="ArrangingPayment">{{ 'order.state-arranging-payment' | translate }}</option>
                 <option value="PaymentAuthorized">{{ 'order.state-payment-authorized' | translate }}</option>
                 <option value="PaymentAuthorized">{{ 'order.state-payment-authorized' | translate }}</option>
                 <option value="PaymentSettled">{{ 'order.state-payment-settled' | translate }}</option>
                 <option value="PaymentSettled">{{ 'order.state-payment-settled' | translate }}</option>
-                <option value="PartiallyFulfilled">
-                    {{ 'order.state-partially-fulfilled' | translate }}
+                <option value="PartiallyDelivered">
+                    {{ 'order.state-partially-delivered' | translate }}
                 </option>
                 </option>
-                <option value="Fulfilled">{{ 'order.state-fulfilled' | translate }}</option>
+                <option value="Delivered">{{ 'order.state-delivered' | translate }}</option>
                 <option value="Cancelled">{{ 'order.state-cancelled' | translate }}</option>
                 <option value="Cancelled">{{ 'order.state-cancelled' | translate }}</option>
             </select>
             </select>
             <input
             <input

+ 1 - 2
packages/admin-ui/src/lib/order/src/components/order-payment-card/order-payment-card.component.ts

@@ -1,10 +1,9 @@
 import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
 import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
 import { CurrencyCode } from '@vendure/admin-ui/core';
 import { CurrencyCode } from '@vendure/admin-ui/core';
-
 import { OrderDetail } from '@vendure/admin-ui/core';
 import { OrderDetail } from '@vendure/admin-ui/core';
 
 
 @Component({
 @Component({
-    selector: 'vdr-order-payment-detail',
+    selector: 'vdr-order-payment-card',
     templateUrl: './order-payment-card.component.html',
     templateUrl: './order-payment-card.component.html',
     styleUrls: ['./order-payment-card.component.scss'],
     styleUrls: ['./order-payment-card.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
     changeDetection: ChangeDetectionStrategy.OnPush,

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

@@ -4,7 +4,9 @@ import { SharedModule } from '@vendure/admin-ui/core';
 
 
 import { CancelOrderDialogComponent } from './components/cancel-order-dialog/cancel-order-dialog.component';
 import { CancelOrderDialogComponent } from './components/cancel-order-dialog/cancel-order-dialog.component';
 import { FulfillOrderDialogComponent } from './components/fulfill-order-dialog/fulfill-order-dialog.component';
 import { FulfillOrderDialogComponent } from './components/fulfill-order-dialog/fulfill-order-dialog.component';
+import { FulfillmentCardComponent } from './components/fulfillment-card/fulfillment-card.component';
 import { FulfillmentDetailComponent } from './components/fulfillment-detail/fulfillment-detail.component';
 import { FulfillmentDetailComponent } from './components/fulfillment-detail/fulfillment-detail.component';
+import { FulfillmentStateLabelComponent } from './components/fulfillment-state-label/fulfillment-state-label.component';
 import { LineFulfillmentComponent } from './components/line-fulfillment/line-fulfillment.component';
 import { LineFulfillmentComponent } from './components/line-fulfillment/line-fulfillment.component';
 import { LineRefundsComponent } from './components/line-refunds/line-refunds.component';
 import { LineRefundsComponent } from './components/line-refunds/line-refunds.component';
 import { OrderCustomFieldsCardComponent } from './components/order-custom-fields-card/order-custom-fields-card.component';
 import { OrderCustomFieldsCardComponent } from './components/order-custom-fields-card/order-custom-fields-card.component';
@@ -47,6 +49,8 @@ import { orderRoutes } from './order.routes';
         OrderProcessNodeComponent,
         OrderProcessNodeComponent,
         OrderProcessEdgeComponent,
         OrderProcessEdgeComponent,
         OrderProcessGraphDialogComponent,
         OrderProcessGraphDialogComponent,
+        FulfillmentStateLabelComponent,
+        FulfillmentCardComponent,
     ],
     ],
 })
 })
 export class OrderModule {}
 export class OrderModule {}

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

@@ -1,7 +1,9 @@
 // This file was generated by the build-public-api.ts script
 // This file was generated by the build-public-api.ts script
 export * from './components/cancel-order-dialog/cancel-order-dialog.component';
 export * from './components/cancel-order-dialog/cancel-order-dialog.component';
 export * from './components/fulfill-order-dialog/fulfill-order-dialog.component';
 export * from './components/fulfill-order-dialog/fulfill-order-dialog.component';
+export * from './components/fulfillment-card/fulfillment-card.component';
 export * from './components/fulfillment-detail/fulfillment-detail.component';
 export * from './components/fulfillment-detail/fulfillment-detail.component';
+export * from './components/fulfillment-state-label/fulfillment-state-label.component';
 export * from './components/line-fulfillment/line-fulfillment.component';
 export * from './components/line-fulfillment/line-fulfillment.component';
 export * from './components/line-refunds/line-refunds.component';
 export * from './components/line-refunds/line-refunds.component';
 export * from './components/order-custom-fields-card/order-custom-fields-card.component';
 export * from './components/order-custom-fields-card/order-custom-fields-card.component';

+ 30 - 31
packages/admin-ui/src/lib/settings/src/components/channel-detail/channel-detail.component.ts

@@ -54,7 +54,7 @@ export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment
 
 
     ngOnInit() {
     ngOnInit() {
         this.init();
         this.init();
-        this.zones$ = this.dataService.settings.getZones().mapSingle((data) => data.zones);
+        this.zones$ = this.dataService.settings.getZones().mapSingle(data => data.zones);
         this.availableLanguageCodes$ = this.serverConfigService.getAvailableLanguages();
         this.availableLanguageCodes$ = this.serverConfigService.getAvailableLanguages();
     }
     }
 
 
@@ -96,21 +96,21 @@ export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment
                     this.dataService.client.updateUserChannels(me!.channels).pipe(map(() => createChannel)),
                     this.dataService.client.updateUserChannels(me!.channels).pipe(map(() => createChannel)),
                 ),
                 ),
             )
             )
-            .subscribe(
-                (data) => {
-                    this.notificationService.success(_('common.notify-create-success'), {
-                        entity: 'Channel',
-                    });
-                    this.detailForm.markAsPristine();
-                    this.changeDetector.markForCheck();
-                    this.router.navigate(['../', data.id], { relativeTo: this.route });
-                },
-                (err) => {
-                    this.notificationService.error(_('common.notify-create-error'), {
-                        entity: 'Channel',
-                    });
-                },
-            );
+            .subscribe(data => {
+                switch (data.__typename) {
+                    case 'Channel':
+                        this.notificationService.success(_('common.notify-create-success'), {
+                            entity: 'Channel',
+                        });
+                        this.detailForm.markAsPristine();
+                        this.changeDetector.markForCheck();
+                        this.router.navigate(['../', data.id], { relativeTo: this.route });
+                        break;
+                    case 'LanguageNotAvailableError':
+                        this.notificationService.error(data.message);
+                        break;
+                }
+            });
     }
     }
 
 
     save() {
     save() {
@@ -121,7 +121,7 @@ export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment
         this.entity$
         this.entity$
             .pipe(
             .pipe(
                 take(1),
                 take(1),
-                mergeMap((channel) => {
+                mergeMap(channel => {
                     const input = {
                     const input = {
                         id: channel.id,
                         id: channel.id,
                         code: formValue.code,
                         code: formValue.code,
@@ -134,20 +134,19 @@ export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment
                     return this.dataService.settings.updateChannel(input);
                     return this.dataService.settings.updateChannel(input);
                 }),
                 }),
             )
             )
-            .subscribe(
-                () => {
-                    this.notificationService.success(_('common.notify-update-success'), {
-                        entity: 'Channel',
-                    });
-                    this.detailForm.markAsPristine();
-                    this.changeDetector.markForCheck();
-                },
-                (err) => {
-                    this.notificationService.error(_('common.notify-update-error'), {
-                        entity: 'Channel',
-                    });
-                },
-            );
+            .subscribe(({ updateChannel }) => {
+                switch (updateChannel.__typename) {
+                    case 'Channel':
+                        this.notificationService.success(_('common.notify-update-success'), {
+                            entity: 'Channel',
+                        });
+                        this.detailForm.markAsPristine();
+                        this.changeDetector.markForCheck();
+                        break;
+                    case 'LanguageNotAvailableError':
+                        this.notificationService.error(updateChannel.message);
+                }
+            });
     }
     }
 
 
     /**
     /**

+ 18 - 16
packages/admin-ui/src/lib/settings/src/components/global-settings/global-settings.component.ts

@@ -7,7 +7,7 @@ import { CustomFieldConfig, GlobalSettings, LanguageCode, Permission } from '@ve
 import { NotificationService } from '@vendure/admin-ui/core';
 import { NotificationService } from '@vendure/admin-ui/core';
 import { DataService } from '@vendure/admin-ui/core';
 import { DataService } from '@vendure/admin-ui/core';
 import { ServerConfigService } from '@vendure/admin-ui/core';
 import { ServerConfigService } from '@vendure/admin-ui/core';
-import { switchMap } from 'rxjs/operators';
+import { switchMap, tap } from 'rxjs/operators';
 
 
 @Component({
 @Component({
     selector: 'vdr-global-settings',
     selector: 'vdr-global-settings',
@@ -63,21 +63,23 @@ export class GlobalSettingsComponent extends BaseDetailComponent<GlobalSettings>
 
 
         this.dataService.settings
         this.dataService.settings
             .updateGlobalSettings(this.detailForm.value)
             .updateGlobalSettings(this.detailForm.value)
-            .pipe(switchMap(() => this.serverConfigService.refreshGlobalSettings()))
-            .subscribe(
-                () => {
-                    this.detailForm.markAsPristine();
-                    this.changeDetector.markForCheck();
-                    this.notificationService.success(_('common.notify-update-success'), {
-                        entity: 'Settings',
-                    });
-                },
-                (err) => {
-                    this.notificationService.error(_('common.notify-update-error'), {
-                        entity: 'Settings',
-                    });
-                },
-            );
+            .pipe(
+                tap(({ updateGlobalSettings }) => {
+                    switch (updateGlobalSettings.__typename) {
+                        case 'GlobalSettings':
+                            this.detailForm.markAsPristine();
+                            this.changeDetector.markForCheck();
+                            this.notificationService.success(_('common.notify-update-success'), {
+                                entity: 'Settings',
+                            });
+                            break;
+                        case 'ChannelDefaultLanguageError':
+                            this.notificationService.error(updateGlobalSettings.message);
+                    }
+                }),
+                switchMap(() => this.serverConfigService.refreshGlobalSettings()),
+            )
+            .subscribe();
     }
     }
 
 
     protected setFormValues(entity: GlobalSettings, languageCode: LanguageCode): void {
     protected setFormValues(entity: GlobalSettings, languageCode: LanguageCode): void {

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

@@ -51,7 +51,7 @@
                 (remove)="selectedChecker = null"
                 (remove)="selectedChecker = null"
                 formControlName="checker"
                 formControlName="checker"
             ></vdr-configurable-input>
             ></vdr-configurable-input>
-            <div *ngIf="!selectedChecker">
+            <div *ngIf="!selectedChecker || !selectedCheckerDefinition">
                 <vdr-dropdown>
                 <vdr-dropdown>
                     <button class="btn btn-outline" vdrDropdownTrigger>
                     <button class="btn btn-outline" vdrDropdownTrigger>
                         <clr-icon shape="plus"></clr-icon>
                         <clr-icon shape="plus"></clr-icon>
@@ -80,7 +80,7 @@
                 (remove)="selectedCalculator = null"
                 (remove)="selectedCalculator = null"
                 formControlName="calculator"
                 formControlName="calculator"
             ></vdr-configurable-input>
             ></vdr-configurable-input>
-            <div *ngIf="!selectedCalculator">
+            <div *ngIf="!selectedCalculator || !selectedCalculatorDefinition">
                 <vdr-dropdown>
                 <vdr-dropdown>
                     <button class="btn btn-outline" vdrDropdownTrigger>
                     <button class="btn btn-outline" vdrDropdownTrigger>
                         <clr-icon shape="plus"></clr-icon>
                         <clr-icon shape="plus"></clr-icon>

+ 13 - 3
packages/admin-ui/src/lib/static/i18n-messages/de.json

@@ -144,6 +144,7 @@
     "channel": "Kanal",
     "channel": "Kanal",
     "channels": "Kanäle",
     "channels": "Kanäle",
     "code": "Code",
     "code": "Code",
+    "collapse-entries": "",
     "confirm": "",
     "confirm": "",
     "confirm-delete-note": "",
     "confirm-delete-note": "",
     "confirm-navigation": "Navigation bestätigen",
     "confirm-navigation": "Navigation bestätigen",
@@ -158,11 +159,11 @@
     "disabled": "Deaktiviert",
     "disabled": "Deaktiviert",
     "discard-changes": "Änderungen verwerfen",
     "discard-changes": "Änderungen verwerfen",
     "display-custom-fields": "Benutzerdefinierte Felder anzeigen",
     "display-custom-fields": "Benutzerdefinierte Felder anzeigen",
-    "done": "Fertig",
     "edit": "Bearbeiten",
     "edit": "Bearbeiten",
     "edit-field": "Feld bearbeiten",
     "edit-field": "Feld bearbeiten",
     "edit-note": "",
     "edit-note": "",
     "enabled": "Aktiviert",
     "enabled": "Aktiviert",
+    "expand-entries": "",
     "extension-running-in-separate-window": "Die Erweiterung läuft in einem separaten Fenster",
     "extension-running-in-separate-window": "Die Erweiterung läuft in einem separaten Fenster",
     "guest": "Gast",
     "guest": "Gast",
     "hide-custom-fields": "Benutzerdefinierte Felder ausblenden",
     "hide-custom-fields": "Benutzerdefinierte Felder ausblenden",
@@ -522,6 +523,7 @@
     "amount": "Betrag",
     "amount": "Betrag",
     "billing-address": "",
     "billing-address": "",
     "cancel": "Abbrechen",
     "cancel": "Abbrechen",
+    "cancel-fulfillment": "",
     "cancel-order": "Bestellung stornieren",
     "cancel-order": "Bestellung stornieren",
     "cancel-reason-customer-request": "Kundenanfrage",
     "cancel-reason-customer-request": "Kundenanfrage",
     "cancel-reason-not-available": "Nicht verfügbar",
     "cancel-reason-not-available": "Nicht verfügbar",
@@ -539,8 +541,12 @@
     "history-coupon-code-applied": "Gutscheincode aktiviert",
     "history-coupon-code-applied": "Gutscheincode aktiviert",
     "history-coupon-code-removed": "Gutscheincode entfernt",
     "history-coupon-code-removed": "Gutscheincode entfernt",
     "history-fulfillment-created": "Auftrag ausgeführt",
     "history-fulfillment-created": "Auftrag ausgeführt",
+    "history-fulfillment-delivered": "",
+    "history-fulfillment-shipped": "",
+    "history-fulfillment-transition": "",
     "history-items-cancelled": "{count} {count, plural, one {Artikel} other {Artikel}} gestrichen",
     "history-items-cancelled": "{count} {count, plural, one {Artikel} other {Artikel}} gestrichen",
     "history-order-cancelled": "Bestellung storniert",
     "history-order-cancelled": "Bestellung storniert",
+    "history-order-created": "",
     "history-order-fulfilled": "Auftrag ausgeführt",
     "history-order-fulfilled": "Auftrag ausgeführt",
     "history-order-transition": "Auftragsstatus von {from} nach {to}",
     "history-order-transition": "Auftragsstatus von {from} nach {to}",
     "history-payment-settled": "Bezahlt",
     "history-payment-settled": "Bezahlt",
@@ -583,6 +589,7 @@
     "refunded-count": "{count} {count, plural, one {Artikel} other {Artikel}} erstattet",
     "refunded-count": "{count} {count, plural, one {Artikel} other {Artikel}} erstattet",
     "return-to-stock": "Zum Lagerbestand hinzufügen",
     "return-to-stock": "Zum Lagerbestand hinzufügen",
     "search-by-order-code": "Suche nach Bestellcode",
     "search-by-order-code": "Suche nach Bestellcode",
+    "set-fulfillment-state": "",
     "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",
     "settle-payment-success": "Zahlung erfolgreich durchgeführt",
     "settle-payment-success": "Zahlung erfolgreich durchgeführt",
@@ -597,11 +604,14 @@
     "state-all-orders": "Alle Bestellungen",
     "state-all-orders": "Alle Bestellungen",
     "state-arranging-payment": "Zahlung einrichten",
     "state-arranging-payment": "Zahlung einrichten",
     "state-cancelled": "Storniert",
     "state-cancelled": "Storniert",
-    "state-fulfilled": "Ausgeführt",
-    "state-partially-fulfilled": "Teilweise ausgeführt",
+    "state-delivered": "Ausgeführt",
+    "state-partially-delivered": "Teilweise ausgeführt",
+    "state-partially-shipped": "",
     "state-payment-authorized": "Zahlung autorisiert",
     "state-payment-authorized": "Zahlung autorisiert",
     "state-payment-settled": "Bezahlt",
     "state-payment-settled": "Bezahlt",
+    "state-shipped": "",
     "sub-total": "Zwischensumme",
     "sub-total": "Zwischensumme",
+    "successfully-updated-fulfillment": "",
     "total": "Gesamtsumme",
     "total": "Gesamtsumme",
     "tracking-code": "Sendungsverfolgungscode",
     "tracking-code": "Sendungsverfolgungscode",
     "transaction-id": "Transaktions-ID",
     "transaction-id": "Transaktions-ID",

+ 15 - 5
packages/admin-ui/src/lib/static/i18n-messages/en.json

@@ -144,6 +144,7 @@
     "channel": "Channel",
     "channel": "Channel",
     "channels": "Channels",
     "channels": "Channels",
     "code": "Code",
     "code": "Code",
+    "collapse-entries": "Collapse entries",
     "confirm": "Confirm",
     "confirm": "Confirm",
     "confirm-delete-note": "Delete note?",
     "confirm-delete-note": "Delete note?",
     "confirm-navigation": "Confirm navigation",
     "confirm-navigation": "Confirm navigation",
@@ -158,11 +159,11 @@
     "disabled": "Disabled",
     "disabled": "Disabled",
     "discard-changes": "Discard changes",
     "discard-changes": "Discard changes",
     "display-custom-fields": "Display custom fields",
     "display-custom-fields": "Display custom fields",
-    "done": "Done",
     "edit": "Edit",
     "edit": "Edit",
     "edit-field": "Edit field",
     "edit-field": "Edit field",
     "edit-note": "Edit note",
     "edit-note": "Edit note",
     "enabled": "Enabled",
     "enabled": "Enabled",
+    "expand-entries": "Expand entries",
     "extension-running-in-separate-window": "Extension is running in a separate window",
     "extension-running-in-separate-window": "Extension is running in a separate window",
     "guest": "Guest",
     "guest": "Guest",
     "hide-custom-fields": "Hide custom fields",
     "hide-custom-fields": "Hide custom fields",
@@ -522,7 +523,8 @@
     "amount": "Amount",
     "amount": "Amount",
     "billing-address": "Billing address",
     "billing-address": "Billing address",
     "cancel": "Cancel",
     "cancel": "Cancel",
-    "cancel-order": "Cancel Order",
+    "cancel-fulfillment": "Cancel fulfillment",
+    "cancel-order": "Cancel order",
     "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",
@@ -539,8 +541,12 @@
     "history-coupon-code-applied": "Coupon code applied",
     "history-coupon-code-applied": "Coupon code applied",
     "history-coupon-code-removed": "Coupon code removed",
     "history-coupon-code-removed": "Coupon code removed",
     "history-fulfillment-created": "Fulfillment created",
     "history-fulfillment-created": "Fulfillment created",
+    "history-fulfillment-delivered": "Fulfillment delivered",
+    "history-fulfillment-shipped": "Fulfillment shipped",
+    "history-fulfillment-transition": "Fulfillment transitioned from {from} to {to}",
     "history-items-cancelled": "{count} {count, plural, one {item} other {items}} cancelled",
     "history-items-cancelled": "{count} {count, plural, one {item} other {items}} cancelled",
     "history-order-cancelled": "Order cancelled",
     "history-order-cancelled": "Order cancelled",
+    "history-order-created": "Order created",
     "history-order-fulfilled": "Order fulfilled",
     "history-order-fulfilled": "Order fulfilled",
     "history-order-transition": "Order transitioned from {from} to {to}",
     "history-order-transition": "Order transitioned from {from} to {to}",
     "history-payment-settled": "Payment settled",
     "history-payment-settled": "Payment settled",
@@ -583,9 +589,10 @@
     "refunded-count": "{count} {count, plural, one {item} other {items}} refunded",
     "refunded-count": "{count} {count, plural, one {item} other {items}} refunded",
     "return-to-stock": "Return to stock",
     "return-to-stock": "Return to stock",
     "search-by-order-code": "Search by order code",
     "search-by-order-code": "Search by order code",
+    "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",
-    "settle-payment-success": "Sucessfully settled payment",
+    "settle-payment-success": "Successfully settled payment",
     "settle-refund": "Settle refund",
     "settle-refund": "Settle refund",
     "settle-refund-manual-instructions": "After manually refunding via your payment provider ({method}), enter the transaction ID here.",
     "settle-refund-manual-instructions": "After manually refunding via your payment provider ({method}), enter the transaction ID here.",
     "settle-refund-success": "Successfully settled refund",
     "settle-refund-success": "Successfully settled refund",
@@ -597,11 +604,14 @@
     "state-all-orders": "All orders",
     "state-all-orders": "All orders",
     "state-arranging-payment": "Arranging payment",
     "state-arranging-payment": "Arranging payment",
     "state-cancelled": "Cancelled",
     "state-cancelled": "Cancelled",
-    "state-fulfilled": "Fulfilled",
-    "state-partially-fulfilled": "Partially fulfilled",
+    "state-delivered": "Delivered",
+    "state-partially-delivered": "Partially delivered",
+    "state-partially-shipped": "Partially shipped",
     "state-payment-authorized": "Payment authorized",
     "state-payment-authorized": "Payment authorized",
     "state-payment-settled": "Payment settled",
     "state-payment-settled": "Payment settled",
+    "state-shipped": "Shipped",
     "sub-total": "Sub total",
     "sub-total": "Sub total",
+    "successfully-updated-fulfillment": "Successfully updated fulfillment",
     "total": "Total",
     "total": "Total",
     "tracking-code": "Tracking code",
     "tracking-code": "Tracking code",
     "transaction-id": "Transaction ID",
     "transaction-id": "Transaction ID",

+ 13 - 3
packages/admin-ui/src/lib/static/i18n-messages/es.json

@@ -144,6 +144,7 @@
     "channel": "Canal de ventas",
     "channel": "Canal de ventas",
     "channels": "Canales de ventas",
     "channels": "Canales de ventas",
     "code": "Código",
     "code": "Código",
+    "collapse-entries": "",
     "confirm": "Confirmar",
     "confirm": "Confirmar",
     "confirm-delete-note": "¿Eliminar nota?",
     "confirm-delete-note": "¿Eliminar nota?",
     "confirm-navigation": "Confirmar navegación",
     "confirm-navigation": "Confirmar navegación",
@@ -158,11 +159,11 @@
     "disabled": "Deshabilitado",
     "disabled": "Deshabilitado",
     "discard-changes": "Descartar cambios",
     "discard-changes": "Descartar cambios",
     "display-custom-fields": "Mostrar campos personalizados",
     "display-custom-fields": "Mostrar campos personalizados",
-    "done": "Hecho",
     "edit": "Editar",
     "edit": "Editar",
     "edit-field": "Editar campo",
     "edit-field": "Editar campo",
     "edit-note": "Editar nota",
     "edit-note": "Editar nota",
     "enabled": "Habilitado",
     "enabled": "Habilitado",
+    "expand-entries": "",
     "extension-running-in-separate-window": "La extensión se está ejecutando en una nueva ventana",
     "extension-running-in-separate-window": "La extensión se está ejecutando en una nueva ventana",
     "guest": "Invitado",
     "guest": "Invitado",
     "hide-custom-fields": "Ocultar campos personalizados",
     "hide-custom-fields": "Ocultar campos personalizados",
@@ -522,6 +523,7 @@
     "amount": "Precio",
     "amount": "Precio",
     "billing-address": "",
     "billing-address": "",
     "cancel": "",
     "cancel": "",
+    "cancel-fulfillment": "",
     "cancel-order": "",
     "cancel-order": "",
     "cancel-reason-customer-request": "",
     "cancel-reason-customer-request": "",
     "cancel-reason-not-available": "",
     "cancel-reason-not-available": "",
@@ -539,8 +541,12 @@
     "history-coupon-code-applied": "",
     "history-coupon-code-applied": "",
     "history-coupon-code-removed": "",
     "history-coupon-code-removed": "",
     "history-fulfillment-created": "",
     "history-fulfillment-created": "",
+    "history-fulfillment-delivered": "",
+    "history-fulfillment-shipped": "",
+    "history-fulfillment-transition": "",
     "history-items-cancelled": "",
     "history-items-cancelled": "",
     "history-order-cancelled": "",
     "history-order-cancelled": "",
+    "history-order-created": "",
     "history-order-fulfilled": "",
     "history-order-fulfilled": "",
     "history-order-transition": "",
     "history-order-transition": "",
     "history-payment-settled": "",
     "history-payment-settled": "",
@@ -583,6 +589,7 @@
     "refunded-count": "",
     "refunded-count": "",
     "return-to-stock": "",
     "return-to-stock": "",
     "search-by-order-code": "",
     "search-by-order-code": "",
+    "set-fulfillment-state": "",
     "settle-payment": "",
     "settle-payment": "",
     "settle-payment-error": "",
     "settle-payment-error": "",
     "settle-payment-success": "",
     "settle-payment-success": "",
@@ -597,11 +604,14 @@
     "state-all-orders": "",
     "state-all-orders": "",
     "state-arranging-payment": "",
     "state-arranging-payment": "",
     "state-cancelled": "",
     "state-cancelled": "",
-    "state-fulfilled": "",
-    "state-partially-fulfilled": "",
+    "state-delivered": "",
+    "state-partially-delivered": "",
+    "state-partially-shipped": "",
     "state-payment-authorized": "",
     "state-payment-authorized": "",
     "state-payment-settled": "",
     "state-payment-settled": "",
+    "state-shipped": "",
     "sub-total": "Sub total",
     "sub-total": "Sub total",
+    "successfully-updated-fulfillment": "",
     "total": "Total",
     "total": "Total",
     "tracking-code": "",
     "tracking-code": "",
     "transaction-id": "ID de transacción",
     "transaction-id": "ID de transacción",

+ 14 - 4
packages/admin-ui/src/lib/static/i18n-messages/pl.json

@@ -144,6 +144,7 @@
     "channel": "Kanał",
     "channel": "Kanał",
     "channels": "Kanały",
     "channels": "Kanały",
     "code": "Kod",
     "code": "Kod",
+    "collapse-entries": "",
     "confirm": "",
     "confirm": "",
     "confirm-delete-note": "",
     "confirm-delete-note": "",
     "confirm-navigation": "Potwierdź nawigacje",
     "confirm-navigation": "Potwierdź nawigacje",
@@ -158,11 +159,11 @@
     "disabled": "Wyłączony",
     "disabled": "Wyłączony",
     "discard-changes": "Odrzuć zmiany",
     "discard-changes": "Odrzuć zmiany",
     "display-custom-fields": "Wyświetl pola dodatkowe",
     "display-custom-fields": "Wyświetl pola dodatkowe",
-    "done": "Skończone",
     "edit": "Edytuj",
     "edit": "Edytuj",
     "edit-field": "Edytuj pole",
     "edit-field": "Edytuj pole",
     "edit-note": "",
     "edit-note": "",
     "enabled": "Aktywny",
     "enabled": "Aktywny",
+    "expand-entries": "",
     "extension-running-in-separate-window": "Rozszerzenie jest włączone w innym oknie",
     "extension-running-in-separate-window": "Rozszerzenie jest włączone w innym oknie",
     "guest": "Gość",
     "guest": "Gość",
     "hide-custom-fields": "Ukryj pola dodatkowe",
     "hide-custom-fields": "Ukryj pola dodatkowe",
@@ -522,6 +523,7 @@
     "amount": "Ilość",
     "amount": "Ilość",
     "billing-address": "",
     "billing-address": "",
     "cancel": "Anuluj",
     "cancel": "Anuluj",
+    "cancel-fulfillment": "",
     "cancel-order": "Anuluj zamówienie",
     "cancel-order": "Anuluj zamówienie",
     "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",
@@ -538,9 +540,13 @@
     "fulfillment-method": "Metoda realizacji",
     "fulfillment-method": "Metoda realizacji",
     "history-coupon-code-applied": "Użyto kodu rabatowego",
     "history-coupon-code-applied": "Użyto kodu rabatowego",
     "history-coupon-code-removed": "Usunięto kod rabatowy",
     "history-coupon-code-removed": "Usunięto kod rabatowy",
-    "history-fulfillment-created": "Utworzono wypełnienie",
+    "history-fulfillment-created": "",
+    "history-fulfillment-delivered": "",
+    "history-fulfillment-shipped": "",
+    "history-fulfillment-transition": "",
     "history-items-cancelled": "{count} {count, plural, one {element} other {elementów}} anulowano",
     "history-items-cancelled": "{count} {count, plural, one {element} other {elementów}} anulowano",
     "history-order-cancelled": "Zamówienie anulowane",
     "history-order-cancelled": "Zamówienie anulowane",
+    "history-order-created": "",
     "history-order-fulfilled": "Zamówienie zrealizowane",
     "history-order-fulfilled": "Zamówienie zrealizowane",
     "history-order-transition": "Zamówienie wysłane z {from} do {to}",
     "history-order-transition": "Zamówienie wysłane z {from} do {to}",
     "history-payment-settled": "Opłacono",
     "history-payment-settled": "Opłacono",
@@ -583,6 +589,7 @@
     "refunded-count": "{count} {count, plural, one {zamówienie} other {zamówień}} zwrócono",
     "refunded-count": "{count} {count, plural, one {zamówienie} other {zamówień}} zwrócono",
     "return-to-stock": "Zwróć do magazynu",
     "return-to-stock": "Zwróć do magazynu",
     "search-by-order-code": "Szukaj po numerze zamówienia",
     "search-by-order-code": "Szukaj po numerze zamówienia",
+    "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",
     "settle-payment-success": "Płatność rozliczona pomyślnie",
     "settle-payment-success": "Płatność rozliczona pomyślnie",
@@ -597,11 +604,14 @@
     "state-all-orders": "Wszystkie zamówienia",
     "state-all-orders": "Wszystkie zamówienia",
     "state-arranging-payment": "Oczekiwanie na płatność",
     "state-arranging-payment": "Oczekiwanie na płatność",
     "state-cancelled": "Anulowano",
     "state-cancelled": "Anulowano",
-    "state-fulfilled": "Zrealizowano",
-    "state-partially-fulfilled": "Częściowo zrealizowano",
+    "state-delivered": "Zrealizowano",
+    "state-partially-delivered": "Częściowo zrealizowano",
+    "state-partially-shipped": "",
     "state-payment-authorized": "Płatność zaakceptowana",
     "state-payment-authorized": "Płatność zaakceptowana",
     "state-payment-settled": "Płatność rozliczona",
     "state-payment-settled": "Płatność rozliczona",
+    "state-shipped": "",
     "sub-total": "Sub total",
     "sub-total": "Sub total",
+    "successfully-updated-fulfillment": "",
     "total": "Total",
     "total": "Total",
     "tracking-code": "Numer przesyłki",
     "tracking-code": "Numer przesyłki",
     "transaction-id": "Numer transakcji",
     "transaction-id": "Numer transakcji",

+ 14 - 4
packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json

@@ -144,6 +144,7 @@
     "channel": "Canal",
     "channel": "Canal",
     "channels": "Canais",
     "channels": "Canais",
     "code": "Código",
     "code": "Código",
+    "collapse-entries": "",
     "confirm": "Confirme",
     "confirm": "Confirme",
     "confirm-delete-note": "Excluir nota?",
     "confirm-delete-note": "Excluir nota?",
     "confirm-navigation": "Confrme navegação",
     "confirm-navigation": "Confrme navegação",
@@ -158,11 +159,11 @@
     "disabled": "Desabilitado",
     "disabled": "Desabilitado",
     "discard-changes": "Descartar modificações",
     "discard-changes": "Descartar modificações",
     "display-custom-fields": "Exibir campos personalizados",
     "display-custom-fields": "Exibir campos personalizados",
-    "done": "Pronto",
     "edit": "Editar",
     "edit": "Editar",
     "edit-field": "Editar campo",
     "edit-field": "Editar campo",
     "edit-note": "Editar nota",
     "edit-note": "Editar nota",
     "enabled": "Habilitado",
     "enabled": "Habilitado",
+    "expand-entries": "",
     "extension-running-in-separate-window": "A extensão está sendo executada em uma janela separada",
     "extension-running-in-separate-window": "A extensão está sendo executada em uma janela separada",
     "guest": "Convidado",
     "guest": "Convidado",
     "hide-custom-fields": "Ocultar campos personalizados",
     "hide-custom-fields": "Ocultar campos personalizados",
@@ -522,6 +523,7 @@
     "amount": "Total",
     "amount": "Total",
     "billing-address": "Endereço de cobrança",
     "billing-address": "Endereço de cobrança",
     "cancel": "Cancelar",
     "cancel": "Cancelar",
+    "cancel-fulfillment": "",
     "cancel-order": "Cancelar Pedido",
     "cancel-order": "Cancelar Pedido",
     "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",
@@ -538,9 +540,13 @@
     "fulfillment-method": "Método de execução",
     "fulfillment-method": "Método de execução",
     "history-coupon-code-applied": "Código de cupom aplicado",
     "history-coupon-code-applied": "Código de cupom aplicado",
     "history-coupon-code-removed": "Código de cupom excluído",
     "history-coupon-code-removed": "Código de cupom excluído",
-    "history-fulfillment-created": "Execução criada",
+    "history-fulfillment-created": "",
+    "history-fulfillment-delivered": "",
+    "history-fulfillment-shipped": "",
+    "history-fulfillment-transition": "",
     "history-items-cancelled": "{count} {count, plural, one {item} other {items}} cancelado",
     "history-items-cancelled": "{count} {count, plural, one {item} other {items}} cancelado",
     "history-order-cancelled": "Pedido cancelado",
     "history-order-cancelled": "Pedido cancelado",
+    "history-order-created": "",
     "history-order-fulfilled": "Pedido realizado",
     "history-order-fulfilled": "Pedido realizado",
     "history-order-transition": "Pedido transferido de {from} para {to}",
     "history-order-transition": "Pedido transferido de {from} para {to}",
     "history-payment-settled": "Pagamento concluído",
     "history-payment-settled": "Pagamento concluído",
@@ -583,6 +589,7 @@
     "refunded-count": "{count} {count, plural, one {item} other {items}} reembolsado",
     "refunded-count": "{count} {count, plural, one {item} other {items}} reembolsado",
     "return-to-stock": "Retorno ao estoque",
     "return-to-stock": "Retorno ao estoque",
     "search-by-order-code": "Buscar por código do pedido",
     "search-by-order-code": "Buscar por código do pedido",
+    "set-fulfillment-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",
     "settle-payment-success": "Pagamento liquidado com sucesso",
     "settle-payment-success": "Pagamento liquidado com sucesso",
@@ -597,11 +604,14 @@
     "state-all-orders": "Todos os pedidos",
     "state-all-orders": "Todos os pedidos",
     "state-arranging-payment": "Organização de pagamento",
     "state-arranging-payment": "Organização de pagamento",
     "state-cancelled": "Cancelado",
     "state-cancelled": "Cancelado",
-    "state-fulfilled": "Realizado",
-    "state-partially-fulfilled": "Parcialmente realizado",
+    "state-delivered": "Realizado",
+    "state-partially-delivered": "Parcialmente realizado",
+    "state-partially-shipped": "",
     "state-payment-authorized": "Pagamento autorizado",
     "state-payment-authorized": "Pagamento autorizado",
     "state-payment-settled": "Pagamento liquidado",
     "state-payment-settled": "Pagamento liquidado",
+    "state-shipped": "",
     "sub-total": "Subtotal",
     "sub-total": "Subtotal",
+    "successfully-updated-fulfillment": "",
     "total": "Total",
     "total": "Total",
     "tracking-code": "Código de rastreio",
     "tracking-code": "Código de rastreio",
     "transaction-id": "Código ID da transação",
     "transaction-id": "Código ID da transação",

+ 14 - 4
packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json

@@ -144,6 +144,7 @@
     "channel": "销售渠道",
     "channel": "销售渠道",
     "channels": "销售渠道",
     "channels": "销售渠道",
     "code": "编码",
     "code": "编码",
+    "collapse-entries": "",
     "confirm": "",
     "confirm": "",
     "confirm-delete-note": "",
     "confirm-delete-note": "",
     "confirm-navigation": "导航确认",
     "confirm-navigation": "导航确认",
@@ -158,11 +159,11 @@
     "disabled": "禁用",
     "disabled": "禁用",
     "discard-changes": "放弃修改",
     "discard-changes": "放弃修改",
     "display-custom-fields": "显示客户化字段",
     "display-custom-fields": "显示客户化字段",
-    "done": "完成",
     "edit": "编辑",
     "edit": "编辑",
     "edit-field": "编辑域",
     "edit-field": "编辑域",
     "edit-note": "",
     "edit-note": "",
     "enabled": "启用",
     "enabled": "启用",
+    "expand-entries": "",
     "extension-running-in-separate-window": "扩展已在另一个窗口启动",
     "extension-running-in-separate-window": "扩展已在另一个窗口启动",
     "guest": "游客",
     "guest": "游客",
     "hide-custom-fields": "隐藏客户化字段",
     "hide-custom-fields": "隐藏客户化字段",
@@ -522,6 +523,7 @@
     "amount": "金额",
     "amount": "金额",
     "billing-address": "",
     "billing-address": "",
     "cancel": "取消",
     "cancel": "取消",
+    "cancel-fulfillment": "",
     "cancel-order": "取消订单",
     "cancel-order": "取消订单",
     "cancel-reason-customer-request": "客户要求",
     "cancel-reason-customer-request": "客户要求",
     "cancel-reason-not-available": "产品无库存",
     "cancel-reason-not-available": "产品无库存",
@@ -538,9 +540,13 @@
     "fulfillment-method": "配货方式",
     "fulfillment-method": "配货方式",
     "history-coupon-code-applied": "优惠卷已使用",
     "history-coupon-code-applied": "优惠卷已使用",
     "history-coupon-code-removed": "优惠卷已移除",
     "history-coupon-code-removed": "优惠卷已移除",
-    "history-fulfillment-created": "配货记录创建成功",
+    "history-fulfillment-created": "",
+    "history-fulfillment-delivered": "",
+    "history-fulfillment-shipped": "",
+    "history-fulfillment-transition": "",
     "history-items-cancelled": "{count}个已取消",
     "history-items-cancelled": "{count}个已取消",
     "history-order-cancelled": "订单已取消",
     "history-order-cancelled": "订单已取消",
+    "history-order-created": "",
     "history-order-fulfilled": "订单已配货",
     "history-order-fulfilled": "订单已配货",
     "history-order-transition": "订单状态从{from}更新至{to}",
     "history-order-transition": "订单状态从{from}更新至{to}",
     "history-payment-settled": "已结算付款",
     "history-payment-settled": "已结算付款",
@@ -583,6 +589,7 @@
     "refunded-count": "{count}个商品已退款",
     "refunded-count": "{count}个商品已退款",
     "return-to-stock": "返回商品至库存",
     "return-to-stock": "返回商品至库存",
     "search-by-order-code": "输入要搜索的订单编号",
     "search-by-order-code": "输入要搜索的订单编号",
+    "set-fulfillment-state": "",
     "settle-payment": "结算付款",
     "settle-payment": "结算付款",
     "settle-payment-error": "结算付款失败",
     "settle-payment-error": "结算付款失败",
     "settle-payment-success": "结算付款成功",
     "settle-payment-success": "结算付款成功",
@@ -597,11 +604,14 @@
     "state-all-orders": "所有订单",
     "state-all-orders": "所有订单",
     "state-arranging-payment": "正在付款",
     "state-arranging-payment": "正在付款",
     "state-cancelled": "已取消",
     "state-cancelled": "已取消",
-    "state-fulfilled": "已完成",
-    "state-partially-fulfilled": "部分配货",
+    "state-delivered": "已完成",
+    "state-partially-delivered": "部分配货",
+    "state-partially-shipped": "",
     "state-payment-authorized": "已授权支付",
     "state-payment-authorized": "已授权支付",
     "state-payment-settled": "已结算",
     "state-payment-settled": "已结算",
+    "state-shipped": "",
     "sub-total": "小计金额",
     "sub-total": "小计金额",
+    "successfully-updated-fulfillment": "",
     "total": "总计金额",
     "total": "总计金额",
     "tracking-code": "物流码",
     "tracking-code": "物流码",
     "transaction-id": "交易ID",
     "transaction-id": "交易ID",

+ 14 - 4
packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json

@@ -144,6 +144,7 @@
     "channel": "渠道",
     "channel": "渠道",
     "channels": "渠道",
     "channels": "渠道",
     "code": "編碼",
     "code": "編碼",
+    "collapse-entries": "",
     "confirm": "",
     "confirm": "",
     "confirm-delete-note": "",
     "confirm-delete-note": "",
     "confirm-navigation": "導航確認",
     "confirm-navigation": "導航確認",
@@ -158,11 +159,11 @@
     "disabled": "禁用",
     "disabled": "禁用",
     "discard-changes": "放弃修改",
     "discard-changes": "放弃修改",
     "display-custom-fields": "顯示客戶自訂欄位",
     "display-custom-fields": "顯示客戶自訂欄位",
-    "done": "完成",
     "edit": "編辑",
     "edit": "編辑",
     "edit-field": "編辑域",
     "edit-field": "編辑域",
     "edit-note": "",
     "edit-note": "",
     "enabled": "启用",
     "enabled": "启用",
+    "expand-entries": "",
     "extension-running-in-separate-window": "扩展已在另一個窗口启動",
     "extension-running-in-separate-window": "扩展已在另一個窗口启動",
     "guest": "游客",
     "guest": "游客",
     "hide-custom-fields": "隱藏客戶自訂欄位",
     "hide-custom-fields": "隱藏客戶自訂欄位",
@@ -522,6 +523,7 @@
     "amount": "金額",
     "amount": "金額",
     "billing-address": "",
     "billing-address": "",
     "cancel": "取消",
     "cancel": "取消",
+    "cancel-fulfillment": "",
     "cancel-order": "取消訂單",
     "cancel-order": "取消訂單",
     "cancel-reason-customer-request": "客户要求",
     "cancel-reason-customer-request": "客户要求",
     "cancel-reason-not-available": "產品無庫存",
     "cancel-reason-not-available": "產品無庫存",
@@ -538,9 +540,13 @@
     "fulfillment-method": "配貨方式",
     "fulfillment-method": "配貨方式",
     "history-coupon-code-applied": "優惠卷已使用",
     "history-coupon-code-applied": "優惠卷已使用",
     "history-coupon-code-removed": "優惠卷已移除",
     "history-coupon-code-removed": "優惠卷已移除",
-    "history-fulfillment-created": "配貨記錄建立成功",
+    "history-fulfillment-created": "",
+    "history-fulfillment-delivered": "",
+    "history-fulfillment-shipped": "",
+    "history-fulfillment-transition": "",
     "history-items-cancelled": "{count}個已取消",
     "history-items-cancelled": "{count}個已取消",
     "history-order-cancelled": "訂單已取消",
     "history-order-cancelled": "訂單已取消",
+    "history-order-created": "",
     "history-order-fulfilled": "訂單已配貨",
     "history-order-fulfilled": "訂單已配貨",
     "history-order-transition": "訂單狀態從{from}更新至{to}",
     "history-order-transition": "訂單狀態從{from}更新至{to}",
     "history-payment-settled": "已結算付款",
     "history-payment-settled": "已結算付款",
@@ -583,6 +589,7 @@
     "refunded-count": "{count}個商品已退款",
     "refunded-count": "{count}個商品已退款",
     "return-to-stock": "返回商品至庫存",
     "return-to-stock": "返回商品至庫存",
     "search-by-order-code": "輸入要搜索的訂單編號",
     "search-by-order-code": "輸入要搜索的訂單編號",
+    "set-fulfillment-state": "",
     "settle-payment": "結算付款",
     "settle-payment": "結算付款",
     "settle-payment-error": "結算付款失敗",
     "settle-payment-error": "結算付款失敗",
     "settle-payment-success": "結算付款成功",
     "settle-payment-success": "結算付款成功",
@@ -597,11 +604,14 @@
     "state-all-orders": "所有訂單",
     "state-all-orders": "所有訂單",
     "state-arranging-payment": "正在付款",
     "state-arranging-payment": "正在付款",
     "state-cancelled": "已取消",
     "state-cancelled": "已取消",
-    "state-fulfilled": "已完成",
-    "state-partially-fulfilled": "部分配貨",
+    "state-delivered": "已完成",
+    "state-partially-delivered": "部分配貨",
+    "state-partially-shipped": "",
     "state-payment-authorized": "已授權支付",
     "state-payment-authorized": "已授權支付",
     "state-payment-settled": "已結算",
     "state-payment-settled": "已結算",
+    "state-shipped": "",
     "sub-total": "小計金額",
     "sub-total": "小計金額",
+    "successfully-updated-fulfillment": "",
     "total": "總計金額",
     "total": "總計金額",
     "tracking-code": "物流碼",
     "tracking-code": "物流碼",
     "transaction-id": "交易編號",
     "transaction-id": "交易編號",

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

@@ -3,7 +3,7 @@
   "compilerOptions": {
   "compilerOptions": {
     "baseUrl": "./",
     "baseUrl": "./",
     "importHelpers": true,
     "importHelpers": true,
-    "module": "esnext",
+    "module": "es2020",
     "outDir": "./dist/out-tsc",
     "outDir": "./dist/out-tsc",
     "sourceMap": true,
     "sourceMap": true,
     "declaration": false,
     "declaration": false,
@@ -59,4 +59,4 @@
       ]
       ]
     }
     }
   }
   }
-}
+}

+ 15 - 13
packages/asset-server-plugin/e2e/asset-server-plugin.e2e-spec.ts

@@ -63,8 +63,8 @@ describe('AssetServerPlugin', () => {
         const { createAssets }: CreateAssets.Mutation = await adminClient.fileUploadMutation({
         const { createAssets }: CreateAssets.Mutation = await adminClient.fileUploadMutation({
             mutation: CREATE_ASSETS,
             mutation: CREATE_ASSETS,
             filePaths: filesToUpload,
             filePaths: filesToUpload,
-            mapVariables: (filePaths) => ({
-                input: filePaths.map((p) => ({ file: null })),
+            mapVariables: filePaths => ({
+                input: filePaths.map(p => ({ file: null })),
             }),
             }),
         });
         });
 
 
@@ -188,7 +188,7 @@ describe('AssetServerPlugin', () => {
         let testImages: CreateAssets.CreateAssets[] = [];
         let testImages: CreateAssets.CreateAssets[] = [];
 
 
         async function testMimeTypeOfAssetWithExt(ext: string, expectedMimeType: string) {
         async function testMimeTypeOfAssetWithExt(ext: string, expectedMimeType: string) {
-            const testImage = testImages.find((i) => i.source.endsWith(ext))!;
+            const testImage = testImages.find(i => i.source.endsWith(ext))!;
             const result = await fetch(testImage.source);
             const result = await fetch(testImage.source);
             const contentType = result.headers.get('Content-Type');
             const contentType = result.headers.get('Content-Type');
 
 
@@ -198,12 +198,12 @@ describe('AssetServerPlugin', () => {
         beforeAll(async () => {
         beforeAll(async () => {
             const formats = ['gif', 'jpg', 'png', 'svg', 'tiff', 'webp'];
             const formats = ['gif', 'jpg', 'png', 'svg', 'tiff', 'webp'];
 
 
-            const filesToUpload = formats.map((ext) => path.join(__dirname, `fixtures/assets/test.${ext}`));
+            const filesToUpload = formats.map(ext => path.join(__dirname, `fixtures/assets/test.${ext}`));
             const { createAssets }: CreateAssets.Mutation = await adminClient.fileUploadMutation({
             const { createAssets }: CreateAssets.Mutation = await adminClient.fileUploadMutation({
                 mutation: CREATE_ASSETS,
                 mutation: CREATE_ASSETS,
                 filePaths: filesToUpload,
                 filePaths: filesToUpload,
-                mapVariables: (filePaths) => ({
-                    input: filePaths.map((p) => ({ file: null })),
+                mapVariables: filePaths => ({
+                    input: filePaths.map(p => ({ file: null })),
                 }),
                 }),
             });
             });
 
 
@@ -239,13 +239,15 @@ describe('AssetServerPlugin', () => {
 export const CREATE_ASSETS = gql`
 export const CREATE_ASSETS = gql`
     mutation CreateAssets($input: [CreateAssetInput!]!) {
     mutation CreateAssets($input: [CreateAssetInput!]!) {
         createAssets(input: $input) {
         createAssets(input: $input) {
-            id
-            name
-            source
-            preview
-            focalPoint {
-                x
-                y
+            ... on Asset {
+                id
+                name
+                source
+                preview
+                focalPoint {
+                    x
+                    y
+                }
             }
             }
         }
         }
     }
     }

File diff suppressed because it is too large
+ 1623 - 1991
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts


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

@@ -18,21 +18,21 @@
     "access": "public"
     "access": "public"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@types/express": "^4.0.39",
-    "@types/fs-extra": "^8.0.1",
-    "@types/node-fetch": "^2.5.4",
-    "@types/sharp": "^0.24.0",
+    "@types/express": "^4.17.8",
+    "@types/fs-extra": "^9.0.1",
+    "@types/node-fetch": "^2.5.7",
+    "@types/sharp": "^0.26.0",
     "@vendure/common": "^0.15.0",
     "@vendure/common": "^0.15.0",
     "@vendure/core": "^0.15.2",
     "@vendure/core": "^0.15.2",
-    "aws-sdk": "^2.670.0",
-    "express": "^4.16.4",
-    "node-fetch": "^2.6.0",
-    "rimraf": "^3.0.0",
-    "typescript": "3.8.3"
+    "aws-sdk": "^2.766.0",
+    "express": "^4.17.1",
+    "node-fetch": "^2.6.1",
+    "rimraf": "^3.0.2",
+    "typescript": "4.0.3"
   },
   },
   "dependencies": {
   "dependencies": {
-    "file-type": "^14.3.0",
-    "fs-extra": "^9.0.0",
+    "file-type": "^15.0.1",
+    "fs-extra": "^9.0.1",
     "sharp": "0.25.2"
     "sharp": "0.25.2"
   }
   }
 }
 }

+ 5 - 5
packages/asset-server-plugin/src/plugin.ts

@@ -125,7 +125,7 @@ import { AssetServerOptions, ImageTransformPreset } from './types';
  */
  */
 @VendurePlugin({
 @VendurePlugin({
     imports: [PluginCommonModule, TerminusModule],
     imports: [PluginCommonModule, TerminusModule],
-    configuration: (config) => AssetServerPlugin.configure(config),
+    configuration: config => AssetServerPlugin.configure(config),
 })
 })
 export class AssetServerPlugin implements OnVendureBootstrap, OnVendureClose {
 export class AssetServerPlugin implements OnVendureBootstrap, OnVendureClose {
     private server: Server;
     private server: Server;
@@ -177,7 +177,7 @@ export class AssetServerPlugin implements OnVendureBootstrap, OnVendureClose {
     onVendureBootstrap(): void | Promise<void> {
     onVendureBootstrap(): void | Promise<void> {
         if (AssetServerPlugin.options.presets) {
         if (AssetServerPlugin.options.presets) {
             for (const preset of AssetServerPlugin.options.presets) {
             for (const preset of AssetServerPlugin.options.presets) {
-                const existingIndex = this.presets.findIndex((p) => p.name === preset.name);
+                const existingIndex = this.presets.findIndex(p => p.name === preset.name);
                 if (-1 < existingIndex) {
                 if (-1 < existingIndex) {
                     this.presets.splice(existingIndex, 1, preset);
                     this.presets.splice(existingIndex, 1, preset);
                 } else {
                 } else {
@@ -194,7 +194,7 @@ export class AssetServerPlugin implements OnVendureBootstrap, OnVendureClose {
 
 
     /** @internal */
     /** @internal */
     onVendureClose(): Promise<void> {
     onVendureClose(): Promise<void> {
-        return new Promise((resolve) => {
+        return new Promise(resolve => {
             this.server.close(() => resolve());
             this.server.close(() => resolve());
         });
         });
     }
     }
@@ -260,7 +260,7 @@ export class AssetServerPlugin implements OnVendureBootstrap, OnVendureClose {
                         res.status(404).send('Resource not found');
                         res.status(404).send('Resource not found');
                         return;
                         return;
                     }
                     }
-                    const image = await transformImage(file, req.query, this.presets || []);
+                    const image = await transformImage(file, req.query as any, this.presets || []);
                     try {
                     try {
                         const imageBuffer = await image.toBuffer();
                         const imageBuffer = await image.toBuffer();
                         if (!req.query.cache || req.query.cache === 'true') {
                         if (!req.query.cache || req.query.cache === 'true') {
@@ -292,7 +292,7 @@ export class AssetServerPlugin implements OnVendureBootstrap, OnVendureClose {
             const height = h || '';
             const height = h || '';
             imageParamHash = this.md5(`_transform_w${width}_h${height}_m${mode}${focalPoint}`);
             imageParamHash = this.md5(`_transform_w${width}_h${height}_m${mode}${focalPoint}`);
         } else if (preset) {
         } else if (preset) {
-            if (this.presets && !!this.presets.find((p) => p.name === preset)) {
+            if (this.presets && !!this.presets.find(p => p.name === preset)) {
                 imageParamHash = this.md5(`_transform_pre_${preset}${focalPoint}`);
                 imageParamHash = this.md5(`_transform_pre_${preset}${focalPoint}`);
             }
             }
         }
         }

+ 1 - 0
packages/asset-server-plugin/src/s3-asset-storage-strategy.ts

@@ -114,6 +114,7 @@ export function configureS3AssetStorage(s3Config: S3Config) {
  *
  *
  * @docsCategory asset-server-plugin
  * @docsCategory asset-server-plugin
  * @docsPage S3AssetStorageStrategy
  * @docsPage S3AssetStorageStrategy
+ * @docsWeight 0
  */
  */
 export class S3AssetStorageStrategy implements AssetStorageStrategy {
 export class S3AssetStorageStrategy implements AssetStorageStrategy {
     private AWS: typeof import('aws-sdk');
     private AWS: typeof import('aws-sdk');

+ 2 - 2
packages/common/package.json

@@ -17,7 +17,7 @@
     "lib/**/*"
     "lib/**/*"
   ],
   ],
   "devDependencies": {
   "devDependencies": {
-    "rimraf": "^3.0.0",
-    "typescript": "3.8.3"
+    "rimraf": "^3.0.2",
+    "typescript": "4.0.3"
   }
   }
 }
 }

File diff suppressed because it is too large
+ 469 - 413
packages/common/src/generated-shop-types.ts


File diff suppressed because it is too large
+ 889 - 1616
packages/common/src/generated-types.ts


+ 10 - 13
packages/common/src/omit.ts

@@ -21,21 +21,18 @@ export function omit<T extends any, K extends keyof T>(
     }
     }
 
 
     if (recursive && Array.isArray(obj)) {
     if (recursive && Array.isArray(obj)) {
-        return obj.map((item: any) => omit(item, keysToOmit, true));
+        return obj.map((item: any) => omit(item, keysToOmit, true)) as T;
     }
     }
 
 
-    return Object.keys(obj).reduce(
-        (output: any, key) => {
-            if (keysToOmit.includes(key)) {
-                return output;
-            }
-            if (recursive) {
-                return { ...output, [key]: omit((obj as any)[key], keysToOmit, true) };
-            }
-            return { ...output, [key]: (obj as any)[key] };
-        },
-        {} as Omit<T, K>,
-    );
+    return Object.keys(obj as object).reduce((output: any, key) => {
+        if (keysToOmit.includes(key)) {
+            return output;
+        }
+        if (recursive) {
+            return { ...output, [key]: omit((obj as any)[key], keysToOmit, true) };
+        }
+        return { ...output, [key]: (obj as any)[key] };
+    }, {} as Omit<T, K>);
 }
 }
 
 
 function isObject(input: any): input is object {
 function isObject(input: any): input is object {

+ 59 - 42
packages/core/e2e/asset.e2e-spec.ts

@@ -1,7 +1,7 @@
 /* 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 { mergeConfig } from '@vendure/core';
 import { mergeConfig } from '@vendure/core';
-import { createTestEnvironment } from '@vendure/testing';
+import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
@@ -10,6 +10,7 @@ import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-conf
 
 
 import { ASSET_FRAGMENT } from './graphql/fragments';
 import { ASSET_FRAGMENT } from './graphql/fragments';
 import {
 import {
+    AssetFragment,
     CreateAssets,
     CreateAssets,
     DeleteAsset,
     DeleteAsset,
     DeletionResult,
     DeletionResult,
@@ -26,7 +27,6 @@ import {
     GET_PRODUCT_WITH_VARIANTS,
     GET_PRODUCT_WITH_VARIANTS,
     UPDATE_ASSET,
     UPDATE_ASSET,
 } from './graphql/shared-definitions';
 } from './graphql/shared-definitions';
-import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 
 describe('Asset resolver', () => {
 describe('Asset resolver', () => {
     const { server, adminClient } = createTestEnvironment(
     const { server, adminClient } = createTestEnvironment(
@@ -137,6 +137,10 @@ describe('Asset resolver', () => {
     });
     });
 
 
     describe('createAssets', () => {
     describe('createAssets', () => {
+        function isAsset(input: CreateAssets.CreateAssets): input is AssetFragment {
+            return input.hasOwnProperty('name');
+        }
+
         it('permitted types by mime type', async () => {
         it('permitted types by mime type', async () => {
             const filesToUpload = [
             const filesToUpload = [
                 path.join(__dirname, 'fixtures/assets/pps1.jpg'),
                 path.join(__dirname, 'fixtures/assets/pps1.jpg'),
@@ -150,30 +154,30 @@ describe('Asset resolver', () => {
                 }),
                 }),
             });
             });
 
 
-            expect(createAssets.map(a => omit(a, ['id'])).sort((a, b) => (a.name < b.name ? -1 : 1))).toEqual(
-                [
-                    {
-                        fileSize: 1680,
-                        focalPoint: null,
-                        mimeType: 'image/jpeg',
-                        name: 'pps1.jpg',
-                        preview: 'test-url/test-assets/pps1__preview.jpg',
-                        source: 'test-url/test-assets/pps1.jpg',
-                        type: 'IMAGE',
-                    },
-                    {
-                        fileSize: 1680,
-                        focalPoint: null,
-                        mimeType: 'image/jpeg',
-                        name: 'pps2.jpg',
-                        preview: 'test-url/test-assets/pps2__preview.jpg',
-                        source: 'test-url/test-assets/pps2.jpg',
-                        type: 'IMAGE',
-                    },
-                ],
-            );
+            expect(createAssets.length).toBe(2);
+            const results = createAssets.filter(isAsset);
+            expect(results.map(a => omit(a, ['id'])).sort((a, b) => (a.name < b.name ? -1 : 1))).toEqual([
+                {
+                    fileSize: 1680,
+                    focalPoint: null,
+                    mimeType: 'image/jpeg',
+                    name: 'pps1.jpg',
+                    preview: 'test-url/test-assets/pps1__preview.jpg',
+                    source: 'test-url/test-assets/pps1.jpg',
+                    type: 'IMAGE',
+                },
+                {
+                    fileSize: 1680,
+                    focalPoint: null,
+                    mimeType: 'image/jpeg',
+                    name: 'pps2.jpg',
+                    preview: 'test-url/test-assets/pps2__preview.jpg',
+                    source: 'test-url/test-assets/pps2.jpg',
+                    type: 'IMAGE',
+                },
+            ]);
 
 
-            createdAssetId = createAssets[0].id;
+            createdAssetId = results[0].id;
         });
         });
 
 
         it('permitted type by file extension', async () => {
         it('permitted type by file extension', async () => {
@@ -186,7 +190,9 @@ describe('Asset resolver', () => {
                 }),
                 }),
             });
             });
 
 
-            expect(createAssets.map(a => omit(a, ['id']))).toEqual([
+            expect(createAssets.length).toBe(1);
+            const results = createAssets.filter(isAsset);
+            expect(results.map(a => omit(a, ['id']))).toEqual([
                 {
                 {
                     fileSize: 1680,
                     fileSize: 1680,
                     focalPoint: null,
                     focalPoint: null,
@@ -199,19 +205,23 @@ describe('Asset resolver', () => {
             ]);
             ]);
         });
         });
 
 
-        it(
-            'not permitted type',
-            assertThrowsWithMessage(async () => {
-                const filesToUpload = [path.join(__dirname, 'fixtures/assets/dummy.txt')];
-                const { createAssets }: CreateAssets.Mutation = await adminClient.fileUploadMutation({
-                    mutation: CREATE_ASSETS,
-                    filePaths: filesToUpload,
-                    mapVariables: filePaths => ({
-                        input: filePaths.map(p => ({ file: null })),
-                    }),
-                });
-            }, `The MIME type 'text/plain' is not permitted.`),
-        );
+        it('not permitted type', async () => {
+            const filesToUpload = [path.join(__dirname, 'fixtures/assets/dummy.txt')];
+            const { createAssets }: CreateAssets.Mutation = await adminClient.fileUploadMutation({
+                mutation: CREATE_ASSETS,
+                filePaths: filesToUpload,
+                mapVariables: filePaths => ({
+                    input: filePaths.map(p => ({ file: null })),
+                }),
+            });
+
+            expect(createAssets.length).toBe(1);
+            expect(createAssets[0]).toEqual({
+                message: `The MIME type 'text/plain' is not permitted.`,
+                mimeType: 'text/plain',
+                fileName: 'dummy.txt',
+            });
+        });
     });
     });
 
 
     describe('updateAsset', () => {
     describe('updateAsset', () => {
@@ -375,9 +385,16 @@ export const CREATE_ASSETS = gql`
     mutation CreateAssets($input: [CreateAssetInput!]!) {
     mutation CreateAssets($input: [CreateAssetInput!]!) {
         createAssets(input: $input) {
         createAssets(input: $input) {
             ...Asset
             ...Asset
-            focalPoint {
-                x
-                y
+            ... on Asset {
+                focalPoint {
+                    x
+                    y
+                }
+            }
+            ... on MimeTypeError {
+                message
+                fileName
+                mimeType
             }
             }
         }
         }
     }
     }

+ 10 - 10
packages/core/e2e/auth.e2e-spec.ts

@@ -11,6 +11,7 @@ import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-conf
 import {
 import {
     CreateAdministrator,
     CreateAdministrator,
     CreateRole,
     CreateRole,
+    ErrorCode,
     GetCustomerList,
     GetCustomerList,
     Me,
     Me,
     MutationCreateProductArgs,
     MutationCreateProductArgs,
@@ -76,12 +77,11 @@ describe('Authorization & permissions', () => {
                 customerEmailAddress = customers.items[0].emailAddress;
                 customerEmailAddress = customers.items[0].emailAddress;
             });
             });
 
 
-            it(
-                'cannot login',
-                assertThrowsWithMessage(async () => {
-                    await adminClient.asUserWithCredentials(customerEmailAddress, 'test');
-                }, 'The credentials did not match. Please check and try again'),
-            );
+            it('cannot login', async () => {
+                const result = await adminClient.asUserWithCredentials(customerEmailAddress, 'test');
+
+                expect(result.errorCode).toBe(ErrorCode.INVALID_CREDENTIALS_ERROR);
+            });
         });
         });
 
 
         describe('ReadCatalog permission', () => {
         describe('ReadCatalog permission', () => {
@@ -153,7 +153,9 @@ describe('Authorization & permissions', () => {
                     gql`
                     gql`
                         mutation CanCreateCustomer($input: CreateCustomerInput!) {
                         mutation CanCreateCustomer($input: CreateCustomerInput!) {
                             createCustomer(input: $input) {
                             createCustomer(input: $input) {
-                                id
+                                ... on Customer {
+                                    id
+                                }
                             }
                             }
                         }
                         }
                     `,
                     `,
@@ -214,9 +216,7 @@ describe('Authorization & permissions', () => {
 
 
         const role = roleResult.createRole;
         const role = roleResult.createRole;
 
 
-        const identifier = `${code}@${Math.random()
-            .toString(16)
-            .substr(2, 8)}`;
+        const identifier = `${code}@${Math.random().toString(16).substr(2, 8)}`;
         const password = `test`;
         const password = `test`;
 
 
         const adminResult = await adminClient.query<
         const adminResult = await adminClient.query<

+ 44 - 22
packages/core/e2e/authentication-strategy.e2e-spec.ts

@@ -1,16 +1,21 @@
+import { ErrorCode } from '@vendure/common/lib/generated-shop-types';
 import { pick } from '@vendure/common/lib/pick';
 import { pick } from '@vendure/common/lib/pick';
 import { mergeConfig } from '@vendure/core';
 import { mergeConfig } from '@vendure/core';
-import { createTestEnvironment } from '@vendure/testing';
+import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 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 { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
 import { NativeAuthenticationStrategy } from '../src/config/auth/native-authentication-strategy';
 import { NativeAuthenticationStrategy } from '../src/config/auth/native-authentication-strategy';
+import { DefaultLogger } from '../src/config/logger/default-logger';
 
 
 import { TestAuthenticationStrategy, VALID_AUTH_TOKEN } from './fixtures/test-authentication-strategies';
 import { TestAuthenticationStrategy, VALID_AUTH_TOKEN } from './fixtures/test-authentication-strategies';
+import { CURRENT_USER_FRAGMENT } from './graphql/fragments';
 import {
 import {
     Authenticate,
     Authenticate,
+    CurrentUser,
+    CurrentUserFragment,
     GetCustomerHistory,
     GetCustomerHistory,
     GetCustomers,
     GetCustomers,
     GetCustomerUserAuth,
     GetCustomerUserAuth,
@@ -47,6 +52,10 @@ describe('AuthenticationStrategy', () => {
         await server.destroy();
         await server.destroy();
     });
     });
 
 
+    const currentUserGuard: ErrorResultGuard<CurrentUserFragment> = createErrorResultGuard<
+        CurrentUserFragment
+    >(input => input.identifier != null);
+
     describe('external auth', () => {
     describe('external auth', () => {
         const userData = {
         const userData = {
             email: 'test@email.com',
             email: 'test@email.com',
@@ -55,24 +64,24 @@ describe('AuthenticationStrategy', () => {
         };
         };
         let newCustomerId: string;
         let newCustomerId: string;
 
 
-        it(
-            'fails with a bad token',
-            assertThrowsWithMessage(async () => {
-                await shopClient.query(AUTHENTICATE, {
-                    input: {
-                        test_strategy: {
-                            token: 'bad-token',
-                        },
+        it('fails with a bad token', async () => {
+            const { authenticate } = await shopClient.query(AUTHENTICATE, {
+                input: {
+                    test_strategy: {
+                        token: 'bad-token',
                     },
                     },
-                });
-            }, 'The credentials did not match. Please check and try again'),
-        );
+                },
+            });
+
+            expect(authenticate.message).toBe('The provided credentials are invalid');
+            expect(authenticate.errorCode).toBe(ErrorCode.INVALID_CREDENTIALS_ERROR);
+        });
 
 
         it('creates a new Customer with valid token', async () => {
         it('creates a new Customer with valid token', async () => {
             const { customers: before } = await adminClient.query<GetCustomers.Query>(GET_CUSTOMERS);
             const { customers: before } = await adminClient.query<GetCustomers.Query>(GET_CUSTOMERS);
             expect(before.totalItems).toBe(1);
             expect(before.totalItems).toBe(1);
 
 
-            const result = await shopClient.query<Authenticate.Mutation>(AUTHENTICATE, {
+            const { authenticate } = await shopClient.query<Authenticate.Mutation>(AUTHENTICATE, {
                 input: {
                 input: {
                     test_strategy: {
                     test_strategy: {
                         token: VALID_AUTH_TOKEN,
                         token: VALID_AUTH_TOKEN,
@@ -80,7 +89,9 @@ describe('AuthenticationStrategy', () => {
                     },
                     },
                 },
                 },
             });
             });
-            expect(result.authenticate.user.identifier).toEqual(userData.email);
+            currentUserGuard.assertSuccess(authenticate);
+
+            expect(authenticate.identifier).toEqual(userData.email);
 
 
             const { customers: after } = await adminClient.query<GetCustomers.Query>(GET_CUSTOMERS);
             const { customers: after } = await adminClient.query<GetCustomers.Query>(GET_CUSTOMERS);
             expect(after.totalItems).toBe(2);
             expect(after.totalItems).toBe(2);
@@ -99,7 +110,9 @@ describe('AuthenticationStrategy', () => {
                 id: newCustomerId,
                 id: newCustomerId,
             });
             });
 
 
-            expect(customer?.history.items.map(pick(['type', 'data']))).toEqual([
+            expect(
+                customer?.history.items.sort((a, b) => (a.id > b.id ? 1 : -1)).map(pick(['type', 'data'])),
+            ).toEqual([
                 {
                 {
                     type: HistoryEntryType.CUSTOMER_REGISTERED,
                     type: HistoryEntryType.CUSTOMER_REGISTERED,
                     data: {
                     data: {
@@ -138,7 +151,7 @@ describe('AuthenticationStrategy', () => {
         });
         });
 
 
         it('logging in again re-uses created User & Customer', async () => {
         it('logging in again re-uses created User & Customer', async () => {
-            const result = await shopClient.query<Authenticate.Mutation>(AUTHENTICATE, {
+            const { authenticate } = await shopClient.query<Authenticate.Mutation>(AUTHENTICATE, {
                 input: {
                 input: {
                     test_strategy: {
                     test_strategy: {
                         token: VALID_AUTH_TOKEN,
                         token: VALID_AUTH_TOKEN,
@@ -146,7 +159,9 @@ describe('AuthenticationStrategy', () => {
                     },
                     },
                 },
                 },
             });
             });
-            expect(result.authenticate.user.identifier).toEqual(userData.email);
+            currentUserGuard.assertSuccess(authenticate);
+
+            expect(authenticate.identifier).toEqual(userData.email);
 
 
             const { customers: after } = await adminClient.query<GetCustomers.Query>(GET_CUSTOMERS);
             const { customers: after } = await adminClient.query<GetCustomers.Query>(GET_CUSTOMERS);
             expect(after.totalItems).toBe(2);
             expect(after.totalItems).toBe(2);
@@ -157,6 +172,10 @@ describe('AuthenticationStrategy', () => {
         });
         });
 
 
         it('registerCustomerAccount with external email', async () => {
         it('registerCustomerAccount with external email', async () => {
+            const successErrorGuard: ErrorResultGuard<{ success: boolean }> = createErrorResultGuard<{
+                success: boolean;
+            }>(input => input.success != null);
+
             const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
             const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
                 REGISTER_ACCOUNT,
                 REGISTER_ACCOUNT,
                 {
                 {
@@ -165,8 +184,9 @@ describe('AuthenticationStrategy', () => {
                     },
                     },
                 },
                 },
             );
             );
+            successErrorGuard.assertSuccess(registerCustomerAccount);
 
 
-            expect(registerCustomerAccount).toBe(true);
+            expect(registerCustomerAccount.success).toBe(true);
             const { customer } = await adminClient.query<
             const { customer } = await adminClient.query<
                 GetCustomerUserAuth.Query,
                 GetCustomerUserAuth.Query,
                 GetCustomerUserAuth.Variables
                 GetCustomerUserAuth.Variables
@@ -202,12 +222,14 @@ describe('AuthenticationStrategy', () => {
 const AUTHENTICATE = gql`
 const AUTHENTICATE = gql`
     mutation Authenticate($input: AuthenticationInput!) {
     mutation Authenticate($input: AuthenticationInput!) {
         authenticate(input: $input) {
         authenticate(input: $input) {
-            user {
-                id
-                identifier
+            ...CurrentUser
+            ... on ErrorResult {
+                errorCode
+                message
             }
             }
         }
         }
     }
     }
+    ${CURRENT_USER_FRAGMENT}
 `;
 `;
 
 
 const GET_CUSTOMERS = gql`
 const GET_CUSTOMERS = gql`

+ 65 - 34
packages/core/e2e/channel.e2e-spec.ts

@@ -1,20 +1,27 @@
 /* tslint:disable:no-non-null-assertion */
 /* tslint:disable:no-non-null-assertion */
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
-import { createTestEnvironment, E2E_DEFAULT_CHANNEL_TOKEN } from '@vendure/testing';
+import {
+    createErrorResultGuard,
+    createTestEnvironment,
+    E2E_DEFAULT_CHANNEL_TOKEN,
+    ErrorResultGuard,
+} from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 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 { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
 
 
 import {
 import {
     AssignProductsToChannel,
     AssignProductsToChannel,
+    ChannelFragment,
     CreateAdministrator,
     CreateAdministrator,
     CreateChannel,
     CreateChannel,
     CreateRole,
     CreateRole,
     CurrencyCode,
     CurrencyCode,
     DeleteChannel,
     DeleteChannel,
     DeletionResult,
     DeletionResult,
+    ErrorCode,
     GetChannels,
     GetChannels,
     GetCustomerList,
     GetCustomerList,
     GetProductWithVariants,
     GetProductWithVariants,
@@ -23,7 +30,7 @@ import {
     Permission,
     Permission,
     RemoveProductsFromChannel,
     RemoveProductsFromChannel,
     UpdateChannel,
     UpdateChannel,
-    UpdateGlobalSettings,
+    UpdateGlobalLanguages,
 } from './graphql/generated-e2e-admin-types';
 } from './graphql/generated-e2e-admin-types';
 import {
 import {
     ASSIGN_PRODUCT_TO_CHANNEL,
     ASSIGN_PRODUCT_TO_CHANNEL,
@@ -45,6 +52,10 @@ describe('Channels', () => {
     let secondChannelAdminRole: CreateRole.CreateRole;
     let secondChannelAdminRole: CreateRole.CreateRole;
     let customerUser: GetCustomerList.Items;
     let customerUser: GetCustomerList.Items;
 
 
+    const channelGuard: ErrorResultGuard<ChannelFragment> = createErrorResultGuard<ChannelFragment>(
+        input => !!input.defaultLanguageCode,
+    );
+
     beforeAll(async () => {
     beforeAll(async () => {
         await server.init({
         await server.init({
             initialData,
             initialData,
@@ -66,6 +77,30 @@ describe('Channels', () => {
         await server.destroy();
         await server.destroy();
     });
     });
 
 
+    it('createChannel returns error result defaultLanguageCode not available', async () => {
+        const { createChannel } = await adminClient.query<CreateChannel.Mutation, CreateChannel.Variables>(
+            CREATE_CHANNEL,
+            {
+                input: {
+                    code: 'second-channel',
+                    token: SECOND_CHANNEL_TOKEN,
+                    defaultLanguageCode: LanguageCode.zh,
+                    currencyCode: CurrencyCode.GBP,
+                    pricesIncludeTax: true,
+                    defaultShippingZoneId: 'T_1',
+                    defaultTaxZoneId: 'T_1',
+                },
+            },
+        );
+        channelGuard.assertErrorResult(createChannel);
+
+        expect(createChannel.message).toBe(
+            'Language "zh" is not available. First enable it via GlobalSettings and try again',
+        );
+        expect(createChannel.errorCode).toBe(ErrorCode.LANGUAGE_NOT_AVAILABLE_ERROR);
+        expect(createChannel.languageCode).toBe('zh');
+    });
+
     it('create a new Channel', async () => {
     it('create a new Channel', async () => {
         const { createChannel } = await adminClient.query<CreateChannel.Mutation, CreateChannel.Variables>(
         const { createChannel } = await adminClient.query<CreateChannel.Mutation, CreateChannel.Variables>(
             CREATE_CHANNEL,
             CREATE_CHANNEL,
@@ -81,6 +116,7 @@ describe('Channels', () => {
                 },
                 },
             },
             },
         );
         );
+        channelGuard.assertSuccess(createChannel);
 
 
         expect(createChannel).toEqual({
         expect(createChannel).toEqual({
             id: 'T_2',
             id: 'T_2',
@@ -356,21 +392,28 @@ describe('Channels', () => {
     });
     });
 
 
     describe('setting defaultLanguage', () => {
     describe('setting defaultLanguage', () => {
-        it(
-            'throws if languageCode not in availableLanguages',
-            assertThrowsWithMessage(async () => {
-                await adminClient.query<UpdateChannel.Mutation, UpdateChannel.Variables>(UPDATE_CHANNEL, {
-                    input: {
-                        id: 'T_1',
-                        defaultLanguageCode: LanguageCode.zh,
-                    },
-                });
-            }, 'Language "zh" is not available. First enable it via GlobalSettings and try again.'),
-        );
+        it('returns error result if languageCode not in availableLanguages', async () => {
+            const { updateChannel } = await adminClient.query<
+                UpdateChannel.Mutation,
+                UpdateChannel.Variables
+            >(UPDATE_CHANNEL, {
+                input: {
+                    id: 'T_1',
+                    defaultLanguageCode: LanguageCode.zh,
+                },
+            });
+            channelGuard.assertErrorResult(updateChannel);
+
+            expect(updateChannel.message).toBe(
+                'Language "zh" is not available. First enable it via GlobalSettings and try again',
+            );
+            expect(updateChannel.errorCode).toBe(ErrorCode.LANGUAGE_NOT_AVAILABLE_ERROR);
+            expect(updateChannel.languageCode).toBe('zh');
+        });
 
 
         it('allows setting to an available language', async () => {
         it('allows setting to an available language', async () => {
-            await adminClient.query<UpdateGlobalSettings.Mutation, UpdateGlobalSettings.Variables>(
-                UPDATE_GLOBAL_SETTINGS,
+            await adminClient.query<UpdateGlobalLanguages.Mutation, UpdateGlobalLanguages.Variables>(
+                UPDATE_GLOBAL_LANGUAGES,
                 {
                 {
                     input: {
                     input: {
                         availableLanguages: [LanguageCode.en, LanguageCode.zh],
                         availableLanguages: [LanguageCode.en, LanguageCode.zh],
@@ -390,20 +433,6 @@ describe('Channels', () => {
 
 
             expect(updateChannel.defaultLanguageCode).toBe(LanguageCode.zh);
             expect(updateChannel.defaultLanguageCode).toBe(LanguageCode.zh);
         });
         });
-
-        it(
-            'attempting to remove availableLanguage when used by a Channel throws',
-            assertThrowsWithMessage(async () => {
-                await adminClient.query<UpdateGlobalSettings.Mutation, UpdateGlobalSettings.Variables>(
-                    UPDATE_GLOBAL_SETTINGS,
-                    {
-                        input: {
-                            availableLanguages: [LanguageCode.en],
-                        },
-                    },
-                );
-            }, 'Cannot remove make language "zh" unavailable as it is used as the defaultLanguage by the channel "__default_channel__"'),
-        );
     });
     });
 
 
     it('deleteChannel', async () => {
     it('deleteChannel', async () => {
@@ -461,11 +490,13 @@ const DELETE_CHANNEL = gql`
     }
     }
 `;
 `;
 
 
-const UPDATE_GLOBAL_SETTINGS = gql`
-    mutation UpdateGlobalSettings($input: UpdateGlobalSettingsInput!) {
+const UPDATE_GLOBAL_LANGUAGES = gql`
+    mutation UpdateGlobalLanguages($input: UpdateGlobalSettingsInput!) {
         updateGlobalSettings(input: $input) {
         updateGlobalSettings(input: $input) {
-            id
-            availableLanguages
+            ... on GlobalSettings {
+                id
+                availableLanguages
+            }
         }
         }
     }
     }
 `;
 `;

Some files were not shown because too many files changed in this diff