Просмотр исходного кода

feat(admin-ui): Draft Order creation UI

Relates to #1453
Michael Bromley 3 лет назад
Родитель
Сommit
d15cd3451f
62 измененных файлов с 2369 добавлено и 111 удалено
  1. 28 28
      packages/admin-ui/i18n-coverage.json
  2. 459 6
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  3. 30 0
      packages/admin-ui/src/lib/core/src/common/introspection-result.ts
  4. 125 0
      packages/admin-ui/src/lib/core/src/data/definitions/order-definitions.ts
  5. 6 0
      packages/admin-ui/src/lib/core/src/data/definitions/product-definitions.ts
  6. 6 1
      packages/admin-ui/src/lib/core/src/data/providers/customer-data.service.ts
  7. 124 0
      packages/admin-ui/src/lib/core/src/data/providers/order-data.service.ts
  8. 6 0
      packages/admin-ui/src/lib/core/src/data/utils/remove-readonly-custom-fields.ts
  9. 1 0
      packages/admin-ui/src/lib/core/src/shared/components/order-state-label/order-state-label.component.ts
  10. 3 1
      packages/admin-ui/src/lib/core/src/shared/components/product-selector/product-selector.component.scss
  11. 4 0
      packages/admin-ui/src/lib/core/src/shared/components/radio-card/radio-card-fieldset.component.scss
  12. 69 0
      packages/admin-ui/src/lib/core/src/shared/components/radio-card/radio-card-fieldset.component.ts
  13. 21 0
      packages/admin-ui/src/lib/core/src/shared/components/radio-card/radio-card.component.html
  14. 36 0
      packages/admin-ui/src/lib/core/src/shared/components/radio-card/radio-card.component.scss
  15. 63 0
      packages/admin-ui/src/lib/core/src/shared/components/radio-card/radio-card.component.ts
  16. 1 0
      packages/admin-ui/src/lib/core/src/shared/pipes/state-i18n-token.pipe.ts
  17. 4 0
      packages/admin-ui/src/lib/core/src/shared/shared.module.ts
  18. 20 0
      packages/admin-ui/src/lib/order/src/components/coupon-code-selector/coupon-code-selector.component.html
  19. 0 0
      packages/admin-ui/src/lib/order/src/components/coupon-code-selector/coupon-code-selector.component.scss
  20. 43 0
      packages/admin-ui/src/lib/order/src/components/coupon-code-selector/coupon-code-selector.component.ts
  21. 175 0
      packages/admin-ui/src/lib/order/src/components/draft-order-detail/draft-order-detail.component.html
  22. 0 0
      packages/admin-ui/src/lib/order/src/components/draft-order-detail/draft-order-detail.component.scss
  23. 234 0
      packages/admin-ui/src/lib/order/src/components/draft-order-detail/draft-order-detail.component.ts
  24. 52 0
      packages/admin-ui/src/lib/order/src/components/draft-order-variant-selector/draft-order-variant-selector.component.html
  25. 25 0
      packages/admin-ui/src/lib/order/src/components/draft-order-variant-selector/draft-order-variant-selector.component.scss
  26. 60 0
      packages/admin-ui/src/lib/order/src/components/draft-order-variant-selector/draft-order-variant-selector.component.ts
  27. 1 0
      packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.html
  28. 3 18
      packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.html
  29. 1 18
      packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.ts
  30. 11 3
      packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.html
  31. 9 1
      packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.ts
  32. 8 0
      packages/admin-ui/src/lib/order/src/components/order-table/order-table-mixin.scss
  33. 30 13
      packages/admin-ui/src/lib/order/src/components/order-table/order-table.component.html
  34. 11 2
      packages/admin-ui/src/lib/order/src/components/order-table/order-table.component.ts
  35. 45 0
      packages/admin-ui/src/lib/order/src/components/select-address-dialog/select-address-dialog.component.html
  36. 0 0
      packages/admin-ui/src/lib/order/src/components/select-address-dialog/select-address-dialog.component.scss
  37. 115 0
      packages/admin-ui/src/lib/order/src/components/select-address-dialog/select-address-dialog.component.ts
  38. 14 0
      packages/admin-ui/src/lib/order/src/components/select-address-dialog/select-address-dialog.graphql.ts
  39. 74 0
      packages/admin-ui/src/lib/order/src/components/select-customer-dialog/select-customer-dialog.component.html
  40. 0 0
      packages/admin-ui/src/lib/order/src/components/select-customer-dialog/select-customer-dialog.component.scss
  41. 72 0
      packages/admin-ui/src/lib/order/src/components/select-customer-dialog/select-customer-dialog.component.ts
  42. 35 0
      packages/admin-ui/src/lib/order/src/components/select-shipping-method-dialog/select-shipping-method-dialog.component.html
  43. 0 0
      packages/admin-ui/src/lib/order/src/components/select-shipping-method-dialog/select-shipping-method-dialog.component.scss
  44. 45 0
      packages/admin-ui/src/lib/order/src/components/select-shipping-method-dialog/select-shipping-method-dialog.component.ts
  45. 12 0
      packages/admin-ui/src/lib/order/src/order.module.ts
  46. 21 3
      packages/admin-ui/src/lib/order/src/order.routes.ts
  47. 46 17
      packages/admin-ui/src/lib/order/src/providers/routing/order-resolver.ts
  48. 35 0
      packages/admin-ui/src/lib/order/src/providers/routing/order.guard.ts
  49. 14 0
      packages/admin-ui/src/lib/static/i18n-messages/cs.json
  50. 14 0
      packages/admin-ui/src/lib/static/i18n-messages/de.json
  51. 14 0
      packages/admin-ui/src/lib/static/i18n-messages/en.json
  52. 14 0
      packages/admin-ui/src/lib/static/i18n-messages/es.json
  53. 14 0
      packages/admin-ui/src/lib/static/i18n-messages/fr.json
  54. 14 0
      packages/admin-ui/src/lib/static/i18n-messages/it.json
  55. 14 0
      packages/admin-ui/src/lib/static/i18n-messages/pl.json
  56. 14 0
      packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json
  57. 14 0
      packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json
  58. 14 0
      packages/admin-ui/src/lib/static/i18n-messages/ru.json
  59. 14 0
      packages/admin-ui/src/lib/static/i18n-messages/uk.json
  60. 14 0
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json
  61. 14 0
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json
  62. 4 0
      packages/admin-ui/src/lib/static/styles/global/_utilities.scss

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

@@ -1,71 +1,71 @@
 {
-  "generatedOn": "2022-09-28T09:24:11.676Z",
-  "lastCommit": "76d1ca6a70122047781465079dc09c2ed66f5246",
+  "generatedOn": "2022-10-18T18:49:58.336Z",
+  "lastCommit": "43421c67f52ac66880b606a17fe62b19d6385d85",
   "translationStatus": {
     "cs": {
-      "tokenCount": 673,
+      "tokenCount": 687,
       "translatedCount": 593,
-      "percentage": 88
+      "percentage": 86
     },
     "de": {
-      "tokenCount": 673,
+      "tokenCount": 687,
       "translatedCount": 572,
-      "percentage": 85
+      "percentage": 83
     },
     "en": {
-      "tokenCount": 673,
-      "translatedCount": 672,
+      "tokenCount": 687,
+      "translatedCount": 685,
       "percentage": 100
     },
     "es": {
-      "tokenCount": 673,
+      "tokenCount": 687,
       "translatedCount": 624,
-      "percentage": 93
+      "percentage": 91
     },
     "fr": {
-      "tokenCount": 673,
+      "tokenCount": 687,
       "translatedCount": 614,
-      "percentage": 91
+      "percentage": 89
     },
     "it": {
-      "tokenCount": 673,
+      "tokenCount": 687,
       "translatedCount": 622,
-      "percentage": 92
+      "percentage": 91
     },
     "pl": {
-      "tokenCount": 673,
+      "tokenCount": 687,
       "translatedCount": 407,
-      "percentage": 60
+      "percentage": 59
     },
     "pt_BR": {
-      "tokenCount": 673,
+      "tokenCount": 687,
       "translatedCount": 591,
-      "percentage": 88
+      "percentage": 86
     },
     "pt_PT": {
-      "tokenCount": 673,
+      "tokenCount": 687,
       "translatedCount": 635,
-      "percentage": 94
+      "percentage": 92
     },
     "ru": {
-      "tokenCount": 673,
+      "tokenCount": 687,
       "translatedCount": 621,
-      "percentage": 92
+      "percentage": 90
     },
     "uk": {
-      "tokenCount": 673,
+      "tokenCount": 687,
       "translatedCount": 621,
-      "percentage": 92
+      "percentage": 90
     },
     "zh_Hans": {
-      "tokenCount": 673,
+      "tokenCount": 687,
       "translatedCount": 559,
-      "percentage": 83
+      "percentage": 81
     },
     "zh_Hant": {
-      "tokenCount": 673,
+      "tokenCount": 687,
       "translatedCount": 387,
-      "percentage": 58
+      "percentage": 56
     }
   }
 }

+ 459 - 6
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -24,6 +24,13 @@ export type AddFulfillmentToOrderResult = Fulfillment | EmptyOrderLineSelectionE
 export type AddItemInput = {
   productVariantId: Scalars['ID'];
   quantity: Scalars['Int'];
+  customFields?: Maybe<OrderLineCustomFieldsInput>;
+};
+
+export type AddItemToDraftOrderInput = {
+  productVariantId: Scalars['ID'];
+  quantity: Scalars['Int'];
+  customFields?: Maybe<OrderLineCustomFieldsInput>;
 };
 
 export type AddManualPaymentToOrderResult = Order | ManualPaymentStateError;
@@ -59,9 +66,16 @@ export type Address = Node & {
   customFields?: Maybe<Scalars['JSON']>;
 };
 
