Browse Source

Merge branch 'master' into next

Michael Bromley 5 years ago
parent
commit
7abb4104b8
39 changed files with 505 additions and 193 deletions
  1. 2 0
      .github/workflows/build_and_test.yml
  2. 11 11
      packages/admin-ui/i18n-coverage.json
  3. 2 0
      packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.html
  4. 6 1
      packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts
  5. 8 8
      packages/admin-ui/src/lib/catalog/src/components/product-variants-editor/product-variants-editor.component.ts
  6. 3 3
      packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.html
  7. 33 16
      packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.ts
  8. 13 2
      packages/admin-ui/src/lib/catalog/src/components/update-product-option-dialog/update-product-option-dialog.component.html
  9. 36 4
      packages/admin-ui/src/lib/catalog/src/components/update-product-option-dialog/update-product-option-dialog.component.ts
  10. 13 4
      packages/admin-ui/src/lib/core/src/common/base-detail.component.ts
  11. 56 29
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  12. 7 4
      packages/admin-ui/src/lib/core/src/data/definitions/order-definitions.ts
  13. 43 30
      packages/admin-ui/src/lib/core/src/data/definitions/product-definitions.ts
  14. 1 1
      packages/admin-ui/src/lib/core/src/data/providers/product-data.service.ts
  15. 9 7
      packages/admin-ui/src/lib/core/src/providers/job-queue/job-queue.service.ts
  16. 3 3
      packages/admin-ui/src/lib/core/src/shared/components/channel-assignment-control/channel-assignment-control.component.ts
  17. 5 1
      packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.html
  18. 26 26
      packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.ts
  19. 16 5
      packages/admin-ui/src/lib/settings/src/components/role-detail/role-detail.component.ts
  20. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/de.json
  21. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/en.json
  22. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/es.json
  23. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/pl.json
  24. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json
  25. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json
  26. 9 0
      packages/common/src/generated-shop-types.ts
  27. 1 1
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  28. 43 0
      packages/core/e2e/graphql/generated-e2e-shop-types.ts
  29. 1 0
      packages/core/e2e/graphql/shared-definitions.ts
  30. 18 0
      packages/core/e2e/graphql/shop-definitions.ts
  31. 34 16
      packages/core/e2e/shop-auth.e2e-spec.ts
  32. 34 0
      packages/core/e2e/shop-order.e2e-spec.ts
  33. 15 3
      packages/core/src/api/resolvers/entity/order-line-entity.resolver.ts
  34. 18 1
      packages/core/src/api/resolvers/shop/shop-order.resolver.ts
  35. 5 0
      packages/core/src/api/schema/shop-api/shop.api.graphql
  36. 6 4
      packages/core/src/service/services/asset.service.ts
  37. 6 6
      packages/core/src/service/services/customer.service.ts
  38. 14 1
      packages/core/src/service/services/order.service.ts
  39. 2 6
      packages/dev-server/dev-config.ts

+ 2 - 0
.github/workflows/build_and_test.yml

@@ -4,9 +4,11 @@ on:
   push:
     branches:
     - master
+    - next
   pull_request:
     branches:
     - master
+    - next
 env:
   CI: true
   node: 12.x

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

