Browse Source

feat(admin-ui): Customer detail includes editable addresses

Relates to #52
Michael Bromley 7 years ago
parent
commit
21dad821e3

+ 102 - 0
admin-ui/src/app/customer/components/address-card/address-card.component.html

@@ -0,0 +1,102 @@
+<div class="card" *ngIf="addressForm.value as address">
+    <div class="card-header">
+        <div class="address-title">
+            <span class="street-line" *ngIf="address.streetLine1">{{ address.streetLine1 }},</span>
+            {{ getCountryName(address.countryCode) }}
+        </div>
+        <div class="default-controls">
+            <vdr-chip class="is-default p8" *ngIf="isDefaultShipping">
+                <clr-icon shape="truck"></clr-icon>
+                {{ 'customer.default-shipping-address' | translate }}
+            </vdr-chip>
+            <vdr-chip class="is-default p8" *ngIf="isDefaultBilling">
+                <clr-icon shape="credit-card"></clr-icon>
+                {{ 'customer.default-billing-address' | translate }}
+            </vdr-chip>
+        </div>
+    </div>
+    <div class="card-block">
+        <div class="card-text">
+            <ul class="address-lines" *ngIf="!editing">
+                <li *ngIf="address.fullName">{{ address.fullName }}</li>
+                <li *ngIf="address.streetLine1">{{ address.streetLine1 }}</li>
+                <li *ngIf="address.streetLine2">{{ address.streetLine2 }}</li>
+                <li *ngIf="address.city">{{ address.city }}</li>
+                <li *ngIf="address.province">{{ address.province }}</li>
+                <li *ngIf="address.postalCode">{{ address.postalCode }}</li>
+                <li *ngIf="address.countryCode">{{ getCountryName(address.countryCode) }}</li>
+                <li *ngIf="address.phoneNumber">{{ address.phoneNumber }}</li>
+            </ul>
+            <form [formGroup]="addressForm" *ngIf="editing">
+                <clr-input-container>
+                    <label>{{ 'customer.full-name' | translate }}</label>
+                    <input formControlName="fullName" type="text" clrInput />
+                </clr-input-container>
+                <clr-input-container>
+                    <label>{{ 'customer.street-line-1' | translate }}</label>
+                    <input formControlName="streetLine1" type="text" clrInput />
+                </clr-input-container>
+                <clr-input-container>
+                    <label>{{ 'customer.street-line-2' | translate }}</label>
+                    <input formControlName="streetLine2" type="text" clrInput />
+                </clr-input-container>
+                <clr-input-container>
+                    <label>{{ 'customer.city' | translate }}</label>
+                    <input formControlName="city" type="text" clrInput />
+                </clr-input-container>
+                <clr-input-container>
+                    <label>{{ 'customer.province' | translate }}</label>
+                    <input formControlName="province" type="text" clrInput />
+                </clr-input-container>
+                <clr-input-container>
+                    <label>{{ 'customer.postal-code' | translate }}</label>
+                    <input formControlName="postalCode" type="text" clrInput />
+                </clr-input-container>
+                <clr-input-container>
+                    <label>{{ 'customer.country' | translate }}</label>
+                    <select name="countryCode" formControlName="countryCode" clrInput clrSelect>
+                        <option *ngFor="let country of availableCountries" [value]="country.code">
+                            {{ country.name }}
+                        </option>
+                    </select>
+                </clr-input-container>
+                <clr-input-container>
+                    <label>{{ 'customer.phone-number' | translate }}</label>
+                    <input formControlName="phoneNumber" type="text" clrInput />
+                </clr-input-container>
+            </form>
+        </div>
+    </div>
+    <div class="card-footer">
+        <button class="btn btn-sm btn-link" *ngIf="editing" (click)="editing = false">
+            {{ 'common.done' | translate }}
+        </button>
+        <button class="btn btn-sm btn-link" *ngIf="!editing" (click)="editing = true">
+            {{ 'common.edit' | translate }}
+        </button>
+        <clr-dropdown>
+            <button type="button" class="btn btn-sm btn-link" clrDropdownTrigger>
+                {{ 'common.more' | translate }}
+                <clr-icon shape="caret down"></clr-icon>
+            </button>
+            <clr-dropdown-menu *clrIfOpen>
+                <button
+                    clrDropdownItem
+                    class="button"
+                    [disabled]="isDefaultShipping || editing"
+                    (click)="setAsDefaultShippingAddress()"
+                >
+                    {{ 'customer.set-as-default-shipping-address' | translate }}
+                </button>
+                <button
+                    clrDropdownItem
+                    class="button"
+                    [disabled]="isDefaultBilling || editing"
+                    (click)="setAsDefaultBillingAddress()"
+                >
+                    {{ 'customer.set-as-default-billing-address' | translate }}
+                </button>
+            </clr-dropdown-menu>
+        </clr-dropdown>
+    </div>
+</div>

