Browse Source

feat(admin-ui): Implement Zone list view, improved Zone/Country admin

Relates to #323
Michael Bromley 5 years ago
parent
commit
821f258036
29 changed files with 728 additions and 207 deletions
  1. 1 1
      packages/admin-ui/src/lib/catalog/src/components/collection-list/collection-list.component.scss
  2. 23 4
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  3. 6 0
      packages/admin-ui/src/lib/core/src/components/main-nav/main-nav.component.ts
  4. 14 0
      packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts
  5. 10 2
      packages/admin-ui/src/lib/core/src/data/providers/settings-data.service.ts
  6. 16 13
      packages/admin-ui/src/lib/core/src/shared/components/data-table/data-table.component.html
  7. 15 0
      packages/admin-ui/src/lib/settings/src/components/add-country-to-zone-dialog/add-country-to-zone-dialog.component.html
  8. 0 0
      packages/admin-ui/src/lib/settings/src/components/add-country-to-zone-dialog/add-country-to-zone-dialog.component.scss
  9. 36 0
      packages/admin-ui/src/lib/settings/src/components/add-country-to-zone-dialog/add-country-to-zone-dialog.component.ts
  10. 4 14
      packages/admin-ui/src/lib/settings/src/components/country-list/country-list.component.html
  11. 0 3
      packages/admin-ui/src/lib/settings/src/components/country-list/country-list.component.scss
  12. 11 107
      packages/admin-ui/src/lib/settings/src/components/country-list/country-list.component.ts
  13. 16 0
      packages/admin-ui/src/lib/settings/src/components/zone-detail-dialog/zone-detail-dialog.component.html
  14. 0 0
      packages/admin-ui/src/lib/settings/src/components/zone-detail-dialog/zone-detail-dialog.component.scss
  15. 22 0
      packages/admin-ui/src/lib/settings/src/components/zone-detail-dialog/zone-detail-dialog.component.ts
  16. 132 0
      packages/admin-ui/src/lib/settings/src/components/zone-list/zone-list.component.html
  17. 50 0
      packages/admin-ui/src/lib/settings/src/components/zone-list/zone-list.component.scss
  18. 188 0
      packages/admin-ui/src/lib/settings/src/components/zone-list/zone-list.component.ts
  19. 8 0
      packages/admin-ui/src/lib/settings/src/components/zone-member-list/zone-member-controls.directive.ts
  20. 8 0
      packages/admin-ui/src/lib/settings/src/components/zone-member-list/zone-member-list-header.directive.ts
  21. 40 0
      packages/admin-ui/src/lib/settings/src/components/zone-member-list/zone-member-list.component.html
  22. 20 0
      packages/admin-ui/src/lib/settings/src/components/zone-member-list/zone-member-list.component.scss
  23. 61 0
      packages/admin-ui/src/lib/settings/src/components/zone-member-list/zone-member-list.component.ts
  24. 0 18
      packages/admin-ui/src/lib/settings/src/components/zone-selector-dialog/zone-selector-dialog.component.html
  25. 0 28
      packages/admin-ui/src/lib/settings/src/components/zone-selector-dialog/zone-selector-dialog.component.ts
  26. 6 1
      packages/admin-ui/src/lib/settings/src/public_api.ts
  27. 12 2
      packages/admin-ui/src/lib/settings/src/settings.module.ts
  28. 16 8
      packages/admin-ui/src/lib/settings/src/settings.routes.ts
  29. 13 6
      packages/admin-ui/src/lib/static/i18n-messages/en.json

+ 1 - 1
packages/admin-ui/src/lib/catalog/src/components/collection-list/collection-list.component.scss