@@ -1,34 +1,34 @@
 {
-  "generatedOn": "2020-06-25T09:38:44.987Z",
-  "lastCommit": "bdfc43d36aba9241598a3d5ce121a0399d44c990",
+  "generatedOn": "2020-06-30T08:09:29.407Z",
+  "lastCommit": "83347b27acf895b746b7b3288593d6c60d1a14dc",
   "translationStatus": {
     "de": {
-      "tokenCount": 654,
+      "tokenCount": 651,
       "translatedCount": 609,
-      "percentage": 93
+      "percentage": 94
     },
     "en": {
-      "tokenCount": 654,
-      "translatedCount": 652,
+      "tokenCount": 651,
+      "translatedCount": 650,
       "percentage": 100
     },
     "es": {
-      "tokenCount": 654,
+      "tokenCount": 651,
       "translatedCount": 467,
-      "percentage": 71
+      "percentage": 72
     },
     "pl": {
-      "tokenCount": 654,
+      "tokenCount": 651,
       "translatedCount": 566,
       "percentage": 87
     },
     "zh_Hans": {
-      "tokenCount": 654,
+      "tokenCount": 651,
       "translatedCount": 550,
       "percentage": 84
     },
     "zh_Hant": {
-      "tokenCount": 654,
+      "tokenCount": 651,
       "translatedCount": 550,
       "percentage": 84
     }

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

@@ -208,6 +208,8 @@
                         [productVariantsFormArray]="detailForm.get('variants')"
                         [taxCategories]="taxCategories$ | async"
                         [customFields]="customVariantFields"
+                        [customOptionFields]="customOptionFields"
+                        [activeLanguage]="languageCode$ | async"
                         (assetChange)="variantAssetChange($event)"
                         (updateProductOption)="updateProductOption($event)"
                         (selectionChange)="selectedVariantIds = $event"

+ 6 - 1
packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts

@@ -76,6 +76,8 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
     taxCategories$: Observable<TaxCategory.Fragment[]>;
     customFields: CustomFieldConfig[];
     customVariantFields: CustomFieldConfig[];
+    customOptionGroupFields: CustomFieldConfig[];
+    customOptionFields: CustomFieldConfig[];
     detailForm: FormGroup;
     assetChanges: SelectedAssets = {};
     variantAssetChanges: { [variantId: string]: SelectedAssets } = {};
@@ -101,6 +103,8 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
         super(route, router, serverConfigService, dataService);
         this.customFields = this.getCustomFieldConfig('Product');
         this.customVariantFields = this.getCustomFieldConfig('ProductVariant');
+        this.customOptionGroupFields = this.getCustomFieldConfig('ProductOptionGroup');
+        this.customOptionFields = this.getCustomFieldConfig('ProductOption');
         this.detailForm = this.formBuilder.group({
             product: this.formBuilder.group({
                 enabled: true,
@@ -156,7 +160,8 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
     }
 
     navigateToTab(tabName: TabName) {
-        this.router.navigate(['./', { tab: tabName }], {
+        this.router.navigate(['./', { ...this.route.snapshot.params, tab: tabName }], {
+            queryParamsHandling: 'merge',
             relativeTo: this.route,
             state: {
                 [IGNORE_CAN_DEACTIVATE_GUARD]: true,

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

@@ -1,19 +1,19 @@
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 import { ActivatedRoute } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import { DeactivateAware } from '@vendure/admin-ui/core';
-import { NotificationService } from '@vendure/admin-ui/core';
-import { ModalService } from '@vendure/admin-ui/core';
-import { getDefaultUiLanguage } from '@vendure/admin-ui/core';
 import {
     CreateProductOptionGroup,
     CreateProductOptionInput,
     CurrencyCode,
+    DataService,
+    DeactivateAware,
+    getDefaultUiLanguage,
     GetProductVariantOptions,
     LanguageCode,
-    ProductOptionGroupFragment,
+    ModalService,
+    NotificationService,
+    ProductOptionGroupWithOptionsFragment,
 } from '@vendure/admin-ui/core';
-import { DataService } from '@vendure/admin-ui/core';
 import { normalizeString } from '@vendure/common/lib/normalize-string';
 import { generateAllCombinations, notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { EMPTY, forkJoin, Observable, of } from 'rxjs';
@@ -290,7 +290,7 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
         }
     }
 
-    private fetchOptionGroups(groupsIds: string[]): Observable<ProductOptionGroupFragment[]> {
+    private fetchOptionGroups(groupsIds: string[]): Observable<ProductOptionGroupWithOptionsFragment[]> {
         return forkJoin(
             groupsIds.map((id) =>
                 this.dataService.product
@@ -301,7 +301,7 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
         );
     }
 
-    private createNewProductVariants(groups: ProductOptionGroupFragment[]) {
+    private createNewProductVariants(groups: ProductOptionGroupWithOptionsFragment[]) {
         const options = groups
             .filter(notNullOrUndefined)
             .map((og) => og.options)

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

@@ -2,9 +2,9 @@
     <div
         class="variant-container card"
         *ngFor="let variant of variants; let i = index"
-        [class.disabled]="!formArray.get([i, 'enabled'])!.value"
+        [class.disabled]="!formArray.get([i, 'enabled'])?.value"
     >
-        <ng-container [formGroup]="formArray.at(i)">
+        <ng-container *ngIf="formArray.at(i)" [formGroup]="formArray.at(i)">
             <div class="card-block header-row">
                 <div class="details">
                     <vdr-title-input class="sku" [readonly]="!('UpdateCatalog' | hasPermission)">
@@ -145,7 +145,7 @@
                                 [icon]="('UpdateCatalog' | hasPermission) && 'pencil'"
                             >
                                 <span class="option-group-name">{{ optionGroupName(option.groupId) }}</span>
-                                {{ option.name }}
+                                {{ optionName(option) }}
                             </vdr-chip>
                         </div>
                     </div>

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

@@ -11,13 +11,12 @@ import {
     SimpleChanges,
 } from '@angular/core';
 import { FormArray } from '@angular/forms';
-import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
-import { Subscription } from 'rxjs';
-
 import {
     CustomFieldConfig,
     FacetValue,
     FacetWithValues,
+    LanguageCode,
+    ProductOptionFragment,
     ProductVariant,
     ProductWithVariants,
     TaxCategory,
@@ -25,6 +24,9 @@ import {
 } from '@vendure/admin-ui/core';
 import { flattenFacetValues } from '@vendure/admin-ui/core';
 import { ModalService } from '@vendure/admin-ui/core';
+import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
+import { Subscription } from 'rxjs';
+
 import { AssetChange } from '../product-assets/product-assets.component';
 import { VariantFormValue } from '../product-detail/product-detail.component';
 import { UpdateProductOptionDialogComponent } from '../update-product-option-dialog/update-product-option-dialog.component';
@@ -46,6 +48,8 @@ export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestro
     @Input() facets: FacetWithValues.Fragment[];
     @Input() optionGroups: ProductWithVariants.OptionGroups[];
     @Input() customFields: CustomFieldConfig[];
+    @Input() customOptionFields: CustomFieldConfig[];
+    @Input() activeLanguage: LanguageCode;
     @Output() assetChange = new EventEmitter<VariantAssetChange>();
     @Output() selectionChange = new EventEmitter<string[]>();
     @Output() selectFacetValueClick = new EventEmitter<string[]>();
@@ -77,7 +81,7 @@ export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestro
     getTaxCategoryName(index: number): string {
         const control = this.formArray.at(index).get(['taxCategoryId']);
         if (control && this.taxCategories) {
-            const match = this.taxCategories.find(t => t.id === control.value);
+            const match = this.taxCategories.find((t) => t.id === control.value);
             return match ? match.name : '';
         }
         return '';
@@ -92,7 +96,7 @@ export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestro
             variantId,
             ...event,
         });
-        const index = this.variants.findIndex(v => v.id === variantId);
+        const index = this.variants.findIndex((v) => v.id === variantId);
         this.formArray.at(index).markAsDirty();
     }
 
@@ -100,7 +104,7 @@ export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestro
         if (this.areAllSelected()) {
             this.selectedVariantIds = [];
         } else {
-            this.selectedVariantIds = this.variants.map(v => v.id);
+            this.selectedVariantIds = this.variants.map((v) => v.id);
         }
         this.selectionChange.emit(this.selectedVariantIds);
     }
@@ -116,17 +120,28 @@ export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestro
     }
 
     optionGroupName(optionGroupId: string): string | undefined {
-        const group = this.optionGroups.find(g => g.id === optionGroupId);
-        return group && group.name;
+        const group = this.optionGroups.find((g) => g.id === optionGroupId);
+        if (group) {
+            const translation =
+                group?.translations.find((t) => t.languageCode === this.activeLanguage) ??
+                group.translations[0];
+            return translation.name;
+        }
+    }
+
+    optionName(option: ProductOptionFragment) {
+        const translation =
+            option.translations.find((t) => t.languageCode === this.activeLanguage) ?? option.translations[0];
+        return translation.name;
     }
 
     pendingFacetValues(index: number) {
         if (this.facets) {
             const formFacetValueIds = this.getFacetValueIds(index);
-            const variantFacetValueIds = this.variants[index].facetValues.map(fv => fv.id);
+            const variantFacetValueIds = this.variants[index].facetValues.map((fv) => fv.id);
             return formFacetValueIds
-                .filter(x => !variantFacetValueIds.includes(x))
-                .map(id => this.facetValues.find(fv => fv.id === id))
+                .filter((x) => !variantFacetValueIds.includes(x))
+                .map((id) => this.facetValues.find((fv) => fv.id === id))
                 .filter(notNullOrUndefined);
         } else {
             return [];
@@ -136,18 +151,18 @@ export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestro
     existingFacetValues(index: number) {
         const variant = this.variants[index];
         const formFacetValueIds = this.getFacetValueIds(index);
-        const intersection = [...formFacetValueIds].filter(x =>
-            variant.facetValues.map(fv => fv.id).includes(x),
+        const intersection = [...formFacetValueIds].filter((x) =>
+            variant.facetValues.map((fv) => fv.id).includes(x),
         );
         return intersection
-            .map(id => variant.facetValues.find(fv => fv.id === id))
+            .map((id) => variant.facetValues.find((fv) => fv.id === id))
             .filter(notNullOrUndefined);
     }
 
     removeFacetValue(index: number, facetValueId: string) {
         const formGroup = this.formArray.at(index);
         const newValue = (formGroup.value as VariantFormValue).facetValueIds.filter(
-            id => id !== facetValueId,
+            (id) => id !== facetValueId,
         );
         formGroup.patchValue({
             facetValueIds: newValue,
@@ -169,9 +184,11 @@ export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestro
                 size: 'md',
                 locals: {
                     productOption: option,
+                    activeLanguage: this.activeLanguage,
+                    customFields: this.customOptionFields,
                 },
             })
-            .subscribe(result => {
+            .subscribe((result) => {
                 if (result) {
                     this.updateProductOption.emit(result);
                 }

+ 13 - 2
packages/admin-ui/src/lib/catalog/src/components/update-product-option-dialog/update-product-option-dialog.component.html

@@ -1,5 +1,4 @@
 <ng-template vdrDialogTitle>{{ 'catalog.update-product-option' | translate }}</ng-template>
-
 <vdr-form-field [label]="'catalog.option-name' | translate" for="name">
     <input
         id="name"
@@ -13,13 +12,25 @@
 <vdr-form-field [label]="'common.code' | translate" for="code">
     <input id="code" type="text" #codeInput="ngModel" required [(ngModel)]="code" pattern="[a-z0-9_-]+" />
 </vdr-form-field>
+<section *ngIf="customFields.length">
+    <label>{{ 'common.custom-fields' | translate }}</label>
+    <ng-container *ngFor="let customField of customFields">
+        <vdr-custom-field-control
+            *ngIf="customFieldsForm.get(customField.name)"
+            entityName="ProductOption"
+            [customFieldsFormGroup]="customFieldsForm"
+            [customField]="customField"
+            [readonly]="!('UpdateCatalog' | hasPermission)"
+        ></vdr-custom-field-control>
+    </ng-container>
+</section>
 
 <ng-template vdrDialogButtons>
     <button type="button" class="btn" (click)="cancel()">{{ 'common.cancel' | translate }}</button>
     <button
         type="submit"
         (click)="update()"
-        [disabled]="nameInput.invalid || codeInput.invalid || (nameInput.pristine && codeInput.pristine)"
+        [disabled]="nameInput.invalid || codeInput.invalid || (nameInput.pristine && codeInput.pristine && customFieldsForm.pristine)"
         class="btn btn-primary"
     >
         {{ 'catalog.update-product-option' | translate }}

+ 36 - 4
packages/admin-ui/src/lib/catalog/src/components/update-product-option-dialog/update-product-option-dialog.component.ts

@@ -1,5 +1,11 @@
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
-import { ProductVariant, UpdateProductOptionInput } from '@vendure/admin-ui/core';
+import { FormControl, FormGroup } from '@angular/forms';
+import {
+    CustomFieldConfig,
+    LanguageCode,
+    ProductVariant,
+    UpdateProductOptionInput,
+} from '@vendure/admin-ui/core';
 import { createUpdatedTranslatable } from '@vendure/admin-ui/core';
 import { Dialog } from '@vendure/admin-ui/core';
 import { normalizeString } from '@vendure/common/lib/normalize-string';
@@ -14,22 +20,48 @@ export class UpdateProductOptionDialogComponent implements Dialog<UpdateProductO
     resolveWith: (result?: UpdateProductOptionInput) => void;
     // Provided by caller
     productOption: ProductVariant.Options;
+    activeLanguage: LanguageCode;
     name: string;
     code: string;
+    customFields: CustomFieldConfig[];
     codeInputTouched = false;
+    customFieldsForm: FormGroup;
 
     ngOnInit(): void {
-        this.name = this.productOption.name;
+        const currentTranslation = this.productOption.translations.find(
+            (t) => t.languageCode === this.activeLanguage,
+        );
+        this.name = currentTranslation?.name ?? '';
         this.code = this.productOption.code;
+        this.customFieldsForm = new FormGroup({});
+        if (this.customFields) {
+            const cfCurrentTranslation =
+                (currentTranslation && (currentTranslation as any).customFields) || {};
+
+            for (const fieldDef of this.customFields) {
+                const key = fieldDef.name;
+                const value =
+                    fieldDef.type === 'localeString'
+                        ? cfCurrentTranslation[key]
+                        : (this.productOption as any).customFields[key];
+                this.customFieldsForm.addControl(fieldDef.name, new FormControl(value));
+            }
+        }
     }
 
     update() {
         const result = createUpdatedTranslatable({
             translatable: this.productOption,
-            languageCode: this.productOption.languageCode,
+            languageCode: this.activeLanguage,
             updatedFields: {
                 code: this.code,
                 name: this.name,
+                customFields: this.customFieldsForm.value,
+            },
+            customFieldConfig: this.customFields,
+            defaultTranslation: {
+                languageCode: this.activeLanguage,
+                name: '',
             },
         });
         this.resolveWith(result);
@@ -40,7 +72,7 @@ export class UpdateProductOptionDialogComponent implements Dialog<UpdateProductO
     }
 
     updateCode(nameValue: string) {
-        if (!this.codeInputTouched) {
+        if (!this.codeInputTouched && !this.productOption.code) {
             this.code = normalizeString(nameValue, '-');
         }
     }

+ 13 - 4
packages/admin-ui/src/lib/core/src/common/base-detail.component.ts

@@ -82,9 +82,18 @@ export abstract class BaseDetailComponent<Entity extends { id: string; updatedAt
     }
 
     protected setQueryParam(key: string, value: any) {
-        this.router.navigate(['./', { [key]: value }], {
-            relativeTo: this.route,
-            queryParamsHandling: 'merge',
-        });
+        this.router.navigate(
+            [
+                './',
+                {
+                    ...this.route.snapshot.params,
+                    [key]: value,
+                },
+            ],
+            {
+                relativeTo: this.route,
+                queryParamsHandling: 'merge',
+            },
+        );
     }
 }

+ 56 - 29
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -4734,7 +4734,7 @@ export type RefundFragment = (
   & Pick<Refund, 'id' | 'state' | 'items' | 'shipping' | 'adjustment' | 'transactionId' | 'paymentId'>
 );
 
-export type ShippingAddressFragment = (
+export type OrderAddressFragment = (
   { __typename?: 'OrderAddress' }
   & Pick<OrderAddress, 'fullName' | 'company' | 'streetLine1' | 'streetLine2' | 'city' | 'province' | 'postalCode' | 'country' | 'phoneNumber'>
 );
@@ -4795,7 +4795,10 @@ export type OrderDetailFragment = (
     & Pick<ShippingMethod, 'id' | 'code' | 'description'>
   )>, shippingAddress?: Maybe<(
     { __typename?: 'OrderAddress' }
-    & ShippingAddressFragment
+    & OrderAddressFragment
+  )>, billingAddress?: Maybe<(
+    { __typename?: 'OrderAddress' }
+    & OrderAddressFragment
   )>, payments?: Maybe<Array<(
     { __typename?: 'Payment' }
     & Pick<Payment, 'id' | 'createdAt' | 'transactionId' | 'amount' | 'method' | 'state' | 'metadata'>
@@ -4982,6 +4985,24 @@ export type AssetFragment = (
   )> }
 );
 
+export type ProductOptionGroupFragment = (
+  { __typename?: 'ProductOptionGroup' }
+  & Pick<ProductOptionGroup, 'id' | 'code' | 'languageCode' | 'name'>
+  & { translations: Array<(
+    { __typename?: 'ProductOptionGroupTranslation' }
+    & Pick<ProductOptionGroupTranslation, 'id' | 'languageCode' | 'name'>
+  )> }
+);
+
+export type ProductOptionFragment = (
+  { __typename?: 'ProductOption' }
+  & Pick<ProductOption, 'id' | 'code' | 'languageCode' | 'name' | 'groupId'>
+  & { translations: Array<(
+    { __typename?: 'ProductOptionTranslation' }
+    & Pick<ProductOptionTranslation, 'id' | 'languageCode' | 'name'>
+  )> }
+);
+
 export type ProductVariantFragment = (
   { __typename?: 'ProductVariant' }
   & Pick<ProductVariant, 'id' | 'createdAt' | 'updatedAt' | 'enabled' | 'languageCode' | 'name' | 'price' | 'currencyCode' | 'priceIncludesTax' | 'priceWithTax' | 'stockOnHand' | 'trackInventory' | 'sku'>
@@ -4993,11 +5014,7 @@ export type ProductVariantFragment = (
     & Pick<TaxCategory, 'id' | 'name'>
   ), options: Array<(
     { __typename?: 'ProductOption' }
-    & Pick<ProductOption, 'id' | 'code' | 'languageCode' | 'name' | 'groupId'>
-    & { translations: Array<(
-      { __typename?: 'ProductOptionTranslation' }
-      & Pick<ProductOptionTranslation, 'id' | 'languageCode' | 'name'>
-    )> }
+    & ProductOptionFragment
   )>, facetValues: Array<(
     { __typename?: 'FacetValue' }
     & Pick<FacetValue, 'id' | 'code' | 'name'>
@@ -5031,7 +5048,7 @@ export type ProductWithVariantsFragment = (
     & Pick<ProductTranslation, 'id' | 'languageCode' | 'name' | 'slug' | 'description'>
   )>, optionGroups: Array<(
     { __typename?: 'ProductOptionGroup' }
-    & Pick<ProductOptionGroup, 'id' | 'languageCode' | 'code' | 'name'>
+    & ProductOptionGroupFragment
   )>, variants: Array<(
     { __typename?: 'ProductVariant' }
     & ProductVariantFragment
@@ -5048,7 +5065,7 @@ export type ProductWithVariantsFragment = (
   )> }
 );
 
-export type ProductOptionGroupFragment = (
+export type ProductOptionGroupWithOptionsFragment = (
   { __typename?: 'ProductOptionGroup' }
   & Pick<ProductOptionGroup, 'id' | 'createdAt' | 'updatedAt' | 'languageCode' | 'code' | 'name'>
   & { translations: Array<(
@@ -5138,7 +5155,7 @@ export type CreateProductOptionGroupMutation = (
   { __typename?: 'Mutation' }
   & { createProductOptionGroup: (
     { __typename?: 'ProductOptionGroup' }
-    & ProductOptionGroupFragment
+    & ProductOptionGroupWithOptionsFragment
   ) }
 );
 
@@ -5151,7 +5168,7 @@ export type GetProductOptionGroupQuery = (
   { __typename?: 'Query' }
   & { productOptionGroup?: Maybe<(
     { __typename?: 'ProductOptionGroup' }
-    & ProductOptionGroupFragment
+    & ProductOptionGroupWithOptionsFragment
   )> }
 );
 
@@ -5385,7 +5402,7 @@ export type UpdateProductOptionMutation = (
   { __typename?: 'Mutation' }
   & { updateProductOption: (
     { __typename?: 'ProductOption' }
-    & Pick<ProductOption, 'id' | 'createdAt' | 'updatedAt' | 'code' | 'name'>
+    & ProductOptionFragment
   ) }
 );
 
@@ -5417,7 +5434,7 @@ export type GetProductVariantOptionsQuery = (
       & Pick<ProductOptionGroup, 'id' | 'name' | 'code'>
       & { options: Array<(
         { __typename?: 'ProductOption' }
-        & Pick<ProductOption, 'id' | 'createdAt' | 'updatedAt' | 'name' | 'code'>
+        & ProductOptionFragment
       )> }
     )>, variants: Array<(
       { __typename?: 'ProductVariant' }
@@ -7135,8 +7152,8 @@ export namespace Refund {
   export type Fragment = RefundFragment;
 }
 
-export namespace ShippingAddress {
-  export type Fragment = ShippingAddressFragment;
+export namespace OrderAddress {
+  export type Fragment = OrderAddressFragment;
 }
 
 export namespace Order {
@@ -7164,7 +7181,8 @@ export namespace OrderDetail {
   export type Adjustments = AdjustmentFragment;
   export type Promotions = (NonNullable<OrderDetailFragment['promotions'][0]>);
   export type ShippingMethod = (NonNullable<OrderDetailFragment['shippingMethod']>);
-  export type ShippingAddress = ShippingAddressFragment;
+  export type ShippingAddress = OrderAddressFragment;
+  export type BillingAddress = OrderAddressFragment;
   export type Payments = (NonNullable<(NonNullable<OrderDetailFragment['payments']>)[0]>);
   export type Refunds = (NonNullable<(NonNullable<(NonNullable<OrderDetailFragment['payments']>)[0]>)['refunds'][0]>);
   export type OrderItems = (NonNullable<(NonNullable<(NonNullable<(NonNullable<OrderDetailFragment['payments']>)[0]>)['refunds'][0]>)['orderItems'][0]>);
@@ -7246,17 +7264,26 @@ export namespace Asset {
   export type FocalPoint = (NonNullable<AssetFragment['focalPoint']>);
 }
 
+export namespace ProductOptionGroup {
+  export type Fragment = ProductOptionGroupFragment;
+  export type Translations = (NonNullable<ProductOptionGroupFragment['translations'][0]>);
+}
+
+export namespace ProductOption {
+  export type Fragment = ProductOptionFragment;
+  export type Translations = (NonNullable<ProductOptionFragment['translations'][0]>);
+}
+
 export namespace ProductVariant {
   export type Fragment = ProductVariantFragment;
   export type TaxRateApplied = ProductVariantFragment['taxRateApplied'];
   export type TaxCategory = ProductVariantFragment['taxCategory'];
-  export type Options = (NonNullable<ProductVariantFragment['options'][0]>);
-  export type Translations = (NonNullable<(NonNullable<ProductVariantFragment['options'][0]>)['translations'][0]>);
+  export type Options = ProductOptionFragment;
   export type FacetValues = (NonNullable<ProductVariantFragment['facetValues'][0]>);
   export type Facet = (NonNullable<ProductVariantFragment['facetValues'][0]>)['facet'];
   export type FeaturedAsset = AssetFragment;
   export type Assets = AssetFragment;
-  export type _Translations = (NonNullable<ProductVariantFragment['translations'][0]>);
+  export type Translations = (NonNullable<ProductVariantFragment['translations'][0]>);
 }
 
 export namespace ProductWithVariants {
@@ -7264,18 +7291,18 @@ export namespace ProductWithVariants {
   export type FeaturedAsset = AssetFragment;
   export type Assets = AssetFragment;
   export type Translations = (NonNullable<ProductWithVariantsFragment['translations'][0]>);
-  export type OptionGroups = (NonNullable<ProductWithVariantsFragment['optionGroups'][0]>);
+  export type OptionGroups = ProductOptionGroupFragment;
   export type Variants = ProductVariantFragment;
   export type FacetValues = (NonNullable<ProductWithVariantsFragment['facetValues'][0]>);
   export type Facet = (NonNullable<ProductWithVariantsFragment['facetValues'][0]>)['facet'];
   export type Channels = (NonNullable<ProductWithVariantsFragment['channels'][0]>);
 }
 
-export namespace ProductOptionGroup {
-  export type Fragment = ProductOptionGroupFragment;
-  export type Translations = (NonNullable<ProductOptionGroupFragment['translations'][0]>);
-  export type Options = (NonNullable<ProductOptionGroupFragment['options'][0]>);
-  export type _Translations = (NonNullable<(NonNullable<ProductOptionGroupFragment['options'][0]>)['translations'][0]>);
+export namespace ProductOptionGroupWithOptions {
+  export type Fragment = ProductOptionGroupWithOptionsFragment;
+  export type Translations = (NonNullable<ProductOptionGroupWithOptionsFragment['translations'][0]>);
+  export type Options = (NonNullable<ProductOptionGroupWithOptionsFragment['options'][0]>);
+  export type _Translations = (NonNullable<(NonNullable<ProductOptionGroupWithOptionsFragment['options'][0]>)['translations'][0]>);
 }
 
 export namespace UpdateProduct {
@@ -7311,13 +7338,13 @@ export namespace UpdateProductVariants {
 export namespace CreateProductOptionGroup {
   export type Variables = CreateProductOptionGroupMutationVariables;
   export type Mutation = CreateProductOptionGroupMutation;
-  export type CreateProductOptionGroup = ProductOptionGroupFragment;
+  export type CreateProductOptionGroup = ProductOptionGroupWithOptionsFragment;
 }
 
 export namespace GetProductOptionGroup {
   export type Variables = GetProductOptionGroupQueryVariables;
   export type Query = GetProductOptionGroupQuery;
-  export type ProductOptionGroup = ProductOptionGroupFragment;
+  export type ProductOptionGroup = ProductOptionGroupWithOptionsFragment;
 }
 
 export namespace AddOptionToGroup {
@@ -7411,7 +7438,7 @@ export namespace SearchProducts {
 export namespace UpdateProductOption {
   export type Variables = UpdateProductOptionMutationVariables;
   export type Mutation = UpdateProductOptionMutation;
-  export type UpdateProductOption = UpdateProductOptionMutation['updateProductOption'];
+  export type UpdateProductOption = ProductOptionFragment;
 }
 
 export namespace DeleteProductVariant {
@@ -7425,7 +7452,7 @@ export namespace GetProductVariantOptions {
   export type Query = GetProductVariantOptionsQuery;
   export type Product = (NonNullable<GetProductVariantOptionsQuery['product']>);
   export type OptionGroups = (NonNullable<(NonNullable<GetProductVariantOptionsQuery['product']>)['optionGroups'][0]>);
-  export type Options = (NonNullable<(NonNullable<(NonNullable<GetProductVariantOptionsQuery['product']>)['optionGroups'][0]>)['options'][0]>);
+  export type Options = ProductOptionFragment;
   export type Variants = (NonNullable<(NonNullable<GetProductVariantOptionsQuery['product']>)['variants'][0]>);
   export type _Options = (NonNullable<(NonNullable<(NonNullable<GetProductVariantOptionsQuery['product']>)['variants'][0]>)['options'][0]>);
 }

+ 7 - 4
packages/admin-ui/src/lib/core/src/data/definitions/order-definitions.ts

@@ -21,8 +21,8 @@ export const REFUND_FRAGMENT = gql`
     }
 `;
 
-export const SHIPPING_ADDRESS_FRAGMENT = gql`
-    fragment ShippingAddress on OrderAddress {
+export const ORDER_ADDRESS_FRAGMENT = gql`
+    fragment OrderAddress on OrderAddress {
         fullName
         company
         streetLine1
@@ -130,7 +130,10 @@ export const ORDER_DETAIL_FRAGMENT = gql`
             description
         }
         shippingAddress {
-            ...ShippingAddress
+            ...OrderAddress
+        }
+        billingAddress {
+            ...OrderAddress
         }
         payments {
             id
@@ -163,7 +166,7 @@ export const ORDER_DETAIL_FRAGMENT = gql`
         total
     }
     ${ADJUSTMENT_FRAGMENT}
-    ${SHIPPING_ADDRESS_FRAGMENT}
+    ${ORDER_ADDRESS_FRAGMENT}
     ${FULFILLMENT_FRAGMENT}
     ${ORDER_LINE_FRAGMENT}
 `;

+ 43 - 30
packages/admin-ui/src/lib/core/src/data/definitions/product-definitions.ts

@@ -20,6 +20,35 @@ export const ASSET_FRAGMENT = gql`
     }
 `;
 
+export const PRODUCT_OPTION_GROUP_FRAGMENT = gql`
+    fragment ProductOptionGroup on ProductOptionGroup {
+        id
+        code
+        languageCode
+        name
+        translations {
+            id
+            languageCode
+            name
+        }
+    }
+`;
+
+export const PRODUCT_OPTION_FRAGMENT = gql`
+    fragment ProductOption on ProductOption {
+        id
+        code
+        languageCode
+        name
+        groupId
+        translations {
+            id
+            languageCode
+            name
+        }
+    }
+`;
+
 export const PRODUCT_VARIANT_FRAGMENT = gql`
     fragment ProductVariant on ProductVariant {
         id
@@ -45,16 +74,7 @@ export const PRODUCT_VARIANT_FRAGMENT = gql`
         }
         sku
         options {
-            id
-            code
-            languageCode
-            name
-            groupId
-            translations {
-                id
-                languageCode
-                name
-            }
+            ...ProductOption
         }
         facetValues {
             id
@@ -77,6 +97,7 @@ export const PRODUCT_VARIANT_FRAGMENT = gql`
             name
         }
     }
+    ${PRODUCT_OPTION_FRAGMENT}
     ${ASSET_FRAGMENT}
 `;
 
@@ -104,10 +125,7 @@ export const PRODUCT_WITH_VARIANTS_FRAGMENT = gql`
             description
         }
         optionGroups {
-            id
-            languageCode
-            code
-            name
+            ...ProductOptionGroup
         }
         variants {
             ...ProductVariant
@@ -126,12 +144,13 @@ export const PRODUCT_WITH_VARIANTS_FRAGMENT = gql`
             code
         }
     }
+    ${PRODUCT_OPTION_GROUP_FRAGMENT}
     ${PRODUCT_VARIANT_FRAGMENT}
     ${ASSET_FRAGMENT}
 `;
 
-export const PRODUCT_OPTION_GROUP_FRAGMENT = gql`
-    fragment ProductOptionGroup on ProductOptionGroup {
+export const PRODUCT_OPTION_GROUP_WITH_OPTIONS_FRAGMENT = gql`
+    fragment ProductOptionGroupWithOptions on ProductOptionGroup {
         id
         createdAt
         updatedAt
@@ -202,19 +221,19 @@ export const UPDATE_PRODUCT_VARIANTS = gql`
 export const CREATE_PRODUCT_OPTION_GROUP = gql`
     mutation CreateProductOptionGroup($input: CreateProductOptionGroupInput!) {
         createProductOptionGroup(input: $input) {
-            ...ProductOptionGroup
+            ...ProductOptionGroupWithOptions
         }
     }
-    ${PRODUCT_OPTION_GROUP_FRAGMENT}
+    ${PRODUCT_OPTION_GROUP_WITH_OPTIONS_FRAGMENT}
 `;
 
 export const GET_PRODUCT_OPTION_GROUP = gql`
     query GetProductOptionGroup($id: ID!) {
         productOptionGroup(id: $id) {
-            ...ProductOptionGroup
+            ...ProductOptionGroupWithOptions
         }
     }
-    ${PRODUCT_OPTION_GROUP_FRAGMENT}
+    ${PRODUCT_OPTION_GROUP_WITH_OPTIONS_FRAGMENT}
 `;
 
 export const ADD_OPTION_TO_GROUP = gql`
@@ -426,13 +445,10 @@ export const SEARCH_PRODUCTS = gql`
 export const UPDATE_PRODUCT_OPTION = gql`
     mutation UpdateProductOption($input: UpdateProductOptionInput!) {
         updateProductOption(input: $input) {
-            id
-            createdAt
-            updatedAt
-            code
-            name
+            ...ProductOption
         }
     }
+    ${PRODUCT_OPTION_FRAGMENT}
 `;
 
 export const DELETE_PRODUCT_VARIANT = gql`
@@ -456,11 +472,7 @@ export const GET_PRODUCT_VARIANT_OPTIONS = gql`
                 name
                 code
                 options {
-                    id
-                    createdAt
-                    updatedAt
-                    name
-                    code
+                    ...ProductOption
                 }
             }
             variants {
@@ -484,6 +496,7 @@ export const GET_PRODUCT_VARIANT_OPTIONS = gql`
             }
         }
     }
+    ${PRODUCT_OPTION_FRAGMENT}
 `;
 
 export const ASSIGN_PRODUCTS_TO_CHANNEL = gql`

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

@@ -240,7 +240,7 @@ export class ProductDataService {
         return this.baseDataService.mutate<UpdateProductOption.Mutation, UpdateProductOption.Variables>(
             UPDATE_PRODUCT_OPTION,
             {
-                input: pick(input, ['id', 'code', 'translations']),
+                input: pick(input, ['id', 'code', 'translations', 'customFields']),
             },
         );
     }

+ 9 - 7
packages/admin-ui/src/lib/core/src/providers/job-queue/job-queue.service.ts

@@ -2,7 +2,7 @@ import { Injectable, OnDestroy } from '@angular/core';
 import { EMPTY, interval, Observable, of, Subject, Subscription, timer } from 'rxjs';
 import { debounceTime, map, mapTo, scan, shareReplay, switchMap } from 'rxjs/operators';
 
-import { JobInfoFragment, JobState } from '../../common/generated-types';
+import { JobInfoFragment, JobState, Permission } from '../../common/generated-types';
 import { DataService } from '../../data/providers/data.service';
 
 @Injectable({
@@ -61,12 +61,14 @@ export class JobQueueService implements OnDestroy {
     checkForJobs(delay: number = 1000) {
         timer(delay)
             .pipe(
-                switchMap(() =>
-                    this.dataService.client.userStatus().mapSingle((data) => data.userStatus.isLoggedIn),
-                ),
-                switchMap((isLoggedIn) =>
-                    isLoggedIn ? this.dataService.settings.getRunningJobs().single$ : EMPTY,
-                ),
+                switchMap(() => this.dataService.client.userStatus().mapSingle((data) => data.userStatus)),
+                switchMap((userStatus) => {
+                    if (userStatus.permissions.includes(Permission.ReadSettings) && userStatus.isLoggedIn) {
+                        return this.dataService.settings.getRunningJobs().single$;
+                    } else {
+                        return EMPTY;
+                    }
+                }),
             )
             .subscribe((data) => data.jobs.items.forEach((job) => this.updateJob$.next(job)));
     }

+ 3 - 3
packages/admin-ui/src/lib/core/src/shared/components/channel-assignment-control/channel-assignment-control.component.ts

@@ -11,7 +11,7 @@ import { DataService } from '../../../data/providers/data.service';
     selector: 'vdr-channel-assignment-control',
     templateUrl: './channel-assignment-control.component.html',
     styleUrls: ['./channel-assignment-control.component.scss'],
-    changeDetection: ChangeDetectionStrategy.OnPush,
+    changeDetection: ChangeDetectionStrategy.Default,
     providers: [
         {
             provide: NG_VALUE_ACCESSOR,
@@ -37,7 +37,7 @@ export class ChannelAssignmentControlComponent implements OnInit, ControlValueAc
             .userStatus()
             .single$.pipe(
                 map(({ userStatus }) =>
-                    userStatus.channels.filter(c =>
+                    userStatus.channels.filter((c) =>
                         this.includeDefaultChannel ? true : c.code !== DEFAULT_CHANNEL_CODE,
                     ),
                 ),
@@ -70,7 +70,7 @@ export class ChannelAssignmentControlComponent implements OnInit, ControlValueAc
 
     valueChanged(value: CurrentUserChannel[] | CurrentUserChannel | undefined) {
         if (Array.isArray(value)) {
-            this.onChange(value.map(c => c.id));
+            this.onChange(value.map((c) => c.id));
         } else {
             this.onChange([value ? value.id : undefined]);
         }

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

@@ -234,10 +234,14 @@
                 <div class="card-block">
                     <div class="card-text">
                         <vdr-customer-label [customer]="order.customer"></vdr-customer-label>
-                        <h6 *ngIf="getShippingAddressLines(order.shippingAddress).length">
+                        <h6 *ngIf="getOrderAddressLines(order.shippingAddress).length">
                             {{ 'order.shipping-address' | translate }}
                         </h6>
                         <vdr-formatted-address [address]="order.shippingAddress"></vdr-formatted-address>
+                        <h6 *ngIf="getOrderAddressLines(order.billingAddress).length">
+                            {{ 'order.billing-address' | translate }}
+                        </h6>
+                        <vdr-formatted-address [address]="order.billingAddress"></vdr-formatted-address>
                     </div>
                 </div>
             </div>

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

@@ -74,7 +74,7 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
                             createdAt: SortOrder.DESC,
                         },
                     })
-                    .mapStream(data => data.order?.history.items);
+                    .mapStream((data) => data.order?.history.items);
             }),
         );
     }
@@ -88,7 +88,7 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
     }
 
     getLinePromotions(line: OrderDetail.Lines) {
-        return line.adjustments.filter(a => a.type === AdjustmentType.PROMOTION);
+        return line.adjustments.filter((a) => a.type === AdjustmentType.PROMOTION);
     }
 
     getPromotionLink(promotion: OrderDetail.Adjustments): any[] {
@@ -101,19 +101,19 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
         promotionAdjustment: OrderDetail.Adjustments,
     ): string | undefined {
         const id = promotionAdjustment.adjustmentSource.split(':')[1];
-        const promotion = order.promotions.find(p => p.id === id);
+        const promotion = order.promotions.find((p) => p.id === id);
         if (promotion) {
             return promotion.couponCode || undefined;
         }
     }
 
-    getShippingAddressLines(shippingAddress?: { [key: string]: string }): string[] {
-        if (!shippingAddress) {
+    getOrderAddressLines(orderAddress?: { [key: string]: string }): string[] {
+        if (!orderAddress) {
             return [];
         }
-        return Object.values(shippingAddress)
-            .filter(val => val !== 'OrderAddress')
-            .filter(line => !!line);
+        return Object.values(orderAddress)
+            .filter((val) => val !== 'OrderAddress')
+            .filter((line) => !!line);
     }
 
     settlePayment(payment: OrderDetail.Payments) {
@@ -134,7 +134,7 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
         this.entity$
             .pipe(
                 take(1),
-                switchMap(order => {
+                switchMap((order) => {
                     return this.modalService.fromComponent(FulfillOrderDialogComponent, {
                         size: 'xl',
                         locals: {
@@ -142,16 +142,16 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
                         },
                     });
                 }),
-                switchMap(input => {
+                switchMap((input) => {
                     if (input) {
                         return this.dataService.order.createFullfillment(input);
                     } else {
                         return of(undefined);
                     }
                 }),
-                switchMap(result => this.refetchOrder(result)),
+                switchMap((result) => this.refetchOrder(result)),
             )
-            .subscribe(result => {
+            .subscribe((result) => {
                 if (result) {
                     this.notificationService.success(_('order.create-fulfillment-success'));
                 }
@@ -175,7 +175,7 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
                 },
             })
             .pipe(
-                switchMap(transactionId => {
+                switchMap((transactionId) => {
                     if (transactionId) {
                         return this.dataService.order.settleRefund(
                             {
@@ -190,7 +190,7 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
                 }),
                 // switchMap(result => this.refetchOrder(result)),
             )
-            .subscribe(result => {
+            .subscribe((result) => {
                 if (result) {
                     this.notificationService.success(_('order.settle-refund-success'));
                 }
@@ -205,8 +205,8 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
                 note,
                 isPublic,
             })
-            .pipe(switchMap(result => this.refetchOrder(result)))
-            .subscribe(result => {
+            .pipe(switchMap((result) => this.refetchOrder(result)))
+            .subscribe((result) => {
                 this.notificationService.success(_('common.notify-create-success'), {
                     entity: 'Note',
                 });
@@ -224,7 +224,7 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
                 },
             })
             .pipe(
-                switchMap(result => {
+                switchMap((result) => {
                     if (result) {
                         return this.dataService.order.updateOrderNote({
                             noteId: entry.id,
@@ -236,7 +236,7 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
                     }
                 }),
             )
-            .subscribe(result => {
+            .subscribe((result) => {
                 this.fetchHistory.next();
                 this.notificationService.success(_('common.notify-update-success'), {
                     entity: 'Note',
@@ -254,7 +254,7 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
                     { type: 'danger', label: _('common.delete'), returnValue: true },
                 ],
             })
-            .pipe(switchMap(res => (res ? this.dataService.order.deleteOrderNote(entry.id) : EMPTY)))
+            .pipe(switchMap((res) => (res ? this.dataService.order.deleteOrderNote(entry.id) : EMPTY)))
             .subscribe(() => {
                 this.fetchHistory.next();
                 this.notificationService.success(_('common.notify-delete-success'), {
@@ -272,16 +272,16 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
                 },
             })
             .pipe(
-                switchMap(input => {
+                switchMap((input) => {
                     if (input) {
                         return this.dataService.order.cancelOrder(input);
                     } else {
                         return of(undefined);
                     }
                 }),
-                switchMap(result => this.refetchOrder(result)),
+                switchMap((result) => this.refetchOrder(result)),
             )
-            .subscribe(result => {
+            .subscribe((result) => {
                 if (result) {
                     this.notificationService.success(_('order.cancelled-order-success'));
                 }
@@ -297,10 +297,10 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
                 },
             })
             .pipe(
-                switchMap(input => {
+                switchMap((input) => {
                     if (input) {
                         return this.dataService.order.refundOrder(omit(input, ['cancel'])).pipe(
-                            switchMap(result => {
+                            switchMap((result) => {
                                 if (input.cancel.length) {
                                     return this.dataService.order.cancelOrder({
                                         orderId: this.id,
@@ -316,9 +316,9 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
                         return of(undefined);
                     }
                 }),
-                switchMap(result => this.refetchOrder(result)),
+                switchMap((result) => this.refetchOrder(result)),
             )
-            .subscribe(result => {
+            .subscribe((result) => {
                 if (result) {
                     this.notificationService.success(_('order.refund-order-success'));
                 }

+ 16 - 5
packages/admin-ui/src/lib/settings/src/components/role-detail/role-detail.component.ts

@@ -2,11 +2,17 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnIni
 import { FormBuilder, FormGroup, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import { BaseDetailComponent } from '@vendure/admin-ui/core';
-import { CreateRoleInput, LanguageCode, Permission, Role, UpdateRoleInput } from '@vendure/admin-ui/core';
-import { NotificationService } from '@vendure/admin-ui/core';
-import { DataService } from '@vendure/admin-ui/core';
-import { ServerConfigService } from '@vendure/admin-ui/core';
+import {
+    BaseDetailComponent,
+    CreateRoleInput,
+    DataService,
+    LanguageCode,
+    NotificationService,
+    Permission,
+    Role,
+    ServerConfigService,
+    UpdateRoleInput,
+} from '@vendure/admin-ui/core';
 import { normalizeString } from '@vendure/common/lib/normalize-string';
 import { Observable } from 'rxjs';
 import { mergeMap, take } from 'rxjs/operators';
@@ -46,6 +52,7 @@ export class RoleDetailComponent extends BaseDetailComponent<Role> implements On
     ngOnInit() {
         this.init();
         this.role$ = this.entity$;
+        // setTimeout(() => this.changeDetector.markForCheck(), 2000);
     }
 
     ngOnDestroy(): void {
@@ -128,6 +135,10 @@ export class RoleDetailComponent extends BaseDetailComponent<Role> implements On
         for (const permission of Object.keys(this.permissions)) {
             this.permissions[permission] = role.permissions.includes(permission as Permission);
         }
+        // This was required to get the channel selector component to
+        // correctly display its contents. A while spent debugging the root
+        // cause did not yield a solution, therefore this next line.
+        this.changeDetector.detectChanges();
     }
 
     private getSelectedPermissions(): Permission[] {

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

@@ -516,6 +516,7 @@
   "order": {
     "add-note": "Notiz hinzufügen",
     "amount": "Betrag",
+    "billing-address": "",
     "cancel": "Abbrechen",
     "cancel-order": "Bestellung stornieren",
     "cancel-reason-customer-request": "Kundenanfrage",

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

@@ -516,6 +516,7 @@
   "order": {
     "add-note": "Add note",
     "amount": "Amount",
+    "billing-address": "Billing address",
     "cancel": "Cancel",
     "cancel-order": "Cancel Order",
     "cancel-reason-customer-request": "Customer request",

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

@@ -516,6 +516,7 @@
   "order": {
     "add-note": "",
     "amount": "Precio",
+    "billing-address": "",
     "cancel": "",
     "cancel-order": "",
     "cancel-reason-customer-request": "",

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

@@ -516,6 +516,7 @@
   "order": {
     "add-note": "Dodaj notatke",
     "amount": "Ilość",
+    "billing-address": "",
     "cancel": "Anuluj",
     "cancel-order": "Anuluj zamówienie",
     "cancel-reason-customer-request": "Prośba klienta",

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

@@ -516,6 +516,7 @@
   "order": {
     "add-note": "添加备注",
     "amount": "金额",
+    "billing-address": "",
     "cancel": "取消",
     "cancel-order": "取消订单",
     "cancel-reason-customer-request": "客户要求",

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

@@ -516,6 +516,7 @@
   "order": {
     "add-note": "新增備注",
     "amount": "金額",
+    "billing-address": "",
     "cancel": "取消",
     "cancel-order": "取消訂單",
     "cancel-reason-customer-request": "客户要求",

+ 9 - 0
packages/common/src/generated-shop-types.ts

@@ -1353,7 +1353,11 @@ export type Mutation = {
     /** Removes the given coupon code from the active Order */
     removeCouponCode?: Maybe<Order>;
     transitionOrderToState?: Maybe<Order>;
+    /** Sets the shipping address for this order */
     setOrderShippingAddress?: Maybe<Order>;
+    /** Sets the billing address for this order */
+    setOrderBillingAddress?: Maybe<Order>;
+    /** Sets the shipping method by id, which can be obtained with the `eligibleShippingMethods` query */
     setOrderShippingMethod?: Maybe<Order>;
     addPaymentToOrder?: Maybe<Order>;
     setCustomerForOrder?: Maybe<Order>;
@@ -1431,6 +1435,10 @@ export type MutationSetOrderShippingAddressArgs = {
     input: CreateAddressInput;
 };
 
+export type MutationSetOrderBillingAddressArgs = {
+    input: CreateAddressInput;
+};
+
 export type MutationSetOrderShippingMethodArgs = {
     shippingMethodId: Scalars['ID'];
 };
@@ -2012,6 +2020,7 @@ export type RegisterCustomerInput = {
     title?: Maybe<Scalars['String']>;
     firstName?: Maybe<Scalars['String']>;
     lastName?: Maybe<Scalars['String']>;
+    phoneNumber?: Maybe<Scalars['String']>;
     password?: Maybe<Scalars['String']>;
 };
 

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

@@ -4758,7 +4758,7 @@ export type GetCustomerListQuery = { __typename?: 'Query' } & {
             items: Array<
                 { __typename?: 'Customer' } & Pick<
                     Customer,
-                    'id' | 'title' | 'firstName' | 'lastName' | 'emailAddress'
+                    'id' | 'title' | 'firstName' | 'lastName' | 'emailAddress' | 'phoneNumber'
                 > & { user?: Maybe<{ __typename?: 'User' } & Pick<User, 'id' | 'verified'>> }
             >;
         };

+ 43 - 0
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -1353,7 +1353,11 @@ export type Mutation = {
     /** Removes the given coupon code from the active Order */
     removeCouponCode?: Maybe<Order>;
     transitionOrderToState?: Maybe<Order>;
+    /** Sets the shipping address for this order */
     setOrderShippingAddress?: Maybe<Order>;
+    /** Sets the billing address for this order */
+    setOrderBillingAddress?: Maybe<Order>;
+    /** Sets the shipping method by id, which can be obtained with the `eligibleShippingMethods` query */
     setOrderShippingMethod?: Maybe<Order>;
     addPaymentToOrder?: Maybe<Order>;
     setCustomerForOrder?: Maybe<Order>;
@@ -1431,6 +1435,10 @@ export type MutationSetOrderShippingAddressArgs = {
     input: CreateAddressInput;
 };
 
+export type MutationSetOrderBillingAddressArgs = {
+    input: CreateAddressInput;
+};
+
 export type MutationSetOrderShippingMethodArgs = {
     shippingMethodId: Scalars['ID'];
 };
@@ -2012,6 +2020,7 @@ export type RegisterCustomerInput = {
     title?: Maybe<Scalars['String']>;
     firstName?: Maybe<Scalars['String']>;
     lastName?: Maybe<Scalars['String']>;
+    phoneNumber?: Maybe<Scalars['String']>;
     password?: Maybe<Scalars['String']>;
 };
 
@@ -2616,6 +2625,31 @@ export type SetShippingAddressMutation = { __typename?: 'Mutation' } & {
     >;
 };
 
+export type SetBillingAddressMutationVariables = {
+    input: CreateAddressInput;
+};
+
+export type SetBillingAddressMutation = { __typename?: 'Mutation' } & {
+    setOrderBillingAddress?: Maybe<
+        { __typename?: 'Order' } & {
+            billingAddress?: Maybe<
+                { __typename?: 'OrderAddress' } & Pick<
+                    OrderAddress,
+                    | 'fullName'
+                    | 'company'
+                    | 'streetLine1'
+                    | 'streetLine2'
+                    | 'city'
+                    | 'province'
+                    | 'postalCode'
+                    | 'country'
+                    | 'phoneNumber'
+                >
+            >;
+        }
+    >;
+};
+
 export type AddPaymentToOrderMutationVariables = {
     input: PaymentInput;
 };
@@ -2914,6 +2948,15 @@ export namespace SetShippingAddress {
     >;
 }
 
+export namespace SetBillingAddress {
+    export type Variables = SetBillingAddressMutationVariables;
+    export type Mutation = SetBillingAddressMutation;
+    export type SetOrderBillingAddress = NonNullable<SetBillingAddressMutation['setOrderBillingAddress']>;
+    export type BillingAddress = NonNullable<
+        NonNullable<SetBillingAddressMutation['setOrderBillingAddress']>['billingAddress']
+    >;
+}
+
 export namespace AddPaymentToOrder {
     export type Variables = AddPaymentToOrderMutationVariables;
     export type Mutation = AddPaymentToOrderMutation;

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

@@ -124,6 +124,7 @@ export const GET_CUSTOMER_LIST = gql`
                 firstName
                 lastName
                 emailAddress
+                phoneNumber
                 user {
                     id
                     verified

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

@@ -324,6 +324,24 @@ export const SET_SHIPPING_ADDRESS = gql`
     }
 `;
 
+export const SET_BILLING_ADDRESS = gql`
+    mutation SetBillingAddress($input: CreateAddressInput!) {
+        setOrderBillingAddress(input: $input) {
+            billingAddress {
+                fullName
+                company
+                streetLine1
+                streetLine2
+                city
+                province
+                postalCode
+                country
+                phoneNumber
+            }
+        }
+    }
+`;
+
 export const ADD_PAYMENT = gql`
     mutation AddPaymentToOrder($input: PaymentInput!) {
         addPaymentToOrder(input: $input) {

+ 34 - 16
packages/core/e2e/shop-auth.e2e-spec.ts

@@ -18,13 +18,14 @@ import gql from 'graphql-tag';
 import path from 'path';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
-import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
+import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
 
 import {
     CreateAdministrator,
     CreateRole,
     GetCustomer,
     GetCustomerHistory,
+    GetCustomerList,
     HistoryEntryType,
     Permission,
 } from './graphql/generated-e2e-admin-types';
@@ -43,6 +44,7 @@ import {
     CREATE_ROLE,
     GET_CUSTOMER,
     GET_CUSTOMER_HISTORY,
+    GET_CUSTOMER_LIST,
 } from './graphql/shared-definitions';
 import {
     GET_ACTIVE_CUSTOMER,
@@ -68,16 +70,16 @@ let sendEmailFn: jest.Mock;
 class TestEmailPlugin implements OnModuleInit {
     constructor(private eventBus: EventBus) {}
     onModuleInit() {
-        this.eventBus.ofType(AccountRegistrationEvent).subscribe(event => {
+        this.eventBus.ofType(AccountRegistrationEvent).subscribe((event) => {
             sendEmailFn(event);
         });
-        this.eventBus.ofType(PasswordResetEvent).subscribe(event => {
+        this.eventBus.ofType(PasswordResetEvent).subscribe((event) => {
             sendEmailFn(event);
         });
-        this.eventBus.ofType(IdentifierChangeRequestEvent).subscribe(event => {
+        this.eventBus.ofType(IdentifierChangeRequestEvent).subscribe((event) => {
             sendEmailFn(event);
         });
-        this.eventBus.ofType(IdentifierChangeEvent).subscribe(event => {
+        this.eventBus.ofType(IdentifierChangeEvent).subscribe((event) => {
             sendEmailFn(event);
         });
     }
@@ -134,6 +136,7 @@ describe('Shop auth & accounts', () => {
             const input: RegisterCustomerInput = {
                 firstName: 'Sean',
                 lastName: 'Tester',
+                phoneNumber: '123456',
                 emailAddress,
             };
             const result = await shopClient.query<Register.Mutation, Register.Variables>(REGISTER_ACCOUNT, {
@@ -145,10 +148,27 @@ describe('Shop auth & accounts', () => {
             expect(result.registerCustomerAccount).toBe(true);
             expect(sendEmailFn).toHaveBeenCalled();
             expect(verificationToken).toBeDefined();
+
+            const { customers } = await adminClient.query<GetCustomerList.Query, GetCustomerList.Variables>(
+                GET_CUSTOMER_LIST,
+                {
+                    options: {
+                        filter: {
+                            emailAddress: {
+                                eq: emailAddress,
+                            },
+                        },
+                    },
+                },
+            );
+
+            expect(
+                pick(customers.items[0], ['firstName', 'lastName', 'emailAddress', 'phoneNumber']),
+            ).toEqual(input);
         });
 
         it('issues a new token if attempting to register a second time', async () => {
-            const sendEmail = new Promise<string>(resolve => {
+            const sendEmail = new Promise<string>((resolve) => {
                 sendEmailFn.mockImplementation((event: AccountRegistrationEvent) => {
                     resolve(event.user.getNativeAuthenticationMethod().verificationToken!);
                 });
@@ -172,7 +192,7 @@ describe('Shop auth & accounts', () => {
         });
 
         it('refreshCustomerVerification issues a new token', async () => {
-            const sendEmail = new Promise<string>(resolve => {
+            const sendEmail = new Promise<string>((resolve) => {
                 sendEmailFn.mockImplementation((event: AccountRegistrationEvent) => {
                     resolve(event.user.getNativeAuthenticationMethod().verificationToken!);
                 });
@@ -593,9 +613,7 @@ describe('Shop auth & accounts', () => {
 
         const role = roleResult.createRole;
 
-        const identifier = `${code}@${Math.random()
-            .toString(16)
-            .substr(2, 8)}`;
+        const identifier = `${code}@${Math.random().toString(16).substr(2, 8)}`;
         const password = `test`;
 
         const adminResult = await shopClient.query<
@@ -622,7 +640,7 @@ describe('Shop auth & accounts', () => {
      * A "sleep" function which allows the sendEmailFn time to get called.
      */
     function waitForSendEmailFn() {
-        return new Promise(resolve => setTimeout(resolve, 10));
+        return new Promise((resolve) => setTimeout(resolve, 10));
     }
 });
 
@@ -672,7 +690,7 @@ describe('Expiring tokens', () => {
             expect(sendEmailFn).toHaveBeenCalledTimes(1);
             expect(verificationToken).toBeDefined();
 
-            await new Promise(resolve => setTimeout(resolve, 3));
+            await new Promise((resolve) => setTimeout(resolve, 3));
 
             return shopClient.query(VERIFY_EMAIL, {
                 password: 'test',
@@ -703,7 +721,7 @@ describe('Expiring tokens', () => {
             expect(sendEmailFn).toHaveBeenCalledTimes(1);
             expect(passwordResetToken).toBeDefined();
 
-            await new Promise(resolve => setTimeout(resolve, 3));
+            await new Promise((resolve) => setTimeout(resolve, 3));
 
             return shopClient.query<ResetPassword.Mutation, ResetPassword.Variables>(RESET_PASSWORD, {
                 password: 'test',
@@ -838,7 +856,7 @@ describe('Updating email address without email verification', () => {
 });
 
 function getVerificationTokenPromise(): Promise<string> {
-    return new Promise<any>(resolve => {
+    return new Promise<any>((resolve) => {
         sendEmailFn.mockImplementation((event: AccountRegistrationEvent) => {
             resolve(event.user.getNativeAuthenticationMethod().verificationToken);
         });
@@ -846,7 +864,7 @@ function getVerificationTokenPromise(): Promise<string> {
 }
 
 function getPasswordResetTokenPromise(): Promise<string> {
-    return new Promise<any>(resolve => {
+    return new Promise<any>((resolve) => {
         sendEmailFn.mockImplementation((event: PasswordResetEvent) => {
             resolve(event.user.getNativeAuthenticationMethod().passwordResetToken);
         });
@@ -857,7 +875,7 @@ function getEmailUpdateTokenPromise(): Promise<{
     identifierChangeToken: string | null;
     pendingIdentifier: string | null;
 }> {
-    return new Promise(resolve => {
+    return new Promise((resolve) => {
         sendEmailFn.mockImplementation((event: IdentifierChangeRequestEvent) => {
             resolve(
                 pick(event.user.getNativeAuthenticationMethod(), [

+ 34 - 0
packages/core/e2e/shop-order.e2e-spec.ts

@@ -32,6 +32,7 @@ import {
     GetOrderByCode,
     GetShippingMethods,
     RemoveItemFromOrder,
+    SetBillingAddress,
     SetCustomerForOrder,
     SetShippingAddress,
     SetShippingMethod,
@@ -57,6 +58,7 @@ import {
     GET_NEXT_STATES,
     GET_ORDER_BY_CODE,
     REMOVE_ITEM_FROM_ORDER,
+    SET_BILLING_ADDRESS,
     SET_CUSTOMER,
     SET_SHIPPING_ADDRESS,
     SET_SHIPPING_METHOD,
@@ -382,6 +384,38 @@ describe('Shop orders', () => {
             });
         });
 
+        it('setOrderBillingAddress sets billing address', async () => {
+            const address: CreateAddressInput = {
+                fullName: 'name',
+                company: 'company',
+                streetLine1: '12 the street',
+                streetLine2: null,
+                city: 'foo',
+                province: 'bar',
+                postalCode: '123456',
+                countryCode: 'US',
+                phoneNumber: '4444444',
+            };
+            const { setOrderBillingAddress } = await shopClient.query<
+                SetBillingAddress.Mutation,
+                SetBillingAddress.Variables
+            >(SET_BILLING_ADDRESS, {
+                input: address,
+            });
+
+            expect(setOrderBillingAddress!.billingAddress).toEqual({
+                fullName: 'name',
+                company: 'company',
+                streetLine1: '12 the street',
+                streetLine2: null,
+                city: 'foo',
+                province: 'bar',
+                postalCode: '123456',
+                country: 'United States of America',
+                phoneNumber: '4444444',
+            });
+        });
+
         it('customer default Addresses are not updated before payment', async () => {
             const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
             const { customer } = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(

+ 15 - 3
packages/core/src/api/resolvers/entity/order-line-entity.resolver.ts

@@ -2,14 +2,14 @@ import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
 
 import { Translated } from '../../../common/types/locale-types';
 import { assertFound } from '../../../common/utils';
-import { OrderLine, ProductVariant } from '../../../entity';
-import { ProductVariantService } from '../../../service';
+import { Asset, OrderLine, ProductVariant } from '../../../entity';
+import { AssetService, ProductVariantService } from '../../../service';
 import { RequestContext } from '../../common/request-context';
 import { Ctx } from '../../decorators/request-context.decorator';
 
 @Resolver('OrderLine')
 export class OrderLineEntityResolver {
-    constructor(private productVariantService: ProductVariantService) {}
+    constructor(private productVariantService: ProductVariantService, private assetService: AssetService) {}
 
     @ResolveField()
     async productVariant(
@@ -18,4 +18,16 @@ export class OrderLineEntityResolver {
     ): Promise<Translated<ProductVariant>> {
         return assertFound(this.productVariantService.findOne(ctx, orderLine.productVariant.id));
     }
+
+    @ResolveField()
+    async featuredAsset(
+        @Ctx() ctx: RequestContext,
+        @Parent() orderLine: OrderLine,
+    ): Promise<Asset | undefined> {
+        if (orderLine.featuredAsset) {
+            return orderLine.featuredAsset;
+        } else {
+            return this.assetService.getFeaturedAsset(orderLine);
+        }
+    }
 }

+ 18 - 1
packages/core/src/api/resolvers/shop/shop-order.resolver.ts

@@ -6,6 +6,7 @@ import {
     MutationApplyCouponCodeArgs,
     MutationRemoveOrderLineArgs,
     MutationSetCustomerForOrderArgs,
+    MutationSetOrderBillingAddressArgs,
     MutationSetOrderShippingAddressArgs,
     MutationSetOrderShippingMethodArgs,
     MutationTransitionOrderToStateArgs,
@@ -55,7 +56,7 @@ export class ShopOrderResolver {
                 skip: 0,
                 take: 99999,
             })
-            .then(data => data.items);
+            .then((data) => data.items);
     }
 
     @Query()
@@ -136,6 +137,22 @@ export class ShopOrderResolver {
         }
     }
 
+    @Mutation()
+    @Allow(Permission.Owner)
+    async setOrderBillingAddress(
+        @Ctx() ctx: RequestContext,
+        @Args() args: MutationSetOrderBillingAddressArgs,
+    ): Promise<Order | undefined> {
+        if (ctx.authorizedAsOwnerOnly) {
+            const sessionOrder = await this.getOrderFromContext(ctx);
+            if (sessionOrder) {
+                return this.orderService.setBillingAddress(ctx, sessionOrder.id, args.input);
+            } else {
+                return;
+            }
+        }
+    }
+
     @Query()
     @Allow(Permission.Owner)
     async eligibleShippingMethods(@Ctx() ctx: RequestContext): Promise<ShippingMethodQuote[]> {

+ 5 - 0
packages/core/src/api/schema/shop-api/shop.api.graphql

@@ -27,7 +27,11 @@ type Mutation {
     "Removes the given coupon code from the active Order"
     removeCouponCode(couponCode: String!): Order
     transitionOrderToState(state: String!): Order
+    "Sets the shipping address for this order"
     setOrderShippingAddress(input: CreateAddressInput!): Order
+    "Sets the billing address for this order"
+    setOrderBillingAddress(input: CreateAddressInput!): Order
+    "Sets the shipping method by id, which can be obtained with the `eligibleShippingMethods` query"
     setOrderShippingMethod(shippingMethodId: ID!): Order
     addPaymentToOrder(input: PaymentInput!): Order
     setCustomerForOrder(input: CreateCustomerInput!): Order
@@ -76,6 +80,7 @@ input RegisterCustomerInput {
     title: String
     firstName: String
     lastName: String
+    phoneNumber: String
     password: String
 }
 

+ 6 - 4
packages/core/src/service/services/asset.service.ts

@@ -68,7 +68,9 @@ export class AssetService {
             }));
     }
 
-    async getFeaturedAsset<T extends EntityWithAssets>(entity: T): Promise<Asset | undefined> {
+    async getFeaturedAsset<T extends Omit<EntityWithAssets, 'assets'>>(
+        entity: T,
+    ): Promise<Asset | undefined> {
         const entityType = Object.getPrototypeOf(entity).constructor;
         const entityWithFeaturedAsset = await this.connection
             .getRepository<EntityWithAssets>(entityType)
@@ -89,7 +91,7 @@ export class AssetService {
                 });
             assets = (entityWithAssets && entityWithAssets.assets) || [];
         }
-        return assets.sort((a, b) => a.position - b.position).map(a => a.asset);
+        return assets.sort((a, b) => a.position - b.position).map((a) => a.asset);
     }
 
     async updateFeaturedAsset<T extends EntityWithAssets>(entity: T, input: EntityAssetInput): Promise<T> {
@@ -119,7 +121,7 @@ export class AssetService {
         if (assetIds && assetIds.length) {
             const assets = await this.connection.getRepository(Asset).findByIds(assetIds);
             const sortedAssets = assetIds
-                .map(id => assets.find(a => idsAreEqual(a.id, id)))
+                .map((id) => assets.find((a) => idsAreEqual(a.id, id)))
                 .filter(notNullOrUndefined);
             await this.removeExistingOrderableAssets(entity);
             entity.assets = await this.createOrderableAssets(entity, sortedAssets);
@@ -295,7 +297,7 @@ export class AssetService {
     private getOrderableAssetType(entity: EntityWithAssets): Type<OrderableAsset> {
         const assetRelation = this.connection
             .getRepository(entity.constructor)
-            .metadata.relations.find(r => r.propertyName === 'assets');
+            .metadata.relations.find((r) => r.propertyName === 'assets');
         if (!assetRelation || typeof assetRelation.type === 'string') {
             throw new InternalServerError('error.could-not-find-matching-orderable-asset');
         }

+ 6 - 6
packages/core/src/service/services/customer.service.ts

@@ -87,8 +87,8 @@ export class CustomerService {
             .leftJoinAndSelect('country.translations', 'countryTranslation')
             .where('address.customer = :id', { id: customerId })
             .getMany()
-            .then(addresses => {
-                addresses.forEach(address => {
+            .then((addresses) => {
+                addresses.forEach((address) => {
                     address.country = translateDeep(address.country, ctx.languageCode);
                 });
                 return addresses;
@@ -172,7 +172,7 @@ export class CustomerService {
         }
         let user = await this.userService.getUserByEmailAddress(input.emailAddress);
         const hasNativeAuthMethod = !!user?.authenticationMethods.find(
-            m => m instanceof NativeAuthenticationMethod,
+            (m) => m instanceof NativeAuthenticationMethod,
         );
         if (user && user.verified) {
             if (hasNativeAuthMethod) {
@@ -543,8 +543,8 @@ export class CustomerService {
             .findOne(addressId, { relations: ['customer', 'customer.addresses'] });
         if (result) {
             const customerAddressIds = result.customer.addresses
-                .map(a => a.id)
-                .filter(id => !idsAreEqual(id, addressId)) as string[];
+                .map((a) => a.id)
+                .filter((id) => !idsAreEqual(id, addressId)) as string[];
 
             if (customerAddressIds.length) {
                 if (input.defaultBillingAddress === true) {
@@ -577,7 +577,7 @@ export class CustomerService {
             const customerAddresses = result.customer.addresses;
             if (1 < customerAddresses.length) {
                 const otherAddresses = customerAddresses
-                    .filter(address => !idsAreEqual(address.id, addressToDelete.id))
+                    .filter((address) => !idsAreEqual(address.id, addressToDelete.id))
                     .sort((a, b) => (a.id < b.id ? -1 : 1));
                 if (addressToDelete.defaultShippingAddress) {
                     otherAddresses[0].defaultShippingAddress = true;

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

@@ -144,7 +144,13 @@ export class OrderService {
     ): Promise<PaginatedList<Order>> {
         return this.listQueryBuilder
             .build(Order, options, {
-                relations: ['lines', 'lines.productVariant', 'lines.productVariant.options', 'customer'],
+                relations: [
+                    'lines',
+                    'lines.items',
+                    'lines.productVariant',
+                    'lines.productVariant.options',
+                    'customer',
+                ],
             })
             .andWhere('order.customer.id = :customerId', { customerId })
             .getManyAndCount()
@@ -365,6 +371,13 @@ export class OrderService {
         return this.connection.getRepository(Order).save(order);
     }
 
+    async setBillingAddress(ctx: RequestContext, orderId: ID, input: CreateAddressInput): Promise<Order> {
+        const order = await this.getOrderOrThrow(ctx, orderId);
+        const country = await this.countryService.findOneByCode(ctx, input.countryCode);
+        order.billingAddress = { ...input, countryCode: input.countryCode, country: country.name };
+        return this.connection.getRepository(Order).save(order);
+    }
+
     async getEligibleShippingMethods(ctx: RequestContext, orderId: ID): Promise<ShippingMethodQuote[]> {
         const order = await this.getOrderOrThrow(ctx, orderId);
         const eligibleMethods = await this.shippingCalculator.getEligibleShippingMethods(ctx, order);

+ 2 - 6
packages/dev-server/dev-config.ts

@@ -7,6 +7,7 @@ import {
     DefaultLogger,
     DefaultSearchPlugin,
     examplePaymentHandler,
+    LanguageCode,
     LogLevel,
     VendureConfig,
 } from '@vendure/core';
@@ -51,12 +52,7 @@ export const devConfig: VendureConfig = {
     paymentOptions: {
         paymentMethodHandlers: [examplePaymentHandler],
     },
-    customFields: {
-        /*Product: [
-            { name: 'rating', type: 'float', readonly: true },
-            { name: 'markup', type: 'float', internal: true },
-        ],*/
-    },
+    customFields: {},
     logger: new DefaultLogger({ level: LogLevel.Info }),
     importExportOptions: {
         importAssetsDir: path.join(__dirname, 'import-assets'),