+ 18 - 0
admin-ui/src/app/customer/components/address-card/address-card.component.scss

@@ -0,0 +1,18 @@
+@import "variables";
+
+.address-lines {
+    list-style-type: none;
+}
+
+clr-input-container {
+    margin-bottom: 12px;
+}
+
+.defaul-controls {
+    display: flex;
+}
+
+.is-default {
+    margin: 0;
+    color: $color-success;
+}

+ 44 - 0
admin-ui/src/app/customer/components/address-card/address-card.component.ts

@@ -0,0 +1,44 @@
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { FormControl, FormGroup } from '@angular/forms';
+import { GetAvailableCountries } from 'shared/generated-types';
+
+@Component({
+    selector: 'vdr-address-card',
+    templateUrl: './address-card.component.html',
+    styleUrls: ['./address-card.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class AddressCardComponent implements OnInit {
+    editing = false;
+    @Input() addressForm: FormGroup;
+    @Input() availableCountries: GetAvailableCountries.AvailableCountries[] = [];
+    @Input() isDefaultBilling: string;
+    @Input() isDefaultShipping: string;
+    @Output() setAsDefaultShipping = new EventEmitter<string>();
+    @Output() setAsDefaultBilling = new EventEmitter<string>();
+
+    ngOnInit(): void {
+        const streetLine1 = this.addressForm.get('streetLine1') as FormControl;
+        if (!streetLine1.value) {
+            this.editing = true;
+        }
+    }
+
+    getCountryName(countryCode: string) {
+        if (!this.availableCountries) {
+            return '';
+        }
+        const match = this.availableCountries.find(c => c.code === countryCode);
+        return match ? match.name : '';
+    }
+
+    setAsDefaultBillingAddress() {
+        this.setAsDefaultBilling.emit(this.addressForm.value.id);
+        this.addressForm.markAsDirty();
+    }
+
+    setAsDefaultShippingAddress() {
+        this.setAsDefaultShipping.emit(this.addressForm.value.id);
+        this.addressForm.markAsDirty();
+    }
+}

+ 28 - 4
admin-ui/src/app/customer/components/customer-detail/customer-detail.component.html

@@ -1,12 +1,14 @@
 <vdr-action-bar>
-    <vdr-ab-left></vdr-ab-left>
+    <vdr-ab-left>
+        <vdr-customer-status-label [customer]="entity$ | async"></vdr-customer-status-label>
+    </vdr-ab-left>
 
     <vdr-ab-right>
         <button
             class="btn btn-primary"
             *ngIf="(isNew$ | async); else: updateButton"
             (click)="create()"
-            [disabled]="detailForm.invalid || detailForm.pristine"
+            [disabled]="!(addressDefaultsUpdated || (detailForm.valid && detailForm.dirty))"
         >
             {{ 'common.create' | translate }}
         </button>
@@ -14,7 +16,7 @@
             <button
                 class="btn btn-primary"
                 (click)="save()"
-                [disabled]="detailForm.invalid || detailForm.pristine"
+                [disabled]="!(addressDefaultsUpdated || (detailForm.valid && detailForm.dirty))"
             >
                 {{ 'common.update' | translate }}
             </button>
@@ -22,7 +24,7 @@
     </vdr-ab-right>
 </vdr-action-bar>
 
-<form class="form" [formGroup]="detailForm">
+<form class="form" [formGroup]="detailForm.get('customer')">
     <section class="form-block">
         <vdr-form-field
             [label]="'customer.title' | translate"
@@ -68,3 +70,25 @@
         </section>
     </section>
 </form>
+
+<div class="clr-row">
+    <div class="clr-col-md-4">
+        <h3>{{ 'customer.addresses' | translate }}</h3>
+        <vdr-address-card
+            *ngFor="let addressForm of getAddressFormControls()"
+            [availableCountries]="availableCountries$ | async"
+            [isDefaultBilling]="defaultBillingAddressId === addressForm.value.id"
+            [isDefaultShipping]="defaultShippingAddressId === addressForm.value.id"
+            [addressForm]="addressForm"
+            (setAsDefaultBilling)="setDefaultBillingAddressId($event)"
+            (setAsDefaultShipping)="setDefaultShippingAddressId($event)"
+        ></vdr-address-card>
+        <button class="btn btn-secondary" (click)="addAddress()">
+            <clr-icon shape="plus"></clr-icon>
+            {{ 'customer.create-new-address' | translate }}
+        </button>
+    </div>
+    <div class="clr-col-md-8">
+        <h3>{{ 'customer.orders' | translate }}</h3>
+    </div>
+</div>

+ 138 - 31
admin-ui/src/app/customer/components/customer-detail/customer-detail.component.ts

@@ -1,12 +1,14 @@
 import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
-import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
-import { map, mergeMap, take } from 'rxjs/operators';
+import { forkJoin, Observable } from 'rxjs';
+import { mergeMap, publishBehavior, publishReplay, shareReplay, take } from 'rxjs/operators';
 import {
-    CreateAdministratorInput,
+    CreateAddressInput,
     CreateCustomerInput,
     Customer,
-    UpdateAdministratorInput,
+    GetAvailableCountries,
+    UpdateAddressInput,
     UpdateCustomerInput,
 } from 'shared/generated-types';
 import { CustomFieldConfig } from 'shared/shared-types';
@@ -27,6 +29,10 @@ export class CustomerDetailComponent extends BaseDetailComponent<Customer.Fragme
     implements OnInit, OnDestroy {
     detailForm: FormGroup;
     customFields: CustomFieldConfig[];
+    availableCountries$: Observable<GetAvailableCountries.AvailableCountries[]>;
+    defaultShippingAddressId: string;
+    defaultBillingAddressId: string;
+    addressDefaultsUpdated = false;
 
     constructor(
         route: ActivatedRoute,
@@ -41,19 +47,26 @@ export class CustomerDetailComponent extends BaseDetailComponent<Customer.Fragme
 
         this.customFields = this.getCustomFieldConfig('Customer');
         this.detailForm = this.formBuilder.group({
-            title: '',
-            firstName: ['', Validators.required],
-            lastName: ['', Validators.required],
-            phoneNumber: '',
-            emailAddress: '',
-            password: '',
-            customFields: this.formBuilder.group(
-                this.customFields.reduce((hash, field) => ({ ...hash, [field.name]: '' }), {}),
-            ),
+            customer: this.formBuilder.group({
+                title: '',
+                firstName: ['', Validators.required],
+                lastName: ['', Validators.required],
+                phoneNumber: '',
+                emailAddress: '',
+                password: '',
+                customFields: this.formBuilder.group(
+                    this.customFields.reduce((hash, field) => ({ ...hash, [field.name]: '' }), {}),
+                ),
+            }),
+            addresses: new FormArray([]),
         });
     }
 
     ngOnInit() {
+        this.availableCountries$ = this.dataService.settings
+            .getAvailableCountries()
+            .mapSingle(result => result.availableCountries)
+            .pipe(shareReplay(1));
         this.init();
     }
 
@@ -62,11 +75,48 @@ export class CustomerDetailComponent extends BaseDetailComponent<Customer.Fragme
     }
 
     customFieldIsSet(name: string): boolean {
-        return !!this.detailForm.get(['customFields', name]);
+        return !!this.detailForm.get(['customer', 'customFields', name]);
+    }
+
+    getAddressFormControls(): FormControl[] {
+        const formArray = this.detailForm.get(['addresses']) as FormArray;
+        return formArray.controls as FormControl[];
+    }
+
+    setDefaultBillingAddressId(id: string) {
+        this.defaultBillingAddressId = id;
+        this.addressDefaultsUpdated = true;
+    }
+
+    setDefaultShippingAddressId(id: string) {
+        this.defaultShippingAddressId = id;
+        this.addressDefaultsUpdated = true;
+    }
+
+    addAddress() {
+        const addressFormArray = this.detailForm.get('addresses') as FormArray;
+        const newAddress = this.formBuilder.group({
+            fullName: '',
+            company: '',
+            streetLine1: ['', Validators.required],
+            streetLine2: '',
+            city: '',
+            province: '',
+            postalCode: '',
+            countryCode: ['', Validators.required],
+            phoneNumber: '',
+            defaultShippingAddress: false,
+            defaultBillingAddress: false,
+        });
+        addressFormArray.push(newAddress);
     }
 
     create() {
-        const formValue = this.detailForm.value;
+        const customerForm = this.detailForm.get('customer');
+        if (!customerForm) {
+            return;
+        }
+        const formValue = customerForm.value;
         const customer: CreateCustomerInput = {
             title: formValue.title,
             emailAddress: formValue.emailAddress,
@@ -79,6 +129,7 @@ export class CustomerDetailComponent extends BaseDetailComponent<Customer.Fragme
                     entity: 'Customer',
                 });
                 this.detailForm.markAsPristine();
+                this.addressDefaultsUpdated = false;
                 this.changeDetector.markForCheck();
                 this.router.navigate(['../', data.createCustomer.id], { relativeTo: this.route });
             },
@@ -95,15 +146,53 @@ export class CustomerDetailComponent extends BaseDetailComponent<Customer.Fragme
             .pipe(
                 take(1),
                 mergeMap(({ id }) => {
-                    const formValue = this.detailForm.value;
-                    const customer: UpdateCustomerInput = {
-                        id,
-                        title: formValue.title,
-                        emailAddress: formValue.emailAddress,
-                        firstName: formValue.firstName,
-                        lastName: formValue.lastName,
-                    };
-                    return this.dataService.customer.updateCustomer(customer);
+                    const saveOperations: Array<Observable<any>> = [];
+                    const customerForm = this.detailForm.get('customer');
+                    if (customerForm && customerForm.dirty) {
+                        const formValue = customerForm.value;
+                        const customer: UpdateCustomerInput = {
+                            id,
+                            title: formValue.title,
+                            emailAddress: formValue.emailAddress,
+                            firstName: formValue.firstName,
+                            lastName: formValue.lastName,
+                        };
+                        saveOperations.push(this.dataService.customer.updateCustomer(customer));
+                    }
+                    const addressFormArray = this.detailForm.get('addresses') as FormArray;
+                    if ((addressFormArray && addressFormArray.dirty) || this.addressDefaultsUpdated) {
+                        for (const addressControl of addressFormArray.controls) {
+                            if (addressControl.dirty || this.addressDefaultsUpdated) {
+                                const address = addressControl.value;
+                                const input: CreateAddressInput = {
+                                    fullName: address.fullName,
+                                    company: address.company,
+                                    streetLine1: address.streetLine1,
+                                    streetLine2: address.streetLine2,
+                                    city: address.city,
+                                    province: address.province,
+                                    postalCode: address.postalCode,
+                                    countryCode: address.countryCode,
+                                    phoneNumber: address.phoneNumber,
+                                    defaultShippingAddress: this.defaultShippingAddressId === address.id,
+                                    defaultBillingAddress: this.defaultBillingAddressId === address.id,
+                                };
+                                if (!address.id) {
+                                    saveOperations.push(
+                                        this.dataService.customer.createCustomerAddress(id, input),
+                                    );
+                                } else {
+                                    saveOperations.push(
+                                        this.dataService.customer.updateCustomerAddress({
+                                            ...input,
+                                            id: address.id,
+                                        }),
+                                    );
+                                }
+                            }
+                        }
+                    }
+                    return forkJoin(saveOperations);
                 }),
             )
             .subscribe(
@@ -112,6 +201,7 @@ export class CustomerDetailComponent extends BaseDetailComponent<Customer.Fragme
                         entity: 'Customer',
                     });
                     this.detailForm.markAsPristine();