+export type AdjustDraftOrderLineInput = {
+  orderLineId: Scalars['ID'];
+  quantity: Scalars['Int'];
+  customFields?: Maybe<OrderLineCustomFieldsInput>;
+};
+
 export type AdjustOrderLineInput = {
   orderLineId: Scalars['ID'];
   quantity: Scalars['Int'];
+  customFields?: Maybe<OrderLineCustomFieldsInput>;
 };
 
 export type Adjustment = {
@@ -156,6 +170,8 @@ export type AlreadyRefundedError = ErrorResult & {
   refundId: Scalars['ID'];
 };
 
+export type ApplyCouponCodeResult = Order | CouponCodeExpiredError | CouponCodeInvalidError | CouponCodeLimitError;
+
 export type Asset = Node & {
   __typename?: 'Asset';
   tags: Array<Tag>;
@@ -1469,7 +1485,10 @@ export enum ErrorCode {
   INSUFFICIENT_STOCK_ERROR = 'INSUFFICIENT_STOCK_ERROR',
   COUPON_CODE_INVALID_ERROR = 'COUPON_CODE_INVALID_ERROR',
   COUPON_CODE_EXPIRED_ERROR = 'COUPON_CODE_EXPIRED_ERROR',
-  COUPON_CODE_LIMIT_ERROR = 'COUPON_CODE_LIMIT_ERROR'
+  COUPON_CODE_LIMIT_ERROR = 'COUPON_CODE_LIMIT_ERROR',
+  ORDER_MODIFICATION_ERROR = 'ORDER_MODIFICATION_ERROR',
+  INELIGIBLE_SHIPPING_METHOD_ERROR = 'INELIGIBLE_SHIPPING_METHOD_ERROR',
+  NO_ACTIVE_ORDER_ERROR = 'NO_ACTIVE_ORDER_ERROR'
 }
 
 export type ErrorResult = {
@@ -1765,6 +1784,13 @@ export type ImportInfo = {
   imported: Scalars['Int'];
 };
 
+/** Returned when attempting to set a ShippingMethod for which the Order is not eligible */
+export type IneligibleShippingMethodError = ErrorResult & {
+  __typename?: 'IneligibleShippingMethodError';
+  errorCode: ErrorCode;
+  message: Scalars['String'];
+};
+
 /** Returned when attempting to add more items to the Order than are available */
 export type InsufficientStockError = ErrorResult & {
   __typename?: 'InsufficientStockError';
@@ -2320,6 +2346,7 @@ export type ModifyOrderInput = {
   refund?: Maybe<AdministratorRefundInput>;
   options?: Maybe<ModifyOrderOptions>;
   couponCodes?: Maybe<Array<Scalars['String']>>;
+  customFields?: Maybe<UpdateOrderCustomFieldsInput>;
 };
 
 export type ModifyOrderOptions = {
@@ -2347,6 +2374,8 @@ export type Mutation = {
   /** Add Customers to a CustomerGroup */
   addCustomersToGroup: CustomerGroup;
   addFulfillmentToOrder: AddFulfillmentToOrderResult;
+  /** Adds an item to the draft Order. */
+  addItemToDraftOrder: UpdateOrderItemsResult;
   /**
    * Used to manually create a new Payment against an Order.
    * This can be used by an Administrator when an Order is in the ArrangingPayment state.
@@ -2363,6 +2392,10 @@ export type Mutation = {
   addNoteToOrder: Order;
   /** Add an OptionGroup to a Product */
   addOptionGroupToProduct: Product;
+  /** Adjusts a draft OrderLine. If custom fields are defined on the OrderLine entity, a third argument 'customFields' of type `OrderLineCustomFieldsInput` will be available. */
+  adjustDraftOrderLine: UpdateOrderItemsResult;
+  /** Applies the given coupon code to the draft Order */
+  applyCouponCodeToDraftOrder: ApplyCouponCodeResult;
   /** Assign assets to channel */
   assignAssetsToChannel: Array<Asset>;
   /** Assigns Collections to the specified Channel */
@@ -2398,6 +2431,8 @@ export type Mutation = {
   createCustomerAddress: Address;
   /** Create a new CustomerGroup */
   createCustomerGroup: CustomerGroup;
+  /** Creates a draft Order */
+  createDraftOrder: Order;
   /** Create a new Facet */
   createFacet: Facet;
   /** Create one or more FacetValues */
@@ -2446,6 +2481,8 @@ export type Mutation = {
   /** Delete a CustomerGroup */
   deleteCustomerGroup: DeletionResponse;
   deleteCustomerNote: DeletionResponse;
+  /** Deletes a draft Order */
+  deleteDraftOrder: DeletionResponse;
   /** Delete an existing Facet */
   deleteFacet: DeletionResponse;
   /** Delete one or more FacetValues */
@@ -2494,8 +2531,12 @@ export type Mutation = {
   reindex: Job;
   /** Removes Collections from the specified Channel */
   removeCollectionsFromChannel: Array<Collection>;
+  /** Removes the given coupon code from the draft Order */
+  removeCouponCodeFromDraftOrder?: Maybe<Order>;
   /** Remove Customers from a CustomerGroup */
   removeCustomersFromGroup: CustomerGroup;
+  /** Remove an OrderLine from the draft Order */
+  removeDraftOrderLine: RemoveOrderItemsResult;
   /** Removes Facets from the specified Channel */
   removeFacetsFromChannel: Array<RemoveFacetFromChannelResult>;
   /** Remove members from a Zone */
@@ -2517,7 +2558,16 @@ export type Mutation = {
   setAsLoggedIn: UserStatus;
   setAsLoggedOut: UserStatus;
   setContentLanguage: LanguageCode;
+  setCustomerForDraftOrder: SetCustomerForDraftOrderResult;
   setDisplayUiExtensionPoints: Scalars['Boolean'];
+  /** Sets the billing address for a draft Order */
+  setDraftOrderBillingAddress: Order;
+  /** Allows any custom fields to be set for the active order */
+  setDraftOrderCustomFields: Order;
+  /** Sets the shipping address for a draft Order */
+  setDraftOrderShippingAddress: Order;
+  /** Sets the shipping method by id, which can be obtained with the `eligibleShippingMethodsForDraftOrder` query */
+  setDraftOrderShippingMethod: SetOrderShippingMethodResult;
   setOrderCustomFields?: Maybe<Order>;
   setUiLanguage: LanguageCode;
   setUiLocale?: Maybe<Scalars['String']>;
@@ -2592,6 +2642,12 @@ export type MutationAddFulfillmentToOrderArgs = {
 };
 
 
+export type MutationAddItemToDraftOrderArgs = {
+  orderId: Scalars['ID'];
+  input: AddItemToDraftOrderInput;
+};
+
+
 export type MutationAddManualPaymentToOrderArgs = {
   input: ManualPaymentInput;
 };
@@ -2619,6 +2675,18 @@ export type MutationAddOptionGroupToProductArgs = {
 };
 
 
+export type MutationAdjustDraftOrderLineArgs = {
+  orderId: Scalars['ID'];
+  input: AdjustDraftOrderLineInput;
+};
+
+
+export type MutationApplyCouponCodeToDraftOrderArgs = {
+  orderId: Scalars['ID'];
+  couponCode: Scalars['String'];
+};
+
+
 export type MutationAssignAssetsToChannelArgs = {
   input: AssignAssetsToChannelInput;
 };
@@ -2843,6 +2911,11 @@ export type MutationDeleteCustomerNoteArgs = {
 };
 
 
+export type MutationDeleteDraftOrderArgs = {
+  orderId: Scalars['ID'];
+};
+
+
 export type MutationDeleteFacetArgs = {
   id: Scalars['ID'];
   force?: Maybe<Scalars['Boolean']>;
@@ -2969,12 +3042,24 @@ export type MutationRemoveCollectionsFromChannelArgs = {
 };
 
 
+export type MutationRemoveCouponCodeFromDraftOrderArgs = {
+  orderId: Scalars['ID'];
+  couponCode: Scalars['String'];
+};
+
+
 export type MutationRemoveCustomersFromGroupArgs = {
   customerGroupId: Scalars['ID'];
   customerIds: Array<Scalars['ID']>;
 };
 
 
+export type MutationRemoveDraftOrderLineArgs = {
+  orderId: Scalars['ID'];
+  orderLineId: Scalars['ID'];
+};
+
+
 export type MutationRemoveFacetsFromChannelArgs = {
   input: RemoveFacetsFromChannelInput;
 };
@@ -3028,11 +3113,42 @@ export type MutationSetContentLanguageArgs = {
 };
 
 
+export type MutationSetCustomerForDraftOrderArgs = {
+  orderId: Scalars['ID'];
+  customerId?: Maybe<Scalars['ID']>;
+  input?: Maybe<CreateCustomerInput>;
+};
+
+
 export type MutationSetDisplayUiExtensionPointsArgs = {
   display: Scalars['Boolean'];
 };
 
 
+export type MutationSetDraftOrderBillingAddressArgs = {
+  orderId: Scalars['ID'];
+  input: CreateAddressInput;
+};
+
+
+export type MutationSetDraftOrderCustomFieldsArgs = {
+  orderId: Scalars['ID'];
+  input: UpdateOrderInput;
+};
+
+
+export type MutationSetDraftOrderShippingAddressArgs = {
+  orderId: Scalars['ID'];
+  input: CreateAddressInput;
+};
+
+
+export type MutationSetDraftOrderShippingMethodArgs = {
+  orderId: Scalars['ID'];
+  shippingMethodId: Scalars['ID'];
+};
+
+
 export type MutationSetOrderCustomFieldsArgs = {
   input: UpdateOrderInput;
 };
@@ -3246,6 +3362,16 @@ export type NetworkStatus = {
   inFlightRequests: Scalars['Int'];
 };
 
+/**
+ * Returned when invoking a mutation which depends on there being an active Order on the
+ * current session.
+ */
+export type NoActiveOrderError = ErrorResult & {
+  __typename?: 'NoActiveOrderError';
+  errorCode: ErrorCode;
+  message: Scalars['String'];
+};
+
 /** Returned when a call to modifyOrder fails to specify any changes */
 export type NoChangesSpecifiedError = ErrorResult & {
   __typename?: 'NoChangesSpecifiedError';
@@ -3340,7 +3466,7 @@ export type Order = Node & {
   /** A summary of the taxes being applied to this Order */
   taxSummary: Array<OrderTaxSummary>;
   history: HistoryEntryList;
-  customFields?: Maybe<Scalars['JSON']>;
+  customFields?: Maybe<OrderCustomFields>;
 };
 
 
@@ -3363,6 +3489,11 @@ export type OrderAddress = {
   customFields?: Maybe<Scalars['JSON']>;
 };
 
+export type OrderCustomFields = {
+  __typename?: 'OrderCustomFields';
+  tags?: Maybe<Array<Scalars['String']>>;
+};
+
 export type OrderFilterParameter = {
   customerLastName?: Maybe<StringOperators>;
   transactionId?: Maybe<StringOperators>;
@@ -3381,6 +3512,7 @@ export type OrderFilterParameter = {
   shippingWithTax?: Maybe<NumberOperators>;
   total?: Maybe<NumberOperators>;
   totalWithTax?: Maybe<NumberOperators>;
+  tags?: Maybe<StringListOperators>;
 };
 
 export type OrderItem = Node & {
@@ -3487,7 +3619,18 @@ export type OrderLine = Node & {
   taxLines: Array<TaxLine>;
   order: Order;
   fulfillments?: Maybe<Array<Fulfillment>>;
-  customFields?: Maybe<Scalars['JSON']>;
+  customFields?: Maybe<OrderLineCustomFields>;
+};
+
+export type OrderLineCustomFields = {
+  __typename?: 'OrderLineCustomFields';
+  referrer?: Maybe<Customer>;
+  description?: Maybe<Scalars['String']>;
+};
+
+export type OrderLineCustomFieldsInput = {
+  referrerId?: Maybe<Scalars['ID']>;
+  description?: Maybe<Scalars['String']>;
 };
 
 export type OrderLineInput = {
@@ -3528,6 +3671,13 @@ export type OrderModification = Node & {
   isSettled: Scalars['Boolean'];
 };
 
+/** Returned when attempting to modify the contents of an Order that is not in the `AddingItems` state. */
+export type OrderModificationError = ErrorResult & {
+  __typename?: 'OrderModificationError';
+  errorCode: ErrorCode;
+  message: Scalars['String'];
+};
+
 /** Returned when attempting to modify the contents of an Order that is not in the `Modifying` state. */
 export type OrderModificationStateError = ErrorResult & {
   __typename?: 'OrderModificationStateError';
@@ -4269,6 +4419,8 @@ export type Query = {
   customerGroup?: Maybe<CustomerGroup>;
   customerGroups: CustomerGroupList;
   customers: CustomerList;
+  /** Returns a list of eligible shipping methods for the draft Order */
+  eligibleShippingMethodsForDraftOrder: Array<ShippingMethodQuote>;
   facet?: Maybe<Facet>;
   facets: FacetList;
   fulfillmentHandlers: Array<ConfigurableOperationDefinition>;
@@ -4391,6 +4543,11 @@ export type QueryCustomersArgs = {
 };
 
 
+export type QueryEligibleShippingMethodsForDraftOrderArgs = {
+  orderId: Scalars['ID'];
+};
+
+
 export type QueryFacetArgs = {
   id: Scalars['ID'];
 };
@@ -4650,6 +4807,8 @@ export type RemoveFacetsFromChannelInput = {
 
 export type RemoveOptionGroupFromProductResult = Product | ProductOptionInUseError;
 
+export type RemoveOrderItemsResult = Order | OrderModificationError;
+
 export type RemoveProductVariantsFromChannelInput = {
   productVariantIds: Array<Scalars['ID']>;
   channelId: Scalars['ID'];
@@ -4807,6 +4966,10 @@ export type ServerConfig = {
   customFieldConfig: CustomFields;
 };
 
+export type SetCustomerForDraftOrderResult = Order | EmailAddressConflictError;
+
+export type SetOrderShippingMethodResult = Order | OrderModificationError | IneligibleShippingMethodError | NoActiveOrderError;
+
 /** Returned if the Payment settlement fails */
 export type SettlePaymentError = ErrorResult & {
   __typename?: 'SettlePaymentError';
@@ -5349,11 +5512,17 @@ export type UpdateOrderAddressInput = {
   phoneNumber?: Maybe<Scalars['String']>;
 };
 
+export type UpdateOrderCustomFieldsInput = {
+  tags?: Maybe<Array<Scalars['String']>>;
+};
+
 export type UpdateOrderInput = {
   id: Scalars['ID'];
-  customFields?: Maybe<Scalars['JSON']>;
+  customFields?: Maybe<UpdateOrderCustomFieldsInput>;
 };
 
+export type UpdateOrderItemsResult = Order | OrderModificationError | OrderLimitError | NegativeQuantityError | InsufficientStockError;
+
 export type UpdateOrderNoteInput = {
   noteId: Scalars['ID'];
   note?: Maybe<Scalars['String']>;
@@ -6981,6 +7150,182 @@ export type AddManualPaymentMutation = { addManualPaymentToOrder: (
     & ErrorResult_ManualPaymentStateError_Fragment
   ) };
 
+export type CreateDraftOrderMutationVariables = Exact<{ [key: string]: never; }>;
+
+
+export type CreateDraftOrderMutation = { createDraftOrder: (
+    { __typename?: 'Order' }
+    & OrderDetailFragment
+  ) };
+
+export type DeleteDraftOrderMutationVariables = Exact<{
+  orderId: Scalars['ID'];
+}>;
+
+
+export type DeleteDraftOrderMutation = { deleteDraftOrder: (
+    { __typename?: 'DeletionResponse' }
+    & Pick<DeletionResponse, 'result' | 'message'>
+  ) };
+
+export type AddItemToDraftOrderMutationVariables = Exact<{
+  orderId: Scalars['ID'];
+  input: AddItemToDraftOrderInput;
+}>;
+
+
+export type AddItemToDraftOrderMutation = { addItemToDraftOrder: (
+    { __typename?: 'Order' }
+    & OrderDetailFragment
+  ) | (
+    { __typename?: 'OrderModificationError' }
+    & ErrorResult_OrderModificationError_Fragment
+  ) | (
+    { __typename?: 'OrderLimitError' }
+    & ErrorResult_OrderLimitError_Fragment
+  ) | (
+    { __typename?: 'NegativeQuantityError' }
+    & ErrorResult_NegativeQuantityError_Fragment
+  ) | (
+    { __typename?: 'InsufficientStockError' }
+    & ErrorResult_InsufficientStockError_Fragment
+  ) };
+
+export type AdjustDraftOrderLineMutationVariables = Exact<{
+  orderId: Scalars['ID'];
+  input: AdjustDraftOrderLineInput;
+}>;
+
+
+export type AdjustDraftOrderLineMutation = { adjustDraftOrderLine: (
+    { __typename?: 'Order' }
+    & OrderDetailFragment
+  ) | (
+    { __typename?: 'OrderModificationError' }
+    & ErrorResult_OrderModificationError_Fragment
+  ) | (
+    { __typename?: 'OrderLimitError' }
+    & ErrorResult_OrderLimitError_Fragment
+  ) | (
+    { __typename?: 'NegativeQuantityError' }
+    & ErrorResult_NegativeQuantityError_Fragment
+  ) | (
+    { __typename?: 'InsufficientStockError' }
+    & ErrorResult_InsufficientStockError_Fragment
+  ) };
+
+export type RemoveDraftOrderLineMutationVariables = Exact<{
+  orderId: Scalars['ID'];
+  orderLineId: Scalars['ID'];
+}>;
+
+
+export type RemoveDraftOrderLineMutation = { removeDraftOrderLine: (
+    { __typename?: 'Order' }
+    & OrderDetailFragment
+  ) | (
+    { __typename?: 'OrderModificationError' }
+    & ErrorResult_OrderModificationError_Fragment
+  ) };
+
+export type SetCustomerForDraftOrderMutationVariables = Exact<{
+  orderId: Scalars['ID'];
+  customerId?: Maybe<Scalars['ID']>;
+  input?: Maybe<CreateCustomerInput>;
+}>;
+
+
+export type SetCustomerForDraftOrderMutation = { setCustomerForDraftOrder: (
+    { __typename?: 'Order' }
+    & OrderDetailFragment
+  ) | (
+    { __typename?: 'EmailAddressConflictError' }
+    & ErrorResult_EmailAddressConflictError_Fragment
+  ) };
+
+export type SetDraftOrderShippingAddressMutationVariables = Exact<{
+  orderId: Scalars['ID'];
+  input: CreateAddressInput;
+}>;
+
+
+export type SetDraftOrderShippingAddressMutation = { setDraftOrderShippingAddress: (
+    { __typename?: 'Order' }
+    & OrderDetailFragment
+  ) };
+
+export type SetDraftOrderBillingAddressMutationVariables = Exact<{
+  orderId: Scalars['ID'];
+  input: CreateAddressInput;
+}>;
+
+
+export type SetDraftOrderBillingAddressMutation = { setDraftOrderBillingAddress: (
+    { __typename?: 'Order' }
+    & OrderDetailFragment
+  ) };
+
+export type ApplyCouponCodeToDraftOrderMutationVariables = Exact<{
+  orderId: Scalars['ID'];
+  couponCode: Scalars['String'];
+}>;
+
+
+export type ApplyCouponCodeToDraftOrderMutation = { applyCouponCodeToDraftOrder: (
+    { __typename?: 'Order' }
+    & OrderDetailFragment
+  ) | (
+    { __typename?: 'CouponCodeExpiredError' }
+    & ErrorResult_CouponCodeExpiredError_Fragment
+  ) | (
+    { __typename?: 'CouponCodeInvalidError' }
+    & ErrorResult_CouponCodeInvalidError_Fragment
+  ) | (
+    { __typename?: 'CouponCodeLimitError' }
+    & ErrorResult_CouponCodeLimitError_Fragment
+  ) };
+
+export type RemoveCouponCodeFromDraftOrderMutationVariables = Exact<{
+  orderId: Scalars['ID'];
+  couponCode: Scalars['String'];
+}>;
+
+
+export type RemoveCouponCodeFromDraftOrderMutation = { removeCouponCodeFromDraftOrder?: Maybe<(
+    { __typename?: 'Order' }
+    & OrderDetailFragment
+  )> };
+
+export type DraftOrderEligibleShippingMethodsQueryVariables = Exact<{
+  orderId: Scalars['ID'];
+}>;
+
+
+export type DraftOrderEligibleShippingMethodsQuery = { eligibleShippingMethodsForDraftOrder: Array<(
+    { __typename?: 'ShippingMethodQuote' }
+    & Pick<ShippingMethodQuote, 'id' | 'name' | 'code' | 'description' | 'price' | 'priceWithTax' | 'metadata'>
+  )> };
+
+export type SetDraftOrderShippingMethodMutationVariables = Exact<{
+  orderId: Scalars['ID'];
+  shippingMethodId: Scalars['ID'];
+}>;
+
+
+export type SetDraftOrderShippingMethodMutation = { setDraftOrderShippingMethod: (
+    { __typename?: 'Order' }
+    & OrderDetailFragment
+  ) | (
+    { __typename?: 'OrderModificationError' }
+    & ErrorResult_OrderModificationError_Fragment
+  ) | (
+    { __typename?: 'IneligibleShippingMethodError' }
+    & ErrorResult_IneligibleShippingMethodError_Fragment
+  ) | (
+    { __typename?: 'NoActiveOrderError' }
+    & ErrorResult_NoActiveOrderError_Fragment
+  ) };
+
 export type AssetFragment = (
   { __typename?: 'Asset' }
   & Pick<Asset, 'id' | 'createdAt' | 'updatedAt' | 'name' | 'fileSize' | 'mimeType' | 'type' | 'preview' | 'source' | 'width' | 'height'>
@@ -7574,7 +7919,7 @@ export type GetProductVariantQueryVariables = Exact<{
 
 export type GetProductVariantQuery = { productVariant?: Maybe<(
     { __typename?: 'ProductVariant' }
-    & Pick<ProductVariant, 'id' | 'name' | 'sku'>
+    & Pick<ProductVariant, 'id' | 'name' | 'sku' | 'stockOnHand' | 'stockAllocated' | 'stockLevel' | 'useGlobalOutOfStockThreshold' | 'price' | 'priceWithTax'>
     & { featuredAsset?: Maybe<(
       { __typename?: 'Asset' }
       & Pick<Asset, 'id' | 'preview'>
@@ -9239,6 +9584,11 @@ type ErrorResult_FulfillmentStateTransitionError_Fragment = (
   & Pick<FulfillmentStateTransitionError, 'errorCode' | 'message'>
 );
 
+type ErrorResult_IneligibleShippingMethodError_Fragment = (
+  { __typename?: 'IneligibleShippingMethodError' }
+  & Pick<IneligibleShippingMethodError, 'errorCode' | 'message'>
+);
+
 type ErrorResult_InsufficientStockError_Fragment = (
   { __typename?: 'InsufficientStockError' }
   & Pick<InsufficientStockError, 'errorCode' | 'message'>
@@ -9299,6 +9649,11 @@ type ErrorResult_NegativeQuantityError_Fragment = (
   & Pick<NegativeQuantityError, 'errorCode' | 'message'>
 );
 
+type ErrorResult_NoActiveOrderError_Fragment = (
+  { __typename?: 'NoActiveOrderError' }
+  & Pick<NoActiveOrderError, 'errorCode' | 'message'>
+);
+
 type ErrorResult_NoChangesSpecifiedError_Fragment = (
   { __typename?: 'NoChangesSpecifiedError' }
   & Pick<NoChangesSpecifiedError, 'errorCode' | 'message'>
@@ -9314,6 +9669,11 @@ type ErrorResult_OrderLimitError_Fragment = (
   & Pick<OrderLimitError, 'errorCode' | 'message'>
 );
 
+type ErrorResult_OrderModificationError_Fragment = (
+  { __typename?: 'OrderModificationError' }
+  & Pick<OrderModificationError, 'errorCode' | 'message'>
+);
+
 type ErrorResult_OrderModificationStateError_Fragment = (
   { __typename?: 'OrderModificationStateError' }
   & Pick<OrderModificationStateError, 'errorCode' | 'message'>
@@ -9369,7 +9729,7 @@ type ErrorResult_SettlePaymentError_Fragment = (
   & Pick<SettlePaymentError, 'errorCode' | 'message'>
 );
 
-export type ErrorResultFragment = ErrorResult_AlreadyRefundedError_Fragment | ErrorResult_CancelActiveOrderError_Fragment | ErrorResult_CancelPaymentError_Fragment | ErrorResult_ChannelDefaultLanguageError_Fragment | ErrorResult_CouponCodeExpiredError_Fragment | ErrorResult_CouponCodeInvalidError_Fragment | ErrorResult_CouponCodeLimitError_Fragment | ErrorResult_CreateFulfillmentError_Fragment | ErrorResult_EmailAddressConflictError_Fragment | ErrorResult_EmptyOrderLineSelectionError_Fragment | ErrorResult_FacetInUseError_Fragment | ErrorResult_FulfillmentStateTransitionError_Fragment | ErrorResult_InsufficientStockError_Fragment | ErrorResult_InsufficientStockOnHandError_Fragment | ErrorResult_InvalidCredentialsError_Fragment | ErrorResult_InvalidFulfillmentHandlerError_Fragment | ErrorResult_ItemsAlreadyFulfilledError_Fragment | ErrorResult_LanguageNotAvailableError_Fragment | ErrorResult_ManualPaymentStateError_Fragment | ErrorResult_MimeTypeError_Fragment | ErrorResult_MissingConditionsError_Fragment | ErrorResult_MultipleOrderError_Fragment | ErrorResult_NativeAuthStrategyError_Fragment | ErrorResult_NegativeQuantityError_Fragment | ErrorResult_NoChangesSpecifiedError_Fragment | ErrorResult_NothingToRefundError_Fragment | ErrorResult_OrderLimitError_Fragment | ErrorResult_OrderModificationStateError_Fragment | ErrorResult_OrderStateTransitionError_Fragment | ErrorResult_PaymentMethodMissingError_Fragment | ErrorResult_PaymentOrderMismatchError_Fragment | ErrorResult_PaymentStateTransitionError_Fragment | ErrorResult_ProductOptionInUseError_Fragment | ErrorResult_QuantityTooGreatError_Fragment | ErrorResult_RefundOrderStateError_Fragment | ErrorResult_RefundPaymentIdMissingError_Fragment | ErrorResult_RefundStateTransitionError_Fragment | ErrorResult_SettlePaymentError_Fragment;
+export type ErrorResultFragment = ErrorResult_AlreadyRefundedError_Fragment | ErrorResult_CancelActiveOrderError_Fragment | ErrorResult_CancelPaymentError_Fragment | ErrorResult_ChannelDefaultLanguageError_Fragment | ErrorResult_CouponCodeExpiredError_Fragment | ErrorResult_CouponCodeInvalidError_Fragment | ErrorResult_CouponCodeLimitError_Fragment | ErrorResult_CreateFulfillmentError_Fragment | ErrorResult_EmailAddressConflictError_Fragment | ErrorResult_EmptyOrderLineSelectionError_Fragment | ErrorResult_FacetInUseError_Fragment | ErrorResult_FulfillmentStateTransitionError_Fragment | ErrorResult_IneligibleShippingMethodError_Fragment | ErrorResult_InsufficientStockError_Fragment | ErrorResult_InsufficientStockOnHandError_Fragment | ErrorResult_InvalidCredentialsError_Fragment | ErrorResult_InvalidFulfillmentHandlerError_Fragment | ErrorResult_ItemsAlreadyFulfilledError_Fragment | ErrorResult_LanguageNotAvailableError_Fragment | ErrorResult_ManualPaymentStateError_Fragment | ErrorResult_MimeTypeError_Fragment | ErrorResult_MissingConditionsError_Fragment | ErrorResult_MultipleOrderError_Fragment | ErrorResult_NativeAuthStrategyError_Fragment | ErrorResult_NegativeQuantityError_Fragment | ErrorResult_NoActiveOrderError_Fragment | ErrorResult_NoChangesSpecifiedError_Fragment | ErrorResult_NothingToRefundError_Fragment | ErrorResult_OrderLimitError_Fragment | ErrorResult_OrderModificationError_Fragment | ErrorResult_OrderModificationStateError_Fragment | ErrorResult_OrderStateTransitionError_Fragment | ErrorResult_PaymentMethodMissingError_Fragment | ErrorResult_PaymentOrderMismatchError_Fragment | ErrorResult_PaymentStateTransitionError_Fragment | ErrorResult_ProductOptionInUseError_Fragment | ErrorResult_QuantityTooGreatError_Fragment | ErrorResult_RefundOrderStateError_Fragment | ErrorResult_RefundPaymentIdMissingError_Fragment | ErrorResult_RefundStateTransitionError_Fragment | ErrorResult_SettlePaymentError_Fragment;
 
 export type ShippingMethodFragment = (
   { __typename?: 'ShippingMethod' }
@@ -9478,6 +9838,20 @@ export type TestEligibleShippingMethodsQuery = { testEligibleShippingMethods: Ar
     & Pick<ShippingMethodQuote, 'id' | 'name' | 'code' | 'description' | 'price' | 'priceWithTax' | 'metadata'>
   )> };
 
+export type GetCustomerAddressesQueryVariables = Exact<{
+  customerId: Scalars['ID'];
+}>;
+
+
+export type GetCustomerAddressesQuery = { customer?: Maybe<(
+    { __typename?: 'Customer' }
+    & Pick<Customer, 'id'>
+    & { addresses?: Maybe<Array<(
+      { __typename?: 'Address' }
+      & AddressFragment
+    )>> }
+  )> };
+
 type DiscriminateUnion<T, U> = T extends U ? T : never;
 
 export namespace GetProductsWithFacetValuesByIds {
@@ -10216,6 +10590,78 @@ export namespace AddManualPayment {
   export type AddManualPaymentToOrder = (NonNullable<AddManualPaymentMutation['addManualPaymentToOrder']>);
 }
 
+export namespace CreateDraftOrder {
+  export type Variables = CreateDraftOrderMutationVariables;
+  export type Mutation = CreateDraftOrderMutation;
+  export type CreateDraftOrder = (NonNullable<CreateDraftOrderMutation['createDraftOrder']>);
+}
+
+export namespace DeleteDraftOrder {
+  export type Variables = DeleteDraftOrderMutationVariables;
+  export type Mutation = DeleteDraftOrderMutation;
+  export type DeleteDraftOrder = (NonNullable<DeleteDraftOrderMutation['deleteDraftOrder']>);
+}
+
+export namespace AddItemToDraftOrder {
+  export type Variables = AddItemToDraftOrderMutationVariables;
+  export type Mutation = AddItemToDraftOrderMutation;
+  export type AddItemToDraftOrder = (NonNullable<AddItemToDraftOrderMutation['addItemToDraftOrder']>);
+}
+
+export namespace AdjustDraftOrderLine {
+  export type Variables = AdjustDraftOrderLineMutationVariables;
+  export type Mutation = AdjustDraftOrderLineMutation;
+  export type AdjustDraftOrderLine = (NonNullable<AdjustDraftOrderLineMutation['adjustDraftOrderLine']>);
+}
+
+export namespace RemoveDraftOrderLine {
+  export type Variables = RemoveDraftOrderLineMutationVariables;
+  export type Mutation = RemoveDraftOrderLineMutation;
+  export type RemoveDraftOrderLine = (NonNullable<RemoveDraftOrderLineMutation['removeDraftOrderLine']>);
+}
+
+export namespace SetCustomerForDraftOrder {
+  export type Variables = SetCustomerForDraftOrderMutationVariables;
+  export type Mutation = SetCustomerForDraftOrderMutation;
+  export type SetCustomerForDraftOrder = (NonNullable<SetCustomerForDraftOrderMutation['setCustomerForDraftOrder']>);
+}
+
+export namespace SetDraftOrderShippingAddress {
+  export type Variables = SetDraftOrderShippingAddressMutationVariables;
+  export type Mutation = SetDraftOrderShippingAddressMutation;
+  export type SetDraftOrderShippingAddress = (NonNullable<SetDraftOrderShippingAddressMutation['setDraftOrderShippingAddress']>);
+}
+
+export namespace SetDraftOrderBillingAddress {
+  export type Variables = SetDraftOrderBillingAddressMutationVariables;
+  export type Mutation = SetDraftOrderBillingAddressMutation;
+  export type SetDraftOrderBillingAddress = (NonNullable<SetDraftOrderBillingAddressMutation['setDraftOrderBillingAddress']>);
+}
+
+export namespace ApplyCouponCodeToDraftOrder {
+  export type Variables = ApplyCouponCodeToDraftOrderMutationVariables;
+  export type Mutation = ApplyCouponCodeToDraftOrderMutation;
+  export type ApplyCouponCodeToDraftOrder = (NonNullable<ApplyCouponCodeToDraftOrderMutation['applyCouponCodeToDraftOrder']>);
+}
+
+export namespace RemoveCouponCodeFromDraftOrder {
+  export type Variables = RemoveCouponCodeFromDraftOrderMutationVariables;
+  export type Mutation = RemoveCouponCodeFromDraftOrderMutation;
+  export type RemoveCouponCodeFromDraftOrder = (NonNullable<RemoveCouponCodeFromDraftOrderMutation['removeCouponCodeFromDraftOrder']>);
+}
+
+export namespace DraftOrderEligibleShippingMethods {
+  export type Variables = DraftOrderEligibleShippingMethodsQueryVariables;
+  export type Query = DraftOrderEligibleShippingMethodsQuery;
+  export type EligibleShippingMethodsForDraftOrder = NonNullable<(NonNullable<DraftOrderEligibleShippingMethodsQuery['eligibleShippingMethodsForDraftOrder']>)[number]>;
+}
+
+export namespace SetDraftOrderShippingMethod {
+  export type Variables = SetDraftOrderShippingMethodMutationVariables;
+  export type Mutation = SetDraftOrderShippingMethodMutation;
+  export type SetDraftOrderShippingMethod = (NonNullable<SetDraftOrderShippingMethodMutation['setDraftOrderShippingMethod']>);
+}
+
 export namespace Asset {
   export type Fragment = AssetFragment;
   export type FocalPoint = (NonNullable<AssetFragment['focalPoint']>);
@@ -11090,3 +11536,10 @@ export namespace TestEligibleShippingMethods {
   export type Query = TestEligibleShippingMethodsQuery;
   export type TestEligibleShippingMethods = NonNullable<(NonNullable<TestEligibleShippingMethodsQuery['testEligibleShippingMethods']>)[number]>;
 }
+
+export namespace GetCustomerAddresses {
+  export type Variables = GetCustomerAddressesQueryVariables;
+  export type Query = GetCustomerAddressesQuery;
+  export type Customer = (NonNullable<GetCustomerAddressesQuery['customer']>);
+  export type Addresses = NonNullable<(NonNullable<(NonNullable<GetCustomerAddressesQuery['customer']>)['addresses']>)[number]>;
+}

+ 30 - 0
packages/admin-ui/src/lib/core/src/common/introspection-result.ts

@@ -20,6 +20,12 @@
       "Order",
       "ManualPaymentStateError"
     ],
+    "ApplyCouponCodeResult": [
+      "Order",
+      "CouponCodeExpiredError",
+      "CouponCodeInvalidError",
+      "CouponCodeLimitError"
+    ],
     "AuthenticationResult": [
       "CurrentUser",
       "InvalidCredentialsError"
@@ -86,6 +92,7 @@
       "EmptyOrderLineSelectionError",
       "FacetInUseError",
       "FulfillmentStateTransitionError",
+      "IneligibleShippingMethodError",
       "InsufficientStockError",
       "InsufficientStockOnHandError",
       "InvalidCredentialsError",
@@ -98,9 +105,11 @@
       "MultipleOrderError",
       "NativeAuthStrategyError",
       "NegativeQuantityError",
+      "NoActiveOrderError",
       "NoChangesSpecifiedError",
       "NothingToRefundError",
       "OrderLimitError",
+      "OrderModificationError",
       "OrderModificationStateError",
       "OrderStateTransitionError",
       "PaymentMethodMissingError",
@@ -212,10 +221,24 @@
       "Product",
       "ProductOptionInUseError"
     ],
+    "RemoveOrderItemsResult": [
+      "Order",
+      "OrderModificationError"
+    ],
     "SearchResultPrice": [
       "PriceRange",
       "SinglePrice"
     ],
+    "SetCustomerForDraftOrderResult": [
+      "Order",
+      "EmailAddressConflictError"
+    ],
+    "SetOrderShippingMethodResult": [
+      "Order",
+      "OrderModificationError",
+      "IneligibleShippingMethodError",
+      "NoActiveOrderError"
+    ],
     "SettlePaymentResult": [
       "Payment",
       "SettlePaymentError",
@@ -266,6 +289,13 @@
       "GlobalSettings",
       "ChannelDefaultLanguageError"
     ],
+    "UpdateOrderItemsResult": [
+      "Order",
+      "OrderModificationError",
+      "OrderLimitError",
+      "NegativeQuantityError",
+      "InsufficientStockError"
+    ],
     "UpdatePromotionResult": [
       "Promotion",
       "MissingConditionsError"

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

@@ -497,3 +497,128 @@ export const ADD_MANUAL_PAYMENT_TO_ORDER = gql`
     ${ORDER_DETAIL_FRAGMENT}
     ${ERROR_RESULT_FRAGMENT}
 `;
+
+export const CREATE_DRAFT_ORDER = gql`
+    mutation CreateDraftOrder {
+        createDraftOrder {
+            ...OrderDetail
+        }
+    }
+    ${ORDER_DETAIL_FRAGMENT}
+`;
+
+export const DELETE_DRAFT_ORDER = gql`
+    mutation DeleteDraftOrder($orderId: ID!) {
+        deleteDraftOrder(orderId: $orderId) {
+            result
+            message
+        }
+    }
+`;
+
+export const ADD_ITEM_TO_DRAFT_ORDER = gql`
+    mutation AddItemToDraftOrder($orderId: ID!, $input: AddItemToDraftOrderInput!) {
+        addItemToDraftOrder(orderId: $orderId, input: $input) {
+            ...OrderDetail
+            ...ErrorResult
+        }
+    }
+    ${ORDER_DETAIL_FRAGMENT}
+    ${ERROR_RESULT_FRAGMENT}
+`;
+
+export const ADJUST_DRAFT_ORDER_LINE = gql`
+    mutation AdjustDraftOrderLine($orderId: ID!, $input: AdjustDraftOrderLineInput!) {
+        adjustDraftOrderLine(orderId: $orderId, input: $input) {
+            ...OrderDetail
+            ...ErrorResult
+        }
+    }
+    ${ORDER_DETAIL_FRAGMENT}
+    ${ERROR_RESULT_FRAGMENT}
+`;
+
+export const REMOVE_DRAFT_ORDER_LINE = gql`
+    mutation RemoveDraftOrderLine($orderId: ID!, $orderLineId: ID!) {
+        removeDraftOrderLine(orderId: $orderId, orderLineId: $orderLineId) {
+            ...OrderDetail
+            ...ErrorResult
+        }
+    }
+    ${ORDER_DETAIL_FRAGMENT}
+    ${ERROR_RESULT_FRAGMENT}
+`;
+
+export const SET_CUSTOMER_FOR_DRAFT_ORDER = gql`
+    mutation SetCustomerForDraftOrder($orderId: ID!, $customerId: ID, $input: CreateCustomerInput) {
+        setCustomerForDraftOrder(orderId: $orderId, customerId: $customerId, input: $input) {
+            ...OrderDetail
+            ...ErrorResult
+        }
+    }
+    ${ORDER_DETAIL_FRAGMENT}
+    ${ERROR_RESULT_FRAGMENT}
+`;
+
+export const SET_SHIPPING_ADDRESS_FOR_DRAFT_ORDER = gql`
+    mutation SetDraftOrderShippingAddress($orderId: ID!, $input: CreateAddressInput!) {
+        setDraftOrderShippingAddress(orderId: $orderId, input: $input) {
+            ...OrderDetail
+        }
+    }
+    ${ORDER_DETAIL_FRAGMENT}
+`;
+
+export const SET_BILLING_ADDRESS_FOR_DRAFT_ORDER = gql`
+    mutation SetDraftOrderBillingAddress($orderId: ID!, $input: CreateAddressInput!) {
+        setDraftOrderBillingAddress(orderId: $orderId, input: $input) {
+            ...OrderDetail
+        }
+    }
+    ${ORDER_DETAIL_FRAGMENT}
+`;
+
+export const APPLY_COUPON_CODE_TO_DRAFT_ORDER = gql`
+    mutation ApplyCouponCodeToDraftOrder($orderId: ID!, $couponCode: String!) {
+        applyCouponCodeToDraftOrder(orderId: $orderId, couponCode: $couponCode) {
+            ...OrderDetail
+            ...ErrorResult
+        }
+    }
+    ${ORDER_DETAIL_FRAGMENT}
+    ${ERROR_RESULT_FRAGMENT}
+`;
+
+export const REMOVE_COUPON_CODE_FROM_DRAFT_ORDER = gql`
+    mutation RemoveCouponCodeFromDraftOrder($orderId: ID!, $couponCode: String!) {
+        removeCouponCodeFromDraftOrder(orderId: $orderId, couponCode: $couponCode) {
+            ...OrderDetail
+        }
+    }
+    ${ORDER_DETAIL_FRAGMENT}
+`;
+
+export const DRAFT_ORDER_ELIGIBLE_SHIPPING_METHODS = gql`
+    query DraftOrderEligibleShippingMethods($orderId: ID!) {
+        eligibleShippingMethodsForDraftOrder(orderId: $orderId) {
+            id
+            name
+            code
+            description
+            price
+            priceWithTax
+            metadata
+        }
+    }
+`;
+
+export const SET_DRAFT_ORDER_SHIPPING_METHOD = gql`
+    mutation SetDraftOrderShippingMethod($orderId: ID!, $shippingMethodId: ID!) {
+        setDraftOrderShippingMethod(orderId: $orderId, shippingMethodId: $shippingMethodId) {
+            ...OrderDetail
+            ...ErrorResult
+        }
+    }
+    ${ORDER_DETAIL_FRAGMENT}
+    ${ERROR_RESULT_FRAGMENT}
+`;

+ 6 - 0
packages/admin-ui/src/lib/core/src/data/definitions/product-definitions.ts

@@ -683,6 +683,10 @@ export const GET_PRODUCT_VARIANT = gql`
             id
             name
             sku
+            stockOnHand
+            stockAllocated
+            stockLevel
+            useGlobalOutOfStockThreshold
             featuredAsset {
                 id
                 preview
@@ -691,6 +695,8 @@ export const GET_PRODUCT_VARIANT = gql`
                     y
                 }
             }
+            price
+            priceWithTax
             product {
                 id
                 featuredAsset {

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

@@ -19,6 +19,7 @@ import {
     GetCustomerHistory,
     GetCustomerList,
     HistoryEntryListOptions,
+    LogicalOperator,
     OrderListOptions,
     RemoveCustomersFromGroup,
     UpdateAddressInput,
@@ -41,8 +42,8 @@ import {
     DELETE_CUSTOMER_GROUP,
     DELETE_CUSTOMER_NOTE,
     GET_CUSTOMER,
-    GET_CUSTOMER_GROUPS,
     GET_CUSTOMER_GROUP_WITH_CUSTOMERS,
+    GET_CUSTOMER_GROUPS,
     GET_CUSTOMER_HISTORY,
     GET_CUSTOMER_LIST,
     REMOVE_CUSTOMERS_FROM_GROUP,
@@ -64,6 +65,9 @@ export class CustomerDataService {
                       emailAddress: {
                           contains: filterTerm,
                       },
+                      lastName: {
+                          contains: filterTerm,
+                      },
                   },
               }
             : {};
@@ -74,6 +78,7 @@ export class CustomerDataService {
                     take,
                     skip,
                     ...filter,
+                    filterOperator: LogicalOperator.OR,
                 },
             },
         );

+ 124 - 0
packages/admin-ui/src/lib/core/src/data/providers/order-data.service.ts

@@ -1,12 +1,28 @@
 import {
+    AddItemToDraftOrderInput,
+    AddItemToDraftOrderMutation,
+    AddItemToDraftOrderMutationVariables,
     AddManualPayment,
     AddNoteToOrder,
     AddNoteToOrderInput,
+    AdjustDraftOrderLineInput,
+    AdjustDraftOrderLineMutation,
+    AdjustDraftOrderLineMutationVariables,
+    ApplyCouponCodeToDraftOrderMutation,
+    ApplyCouponCodeToDraftOrderMutationVariables,
     CancelOrder,
     CancelOrderInput,
     CancelPayment,
+    CreateAddressInput,
+    CreateCustomerInput,
+    CreateDraftOrderMutation,
+    CreateDraftOrderMutationVariables,
     CreateFulfillment,
+    DeleteDraftOrderMutation,
+    DeleteDraftOrderMutationVariables,
     DeleteOrderNote,
+    DraftOrderEligibleShippingMethodsQuery,
+    DraftOrderEligibleShippingMethodsQueryVariables,
     FulfillOrderInput,
     GetOrder,
     GetOrderHistory,
@@ -19,6 +35,18 @@ import {
     OrderListOptions,
     RefundOrder,
     RefundOrderInput,
+    RemoveCouponCodeFromDraftOrderMutation,
+    RemoveCouponCodeFromDraftOrderMutationVariables,
+    RemoveDraftOrderLineMutation,
+    RemoveDraftOrderLineMutationVariables,
+    SetCustomerForDraftOrderMutation,
+    SetCustomerForDraftOrderMutationVariables,
+    SetDraftOrderBillingAddressMutation,
+    SetDraftOrderBillingAddressMutationVariables,
+    SetDraftOrderShippingAddressMutation,
+    SetDraftOrderShippingAddressMutationVariables,
+    SetDraftOrderShippingMethodMutation,
+    SetDraftOrderShippingMethodMutationVariables,
     SettlePayment,
     SettleRefund,
     SettleRefundInput,
@@ -31,10 +59,13 @@ import {
     UpdateOrderNoteInput,
 } from '../../common/generated-types';
 import {
+    ADD_ITEM_TO_DRAFT_ORDER,
     ADD_MANUAL_PAYMENT_TO_ORDER,
     ADD_NOTE_TO_ORDER,
+    ADJUST_DRAFT_ORDER_LINE,
     CANCEL_ORDER,
     CANCEL_PAYMENT,
+    CREATE_DRAFT_ORDER,
     CREATE_FULFILLMENT,
     DELETE_ORDER_NOTE,
     GET_ORDER,
@@ -43,13 +74,22 @@ import {
     GET_ORDER_SUMMARY,
     MODIFY_ORDER,
     REFUND_ORDER,
+    REMOVE_DRAFT_ORDER_LINE,
     SETTLE_PAYMENT,
     SETTLE_REFUND,
+    SET_CUSTOMER_FOR_DRAFT_ORDER,
     TRANSITION_FULFILLMENT_TO_STATE,
     TRANSITION_ORDER_TO_STATE,
     TRANSITION_PAYMENT_TO_STATE,
     UPDATE_ORDER_CUSTOM_FIELDS,
     UPDATE_ORDER_NOTE,
+    SET_SHIPPING_ADDRESS_FOR_DRAFT_ORDER,
+    SET_BILLING_ADDRESS_FOR_DRAFT_ORDER,
+    APPLY_COUPON_CODE_TO_DRAFT_ORDER,
+    REMOVE_COUPON_CODE_FROM_DRAFT_ORDER,
+    DRAFT_ORDER_ELIGIBLE_SHIPPING_METHODS,
+    SET_DRAFT_ORDER_SHIPPING_METHOD,
+    DELETE_DRAFT_ORDER,
 } from '../definitions/order-definitions';
 
 import { BaseDataService } from './base-data.service';
@@ -204,4 +244,88 @@ export class OrderDataService {
             { input },
         );
     }
+
+    createDraftOrder() {
+        return this.baseDataService.mutate<CreateDraftOrderMutation>(CREATE_DRAFT_ORDER);
+    }
+
+    deleteDraftOrder(orderId: string) {
+        return this.baseDataService.mutate<DeleteDraftOrderMutation, DeleteDraftOrderMutationVariables>(
+            DELETE_DRAFT_ORDER,
+            { orderId },
+        );
+    }
+
+    addItemToDraftOrder(orderId: string, input: AddItemToDraftOrderInput) {
+        return this.baseDataService.mutate<AddItemToDraftOrderMutation, AddItemToDraftOrderMutationVariables>(
+            ADD_ITEM_TO_DRAFT_ORDER,
+            { orderId, input },
+        );
+    }
+
+    adjustDraftOrderLine(orderId: string, input: AdjustDraftOrderLineInput) {
+        return this.baseDataService.mutate<
+            AdjustDraftOrderLineMutation,
+            AdjustDraftOrderLineMutationVariables
+        >(ADJUST_DRAFT_ORDER_LINE, { orderId, input });
+    }
+
+    removeDraftOrderLine(orderId: string, orderLineId: string) {
+        return this.baseDataService.mutate<
+            RemoveDraftOrderLineMutation,
+            RemoveDraftOrderLineMutationVariables
+        >(REMOVE_DRAFT_ORDER_LINE, { orderId, orderLineId });
+    }
+
+    setCustomerForDraftOrder(
+        orderId: string,
+        { customerId, input }: { customerId?: string; input?: CreateCustomerInput },
+    ) {
+        return this.baseDataService.mutate<
+            SetCustomerForDraftOrderMutation,
+            SetCustomerForDraftOrderMutationVariables
+        >(SET_CUSTOMER_FOR_DRAFT_ORDER, { orderId, customerId, input });
+    }
+
+    setDraftOrderShippingAddress(orderId: string, input: CreateAddressInput) {
+        return this.baseDataService.mutate<
+            SetDraftOrderShippingAddressMutation,
+            SetDraftOrderShippingAddressMutationVariables
+        >(SET_SHIPPING_ADDRESS_FOR_DRAFT_ORDER, { orderId, input });
+    }
+
+    setDraftOrderBillingAddress(orderId: string, input: CreateAddressInput) {
+        return this.baseDataService.mutate<
+            SetDraftOrderBillingAddressMutation,
+            SetDraftOrderBillingAddressMutationVariables
+        >(SET_BILLING_ADDRESS_FOR_DRAFT_ORDER, { orderId, input });
+    }
+
+    applyCouponCodeToDraftOrder(orderId: string, couponCode: string) {
+        return this.baseDataService.mutate<
+            ApplyCouponCodeToDraftOrderMutation,
+            ApplyCouponCodeToDraftOrderMutationVariables
+        >(APPLY_COUPON_CODE_TO_DRAFT_ORDER, { orderId, couponCode });
+    }
+
+    removeCouponCodeFromDraftOrder(orderId: string, couponCode: string) {
+        return this.baseDataService.mutate<
+            RemoveCouponCodeFromDraftOrderMutation,
+            RemoveCouponCodeFromDraftOrderMutationVariables
+        >(REMOVE_COUPON_CODE_FROM_DRAFT_ORDER, { orderId, couponCode });
+    }
+
+    getDraftOrderEligibleShippingMethods(orderId: string) {
+        return this.baseDataService.query<
+            DraftOrderEligibleShippingMethodsQuery,
+            DraftOrderEligibleShippingMethodsQueryVariables
+        >(DRAFT_ORDER_ELIGIBLE_SHIPPING_METHODS, { orderId });
+    }
+
+    setDraftOrderShippingMethod(orderId: string, shippingMethodId: string) {
+        return this.baseDataService.mutate<
+            SetDraftOrderShippingMethodMutation,
+            SetDraftOrderShippingMethodMutationVariables
+        >(SET_DRAFT_ORDER_SHIPPING_METHOD, { orderId, shippingMethodId });
+    }
 }

+ 6 - 0
packages/admin-ui/src/lib/core/src/data/utils/remove-readonly-custom-fields.ts

@@ -35,6 +35,12 @@ export function isEntityCreateOrUpdateMutation(documentNode: DocumentNode): stri
             if (inputTypeName === 'ModifyOrderInput') {
                 return 'Order';
             }
+            if (
+                inputTypeName === 'AddItemToDraftOrderInput' ||
+                inputTypeName === 'AdjustDraftOrderLineInput'
+            ) {
+                return 'OrderLine';
+            }
 
             const createMatch = inputTypeName.match(CREATE_ENTITY_REGEX);
             if (createMatch) {

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

@@ -27,6 +27,7 @@ export class OrderStateLabelComponent {
             case 'Delivered':
                 return 'success';
             case 'Cancelled':
+            case 'Draft':
                 return 'error';
             case 'PaymentAuthorized':
             case 'PaymentSettled':

+ 3 - 1
packages/admin-ui/src/lib/core/src/shared/components/product-selector/product-selector.component.scss

@@ -1,4 +1,3 @@
-
 :host {
     display: block;
 }
@@ -7,3 +6,6 @@
     margin-left: 12px;
     color: var(--color-grey-500);
 }
+img {
+    border-radius: var(--border-radius-img);
+}

+ 4 - 0
packages/admin-ui/src/lib/core/src/shared/components/radio-card/radio-card-fieldset.component.scss

@@ -0,0 +1,4 @@
+fieldset {
+    display: flex;
+    align-items: flex-start;
+}

+ 69 - 0
packages/admin-ui/src/lib/core/src/shared/components/radio-card/radio-card-fieldset.component.ts

@@ -0,0 +1,69 @@
+import {
+    ChangeDetectionStrategy,
+    ChangeDetectorRef,
+    Component,
+    ContentChild,
+    EventEmitter,
+    Input,
+    OnChanges,
+    OnDestroy,
+    OnInit,
+    Output,
+    SimpleChanges,
+    TemplateRef,
+} from '@angular/core';
+import { Subject, Subscription } from 'rxjs';
+import { debounceTime, throttleTime } from 'rxjs/operators';
+
+@Component({
+    selector: 'vdr-radio-card-fieldset',
+    template: `<fieldset><ng-content></ng-content></fieldset> `,
+    styleUrls: ['radio-card-fieldset.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class RadioCardFieldsetComponent<T = any> implements OnInit, OnChanges, OnDestroy {
+    @Input() selectedItemId: string;
+    @Input() idFn: (item: T) => string;
+    @Output() selectItem = new EventEmitter<T>();
+    groupName = 'radio-group-' + Math.random().toString(36);
+    selectedIdChange$ = new Subject<string>();
+    focussedId: string | undefined = undefined;
+    private idChange$ = new Subject<T>();
+    private subscription: Subscription;
+
+    constructor(private changeDetector: ChangeDetectorRef) {}
+
+    ngOnInit() {
+        this.subscription = this.idChange$
+            .pipe(throttleTime(200))
+            .subscribe(item => this.selectItem.emit(item));
+    }
+
+    ngOnChanges(changes: SimpleChanges) {
+        if ('selectedItemId' in changes) {
+            this.selectedIdChange$.next(this.selectedItemId);
+        }
+    }
+
+    ngOnDestroy() {
+        if (this.subscription) {
+            this.subscription.unsubscribe();
+        }
+    }
+
+    isSelected(item: T): boolean {
+        return this.selectedItemId === this.idFn(item);
+    }
+
+    isFocussed(item: T): boolean {
+        return this.focussedId === this.idFn(item);
+    }
+
+    selectChanged(item: T) {
+        this.idChange$.next(item);
+    }
+
+    setFocussedId(item: T | undefined) {
+        this.focussedId = item && this.idFn(item);
+    }
+}

+ 21 - 0
packages/admin-ui/src/lib/core/src/shared/components/radio-card/radio-card.component.html

@@ -0,0 +1,21 @@
+<label
+    [ngClass]="{
+        'selected': isSelected(item),
+        'focussed': isFocussed(item)
+    }"
+    class="radio-card"
+>
+    <input
+        type="radio"
+        [name]="name"
+        [value]="getItemId(item)"
+        class="hidden"
+        (focus)="setFocussedId(item)"
+        (blur)="setFocussedId(undefined)"
+        (change)="selectChanged(item)"
+    />
+    <vdr-select-toggle [selected]="isSelected(item)" size="small"></vdr-select-toggle>
+    <div class="content">
+        <ng-content></ng-content>
+    </div>
+</label>

+ 36 - 0
packages/admin-ui/src/lib/core/src/shared/components/radio-card/radio-card.component.scss

@@ -0,0 +1,36 @@
+:host {
+    display: inline-block;
+}
+.radio-card {
+    background: none;
+    position: relative;
+    display: block;
+    border: 1px solid var(--clr-btn-default-border-color, #0072a3);
+    border-radius: var(--clr-btn-border-radius, 0.15rem);
+    padding: 6px;
+    text-align: left;
+    margin: 6px;
+    &:hover {
+        cursor: pointer;
+        outline: 1px solid var(--color-primary-500);
+    }
+    &.selected {
+        outline: 1px solid var(--color-primary-500);
+        background-color: var(--color-primary-100);
+    }
+}
+
+input.hidden {
+    visibility: hidden;
+    position: absolute;
+}
+
+vdr-select-toggle {
+    position: absolute;
+    top: 3px;
+    left: 3px;
+}
+
+.content {
+    margin-left: 24px;
+}

+ 63 - 0
packages/admin-ui/src/lib/core/src/shared/components/radio-card/radio-card.component.ts

@@ -0,0 +1,63 @@
+import {
+    ChangeDetectionStrategy,
+    ChangeDetectorRef,
+    Component,
+    ContentChild,
+    Input,
+    OnDestroy,
+    OnInit,
+    TemplateRef,
+} from '@angular/core';
+import { Subject, Subscription } from 'rxjs';
+
+import { RadioCardFieldsetComponent } from './radio-card-fieldset.component';
+
+@Component({
+    selector: 'vdr-radio-card',
+    templateUrl: './radio-card.component.html',
+    styleUrls: ['./radio-card.component.scss'],
+    exportAs: 'VdrRadioCard',
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class RadioCardComponent<T = any> implements OnInit, OnDestroy {
+    @Input() item: T;
+    @ContentChild(TemplateRef) itemTemplate: TemplateRef<T>;
+
+    constructor(private fieldset: RadioCardFieldsetComponent, private changeDetector: ChangeDetectorRef) {}
+
+    private idChange$ = new Subject<T>();
+    private subscription: Subscription;
+    name = this.fieldset.groupName;
+
+    ngOnInit() {
+        this.subscription = this.fieldset.selectedIdChange$.subscribe(id => {
+            this.changeDetector.markForCheck();
+        });
+    }
+
+    ngOnDestroy() {
+        if (this.subscription) {
+            this.subscription.unsubscribe();
+        }
+    }
+
+    isSelected(item: T): boolean {
+        return this.fieldset.isSelected(item);
+    }
+
+    isFocussed(item: T): boolean {
+        return this.fieldset.isFocussed(item);
+    }
+
+    selectChanged(item: T) {
+        this.fieldset.selectChanged(item);
+    }
+
+    setFocussedId(item: T | undefined) {
+        this.fieldset.setFocussedId(item);
+    }
+
+    getItemId(item: T): string {
+        return this.fieldset.idFn(item);
+    }
+}

+ 1 - 0
packages/admin-ui/src/lib/core/src/shared/pipes/state-i18n-token.pipe.ts

@@ -7,6 +7,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 export class StateI18nTokenPipe implements PipeTransform {
     private readonly stateI18nTokens = {
         Created: _('state.created'),
+        Draft: _('state.draft'),
         AddingItems: _('state.adding-items'),
         ArrangingPayment: _('state.arranging-payment'),
         PaymentAuthorized: _('state.payment-authorized'),

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

@@ -73,6 +73,8 @@ import { PaginationControlsComponent } from './components/pagination-controls/pa
 import { ProductMultiSelectorDialogComponent } from './components/product-multi-selector-dialog/product-multi-selector-dialog.component';
 import { ProductSearchInputComponent } from './components/product-search-input/product-search-input.component';
 import { ProductSelectorComponent } from './components/product-selector/product-selector.component';
+import { RadioCardFieldsetComponent } from './components/radio-card/radio-card-fieldset.component';
+import { RadioCardComponent } from './components/radio-card/radio-card.component';
 import { ExternalImageDialogComponent } from './components/rich-text-editor/external-image-dialog/external-image-dialog.component';
 import { LinkDialogComponent } from './components/rich-text-editor/link-dialog/link-dialog.component';
 import { ContextMenuComponent } from './components/rich-text-editor/prosemirror/context-menu/context-menu.component';
@@ -247,6 +249,8 @@ const DECLARATIONS = [
     ContextMenuComponent,
     RawHtmlDialogComponent,
     BulkActionMenuComponent,
+    RadioCardComponent,
+    RadioCardFieldsetComponent,
 ];
 
 const DYNAMIC_FORM_INPUTS = [

+ 20 - 0
packages/admin-ui/src/lib/order/src/components/coupon-code-selector/coupon-code-selector.component.html

@@ -0,0 +1,20 @@
+<ng-select
+    [items]="availableCouponCodes$ | async"
+    appendTo="body"
+    bindLabel="code"
+    bindValue="code"
+    [addTag]="false"
+    [multiple]="true"
+    [hideSelected]="true"
+    [minTermLength]="2"
+    typeToSearchText=""
+    [typeahead]="couponCodeInput$"
+    [formControl]="control"
+    (add)="addCouponCode.emit($event.code)"
+    (remove)="removeCouponCode.emit($event.value?.code)"
+>
+    <ng-template ng-option-tmp let-item="item">
+        <vdr-chip>{{ item.code }}</vdr-chip>
+        {{ item.promotionName }}
+    </ng-template>
+</ng-select>

+ 0 - 0
packages/admin-ui/src/lib/order/src/components/coupon-code-selector/coupon-code-selector.component.scss


+ 43 - 0
packages/admin-ui/src/lib/order/src/components/coupon-code-selector/coupon-code-selector.component.ts

@@ -0,0 +1,43 @@
+import { Component, OnInit, ChangeDetectionStrategy, Input, Output, EventEmitter } from '@angular/core';
+import { FormControl } from '@angular/forms';
+import { DataService } from '@vendure/admin-ui/core';
+import { concat, Observable, Subject } from 'rxjs';
+import { distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators';
+
+@Component({
+    selector: 'vdr-coupon-code-selector',
+    templateUrl: './coupon-code-selector.component.html',
+    styleUrls: ['./coupon-code-selector.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class CouponCodeSelectorComponent implements OnInit {
+    @Input() couponCodes: string[];
+    @Input() control: FormControl | undefined;
+    @Output() addCouponCode = new EventEmitter<string>();
+    @Output() removeCouponCode = new EventEmitter<string>();
+    availableCouponCodes$: Observable<Array<{ code: string; promotionName: string }>>;
+    couponCodeInput$ = new Subject<string>();
+    constructor(private dataService: DataService) {}
+
+    ngOnInit(): void {
+        this.availableCouponCodes$ = concat(
+            this.couponCodeInput$.pipe(
+                distinctUntilChanged(),
+                switchMap(
+                    term =>
+                        this.dataService.promotion.getPromotions(10, 0, {
+                            couponCode: { contains: term },
+                        }).single$,
+                ),
+                map(({ promotions }) =>
+                    // tslint:disable-next-line:no-non-null-assertion
+                    promotions.items.map(p => ({ code: p.couponCode!, promotionName: p.name })),
+                ),
+                startWith([]),
+            ),
+        );
+        if (!this.control) {
+            this.control = new FormControl(this.couponCodes ?? []);
+        }
+    }
+}

+ 175 - 0
packages/admin-ui/src/lib/order/src/components/draft-order-detail/draft-order-detail.component.html

@@ -0,0 +1,175 @@
+<vdr-action-bar *ngIf="entity$ | async as order">
+    <vdr-ab-left>
+        <div class="flex clr-align-items-center">
+            <vdr-entity-info [entity]="entity$ | async"></vdr-entity-info>
+            <vdr-order-state-label [state]="order.state"></vdr-order-state-label>
+        </div>
+    </vdr-ab-left>
+
+    <vdr-ab-right>
+        <button
+            class="btn btn-primary"
+            (click)="completeOrder()"
+            [disabled]="!order.customer || !order.lines.length || !order.shippingLines.length"
+        >
+            <clr-icon shape="check"></clr-icon>
+            {{ 'order.complete-draft-order' | translate }}
+        </button>
+        <vdr-dropdown>
+            <button class="icon-button" vdrDropdownTrigger>
+                <clr-icon shape="ellipsis-vertical"></clr-icon>
+            </button>
+            <vdr-dropdown-menu vdrPosition="bottom-right">
+                <button type="button" class="btn" vdrDropdownItem (click)="deleteOrder()">
+                    <clr-icon shape="trash" class="is-danger"></clr-icon>
+                    {{ 'order.delete-draft-order' | translate }}
+                </button>
+            </vdr-dropdown-menu>
+        </vdr-dropdown>
+    </vdr-ab-right>
+</vdr-action-bar>
+
+<div *ngIf="entity$ | async as order">
+    <div class="clr-row">
+        <div class="clr-col-lg-8">
+            <vdr-draft-order-variant-selector
+                [orderLineCustomFields]="orderLineCustomFields"
+                [currencyCode]="order.currencyCode"
+                (addItem)="addItemToOrder($event)"
+            ></vdr-draft-order-variant-selector>
+            <vdr-order-table
+                [order]="order"
+                [orderLineCustomFields]="orderLineCustomFields"
+                [isDraft]="true"
+                (adjust)="adjustOrderLine($event)"
+                (remove)="removeOrderLine($event)"
+            ></vdr-order-table>
+            <div class="flex">
+                <button
+                    *ngIf="order.couponCodes.length === 0 && !displayCouponCodeInput"
+                    class="btn btn-link btn-sm mr2"
+                    (click)="displayCouponCodeInput = !displayCouponCodeInput"
+                >
+                    {{ 'order.set-coupon-codes' | translate }}
+                </button>
+                <div *ngIf="order.couponCodes.length || displayCouponCodeInput">
+                    <label>{{ 'order.set-coupon-codes' | translate }}</label>
+                    <vdr-coupon-code-selector
+                        [couponCodes]="order.couponCodes"
+                        (addCouponCode)="applyCouponCode($event)"
+                        (removeCouponCode)="removeCouponCode($event)"
+                    ></vdr-coupon-code-selector>
+                </div>
+            </div>
+            <ng-container *ngIf="order.taxSummary.length">
+                <h4>{{ 'order.tax-summary' | translate }}</h4>
+                <table class="table">
+                    <thead>
+                        <tr>
+                            <th>{{ 'common.description' | translate }}</th>
+                            <th>{{ 'order.tax-rate' | translate }}</th>
+                            <th>{{ 'order.tax-base' | translate }}</th>
+                            <th>{{ 'order.tax-total' | translate }}</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr *ngFor="let row of order.taxSummary">
+                            <td>{{ row.description }}</td>
+                            <td>{{ row.taxRate / 100 | percent }}</td>
+                            <td>{{ row.taxBase | localeCurrency: order.currencyCode }}</td>
+                            <td>{{ row.taxTotal | localeCurrency: order.currencyCode }}</td>
+                        </tr>
+                    </tbody>
+                </table>
+            </ng-container>
+        </div>
+        <div class="clr-col-lg-4 order-cards">
+            <div class="card">
+                <div class="card-header">
+                    <clr-icon *ngIf="!order.customer" shape="unknown-status" class="is-warning"></clr-icon>
+                    <clr-icon *ngIf="order.customer" shape="check" class="is-success"></clr-icon>
+                    {{ 'order.customer' | translate }}
+                </div>
+                <div class="card-block">
+                    <div class="card-text">
+                        <vdr-customer-label
+                            class="block mb2"
+                            *ngIf="order.customer"
+                            [customer]="order.customer"
+                        ></vdr-customer-label>
+                        <button class="btn btn-link btn-sm" (click)="setCustomer()">
+                            {{ 'order.set-customer-for-order' | translate }}
+                        </button>
+                    </div>
+                </div>
+                <div class="card-block">
+                    <h4 class="card-title">
+                        <clr-icon
+                            *ngIf="!order.billingAddress.streetLine1"
+                            shape="unknown-status"
+                            class="is-warning"
+                        ></clr-icon>
+                        <clr-icon
+                            *ngIf="order.billingAddress.streetLine1"
+                            shape="check"
+                            class="is-success"
+                        ></clr-icon>
+                        {{ 'order.billing-address' | translate }}
+                    </h4>
+                    <div class="card-text">
+                        <vdr-formatted-address
+                            class="block mb2"
+                            *ngIf="order.billingAddress"
+                            [address]="order.billingAddress"
+                        ></vdr-formatted-address>
+                        <button class="btn btn-link btn-sm" (click)="setBillingAddress()">
+                            {{ 'order.set-billing-address' | translate }}
+                        </button>
+                    </div>
+                </div>
+            </div>
+            <div class="card">
+                <div class="card-header">
+                    <clr-icon
+                        *ngIf="!order.shippingAddress.streetLine1 || !order.shippingLines.length"
+                        shape="unknown-status"
+                        class="is-warning"
+                    ></clr-icon>
+                    <clr-icon
+                        *ngIf="order.shippingAddress.streetLine1 && order.shippingLines.length"
+                        shape="check"
+                        class="is-success"
+                    ></clr-icon>
+                    {{ 'order.shipping' | translate }}
+                </div>
+                <div class="card-block">
+                    <div class="card-text">
+                        <vdr-formatted-address
+                            class="block mb2"
+                            *ngIf="order.shippingAddress"
+                            [address]="order.shippingAddress"
+                        ></vdr-formatted-address>
+                        <button class="btn btn-link btn-sm" (click)="setShippingAddress()">
+                            {{ 'order.set-shipping-address' | translate }}
+                        </button>
+                    </div>
+                </div>
+                <div class="card-block">
+                    <div class="card-text">
+                        <div *ngFor="let shippingLine of order.shippingLines">
+                            {{ shippingLine.shippingMethod.name }}
+                        </div>
+                        <button class="btn btn-link btn-sm" (click)="setShippingMethod()">
+                            {{ 'order.set-shipping-method' | translate }}
+                        </button>
+                    </div>
+                </div>
+            </div>
+            <vdr-order-custom-fields-card
+                [customFieldsConfig]="customFields"
+                [customFieldValues]="order.customFields"
+                (updateClick)="updateCustomFields($event)"
+            ></vdr-order-custom-fields-card>
+        </div>
+    </div>
+</div>

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


+ 234 - 0
packages/admin-ui/src/lib/order/src/components/draft-order-detail/draft-order-detail.component.ts

@@ -0,0 +1,234 @@
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
+import { FormGroup } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
+import {
+    BaseDetailComponent,
+    CustomFieldConfig,
+    DataService,
+    DeletionResult,
+    DraftOrderEligibleShippingMethodsQuery,
+    ModalService,
+    NotificationService,
+    Order,
+    OrderDetail,
+    ServerConfigService,
+} from '@vendure/admin-ui/core';
+import { OrderTransitionService } from '@vendure/admin-ui/order';
+import { combineLatest, Observable, Subject } from 'rxjs';
+import { switchMap, take } from 'rxjs/operators';
+import { SelectAddressDialogComponent } from '../select-address-dialog/select-address-dialog.component';
+import { SelectCustomerDialogComponent } from '../select-customer-dialog/select-customer-dialog.component';
+import { SelectShippingMethodDialogComponent } from '../select-shipping-method-dialog/select-shipping-method-dialog.component';
+
+@Component({
+    selector: 'vdr-draft-order-detail',
+    templateUrl: './draft-order-detail.component.html',
+    styleUrls: ['./draft-order-detail.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class DraftOrderDetailComponent
+    extends BaseDetailComponent<OrderDetail.Fragment>
+    implements OnInit, OnDestroy
+{
+    detailForm = new FormGroup({});
+    eligibleShippingMethods$: Observable<
+        DraftOrderEligibleShippingMethodsQuery['eligibleShippingMethodsForDraftOrder']
+    >;
+    nextStates$: Observable<string[]>;
+    fetchHistory = new Subject<void>();
+    customFields: CustomFieldConfig[];
+    orderLineCustomFields: CustomFieldConfig[];
+    displayCouponCodeInput = false;
+
+    constructor(
+        router: Router,
+        route: ActivatedRoute,
+        serverConfigService: ServerConfigService,
+        private changeDetector: ChangeDetectorRef,
+        protected dataService: DataService,
+        private notificationService: NotificationService,
+        private modalService: ModalService,
+        private orderTransitionService: OrderTransitionService,
+    ) {
+        super(route, router, serverConfigService, dataService);
+    }
+
+    ngOnInit() {
+        this.init();
+        this.orderLineCustomFields = this.getCustomFieldConfig('OrderLine');
+        this.eligibleShippingMethods$ = this.entity$.pipe(
+            switchMap(order =>
+                this.dataService.order
+                    .getDraftOrderEligibleShippingMethods(order.id)
+                    .mapSingle(
+                        ({ eligibleShippingMethodsForDraftOrder }) => eligibleShippingMethodsForDraftOrder,
+                    ),
+            ),
+        );
+        this.customFields = this.getCustomFieldConfig('Order');
+        this.orderLineCustomFields = this.getCustomFieldConfig('OrderLine');
+    }
+
+    ngOnDestroy() {
+        this.destroy();
+    }
+
+    addItemToOrder(event: { productVariantId: string; quantity: number; customFields: any }) {
+        this.dataService.order.addItemToDraftOrder(this.id, event).subscribe(result => {
+            if (result.addItemToDraftOrder.__typename !== 'Order') {
+                this.notificationService.error((result.addItemToDraftOrder as any).message);
+            }
+        });
+    }
+
+    adjustOrderLine(event: { lineId: string; quantity: number }) {
+        this.dataService.order
+            .adjustDraftOrderLine(this.id, { orderLineId: event.lineId, quantity: event.quantity })
+            .subscribe(result => {
+                if (result.adjustDraftOrderLine.__typename !== 'Order') {
+                    this.notificationService.error((result.adjustDraftOrderLine as any).message);
+                }
+            });
+    }
+
+    removeOrderLine(event: { lineId: string }) {
+        this.dataService.order.removeDraftOrderLine(this.id, event.lineId).subscribe(result => {
+            if (result.removeDraftOrderLine.__typename !== 'Order') {
+                this.notificationService.error((result.removeDraftOrderLine as any).message);
+            }
+        });
+    }
+
+    getOrderAddressLines(orderAddress?: { [key: string]: string }): string[] {
+        if (!orderAddress) {
+            return [];
+        }
+        return Object.values(orderAddress)
+            .filter(val => val !== 'OrderAddress')
+            .filter(line => !!line);
+    }
+
+    setCustomer() {
+        this.modalService.fromComponent(SelectCustomerDialogComponent).subscribe(result => {
+            if (this.hasId(result)) {
+                this.dataService.order
+                    .setCustomerForDraftOrder(this.id, { customerId: result.id })
+                    .subscribe();
+            } else if (result) {
+                this.dataService.order.setCustomerForDraftOrder(this.id, { input: result }).subscribe();
+            }
+        });
+    }
+
+    setShippingAddress() {
+        this.entity$
+            .pipe(
+                take(1),
+                switchMap(order => {
+                    return this.modalService.fromComponent(SelectAddressDialogComponent, {
+                        locals: {
+                            customerId: order.customer?.id,
+                            currentAddress: order.shippingAddress ?? undefined,
+                        },
+                    });
+                }),
+            )
+            .subscribe(result => {
+                if (result) {
+                    this.dataService.order.setDraftOrderShippingAddress(this.id, result).subscribe();
+                }
+            });
+    }
+
+    setBillingAddress() {
+        this.entity$
+            .pipe(
+                take(1),
+                switchMap(order => {
+                    return this.modalService.fromComponent(SelectAddressDialogComponent, {
+                        locals: {
+                            customerId: order.customer?.id,
+                            currentAddress: order.billingAddress ?? undefined,
+                        },
+                    });
+                }),
+            )
+            .subscribe(result => {
+                if (result) {
+                    this.dataService.order.setDraftOrderBillingAddress(this.id, result).subscribe();
+                }
+            });
+    }
+
+    applyCouponCode(couponCode: string) {
+        this.dataService.order.applyCouponCodeToDraftOrder(this.id, couponCode).subscribe();
+    }
+
+    removeCouponCode(couponCode: string) {
+        this.dataService.order.removeCouponCodeFromDraftOrder(this.id, couponCode).subscribe();
+    }
+
+    setShippingMethod() {
+        combineLatest(this.entity$, this.eligibleShippingMethods$)
+            .pipe(
+                take(1),
+                switchMap(([order, methods]) =>
+                    this.modalService.fromComponent(SelectShippingMethodDialogComponent, {
+                        locals: {
+                            eligibleShippingMethods: methods,
+                            currencyCode: order.currencyCode,
+                            currentSelectionId: order.shippingLines?.[0]?.shippingMethod.id,
+                        },
+                    }),
+                ),
+            )
+            .subscribe(result => {
+                if (result) {
+                    this.dataService.order.setDraftOrderShippingMethod(this.id, result).subscribe();
+                }
+            });
+    }
+
+    updateCustomFields(customFieldsValue: any) {
+        this.dataService.order
+            .updateOrderCustomFields({
+                id: this.id,
+                customFields: customFieldsValue,
+            })
+            .subscribe();
+    }
+
+    deleteOrder() {
+        this.dataService.order.deleteDraftOrder(this.id).subscribe(({ deleteDraftOrder }) => {
+            if (deleteDraftOrder.result === DeletionResult.DELETED) {
+                this.notificationService.success(_('common.notify-delete-success'), {
+                    entity: 'Order',
+                });
+                this.router.navigate(['/orders']);
+            } else if (deleteDraftOrder.message) {
+                this.notificationService.error(deleteDraftOrder.message);
+            }
+        });
+    }
+
+    completeOrder() {
+        this.dataService.order
+            .transitionToState(this.id, 'ArrangingPayment')
+            .subscribe(({ transitionOrderToState }) => {
+                if (transitionOrderToState?.__typename === 'Order') {
+                    this.router.navigate(['/orders', this.id]);
+                } else if (transitionOrderToState?.__typename === 'OrderStateTransitionError') {
+                    this.notificationService.error(transitionOrderToState.transitionError);
+                }
+            });
+    }
+
+    private hasId<T extends { id: string }>(input: T | any): input is { id: string } {
+        return typeof input === 'object' && !!input.id;
+    }
+
+    protected setFormValues(entity: Order.Fragment): void {
+        // empty
+    }
+}

+ 52 - 0
packages/admin-ui/src/lib/order/src/components/draft-order-variant-selector/draft-order-variant-selector.component.html

@@ -0,0 +1,52 @@
+<div class="card">
+    <div class="card-block">
+        <h4 class="card-title">{{ 'order.add-item-to-order' | translate }}</h4>
+        <vdr-product-selector
+            (productSelected)="selectedVariantId$.next($event.productVariantId)"
+        ></vdr-product-selector>
+    </div>
+    <div class="card-block" *ngIf="selectedVariant$ | async as selectedVariant">
+        <div class="variant-details">
+            <img class="mr2" [src]="selectedVariant.featuredAsset || selectedVariant.product.featuredAsset | assetPreview: 32">
+            <div class="details">
+                <div>{{ selectedVariant?.name }}</div>
+                <div class="small">{{ selectedVariant?.sku }}</div>
+            </div>
+            <div class="details ml4">
+                <div class="small">
+                    {{ 'catalog.stock-on-hand' | translate }}: {{ selectedVariant.stockOnHand }}
+                </div>
+                <div class="small">
+                    {{ 'catalog.stock-allocated' | translate }}: {{ selectedVariant.stockAllocated }}
+                </div>
+            </div>
+            <div class="flex-spacer"></div>
+            <div class="details">
+                <div>{{ selectedVariant?.priceWithTax | localeCurrency: currencyCode }}</div>
+                <div class="small" [title]="'order.net-price' | translate">
+                    {{ selectedVariant?.price | localeCurrency: currencyCode }}
+                </div>
+            </div>
+            <div>
+                <input [disabled]="!selectedVariant" type="number" min="0" [(ngModel)]="quantity" />
+            </div>
+            <button
+                [disabled]="!selectedVariant"
+                class="btn btn-small btn-primary"
+                (click)="addItemClick(selectedVariant)"
+            >
+                {{ 'order.add-item-to-order' | translate }}
+            </button>
+        </div>
+        <ng-container *ngIf="orderLineCustomFields.length">
+            <div class="custom-field" *ngFor="let field of orderLineCustomFields">
+                <vdr-custom-field-control
+                    [compact]="true"
+                    [readonly]="false"
+                    [customField]="field"
+                    [customFieldsFormGroup]="customFieldsFormGroup"
+                ></vdr-custom-field-control>
+            </div>
+        </ng-container>
+    </div>
+</div>

+ 25 - 0
packages/admin-ui/src/lib/order/src/components/draft-order-variant-selector/draft-order-variant-selector.component.scss

@@ -0,0 +1,25 @@
+.variant-details {
+    display: flex;
+    align-items: center;
+
+    img {
+        border-radius: var(--border-radius-img);
+        width: 32px;
+        height: 32px;
+    }
+
+    .details {
+        font-size: 0.65rem;
+        line-height: 0.7rem;
+    }
+
+    input {
+        width: 48px;
+        margin: 0 6px;
+    }
+
+    .small {
+        font-size: 11px;
+        color: var(--color-text-300);
+    }
+}

+ 60 - 0
packages/admin-ui/src/lib/order/src/components/draft-order-variant-selector/draft-order-variant-selector.component.ts

@@ -0,0 +1,60 @@
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { FormControl, FormGroup } from '@angular/forms';
+import {
+    CurrencyCode,
+    CustomFieldConfig,
+    DataService,
+    GetProductVariant,
+    GetProductVariantQuery,
+    ProductSelectorSearchQuery,
+} from '@vendure/admin-ui/core';
+import { Observable, Subject } from 'rxjs';
+import { switchMap } from 'rxjs/operators';
+
+@Component({
+    selector: 'vdr-draft-order-variant-selector',
+    templateUrl: './draft-order-variant-selector.component.html',
+    styleUrls: ['./draft-order-variant-selector.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class DraftOrderVariantSelectorComponent implements OnInit {
+    @Input() currencyCode: CurrencyCode;
+    @Input() orderLineCustomFields: CustomFieldConfig[];
+    @Output() addItem = new EventEmitter<{ productVariantId: string; quantity: number; customFields: any }>();
+    customFieldsFormGroup = new FormGroup({});
+    selectedVariant$: Observable<GetProductVariantQuery['productVariant']>;
+    selectedVariantId$ = new Subject<string>();
+    quantity = 1;
+    constructor(private dataService: DataService) {}
+
+    ngOnInit(): void {
+        this.selectedVariant$ = this.selectedVariantId$.pipe(
+            switchMap(id => {
+                if (id) {
+                    return this.dataService.product
+                        .getProductVariant(id)
+                        .mapSingle(({ productVariant }) => productVariant);
+                } else {
+                    return [undefined];
+                }
+            }),
+        );
+        for (const customField of this.orderLineCustomFields) {
+            this.customFieldsFormGroup.addControl(customField.name, new FormControl(''));
+        }
+    }
+
+    addItemClick(selectedVariant: GetProductVariantQuery['productVariant']) {
+        if (selectedVariant) {
+            this.addItem.emit({
+                productVariantId: selectedVariant.id,
+                quantity: this.quantity,
+                customFields: this.orderLineCustomFields.length
+                    ? this.customFieldsFormGroup.value
+                    : undefined,
+            });
+            this.selectedVariantId$.next(undefined);
+            this.customFieldsFormGroup.reset();
+        }
+    }
+}

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

@@ -32,6 +32,7 @@
             *ngIf="
                 order.active === false &&
                 order.state !== 'ArrangingAdditionalPayment' &&
+                order.state !== 'ArrangingPayment' &&
                 0 < outstandingPaymentAmount(order)
             "
             (click)="transitionToState('ArrangingAdditionalPayment')"

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

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

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

@@ -67,8 +67,6 @@ export class OrderEditorComponent
     implements OnInit, OnDestroy
 {
     availableCountries$: Observable<GetAvailableCountries.Items[]>;
-    availableCouponCodes$: Observable<Array<{ code: string; promotionName: string }>>;
-    couponCodeInput$ = new Subject<string>();
     addressCustomFields: CustomFieldConfig[];
     detailForm = new FormGroup({});
     couponCodesControl = new FormControl();
@@ -196,22 +194,7 @@ export class OrderEditorComponent
                 this.orderLineCustomFieldsFormArray.push(formGroup);
             }
         });
-        this.availableCouponCodes$ = concat(
-            this.couponCodeInput$.pipe(
-                distinctUntilChanged(),
-                switchMap(
-                    term =>
-                        this.dataService.promotion.getPromotions(10, 0, {
-                            couponCode: { contains: term },
-                        }).single$,
-                ),
-                map(({ promotions }) =>
-                    // tslint:disable-next-line:no-non-null-assertion
-                    promotions.items.map(p => ({ code: p.couponCode!, promotionName: p.name })),
-                ),
-                startWith([]),
-            ),
-        );
+
         this.addItemCustomFieldsFormArray = new FormArray([]);
         this.addItemCustomFieldsForm = new FormGroup({});
         for (const customField of this.orderLineCustomFields) {

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

@@ -40,10 +40,14 @@
                         [clearable]="true"
                         [searchable]="false"
                     >
-                        <ng-template ng-option-tmp let-item="item">{{ item | stateI18nToken | translate }}</ng-template>
+                        <ng-template ng-option-tmp let-item="item">{{
+                            item | stateI18nToken | translate
+                        }}</ng-template>
                         <ng-template ng-label-tmp let-item="item" let-clear="clear">
                             <span class="ng-value-label"> {{ item | stateI18nToken | translate }}</span>
-                            <span class="ng-value-icon right" (click)="clear(item)" aria-hidden="true">×</span>
+                            <span class="ng-value-icon right" (click)="clear(item)" aria-hidden="true"
+                                >×</span
+                            >
                         </ng-template>
                     </ng-select>
                     <button
@@ -70,6 +74,10 @@
     </vdr-ab-left>
     <vdr-ab-right>
         <vdr-action-bar-items locationId="order-list"></vdr-action-bar-items>
+        <a class="btn btn-primary" *vdrIfPermissions="['CreateOrder']" [routerLink]="['./draft/create']">
+            <clr-icon shape="plus"></clr-icon>
+            {{ 'catalog.create-draft-order' | translate }}
+        </a>
     </vdr-ab-right>
 </vdr-action-bar>
 
@@ -105,7 +113,7 @@
             <vdr-table-row-action
                 iconShape="shopping-cart"
                 [label]="'common.open' | translate"
-                [linkTo]="order.state === 'Modifying' ? ['./', order.id, 'modify'] : ['./', order.id]"
+                [linkTo]="order.state === 'Modifying' ? ['./', order.id, 'modify'] : order.state === 'Draft' ? ['./draft', order.id] : ['./', order.id]"
             ></vdr-table-row-action>
         </td>
     </ng-template>

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

@@ -49,7 +49,7 @@ export class OrderListComponent
             config: {
                 active: false,
                 states: this.orderStates.filter(
-                    s => s !== 'Delivered' && s !== 'Cancelled' && s !== 'Shipped',
+                    s => s !== 'Delivered' && s !== 'Cancelled' && s !== 'Shipped' && s !== 'Draft',
                 ),
             },
         },
@@ -76,6 +76,14 @@ export class OrderListComponent
                 active: true,
             },
         },
+        {
+            name: 'draft',
+            label: _('order.filter-preset-draft'),
+            config: {
+                active: false,
+                states: ['Draft'],
+            },
+        },
     ];
     activePreset$: Observable<string>;
 

+ 8 - 0
packages/admin-ui/src/lib/order/src/components/order-table/order-table-mixin.scss

@@ -18,6 +18,10 @@
         border-top-color: var(--color-grey-200);
     }
 
+    img {
+        border-radius: var(--border-radius-img);
+    }
+
     .order-line-custom-fields {
         display: flex;
         flex-wrap: wrap;
@@ -32,6 +36,10 @@
         }
     }
 
+    .draft-qty {
+        max-width: 48px;
+    }
+
     .order-line-custom-field {
         background-color: var(--color-component-bg-100);
 

+ 30 - 13
packages/admin-ui/src/lib/order/src/components/order-table/order-table.component.html

@@ -24,7 +24,24 @@
                     </div>
                 </td>
                 <td class="align-middle quantity">
-                    {{ line.quantity }}
+                    <ng-container *ngIf="!isDraft; else draft">
+                        {{ line.quantity }}
+                    </ng-container>
+                    <ng-template #draft>
+                        <div class="flex">
+                            <input
+                                class="draft-qty"
+                                type="number"
+                                min="0"
+                                #qtyInput
+                                [value]="line.quantity"
+                                (blur)="draftInputBlur(line, qtyInput.valueAsNumber)"
+                            />
+                            <button class="icon-button" (click)="remove.emit({ lineId: line.id })">
+                                <clr-icon shape="trash"></clr-icon>
+                            </button>
+                        </div>
+                    </ng-template>
                     <vdr-line-refunds [line]="line" [payments]="order.payments"></vdr-line-refunds>
                     <vdr-line-fulfillment [line]="line" [orderState]="order.state"></vdr-line-fulfillment>
                 </td>
@@ -86,18 +103,18 @@
         </tr>
         <ng-container *ngFor="let discount of order.discounts">
             <tr class="order-adjustment" *ngIf="discount.type !== 'OTHER'">
-            <td colspan="5" class="left clr-align-middle">
-                <a [routerLink]="getPromotionLink(discount)">{{ discount.description }}</a>
-                <vdr-chip *ngIf="getCouponCodeForAdjustment(order, discount) as couponCode">{{
-                    couponCode
-                }}</vdr-chip>
-            </td>
-            <td class="clr-align-middle">
-                {{ discount.amountWithTax | localeCurrency: order.currencyCode }}
-                <div class="net-price" [title]="'order.net-price' | translate">
-                    {{ discount.amount | localeCurrency: order.currencyCode }}
-                </div>
-            </td>
+                <td colspan="5" class="left clr-align-middle">
+                    <a [routerLink]="getPromotionLink(discount)">{{ discount.description }}</a>
+                    <vdr-chip *ngIf="getCouponCodeForAdjustment(order, discount) as couponCode">{{
+                        couponCode
+                    }}</vdr-chip>
+                </td>
+                <td class="clr-align-middle">
+                    {{ discount.amountWithTax | localeCurrency: order.currencyCode }}
+                    <div class="net-price" [title]="'order.net-price' | translate">
+                        {{ discount.amount | localeCurrency: order.currencyCode }}
+                    </div>
+                </td>
             </tr>
         </ng-container>
         <tr class="sub-total">

+ 11 - 2
packages/admin-ui/src/lib/order/src/components/order-table/order-table.component.ts

@@ -1,6 +1,6 @@
-import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
 import { FormControl, FormGroup } from '@angular/forms';
-import { AdjustmentType, CustomFieldConfig, OrderDetail } from '@vendure/admin-ui/core';
+import { AdjustmentType, CustomFieldConfig, OrderDetail, OrderDetailFragment } from '@vendure/admin-ui/core';
 
 @Component({
     selector: 'vdr-order-table',
@@ -11,6 +11,9 @@ import { AdjustmentType, CustomFieldConfig, OrderDetail } from '@vendure/admin-u
 export class OrderTableComponent implements OnInit {
     @Input() order: OrderDetail.Fragment;
     @Input() orderLineCustomFields: CustomFieldConfig[];
+    @Input() isDraft = false;
+    @Output() adjust = new EventEmitter<{ lineId: string; quantity: number }>();
+    @Output() remove = new EventEmitter<{ lineId: string }>();
     orderLineCustomFieldsVisible = false;
     customFieldsForLine: {
         [lineId: string]: Array<{ config: CustomFieldConfig; formGroup: FormGroup; value: any }>;
@@ -29,6 +32,12 @@ export class OrderTableComponent implements OnInit {
         this.getLineCustomFields();
     }
 
+    draftInputBlur(line: OrderDetailFragment['lines'][number], quantity: number) {
+        if (line.quantity !== quantity) {
+            this.adjust.emit({ lineId: line.id, quantity });
+        }
+    }
+
     toggleOrderLineCustomFields() {
         this.orderLineCustomFieldsVisible = !this.orderLineCustomFieldsVisible;
     }

+ 45 - 0
packages/admin-ui/src/lib/order/src/components/select-address-dialog/select-address-dialog.component.html

@@ -0,0 +1,45 @@
+<ng-template vdrDialogTitle>{{ 'order.select-address' | translate }}</ng-template>
+
+<clr-tabs *ngIf="addresses$ | async as addresses">
+    <clr-tab *ngIf="customerId && addresses.length">
+        <button clrTabLink>{{ 'order.existing-address' | translate }}</button>
+        <ng-template [(clrIfActive)]="useExisting">
+            <clr-tab-content>
+                <vdr-radio-card-fieldset
+                    class="block mt4"
+                    [idFn]="addressIdFn"
+                    [selectedItemId]="selectedAddress && addressIdFn(selectedAddress)"
+                    (selectItem)="selectedAddress = $event"
+                >
+                    <vdr-radio-card *ngFor="let address of addresses" [item]="address">
+                        <vdr-formatted-address [address]="address"></vdr-formatted-address>
+                    </vdr-radio-card>
+                </vdr-radio-card-fieldset>
+            </clr-tab-content>
+        </ng-template>
+    </clr-tab>
+    <clr-tab>
+        <button clrTabLink>{{ 'customer.create-new-address' | translate }}</button>
+
+        <ng-template [(clrIfActive)]="createNew">
+            <clr-tab-content>
+                <vdr-address-form
+                    [formGroup]="addressForm"
+                    [availableCountries]="availableCountries$ | async"
+                ></vdr-address-form>
+            </clr-tab-content>
+        </ng-template>
+    </clr-tab>
+</clr-tabs>
+
+<ng-template vdrDialogButtons>
+    <button type="button" class="btn" (click)="cancel()">{{ 'common.cancel' | translate }}</button>
+    <button
+        type="submit"
+        (click)="select()"
+        [disabled]="(useExisting && !selectedAddress) || (createNew && addressForm.invalid)"
+        class="btn btn-primary"
+    >
+        {{ 'common.okay' | translate }}
+    </button>
+</ng-template>

+ 0 - 0
packages/admin-ui/src/lib/order/src/components/select-address-dialog/select-address-dialog.component.scss


+ 115 - 0
packages/admin-ui/src/lib/order/src/components/select-address-dialog/select-address-dialog.component.ts

@@ -0,0 +1,115 @@
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import {
+    AddressFragment,
+    CreateAddressInput,
+    CreateCustomerInput,
+    DataService,
+    Dialog,
+    GetAvailableCountriesQuery,
+    GetCustomerAddressesQuery,
+    GetCustomerAddressesQueryVariables,
+    OrderAddressFragment,
+} from '@vendure/admin-ui/core';
+import { pick } from '@vendure/common/lib/pick';
+import { Observable, of } from 'rxjs';
+import { tap } from 'rxjs/operators';
+
+import { Customer } from '../select-customer-dialog/select-customer-dialog.component';
+
+import { GET_CUSTOMER_ADDRESSES } from './select-address-dialog.graphql';
+
+@Component({
+    selector: 'vdr-select-address-dialog',
+    templateUrl: './select-address-dialog.component.html',
+    styleUrls: ['./select-address-dialog.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class SelectAddressDialogComponent implements OnInit, Dialog<CreateAddressInput> {
+    resolveWith: (result?: CreateAddressInput) => void;
+    availableCountries$: Observable<GetAvailableCountriesQuery['countries']['items']>;
+    addresses$: Observable<AddressFragment[]>;
+    customerId: string | undefined;
+    currentAddress: OrderAddressFragment | undefined;
+    addressForm: FormGroup;
+    selectedAddress: AddressFragment | undefined;
+    useExisting = true;
+    createNew = false;
+
+    constructor(private dataService: DataService, private formBuilder: FormBuilder) {}
+
+    ngOnInit(): void {
+        this.addressForm = this.formBuilder.group({
+            fullName: [this.currentAddress?.fullName ?? ''],
+            company: [this.currentAddress?.company ?? ''],
+            streetLine1: [this.currentAddress?.streetLine1 ?? '', Validators.required],
+            streetLine2: [this.currentAddress?.streetLine2 ?? ''],
+            city: [this.currentAddress?.city ?? '', Validators.required],
+            province: [this.currentAddress?.province ?? ''],
+            postalCode: [this.currentAddress?.postalCode ?? '', Validators.required],
+            countryCode: [this.currentAddress?.countryCode ?? '', Validators.required],
+            phoneNumber: [this.currentAddress?.phoneNumber ?? ''],
+        });
+        this.useExisting = !!this.customerId;
+        this.addresses$ = this.customerId
+            ? this.dataService
+                  .query<GetCustomerAddressesQuery, GetCustomerAddressesQueryVariables>(
+                      GET_CUSTOMER_ADDRESSES,
+                      { customerId: this.customerId },
+                  )
+                  .mapSingle(({ customer }) => customer?.addresses ?? [])
+                  .pipe(
+                      tap(addresses => {
+                          if (this.currentAddress) {
+                              this.selectedAddress = addresses.find(
+                                  a =>
+                                      a.streetLine1 === this.currentAddress?.streetLine1 &&
+                                      a.postalCode === this.currentAddress?.postalCode,
+                              );
+                          }
+                          if (addresses.length === 0) {
+                              this.createNew = true;
+                              this.useExisting = false;
+                          }
+                      }),
+                  )
+            : of([]);
+        this.availableCountries$ = this.dataService.settings
+            .getAvailableCountries()
+            .mapSingle(({ countries }) => countries.items);
+    }
+
+    trackByFn(item: Customer) {
+        return item.id;
+    }
+
+    addressIdFn(item: AddressFragment) {
+        return item.streetLine1 + item.postalCode;
+    }
+
+    cancel() {
+        this.resolveWith();
+    }
+
+    select() {
+        if (this.useExisting && this.selectedAddress) {
+            this.resolveWith({
+                ...pick(this.selectedAddress, [
+                    'fullName',
+                    'company',
+                    'streetLine1',
+                    'streetLine2',
+                    'city',
+                    'province',
+                    'phoneNumber',
+                    'postalCode',
+                ]),
+                countryCode: this.selectedAddress.country.code,
+            });
+        }
+        if (this.createNew && this.addressForm.valid) {
+            const formValue = this.addressForm.value;
+            this.resolveWith(formValue);
+        }
+    }
+}

+ 14 - 0
packages/admin-ui/src/lib/order/src/components/select-address-dialog/select-address-dialog.graphql.ts

@@ -0,0 +1,14 @@
+import { ADDRESS_FRAGMENT } from '@vendure/admin-ui/core';
+import { gql } from 'apollo-angular';
+
+export const GET_CUSTOMER_ADDRESSES = gql`
+    query GetCustomerAddresses($customerId: ID!) {
+        customer(id: $customerId) {
+            id
+            addresses {
+                ...Address
+            }
+        }
+    }
+    ${ADDRESS_FRAGMENT}
+`;

+ 74 - 0
packages/admin-ui/src/lib/order/src/components/select-customer-dialog/select-customer-dialog.component.html

@@ -0,0 +1,74 @@
+<ng-template vdrDialogTitle>{{ 'order.set-customer-for-order' | translate }}</ng-template>
+
+<clr-tabs>
+    <clr-tab>
+        <button clrTabLink>{{ 'order.existing-customer' | translate }}</button>
+
+        <ng-template [(clrIfActive)]="useExisting">
+            <clr-tab-content>
+                <ng-select
+                    [items]="customers$ | async"
+                    appendTo="body"
+                    bindLabel="name"
+                    [addTag]="false"
+                    [multiple]="true"
+                    [hideSelected]="true"
+                    [trackByFn]="trackByFn"
+                    [minTermLength]="2"
+                    [loading]="isLoading"
+                    [typeahead]="input$"
+                    [(ngModel)]="selectedCustomer"
+                    class="mt4"
+                >
+                    <ng-template ng-label-tmp let-item="item" let-clear="clear">
+                        <clr-icon shape="user" class="is-solid"></clr-icon
+                        ><span class="ml2 mr2">{{ item.firstName }} {{ item.lastName }}</span>
+                        <vdr-chip>{{ item.emailAddress }}</vdr-chip>
+                    </ng-template>
+                    <ng-template ng-option-tmp let-item="item">
+                        <clr-icon shape="user" class="is-solid"></clr-icon
+                        ><span class="ml2 mr2">{{ item.firstName }} {{ item.lastName }}</span>
+                        <vdr-chip>{{ item.emailAddress }}</vdr-chip>
+                    </ng-template>
+                </ng-select>
+            </clr-tab-content>
+        </ng-template>
+    </clr-tab>
+    <clr-tab>
+        <button clrTabLink>{{ 'customer.create-new-customer' | translate }}</button>
+
+        <ng-template [(clrIfActive)]="createNew">
+            <clr-tab-content>
+                <form [formGroup]="customerForm">
+                <vdr-form-field [label]="'customer.title' | translate" for="title">
+                    <input id="title" type="text" formControlName="title" />
+                </vdr-form-field>
+                <vdr-form-field [label]="'customer.first-name' | translate" for="firstName">
+                    <input id="firstName" type="text" formControlName="firstName" />
+                </vdr-form-field>
+                <vdr-form-field [label]="'customer.last-name' | translate" for="lastName">
+                    <input id="lastName" type="text" formControlName="lastName" />
+                </vdr-form-field>
+                <vdr-form-field [label]="'customer.email-address' | translate" for="emailAddress">
+                    <input id="emailAddress" type="text" formControlName="emailAddress" />
+                </vdr-form-field>
+                <vdr-form-field [label]="'customer.phone-number' | translate" for="phoneNumber">
+                    <input id="phoneNumber" type="text" formControlName="phoneNumber" />
+                </vdr-form-field>
+                </form>
+            </clr-tab-content>
+        </ng-template>
+    </clr-tab>
+</clr-tabs>
+
+<ng-template vdrDialogButtons>
+    <button type="button" class="btn" (click)="cancel()">{{ 'common.cancel' | translate }}</button>
+    <button
+        type="submit"
+        (click)="select()"
+        [disabled]="(useExisting && selectedCustomer.length === 0) || (createNew && customerForm.invalid)"
+        class="btn btn-primary"
+    >
+        {{ 'common.okay' | translate }}
+    </button>
+</ng-template>

+ 0 - 0
packages/admin-ui/src/lib/order/src/components/select-customer-dialog/select-customer-dialog.component.scss


+ 72 - 0
packages/admin-ui/src/lib/order/src/components/select-customer-dialog/select-customer-dialog.component.ts

@@ -0,0 +1,72 @@
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { CreateCustomerInput, DataService, Dialog, GetCustomerListQuery } from '@vendure/admin-ui/core';
+import { concat, Observable, of, Subject } from 'rxjs';
+import { catchError, debounceTime, distinctUntilChanged, switchMap, tap } from 'rxjs/operators';
+
+export type Customer = GetCustomerListQuery['customers']['items'][number];
+
+@Component({
+    selector: 'vdr-select-customer-dialog',
+    templateUrl: './select-customer-dialog.component.html',
+    styleUrls: ['./select-customer-dialog.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class SelectCustomerDialogComponent implements OnInit, Dialog<Customer | CreateCustomerInput> {
+    resolveWith: (result?: Customer | CreateCustomerInput) => void;
+    customerForm: FormGroup;
+    customers$: Observable<Customer[]>;
+    isLoading = false;
+    input$ = new Subject<string>();
+    selectedCustomer: Customer[] = [];
+    useExisting = true;
+    createNew = false;
+
+    constructor(private dataService: DataService, private formBuilder: FormBuilder) {
+        this.customerForm = this.formBuilder.group({
+            title: '',
+            firstName: ['', Validators.required],
+            lastName: ['', Validators.required],
+            phoneNumber: '',
+            emailAddress: ['', [Validators.required, Validators.email]],
+        });
+    }
+
+    ngOnInit(): void {
+        this.customers$ = concat(
+            of([]), // default items
+            this.input$.pipe(
+                debounceTime(200),
+                distinctUntilChanged(),
+                tap(() => (this.isLoading = true)),
+                switchMap(term =>
+                    this.dataService.customer
+                        .getCustomerList(10, 0, term)
+                        .mapStream(({ customers }) => customers.items)
+                        .pipe(
+                            catchError(() => of([])), // empty list on error
+                            tap(() => (this.isLoading = false)),
+                        ),
+                ),
+            ),
+        );
+    }
+
+    trackByFn(item: Customer) {
+        return item.id;
+    }
+
+    cancel() {
+        this.resolveWith();
+    }
+
+    select() {
+        if (this.useExisting && this.selectedCustomer.length === 1) {
+            this.resolveWith(this.selectedCustomer[0]);
+        }
+        if (this.createNew && this.customerForm.valid) {
+            const formValue = this.customerForm.value;
+            this.resolveWith(formValue);
+        }
+    }
+}

+ 35 - 0
packages/admin-ui/src/lib/order/src/components/select-shipping-method-dialog/select-shipping-method-dialog.component.html

@@ -0,0 +1,35 @@
+<ng-template vdrDialogTitle>{{ 'order.select-shipping-method' | translate }}</ng-template>
+<vdr-radio-card-fieldset
+    [idFn]="methodIdFn"
+    [selectedItemId]="selectedMethod?.id"
+    (selectItem)="selectedMethod = $event"
+>
+    <vdr-radio-card *ngFor="let quote of eligibleShippingMethods" [item]="quote">
+        <div class="result-details">
+            <vdr-labeled-data [label]="'settings.shipping-method' | translate">
+                {{ quote.name }}
+            </vdr-labeled-data>
+            <div class="price-row">
+                <vdr-labeled-data [label]="'common.price' | translate">
+                    {{ quote.price | localeCurrency: currencyCode }}
+                </vdr-labeled-data>
+                <vdr-labeled-data [label]="'common.price-with-tax' | translate">
+                    {{ quote.priceWithTax | localeCurrency: currencyCode }}
+                </vdr-labeled-data>
+            </div>
+            <vdr-object-tree *ngIf="quote.metadata" [value]="quote.metadata"></vdr-object-tree>
+        </div>
+    </vdr-radio-card>
+</vdr-radio-card-fieldset>
+
+<ng-template vdrDialogButtons>
+    <button type="button" class="btn" (click)="cancel()">{{ 'common.cancel' | translate }}</button>
+    <button
+        type="submit"
+        (click)="select()"
+        [disabled]="!selectedMethod"
+        class="btn btn-primary"
+    >
+        {{ 'common.okay' | translate }}
+    </button>
+</ng-template>

+ 0 - 0
packages/admin-ui/src/lib/order/src/components/select-shipping-method-dialog/select-shipping-method-dialog.component.scss


+ 45 - 0
packages/admin-ui/src/lib/order/src/components/select-shipping-method-dialog/select-shipping-method-dialog.component.ts

@@ -0,0 +1,45 @@
+import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
+import {
+    CreateAddressInput,
+    CurrencyCode,
+    Dialog,
+    DraftOrderEligibleShippingMethodsQuery,
+} from '@vendure/admin-ui/core';
+
+type ShippingMethodQuote =
+    DraftOrderEligibleShippingMethodsQuery['eligibleShippingMethodsForDraftOrder'][number];
+
+@Component({
+    selector: 'vdr-select-shipping-method-dialog',
+    templateUrl: './select-shipping-method-dialog.component.html',
+    styleUrls: ['./select-shipping-method-dialog.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class SelectShippingMethodDialogComponent implements OnInit, Dialog<string> {
+    resolveWith: (result?: string) => void;
+    eligibleShippingMethods: ShippingMethodQuote[];
+    currentSelectionId: string;
+    currencyCode: CurrencyCode;
+    selectedMethod: ShippingMethodQuote | undefined;
+    constructor() {}
+
+    ngOnInit(): void {
+        if (this.currentSelectionId) {
+            this.selectedMethod = this.eligibleShippingMethods.find(m => m.id === this.currentSelectionId);
+        }
+    }
+
+    methodIdFn(item: ShippingMethodQuote) {
+        return item.id;
+    }
+
+    cancel() {
+        this.resolveWith();
+    }
+
+    select() {
+        if (this.selectedMethod) {
+            this.resolveWith(this.selectedMethod.id);
+        }
+    }
+}

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

@@ -4,6 +4,8 @@ import { SharedModule } from '@vendure/admin-ui/core';
 
 import { AddManualPaymentDialogComponent } from './components/add-manual-payment-dialog/add-manual-payment-dialog.component';
 import { CancelOrderDialogComponent } from './components/cancel-order-dialog/cancel-order-dialog.component';
+import { DraftOrderDetailComponent } from './components/draft-order-detail/draft-order-detail.component';
+import { DraftOrderVariantSelectorComponent } from './components/draft-order-variant-selector/draft-order-variant-selector.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';
@@ -28,9 +30,13 @@ import { PaymentDetailComponent } from './components/payment-detail/payment-deta
 import { PaymentStateLabelComponent } from './components/payment-state-label/payment-state-label.component';
 import { RefundOrderDialogComponent } from './components/refund-order-dialog/refund-order-dialog.component';
 import { RefundStateLabelComponent } from './components/refund-state-label/refund-state-label.component';
+import { SelectCustomerDialogComponent } from './components/select-customer-dialog/select-customer-dialog.component';
 import { SettleRefundDialogComponent } from './components/settle-refund-dialog/settle-refund-dialog.component';
 import { SimpleItemListComponent } from './components/simple-item-list/simple-item-list.component';
 import { orderRoutes } from './order.routes';
+import { SelectAddressDialogComponent } from './components/select-address-dialog/select-address-dialog.component';
+import { CouponCodeSelectorComponent } from './components/coupon-code-selector/coupon-code-selector.component';
+import { SelectShippingMethodDialogComponent } from './components/select-shipping-method-dialog/select-shipping-method-dialog.component';
 
 @NgModule({
     imports: [SharedModule, RouterModule.forChild(orderRoutes)],
@@ -63,6 +69,12 @@ import { orderRoutes } from './order.routes';
         ModificationDetailComponent,
         AddManualPaymentDialogComponent,
         OrderStateSelectDialogComponent,
+        DraftOrderDetailComponent,
+        DraftOrderVariantSelectorComponent,
+        SelectCustomerDialogComponent,
+        SelectAddressDialogComponent,
+        CouponCodeSelectorComponent,
+        SelectShippingMethodDialogComponent,
     ],
 })
 export class OrderModule {}

+ 21 - 3
packages/admin-ui/src/lib/order/src/order.routes.ts

@@ -3,16 +3,17 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
     BreadcrumbLabelLinkPair,
     CanDeactivateDetailGuard,
-    createResolveData,
     detailBreadcrumb,
     OrderDetail,
 } from '@vendure/admin-ui/core';
 import { map } from 'rxjs/operators';
 
+import { DraftOrderDetailComponent } from './components/draft-order-detail/draft-order-detail.component';
 import { OrderDetailComponent } from './components/order-detail/order-detail.component';
 import { OrderEditorComponent } from './components/order-editor/order-editor.component';
 import { OrderListComponent } from './components/order-list/order-list.component';
 import { OrderResolver } from './providers/routing/order-resolver';
+import { OrderGuard } from './providers/routing/order.guard';
 
 export const orderRoutes: Route[] = [
     {
@@ -22,10 +23,25 @@ export const orderRoutes: Route[] = [
             breadcrumb: _('breadcrumb.orders'),
         },
     },
+    {
+        path: 'draft/:id',
+        component: DraftOrderDetailComponent,
+        resolve: {
+            entity: OrderResolver,
+        },
+        canActivate: [OrderGuard],
+        canDeactivate: [CanDeactivateDetailGuard],
+        data: {
+            breadcrumb: orderBreadcrumb,
+        },
+    },
     {
         path: ':id',
         component: OrderDetailComponent,
-        resolve: createResolveData(OrderResolver),
+        resolve: {
+            entity: OrderResolver,
+        },
+        canActivate: [OrderGuard],
         canDeactivate: [CanDeactivateDetailGuard],
         data: {
             breadcrumb: orderBreadcrumb,
@@ -34,7 +50,9 @@ export const orderRoutes: Route[] = [
     {
         path: ':id/modify',
         component: OrderEditorComponent,
-        resolve: createResolveData(OrderResolver),
+        resolve: {
+            entity: OrderResolver,
+        },
         // canDeactivate: [CanDeactivateDetailGuard],
         data: {
             breadcrumb: modifyingOrderBreadcrumb,

+ 46 - 17
packages/admin-ui/src/lib/order/src/providers/routing/order-resolver.ts

@@ -1,8 +1,16 @@
 import { Injectable } from '@angular/core';
-import { Router } from '@angular/router';
-import { BaseEntityResolver } from '@vendure/admin-ui/core';
-import { OrderDetail } from '@vendure/admin-ui/core';
-import { DataService } from '@vendure/admin-ui/core';
+import {
+    ActivatedRouteSnapshot,
+    ActivationStart,
+    Resolve,
+    Router,
+    RouterStateSnapshot,
+} from '@angular/router';
+import { DataService, OrderDetailFragment } from '@vendure/admin-ui/core';
+import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
+import { EMPTY, Observable } from 'rxjs';
+import { filter, map, shareReplay, switchMap, take, takeUntil } from 'rxjs/operators';
+import { DraftOrderDetailComponent } from '../../components/draft-order-detail/draft-order-detail.component';
 
 /**
  * Resolves the id from the path into a Customer entity.
@@ -10,19 +18,40 @@ import { DataService } from '@vendure/admin-ui/core';
 @Injectable({
     providedIn: 'root',
 })
-export class OrderResolver extends BaseEntityResolver<OrderDetail.Fragment> {
-    constructor(router: Router, dataService: DataService) {
-        super(
-            router,
-            {
-                __typename: 'Order',
-                id: '',
-                code: '',
-                createdAt: '',
-                updatedAt: '',
-                total: 0,
-            } as any,
-            id => dataService.order.getOrder(id).mapStream(data => data.order),
+export class OrderResolver implements Resolve<Observable<OrderDetailFragment>> {
+    constructor(private router: Router, private dataService: DataService) {}
+
+    /** @internal */
+    resolve(
+        route: ActivatedRouteSnapshot,
+        state: RouterStateSnapshot,
+    ): Observable<Observable<OrderDetailFragment>> {
+        const id = route.paramMap.get('id');
+
+        // Complete the entity stream upon navigating away
+        const navigateAway$ = this.router.events.pipe(filter(event => event instanceof ActivationStart));
+
+        const stream = this.dataService.order
+            .getOrder(id!)
+            .mapStream(data => data.order)
+            .pipe(
+                switchMap(order => {
+                    if (order?.state === 'Draft' && route.component !== DraftOrderDetailComponent) {
+                        // Make sure Draft orders only get displayed with the DraftOrderDetailComponent
+                        this.router.navigate(['/orders/draft', id]);
+                        return EMPTY;
+                    } else {
+                        return [order];
+                    }
+                }),
+                takeUntil(navigateAway$),
+                filter(notNullOrUndefined),
+                shareReplay(1),
+            );
+
+        return stream.pipe(
+            take(1),
+            map(() => stream),
         );
     }
 }

+ 35 - 0
packages/admin-ui/src/lib/order/src/providers/routing/order.guard.ts

@@ -0,0 +1,35 @@
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
+import { DataService } from '@vendure/admin-ui/core';
+import { EMPTY, Observable } from 'rxjs';
+import { map, mergeMapTo, tap } from 'rxjs/operators';
+
+@Injectable({
+    providedIn: 'root',
+})
+export class OrderGuard implements CanActivate {
+    constructor(private dataService: DataService, private router: Router) {}
+
+    canActivate(
+        route: ActivatedRouteSnapshot,
+        state: RouterStateSnapshot,
+    ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
+        const isDraft = state.url.includes('orders/draft');
+        const id = route.paramMap.get('id');
+        if (isDraft) {
+            if (id === 'create') {
+                return this.dataService.order
+                    .createDraftOrder()
+                    .pipe(
+                        map(({ createDraftOrder }) =>
+                            this.router.parseUrl(`/orders/draft/${createDraftOrder.id}`),
+                        ),
+                    );
+            } else {
+                return true;
+            }
+        } else {
+            return true;
+        }
+    }
+}

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

@@ -90,6 +90,7 @@
     "confirm-delete-zone": "Smazat zónu?",
     "confirm-deletion-of-unused-variants-body": "",
     "confirm-deletion-of-unused-variants-title": "",
+    "create-draft-order": "",
     "create-new-collection": "Vytvořit kolekci",
     "create-new-facet": "Vytvořit nový atribut",
     "create-new-product": "Nový produkt",
@@ -254,6 +255,7 @@
     "notify-update-error": "Vyskytla se chyba, nebylo aktualizováno: { entity }",
     "notify-update-success": "Aktualizováno: { entity }",
     "notify-updated-tags-success": "",
+    "okay": "",
     "open": "Otevřít",
     "password": "Heslo",
     "price": "Cena",
@@ -483,17 +485,22 @@
     "cancel-specified-items": "",
     "cancellation-reason": "Důvod zrušení",
     "cancelled-order-success": "Objednávka úspěšně zrušena",
+    "complete-draft-order": "",
     "confirm-modifications": "Potvrdit úpravy",
     "contents": "Obsah",
     "create-fulfillment": "Zpracovat",
     "create-fulfillment-success": "Zpracováno",
     "customer": "Zákazník",
+    "delete-draft-order": "",
     "edit-billing-address": "Upravit fakturační adresu",
     "edit-shipping-address": "Upravit dodací adresu",
     "error-message": "",
+    "existing-address": "",
+    "existing-customer": "",
     "filter-custom": "Vlastní",
     "filter-preset-active": "Aktivní",
     "filter-preset-completed": "Uzavřené",
+    "filter-preset-draft": "",
     "filter-preset-open": "Otevřené",
     "filter-preset-shipped": "Expedované",
     "fulfill": "Zpracovat",
@@ -574,9 +581,15 @@
     "refunded-count": "{count} {count, plural, one {položka} other {položky}} refundovány",
     "removed-items": "Odebrané položky",
     "search-by-order-filters": "Hledat na základě kódu objednávky / Příjmení / ID transakce",
+    "select-address": "",
+    "select-shipping-method": "",
     "select-state": "Vyberte stav",
+    "set-billing-address": "",
     "set-coupon-codes": "",
+    "set-customer-for-order": "",
     "set-fulfillment-state": "Označit jako {state}",
+    "set-shipping-address": "",
+    "set-shipping-method": "",
     "settle-payment": "Vypořádání platby",
     "settle-payment-error": "Nelze vyřídit platbu",
     "settle-payment-success": "Platba úspěšně vypořádana",
@@ -675,6 +688,7 @@
     "created": "Vytvořeno",
     "declined": "Odmítnuto",
     "delivered": "Doručeno",
+    "draft": "",
     "error": "Chyba",
     "failed": "Selhalo",
     "modifying": "Upravuje se",

+ 14 - 0
packages/admin-ui/src/lib/static/i18n-messages/de.json

@@ -90,6 +90,7 @@
     "confirm-delete-zone": "Zone löschen?",
     "confirm-deletion-of-unused-variants-body": "",
     "confirm-deletion-of-unused-variants-title": "",
+    "create-draft-order": "",
     "create-new-collection": "Neue Kollektion anlegen",
     "create-new-facet": "Neue Facette erstellen",
     "create-new-product": "Neues Produkt",
@@ -254,6 +255,7 @@
     "notify-update-error": "Ein Fehler ist aufgetreten, { entity } konnte nicht aktualisiert werden",
     "notify-update-success": "{ entity } aktualisiert",
     "notify-updated-tags-success": "Tags aktualisiert",
+    "okay": "",
     "open": "Öffnen",
     "password": "Passwort",
     "price": "Preis",
@@ -483,17 +485,22 @@
     "cancel-specified-items": "",
     "cancellation-reason": "Stornierungsgrund",
     "cancelled-order-success": "Bestellung erfolgreich storniert",
+    "complete-draft-order": "",
     "confirm-modifications": "Änderungen bestätigen",
     "contents": "Inhalt",
     "create-fulfillment": "Auftrag ausführen",
     "create-fulfillment-success": "Auftrag ausgeführt",
     "customer": "Kunde",
+    "delete-draft-order": "",
     "edit-billing-address": "Rechnungsadresse bearbeiten",
     "edit-shipping-address": "Versandadresse bearbeiten",
     "error-message": "",
+    "existing-address": "",
+    "existing-customer": "",
     "filter-custom": "Benutzerdefiniert",
     "filter-preset-active": "Aktiv",
     "filter-preset-completed": "Abgeschlossen",
+    "filter-preset-draft": "",
     "filter-preset-open": "Ausstehend",
     "filter-preset-shipped": "Versandt",
     "fulfill": "Ausführen",
@@ -574,9 +581,15 @@
     "refunded-count": "{count} {count, plural, one {Artikel} other {Artikel}} erstattet",
     "removed-items": "Entfernte Artikel",
     "search-by-order-filters": "Suche nach Name / Bestellnummer / Transaktions-ID",
+    "select-address": "",
+    "select-shipping-method": "",
     "select-state": "Status auswählen",
+    "set-billing-address": "",
     "set-coupon-codes": "",
+    "set-customer-for-order": "",
     "set-fulfillment-state": "Abwicklungsstatus wählen",
+    "set-shipping-address": "",
+    "set-shipping-method": "",
     "settle-payment": "Zahlung durchführen",
     "settle-payment-error": "Die Zahlung konnte nicht durchgeführt werden",
     "settle-payment-success": "Zahlung erfolgreich durchgeführt",
@@ -675,6 +688,7 @@
     "created": "Erstellt",
     "declined": "Abgelehnt",
     "delivered": "Zugestellt",
+    "draft": "",
     "error": "Fehler",
     "failed": "Fehlgeschlagen",
     "modifying": "In Bearbeitung",

+ 14 - 0
packages/admin-ui/src/lib/static/i18n-messages/en.json

@@ -90,6 +90,7 @@
     "confirm-delete-zone": "Delete zone?",
     "confirm-deletion-of-unused-variants-body": "The following product variants have been made obsolete due to the addition of new options. They will be deleted during the creation of the new product variants.",
     "confirm-deletion-of-unused-variants-title": "Delete obsolete product variants?",
+    "create-draft-order": "Create draft order",
     "create-new-collection": "Create new collection",
     "create-new-facet": "Create new facet",
     "create-new-product": "New product",
@@ -254,6 +255,7 @@
     "notify-update-error": "An error occurred, could not update { entity }",
     "notify-update-success": "Updated { entity }",
     "notify-updated-tags-success": "Successfully updated tags",
+    "okay": "Okay",
     "open": "Open",
     "password": "Password",
     "price": "Price",
@@ -483,17 +485,22 @@
     "cancel-specified-items": "Cancel specified items",
     "cancellation-reason": "Cancellation reason",
     "cancelled-order-success": "Successfully cancelled order",
+    "complete-draft-order": "Complete draft",
     "confirm-modifications": "Confirm modifications",
     "contents": "Contents",
     "create-fulfillment": "Create fulfillment",
     "create-fulfillment-success": "Created fulfillment",
     "customer": "Customer",
+    "delete-draft-order": "Delete draft",
     "edit-billing-address": "Edit billing address",
     "edit-shipping-address": "Edit shipping address",
     "error-message": "Error message",
+    "existing-address": "Existing address",
+    "existing-customer": "Existing customer",
     "filter-custom": "Custom",
     "filter-preset-active": "Active",
     "filter-preset-completed": "Completed",
+    "filter-preset-draft": "Draft",
     "filter-preset-open": "Open",
     "filter-preset-shipped": "Shipped",
     "fulfill": "Fulfill",
@@ -574,9 +581,15 @@
     "refunded-count": "{count} {count, plural, one {item} other {items}} refunded",
     "removed-items": "Removed items",
     "search-by-order-filters": "Search by name / code / transaction ID",
+    "select-address": "Select address",
+    "select-shipping-method": "Select shipping method",
     "select-state": "Select state",
+    "set-billing-address": "Set billing address",
     "set-coupon-codes": "Set coupon codes",
+    "set-customer-for-order": "Set customer",
     "set-fulfillment-state": "Mark as {state}",
+    "set-shipping-address": "Set shipping address",
+    "set-shipping-method": "Set shipping method",
     "settle-payment": "Settle payment",
     "settle-payment-error": "Could not settle payment",
     "settle-payment-success": "Successfully settled payment",
@@ -675,6 +688,7 @@
     "created": "Created",
     "declined": "Declined",
     "delivered": "Delivered",
+    "draft": "Draft",
     "error": "Error",
     "failed": "Failed",
     "modifying": "Modifying",

+ 14 - 0
packages/admin-ui/src/lib/static/i18n-messages/es.json

@@ -90,6 +90,7 @@
     "confirm-delete-zone": "¿Eliminar zona?",
     "confirm-deletion-of-unused-variants-body": "Las siguientes variantes de producto han quedado obsoletas debido a la incorporación de nuevas opciones. Se eliminarán durante la creación de las nuevas variantes de producto.",
     "confirm-deletion-of-unused-variants-title": "¿Eliminar variantes de producto obsoletas?",
+    "create-draft-order": "",
     "create-new-collection": "Crear nueva colección",
     "create-new-facet": "Crear nueva faceta",
     "create-new-product": "Crear nuevo producto",
@@ -254,6 +255,7 @@
     "notify-update-error": "Ha ocurrido un problema, imposible de actualizar{ entity }",
     "notify-update-success": "Actualizado { entity }",
     "notify-updated-tags-success": "Etiquetas actualizadas con éxito",
+    "okay": "",
     "open": "Abrir",
     "password": "Contraseña",
     "price": "Precio",
@@ -483,17 +485,22 @@
     "cancel-specified-items": "",
     "cancellation-reason": "Motivo de la cancelación",
     "cancelled-order-success": "Pedido cancelado con éxito",
+    "complete-draft-order": "",
     "confirm-modifications": "Confirmar modificaciones",
     "contents": "Contenidos",
     "create-fulfillment": "Crear fulfillment",
     "create-fulfillment-success": "Fulfillment creado",
     "customer": "Cliente",
+    "delete-draft-order": "",
     "edit-billing-address": "Editar dirección de facturación",
     "edit-shipping-address": "Editar dirección de envío",
     "error-message": "Mensaje de error",
+    "existing-address": "",
+    "existing-customer": "",
     "filter-custom": "Personalizado",
     "filter-preset-active": "Activo",
     "filter-preset-completed": "Completado",
+    "filter-preset-draft": "",
     "filter-preset-open": "En curso",
     "filter-preset-shipped": "Enviado",
     "fulfill": "Completar",
@@ -574,9 +581,15 @@
     "refunded-count": "{count} {count, plural, one {artículo} other {artículos}} {count, plural, one {reembolsado} other {reembolsados}}",
     "removed-items": "Artículos eliminados",
     "search-by-order-filters": "Buscar por el código de pedido / apellido del cliente / ID de transacción",
+    "select-address": "",
+    "select-shipping-method": "",
     "select-state": "Seleccionar estado",
+    "set-billing-address": "",
     "set-coupon-codes": "",
+    "set-customer-for-order": "",
     "set-fulfillment-state": "Fijar como {state}",
+    "set-shipping-address": "",
+    "set-shipping-method": "",
     "settle-payment": "Liquidar pago",
     "settle-payment-error": "No pudo liquidar el pago",
     "settle-payment-success": "Pago liquidado con éxito",
@@ -675,6 +688,7 @@
     "created": "Creado",
     "declined": "Rechazado",
     "delivered": "Entregado",
+    "draft": "",
     "error": "Error",
     "failed": "Fallido",
     "modifying": "Modificando",

+ 14 - 0
packages/admin-ui/src/lib/static/i18n-messages/fr.json

@@ -90,6 +90,7 @@
     "confirm-delete-zone": "Supprimer zone ?",
     "confirm-deletion-of-unused-variants-body": "",
     "confirm-deletion-of-unused-variants-title": "",
+    "create-draft-order": "",
     "create-new-collection": "Créer nouvelle collection",
     "create-new-facet": "Créer nouveau composant",
     "create-new-product": "Nouveau produit",
@@ -254,6 +255,7 @@
     "notify-update-error": "Une erreur est survenue, mise à jour de { entity } échouée",
     "notify-update-success": "{ entity } mis à jour",
     "notify-updated-tags-success": "Mots-clés mis à jour avec succès",
+    "okay": "",
     "open": "Ouvert",
     "password": "Mot de passe",
     "price": "Prix",
@@ -483,17 +485,22 @@
     "cancel-specified-items": "",
     "cancellation-reason": "Raison de l'annulation",
     "cancelled-order-success": "Commande annulée",
+    "complete-draft-order": "",
     "confirm-modifications": "Confirmer les modifications",
     "contents": "Contenu",
     "create-fulfillment": "Créer préparation",
     "create-fulfillment-success": "Préparation créée",
     "customer": "Client",
+    "delete-draft-order": "",
     "edit-billing-address": "Modifier l'adresse de facturation",
     "edit-shipping-address": "Modifier l'adresse de livraison",
     "error-message": "Message d'erreur",
+    "existing-address": "",
+    "existing-customer": "",
     "filter-custom": "Personnalisé",
     "filter-preset-active": "Active",
     "filter-preset-completed": "Terminée",
+    "filter-preset-draft": "",
     "filter-preset-open": "Ouverte",
     "filter-preset-shipped": "Expédiée",
     "fulfill": "Préparer",
@@ -574,9 +581,15 @@
     "refunded-count": "{count} {count, plural, one {article remboursé} other {articles remboursés}}",
     "removed-items": "Articles supprimés",
     "search-by-order-filters": "Rehercher par numéro de commande / nom du client / Numéro de transaction",
+    "select-address": "",
+    "select-shipping-method": "",
     "select-state": "Sélectionner un état",
+    "set-billing-address": "",
     "set-coupon-codes": "",
+    "set-customer-for-order": "",
     "set-fulfillment-state": "Marquer {state}",
+    "set-shipping-address": "",
+    "set-shipping-method": "",
     "settle-payment": "Régler le paiement",
     "settle-payment-error": "Règlement du paiement échoué",
     "settle-payment-success": "Paiement réglé",
@@ -675,6 +688,7 @@
     "created": "Créé",
     "declined": "Decliné",
     "delivered": "Livré",
+    "draft": "",
     "error": "Erreur",
     "failed": "Echec",
     "modifying": "En cours de modification",

+ 14 - 0
packages/admin-ui/src/lib/static/i18n-messages/it.json

@@ -90,6 +90,7 @@
     "confirm-delete-zone": "Eliminare la zona?",
     "confirm-deletion-of-unused-variants-body": "Le seguenti varianti sono diventate obsolete a seguito dell'aggiunta di nuove opzioni. Queste verranno cancellate durante la creazione delle nuove varianti.",
     "confirm-deletion-of-unused-variants-title": "Cancellare le varianti obsolete?",
+    "create-draft-order": "",
     "create-new-collection": "Crea nuova Collezione",
     "create-new-facet": "Crea nuovo attributo",
     "create-new-product": "Crea nuovo prodotto",
@@ -254,6 +255,7 @@
     "notify-update-error": "Si è verificato un errore, impossibile aggiornare { entity }",
     "notify-update-success": "Aggiornato { entity }",
     "notify-updated-tags-success": "Tags aggiornati con successo",
+    "okay": "",
     "open": "Apri",
     "password": "Password",
     "price": "Prezzo",
@@ -483,17 +485,22 @@
     "cancel-specified-items": "",
     "cancellation-reason": "Motivo dell'annullamento",
     "cancelled-order-success": "Ordine cancellato con successo",
+    "complete-draft-order": "",
     "confirm-modifications": "Conferma modifiche",
     "contents": "Contenuti",
     "create-fulfillment": "Crea consegna",
     "create-fulfillment-success": "Consegna creata",
     "customer": "Cliente",
+    "delete-draft-order": "",
     "edit-billing-address": "Modifica indirizzo di fatturazione",
     "edit-shipping-address": "Modifica indirizzo di spedizione",
     "error-message": "Messaggio di errore",
+    "existing-address": "",
+    "existing-customer": "",
     "filter-custom": "Personalizzato",
     "filter-preset-active": "Attivi",
     "filter-preset-completed": "Completati",
+    "filter-preset-draft": "",
     "filter-preset-open": "Aperti",
     "filter-preset-shipped": "Spediti",
     "fulfill": "Consegna",
@@ -574,9 +581,15 @@
     "refunded-count": "Hai rimborsato {count} {count, plural, one {prodotto} other {prodotti}}",
     "removed-items": "Prodotti rimossi",
     "search-by-order-filters": "Cerca per codice ordine / cognome cliente / ID transazione",
+    "select-address": "",
+    "select-shipping-method": "",
     "select-state": "Seleziona stato",
+    "set-billing-address": "",
     "set-coupon-codes": "",
+    "set-customer-for-order": "",
     "set-fulfillment-state": "Segna come {state}",
+    "set-shipping-address": "",
+    "set-shipping-method": "",
     "settle-payment": "Incassa pagamento",
     "settle-payment-error": "Non è stato possibile incassare il pagamento",
     "settle-payment-success": "Pagamento incassato con successo",
@@ -675,6 +688,7 @@
     "created": "Creato",
     "declined": "Rifiutato",
     "delivered": "Consegnato",
+    "draft": "",
     "error": "Errore",
     "failed": "Fallito",
     "modifying": "In modifica",

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

@@ -90,6 +90,7 @@
     "confirm-delete-zone": "",
     "confirm-deletion-of-unused-variants-body": "",
     "confirm-deletion-of-unused-variants-title": "",
+    "create-draft-order": "",
     "create-new-collection": "Utwórz nową kolekcje",
     "create-new-facet": "Utwórz faset",
     "create-new-product": "Nowy produkt",
@@ -254,6 +255,7 @@
     "notify-update-error": "Wystąpił błąd, nie można zaktualizować { entity }",
     "notify-update-success": "Zaktualizowano { entity }",
     "notify-updated-tags-success": "",
+    "okay": "",
     "open": "Otwórz",
     "password": "Hasło",
     "price": "Cena",
@@ -483,17 +485,22 @@
     "cancel-specified-items": "",
     "cancellation-reason": "Powód anulowania",
     "cancelled-order-success": "Pomyślnie anulowano zamówienie",
+    "complete-draft-order": "",
     "confirm-modifications": "",
     "contents": "Zawartość",
     "create-fulfillment": "Utwórz wypełnienie",
     "create-fulfillment-success": "Utworzono wypełnienie pomyślnie",
     "customer": "Klient",
+    "delete-draft-order": "",
     "edit-billing-address": "",
     "edit-shipping-address": "",
     "error-message": "",
+    "existing-address": "",
+    "existing-customer": "",
     "filter-custom": "",
     "filter-preset-active": "",
     "filter-preset-completed": "",
+    "filter-preset-draft": "",
     "filter-preset-open": "",
     "filter-preset-shipped": "",
     "fulfill": "Zrealizuj",
@@ -574,9 +581,15 @@
     "refunded-count": "{count} {count, plural, one {zamówienie} other {zamówień}} zwrócono",
     "removed-items": "",
     "search-by-order-filters": "Szukaj po numerze zamówienia / Nazwisko / Numer transakcji",
+    "select-address": "",
+    "select-shipping-method": "",
     "select-state": "",
+    "set-billing-address": "",
     "set-coupon-codes": "",
+    "set-customer-for-order": "",
     "set-fulfillment-state": "",
+    "set-shipping-address": "",
+    "set-shipping-method": "",
     "settle-payment": "Rozlicz płatność",
     "settle-payment-error": "Nie można rozliczyć płatności",
     "settle-payment-success": "Płatność rozliczona pomyślnie",
@@ -675,6 +688,7 @@
     "created": "",
     "declined": "",
     "delivered": "Zrealizowano",
+    "draft": "",
     "error": "",
     "failed": "",
     "modifying": "",

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

@@ -90,6 +90,7 @@
     "confirm-delete-zone": "Excluir zona?",
     "confirm-deletion-of-unused-variants-body": "",
     "confirm-deletion-of-unused-variants-title": "",
+    "create-draft-order": "",
     "create-new-collection": "Criar nova categoria",
     "create-new-facet": "Criar nova etiqueta",
     "create-new-product": "Novo produto",
@@ -254,6 +255,7 @@
     "notify-update-error": "Ocorreu um erro, não foi possível atualizar { entity }",
     "notify-update-success": "Atualizado { entity }",
     "notify-updated-tags-success": "",
+    "okay": "",
     "open": "Aberto",
     "password": "Senha",
     "price": "Preço",
@@ -483,17 +485,22 @@
     "cancel-specified-items": "",
     "cancellation-reason": "Motivo do cancelamento",
     "cancelled-order-success": "Pedido cancelado com sucesso",
+    "complete-draft-order": "",
     "confirm-modifications": "Confirmar modificações",
     "contents": "Conteúdo",
     "create-fulfillment": "Criar a execução",
     "create-fulfillment-success": "Execução criada",
     "customer": "Cliente",
+    "delete-draft-order": "",
     "edit-billing-address": "Editar endereço de fatura",
     "edit-shipping-address": "Editar endereço de envio",
     "error-message": "",
+    "existing-address": "",
+    "existing-customer": "",
     "filter-custom": "Customizar",
     "filter-preset-active": "Ativo",
     "filter-preset-completed": "Concluído",
+    "filter-preset-draft": "",
     "filter-preset-open": "Aberto",
     "filter-preset-shipped": "Enviado",
     "fulfill": "Executar",
@@ -574,9 +581,15 @@
     "refunded-count": "{count} {count, plural, one {item} other {items}} reembolsado",
     "removed-items": "Itens removidos",
     "search-by-order-filters": "Buscar por código do pedido / Sobrenome / Código ID da transação",
+    "select-address": "",
+    "select-shipping-method": "",
     "select-state": "Selecionar estado",
+    "set-billing-address": "",
     "set-coupon-codes": "",
+    "set-customer-for-order": "",
     "set-fulfillment-state": "Marcar como {state}",
+    "set-shipping-address": "",
+    "set-shipping-method": "",
     "settle-payment": "Liquidar pagamento",
     "settle-payment-error": "Não posso liquidar pagamento",
     "settle-payment-success": "Pagamento liquidado com sucesso",
@@ -675,6 +688,7 @@
     "created": "Criado",
     "declined": "Recusado",
     "delivered": "Entregue",
+    "draft": "",
     "error": "Erro",
     "failed": "Falhado",
     "modifying": "Modificando",

+ 14 - 0
packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json

@@ -90,6 +90,7 @@
     "confirm-delete-zone": "Eliminar região?",
     "confirm-deletion-of-unused-variants-body": "As variantes listadas abaixo estão obsoletas e serão eliminadas devido à adição de novas opções.",
     "confirm-deletion-of-unused-variants-title": "Eliminar as variantes obsoletas?",
+    "create-draft-order": "",
     "create-new-collection": "Criar nova categoria",
     "create-new-facet": "Criar nova etiqueta",
     "create-new-product": "Novo produto",
@@ -254,6 +255,7 @@
     "notify-update-error": "Ocorreu um erro. Não foi possível actualizar a entidade { entity }",
     "notify-update-success": "Entidade ({ entity }) actualizada com sucesso",
     "notify-updated-tags-success": "Tags actualizadas com sucesso",
+    "okay": "",
     "open": "Visualizar",
     "password": "Palavra passe",
     "price": "Preço",
@@ -483,17 +485,22 @@
     "cancel-specified-items": "",
     "cancellation-reason": "Motivo do cancelamento",
     "cancelled-order-success": "Encomenda cancelada com sucesso",
+    "complete-draft-order": "",
     "confirm-modifications": "Confirmar modificações",
     "contents": "Conteúdo",
     "create-fulfillment": "Criar entrega",
     "create-fulfillment-success": "O processo entrega foi criado com sucesso",
     "customer": "Cliente",
+    "delete-draft-order": "",
     "edit-billing-address": "Editar morada de faturação",
     "edit-shipping-address": "Editar morada de entrega",
     "error-message": "Mensagem de erro",
+    "existing-address": "",
+    "existing-customer": "",
     "filter-custom": "Customizar",
     "filter-preset-active": "Activa",
     "filter-preset-completed": "Concluída",
+    "filter-preset-draft": "",
     "filter-preset-open": "A processar",
     "filter-preset-shipped": "Enviada",
     "fulfill": "Enviar",
@@ -574,9 +581,15 @@
     "refunded-count": "{count} {count, plural, one {item reembolsado} other {itens reembolsados}}",
     "removed-items": "Itens removidos",
     "search-by-order-filters": "Pesqusiar pelo código da encomenda / apelido do cliente / ID da transação",
+    "select-address": "",
+    "select-shipping-method": "",
     "select-state": "Seleccionar estado",
+    "set-billing-address": "",
     "set-coupon-codes": "",
+    "set-customer-for-order": "",
     "set-fulfillment-state": "Marcar como {state}",
+    "set-shipping-address": "",
+    "set-shipping-method": "",
     "settle-payment": "Liquidar pagamento",
     "settle-payment-error": "Não foi possível liquidar o pagamento",
     "settle-payment-success": "Pagamento liquidado com sucesso",
@@ -675,6 +688,7 @@
     "created": "Criado",
     "declined": "Recusado",
     "delivered": "Entregue",
+    "draft": "",
     "error": "Erro",
     "failed": "Falhou",
     "modifying": "A modificar",

+ 14 - 0
packages/admin-ui/src/lib/static/i18n-messages/ru.json

@@ -90,6 +90,7 @@
     "confirm-delete-zone": "Удалить зону?",
     "confirm-deletion-of-unused-variants-body": "Следующие варианты товаров устарели из за добавления новых опций. Они будут удалены во время создания новых вариантов товара.",
     "confirm-deletion-of-unused-variants-title": "Удалить устаревшие варианты товара?",
+    "create-draft-order": "",
     "create-new-collection": "Создать новую коллекцию",
     "create-new-facet": "Создать новый тег",
     "create-new-product": "Создать новый товар",
@@ -254,6 +255,7 @@
     "notify-update-error": "Произошла ошибка, не удалось обновить { entity }",
     "notify-update-success": "Обновлено { entity }",
     "notify-updated-tags-success": "Успешно обновлены теги",
+    "okay": "",
     "open": "Открыть",
     "password": "Пароль",
     "price": "Цена",
@@ -483,17 +485,22 @@
     "cancel-specified-items": "",
     "cancellation-reason": "Причина отмены",
     "cancelled-order-success": "Успешно отмененный заказ",
+    "complete-draft-order": "",
     "confirm-modifications": "Подтвердите изменения",
     "contents": "Содержимое",
     "create-fulfillment": "Создать выполнение",
     "create-fulfillment-success": "Созданное выполнение",
     "customer": "Клиент",
+    "delete-draft-order": "",
     "edit-billing-address": "Изменить платежный адрес",
     "edit-shipping-address": "Изменить адрес доставки",
     "error-message": "Сообщение об ошибке",
+    "existing-address": "",
+    "existing-customer": "",
     "filter-custom": "Под заказ",
     "filter-preset-active": "Активные",
     "filter-preset-completed": "Завершенные",
+    "filter-preset-draft": "",
     "filter-preset-open": "Открытые",
     "filter-preset-shipped": "Отправленные",
     "fulfill": "Выполнить",
@@ -574,9 +581,15 @@
     "refunded-count": "{count} {count, plural, one {позиция} other {позиций}} возвращен",
     "removed-items": "Удаленные позиции",
     "search-by-order-filters": "Поиск по фамилии клиента / коду заказа / ID транзакции",
+    "select-address": "",
+    "select-shipping-method": "",
     "select-state": "Выберите состояние",
+    "set-billing-address": "",
     "set-coupon-codes": "",
+    "set-customer-for-order": "",
     "set-fulfillment-state": "Отметить как {state}",
+    "set-shipping-address": "",
+    "set-shipping-method": "",
     "settle-payment": "Расчет платежа",
     "settle-payment-error": "Не удалось провести оплату",
     "settle-payment-success": "Успешно проведенный платеж",
@@ -675,6 +688,7 @@
     "created": "Создано",
     "declined": "Отклонено",
     "delivered": "Доставлено",
+    "draft": "",
     "error": "Ошибка",
     "failed": "Неудачно",
     "modifying": "Изменение",

+ 14 - 0
packages/admin-ui/src/lib/static/i18n-messages/uk.json

@@ -90,6 +90,7 @@
     "confirm-delete-zone": "Видалити зону?",
     "confirm-deletion-of-unused-variants-body": "Наступні варіанти товару застаріли через додавання нових опцій. Вони будуть видалені під час створення нових варіантів товару.",
     "confirm-deletion-of-unused-variants-title": "Видалити застарілі варіанти товару?",
+    "create-draft-order": "",
     "create-new-collection": "Створити нову колекцію",
     "create-new-facet": "Створити новий тег",
     "create-new-product": "Створити новий товар",
@@ -254,6 +255,7 @@
     "notify-update-error": "Сталася помилка, не вдалося оновити { entity }",
     "notify-update-success": "Оновлено { entity }",
     "notify-updated-tags-success": "Успішно оновлені теги",
+    "okay": "",
     "open": "Відкрити",
     "password": "Пароль",
     "price": "Ціна",
@@ -483,17 +485,22 @@
     "cancel-specified-items": "",
     "cancellation-reason": "Причина скасування",
     "cancelled-order-success": "Успішно скасоване замовлення",
+    "complete-draft-order": "",
     "confirm-modifications": "Підтвердіть зміни",
     "contents": "Вміст",
     "create-fulfillment": "Створити виконання",
     "create-fulfillment-success": "Створене виконання",
     "customer": "Клієнт",
+    "delete-draft-order": "",
     "edit-billing-address": "Змінити платіжну адресу",
     "edit-shipping-address": "Змінити адресу доставки",
     "error-message": "Повідомлення про помилку",
+    "existing-address": "",
+    "existing-customer": "",
     "filter-custom": "Під замовлення",
     "filter-preset-active": "Активні",
     "filter-preset-completed": "Завершені",
+    "filter-preset-draft": "",
     "filter-preset-open": "Відкриті",
     "filter-preset-shipped": "Відправлені",
     "fulfill": "Виконати",
@@ -574,9 +581,15 @@
     "refunded-count": "{count} {count, plural, one {позиція} other {позицій}} повернено",
     "removed-items": "Видалені позиції",
     "search-by-order-filters": "Пошук за коду замовлення / прізвищем клієнта / ID транзакції",
+    "select-address": "",
+    "select-shipping-method": "",
     "select-state": "Виберіть стан",
+    "set-billing-address": "",
     "set-coupon-codes": "",
+    "set-customer-for-order": "",
     "set-fulfillment-state": "Помітити як {state}",
+    "set-shipping-address": "",
+    "set-shipping-method": "",
     "settle-payment": "Розрахунок платежу",
     "settle-payment-error": "Не вдалося провести оплату",
     "settle-payment-success": "Успішно проведений платіж",
@@ -675,6 +688,7 @@
     "created": "Створено",
     "declined": "Відхилено",
     "delivered": "Доставлено",
+    "draft": "",
     "error": "Помилка",
     "failed": "Невдало",
     "modifying": "Зміна",

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

@@ -90,6 +90,7 @@
     "confirm-delete-zone": "确认删除分区么?",
     "confirm-deletion-of-unused-variants-body": "",
     "confirm-deletion-of-unused-variants-title": "",
+    "create-draft-order": "",
     "create-new-collection": "添加系列",
     "create-new-facet": "添加特征",
     "create-new-product": "添加商品",
@@ -254,6 +255,7 @@
     "notify-update-error": "更新{ entity }失败",
     "notify-update-success": "{ entity }已更新",
     "notify-updated-tags-success": "成功更新标签",
+    "okay": "",
     "open": "详情",
     "password": "密码",
     "price": "价格",
@@ -483,17 +485,22 @@
     "cancel-specified-items": "",
     "cancellation-reason": "取消原因",
     "cancelled-order-success": "订单成功取消",
+    "complete-draft-order": "",
     "confirm-modifications": "确认修改",
     "contents": "具体内容",
     "create-fulfillment": "确认配货",
     "create-fulfillment-success": "确认配货成功",
     "customer": "客户",
+    "delete-draft-order": "",
     "edit-billing-address": "编辑账单地址",
     "edit-shipping-address": "编辑邮寄地址",
     "error-message": "错误消息",
+    "existing-address": "",
+    "existing-customer": "",
     "filter-custom": "自定义",
     "filter-preset-active": "正在选择商品",
     "filter-preset-completed": "已完成",
+    "filter-preset-draft": "",
     "filter-preset-open": "已下单",
     "filter-preset-shipped": "已发货",
     "fulfill": "已配货",
@@ -574,9 +581,15 @@
     "refunded-count": "{count}个商品已退款",
     "removed-items": "",
     "search-by-order-filters": "输入要搜索的订单编号 / 姓 / 交易ID ",
+    "select-address": "",
+    "select-shipping-method": "",
     "select-state": "",
+    "set-billing-address": "",
     "set-coupon-codes": "",
+    "set-customer-for-order": "",
     "set-fulfillment-state": "",
+    "set-shipping-address": "",
+    "set-shipping-method": "",
     "settle-payment": "结算付款",
     "settle-payment-error": "结算付款失败",
     "settle-payment-success": "结算付款成功",
@@ -675,6 +688,7 @@
     "created": "已创建",
     "declined": "已拒绝",
     "delivered": "已完成",
+    "draft": "",
     "error": "错误",
     "failed": "失败",
     "modifying": "正在修改",

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

@@ -90,6 +90,7 @@
     "confirm-delete-zone": "",
     "confirm-deletion-of-unused-variants-body": "",
     "confirm-deletion-of-unused-variants-title": "",
+    "create-draft-order": "",
     "create-new-collection": "新增系列",
     "create-new-facet": "新增特徵",
     "create-new-product": "新增商品",
@@ -254,6 +255,7 @@
     "notify-update-error": "更新{ entity }失敗",
     "notify-update-success": "{ entity }已更新",
     "notify-updated-tags-success": "",
+    "okay": "",
     "open": "詳情",
     "password": "密碼",
     "price": "價格",
@@ -483,17 +485,22 @@
     "cancel-specified-items": "",
     "cancellation-reason": "取消原因",
     "cancelled-order-success": "訂單取消成功",
+    "complete-draft-order": "",
     "confirm-modifications": "",
     "contents": "内容",
     "create-fulfillment": "確認配貨",
     "create-fulfillment-success": "確認配貨成功",
     "customer": "客户",
+    "delete-draft-order": "",
     "edit-billing-address": "",
     "edit-shipping-address": "",
     "error-message": "",
+    "existing-address": "",
+    "existing-customer": "",
     "filter-custom": "",
     "filter-preset-active": "",
     "filter-preset-completed": "",
+    "filter-preset-draft": "",
     "filter-preset-open": "",
     "filter-preset-shipped": "",
     "fulfill": "已配貨",
@@ -574,9 +581,15 @@
     "refunded-count": "{count}個商品已退款",
     "removed-items": "",
     "search-by-order-filters": "輸入要搜索的訂單編號 / 姓 / 交易編號 ",
+    "select-address": "",
+    "select-shipping-method": "",
     "select-state": "",
+    "set-billing-address": "",
     "set-coupon-codes": "",
+    "set-customer-for-order": "",
     "set-fulfillment-state": "",
+    "set-shipping-address": "",
+    "set-shipping-method": "",
     "settle-payment": "結算付款",
     "settle-payment-error": "結算付款失敗",
     "settle-payment-success": "結算付款成功",
@@ -675,6 +688,7 @@
     "created": "",
     "declined": "",
     "delivered": "已完成",
+    "draft": "",
     "error": "",
     "failed": "",
     "modifying": "",

+ 4 - 0
packages/admin-ui/src/lib/static/styles/global/_utilities.scss

@@ -10,6 +10,10 @@ $space-4: $space-unit * 3;
 $space-5: $space-unit * 4;
 
 /////////////// layout ///////////////
+.block {
+    display: block;
+}
+
 .flex {
     display: flex;
 }