@@ -13,7 +13,7 @@
 
 .collection-wrapper {
     display: flex;
-    height: 100%;
+    height: calc(100% - 50px);
 
     vdr-collection-tree {
         flex: 1;

+ 23 - 4
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -640,9 +640,9 @@ export enum CurrencyCode {
   /** Canadian dollar */
   CAD = 'CAD',
   /** Congolese franc */
-  CHE = 'CHE',
+  CDF = 'CDF',
   /** Swiss franc */
-  CHW = 'CHW',
+  CHF = 'CHF',
   /** Chilean peso */
   CLP = 'CLP',
   /** Renminbi (Chinese) yuan */
@@ -5344,10 +5344,10 @@ export type GetZonesQuery = (
   { __typename?: 'Query' }
   & { zones: Array<(
     { __typename?: 'Zone' }
-    & Pick<Zone, 'id' | 'name'>
+    & Pick<Zone, 'id' | 'createdAt' | 'updatedAt' | 'name'>
     & { members: Array<(
       { __typename?: 'Country' }
-      & Pick<Country, 'id' | 'name' | 'code'>
+      & Pick<Country, 'createdAt' | 'updatedAt' | 'id' | 'name' | 'code' | 'enabled'>
     )> }
   )> }
 );
@@ -5391,6 +5391,19 @@ export type UpdateZoneMutation = (
   ) }
 );
 
+export type DeleteZoneMutationVariables = {
+  id: Scalars['ID'];
+};
+
+
+export type DeleteZoneMutation = (
+  { __typename?: 'Mutation' }
+  & { deleteZone: (
+    { __typename?: 'DeletionResponse' }
+    & Pick<DeletionResponse, 'message' | 'result'>
+  ) }
+);
+
 export type AddMembersToZoneMutationVariables = {
   zoneId: Scalars['ID'];
   memberIds: Array<Scalars['ID']>;
@@ -7134,6 +7147,12 @@ export namespace UpdateZone {
   export type UpdateZone = ZoneFragment;
 }
 
+export namespace DeleteZone {
+  export type Variables = DeleteZoneMutationVariables;
+  export type Mutation = DeleteZoneMutation;
+  export type DeleteZone = DeleteZoneMutation['deleteZone'];
+}
+
 export namespace AddMembersToZone {
   export type Variables = AddMembersToZoneMutationVariables;
   export type Mutation = AddMembersToZoneMutation;

+ 6 - 0
packages/admin-ui/src/lib/core/src/components/main-nav/main-nav.component.ts

@@ -144,6 +144,12 @@ export class MainNavComponent implements OnInit {
                         id: 'countries',
                         label: _('nav.countries'),
                         routerLink: ['/settings', 'countries'],
+                        icon: 'flag',
+                    },
+                    {
+                        id: 'zones',
+                        label: _('nav.zones'),
+                        routerLink: ['/settings', 'zones'],
                         icon: 'world',
                     },
                     {

+ 14 - 0
packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts

@@ -94,11 +94,16 @@ export const GET_ZONES = gql`
     query GetZones {
         zones {
             id
+            createdAt
+            updatedAt
             name
             members {
+                createdAt
+                updatedAt
                 id
                 name
                 code
+                enabled
             }
         }
     }
@@ -131,6 +136,15 @@ export const UPDATE_ZONE = gql`
     ${ZONE_FRAGMENT}
 `;
 
+export const DELETE_ZONE = gql`
+    mutation DeleteZone($id: ID!) {
+        deleteZone(id: $id) {
+            message
+            result
+        }
+    }
+`;
+
 export const ADD_MEMBERS_TO_ZONE = gql`
     mutation AddMembersToZone($zoneId: ID!, $memberIds: [ID!]!) {
         addMembersToZone(zoneId: $zoneId, memberIds: $memberIds) {

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

@@ -17,6 +17,7 @@ import {
     DeleteCountry,
     DeleteTaxCategory,
     DeleteTaxRate,
+    DeleteZone,
     GetActiveChannel,
     GetAllJobs,
     GetAvailableCountries,
@@ -66,6 +67,7 @@ import {
     DELETE_COUNTRY,
     DELETE_TAX_CATEGORY,
     DELETE_TAX_RATE,
+    DELETE_ZONE,
     GET_ACTIVE_CHANNEL,
     GET_AVAILABLE_COUNTRIES,
     GET_CHANNEL,
@@ -73,10 +75,10 @@ import {
     GET_COUNTRY,
     GET_COUNTRY_LIST,
     GET_GLOBAL_SETTINGS,
-    GET_JOB_INFO,
-    GET_JOB_QUEUE_LIST,
     GET_JOBS_BY_ID,
     GET_JOBS_LIST,
+    GET_JOB_INFO,
+    GET_JOB_QUEUE_LIST,
     GET_PAYMENT_METHOD,
     GET_PAYMENT_METHOD_LIST,
     GET_TAX_CATEGORIES,
@@ -158,6 +160,12 @@ export class SettingsDataService {
         });
     }
 
+    deleteZone(id: string) {
+        return this.baseDataService.mutate<DeleteZone.Mutation, DeleteZone.Variables>(DELETE_ZONE, {
+            id,
+        });
+    }
+
     addMembersToZone(zoneId: string, memberIds: string[]) {
         return this.baseDataService.mutate<AddMembersToZone.Mutation, AddMembersToZone.Variables>(
             ADD_MEMBERS_TO_ZONE,

+ 16 - 13
packages/admin-ui/src/lib/core/src/shared/components/data-table/data-table.component.html

@@ -2,12 +2,13 @@
     <table class="table">
         <thead>
             <tr>
-                <th *ngIf="isRowSelectedFn">
-                    <vdr-select-toggle
-                        size="small"
-                        [selected]="allSelected"
-                        (selectedChange)="allSelectChange.emit()"
-                    ></vdr-select-toggle>
+                <th *ngIf="isRowSelectedFn" class="align-middle">
+                    <input
+                        type="checkbox"
+                        clrCheckbox
+                        [checked]="allSelected"
+                        (change)="allSelectChange.emit()"
+                    />
                 </th>
                 <th *ngFor="let header of columns?.toArray()" class="left" [class.expand]="header.expand">
                     <ng-container *ngTemplateOutlet="header.template"></ng-container>
@@ -24,15 +25,17 @@
                                   currentPage: currentPage,
                                   totalItems: totalItems
                               };
-                    index as i; trackBy: trackByFn
+                    index as i;
+                    trackBy: trackByFn
                 "
             >
-                <td *ngIf="isRowSelectedFn">
-                    <vdr-select-toggle
-                        size="small"
-                        [selected]="isRowSelectedFn(item)"
-                        (selectedChange)="rowSelectChange.emit(item)"
-                    ></vdr-select-toggle>
+                <td *ngIf="isRowSelectedFn" class="align-middle">
+                    <input
+                        type="checkbox"
+                        clrCheckbox
+                        [checked]="isRowSelectedFn(item)"
+                        (change)="rowSelectChange.emit(item)"
+                    />
                 </td>
                 <ng-container
                     *ngTemplateOutlet="rowTemplate; context: { item: item, index: i }"

+ 15 - 0
packages/admin-ui/src/lib/settings/src/components/add-country-to-zone-dialog/add-country-to-zone-dialog.component.html

@@ -0,0 +1,15 @@
+<ng-template vdrDialogTitle>{{ 'settings.add-countries-to-zone' | translate: { zoneName: zoneName } }}</ng-template>
+
+<vdr-zone-member-list
+    [members]="availableCountries$ | async"
+    [selectedMemberIds]="selectedMemberIds"
+    (selectionChange)="selectedMemberIds = $event"
+>
+</vdr-zone-member-list>
+
+<ng-template vdrDialogButtons>
+    <button type="button" class="btn" (click)="cancel()">{{ 'common.cancel' | translate }}</button>
+    <button type="submit" (click)="add()" [disabled]="!selectedMemberIds.length" class="btn btn-primary">
+        {{ 'settings.add-countries-to-zone' | translate: { zoneName: zoneName } }}
+    </button>
+</ng-template>

+ 0 - 0
packages/admin-ui/src/lib/settings/src/components/zone-selector-dialog/zone-selector-dialog.component.scss → packages/admin-ui/src/lib/settings/src/components/add-country-to-zone-dialog/add-country-to-zone-dialog.component.scss


+ 36 - 0
packages/admin-ui/src/lib/settings/src/components/add-country-to-zone-dialog/add-country-to-zone-dialog.component.ts

@@ -0,0 +1,36 @@
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { DataService, Dialog, GetCountryList, GetZones } from '@vendure/admin-ui/core';
+import { Observable } from 'rxjs';
+import { filter, map } from 'rxjs/operators';
+
+@Component({
+    selector: 'vdr-add-country-to-zone-dialog',
+    templateUrl: './add-country-to-zone-dialog.component.html',
+    styleUrls: ['./add-country-to-zone-dialog.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class AddCountryToZoneDialogComponent implements Dialog<string[]>, OnInit {
+    resolveWith: (result?: string[]) => void;
+    zoneName: string;
+    currentMembers: GetZones.Members[] = [];
+    availableCountries$: Observable<GetCountryList.Items[]>;
+    selectedMemberIds: string[] = [];
+
+    constructor(private dataService: DataService) {}
+
+    ngOnInit(): void {
+        const currentMemberIds = this.currentMembers.map(m => m.id);
+        this.availableCountries$ = this.dataService.settings
+            .getCountries(99999)
+            .mapStream(data => data.countries.items)
+            .pipe(map(countries => countries.filter(c => !currentMemberIds.includes(c.id))));
+    }
+
+    cancel() {
+        this.resolveWith();
+    }
+
+    add() {
+        this.resolveWith(this.selectedMemberIds);
+    }
+}

+ 4 - 14
packages/admin-ui/src/lib/settings/src/components/country-list/country-list.component.html

@@ -7,14 +7,6 @@
             [placeholder]="'settings.search-country-by-name' | translate"
             class="clr-input search-input"
         />
-        <div *ngIf="selectedCountryIds.length">
-            <button class="btn btn-sm" (click)="addCountriesToZone()">
-                {{ 'settings.add-countries-to-zone' | translate }}
-            </button>
-            <button class="btn btn-sm" (click)="removeCountriesFromZone()">
-                {{ 'settings.remove-countries-from-zone' | translate }}
-            </button>
-        </div>
     </vdr-ab-left>
 
     <vdr-ab-right>
@@ -28,10 +20,6 @@
 
 <vdr-data-table
     [items]="countriesWithZones$ | async"
-    [allSelected]="areAllSelected()"
-    [isRowSelectedFn]="('UpdateSettings' | hasPermission) && isCountrySelected"
-    (rowSelectChange)="toggleSelectCountry($event)"
-    (allSelectChange)="toggleSelectAll()"
 >
     <vdr-dt-column>{{ 'common.code' | translate }}</vdr-dt-column>
     <vdr-dt-column [expand]="true">{{ 'common.name' | translate }}</vdr-dt-column>
@@ -43,11 +31,13 @@
         <td class="left align-middle">{{ country.code }}</td>
         <td class="left align-middle">{{ country.name }}</td>
         <td class="left align-middle">
-            <vdr-chip *ngFor="let zone of country.zones" [colorFrom]="zone.id">{{ zone.name }}</vdr-chip>
+            <a [routerLink]="['/settings', 'zones', { contents: zone.id }]" *ngFor="let zone of country.zones">
+            <vdr-chip [colorFrom]="zone.name">{{ zone.name }}</vdr-chip>
+            </a>
         </td>
         <td class="left align-middle">
             <clr-icon
-                [class.enabled]="country.enabled"
+                [class.is-success]="country.enabled"
                 [attr.shape]="country.enabled ? 'check' : 'times'"
             ></clr-icon>
         </td>

+ 0 - 3
packages/admin-ui/src/lib/settings/src/components/country-list/country-list.component.scss

@@ -1,8 +1,5 @@
 @import "variables";
 
-clr-icon.enabled {
-    color: $color-success-500;
-}
 .search-input {
     margin-top: 6px;
     min-width: 300px;

+ 11 - 107
packages/admin-ui/src/lib/settings/src/components/country-list/country-list.component.ts

@@ -1,14 +1,17 @@
 import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
 import { FormControl } from '@angular/forms';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import { combineLatest, EMPTY, Observable, of, Subject } from 'rxjs';
-import { map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
-
-import { Country, DeletionResult, GetCountryList, GetZones, Zone } from '@vendure/admin-ui/core';
-import { NotificationService } from '@vendure/admin-ui/core';
-import { DataService } from '@vendure/admin-ui/core';
-import { ModalService } from '@vendure/admin-ui/core';
-import { ZoneSelectorDialogComponent } from '../zone-selector-dialog/zone-selector-dialog.component';
+import {
+    DataService,
+    DeletionResult,
+    GetCountryList,
+    GetZones,
+    ModalService,
+    NotificationService,
+    Zone,
+} from '@vendure/admin-ui/core';
+import { combineLatest, EMPTY, Observable, Subject } from 'rxjs';
+import { map, startWith, switchMap, tap } from 'rxjs/operators';
 
 @Component({
     selector: 'vdr-country-list',
@@ -21,7 +24,6 @@ export class CountryListComponent implements OnInit, OnDestroy {
     countriesWithZones$: Observable<Array<GetCountryList.Items & { zones: GetZones.Zones[] }>>;
     zones$: Observable<GetZones.Zones[]>;
 
-    selectedCountryIds: string[] = [];
     private countries: GetCountryList.Items[] = [];
     private destroy$ = new Subject();
 
@@ -54,104 +56,6 @@ export class CountryListComponent implements OnInit, OnDestroy {
         this.destroy$.complete();
     }
 
-    areAllSelected(): boolean {
-        return this.selectedCountryIds.length === this.countries.length;
-    }
-
-    toggleSelectAll() {
-        if (this.areAllSelected()) {
-            this.selectedCountryIds = [];
-        } else {
-            this.selectedCountryIds = this.countries.map(v => v.id);
-        }
-    }
-
-    toggleSelectCountry(country: Country.Fragment) {
-        const index = this.selectedCountryIds.indexOf(country.id);
-        if (-1 < index) {
-            this.selectedCountryIds.splice(index, 1);
-        } else {
-            this.selectedCountryIds.push(country.id);
-        }
-    }
-
-    isCountrySelected = (country: Country.Fragment): boolean => {
-        return -1 < this.selectedCountryIds.indexOf(country.id);
-    };
-
-    addCountriesToZone() {
-        this.zones$
-            .pipe(
-                take(1),
-                mergeMap(zones => {
-                    return this.modalService.fromComponent(ZoneSelectorDialogComponent, {
-                        locals: {
-                            allZones: zones,
-                            canCreateNewZone: true,
-                        },
-                    });
-                }),
-                mergeMap(selection => {
-                    if (selection && this.isZone(selection)) {
-                        return this.dataService.settings
-                            .addMembersToZone(selection.id, this.selectedCountryIds)
-                            .pipe(map(data => data.addMembersToZone));
-                    } else if (selection) {
-                        return this.dataService.settings
-                            .createZone({
-                                name: typeof selection === 'string' ? selection : selection.name,
-                                memberIds: this.selectedCountryIds,
-                            })
-                            .pipe(map(data => data.createZone));
-                    } else {
-                        return of(undefined);
-                    }
-                }),
-            )
-            .subscribe(result => {
-                if (result) {
-                    this.notificationService.success(_(`settings.add-countries-to-zone-success`), {
-                        countryCount: this.selectedCountryIds.length,
-                        zoneName: result.name,
-                    });
-                    this.selectedCountryIds = [];
-                }
-            });
-    }
-
-    removeCountriesFromZone() {
-        this.zones$
-            .pipe(
-                take(1),
-                mergeMap(zones => {
-                    return this.modalService.fromComponent(ZoneSelectorDialogComponent, {
-                        locals: {
-                            allZones: zones,
-                            canCreateNewZone: false,
-                        },
-                    });
-                }),
-                mergeMap(selection => {
-                    if (selection && this.isZone(selection)) {
-                        return this.dataService.settings
-                            .removeMembersFromZone(selection.id, this.selectedCountryIds)
-                            .pipe(map(data => data.removeMembersFromZone));
-                    } else {
-                        return of(undefined);
-                    }
-                }),
-            )
-            .subscribe(result => {
-                if (result) {
-                    this.notificationService.success(_(`settings.remove-countries-from-zone-success`), {
-                        countryCount: this.selectedCountryIds.length,
-                        zoneName: result.name,
-                    });
-                    this.selectedCountryIds = [];
-                }
-            });
-    }
-
     deleteCountry(countryId: string) {
         this.modalService
             .dialog({

+ 16 - 0
packages/admin-ui/src/lib/settings/src/components/zone-detail-dialog/zone-detail-dialog.component.html

@@ -0,0 +1,16 @@
+<ng-template vdrDialogTitle>
+    <span *ngIf="zone.id">{{ 'settings.update-zone' | translate }}</span>
+    <span *ngIf="!zone.id">{{ 'settings.create-zone' | translate }}</span>
+</ng-template>
+
+<vdr-form-field [label]="'common.name' | translate" for="name">
+    <input id="name" type="text" [(ngModel)]="zone.name" [readonly]="!('UpdateSettings' | hasPermission)" />
+</vdr-form-field>
+
+<ng-template vdrDialogButtons>
+    <button type="button" class="btn" (click)="cancel()">{{ 'common.cancel' | translate }}</button>
+    <button type="submit" (click)="save()" [disabled]="!zone.name" class="btn btn-primary">
+        <span *ngIf="zone.id">{{ 'settings.update-zone' | translate }}</span>
+        <span *ngIf="!zone.id">{{ 'settings.create-zone' | translate }}</span>
+    </button>
+</ng-template>

+ 0 - 0
packages/admin-ui/src/lib/settings/src/components/zone-detail-dialog/zone-detail-dialog.component.scss


+ 22 - 0
packages/admin-ui/src/lib/settings/src/components/zone-detail-dialog/zone-detail-dialog.component.ts

@@ -0,0 +1,22 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { CreateZoneInput, DataService, Dialog, UpdateZoneInput } from '@vendure/admin-ui/core';
+
+@Component({
+    selector: 'vdr-zone-detail-dialog',
+    templateUrl: './zone-detail-dialog.component.html',
+    styleUrls: ['./zone-detail-dialog.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ZoneDetailDialogComponent implements Dialog<string> {
+    zone: { id?: string; name: string };
+    resolveWith: (result?: string) => void;
+    constructor(private dataService: DataService) {}
+
+    cancel() {
+        this.resolveWith();
+    }
+
+    save() {
+        this.resolveWith(this.zone.name);
+    }
+}

+ 132 - 0
packages/admin-ui/src/lib/settings/src/components/zone-list/zone-list.component.html

@@ -0,0 +1,132 @@
+<vdr-action-bar>
+    <vdr-ab-left> </vdr-ab-left>
+    <vdr-ab-right>
+        <vdr-action-bar-items locationId="zone-list"></vdr-action-bar-items>
+        <button class="btn btn-primary" *vdrIfPermissions="'CreateSettings'" (click)="create()">
+            <clr-icon shape="plus"></clr-icon>
+            {{ 'settings.create-new-zone' | translate }}
+        </button>
+    </vdr-ab-right>
+</vdr-action-bar>
+<div class="zone-wrapper">
+    <table class="table zone-list">
+        <tbody>
+            <tr *ngFor="let zone of zones$ | async" [class.active]="zone.id === (activeZone$ | async)?.id">
+                <td class="left align-middle"><vdr-entity-info [entity]="zone"></vdr-entity-info></td>
+                <td class="left align-middle"><vdr-chip [colorFrom]="zone.name">{{ zone.name }}</vdr-chip></td>
+                <td class="text-right align-middle">
+                    <a
+                        class="btn btn-link btn-sm"
+                        [routerLink]="['./', { contents: zone.id }]"
+                        queryParamsHandling="preserve"
+                    >
+                        <clr-icon shape="view-list"></clr-icon>
+                        {{ 'settings.view-zone-members' | translate }}
+                    </a>
+                </td>
+                <td class="align-middle">
+                    <button class="btn btn-link btn-sm" (click)="update(zone)">
+                        <clr-icon shape="edit"></clr-icon>
+                        {{ 'common.edit' | translate }}
+                    </button>
+                </td>
+                <td class="align-middle">
+                    <vdr-dropdown>
+                        <button type="button" class="btn btn-link btn-sm" vdrDropdownTrigger>
+                            {{ 'common.actions' | translate }}
+                            <clr-icon shape="caret down"></clr-icon>
+                        </button>
+                        <vdr-dropdown-menu vdrPosition="bottom-right">
+                            <button
+                                class="button"
+                                vdrDropdownItem
+                                (click)="delete(zone.id)"
+                                [disabled]="!('DeleteSettings' | hasPermission)"
+                            >
+                                <clr-icon shape="trash" class="is-danger"></clr-icon>
+                                {{ 'common.delete' | translate }}
+                            </button>
+                        </vdr-dropdown-menu>
+                    </vdr-dropdown>
+                </td>
+            </tr>
+        </tbody>
+    </table>
+    <div class="zone-members" [class.expanded]="activeZone$ | async">
+        <ng-container *ngIf="activeZone$ | async as activeZone">
+            <vdr-zone-member-list
+                [members]="activeZone.members"
+                [selectedMemberIds]="selectedMemberIds"
+                (selectionChange)="selectedMemberIds = $event"
+            >
+                <div *vdrZoneMemberListHeader>
+                    <div class="flex">
+                        <div class="header-title-row">
+                            {{ activeZone.name }} ({{ activeZone.members.length }})
+                        </div>
+                        <div class="flex-spacer"></div>
+                        <button type="button" class="close-button" (click)="closeMembers()">
+                            <clr-icon shape="close"></clr-icon>
+                        </button>
+                    </div>
+                    <div class="controls">
+                        <vdr-dropdown>
+                            <button
+                                type="button"
+                                class="btn btn-secondary btn-sm"
+                                vdrDropdownTrigger
+                                [disabled]="selectedMemberIds.length === 0"
+                            >
+                                {{ 'common.with-selected' | translate }}
+                                <clr-icon shape="caret down"></clr-icon>
+                            </button>
+                            <vdr-dropdown-menu vdrPosition="bottom-right">
+                                <button
+                                    type="button"
+                                    class="delete-button"
+                                    (click)="removeFromZone(activeZone, selectedMemberIds)"
+                                    vdrDropdownItem
+                                    [disabled]="!('DeleteSettings' | hasPermission)"
+                                >
+                                    <clr-icon shape="trash" class="is-danger"></clr-icon>
+                                    {{ 'settings.remove-from-zone' | translate }}
+                                </button>
+                            </vdr-dropdown-menu>
+                        </vdr-dropdown>
+                        <button class="btn btn-secondary btn-sm" (click)="addToZone(activeZone)">
+                            {{ 'settings.add-countries-to-zone' | translate: { zoneName: activeZone.name } }}
+                        </button>
+                    </div>
+                </div>
+                <div *vdrZoneMemberControls="let member = member">
+                    <vdr-dropdown>
+                        <button type="button" class="btn btn-link btn-sm" vdrDropdownTrigger>
+                            {{ 'common.actions' | translate }}
+                            <clr-icon shape="caret down"></clr-icon>
+                        </button>
+                        <vdr-dropdown-menu vdrPosition="bottom-right">
+                            <a
+                                type="button"
+                                [routerLink]="['/settings', 'countries', member.id]"
+                                vdrDropdownItem
+                            >
+                                <clr-icon shape="edit"></clr-icon>
+                                {{ 'common.edit' | translate }}
+                            </a>
+                            <button
+                                type="button"
+                                class="delete-button"
+                                (click)="removeFromZone(activeZone, [member.id])"
+                                vdrDropdownItem
+                                [disabled]="!('DeleteSettings' | hasPermission)"
+                            >
+                                <clr-icon shape="trash" class="is-danger"></clr-icon>
+                                {{ 'settings.remove-from-zone' | translate }}
+                            </button>
+                        </vdr-dropdown-menu>
+                    </vdr-dropdown>
+                </div>
+            </vdr-zone-member-list>
+        </ng-container>
+    </div>
+</div>

+ 50 - 0
packages/admin-ui/src/lib/settings/src/components/zone-list/zone-list.component.scss

@@ -0,0 +1,50 @@
+@import 'variables';
+
+.zone-wrapper {
+    display: flex;
+    height: calc(100% - 50px);
+
+    .zone-list {
+        flex: 1;
+        overflow: auto;
+        margin-top: 0;
+
+        tr.active {
+            background-color: $color-grey-200;
+        }
+    }
+}
+.zone-members {
+    height: 100%;
+    width: 0;
+    opacity: 0;
+    visibility: hidden;
+    overflow: auto;
+    transition: width 0.3s, opacity 0.2s 0.3s, visibility 0s 0.3s;
+
+    &.expanded {
+        width: 40vw;
+        visibility: visible;
+        opacity: 1;
+        padding-left: 12px;
+    }
+
+    .close-button {
+        margin: 0;
+        background: none;
+        border: none;
+        cursor: pointer;
+    }
+
+    ::ng-deep table.table {
+        margin-top: 0;
+        th {
+            top: 0;
+        }
+    }
+
+    .controls {
+        display: flex;
+        justify-content: space-between;
+    }
+}

+ 188 - 0
packages/admin-ui/src/lib/settings/src/components/zone-list/zone-list.component.ts

@@ -0,0 +1,188 @@
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
+import {
+    Country,
+    DataService,
+    DeletionResult,
+    GetZones,
+    ModalService,
+    NotificationService,
+} from '@vendure/admin-ui/core';
+import { combineLatest, EMPTY, Observable, of } from 'rxjs';
+import { distinctUntilChanged, map, mapTo, startWith, switchMap, tap } from 'rxjs/operators';
+
+import { AddCountryToZoneDialogComponent } from '../add-country-to-zone-dialog/add-country-to-zone-dialog.component';
+import { ZoneDetailDialogComponent } from '../zone-detail-dialog/zone-detail-dialog.component';
+
+@Component({
+    selector: 'vdr-zone-list',
+    templateUrl: './zone-list.component.html',
+    styleUrls: ['./zone-list.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ZoneListComponent implements OnInit {
+    activeZone$: Observable<GetZones.Zones | undefined>;
+    zones$: Observable<GetZones.Zones[]>;
+    members$: Observable<GetZones.Members[]>;
+    selectedMemberIds: string[] = [];
+
+    constructor(
+        private dataService: DataService,
+        private notificationService: NotificationService,
+        private modalService: ModalService,
+        private route: ActivatedRoute,
+        private router: Router,
+    ) {}
+
+    ngOnInit(): void {
+        this.zones$ = this.dataService.settings.getZones().mapStream(data => data.zones);
+        const activeZoneId$ = this.route.paramMap.pipe(
+            map(pm => pm.get('contents')),
+            distinctUntilChanged(),
+            tap(() => (this.selectedMemberIds = [])),
+        );
+        this.activeZone$ = combineLatest(this.zones$, activeZoneId$).pipe(
+            map(([zones, activeZoneId]) => {
+                if (activeZoneId) {
+                    return zones.find(z => z.id === activeZoneId);
+                }
+            }),
+        );
+    }
+
+    create() {
+        this.modalService
+            .fromComponent(ZoneDetailDialogComponent, { locals: { zone: { name: '' } } })
+            .pipe(
+                switchMap(name =>
+                    name ? this.dataService.settings.createZone({ name, memberIds: [] }) : EMPTY,
+                ),
+                // refresh list
+                switchMap(() => this.dataService.settings.getZones().single$),
+            )
+            .subscribe(
+                () => {
+                    this.notificationService.success(_('common.notify-create-success'), {
+                        entity: 'Zone',
+                    });
+                },
+                err => {
+                    this.notificationService.error(_('common.notify-create-error'), {
+                        entity: 'Zone',
+                    });
+                },
+            );
+    }
+
+    delete(zoneId: string) {
+        this.modalService
+            .dialog({
+                title: _('catalog.confirm-delete-zone'),
+                buttons: [
+                    { type: 'secondary', label: _('common.cancel') },
+                    { type: 'danger', label: _('common.delete'), returnValue: true },
+                ],
+            })
+            .pipe(
+                switchMap(response => (response ? this.dataService.settings.deleteZone(zoneId) : EMPTY)),
+
+                switchMap(result => {
+                    if (result.deleteZone.result === DeletionResult.DELETED) {
+                        // refresh list
+                        return this.dataService.settings
+                            .getZones()
+                            .mapSingle(() => ({ errorMessage: false }));
+                    } else {
+                        return of({ errorMessage: result.deleteZone.message });
+                    }
+                }),
+            )
+            .subscribe(
+                result => {
+                    if (typeof result.errorMessage === 'string') {
+                        this.notificationService.error(result.errorMessage);
+                    } else {
+                        this.notificationService.success(_('common.notify-delete-success'), {
+                            entity: 'Zone',
+                        });
+                    }
+                },
+                err => {
+                    this.notificationService.error(_('common.notify-delete-error'), {
+                        entity: 'Zone',
+                    });
+                },
+            );
+    }
+
+    update(zone: GetZones.Zones) {
+        this.modalService
+            .fromComponent(ZoneDetailDialogComponent, { locals: { zone } })
+            .pipe(
+                switchMap(name =>
+                    name ? this.dataService.settings.updateZone({ id: zone.id, name }) : EMPTY,
+                ),
+            )
+            .subscribe(
+                () => {
+                    this.notificationService.success(_('common.notify-update-success'), {
+                        entity: 'Zone',
+                    });
+                },
+                err => {
+                    this.notificationService.error(_('common.notify-update-error'), {
+                        entity: 'Zone',
+                    });
+                },
+            );
+    }
+
+    closeMembers() {
+        const params = { ...this.route.snapshot.params };
+        delete params.contents;
+        this.router.navigate(['./', params], { relativeTo: this.route, queryParamsHandling: 'preserve' });
+    }
+
+    addToZone(zone: GetZones.Zones) {
+        this.modalService
+            .fromComponent(AddCountryToZoneDialogComponent, {
+                locals: {
+                    zoneName: zone.name,
+                    currentMembers: zone.members,
+                },
+                size: 'md',
+            })
+            .pipe(
+                switchMap(memberIds =>
+                    memberIds
+                        ? this.dataService.settings
+                              .addMembersToZone(zone.id, memberIds)
+                              .pipe(mapTo(memberIds))
+                        : EMPTY,
+                ),
+            )
+            .subscribe({
+                next: result => {
+                    this.notificationService.success(_(`settings.add-countries-to-zone-success`), {
+                        countryCount: result.length,
+                        zoneName: zone.name,
+                    });
+                },
+                error: err => {
+                    this.notificationService.error(err);
+                },
+            });
+    }
+
+    removeFromZone(zone: GetZones.Zones, memberIds: string[]) {
+        this.dataService.settings.removeMembersFromZone(zone.id, memberIds).subscribe({
+            complete: () => {
+                this.notificationService.success(_(`settings.remove-countries-from-zone-success`), {
+                    countryCount: memberIds.length,
+                    zoneName: zone.name,
+                });
+            },
+        });
+    }
+}

+ 8 - 0
packages/admin-ui/src/lib/settings/src/components/zone-member-list/zone-member-controls.directive.ts

@@ -0,0 +1,8 @@
+import { Directive, TemplateRef } from '@angular/core';
+
+@Directive({
+    selector: '[vdrZoneMemberControls]',
+})
+export class ZoneMemberControlsDirective {
+    constructor(public templateRef: TemplateRef<any>) {}
+}

+ 8 - 0
packages/admin-ui/src/lib/settings/src/components/zone-member-list/zone-member-list-header.directive.ts

@@ -0,0 +1,8 @@
+import { Directive, TemplateRef } from '@angular/core';
+
+@Directive({
+    selector: '[vdrZoneMemberListHeader]',
+})
+export class ZoneMemberListHeaderDirective {
+    constructor(public templateRef: TemplateRef<any>) {}
+}

+ 40 - 0
packages/admin-ui/src/lib/settings/src/components/zone-member-list/zone-member-list.component.html

@@ -0,0 +1,40 @@
+<div class="members-header">
+    <ng-container *ngIf="headerTemplate">
+        <ng-container *ngTemplateOutlet="headerTemplate.templateRef"></ng-container>
+    </ng-container>
+    <input
+        type="text"
+        [placeholder]="'settings.filter-by-member-name' | translate"
+        class="clr-input"
+        [(ngModel)]="filterTerm"
+    />
+</div>
+<vdr-data-table
+    [items]="filteredMembers()"
+    [allSelected]="areAllSelected()"
+    [isRowSelectedFn]="('UpdateSettings' | hasPermission) && isMemberSelected"
+    (rowSelectChange)="toggleSelectMember($event)"
+    (allSelectChange)="toggleSelectAll()"
+>
+    <vdr-dt-column>{{ 'common.code' | translate }}</vdr-dt-column>
+    <vdr-dt-column>{{ 'common.name' | translate }}</vdr-dt-column>
+    <vdr-dt-column>{{ 'common.enabled' | translate }}</vdr-dt-column>
+    <vdr-dt-column></vdr-dt-column>
+    <ng-template let-member="item">
+        <td class="left align-middle">{{ member.code }}</td>
+        <td class="left align-middle">{{ member.name }}</td>
+        <td class="left align-middle">
+            <clr-icon
+                [class.is-success]="member.enabled"
+                [attr.shape]="member.enabled ? 'check' : 'times'"
+            ></clr-icon>
+        </td>
+        <td class="right align-middle">
+            <ng-container *ngIf="controlsTemplate">
+                <ng-container
+                    *ngTemplateOutlet="controlsTemplate.templateRef; context: { member: member }"
+                ></ng-container>
+            </ng-container>
+        </td>
+    </ng-template>
+</vdr-data-table>

+ 20 - 0
packages/admin-ui/src/lib/settings/src/components/zone-member-list/zone-member-list.component.scss

@@ -0,0 +1,20 @@
+@import "variables";
+
+.members-header {
+    background-color: $color-grey-100;
+    position: sticky;
+    top: 0;
+    padding: 6px;
+    z-index: 5;
+    border-bottom: 1px solid $color-grey-300;
+
+    .header-title-row {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+    }
+
+    .clr-input {
+        width: 100%;
+    }
+}

+ 61 - 0
packages/admin-ui/src/lib/settings/src/components/zone-member-list/zone-member-list.component.ts

@@ -0,0 +1,61 @@
+import { ChangeDetectionStrategy, Component, ContentChild, EventEmitter, Input, Output } from '@angular/core';
+import { GetZones } from '@vendure/admin-ui/core';
+
+import { ZoneMemberControlsDirective } from './zone-member-controls.directive';
+import { ZoneMemberListHeaderDirective } from './zone-member-list-header.directive';
+
+export type ZoneMember = { id: string; name: string; code: string };
+
+@Component({
+    selector: 'vdr-zone-member-list',
+    templateUrl: './zone-member-list.component.html',
+    styleUrls: ['./zone-member-list.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ZoneMemberListComponent {
+    @Input() members: ZoneMember[] = [];
+    @Input() selectedMemberIds: string[] = [];
+    @Output() selectionChange = new EventEmitter<string[]>();
+    @ContentChild(ZoneMemberListHeaderDirective) headerTemplate: ZoneMemberListHeaderDirective;
+    @ContentChild(ZoneMemberControlsDirective) controlsTemplate: ZoneMemberControlsDirective;
+    filterTerm = '';
+
+    filteredMembers(): ZoneMember[] {
+        if (this.filterTerm !== '') {
+            const term = this.filterTerm.toLocaleLowerCase();
+            return this.members.filter(
+                m => m.name.toLocaleLowerCase().includes(term) || m.code.toLocaleLowerCase().includes(term),
+            );
+        } else {
+            return this.members;
+        }
+    }
+
+    areAllSelected(): boolean {
+        if (this.members) {
+            return this.selectedMemberIds.length === this.members.length;
+        } else {
+            return false;
+        }
+    }
+
+    toggleSelectAll() {
+        if (this.areAllSelected()) {
+            this.selectionChange.emit([]);
+        } else {
+            this.selectionChange.emit(this.members.map(v => v.id));
+        }
+    }
+
+    toggleSelectMember(member: ZoneMember) {
+        if (this.selectedMemberIds.includes(member.id)) {
+            this.selectionChange.emit(this.selectedMemberIds.filter(id => id !== member.id));
+        } else {
+            this.selectionChange.emit([...this.selectedMemberIds, member.id]);
+        }
+    }
+
+    isMemberSelected = (member: ZoneMember): boolean => {
+        return -1 < this.selectedMemberIds.indexOf(member.id);
+    };
+}

+ 0 - 18
packages/admin-ui/src/lib/settings/src/components/zone-selector-dialog/zone-selector-dialog.component.html

@@ -1,18 +0,0 @@
-<ng-template vdrDialogTitle>{{ 'settings.select-zone' | translate }}</ng-template>
-
-<ng-select
-    [items]="allZones"
-    bindLabel="name"
-    bindValue="id"
-    appendTo="body"
-    [addTag]="canCreateNewZone"
-    (change)="onChange($event)"
-></ng-select>
-
-<ng-template vdrDialogButtons>
-    <button type="button" class="btn" (click)="cancel()">{{ 'common.cancel' | translate }}</button>
-    <button type="submit" (click)="select()" [disabled]="!selected" class="btn btn-primary">
-        <span *ngIf="!!selected && !selected.id">{{ 'settings.create-zone' | translate }}</span>
-        <span *ngIf="!selected || selected.id">{{ 'settings.select-zone' | translate }}</span>
-    </button>
-</ng-template>

+ 0 - 28
packages/admin-ui/src/lib/settings/src/components/zone-selector-dialog/zone-selector-dialog.component.ts

@@ -1,28 +0,0 @@
-import { Component } from '@angular/core';
-
-import { GetZones } from '@vendure/admin-ui/core';
-import { Dialog } from '@vendure/admin-ui/core';
-
-@Component({
-    selector: 'vdr-zone-selector-dialog',
-    templateUrl: './zone-selector-dialog.component.html',
-    styleUrls: ['./zone-selector-dialog.component.scss'],
-})
-export class ZoneSelectorDialogComponent implements Dialog<GetZones.Zones | { name: string } | string> {
-    allZones: GetZones.Zones[];
-    canCreateNewZone = false;
-    resolveWith: (result?: GetZones.Zones | { name: string } | string) => void;
-    selected: any;
-
-    onChange(e) {
-        this.selected = e;
-    }
-
-    select() {
-        this.resolveWith(this.selected);
-    }
-
-    cancel() {
-        this.resolveWith();
-    }
-}

+ 6 - 1
packages/admin-ui/src/lib/settings/src/public_api.ts

@@ -1,4 +1,5 @@
 // This file was generated by the build-public-api.ts script
+export * from './components/add-country-to-zone-dialog/add-country-to-zone-dialog.component';
 export * from './components/admin-detail/admin-detail.component';
 export * from './components/administrator-list/administrator-list.component';
 export * from './components/channel-detail/channel-detail.component';
@@ -23,7 +24,11 @@ export * from './components/tax-rate-detail/tax-rate-detail.component';
 export * from './components/tax-rate-list/tax-rate-list.component';
 export * from './components/test-address-form/test-address-form.component';
 export * from './components/test-order-builder/test-order-builder.component';
-export * from './components/zone-selector-dialog/zone-selector-dialog.component';
+export * from './components/zone-detail-dialog/zone-detail-dialog.component';
+export * from './components/zone-list/zone-list.component';
+export * from './components/zone-member-list/zone-member-controls.directive';
+export * from './components/zone-member-list/zone-member-list-header.directive';
+export * from './components/zone-member-list/zone-member-list.component';
 export * from './providers/routing/administrator-resolver';
 export * from './providers/routing/channel-resolver';
 export * from './providers/routing/country-resolver';

+ 12 - 2
packages/admin-ui/src/lib/settings/src/settings.module.ts

@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
 import { RouterModule } from '@angular/router';
 import { SharedModule } from '@vendure/admin-ui/core';
 
+import { AddCountryToZoneDialogComponent } from './components/add-country-to-zone-dialog/add-country-to-zone-dialog.component';
 import { AdminDetailComponent } from './components/admin-detail/admin-detail.component';
 import { AdministratorListComponent } from './components/administrator-list/administrator-list.component';
 import { ChannelDetailComponent } from './components/channel-detail/channel-detail.component';
@@ -26,7 +27,11 @@ import { TaxRateDetailComponent } from './components/tax-rate-detail/tax-rate-de
 import { TaxRateListComponent } from './components/tax-rate-list/tax-rate-list.component';
 import { TestAddressFormComponent } from './components/test-address-form/test-address-form.component';
 import { TestOrderBuilderComponent } from './components/test-order-builder/test-order-builder.component';
-import { ZoneSelectorDialogComponent } from './components/zone-selector-dialog/zone-selector-dialog.component';
+import { ZoneDetailDialogComponent } from './components/zone-detail-dialog/zone-detail-dialog.component';
+import { ZoneListComponent } from './components/zone-list/zone-list.component';
+import { ZoneMemberControlsDirective } from './components/zone-member-list/zone-member-controls.directive';
+import { ZoneMemberListHeaderDirective } from './components/zone-member-list/zone-member-list-header.directive';
+import { ZoneMemberListComponent } from './components/zone-member-list/zone-member-list.component';
 import { settingsRoutes } from './settings.routes';
 
 @NgModule({
@@ -41,7 +46,6 @@ import { settingsRoutes } from './settings.routes';
         PermissionGridComponent,
         CountryListComponent,
         CountryDetailComponent,
-        ZoneSelectorDialogComponent,
         TaxRateListComponent,
         TaxRateDetailComponent,
         ChannelListComponent,
@@ -57,6 +61,12 @@ import { settingsRoutes } from './settings.routes';
         ShippingEligibilityTestResultComponent,
         JobListComponent,
         JobStateLabelComponent,
+        ZoneListComponent,
+        AddCountryToZoneDialogComponent,
+        ZoneMemberListComponent,
+        ZoneMemberListHeaderDirective,
+        ZoneMemberControlsDirective,
+        ZoneDetailDialogComponent,
     ],
 })
 export class SettingsModule {}

+ 16 - 8
packages/admin-ui/src/lib/settings/src/settings.routes.ts

@@ -31,6 +31,7 @@ import { TaxCategoryDetailComponent } from './components/tax-category-detail/tax
 import { TaxCategoryListComponent } from './components/tax-category-list/tax-category-list.component';
 import { TaxRateDetailComponent } from './components/tax-rate-detail/tax-rate-detail.component';
 import { TaxRateListComponent } from './components/tax-rate-list/tax-rate-list.component';
+import { ZoneListComponent } from './components/zone-list/zone-list.component';
 import { AdministratorResolver } from './providers/routing/administrator-resolver';
 import { ChannelResolver } from './providers/routing/channel-resolver';
 import { CountryResolver } from './providers/routing/country-resolver';
@@ -132,6 +133,13 @@ export const settingsRoutes: Route[] = [
             breadcrumb: countryBreadcrumb,
         },
     },
+    {
+        path: 'zones',
+        component: ZoneListComponent,
+        data: {
+            breadcrumb: _('breadcrumb.zones'),
+        },
+    },
     {
         path: 'shipping-methods',
         component: ShippingMethodListComponent,
@@ -187,7 +195,7 @@ export function administratorBreadcrumb(data: any, params: any) {
         entity: data.entity,
         id: params.id,
         breadcrumbKey: 'breadcrumb.administrators',
-        getName: (admin) => `${admin.firstName} ${admin.lastName}`,
+        getName: admin => `${admin.firstName} ${admin.lastName}`,
         route: 'administrators',
     });
 }
@@ -197,7 +205,7 @@ export function channelBreadcrumb(data: any, params: any) {
         entity: data.entity,
         id: params.id,
         breadcrumbKey: 'breadcrumb.channels',
-        getName: (channel) => channel.code,
+        getName: channel => channel.code,
         route: 'channels',
     });
 }
@@ -207,7 +215,7 @@ export function roleBreadcrumb(data: any, params: any) {
         entity: data.entity,
         id: params.id,
         breadcrumbKey: 'breadcrumb.roles',
-        getName: (role) => role.description,
+        getName: role => role.description,
         route: 'roles',
     });
 }
@@ -217,7 +225,7 @@ export function taxCategoryBreadcrumb(data: any, params: any) {
         entity: data.entity,
         id: params.id,
         breadcrumbKey: 'breadcrumb.tax-categories',
-        getName: (category) => category.name,
+        getName: category => category.name,
         route: 'tax-categories',
     });
 }
@@ -227,7 +235,7 @@ export function taxRateBreadcrumb(data: any, params: any) {
         entity: data.entity,
         id: params.id,
         breadcrumbKey: 'breadcrumb.tax-rates',
-        getName: (category) => category.name,
+        getName: category => category.name,
         route: 'tax-rates',
     });
 }
@@ -237,7 +245,7 @@ export function countryBreadcrumb(data: any, params: any) {
         entity: data.entity,
         id: params.id,
         breadcrumbKey: 'breadcrumb.countries',
-        getName: (promotion) => promotion.name,
+        getName: promotion => promotion.name,
         route: 'countries',
     });
 }
@@ -247,7 +255,7 @@ export function shippingMethodBreadcrumb(data: any, params: any) {
         entity: data.entity,
         id: params.id,
         breadcrumbKey: 'breadcrumb.shipping-methods',
-        getName: (method) => method.description,
+        getName: method => method.description,
         route: 'shipping-methods',
     });
 }
@@ -257,7 +265,7 @@ export function paymentMethodBreadcrumb(data: any, params: any) {
         entity: data.entity,
         id: params.id,
         breadcrumbKey: 'breadcrumb.payment-methods',
-        getName: (method) => method.code,
+        getName: method => method.code,
         route: 'payment-methods',
     });
 }

+ 13 - 6
packages/admin-ui/src/lib/static/i18n-messages/en.json

@@ -42,7 +42,8 @@
     "roles": "Roles",
     "shipping-methods": "Shipping methods",
     "tax-categories": "Tax categories",
-    "tax-rates": "Tax rates"
+    "tax-rates": "Tax rates",
+    "zones": "Zones"
   },
   "catalog": {
     "add-facet-value": "Add facet value",
@@ -67,6 +68,7 @@
     "confirm-delete-product-variant": "Delete product variant?",
     "confirm-delete-promotion": "Delete promotion?",
     "confirm-delete-shipping-method": "Delete shipping method?",
+    "confirm-delete-zone": "Delete zone?",
     "create-new-collection": "Create new collection",
     "create-new-facet": "Create new facet",
     "create-new-product": "New product",
@@ -188,7 +190,8 @@
     "updated-at": "Updated at",
     "username": "Username",
     "view-next-month": "View next month",
-    "view-previous-month": "View previous month"
+    "view-previous-month": "View previous month",
+    "with-selected": "With selected..."
   },
   "customer": {
     "addresses": "Addresses",
@@ -484,7 +487,8 @@
     "settings": "Settings",
     "shipping-methods": "Shipping methods",
     "tax-categories": "Tax categories",
-    "tax-rates": "Tax Rates"
+    "tax-rates": "Tax Rates",
+    "zones": "Zones"
   },
   "order": {
     "add-note": "Add note",
@@ -578,7 +582,7 @@
     "unit-price": "Unit price"
   },
   "settings": {
-    "add-countries-to-zone": "Add countries to zone...",
+    "add-countries-to-zone": "Add countries to { zoneName }",
     "add-countries-to-zone-success": "Added { countryCount } {countryCount, plural, one {country} other {countries}} to zone \"{ zoneName }\"",
     "add-products-to-test-order": "Add products to the test order",
     "administrator": "Administrator",
@@ -596,6 +600,7 @@
     "create-new-shipping-method": "Create new shipping method",
     "create-new-tax-category": "Create tax category",
     "create-new-tax-rate": "Create new tax rate",
+    "create-new-zone": "Create new zone",
     "create-zone": "Create zone",
     "currency": "Currency",
     "customer": "Customer",
@@ -605,6 +610,7 @@
     "delete": "Delete",
     "elibigle": "Eligible",
     "email-address": "Email address",
+    "filter-by-member-name": "Filter by country",
     "first-name": "First name",
     "hide-settled-jobs": "Hide settled jobs",
     "job-data": "Job data",
@@ -623,13 +629,12 @@
     "promotion": "Promotion",
     "rate": "Rate",
     "read": "Read",
-    "remove-countries-from-zone": "Remove countries from zone...",
     "remove-countries-from-zone-success": "Removed { countryCount } {countryCount, plural, one {country} other {countries}} from zone \"{ zoneName }\"",
+    "remove-from-zone": "Remove from zone",
     "roles": "Roles",
     "search-by-product-name-or-sku": "Search by product name or SKU",
     "search-country-by-name": "Search countries by name",
     "section": "Section",
-    "select-zone": "Select zone",
     "settings": "Settings",
     "shipping-calculator": "Shipping calculator",
     "shipping-eligibility-checker": "Shipping eligibility checker",
@@ -643,6 +648,8 @@
     "test-shipping-methods": "Test shipping methods",
     "track-inventory-default": "Track inventory by default",
     "update": "Update",
+    "update-zone": "Update zone",
+    "view-zone-members": "View members",
     "zone": "Zone"
   }
 }