+                    this.addressDefaultsUpdated = false;
                     this.changeDetector.markForCheck();
                 },
                 err => {
@@ -123,13 +213,30 @@ export class CustomerDetailComponent extends BaseDetailComponent<Customer.Fragme
     }
 
     protected setFormValues(entity: Customer.Fragment): void {
-        this.detailForm.patchValue({
-            title: entity.title,
-            firstName: entity.firstName,
-            lastName: entity.lastName,
-            phoneNumber: entity.phoneNumber,
-            emailAddress: entity.emailAddress,
-        });
+        const customerGroup = this.detailForm.get('customer');
+        if (customerGroup) {
+            customerGroup.patchValue({
+                title: entity.title,
+                firstName: entity.firstName,
+                lastName: entity.lastName,
+                phoneNumber: entity.phoneNumber,
+                emailAddress: entity.emailAddress,
+            });
+        }
+
+        if (entity.addresses) {
+            const addressesArray = new FormArray([]);
+            for (const address of entity.addresses) {
+                addressesArray.push(this.formBuilder.group(address));
+                if (address.defaultShippingAddress) {
+                    this.defaultShippingAddressId = address.id;
+                }
+                if (address.defaultBillingAddress) {
+                    this.defaultBillingAddressId = address.id;
+                }
+            }
+            this.detailForm.setControl('addresses', addressesArray);
+        }
 
         if (this.customFields.length) {
             const customFieldsGroup = this.detailForm.get(['customFields']) as FormGroup;

+ 1 - 13
admin-ui/src/app/customer/components/customer-list/customer-list.component.html

@@ -24,19 +24,7 @@
         <td class="left">{{ customer.id }}</td>
         <td class="left">{{ customer.title }} {{ customer.firstName }} {{ customer.lastName }}</td>
         <td class="left">{{ customer.emailAddress }}</td>
-        <td class="left">
-            <vdr-chip *ngIf="customer.user?.id">
-                <ng-container *ngIf="customer.user.verified">
-                    <clr-icon shape="check-circle" class="verified-user-icon"></clr-icon>
-                    {{ 'customer.verified' | translate }}
-                </ng-container>
-                <ng-container *ngIf="!customer.user.verified">
-                    <clr-icon shape="check-circle" class="registered-user-icon"></clr-icon>
-                    {{ 'customer.registered' | translate }}
-                </ng-container>
-            </vdr-chip>
-            <vdr-chip *ngIf="!customer.user?.id">{{ 'customer.guest' | translate }}</vdr-chip>
-        </td>
+        <td class="left"><vdr-customer-status-label [customer]="customer"></vdr-customer-status-label></td>
         <td class="right">
             <vdr-table-row-action
                 iconShape="edit"

+ 0 - 7
admin-ui/src/app/customer/components/customer-list/customer-list.component.scss

@@ -1,8 +1 @@
 @import "variables";
-
-.registered-user-icon {
-    color: $color-grey-3;
-}
-.verified-user-icon {
-    color: $color-success;
-}

+ 11 - 0
admin-ui/src/app/customer/components/customer-status-label/customer-status-label.component.html

@@ -0,0 +1,11 @@
+<vdr-chip *ngIf="customer.user?.id">
+    <ng-container *ngIf="customer.user?.verified">
+        <clr-icon shape="check-circle" class="verified-user-icon"></clr-icon>
+        {{ 'customer.verified' | translate }}
+    </ng-container>
+    <ng-container *ngIf="!customer.user.verified">
+        <clr-icon shape="check-circle" class="registered-user-icon"></clr-icon>
+        {{ 'customer.registered' | translate }}
+    </ng-container>
+</vdr-chip>
+<vdr-chip *ngIf="!customer.user?.id">{{ 'customer.guest' | translate }}</vdr-chip>

+ 8 - 0
admin-ui/src/app/customer/components/customer-status-label/customer-status-label.component.scss

@@ -0,0 +1,8 @@
+@import "variables";
+
+.registered-user-icon {
+    color: $color-grey-3;
+}
+.verified-user-icon {
+    color: $color-success;
+}

+ 12 - 0
admin-ui/src/app/customer/components/customer-status-label/customer-status-label.component.ts

@@ -0,0 +1,12 @@
+import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
+import { Customer } from 'shared/generated-types';
+
+@Component({
+    selector: 'vdr-customer-status-label',
+    templateUrl: './customer-status-label.component.html',
+    styleUrls: ['./customer-status-label.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class CustomerStatusLabelComponent {
+    @Input() customer: Customer.Fragment;
+}

+ 8 - 1
admin-ui/src/app/customer/customer.module.ts

@@ -3,14 +3,21 @@ import { RouterModule } from '@angular/router';
 
 import { SharedModule } from '../shared/shared.module';
 
+import { AddressCardComponent } from './components/address-card/address-card.component';
 import { CustomerDetailComponent } from './components/customer-detail/customer-detail.component';
 import { CustomerListComponent } from './components/customer-list/customer-list.component';
+import { CustomerStatusLabelComponent } from './components/customer-status-label/customer-status-label.component';
 import { customerRoutes } from './customer.routes';
 import { CustomerResolver } from './providers/routing/customer-resolver';
 
 @NgModule({
     imports: [SharedModule, RouterModule.forChild(customerRoutes)],
-    declarations: [CustomerListComponent, CustomerDetailComponent],
+    declarations: [
+        CustomerListComponent,
+        CustomerDetailComponent,
+        CustomerStatusLabelComponent,
+        AddressCardComponent,
+    ],
     providers: [CustomerResolver],
 })
 export class CustomerModule {}

+ 12 - 1
admin-ui/src/app/data/definitions/customer-definitions.ts

@@ -4,12 +4,14 @@ export const ADDRESS_FRAGMENT = gql`
     fragment Address on Address {
         id
         fullName
+        company
         streetLine1
         streetLine2
         city
         province
         postalCode
         country
+        countryCode
         phoneNumber
         defaultShippingAddress
         defaultBillingAddress
@@ -38,7 +40,7 @@ export const CUSTOMER_FRAGMENT = gql`
 `;
 
 export const GET_CUSTOMER_LIST = gql`
-    query GetCustomerList($options: CustomerListOptions!) {
+    query GetCustomerList($options: CustomerListOptions) {
         customers(options: $options) {
             items {
                 id
@@ -83,6 +85,15 @@ export const UPDATE_CUSTOMER = gql`
     ${CUSTOMER_FRAGMENT}
 `;
 
+export const CREATE_CUSTOMER_ADDRESS = gql`
+    mutation CreateCustomerAddress($customerId: ID!, $input: CreateAddressInput!) {
+        createCustomerAddress(customerId: $customerId, input: $input) {
+            ...Address
+        }
+    }
+    ${ADDRESS_FRAGMENT}
+`;
+
 export const UPDATE_CUSTOMER_ADDRESS = gql`
     mutation UpdateCustomerAddress($input: UpdateAddressInput!) {
         updateCustomerAddress(input: $input) {

+ 11 - 0
admin-ui/src/app/data/definitions/settings-definitions.ts

@@ -28,6 +28,17 @@ export const GET_COUNTRY_LIST = gql`
     }
 `;
 
+export const GET_AVAILABLE_COUNTRIES = gql`
+    query GetAvailableCountries {
+        availableCountries {
+            id
+            code
+            name
+            enabled
+        }
+    }
+`;
+
 export const GET_COUNTRY = gql`
     query GetCountry($id: ID!) {
         country(id: $id) {

+ 13 - 0
admin-ui/src/app/data/providers/customer-data.service.ts

@@ -1,5 +1,7 @@
 import {
+    CreateAddressInput,
     CreateCustomer,
+    CreateCustomerAddress,
     CreateCustomerInput,
     GetCustomer,
     GetCustomerList,
@@ -11,6 +13,7 @@ import {
 
 import {
     CREATE_CUSTOMER,
+    CREATE_CUSTOMER_ADDRESS,
     GET_CUSTOMER,
     GET_CUSTOMER_LIST,
     UPDATE_CUSTOMER,
@@ -57,6 +60,16 @@ export class CustomerDataService {
         );
     }
 
+    createCustomerAddress(customerId: string, input: CreateAddressInput) {
+        return this.baseDataService.mutate<CreateCustomerAddress.Mutation, CreateCustomerAddress.Variables>(
+            CREATE_CUSTOMER_ADDRESS,
+            {
+                customerId,
+                input,
+            },
+        );
+    }
+
     updateCustomerAddress(input: UpdateAddressInput) {
         return this.baseDataService.mutate<UpdateCustomerAddress.Mutation, UpdateCustomerAddress.Variables>(
             UPDATE_CUSTOMER_ADDRESS,

+ 6 - 0
admin-ui/src/app/data/providers/settings-data.service.ts

@@ -11,6 +11,7 @@ import {
     CreateZone,
     CreateZoneInput,
     GetActiveChannel,
+    GetAvailableCountries,
     GetChannel,
     GetChannels,
     GetCountry,
@@ -50,6 +51,7 @@ import {
     CREATE_TAX_RATE,
     CREATE_ZONE,
     GET_ACTIVE_CHANNEL,
+    GET_AVAILABLE_COUNTRIES,
     GET_CHANNEL,
     GET_CHANNELS,
     GET_COUNTRY,
@@ -86,6 +88,10 @@ export class SettingsDataService {
         });
     }
 
+    getAvailableCountries() {
+        return this.baseDataService.query<GetAvailableCountries.Query>(GET_AVAILABLE_COUNTRIES);
+    }
+
     getCountry(id: string) {
         return this.baseDataService.query<GetCountry.Query, GetCountry.Variables>(GET_COUNTRY, { id });
     }

+ 17 - 0
admin-ui/src/i18n-messages/en.json

@@ -92,6 +92,7 @@
     "custom-fields": "Custom fields",
     "description": "Description",
     "discard-changes": "Discard changes",
+    "done": "Done",
     "edit": "Edit",
     "edit-field": "Edit field",
     "enabled": "Enabled",
@@ -101,6 +102,7 @@
     "language": "Language",
     "log-out": "Log out",
     "login": "Log in",
+    "more": "More...",
     "name": "Name",
     "next": "Next",
     "notify-create-error": "An error occurred, could not create { entity }",
@@ -120,15 +122,30 @@
     "username": "Username"
   },
   "customer": {
+    "addresses": "Addresses",
+    "city": "City",
+    "country": "Country",
+    "create-new-address": "Create new address",
     "create-new-customer": "Create new customer",
     "customer-type": "Customer type",
+    "default-billing-address": "Default billing address",
+    "default-shipping-address": "Default shipping address",
     "email-address": "Email address",
     "first-name": "First name",
+    "full-name": "Full name",
     "guest": "Guest",
     "last-name": "Last name",
     "name": "Name",
+    "orders": "Orders",
     "password": "Password",
+    "phone-number": "Phone number",
+    "postal-code": "Postal code",
+    "province": "Province",
     "registered": "Registered",
+    "set-as-default-billing-address": "Set as default billing",
+    "set-as-default-shipping-address": "Set as default shipping",
+    "street-line-1": "Street line 1",
+    "street-line-2": "Street line 2",
     "title": "Title",
     "verified": "Verified"
   },

File diff suppressed because it is too large
+ 0 - 0
schema.json


+ 35 - 8
shared/generated-types.ts

@@ -237,7 +237,7 @@ export interface Address extends Node {
     city?: string | null;
     province?: string | null;
     postalCode?: string | null;
-    country?: string | null;
+    country: string;
     countryCode: string;
     phoneNumber?: string | null;
     defaultShippingAddress?: boolean | null;
@@ -3037,7 +3037,7 @@ export namespace AddressResolvers {
         city?: CityResolver<string | null, any, Context>;
         province?: ProvinceResolver<string | null, any, Context>;
         postalCode?: PostalCodeResolver<string | null, any, Context>;
-        country?: CountryResolver<string | null, any, Context>;
+        country?: CountryResolver<string, any, Context>;
         countryCode?: CountryCodeResolver<string, any, Context>;
         phoneNumber?: PhoneNumberResolver<string | null, any, Context>;
         defaultShippingAddress?: DefaultShippingAddressResolver<boolean | null, any, Context>;
@@ -3075,11 +3075,7 @@ export namespace AddressResolvers {
         Parent,
         Context
     >;
-    export type CountryResolver<R = string | null, Parent = any, Context = any> = Resolver<
-        R,
-        Parent,
-        Context
-    >;
+    export type CountryResolver<R = string, Parent = any, Context = any> = Resolver<R, Parent, Context>;
     export type CountryCodeResolver<R = string, Parent = any, Context = any> = Resolver<R, Parent, Context>;
     export type PhoneNumberResolver<R = string | null, Parent = any, Context = any> = Resolver<
         R,
@@ -5464,6 +5460,20 @@ export namespace UpdateCustomer {
     export type UpdateCustomer = Customer.Fragment;
 }
 
+export namespace CreateCustomerAddress {
+    export type Variables = {
+        customerId: string;
+        input: CreateAddressInput;
+    };
+
+    export type Mutation = {
+        __typename?: 'Mutation';
+        createCustomerAddress: CreateCustomerAddress;
+    };
+
+    export type CreateCustomerAddress = Address.Fragment;
+}
+
 export namespace UpdateCustomerAddress {
     export type Variables = {
         input: UpdateAddressInput;
@@ -6065,6 +6075,23 @@ export namespace GetCountryList {
     };
 }
 
+export namespace GetAvailableCountries {
+    export type Variables = {};
+
+    export type Query = {
+        __typename?: 'Query';
+        availableCountries: AvailableCountries[];
+    };
+
+    export type AvailableCountries = {
+        __typename?: 'Country';
+        id: string;
+        code: string;
+        name: string;
+        enabled: boolean;
+    };
+}
+
 export namespace GetCountry {
     export type Variables = {
         id: string;
@@ -6588,7 +6615,7 @@ export namespace Address {
         city?: string | null;
         province?: string | null;
         postalCode?: string | null;
-        country?: string | null;
+        country: string;
         countryCode: string;
         phoneNumber?: string | null;
         defaultShippingAddress?: boolean | null;

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