Browse Source

chore(admin-ui): Refactor all list views to use tabs

Michael Bromley 2 years ago
parent
commit
c60040354e
48 changed files with 1656 additions and 1440 deletions
  1. 18 0
      packages/admin-ui/src/lib/catalog/src/catalog.module.ts
  2. 6 3
      packages/admin-ui/src/lib/catalog/src/catalog.routes.ts
  3. 26 32
      packages/admin-ui/src/lib/catalog/src/components/asset-list/asset-list.component.html
  4. 5 0
      packages/admin-ui/src/lib/catalog/src/components/collection-contents/collection-contents.component.html
  5. 132 152
      packages/admin-ui/src/lib/catalog/src/components/collection-list/collection-list.component.html
  6. 38 7
      packages/admin-ui/src/lib/catalog/src/components/collection-list/collection-list.component.ts
  7. 95 103
      packages/admin-ui/src/lib/catalog/src/components/facet-list/facet-list.component.html
  8. 13 0
      packages/admin-ui/src/lib/catalog/src/components/facet-list/facet-list.component.ts
  9. 0 1
      packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.html
  10. 2 2
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  11. 1 0
      packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts
  12. 42 35
      packages/admin-ui/src/lib/core/src/shared/components/asset-search-input/asset-search-input.component.html
  13. 1 1
      packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table2.component.html
  14. 84 91
      packages/admin-ui/src/lib/marketing/src/components/promotion-list/promotion-list.component.html
  15. 15 0
      packages/admin-ui/src/lib/marketing/src/components/promotion-list/promotion-list.component.ts
  16. 21 5
      packages/admin-ui/src/lib/marketing/src/marketing.module.ts
  17. 6 2
      packages/admin-ui/src/lib/marketing/src/marketing.routes.ts
  18. 72 84
      packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.html
  19. 13 1
      packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.ts
  20. 23 5
      packages/admin-ui/src/lib/order/src/order.module.ts
  21. 6 2
      packages/admin-ui/src/lib/order/src/order.routes.ts
  22. 57 71
      packages/admin-ui/src/lib/settings/src/components/administrator-list/administrator-list.component.html
  23. 15 0
      packages/admin-ui/src/lib/settings/src/components/administrator-list/administrator-list.component.ts
  24. 57 71
      packages/admin-ui/src/lib/settings/src/components/channel-list/channel-list.component.html
  25. 15 0
      packages/admin-ui/src/lib/settings/src/components/channel-list/channel-list.component.ts
  26. 64 73
      packages/admin-ui/src/lib/settings/src/components/country-list/country-list.component.html
  27. 13 0
      packages/admin-ui/src/lib/settings/src/components/country-list/country-list.component.ts
  28. 65 74
      packages/admin-ui/src/lib/settings/src/components/payment-method-list/payment-method-list.component.html
  29. 13 0
      packages/admin-ui/src/lib/settings/src/components/payment-method-list/payment-method-list.component.ts
  30. 103 116
      packages/admin-ui/src/lib/settings/src/components/role-list/role-list.component.html
  31. 1 0
      packages/admin-ui/src/lib/settings/src/components/role-list/role-list.component.scss
  32. 10 0
      packages/admin-ui/src/lib/settings/src/components/role-list/role-list.component.ts
  33. 50 64
      packages/admin-ui/src/lib/settings/src/components/seller-list/seller-list.component.html
  34. 16 0
      packages/admin-ui/src/lib/settings/src/components/seller-list/seller-list.component.ts
  35. 68 110
      packages/admin-ui/src/lib/settings/src/components/shipping-method-list/shipping-method-list.component.html
  36. 0 3
      packages/admin-ui/src/lib/settings/src/components/shipping-method-list/shipping-method-list.component.scss
  37. 14 48
      packages/admin-ui/src/lib/settings/src/components/shipping-method-list/shipping-method-list.component.ts
  38. 57 73
      packages/admin-ui/src/lib/settings/src/components/tax-category-list/tax-category-list.component.html
  39. 15 0
      packages/admin-ui/src/lib/settings/src/components/tax-category-list/tax-category-list.component.ts
  40. 75 85
      packages/admin-ui/src/lib/settings/src/components/tax-rate-list/tax-rate-list.component.html
  41. 15 0
      packages/admin-ui/src/lib/settings/src/components/tax-rate-list/tax-rate-list.component.ts
  42. 17 0
      packages/admin-ui/src/lib/settings/src/components/test-shipping-methods/test-shipping-methods.component.html
  43. 9 0
      packages/admin-ui/src/lib/settings/src/components/test-shipping-methods/test-shipping-methods.component.scss
  44. 63 0
      packages/admin-ui/src/lib/settings/src/components/test-shipping-methods/test-shipping-methods.component.ts
  45. 96 109
      packages/admin-ui/src/lib/settings/src/components/zone-list/zone-list.component.html
  46. 13 1
      packages/admin-ui/src/lib/settings/src/components/zone-list/zone-list.component.ts
  47. 83 5
      packages/admin-ui/src/lib/settings/src/settings.module.ts
  48. 33 11
      packages/admin-ui/src/lib/settings/src/settings.routes.ts

+ 18 - 0
packages/admin-ui/src/lib/catalog/src/catalog.module.ts

@@ -127,5 +127,23 @@ export class CatalogModule {
             route: 'variants',
             component: ProductVariantListComponent,
         });
+        pageService.registerPageTab({
+            location: 'facet-list',
+            tab: _('catalog.facets'),
+            route: '',
+            component: FacetListComponent,
+        });
+        pageService.registerPageTab({
+            location: 'collection-list',
+            tab: _('catalog.facets'),
+            route: '',
+            component: CollectionListComponent,
+        });
+        pageService.registerPageTab({
+            location: 'asset-list',
+            tab: _('catalog.assets'),
+            route: '',
+            component: AssetListComponent,
+        });
     }
 }

+ 6 - 3
packages/admin-ui/src/lib/catalog/src/catalog.routes.ts

@@ -69,8 +69,9 @@ export const createRoutes = (pageService: PageService): Route[] => [
     },
     {
         path: 'facets',
-        component: FacetListComponent,
+        component: PageComponent,
         data: {
+            locationId: 'facet-list',
             breadcrumb: _('breadcrumb.facets'),
         },
         children: pageService.getPageTabRoutes('facet-list'),
@@ -87,8 +88,9 @@ export const createRoutes = (pageService: PageService): Route[] => [
     },
     {
         path: 'collections',
-        component: CollectionListComponent,
+        component: PageComponent,
         data: {
+            locationId: 'collection-list',
             breadcrumb: _('breadcrumb.collections'),
         },
         children: pageService.getPageTabRoutes('collection-list'),
@@ -105,8 +107,9 @@ export const createRoutes = (pageService: PageService): Route[] => [
     },
     {
         path: 'assets',
-        component: AssetListComponent,
+        component: PageComponent,
         data: {
+            locationId: 'asset-list',
             breadcrumb: _('breadcrumb.assets'),
         },
         children: pageService.getPageTabRoutes('asset-list'),

+ 26 - 32
packages/admin-ui/src/lib/catalog/src/components/asset-list/asset-list.component.html

@@ -1,40 +1,34 @@
-<vdr-page-header>
-    <vdr-page-title>
-        <vdr-action-bar-items locationId="asset-list"></vdr-action-bar-items>
+<vdr-page-block>
+    <vdr-asset-search-input
+        class="my-2"
+        [tags]="allTags$ | async"
+        (searchTermChange)="searchTerm$.next($event)"
+        (tagsChange)="filterByTags$.next($event)"
+    >
         <vdr-asset-file-input
             (selectFiles)="filesSelected($event)"
             [uploading]="uploading"
             dropZoneTarget=".content-area"
         ></vdr-asset-file-input>
-    </vdr-page-title>
-</vdr-page-header>
-<vdr-page-body>
-    <div class="ml-4">
-        <vdr-asset-search-input
-            class="my-2"
-            [tags]="allTags$ | async"
-            (searchTermChange)="searchTerm$.next($event)"
-            (tagsChange)="filterByTags$.next($event)"
-        ></vdr-asset-search-input>
-        <vdr-asset-gallery
-            [assets]="(items$ | async)! | paginate : (paginationConfig$ | async) || {}"
-            [multiSelect]="true"
-            [canDelete]="['DeleteCatalog', 'DeleteAsset'] | hasPermission"
-            (deleteAssets)="deleteAssets($event)"
-        ></vdr-asset-gallery>
+    </vdr-asset-search-input>
+    <vdr-asset-gallery
+        [assets]="(items$ | async)! | paginate : (paginationConfig$ | async) || {}"
+        [multiSelect]="true"
+        [canDelete]="['DeleteCatalog', 'DeleteAsset'] | hasPermission"
+        (deleteAssets)="deleteAssets($event)"
+    ></vdr-asset-gallery>
 
-        <div class="paging-controls">
-            <vdr-items-per-page-controls
-                [itemsPerPage]="itemsPerPage$ | async"
-                (itemsPerPageChange)="setItemsPerPage($event)"
-            ></vdr-items-per-page-controls>
+    <div class="paging-controls">
+        <vdr-items-per-page-controls
+            [itemsPerPage]="itemsPerPage$ | async"
+            (itemsPerPageChange)="setItemsPerPage($event)"
+        ></vdr-items-per-page-controls>
 
-            <vdr-pagination-controls
-                [currentPage]="currentPage$ | async"
-                [itemsPerPage]="itemsPerPage$ | async"
-                [totalItems]="totalItems$ | async"
-                (pageChange)="setPageNumber($event)"
-            ></vdr-pagination-controls>
-        </div>
+        <vdr-pagination-controls
+            [currentPage]="currentPage$ | async"
+            [itemsPerPage]="itemsPerPage$ | async"
+            [totalItems]="totalItems$ | async"
+            (pageChange)="setPageNumber($event)"
+        ></vdr-pagination-controls>
     </div>
-</vdr-page-body>
+</vdr-page-block>

+ 5 - 0
packages/admin-ui/src/lib/catalog/src/components/collection-contents/collection-contents.component.html

@@ -14,6 +14,11 @@
             [searchTermControl]="filterTermControl"
             [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
         />
+        <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
+            <ng-template let-variant="item">
+                {{ variant.id }}
+            </ng-template>
+        </vdr-dt2-column>
         <vdr-dt2-column [heading]="'common.created-at' | translate" [hiddenByDefault]="true">
             <ng-template let-variant="item">
                 {{ variant.createdAt | localeDate : 'short' }}

+ 132 - 152
packages/admin-ui/src/lib/catalog/src/components/collection-list/collection-list.component.html

@@ -1,155 +1,135 @@
-<vdr-page-header>
-    <vdr-page-title>
-        <vdr-action-bar-items locationId="collection-list"></vdr-action-bar-items>
-        <a
-            class="btn btn-primary"
-            *vdrIfPermissions="['CreateCatalog', 'CreateCollection']"
-            [routerLink]="['./create']"
-        >
-            <clr-icon shape="plus"></clr-icon>
-            {{ 'catalog.create-new-collection' | translate }}
-        </a>
-    </vdr-page-title>
-</vdr-page-header>
-<vdr-page-body>
-    <div class="flex center mt-2 mr-4" *ngIf="(availableLanguages$ | async)?.length > 1">
-        <vdr-language-selector
+<vdr-page-block>
+    <vdr-language-selector
+        class="mt-2"
+        [availableLanguageCodes]="availableLanguages$ | async"
+        [currentLanguageCode]="contentLanguage$ | async"
+        (languageCodeChange)="setLanguage($event)"
+    ></vdr-language-selector>
+</vdr-page-block>
+<vdr-split-view [rightPanelOpen]="activeCollectionId$ | async" (closeClicked)="closeContents()">
+    <ng-template vdrSplitViewLeft>
+        <vdr-collection-data-table
             class="mt-2"
-            [availableLanguageCodes]="availableLanguages$ | async"
-            [currentLanguageCode]="contentLanguage$ | async"
-            (languageCodeChange)="setLanguage($event)"
-        ></vdr-language-selector>
-    </div>
-    <vdr-split-view [rightPanelOpen]="activeCollectionId$ | async" (closeClicked)="closeContents()">
-        <ng-template vdrSplitViewLeft>
-            <vdr-collection-data-table
-                class="mt-2"
-                id="collection-list"
-                [items]="items$ | async"
-                [subCollections]="subCollections$ | async"
-                [itemsPerPage]="itemsPerPage$ | async"
-                [totalItems]="totalItems$ | async"
-                [currentPage]="currentPage$ | async"
-                [filters]="filters"
-                [activeIndex]="activeCollectionIndex$ | async"
-                (pageChange)="setPageNumber($event)"
-                (itemsPerPageChange)="setItemsPerPage($event)"
-                (changeOrder)="onRearrange($event)"
+            id="collection-list"
+            [items]="items$ | async"
+            [subCollections]="subCollections$ | async"
+            [itemsPerPage]="itemsPerPage$ | async"
+            [totalItems]="totalItems$ | async"
+            [currentPage]="currentPage$ | async"
+            [filters]="filters"
+            [activeIndex]="activeCollectionIndex$ | async"
+            (pageChange)="setPageNumber($event)"
+            (itemsPerPageChange)="setItemsPerPage($event)"
+            (changeOrder)="onRearrange($event)"
+        >
+            <vdr-bulk-action-menu
+                locationId="collection-list"
+                [hostComponent]="this"
+                [selectionManager]="selectionManager"
+            ></vdr-bulk-action-menu>
+            <vdr-dt2-search
+                [searchTermControl]="searchTermControl"
+                [searchTermPlaceholder]="'common.search-by-name' | translate"
+            ></vdr-dt2-search>
+            <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
+                <ng-template let-collection="item">
+                    {{ collection.id }}
+                </ng-template>
+            </vdr-dt2-column>
+            <vdr-dt2-column
+                [heading]="'common.created-at' | translate"
+                [hiddenByDefault]="true"
+                [sort]="sorts.get('createdAt')"
+            >
+                <ng-template let-collection="item">
+                    {{ collection.createdAt | localeDate : 'short' }}
+                </ng-template>
+            </vdr-dt2-column>
+            <vdr-dt2-column
+                [heading]="'common.updated-at' | translate"
+                [hiddenByDefault]="true"
+                [sort]="sorts.get('updatedAt')"
+            >
+                <ng-template let-collection="item">
+                    {{ collection.updatedAt | localeDate : 'short' }}
+                </ng-template>
+            </vdr-dt2-column>
+            <vdr-dt2-column
+                [heading]="'common.position' | translate"
+                [hiddenByDefault]="true"
+                [sort]="sorts.get('position')"
+            >
+                <ng-template let-collection="item">
+                    {{ collection.position }}
+                </ng-template>
+            </vdr-dt2-column>
+            <vdr-dt2-column
+                [heading]="'common.name' | translate"
+                [optional]="false"
+                [sort]="sorts.get('name')"
             >
-                <vdr-bulk-action-menu
-                    locationId="collection-list"
-                    [hostComponent]="this"
-                    [selectionManager]="selectionManager"
-                ></vdr-bulk-action-menu>
-                <vdr-dt2-search
-                    [searchTermControl]="searchTermControl"
-                    [searchTermPlaceholder]="'common.search-by-name' | translate"
-                ></vdr-dt2-search>
-                <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
-                    <ng-template let-collection="item">
-                        {{ collection.id }}
-                    </ng-template>
-                </vdr-dt2-column>
-                <vdr-dt2-column
-                    [heading]="'common.created-at' | translate"
-                    [hiddenByDefault]="true"
-                    [sort]="sorts.get('createdAt')"
-                >
-                    <ng-template let-collection="item">
-                        {{ collection.createdAt | localeDate : 'short' }}
-                    </ng-template>
-                </vdr-dt2-column>
-                <vdr-dt2-column
-                    [heading]="'common.updated-at' | translate"
-                    [hiddenByDefault]="true"
-                    [sort]="sorts.get('updatedAt')"
-                >
-                    <ng-template let-collection="item">
-                        {{ collection.updatedAt | localeDate : 'short' }}
-                    </ng-template>
-                </vdr-dt2-column>
-                <vdr-dt2-column
-                    [heading]="'common.position' | translate"
-                    [hiddenByDefault]="true"
-                    [sort]="sorts.get('position')"
-                >
-                    <ng-template let-collection="item">
-                        {{ collection.position }}
-                    </ng-template>
-                </vdr-dt2-column>
-                <vdr-dt2-column
-                    [heading]="'common.name' | translate"
-                    [optional]="false"
-                    [sort]="sorts.get('name')"
-                >
-                    <ng-template let-collection="item" let-depth="depth">
-                        <div [ngClass]="'indent-' + depth"></div>
-                        <clr-icon
-                            class="child-arrow"
-                            [class.transparent]="depth === 0"
-                            shape="child-arrow"
-                            *ngIf="!collection.children?.length"
-                        ></clr-icon>
-                        <button
-                            class="icon-button folder-button"
-                            *ngIf="collection.children?.length"
-                            (click)="toggleExpanded(collection)"
-                        >
-                            <clr-icon shape="folder" *ngIf="!expandedIds.includes(collection.id)"></clr-icon>
-                            <clr-icon
-                                shape="folder-open"
-                                *ngIf="expandedIds.includes(collection.id)"
-                            ></clr-icon>
-                        </button>
-                        <a class="button-ghost" [routerLink]="['./', collection.id]"
-                            ><span>{{ collection.name }}</span>
-                            <clr-icon shape="arrow right"></clr-icon>
-                        </a>
-                    </ng-template>
-                </vdr-dt2-column>
-                <vdr-dt2-column [heading]="'common.breadcrumb' | translate">
-                    <ng-template let-collection="item">
-                        <div class="breadcrumb">
-                            <ng-container *ngIf="collection | collectionBreadcrumb as breadcrumbs">
-                                <ng-container *ngIf="breadcrumbs.length">
-                                    <div *ngFor="let item of breadcrumbs">
-                                        <span class="separator">/</span>{{ item.name }}
-                                    </div>
-                                </ng-container>
-                                <span class="separator" *ngIf="!breadcrumbs.length">/</span>
+                <ng-template let-collection="item" let-depth="depth">
+                    <div [ngClass]="'indent-' + depth"></div>
+                    <clr-icon
+                        class="child-arrow"
+                        [class.transparent]="depth === 0"
+                        shape="child-arrow"
+                        *ngIf="!collection.children?.length"
+                    ></clr-icon>
+                    <button
+                        class="icon-button folder-button"
+                        *ngIf="collection.children?.length"
+                        (click)="toggleExpanded(collection)"
+                    >
+                        <clr-icon shape="folder" *ngIf="!expandedIds.includes(collection.id)"></clr-icon>
+                        <clr-icon shape="folder-open" *ngIf="expandedIds.includes(collection.id)"></clr-icon>
+                    </button>
+                    <a class="button-ghost" [routerLink]="['./', collection.id]"
+                        ><span>{{ collection.name }}</span>
+                        <clr-icon shape="arrow right"></clr-icon>
+                    </a>
+                </ng-template>
+            </vdr-dt2-column>
+            <vdr-dt2-column [heading]="'common.breadcrumb' | translate">
+                <ng-template let-collection="item">
+                    <div class="breadcrumb">
+                        <ng-container *ngIf="collection | collectionBreadcrumb as breadcrumbs">
+                            <ng-container *ngIf="breadcrumbs.length">
+                                <div *ngFor="let item of breadcrumbs">
+                                    <span class="separator">/</span>{{ item.name }}
+                                </div>
                             </ng-container>
-                        </div>
-                    </ng-template>
-                </vdr-dt2-column>
-                <vdr-dt2-column [heading]="'common.slug' | translate" [sort]="sorts.get('slug')">
-                    <ng-template let-collection="item">
-                        {{ collection.slug }}
-                    </ng-template>
-                </vdr-dt2-column>
-                <vdr-dt2-column [heading]="'common.view-contents' | translate" [optional]="false">
-                    <ng-template let-collection="item">
-                        <a
-                            class="button-small bg-weight-150"
-                            [routerLink]="['./', { contents: collection.id }]"
-                            queryParamsHandling="preserve"
-                        >
-                            <span>{{ 'common.view-contents' | translate }}</span>
-                            <clr-icon shape="file-group"></clr-icon>
-                        </a>
-                    </ng-template>
-                </vdr-dt2-column>
-                <vdr-dt2-custom-field-column
-                    *ngFor="let customField of customFields"
-                    [customField]="customField"
-                />
-            </vdr-collection-data-table>
-        </ng-template>
-        <ng-template vdrSplitViewRight [splitViewTitle]="activeCollectionTitle$ | async">
-            <ng-container *ngIf="activeCollectionId$ | async as activeGroup">
-                <vdr-collection-contents
-                    [collectionId]="activeCollectionId$ | async"
-                ></vdr-collection-contents>
-            </ng-container>
-        </ng-template>
-    </vdr-split-view>
-</vdr-page-body>
+                            <span class="separator" *ngIf="!breadcrumbs.length">/</span>
+                        </ng-container>
+                    </div>
+                </ng-template>
+            </vdr-dt2-column>
+            <vdr-dt2-column [heading]="'common.slug' | translate" [sort]="sorts.get('slug')">
+                <ng-template let-collection="item">
+                    {{ collection.slug }}
+                </ng-template>
+            </vdr-dt2-column>
+            <vdr-dt2-column [heading]="'common.view-contents' | translate" [optional]="false">
+                <ng-template let-collection="item">
+                    <a
+                        class="button-small bg-weight-150"
+                        [routerLink]="['./', { contents: collection.id }]"
+                        queryParamsHandling="preserve"
+                    >
+                        <span>{{ 'common.view-contents' | translate }}</span>
+                        <clr-icon shape="file-group"></clr-icon>
+                    </a>
+                </ng-template>
+            </vdr-dt2-column>
+            <vdr-dt2-custom-field-column
+                *ngFor="let customField of customFields"
+                [customField]="customField"
+            />
+        </vdr-collection-data-table>
+    </ng-template>
+    <ng-template vdrSplitViewRight [splitViewTitle]="activeCollectionTitle$ | async">
+        <ng-container *ngIf="activeCollectionId$ | async as activeGroup">
+            <vdr-collection-contents [collectionId]="activeCollectionId$ | async"></vdr-collection-contents>
+        </ng-container>
+    </ng-template>
+</vdr-split-view>

+ 38 - 7
packages/admin-ui/src/lib/catalog/src/components/collection-list/collection-list.component.ts

@@ -11,6 +11,7 @@ import {
     ItemOf,
     LanguageCode,
     ModalService,
+    NavBuilderService,
     NotificationService,
     ServerConfigService,
 } from '@vendure/admin-ui/core';
@@ -36,7 +37,6 @@ export class CollectionListComponent
     contentLanguage$: Observable<LanguageCode>;
     expandedIds: string[] = [];
     readonly customFields = this.serverConfigService.getCustomFieldsFor('Collection');
-
     readonly filters = this.dataTableService
         .createFilterCollection<CollectionFilterParameter>()
         .addDateFilters()
@@ -46,8 +46,8 @@ export class CollectionListComponent
             type: { kind: 'text' },
             filterField: 'slug',
         })
+        .addCustomFieldFilters(this.customFields)
         .connectToRoute(this.route);
-
     readonly sorts = this.dataTableService
         .createSortCollection<CollectionSortParameter>()
         .defaultSort('position', 'ASC')
@@ -56,6 +56,7 @@ export class CollectionListComponent
         .addSort({ name: 'name' })
         .addSort({ name: 'slug' })
         .addSort({ name: 'position' })
+        .addCustomFieldSorts(this.customFields)
         .connectToRoute(this.route);
 
     constructor(
@@ -64,11 +65,20 @@ export class CollectionListComponent
         private modalService: ModalService,
         router: Router,
         route: ActivatedRoute,
+        navBuilderService: NavBuilderService,
         private serverConfigService: ServerConfigService,
         private changeDetectorRef: ChangeDetectorRef,
         private dataTableService: DataTableService,
     ) {
         super(router, route);
+        navBuilderService.addActionBarItem({
+            id: 'create-collection',
+            label: _('catalog.create-new-collection'),
+            locationId: 'collection-list',
+            icon: 'plus',
+            routerLink: ['./create'],
+            requiresPermission: ['CreateCatalog', 'CreateCollection'],
+        });
         super.setQueryFn(
             (...args: any[]) => this.dataService.collection.getCollections().refetchOnChannelChange(),
             data => data.collections,
@@ -124,17 +134,38 @@ export class CollectionListComponent
             }),
         );
 
-        this.activeCollectionTitle$ = combineLatest(this.activeCollectionId$, this.items$).pipe(
-            map(([id, collections]) => {
+        this.activeCollectionTitle$ = combineLatest(
+            this.activeCollectionId$,
+            this.items$,
+            this.subCollections$,
+        ).pipe(
+            map(([id, collections, subCollections]) => {
                 if (id) {
-                    const match = collections.find(c => c.id === id);
+                    const match = [...collections, ...subCollections].find(c => c.id === id);
                     return match ? match.name : '';
                 }
                 return '';
             }),
         );
-        this.activeCollectionIndex$ = combineLatest(this.activeCollectionId$, this.items$).pipe(
-            map(([id, collections]) => (id ? collections.findIndex(c => c.id === id) : -1)),
+        this.activeCollectionIndex$ = combineLatest(
+            this.activeCollectionId$,
+            this.items$,
+            this.subCollections$,
+        ).pipe(
+            map(([id, collections, subCollections]) => {
+                if (id) {
+                    const allCollections: typeof collections = [];
+                    for (const collection of collections) {
+                        allCollections.push(collection);
+                        const subCollectionMatches = subCollections.filter(
+                            c => c.parentId && c.parentId === collection.id,
+                        );
+                        allCollections.push(...subCollectionMatches);
+                    }
+                    return allCollections.findIndex(c => c.id === id);
+                }
+                return -1;
+            }),
         );
         this.availableLanguages$ = this.serverConfigService.getAvailableLanguages();
         this.contentLanguage$ = this.dataService.client

+ 95 - 103
packages/admin-ui/src/lib/catalog/src/components/facet-list/facet-list.component.html

@@ -1,108 +1,100 @@
-<vdr-page-header>
-    <vdr-page-title>
-        <vdr-action-bar-items locationId="facet-list"></vdr-action-bar-items>
-        <a
-            class="btn btn-primary"
-            *vdrIfPermissions="['CreateCatalog', 'CreateFacet']"
-            [routerLink]="['./create']"
-        >
-            <clr-icon shape="plus"></clr-icon>
-            {{ 'catalog.create-new-facet' | translate }}
-        </a>
-    </vdr-page-title>
-</vdr-page-header>
-<vdr-page-body>
+<vdr-page-block>
     <vdr-language-selector
         [availableLanguageCodes]="availableLanguages$ | async"
         [currentLanguageCode]="contentLanguage$ | async"
         (languageCodeChange)="setLanguage($event)"
     ></vdr-language-selector>
-    <vdr-page-body>
-        <vdr-data-table-2
-            class="mt-2"
-            id="facet-list"
-            [items]="items$ | async"
-            [itemsPerPage]="itemsPerPage$ | async"
-            [totalItems]="totalItems$ | async"
-            [currentPage]="currentPage$ | async"
-            [filters]="filters"
-            (pageChange)="setPageNumber($event)"
-            (itemsPerPageChange)="setItemsPerPage($event)"
-        >
-            <vdr-bulk-action-menu
-                locationId="facet-list"
-                [hostComponent]="this"
-                [selectionManager]="selectionManager"
-            ></vdr-bulk-action-menu>
-            <vdr-dt2-search
-                [searchTermControl]="searchTermControl"
-                [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
-            ></vdr-dt2-search>
-            <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true" [sort]="sorts.get('id')">
-                <ng-template let-facet="item">
-                    {{ facet.id }}
-                </ng-template>
-            </vdr-dt2-column>
-            <vdr-dt2-column [heading]="'common.created-at' | translate" [hiddenByDefault]="true" [sort]="sorts.get('createdAt')">
-                <ng-template let-facet="item">
-                    {{ facet.createdAt | localeDate : 'short' }}
-                </ng-template>
-            </vdr-dt2-column>
-            <vdr-dt2-column [heading]="'common.updated-at' | translate" [hiddenByDefault]="true" [sort]="sorts.get('updatedAt')">
-                <ng-template let-facet="item">
-                    {{ facet.updatedAt | localeDate : 'short' }}
-                </ng-template>
-            </vdr-dt2-column>
-            <vdr-dt2-column
-                [heading]="'common.name' | translate"
-                [optional]="false"
-                [sort]="sorts.get('name')"
-            >
-                <ng-template let-facet="item">
-                    <a class="button-ghost" [routerLink]="['./', facet.id]"
-                        ><span>{{ facet.name }}</span>
-                        <clr-icon shape="arrow right"></clr-icon>
-                    </a>
-                </ng-template>
-            </vdr-dt2-column>
-            <vdr-dt2-column [heading]="'common.code' | translate">
-                <ng-template let-facet="item">
-                    {{ facet.code }}
-                </ng-template>
-            </vdr-dt2-column>
-            <vdr-dt2-column [heading]="'common.visibility' | translate">
-                <ng-template let-facet="item">
-                    <vdr-chip  *ngIf="facet.isPrivate" colorType="warning">{{ 'common.private' | translate }}</vdr-chip>
-                    <vdr-chip  *ngIf="!facet.isPrivate" colorType="success">{{ 'common.public' | translate }}</vdr-chip>
-                </ng-template>
-            </vdr-dt2-column>
-            <vdr-dt2-column [heading]="'catalog.values' | translate">
-                <ng-template let-facet="item">
-                    <div class="facet-values-list">
-                        <vdr-facet-value-chip
-                            *ngFor="let value of facet.values | slice : 0 : displayLimit[facet.id] || 3"
-                            [facetValue]="value"
-                            [removable]="false"
-                            [displayFacetName]="false"
-                        ></vdr-facet-value-chip>
-                        <button
-                            class="button-small"
-                            *ngIf="facet.values.length > initialLimit"
-                            (click)="toggleDisplayLimit(facet)"
-                        >
-                            <ng-container
-                                *ngIf="(displayLimit[facet.id] || 0) < facet.values.length; else collapse"
-                            >
-                                <clr-icon shape="plus"></clr-icon>
-                                {{ facet.values.length - initialLimit }}
-                            </ng-container>
-                            <ng-template #collapse>
-                                <clr-icon shape="minus"></clr-icon>
-                            </ng-template>
-                        </button>
-                    </div>
-                </ng-template>
-            </vdr-dt2-column>
-        </vdr-data-table-2>
-    </vdr-page-body>
-</vdr-page-body>
+</vdr-page-block>
+<vdr-data-table-2
+    class="mt-2"
+    id="facet-list"
+    [items]="items$ | async"
+    [itemsPerPage]="itemsPerPage$ | async"
+    [totalItems]="totalItems$ | async"
+    [currentPage]="currentPage$ | async"
+    [filters]="filters"
+    (pageChange)="setPageNumber($event)"
+    (itemsPerPageChange)="setItemsPerPage($event)"
+>
+    <vdr-bulk-action-menu
+        locationId="facet-list"
+        [hostComponent]="this"
+        [selectionManager]="selectionManager"
+    ></vdr-bulk-action-menu>
+    <vdr-dt2-search
+        [searchTermControl]="searchTermControl"
+        [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
+    ></vdr-dt2-search>
+    <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true" [sort]="sorts.get('id')">
+        <ng-template let-facet="item">
+            {{ facet.id }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.created-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('createdAt')"
+    >
+        <ng-template let-facet="item">
+            {{ facet.createdAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.updated-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('updatedAt')"
+    >
+        <ng-template let-facet="item">
+            {{ facet.updatedAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.name' | translate" [optional]="false" [sort]="sorts.get('name')">
+        <ng-template let-facet="item">
+            <a class="button-ghost" [routerLink]="['./', facet.id]"
+                ><span>{{ facet.name }}</span>
+                <clr-icon shape="arrow right"></clr-icon>
+            </a>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.code' | translate">
+        <ng-template let-facet="item">
+            {{ facet.code }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.visibility' | translate">
+        <ng-template let-facet="item">
+            <vdr-chip *ngIf="facet.isPrivate" colorType="warning">{{
+                'common.private' | translate
+            }}</vdr-chip>
+            <vdr-chip *ngIf="!facet.isPrivate" colorType="success">{{
+                'common.public' | translate
+            }}</vdr-chip>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'catalog.values' | translate">
+        <ng-template let-facet="item">
+            <div class="facet-values-list">
+                <vdr-facet-value-chip
+                    *ngFor="let value of facet.values | slice : 0 : displayLimit[facet.id] || 3"
+                    [facetValue]="value"
+                    [removable]="false"
+                    [displayFacetName]="false"
+                ></vdr-facet-value-chip>
+                <button
+                    class="button-small"
+                    *ngIf="facet.values.length > initialLimit"
+                    (click)="toggleDisplayLimit(facet)"
+                >
+                    <ng-container *ngIf="(displayLimit[facet.id] || 0) < facet.values.length; else collapse">
+                        <clr-icon shape="plus"></clr-icon>
+                        {{ facet.values.length - initialLimit }}
+                    </ng-container>
+                    <ng-template #collapse>
+                        <clr-icon shape="minus"></clr-icon>
+                    </ng-template>
+                </button>
+            </div>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-custom-field-column *ngFor="let customField of customFields" [customField]="customField" />
+</vdr-data-table-2>

+ 13 - 0
packages/admin-ui/src/lib/catalog/src/components/facet-list/facet-list.component.ts

@@ -11,6 +11,7 @@ import {
     ItemOf,
     LanguageCode,
     ModalService,
+    NavBuilderService,
     NotificationService,
     ServerConfigService,
 } from '@vendure/admin-ui/core';
@@ -30,6 +31,7 @@ export class FacetListComponent
     readonly initialLimit = 3;
     displayLimit: { [id: string]: number } = {};
 
+    readonly customFields = this.serverConfigService.getCustomFieldsFor('Facet');
     readonly filters = this.dataTableService
         .createFilterCollection<FacetFilterParameter>()
         .addDateFilters()
@@ -41,6 +43,7 @@ export class FacetListComponent
                 isPrivate: { eq: !value },
             }),
         })
+        .addCustomFieldFilters(this.customFields)
         .connectToRoute(this.route);
 
     readonly sorts = this.dataTableService
@@ -51,6 +54,7 @@ export class FacetListComponent
         .addSort({ name: 'updatedAt' })
         .addSort({ name: 'name' })
         .addSort({ name: 'code' })
+        .addCustomFieldSorts(this.customFields)
         .connectToRoute(this.route);
 
     constructor(
@@ -59,10 +63,19 @@ export class FacetListComponent
         private notificationService: NotificationService,
         private serverConfigService: ServerConfigService,
         private dataTableService: DataTableService,
+        navBuilderService: NavBuilderService,
         router: Router,
         route: ActivatedRoute,
     ) {
         super(router, route);
+        navBuilderService.addActionBarItem({
+            id: 'create-facet',
+            label: _('catalog.create-new-facet'),
+            locationId: 'facet-list',
+            icon: 'plus',
+            routerLink: ['./create'],
+            requiresPermission: ['CreateCatalog', 'CreateFacet'],
+        });
         super.setQueryFn(
             (...args: any[]) => this.dataService.facet.getFacets(...args).refetchOnChannelChange(),
             data => data.facets,

+ 0 - 1
packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.html

@@ -4,7 +4,6 @@
         [currentLanguageCode]="contentLanguage$ | async"
         (languageCodeChange)="setLanguage($event)"
     ></vdr-language-selector>
-    some content
 </vdr-page-block>
 <vdr-data-table-2
     class="mt-2"

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

@@ -6688,7 +6688,7 @@ export type GetCollectionContentsQueryVariables = Exact<{
 }>;
 
 
-export type GetCollectionContentsQuery = { collection?: { __typename?: 'Collection', id: string, name: string, productVariants: { __typename?: 'ProductVariantList', totalItems: number, items: Array<{ __typename?: 'ProductVariant', id: string, productId: string, name: string, sku: string }> } } | null };
+export type GetCollectionContentsQuery = { collection?: { __typename?: 'Collection', id: string, name: string, productVariants: { __typename?: 'ProductVariantList', totalItems: number, items: Array<{ __typename?: 'ProductVariant', id: string, createdAt: any, updatedAt: any, productId: string, name: string, sku: string }> } } | null };
 
 export type PreviewCollectionContentsQueryVariables = Exact<{
   input: PreviewCollectionVariantsInput;
@@ -7571,7 +7571,7 @@ export type GetCountryListQueryVariables = Exact<{
 }>;
 
 
-export type GetCountryListQuery = { countries: { __typename?: 'CountryList', totalItems: number, items: Array<{ __typename?: 'Country', id: string, createdAt: any, updatedAt: any, code: string, name: string, enabled: boolean }> } };
+export type GetCountryListQuery = { countries: { __typename?: 'CountryList', totalItems: number, items: Array<{ __typename?: 'Country', id: string, createdAt: any, updatedAt: any, code: string, name: string, type: string, enabled: boolean }> } };
 
 export type GetAvailableCountriesQueryVariables = Exact<{ [key: string]: never; }>;
 

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

@@ -31,6 +31,7 @@ export const GET_COUNTRY_LIST = gql`
                 updatedAt
                 code
                 name
+                type
                 enabled
             }
             totalItems

+ 42 - 35
packages/admin-ui/src/lib/core/src/shared/components/asset-search-input/asset-search-input.component.html

@@ -1,35 +1,42 @@
-<ng-select
-    [addTag]="addTagFn"
-    [placeholder]="'catalog.search-asset-name-or-tag' | translate"
-    [items]="tags"
-    [searchFn]="filterTagResults"
-    [hideSelected]="true"
-    [multiple]="true"
-    [markFirst]="false"
-    (change)="onSelectChange($event)"
-    #selectComponent
->
-    <ng-template ng-header-tmp>
-        <div
-            class="search-header"
-            *ngIf="selectComponent.searchTerm"
-            [class.selected]="isSearchHeaderSelected()"
-            (click)="selectComponent.selectTag()"
-        >
-            {{ 'catalog.search-for-term' | translate }}: {{ selectComponent.searchTerm }}
-        </div>
-    </ng-template>
-    <ng-template ng-label-tmp let-item="item" let-clear="clear">
-        <ng-container *ngIf="item.value">
-            <vdr-chip [colorFrom]="item.value" icon="close" (iconClick)="clear(item)"><clr-icon shape="tag" class="mr2"></clr-icon> {{ item.value }}</vdr-chip>
-        </ng-container>
-        <ng-container *ngIf="!item.value">
-            <vdr-chip [icon]="'times'" (iconClick)="clear(item)">"{{ item.label || item }}"</vdr-chip>
-        </ng-container>
-    </ng-template>
-    <ng-template ng-option-tmp let-item="item" let-index="index" let-search="searchTerm">
-        <ng-container *ngIf="item.value">
-            <vdr-chip [colorFrom]="item.value"><clr-icon shape="tag" class="mr2"></clr-icon> {{ item.value }}</vdr-chip>
-        </ng-container>
-    </ng-template>
-</ng-select>
+<div class="flex center">
+    <ng-select
+        [addTag]="addTagFn"
+        [placeholder]="'catalog.search-asset-name-or-tag' | translate"
+        [items]="tags"
+        [searchFn]="filterTagResults"
+        [hideSelected]="true"
+        [multiple]="true"
+        [markFirst]="false"
+        (change)="onSelectChange($event)"
+        #selectComponent
+    >
+        <ng-template ng-header-tmp>
+            <div
+                class="search-header"
+                *ngIf="selectComponent.searchTerm"
+                [class.selected]="isSearchHeaderSelected()"
+                (click)="selectComponent.selectTag()"
+            >
+                {{ 'catalog.search-for-term' | translate }}: {{ selectComponent.searchTerm }}
+            </div>
+        </ng-template>
+        <ng-template ng-label-tmp let-item="item" let-clear="clear">
+            <ng-container *ngIf="item.value">
+                <vdr-chip [colorFrom]="item.value" icon="close" (iconClick)="clear(item)"
+                    ><clr-icon shape="tag" class="mr2"></clr-icon> {{ item.value }}</vdr-chip
+                >
+            </ng-container>
+            <ng-container *ngIf="!item.value">
+                <vdr-chip [icon]="'times'" (iconClick)="clear(item)">"{{ item.label || item }}"</vdr-chip>
+            </ng-container>
+        </ng-template>
+        <ng-template ng-option-tmp let-item="item" let-index="index" let-search="searchTerm">
+            <ng-container *ngIf="item.value">
+                <vdr-chip [colorFrom]="item.value"
+                    ><clr-icon shape="tag" class="mr2"></clr-icon> {{ item.value }}</vdr-chip
+                >
+            </ng-container>
+        </ng-template>
+    </ng-select>
+    <ng-content></ng-content>
+</div>

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table2.component.html

@@ -110,7 +110,7 @@
                         ></ng-container>
                     </div>
                 </td>
-                <td><!-- column select --></td>
+                <td [class.active]="activeIndex === i"><!-- column select --></td>
             </tr>
             <ng-container>
                 <tr *ngIf="!items?.length">

+ 84 - 91
packages/admin-ui/src/lib/marketing/src/components/promotion-list/promotion-list.component.html

@@ -1,92 +1,85 @@
-<vdr-page-header>
-    <vdr-page-title>
-        <vdr-action-bar-items locationId="promotion-list"></vdr-action-bar-items>
-        <a class="btn btn-primary" *vdrIfPermissions="'CreatePromotion'" [routerLink]="['./create']">
-            <clr-icon shape="plus"></clr-icon>
-            {{ 'marketing.create-new-promotion' | translate }}
-        </a>
-    </vdr-page-title>
-</vdr-page-header>
-
-<vdr-page-body>
-    <vdr-data-table-2
-        class="mt-2"
-        id="promotion-list"
-        [items]="items$ | async"
-        [itemsPerPage]="itemsPerPage$ | async"
-        [totalItems]="totalItems$ | async"
-        [currentPage]="currentPage$ | async"
-        [filters]="filters"
-        (pageChange)="setPageNumber($event)"
-        (itemsPerPageChange)="setItemsPerPage($event)"
+<vdr-data-table-2
+    class="mt-2"
+    id="promotion-list"
+    [items]="items$ | async"
+    [itemsPerPage]="itemsPerPage$ | async"
+    [totalItems]="totalItems$ | async"
+    [currentPage]="currentPage$ | async"
+    [filters]="filters"
+    (pageChange)="setPageNumber($event)"
+    (itemsPerPageChange)="setItemsPerPage($event)"
+>
+    <vdr-bulk-action-menu
+        locationId="promotion-list"
+        [hostComponent]="this"
+        [selectionManager]="selectionManager"
+    />
+    <vdr-dt2-search
+        [searchTermControl]="searchTermControl"
+        [searchTermPlaceholder]="'marketing.search-by-name-or-coupon-code' | translate"
+    />
+    <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
+        <ng-template let-promotion="item">{{ promotion.id }}</ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.created-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('createdAt')"
     >
-        <vdr-bulk-action-menu
-            locationId="promotion-list"
-            [hostComponent]="this"
-            [selectionManager]="selectionManager"
-        />
-        <vdr-dt2-search
-            [searchTermControl]="searchTermControl"
-            [searchTermPlaceholder]="'marketing.search-by-name-or-coupon-code' | translate"
-        />
-        <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
-            <ng-template let-promotion="item">{{ promotion.id }}</ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.created-at' | translate"
-            [hiddenByDefault]="true"
-            [sort]="sorts.get('createdAt')"
-        >
-            <ng-template let-promotion="item">
-                {{ promotion.createdAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.updated-at' | translate"
-            [hiddenByDefault]="true"
-            [sort]="sorts.get('updatedAt')"
-        >
-            <ng-template let-promotion="item">
-                {{ promotion.updatedAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'common.name' | translate" [optional]="false" [sort]="sorts.get('name')">
-            <ng-template let-promotion="item">
-                <a class="button-ghost" [routerLink]="['./', promotion.id]"
-                    ><span> {{ promotion.name }}</span>
-                    <clr-icon shape="arrow right"></clr-icon>
-                </a>
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'common.enabled' | translate">
-            <ng-template let-promotion="item">
-                <vdr-chip  *ngIf="promotion.enabled" colorType="success">{{ 'common.enabled' | translate }}</vdr-chip>
-                <vdr-chip  *ngIf="!promotion.enabled" colorType="warning">{{ 'common.disabled' | translate }}</vdr-chip>
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'marketing.coupon-code' | translate" [sort]="sorts.get('couponCode')">
-            <ng-template let-promotion="item">
-                {{ promotion.couponCode }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'marketing.starts-at' | translate" [sort]="sorts.get('startsAt')">
-            <ng-template let-promotion="item">
-                {{ promotion.startsAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'marketing.ends-at' | translate" [sort]="sorts.get('endsAt')">
-            <ng-template let-promotion="item">
-                {{ promotion.endsAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'marketing.per-customer-limit' | translate"
-            [sort]="sorts.get('perCustomerUsageLimit')"
-            [hiddenByDefault]="true"
-        >
-            <ng-template let-promotion="item">
-                {{ promotion.perCustomerUsageLimit }}
-            </ng-template>
-        </vdr-dt2-column>
-    </vdr-data-table-2>
-</vdr-page-body>
+        <ng-template let-promotion="item">
+            {{ promotion.createdAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.updated-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('updatedAt')"
+    >
+        <ng-template let-promotion="item">
+            {{ promotion.updatedAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.name' | translate" [optional]="false" [sort]="sorts.get('name')">
+        <ng-template let-promotion="item">
+            <a class="button-ghost" [routerLink]="['./', promotion.id]"
+                ><span> {{ promotion.name }}</span>
+                <clr-icon shape="arrow right"></clr-icon>
+            </a>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.enabled' | translate">
+        <ng-template let-promotion="item">
+            <vdr-chip *ngIf="promotion.enabled" colorType="success">{{
+                'common.enabled' | translate
+            }}</vdr-chip>
+            <vdr-chip *ngIf="!promotion.enabled" colorType="warning">{{
+                'common.disabled' | translate
+            }}</vdr-chip>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'marketing.coupon-code' | translate" [sort]="sorts.get('couponCode')">
+        <ng-template let-promotion="item">
+            {{ promotion.couponCode }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'marketing.starts-at' | translate" [sort]="sorts.get('startsAt')">
+        <ng-template let-promotion="item">
+            {{ promotion.startsAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'marketing.ends-at' | translate" [sort]="sorts.get('endsAt')">
+        <ng-template let-promotion="item">
+            {{ promotion.endsAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'marketing.per-customer-limit' | translate"
+        [sort]="sorts.get('perCustomerUsageLimit')"
+        [hiddenByDefault]="true"
+    >
+        <ng-template let-promotion="item">
+            {{ promotion.perCustomerUsageLimit }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-custom-field-column *ngFor="let customField of customFields" [customField]="customField" />
+</vdr-data-table-2>

+ 15 - 0
packages/admin-ui/src/lib/marketing/src/components/promotion-list/promotion-list.component.ts

@@ -8,9 +8,11 @@ import {
     GetPromotionListQuery,
     ItemOf,
     LogicalOperator,
+    NavBuilderService,
     PromotionFilterParameter,
     PromotionListOptions,
     PromotionSortParameter,
+    ServerConfigService,
 } from '@vendure/admin-ui/core';
 
 export type PromotionSearchForm = {
@@ -28,6 +30,7 @@ export class PromotionListComponent
     extends BaseListComponent<GetPromotionListQuery, ItemOf<GetPromotionListQuery, 'promotions'>>
     implements OnInit
 {
+    readonly customFields = this.serverConfigService.getCustomFieldsFor('Promotion');
     readonly filters = this.dataTableService
         .createFilterCollection<PromotionFilterParameter>()
         .addDateFilters()
@@ -73,6 +76,7 @@ export class PromotionListComponent
             label: _('marketing.per-customer-limit'),
             filterField: 'perCustomerUsageLimit',
         })
+        .addCustomFieldFilters(this.customFields)
         .connectToRoute(this.route);
 
     readonly sorts = this.dataTableService
@@ -85,15 +89,26 @@ export class PromotionListComponent
         .addSort({ name: 'name' })
         .addSort({ name: 'couponCode' })
         .addSort({ name: 'perCustomerUsageLimit' })
+        .addCustomFieldSorts(this.customFields)
         .connectToRoute(this.route);
 
     constructor(
         router: Router,
         route: ActivatedRoute,
+        navBuilderService: NavBuilderService,
+        private serverConfigService: ServerConfigService,
         private dataService: DataService,
         private dataTableService: DataTableService,
     ) {
         super(router, route);
+        navBuilderService.addActionBarItem({
+            id: 'create-promotion',
+            label: _('marketing.create-new-promotion'),
+            locationId: 'promotion-list',
+            icon: 'plus',
+            routerLink: ['./create'],
+            requiresPermission: ['CreatePromotion'],
+        });
         super.setQueryFn(
             (...args: any[]) => this.dataService.promotion.getPromotions(...args).refetchOnChannelChange(),
             data => data.promotions,

+ 21 - 5
packages/admin-ui/src/lib/marketing/src/marketing.module.ts

@@ -1,18 +1,34 @@
 import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-import { BulkActionRegistryService, SharedModule } from '@vendure/admin-ui/core';
+import { RouterModule, ROUTES } from '@angular/router';
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
+import { BulkActionRegistryService, PageService, SharedModule } from '@vendure/admin-ui/core';
 
 import { PromotionDetailComponent } from './components/promotion-detail/promotion-detail.component';
 import { deletePromotionsBulkAction } from './components/promotion-list/promotion-list-bulk-actions';
 import { PromotionListComponent } from './components/promotion-list/promotion-list.component';
-import { marketingRoutes } from './marketing.routes';
+import { createRoutes } from './marketing.routes';
 
 @NgModule({
-    imports: [SharedModule, RouterModule.forChild(marketingRoutes)],
+    imports: [SharedModule, RouterModule.forChild([])],
+    providers: [
+        {
+            provide: ROUTES,
+            useFactory: (pageService: PageService) => createRoutes(pageService),
+            multi: true,
+            deps: [PageService],
+        },
+    ],
     declarations: [PromotionListComponent, PromotionDetailComponent],
 })
 export class MarketingModule {
-    constructor(private bulkActionRegistryService: BulkActionRegistryService) {
+    constructor(private bulkActionRegistryService: BulkActionRegistryService, pageService: PageService) {
         bulkActionRegistryService.registerBulkAction(deletePromotionsBulkAction);
+
+        pageService.registerPageTab({
+            location: 'promotion-list',
+            tab: _('breadcrumb.promotions'),
+            route: '',
+            component: PromotionListComponent,
+        });
     }
 }

+ 6 - 2
packages/admin-ui/src/lib/marketing/src/marketing.routes.ts

@@ -4,6 +4,8 @@ import {
     CanDeactivateDetailGuard,
     createResolveData,
     detailBreadcrumb,
+    PageComponent,
+    PageService,
     PromotionFragment,
 } from '@vendure/admin-ui/core';
 
@@ -11,13 +13,15 @@ import { PromotionDetailComponent } from './components/promotion-detail/promotio
 import { PromotionListComponent } from './components/promotion-list/promotion-list.component';
 import { PromotionResolver } from './providers/routing/promotion-resolver';
 
-export const marketingRoutes: Route[] = [
+export const createRoutes = (pageService: PageService): Route[] => [
     {
         path: 'promotions',
-        component: PromotionListComponent,
+        component: PageComponent,
         data: {
+            locationId: 'promotion-list',
             breadcrumb: _('breadcrumb.promotions'),
         },
+        children: pageService.getPageTabRoutes('promotion-list'),
     },
     {
         path: 'promotions/:id',

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

@@ -1,85 +1,73 @@
-<vdr-page-header>
-    <vdr-page-title>
-        <vdr-action-bar-items locationId="order-list"></vdr-action-bar-items>
-        <ng-container *ngIf="canCreateDraftOrder">
-            <a class="btn" *vdrIfPermissions="['CreateOrder']" [routerLink]="['./draft/create']">
-                <clr-icon shape="plus"></clr-icon>
-                {{ 'catalog.create-draft-order' | translate }}
+<vdr-data-table-2
+    class="mt-2"
+    id="order-list"
+    [items]="items$ | async"
+    [itemsPerPage]="itemsPerPage$ | async"
+    [totalItems]="totalItems$ | async"
+    [currentPage]="currentPage$ | async"
+    [filters]="filters"
+    (pageChange)="setPageNumber($event)"
+    (itemsPerPageChange)="setItemsPerPage($event)"
+>
+    <vdr-bulk-action-menu
+        locationId="order-list"
+        [hostComponent]="this"
+        [selectionManager]="selectionManager"
+    ></vdr-bulk-action-menu>
+    <vdr-dt2-search
+        [searchTermControl]="searchTermControl"
+        [searchTermPlaceholder]="'order.search-by-order-filters' | translate"
+    />
+    <vdr-dt2-column [heading]="'common.created-at' | translate" [hiddenByDefault]="true">
+        <ng-template let-order="item">
+            {{ order.createdAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.code' | translate" [optional]="false">
+        <ng-template let-order="item">
+            <a class="button-ghost" [routerLink]="['./', order.id]"
+                ><span>{{ order.code }}</span>
+                <clr-icon shape="arrow right"></clr-icon>
             </a>
-        </ng-container>
-    </vdr-page-title>
-</vdr-page-header>
-<vdr-page-body>
-    <vdr-data-table-2
-        class="mt-2"
-        id="order-list"
-        [items]="items$ | async"
-        [itemsPerPage]="itemsPerPage$ | async"
-        [totalItems]="totalItems$ | async"
-        [currentPage]="currentPage$ | async"
-        [filters]="filters"
-        (pageChange)="setPageNumber($event)"
-        (itemsPerPageChange)="setItemsPerPage($event)"
-    >
-        <vdr-bulk-action-menu
-            locationId="order-list"
-            [hostComponent]="this"
-            [selectionManager]="selectionManager"
-        ></vdr-bulk-action-menu>
-        <vdr-dt2-search
-            [searchTermControl]="searchTermControl"
-            [searchTermPlaceholder]="'order.search-by-order-filters' | translate"
-        />
-        <vdr-dt2-column [heading]="'common.created-at' | translate" [hiddenByDefault]="true">
-            <ng-template let-order="item">
-                {{ order.createdAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'common.code' | translate" [optional]="false">
-            <ng-template let-order="item">
-                <a class="button-ghost" [routerLink]="['./', order.id]"
-                    ><span>{{ order.code }}</span>
-                    <clr-icon shape="arrow right"></clr-icon>
-                </a>
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'order.customer' | translate" [sort]="sorts.get('customerLastName')">
-            <ng-template let-order="item">
-                <vdr-customer-label
-                    [customer]="order.customer"
-                    (click)="$event.stopPropagation()"
-                ></vdr-customer-label>
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'order.order-type' | translate" [hiddenByDefault]="true">
-            <ng-template let-order="item">
-                <vdr-chip>{{ order.type }}</vdr-chip>
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'order.state' | translate" [sort]="sorts.get('state')">
-            <ng-template let-order="item">
-                <vdr-order-state-label [state]="order.state"></vdr-order-state-label>
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'order.total' | translate" [sort]="sorts.get('totalWithTax')">
-            <ng-template let-order="item">
-                {{ order.totalWithTax | localeCurrency : order.currencyCode }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'common.updated-at' | translate">
-            <ng-template let-order="item">
-                {{ order.updatedAt | timeAgo }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'order.placed-at' | translate" [sort]="sorts.get('orderPlacedAt')">
-            <ng-template let-order="item">
-                {{ order.orderPlacedAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'order.shipping' | translate">
-            <ng-template let-order="item">
-                {{ getShippingNames(order) }}
-            </ng-template>
-        </vdr-dt2-column>
-    </vdr-data-table-2>
-</vdr-page-body>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'order.customer' | translate" [sort]="sorts.get('customerLastName')">
+        <ng-template let-order="item">
+            <vdr-customer-label
+                [customer]="order.customer"
+                (click)="$event.stopPropagation()"
+            ></vdr-customer-label>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'order.order-type' | translate" [hiddenByDefault]="true">
+        <ng-template let-order="item">
+            <vdr-chip>{{ order.type }}</vdr-chip>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'order.state' | translate" [sort]="sorts.get('state')">
+        <ng-template let-order="item">
+            <vdr-order-state-label [state]="order.state"></vdr-order-state-label>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'order.total' | translate" [sort]="sorts.get('totalWithTax')">
+        <ng-template let-order="item">
+            {{ order.totalWithTax | localeCurrency : order.currencyCode }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.updated-at' | translate">
+        <ng-template let-order="item">
+            {{ order.updatedAt | timeAgo }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'order.placed-at' | translate" [sort]="sorts.get('orderPlacedAt')">
+        <ng-template let-order="item">
+            {{ order.orderPlacedAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'order.shipping' | translate">
+        <ng-template let-order="item">
+            {{ getShippingNames(order) }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-custom-field-column *ngFor="let customField of customFields" [customField]="customField" />
+</vdr-data-table-2>

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

@@ -10,6 +10,7 @@ import {
     getOrderStateTranslationToken,
     ItemOf,
     LocalStorageService,
+    NavBuilderService,
     OrderFilterParameter,
     OrderListOptions,
     OrderSortParameter,
@@ -30,7 +31,7 @@ export class OrderListComponent
     implements OnInit
 {
     orderStates = this.serverConfigService.getOrderProcessStates().map(item => item.name);
-
+    readonly customFields = this.serverConfigService.getCustomFieldsFor('Order');
     readonly filters = this.dataTableService
         .createFilterCollection<OrderFilterParameter>()
         .addDateFilters()
@@ -73,6 +74,7 @@ export class OrderListComponent
             label: _('order.transaction-id'),
             filterField: 'transactionId',
         })
+        .addCustomFieldFilters(this.customFields)
         .connectToRoute(this.route);
 
     readonly sorts = this.dataTableService
@@ -84,6 +86,7 @@ export class OrderListComponent
         .addSort({ name: 'customerLastName' })
         .addSort({ name: 'state' })
         .addSort({ name: 'totalWithTax' })
+        .addCustomFieldSorts(this.customFields)
         .connectToRoute(this.route);
 
     canCreateDraftOrder = false;
@@ -95,10 +98,19 @@ export class OrderListComponent
         private localStorageService: LocalStorageService,
         private channelService: ChannelService,
         private dataTableService: DataTableService,
+        navBuilderService: NavBuilderService,
         router: Router,
         route: ActivatedRoute,
     ) {
         super(router, route);
+        navBuilderService.addActionBarItem({
+            id: 'create-draft-order',
+            label: _('catalog.create-draft-order'),
+            locationId: 'order-list',
+            icon: 'plus',
+            routerLink: ['./draft/create'],
+            requiresPermission: ['CreateOrder'],
+        });
         super.setQueryFn(
             // eslint-disable-next-line @typescript-eslint/no-shadow
             (take, skip) => this.dataService.order.getOrders({ take, skip }).refetchOnChannelChange(),

+ 23 - 5
packages/admin-ui/src/lib/order/src/order.module.ts

@@ -1,6 +1,7 @@
 import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-import { SharedModule } from '@vendure/admin-ui/core';
+import { RouterModule, ROUTES } from '@angular/router';
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
+import { PageService, SharedModule } from '@vendure/admin-ui/core';
 
 import { AddManualPaymentDialogComponent } from './components/add-manual-payment-dialog/add-manual-payment-dialog.component';
 import { CancelOrderDialogComponent } from './components/cancel-order-dialog/cancel-order-dialog.component';
@@ -38,10 +39,18 @@ import { SelectShippingMethodDialogComponent } from './components/select-shippin
 import { SellerOrdersCardComponent } from './components/seller-orders-card/seller-orders-card.component';
 import { SettleRefundDialogComponent } from './components/settle-refund-dialog/settle-refund-dialog.component';
 import { SimpleItemListComponent } from './components/simple-item-list/simple-item-list.component';
-import { orderRoutes } from './order.routes';
+import { createRoutes } from './order.routes';
 
 @NgModule({
-    imports: [SharedModule, RouterModule.forChild(orderRoutes)],
+    imports: [SharedModule, RouterModule.forChild([])],
+    providers: [
+        {
+            provide: ROUTES,
+            useFactory: (pageService: PageService) => createRoutes(pageService),
+            multi: true,
+            deps: [PageService],
+        },
+    ],
     declarations: [
         OrderListComponent,
         OrderDetailComponent,
@@ -82,4 +91,13 @@ import { orderRoutes } from './order.routes';
     ],
     exports: [OrderCustomFieldsCardComponent],
 })
-export class OrderModule {}
+export class OrderModule {
+    constructor(private pageService: PageService) {
+        pageService.registerPageTab({
+            location: 'order-list',
+            tab: _('orders.orders'),
+            route: '',
+            component: OrderListComponent,
+        });
+    }
+}

+ 6 - 2
packages/admin-ui/src/lib/order/src/order.routes.ts

@@ -5,6 +5,8 @@ import {
     CanDeactivateDetailGuard,
     detailBreadcrumb,
     OrderDetailFragment,
+    PageComponent,
+    PageService,
 } from '@vendure/admin-ui/core';
 import { map } from 'rxjs/operators';
 
@@ -15,13 +17,15 @@ import { OrderListComponent } from './components/order-list/order-list.component
 import { OrderResolver } from './providers/routing/order-resolver';
 import { OrderGuard } from './providers/routing/order.guard';
 
-export const orderRoutes: Route[] = [
+export const createRoutes = (pageService: PageService): Route[] => [
     {
         path: '',
-        component: OrderListComponent,
+        component: PageComponent,
         data: {
+            locationId: 'order-list',
             breadcrumb: _('breadcrumb.orders'),
         },
+        children: pageService.getPageTabRoutes('order-list'),
     },
     {
         path: 'draft/:id',

+ 57 - 71
packages/admin-ui/src/lib/settings/src/components/administrator-list/administrator-list.component.html

@@ -1,72 +1,58 @@
-<vdr-page-header>
-    <vdr-page-title>
-        <vdr-action-bar-items locationId="administrator-list"></vdr-action-bar-items>
-        <a class="btn btn-primary" [routerLink]="['./create']" *vdrIfPermissions="'CreateAdministrator'">
-            <clr-icon shape="plus"></clr-icon>
-            {{ 'admin.create-new-administrator' | translate }}
-        </a>
-    </vdr-page-title>
-</vdr-page-header>
-<vdr-page-body>
-    <vdr-data-table-2
-        class="mt-2"
-        id="administrator-list"
-        [items]="items$ | async"
-        [itemsPerPage]="itemsPerPage$ | async"
-        [totalItems]="totalItems$ | async"
-        [currentPage]="currentPage$ | async"
-        [filters]="filters"
-        (pageChange)="setPageNumber($event)"
-        (itemsPerPageChange)="setItemsPerPage($event)"
+<vdr-data-table-2
+    class="mt-2"
+    id="administrator-list"
+    [items]="items$ | async"
+    [itemsPerPage]="itemsPerPage$ | async"
+    [totalItems]="totalItems$ | async"
+    [currentPage]="currentPage$ | async"
+    [filters]="filters"
+    (pageChange)="setPageNumber($event)"
+    (itemsPerPageChange)="setItemsPerPage($event)"
+>
+    <vdr-bulk-action-menu
+        locationId="administrator-list"
+        [hostComponent]="this"
+        [selectionManager]="selectionManager"
+    />
+    <vdr-dt2-search
+        [searchTermControl]="searchTermControl"
+        [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
+    />
+    <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
+        <ng-template let-administrator="item">
+            {{ administrator.id }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.created-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('createdAt')"
     >
-        <vdr-bulk-action-menu
-            locationId="administrator-list"
-            [hostComponent]="this"
-            [selectionManager]="selectionManager"
-        />
-        <vdr-dt2-search
-            [searchTermControl]="searchTermControl"
-            [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
-        />
-        <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
-            <ng-template let-administrator="item">
-                {{ administrator.id }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.created-at' | translate"
-            [hiddenByDefault]="true"
-            [sort]="sorts.get('createdAt')"
-        >
-            <ng-template let-administrator="item">
-                {{ administrator.createdAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.updated-at' | translate"
-            [hiddenByDefault]="true"
-            [sort]="sorts.get('updatedAt')"
-        >
-            <ng-template let-administrator="item">
-                {{ administrator.updatedAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.name' | translate"
-            [optional]="false"
-            [sort]="sorts.get('lastName')"
-        >
-            <ng-template let-administrator="item">
-                <a class="button-ghost" [routerLink]="['./', administrator.id]"
-                    ><span>{{ administrator.firstName }} {{ administrator.lastName }}</span>
-                    <clr-icon shape="arrow right"></clr-icon>
-                </a>
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'settings.email-address' | translate" [sort]="sorts.get('emailAddress')">
-            <ng-template let-administrator="item">
-                {{ administrator.emailAddress }}
-            </ng-template>
-        </vdr-dt2-column>
-    </vdr-data-table-2>
-</vdr-page-body>
+        <ng-template let-administrator="item">
+            {{ administrator.createdAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.updated-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('updatedAt')"
+    >
+        <ng-template let-administrator="item">
+            {{ administrator.updatedAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.name' | translate" [optional]="false" [sort]="sorts.get('lastName')">
+        <ng-template let-administrator="item">
+            <a class="button-ghost" [routerLink]="['./', administrator.id]"
+                ><span>{{ administrator.firstName }} {{ administrator.lastName }}</span>
+                <clr-icon shape="arrow right"></clr-icon>
+            </a>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'settings.email-address' | translate" [sort]="sorts.get('emailAddress')">
+        <ng-template let-administrator="item">
+            {{ administrator.emailAddress }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-custom-field-column *ngFor="let customField of customFields" [customField]="customField" />
+</vdr-data-table-2>

+ 15 - 0
packages/admin-ui/src/lib/settings/src/components/administrator-list/administrator-list.component.ts

@@ -11,7 +11,9 @@ import {
     ItemOf,
     LogicalOperator,
     ModalService,
+    NavBuilderService,
     NotificationService,
+    ServerConfigService,
 } from '@vendure/admin-ui/core';
 import { EMPTY } from 'rxjs';
 import { switchMap } from 'rxjs/operators';
@@ -25,6 +27,7 @@ export class AdministratorListComponent
     extends BaseListComponent<GetAdministratorsQuery, ItemOf<GetAdministratorsQuery, 'administrators'>>
     implements OnInit
 {
+    readonly customFields = this.serverConfigService.getCustomFieldsFor('Administrator');
     readonly filters = this.dataTableService
         .createFilterCollection<AdministratorFilterParameter>()
         .addDateFilters()
@@ -46,6 +49,7 @@ export class AdministratorListComponent
             label: _('settings.email-address'),
             filterField: 'emailAddress',
         })
+        .addCustomFieldFilters(this.customFields)
         .connectToRoute(this.route);
 
     readonly sorts = this.dataTableService
@@ -55,17 +59,28 @@ export class AdministratorListComponent
         .addSort({ name: 'updatedAt' })
         .addSort({ name: 'lastName' })
         .addSort({ name: 'emailAddress' })
+        .addCustomFieldSorts(this.customFields)
         .connectToRoute(this.route);
 
     constructor(
         private dataService: DataService,
         router: Router,
         route: ActivatedRoute,
+        navBuilderService: NavBuilderService,
         private modalService: ModalService,
         private notificationService: NotificationService,
         private dataTableService: DataTableService,
+        private serverConfigService: ServerConfigService,
     ) {
         super(router, route);
+        navBuilderService.addActionBarItem({
+            id: 'create-administrator',
+            label: _('admin.create-new-administrator'),
+            locationId: 'administrator-list',
+            icon: 'plus',
+            routerLink: ['./create'],
+            requiresPermission: ['CreateAdministrator'],
+        });
         super.setQueryFn(
             (...args: any[]) => this.dataService.administrator.getAdministrators(...args),
             data => data.administrators,

+ 57 - 71
packages/admin-ui/src/lib/settings/src/components/channel-list/channel-list.component.html

@@ -1,72 +1,58 @@
-<vdr-page-header>
-    <vdr-page-title>
-        <vdr-action-bar-items locationId="channel-list"></vdr-action-bar-items>
-        <a
-            class="btn btn-primary"
-            [routerLink]="['./create']"
-            *vdrIfPermissions="['SuperAdmin', 'CreateChannel']"
-        >
-            <clr-icon shape="plus"></clr-icon>
-            {{ 'settings.create-new-channel' | translate }}
-        </a>
-    </vdr-page-title>
-</vdr-page-header>
-<vdr-page-body>
-    <vdr-data-table-2
-        class="mt-2"
-        id="channel-list"
-        [items]="items$ | async"
-        [itemsPerPage]="itemsPerPage$ | async"
-        [totalItems]="totalItems$ | async"
-        [currentPage]="currentPage$ | async"
-        [filters]="filters"
-        (pageChange)="setPageNumber($event)"
-        (itemsPerPageChange)="setItemsPerPage($event)"
+<vdr-data-table-2
+    class="mt-2"
+    id="channel-list"
+    [items]="items$ | async"
+    [itemsPerPage]="itemsPerPage$ | async"
+    [totalItems]="totalItems$ | async"
+    [currentPage]="currentPage$ | async"
+    [filters]="filters"
+    (pageChange)="setPageNumber($event)"
+    (itemsPerPageChange)="setItemsPerPage($event)"
+>
+    <vdr-bulk-action-menu
+        locationId="channel-list"
+        [hostComponent]="this"
+        [selectionManager]="selectionManager"
+    />
+    <vdr-dt2-search
+        [searchTermControl]="searchTermControl"
+        [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
+    />
+    <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
+        <ng-template let-channel="item">
+            {{ channel.id }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.created-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('createdAt')"
     >
-        <vdr-bulk-action-menu
-            locationId="channel-list"
-            [hostComponent]="this"
-            [selectionManager]="selectionManager"
-        />
-        <vdr-dt2-search
-            [searchTermControl]="searchTermControl"
-            [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
-        />
-        <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
-            <ng-template let-channel="item">
-                {{ channel.id }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.created-at' | translate"
-            [hiddenByDefault]="true"
-            [sort]="sorts.get('createdAt')"
-        >
-            <ng-template let-channel="item">
-                {{ channel.createdAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.updated-at' | translate"
-            [hiddenByDefault]="true"
-            [sort]="sorts.get('updatedAt')"
-        >
-            <ng-template let-channel="item">
-                {{ channel.updatedAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'common.code' | translate" [optional]="false" [sort]="sorts.get('code')">
-            <ng-template let-channel="item">
-                <a class="button-ghost" [routerLink]="['./', channel.id]"
-                    ><span>{{ channel.code | channelCodeToLabel | translate }}</span>
-                    <clr-icon shape="arrow right"></clr-icon>
-                </a>
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'settings.channel-token' | translate" [sort]="sorts.get('token')">
-            <ng-template let-channel="item">
-                {{ channel.token }}
-            </ng-template>
-        </vdr-dt2-column>
-    </vdr-data-table-2>
-</vdr-page-body>
+        <ng-template let-channel="item">
+            {{ channel.createdAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.updated-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('updatedAt')"
+    >
+        <ng-template let-channel="item">
+            {{ channel.updatedAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.code' | translate" [optional]="false" [sort]="sorts.get('code')">
+        <ng-template let-channel="item">
+            <a class="button-ghost" [routerLink]="['./', channel.id]"
+                ><span>{{ channel.code | channelCodeToLabel | translate }}</span>
+                <clr-icon shape="arrow right"></clr-icon>
+            </a>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'settings.channel-token' | translate" [sort]="sorts.get('token')">
+        <ng-template let-channel="item">
+            {{ channel.token }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-custom-field-column *ngFor="let customField of customFields" [customField]="customField" />
+</vdr-data-table-2>

+ 15 - 0
packages/admin-ui/src/lib/settings/src/components/channel-list/channel-list.component.ts

@@ -10,7 +10,9 @@ import {
     GetChannelsQuery,
     ItemOf,
     ModalService,
+    NavBuilderService,
     NotificationService,
+    ServerConfigService,
 } from '@vendure/admin-ui/core';
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 
@@ -24,6 +26,7 @@ export class ChannelListComponent
     extends BaseListComponent<GetChannelsQuery, ItemOf<GetChannelsQuery, 'channels'>>
     implements OnInit
 {
+    readonly customFields = this.serverConfigService.getCustomFieldsFor('Channel');
     readonly filters = this.dataTableService
         .createFilterCollection<ChannelFilterParameter>()
         .addDateFilters()
@@ -39,6 +42,7 @@ export class ChannelListComponent
             label: _('settings.channel-token'),
             filterField: 'token',
         })
+        .addCustomFieldFilters(this.customFields)
         .connectToRoute(this.route);
 
     readonly sorts = this.dataTableService
@@ -48,6 +52,7 @@ export class ChannelListComponent
         .addSort({ name: 'updatedAt' })
         .addSort({ name: 'code' })
         .addSort({ name: 'token' })
+        .addCustomFieldSorts(this.customFields)
         .connectToRoute(this.route);
 
     constructor(
@@ -56,9 +61,19 @@ export class ChannelListComponent
         private notificationService: NotificationService,
         route: ActivatedRoute,
         router: Router,
+        navBuilderService: NavBuilderService,
+        private serverConfigService: ServerConfigService,
         private dataTableService: DataTableService,
     ) {
         super(router, route);
+        navBuilderService.addActionBarItem({
+            id: 'create-channel',
+            label: _('settings.create-new-channel'),
+            locationId: 'channel-list',
+            icon: 'plus',
+            routerLink: ['./create'],
+            requiresPermission: ['SuperAdmin', 'CreateChannel'],
+        });
         super.setQueryFn(
             (...args: any[]) => this.dataService.settings.getChannels(...args).refetchOnChannelChange(),
             data => data.channels,

+ 64 - 73
packages/admin-ui/src/lib/settings/src/components/country-list/country-list.component.html

@@ -1,79 +1,70 @@
-<vdr-page-header>
-    <vdr-page-title>
-        <vdr-action-bar-items locationId="country-list"></vdr-action-bar-items>
-        <a
-            class="btn btn-primary"
-            [routerLink]="['./create']"
-            *vdrIfPermissions="['CreateSettings', 'CreateCountry']"
-        >
-            <clr-icon shape="plus"></clr-icon>
-            {{ 'settings.create-new-country' | translate }}
-        </a>
-    </vdr-page-title>
-</vdr-page-header>
-<vdr-page-body>
+<vdr-page-block>
     <vdr-language-selector
-        class="mx-4 my-2"
         [availableLanguageCodes]="availableLanguages$ | async"
         [currentLanguageCode]="contentLanguage$ | async"
         (languageCodeChange)="setLanguage($event)"
     ></vdr-language-selector>
-    <vdr-data-table-2
-        class="mt-2"
-        id="country-list"
-        [items]="items$ | async"
-        [itemsPerPage]="itemsPerPage$ | async"
-        [totalItems]="totalItems$ | async"
-        [currentPage]="currentPage$ | async"
-        [filters]="filters"
-        (pageChange)="setPageNumber($event)"
-        (itemsPerPageChange)="setItemsPerPage($event)"
+</vdr-page-block>
+<vdr-data-table-2
+    class="mt-2"
+    id="country-list"
+    [items]="items$ | async"
+    [itemsPerPage]="itemsPerPage$ | async"
+    [totalItems]="totalItems$ | async"
+    [currentPage]="currentPage$ | async"
+    [filters]="filters"
+    (pageChange)="setPageNumber($event)"
+    (itemsPerPageChange)="setItemsPerPage($event)"
+>
+    <vdr-bulk-action-menu
+        locationId="country-list"
+        [hostComponent]="this"
+        [selectionManager]="selectionManager"
+    />
+    <vdr-dt2-search
+        [searchTermControl]="searchTermControl"
+        [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
+    />
+    <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
+        <ng-template let-country="item">
+            {{ country.id }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.created-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('createdAt')"
     >
-        <vdr-bulk-action-menu
-            locationId="country-list"
-            [hostComponent]="this"
-            [selectionManager]="selectionManager"
-        />
-        <vdr-dt2-search
-            [searchTermControl]="searchTermControl"
-            [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
-        />
-        <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
-            <ng-template let-country="item">
-                {{ country.id }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.created-at' | translate"
-            [hiddenByDefault]="true"
-            [sort]="sorts.get('createdAt')"
-        >
-            <ng-template let-country="item">
-                {{ country.createdAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.updated-at' | translate"
-            [hiddenByDefault]="true"
-            [sort]="sorts.get('updatedAt')"
-        >
-            <ng-template let-country="item">
-                {{ country.updatedAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'common.name' | translate" [optional]="false" [sort]="sorts.get('name')">
-            <ng-template let-country="item">
-                <a class="button-ghost" [routerLink]="['./', country.id]"
-                    ><span>{{ country.name }}</span>
-                    <clr-icon shape="arrow right"></clr-icon>
-                </a>
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'common.enabled' | translate">
-            <ng-template let-country="item">
-                <vdr-chip  *ngIf="country.enabled" colorType="success">{{ 'common.enabled' | translate }}</vdr-chip>
-                <vdr-chip  *ngIf="!country.enabled" colorType="warning">{{ 'common.disabled' | translate }}</vdr-chip>
-            </ng-template>
-        </vdr-dt2-column>
-    </vdr-data-table-2>
-</vdr-page-body>
+        <ng-template let-country="item">
+            {{ country.createdAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.updated-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('updatedAt')"
+    >
+        <ng-template let-country="item">
+            {{ country.updatedAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.name' | translate" [optional]="false" [sort]="sorts.get('name')">
+        <ng-template let-country="item">
+            <a class="button-ghost" [routerLink]="['./', country.id]"
+                ><span>{{ country.name }}</span>
+                <clr-icon shape="arrow right"></clr-icon>
+            </a>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.enabled' | translate">
+        <ng-template let-country="item">
+            <vdr-chip *ngIf="country.enabled" colorType="success">{{
+                'common.enabled' | translate
+            }}</vdr-chip>
+            <vdr-chip *ngIf="!country.enabled" colorType="warning">{{
+                'common.disabled' | translate
+            }}</vdr-chip>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-custom-field-column *ngFor="let customField of customFields" [customField]="customField" />
+</vdr-data-table-2>

+ 13 - 0
packages/admin-ui/src/lib/settings/src/components/country-list/country-list.component.ts

@@ -10,6 +10,7 @@ import {
     GetCountryListQuery,
     ItemOf,
     LanguageCode,
+    NavBuilderService,
     ServerConfigService,
 } from '@vendure/admin-ui/core';
 import { Observable } from 'rxjs';
@@ -27,6 +28,7 @@ export class CountryListComponent
     availableLanguages$: Observable<LanguageCode[]>;
     contentLanguage$: Observable<LanguageCode>;
 
+    readonly customFields = this.serverConfigService.getCustomFieldsFor('Region');
     readonly filters = this.dataTableService
         .createFilterCollection<CountryFilterParameter>()
         .addDateFilters()
@@ -42,6 +44,7 @@ export class CountryListComponent
             label: _('common.enabled'),
             filterField: 'enabled',
         })
+        .addCustomFieldFilters(this.customFields)
         .connectToRoute(this.route);
 
     readonly sorts = this.dataTableService
@@ -50,16 +53,26 @@ export class CountryListComponent
         .addSort({ name: 'createdAt' })
         .addSort({ name: 'updatedAt' })
         .addSort({ name: 'name' })
+        .addCustomFieldSorts(this.customFields)
         .connectToRoute(this.route);
 
     constructor(
         route: ActivatedRoute,
         router: Router,
+        navBuilderService: NavBuilderService,
         private dataService: DataService,
         private serverConfigService: ServerConfigService,
         private dataTableService: DataTableService,
     ) {
         super(router, route);
+        navBuilderService.addActionBarItem({
+            id: 'create-country',
+            label: _('settings.create-new-country'),
+            locationId: 'country-list',
+            icon: 'plus',
+            routerLink: ['./create'],
+            requiresPermission: ['CreateSettings', 'CreateCountry'],
+        });
         super.setQueryFn(
             (...args: any[]) => this.dataService.settings.getCountries(...args).refetchOnChannelChange(),
             data => data.countries,

+ 65 - 74
packages/admin-ui/src/lib/settings/src/components/payment-method-list/payment-method-list.component.html

@@ -1,79 +1,70 @@
-<vdr-page-header>
-    <vdr-page-title>
-        <vdr-action-bar-items locationId="payment-method-list"></vdr-action-bar-items>
-        <a
-            class="btn btn-primary"
-            [routerLink]="['./create']"
-            *vdrIfPermissions="['CreateSettings', 'CreatePaymentMethod']"
-        >
-            <clr-icon shape="plus"></clr-icon>
-            {{ 'settings.create-new-payment-method' | translate }}
-        </a>
-    </vdr-page-title>
-</vdr-page-header>
-<vdr-page-body>
+<vdr-page-block>
     <vdr-language-selector
-        class="mx-4 my-2"
         [availableLanguageCodes]="availableLanguages$ | async"
         [currentLanguageCode]="contentLanguage$ | async"
         (languageCodeChange)="setLanguage($event)"
-    ></vdr-language-selector>
-    <vdr-data-table-2
-        class="mt-2"
-        id="payment-method-list"
-        [items]="items$ | async"
-        [itemsPerPage]="itemsPerPage$ | async"
-        [totalItems]="totalItems$ | async"
-        [currentPage]="currentPage$ | async"
-        [filters]="filters"
-        (pageChange)="setPageNumber($event)"
-        (itemsPerPageChange)="setItemsPerPage($event)"
+    ></vdr-language-selector
+></vdr-page-block>
+<vdr-data-table-2
+    class="mt-2"
+    id="payment-method-list"
+    [items]="items$ | async"
+    [itemsPerPage]="itemsPerPage$ | async"
+    [totalItems]="totalItems$ | async"
+    [currentPage]="currentPage$ | async"
+    [filters]="filters"
+    (pageChange)="setPageNumber($event)"
+    (itemsPerPageChange)="setItemsPerPage($event)"
+>
+    <vdr-bulk-action-menu
+        locationId="paymentMethod-list"
+        [hostComponent]="this"
+        [selectionManager]="selectionManager"
+    ></vdr-bulk-action-menu>
+    <vdr-dt2-search
+        [searchTermControl]="searchTermControl"
+        [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
+    ></vdr-dt2-search>
+    <vdr-dt2-column
+        [heading]="'common.created-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('createdAt')"
     >
-        <vdr-bulk-action-menu
-            locationId="paymentMethod-list"
-            [hostComponent]="this"
-            [selectionManager]="selectionManager"
-        ></vdr-bulk-action-menu>
-        <vdr-dt2-search
-            [searchTermControl]="searchTermControl"
-            [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
-        ></vdr-dt2-search>
-        <vdr-dt2-column
-            [heading]="'common.created-at' | translate"
-            [hiddenByDefault]="true"
-            [sort]="sorts.get('createdAt')"
-        >
-            <ng-template let-paymentMethod="item">
-                {{ paymentMethod.createdAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.updated-at' | translate"
-            [hiddenByDefault]="true"
-            [sort]="sorts.get('updatedAt')"
-        >
-            <ng-template let-paymentMethod="item">
-                {{ paymentMethod.updatedAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'common.name' | translate" [optional]="false" [sort]="sorts.get('name')">
-            <ng-template let-paymentMethod="item">
-                <a class="button-ghost" [routerLink]="['./', paymentMethod.id]"
-                    ><span>{{ paymentMethod.name }}</span>
-                    <clr-icon shape="arrow right"></clr-icon>
-                </a>
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'common.code' | translate" [sort]="sorts.get('code')">
-            <ng-template let-paymentMethod="item">
-                {{ paymentMethod.code }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'common.enabled' | translate">
-            <ng-template let-paymentMethod="item">
-                <vdr-chip  *ngIf="paymentMethod.enabled" colorType="success">{{ 'common.enabled' | translate }}</vdr-chip>
-                <vdr-chip  *ngIf="!paymentMethod.enabled" colorType="warning">{{ 'common.disabled' | translate }}</vdr-chip>
-            </ng-template>
-        </vdr-dt2-column>
-    </vdr-data-table-2>
-</vdr-page-body>
+        <ng-template let-paymentMethod="item">
+            {{ paymentMethod.createdAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.updated-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('updatedAt')"
+    >
+        <ng-template let-paymentMethod="item">
+            {{ paymentMethod.updatedAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.name' | translate" [optional]="false" [sort]="sorts.get('name')">
+        <ng-template let-paymentMethod="item">
+            <a class="button-ghost" [routerLink]="['./', paymentMethod.id]"
+                ><span>{{ paymentMethod.name }}</span>
+                <clr-icon shape="arrow right"></clr-icon>
+            </a>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.code' | translate" [sort]="sorts.get('code')">
+        <ng-template let-paymentMethod="item">
+            {{ paymentMethod.code }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.enabled' | translate">
+        <ng-template let-paymentMethod="item">
+            <vdr-chip *ngIf="paymentMethod.enabled" colorType="success">{{
+                'common.enabled' | translate
+            }}</vdr-chip>
+            <vdr-chip *ngIf="!paymentMethod.enabled" colorType="warning">{{
+                'common.disabled' | translate
+            }}</vdr-chip>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-custom-field-column *ngFor="let customField of customFields" [customField]="customField" />
+</vdr-data-table-2>

+ 13 - 0
packages/admin-ui/src/lib/settings/src/components/payment-method-list/payment-method-list.component.ts

@@ -8,6 +8,7 @@ import {
     GetPaymentMethodListQuery,
     ItemOf,
     LanguageCode,
+    NavBuilderService,
     PaymentMethodFilterParameter,
     PaymentMethodSortParameter,
     ServerConfigService,
@@ -26,6 +27,7 @@ export class PaymentMethodListComponent
 {
     availableLanguages$: Observable<LanguageCode[]>;
     contentLanguage$: Observable<LanguageCode>;
+    readonly customFields = this.serverConfigService.getCustomFieldsFor('PaymentMethod');
     readonly filters = this.dataTableService
         .createFilterCollection<PaymentMethodFilterParameter>()
         .addDateFilters()
@@ -53,6 +55,7 @@ export class PaymentMethodListComponent
             label: _('common.description'),
             filterField: 'description',
         })
+        .addCustomFieldFilters(this.customFields)
         .connectToRoute(this.route);
 
     readonly sorts = this.dataTableService
@@ -63,16 +66,26 @@ export class PaymentMethodListComponent
         .addSort({ name: 'name' })
         .addSort({ name: 'code' })
         .addSort({ name: 'description' })
+        .addCustomFieldSorts(this.customFields)
         .connectToRoute(this.route);
 
     constructor(
         router: Router,
         route: ActivatedRoute,
+        navBuilderService: NavBuilderService,
         private dataService: DataService,
         private dataTableService: DataTableService,
         private serverConfigService: ServerConfigService,
     ) {
         super(router, route);
+        navBuilderService.addActionBarItem({
+            id: 'create-payment-method',
+            label: _('settings.create-new-payment-method'),
+            locationId: 'payment-method-list',
+            icon: 'plus',
+            routerLink: ['./create'],
+            requiresPermission: ['CreateSettings', 'CreatePaymentMethod'],
+        });
         super.setQueryFn(
             (...args: any[]) => this.dataService.settings.getPaymentMethods(...args).refetchOnChannelChange(),
             data => data.paymentMethods,

+ 103 - 116
packages/admin-ui/src/lib/settings/src/components/role-list/role-list.component.html

@@ -1,120 +1,107 @@
-<vdr-page-header>
-    <vdr-page-title>
-        <vdr-action-bar-items locationId="role-list"></vdr-action-bar-items>
-        <a class="btn btn-primary" [routerLink]="['./create']" *vdrIfPermissions="'CreateAdministrator'">
-            <clr-icon shape="plus"></clr-icon>
-            {{ 'settings.create-new-role' | translate }}
-        </a>
-    </vdr-page-title>
-</vdr-page-header>
-<vdr-page-body>
-    <vdr-data-table-2
-        class="mt-2"
-        id="role-list"
-        [items]="items$ | async"
-        [itemsPerPage]="itemsPerPage$ | async"
-        [totalItems]="totalItems$ | async"
-        [currentPage]="currentPage$ | async"
-        [filters]="filters"
-        (pageChange)="setPageNumber($event)"
-        (itemsPerPageChange)="setItemsPerPage($event)"
+<vdr-data-table-2
+    class="mt-2"
+    id="role-list"
+    [items]="items$ | async"
+    [itemsPerPage]="itemsPerPage$ | async"
+    [totalItems]="totalItems$ | async"
+    [currentPage]="currentPage$ | async"
+    [filters]="filters"
+    (pageChange)="setPageNumber($event)"
+    (itemsPerPageChange)="setItemsPerPage($event)"
+>
+    <vdr-bulk-action-menu
+        locationId="role-list"
+        [hostComponent]="this"
+        [selectionManager]="selectionManager"
+    />
+    <vdr-dt2-search
+        [searchTermControl]="searchTermControl"
+        [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
+    />
+    <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
+        <ng-template let-role="item">
+            {{ role.id }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.created-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('createdAt')"
     >
-        <vdr-bulk-action-menu
-            locationId="role-list"
-            [hostComponent]="this"
-            [selectionManager]="selectionManager"
-        />
-        <vdr-dt2-search
-            [searchTermControl]="searchTermControl"
-            [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
-        />
-        <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
-            <ng-template let-role="item">
-                {{ role.id }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.created-at' | translate"
-            [hiddenByDefault]="true"
-            [sort]="sorts.get('createdAt')"
-        >
-            <ng-template let-role="item">
-                {{ role.createdAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.updated-at' | translate"
-            [hiddenByDefault]="true"
-            [sort]="sorts.get('updatedAt')"
-        >
-            <ng-template let-role="item">
-                {{ role.updatedAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.description' | translate"
-            [optional]="false"
-            [sort]="sorts.get('description')"
-        >
-            <ng-template let-role="item">
-                <a
-                    *ngIf="!isDefaultRole(role); else defaultRole"
-                    class="button-ghost"
-                    [routerLink]="['./', role.id]"
-                    ><span>{{ role.description }}</span>
-                    <clr-icon shape="arrow right"></clr-icon>
-                </a>
-                <ng-template #defaultRole>
-                    {{ role.description }}
-                </ng-template>
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'common.code' | translate" [sort]="sorts.get('code')">
-            <ng-template let-role="item">
-                <span *ngIf="!isDefaultRole(role)">{{ role.code }}</span>
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'settings.channel' | translate">
-            <ng-template let-role="item">
-                <ng-container *ngIf="!isDefaultRole(role)">
-                    <vdr-chip *ngFor="let channel of role.channels">
-                        <vdr-channel-badge [channelCode]="channel.code"></vdr-channel-badge>
-                        {{ channel.code | channelCodeToLabel | translate }}
-                    </vdr-chip>
-                </ng-container>
+        <ng-template let-role="item">
+            {{ role.createdAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.updated-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('updatedAt')"
+    >
+        <ng-template let-role="item">
+            {{ role.updatedAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.description' | translate"
+        [optional]="false"
+        [sort]="sorts.get('description')"
+    >
+        <ng-template let-role="item">
+            <a
+                *ngIf="!isDefaultRole(role); else defaultRole"
+                class="button-ghost"
+                [routerLink]="['./', role.id]"
+                ><span>{{ role.description }}</span>
+                <clr-icon shape="arrow right"></clr-icon>
+            </a>
+            <ng-template #defaultRole>
+                {{ role.description }}
             </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'settings.permissions' | translate">
-            <ng-template let-role="item">
-                <ng-container *ngIf="!isDefaultRole(role); else defaultRole">
-                    <div class="permissions-list">
-                        <vdr-chip
-                            *ngFor="
-                                let permission of role.permissions | slice : 0 : displayLimit[role.id] || 3
-                            "
-                            >{{ permission }}</vdr-chip
-                        >
-                        <button
-                            class="button-ghost"
-                            *ngIf="role.permissions.length > initialLimit"
-                            (click)="toggleDisplayLimit(role)"
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.code' | translate" [sort]="sorts.get('code')">
+        <ng-template let-role="item">
+            <span *ngIf="!isDefaultRole(role)">{{ role.code }}</span>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'settings.channel' | translate">
+        <ng-template let-role="item">
+            <ng-container *ngIf="!isDefaultRole(role)">
+                <vdr-chip *ngFor="let channel of role.channels">
+                    <vdr-channel-badge [channelCode]="channel.code"></vdr-channel-badge>
+                    <div class="ml-1">{{ channel.code | channelCodeToLabel | translate }}</div>
+                </vdr-chip>
+            </ng-container>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'settings.permissions' | translate">
+        <ng-template let-role="item">
+            <ng-container *ngIf="!isDefaultRole(role); else defaultRole">
+                <div class="permissions-list">
+                    <vdr-chip
+                        *ngFor="let permission of role.permissions | slice : 0 : displayLimit[role.id] || 3"
+                        >{{ permission }}</vdr-chip
+                    >
+                    <button
+                        class="button-small"
+                        *ngIf="role.permissions.length > initialLimit"
+                        (click)="toggleDisplayLimit(role)"
+                    >
+                        <ng-container
+                            *ngIf="(displayLimit[role.id] || 0) < role.permissions.length; else collapse"
                         >
-                            <ng-container
-                                *ngIf="(displayLimit[role.id] || 0) < role.permissions.length; else collapse"
-                            >
-                                <clr-icon shape="plus"></clr-icon>
-                                {{ role.permissions.length - initialLimit }}
-                            </ng-container>
-                            <ng-template #collapse>
-                                <clr-icon shape="minus"></clr-icon>
-                            </ng-template>
-                        </button>
-                    </div>
-                </ng-container>
-                <ng-template #defaultRole>
-                    <span class="default-role-label">{{ 'settings.default-role-label' | translate }}</span>
-                </ng-template>
+                            <clr-icon shape="plus"></clr-icon>
+                            {{ role.permissions.length - initialLimit }}
+                        </ng-container>
+                        <ng-template #collapse>
+                            <clr-icon shape="minus"></clr-icon>
+                        </ng-template>
+                    </button>
+                </div>
+            </ng-container>
+            <ng-template #defaultRole>
+                <span class="default-role-label">{{ 'settings.default-role-label' | translate }}</span>
             </ng-template>
-        </vdr-dt2-column>
-    </vdr-data-table-2>
-</vdr-page-body>
+        </ng-template>
+    </vdr-dt2-column>
+</vdr-data-table-2>

+ 1 - 0
packages/admin-ui/src/lib/settings/src/components/role-list/role-list.component.scss

@@ -8,4 +8,5 @@
     display: flex;
     flex-wrap: wrap;
     align-items: center;
+    gap: 4px;
 }

+ 10 - 0
packages/admin-ui/src/lib/settings/src/components/role-list/role-list.component.ts

@@ -7,6 +7,7 @@ import {
     DataTableService,
     GetRolesQuery,
     ItemOf,
+    NavBuilderService,
     Role,
     RoleFilterParameter,
     RoleSortParameter,
@@ -48,10 +49,19 @@ export class RoleListComponent
     constructor(
         router: Router,
         route: ActivatedRoute,
+        navBuilderService: NavBuilderService,
         private dataService: DataService,
         private dataTableService: DataTableService,
     ) {
         super(router, route);
+        navBuilderService.addActionBarItem({
+            id: 'create-role',
+            label: _('settings.create-new-role'),
+            locationId: 'role-list',
+            icon: 'plus',
+            routerLink: ['./create'],
+            requiresPermission: ['CreateAdministrator'],
+        });
         super.setQueryFn(
             (...args: any[]) => this.dataService.administrator.getRoles(...args),
             data => data.roles,

+ 50 - 64
packages/admin-ui/src/lib/settings/src/components/seller-list/seller-list.component.html

@@ -1,65 +1,51 @@
-<vdr-page-header>
-    <vdr-page-title>
-        <vdr-action-bar-items locationId="seller-list"></vdr-action-bar-items>
-        <a
-            class="btn btn-primary"
-            [routerLink]="['./create']"
-            *vdrIfPermissions="['SuperAdmin', 'CreateSeller']"
-        >
-            <clr-icon shape="plus"></clr-icon>
-            {{ 'settings.create-new-seller' | translate }}
-        </a>
-    </vdr-page-title>
-</vdr-page-header>
-<vdr-page-body>
-    <vdr-data-table-2
-        class="mt-2"
-        id="seller-list"
-        [items]="items$ | async"
-        [itemsPerPage]="itemsPerPage$ | async"
-        [totalItems]="totalItems$ | async"
-        [currentPage]="currentPage$ | async"
-        [filters]="filters"
-        (pageChange)="setPageNumber($event)"
-        (itemsPerPageChange)="setItemsPerPage($event)"
+<vdr-data-table-2
+    class="mt-2"
+    id="seller-list"
+    [items]="items$ | async"
+    [itemsPerPage]="itemsPerPage$ | async"
+    [totalItems]="totalItems$ | async"
+    [currentPage]="currentPage$ | async"
+    [filters]="filters"
+    (pageChange)="setPageNumber($event)"
+    (itemsPerPageChange)="setItemsPerPage($event)"
+>
+    <vdr-bulk-action-menu
+        locationId="seller-list"
+        [hostComponent]="this"
+        [selectionManager]="selectionManager"
+    />
+    <vdr-dt2-search
+        [searchTermControl]="searchTermControl"
+        [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
+    />
+    <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
+        <ng-template let-seller="item">{{ seller.id }}</ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.created-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('createdAt')"
     >
-        <vdr-bulk-action-menu
-            locationId="seller-list"
-            [hostComponent]="this"
-            [selectionManager]="selectionManager"
-        />
-        <vdr-dt2-search
-            [searchTermControl]="searchTermControl"
-            [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
-        />
-        <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
-            <ng-template let-seller="item">{{ seller.id }}</ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.created-at' | translate"
-            [hiddenByDefault]="true"
-            [sort]="sorts.get('createdAt')"
-        >
-            <ng-template let-seller="item">
-                {{ seller.createdAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.updated-at' | translate"
-            [hiddenByDefault]="true"
-            [sort]="sorts.get('updatedAt')"
-        >
-            <ng-template let-seller="item">
-                {{ seller.updatedAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'common.name' | translate" [optional]="false" [sort]="sorts.get('name')">
-            <ng-template let-seller="item">
-                <a class="button-ghost" [routerLink]="['./', seller.id]"
-                    ><span>{{ seller.name }}</span>
-                    <clr-icon shape="arrow right"></clr-icon>
-                </a>
-            </ng-template>
-        </vdr-dt2-column>
-    </vdr-data-table-2>
-</vdr-page-body>
+        <ng-template let-seller="item">
+            {{ seller.createdAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.updated-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('updatedAt')"
+    >
+        <ng-template let-seller="item">
+            {{ seller.updatedAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.name' | translate" [optional]="false" [sort]="sorts.get('name')">
+        <ng-template let-seller="item">
+            <a class="button-ghost" [routerLink]="['./', seller.id]"
+                ><span>{{ seller.name }}</span>
+                <clr-icon shape="arrow right"></clr-icon>
+            </a>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-custom-field-column *ngFor="let customField of customFields" [customField]="customField" />
+</vdr-data-table-2>

+ 16 - 0
packages/admin-ui/src/lib/settings/src/components/seller-list/seller-list.component.ts

@@ -1,13 +1,16 @@
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
     BaseListComponent,
     DataService,
     DataTableService,
     GetSellersQuery,
     ItemOf,
+    NavBuilderService,
     SellerFilterParameter,
     SellerSortParameter,
+    ServerConfigService,
 } from '@vendure/admin-ui/core';
 
 @Component({
@@ -20,9 +23,11 @@ export class SellerListComponent
     extends BaseListComponent<GetSellersQuery, ItemOf<GetSellersQuery, 'sellers'>>
     implements OnInit
 {
+    readonly customFields = this.serverConfigService.getCustomFieldsFor('Seller');
     readonly filters = this.dataTableService
         .createFilterCollection<SellerFilterParameter>()
         .addDateFilters()
+        .addCustomFieldFilters(this.customFields)
         .connectToRoute(this.route);
 
     readonly sorts = this.dataTableService
@@ -31,15 +36,26 @@ export class SellerListComponent
         .addSort({ name: 'createdAt' })
         .addSort({ name: 'updatedAt' })
         .addSort({ name: 'name' })
+        .addCustomFieldSorts(this.customFields)
         .connectToRoute(this.route);
 
     constructor(
         route: ActivatedRoute,
         router: Router,
+        navBuilderService: NavBuilderService,
         private dataService: DataService,
         private dataTableService: DataTableService,
+        private serverConfigService: ServerConfigService,
     ) {
         super(router, route);
+        navBuilderService.addActionBarItem({
+            id: 'create-seller',
+            label: _('settings.create-new-seller'),
+            locationId: 'seller-list',
+            icon: 'plus',
+            routerLink: ['./create'],
+            requiresPermission: ['SuperAdmin', 'CreateSeller'],
+        });
         super.setQueryFn(
             (...args: any[]) => this.dataService.settings.getSellerList(...args).refetchOnChannelChange(),
             data => data.sellers,

+ 68 - 110
packages/admin-ui/src/lib/settings/src/components/shipping-method-list/shipping-method-list.component.html

@@ -1,116 +1,74 @@
-<vdr-page-header>
-    <vdr-page-title>
-        <vdr-action-bar-items locationId="shipping-method-list"></vdr-action-bar-items>
-        <a
-            class="btn btn-primary"
-            [routerLink]="['./create']"
-            *vdrIfPermissions="['CreateSettings', 'CreateShippingMethod']"
-        >
-            <clr-icon shape="plus"></clr-icon>
-            {{ 'settings.create-new-shipping-method' | translate }}
-        </a>
-    </vdr-page-title>
-</vdr-page-header>
-<vdr-page-body>
+<vdr-page-block>
     <vdr-language-selector
-        class="mx-4 my-2"
         [availableLanguageCodes]="availableLanguages$ | async"
         [currentLanguageCode]="contentLanguage$ | async"
         (languageCodeChange)="setLanguage($event)"
     ></vdr-language-selector>
-    <vdr-data-table-2
-        class="mt-2"
-        id="shipping-method-list"
-        [items]="items$ | async"
-        [itemsPerPage]="itemsPerPage$ | async"
-        [totalItems]="totalItems$ | async"
-        [currentPage]="currentPage$ | async"
-        [filters]="filters"
-        (pageChange)="setPageNumber($event)"
-        (itemsPerPageChange)="setItemsPerPage($event)"
+</vdr-page-block>
+<vdr-data-table-2
+    class="mt-2"
+    id="shipping-method-list"
+    [items]="items$ | async"
+    [itemsPerPage]="itemsPerPage$ | async"
+    [totalItems]="totalItems$ | async"
+    [currentPage]="currentPage$ | async"
+    [filters]="filters"
+    (pageChange)="setPageNumber($event)"
+    (itemsPerPageChange)="setItemsPerPage($event)"
+>
+    <vdr-bulk-action-menu
+        locationId="shipping-method-list"
+        [hostComponent]="this"
+        [selectionManager]="selectionManager"
+    />
+    <vdr-dt2-search
+        [searchTermControl]="searchTermControl"
+        [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
+    />
+    <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
+        <ng-template let-shippingMethod="item">
+            {{ shippingMethod.id }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.created-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('createdAt')"
     >
-        <vdr-bulk-action-menu
-            locationId="shipping-method-list"
-            [hostComponent]="this"
-            [selectionManager]="selectionManager"
-        />
-        <vdr-dt2-search
-            [searchTermControl]="searchTermControl"
-            [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
-        />
-        <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
-            <ng-template let-shippingMethod="item">
-                {{ shippingMethod.id }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.created-at' | translate"
-            [hiddenByDefault]="true"
-            [sort]="sorts.get('createdAt')"
-        >
-            <ng-template let-shippingMethod="item">
-                {{ shippingMethod.createdAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.updated-at' | translate"
-            [hiddenByDefault]="true"
-            [sort]="sorts.get('updatedAt')"
-        >
-            <ng-template let-shippingMethod="item">
-                {{ shippingMethod.updatedAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'common.name' | translate" [optional]="false" [sort]="sorts.get('name')">
-            <ng-template let-shippingMethod="item">
-                <a class="button-ghost" [routerLink]="['./', shippingMethod.id]"
-                    ><span>{{ shippingMethod.name }}</span>
-                    <clr-icon shape="arrow right"></clr-icon>
-                </a>
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'common.code' | translate" [sort]="sorts.get('code')">
-            <ng-template let-shippingMethod="item">
-                {{ shippingMethod.code }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.description' | translate"
-            [sort]="sorts.get('description')"
-            [hiddenByDefault]="true"
-        >
-            <ng-template let-shippingMethod="item">
-                {{ shippingMethod.description }}
-            </ng-template>
-        </vdr-dt2-column>
-    </vdr-data-table-2>
-
-    <div class="testing-tool">
-        <clr-accordion>
-            <clr-accordion-panel>
-                <clr-accordion-title>{{ 'settings.test-shipping-methods' | translate }}</clr-accordion-title>
-                <clr-accordion-content *clrIfExpanded>
-                    <div class="clr-row">
-                        <div class="clr-col">
-                            <vdr-test-order-builder
-                                (orderLinesChange)="setTestOrderLines($event)"
-                            ></vdr-test-order-builder>
-                        </div>
-                        <div class="clr-col">
-                            <vdr-test-address-form
-                                (addressChange)="setTestAddress($event)"
-                            ></vdr-test-address-form>
-                            <vdr-shipping-eligibility-test-result
-                                [currencyCode]="(activeChannel$ | async)?.currencyCode"
-                                [okToRun]="allTestDataPresent()"
-                                [testDataUpdated]="testDataUpdated"
-                                [testResult]="testResult$ | async"
-                                (runTest)="runTest()"
-                            ></vdr-shipping-eligibility-test-result>
-                        </div>
-                    </div>
-                </clr-accordion-content>
-            </clr-accordion-panel>
-        </clr-accordion>
-    </div>
-</vdr-page-body>
+        <ng-template let-shippingMethod="item">
+            {{ shippingMethod.createdAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.updated-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('updatedAt')"
+    >
+        <ng-template let-shippingMethod="item">
+            {{ shippingMethod.updatedAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.name' | translate" [optional]="false" [sort]="sorts.get('name')">
+        <ng-template let-shippingMethod="item">
+            <a class="button-ghost" [routerLink]="['./', shippingMethod.id]"
+                ><span>{{ shippingMethod.name }}</span>
+                <clr-icon shape="arrow right"></clr-icon>
+            </a>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.code' | translate" [sort]="sorts.get('code')">
+        <ng-template let-shippingMethod="item">
+            {{ shippingMethod.code }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.description' | translate"
+        [sort]="sorts.get('description')"
+        [hiddenByDefault]="true"
+    >
+        <ng-template let-shippingMethod="item">
+            {{ shippingMethod.description }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-custom-field-column *ngFor="let customField of customFields" [customField]="customField" />
+</vdr-data-table-2>

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

@@ -1,3 +0,0 @@
-.testing-tool {
-    margin-top: 48px;
-}

+ 14 - 48
packages/admin-ui/src/lib/settings/src/components/shipping-method-list/shipping-method-list.component.ts

@@ -5,21 +5,15 @@ import {
     BaseListComponent,
     DataService,
     DataTableService,
-    GetActiveChannelQuery,
     GetShippingMethodListQuery,
     ItemOf,
     LanguageCode,
+    NavBuilderService,
     ServerConfigService,
     ShippingMethodFilterParameter,
-    ShippingMethodQuote,
     ShippingMethodSortParameter,
-    TestEligibleShippingMethodsInput,
 } from '@vendure/admin-ui/core';
-import { Observable, Subject } from 'rxjs';
-import { switchMap } from 'rxjs/operators';
-
-import { TestAddress } from '../test-address-form/test-address-form.component';
-import { TestOrderLine } from '../test-order-builder/test-order-builder.component';
+import { Observable } from 'rxjs';
 
 @Component({
     selector: 'vdr-shipping-method-list',
@@ -34,15 +28,9 @@ export class ShippingMethodListComponent
     >
     implements OnInit
 {
-    activeChannel$: Observable<GetActiveChannelQuery['activeChannel']>;
-    testAddress: TestAddress;
-    testOrderLines: TestOrderLine[];
-    testDataUpdated = false;
-    testResult$: Observable<ShippingMethodQuote[] | undefined>;
     availableLanguages$: Observable<LanguageCode[]>;
     contentLanguage$: Observable<LanguageCode>;
-    private fetchTestResult$ = new Subject<[TestAddress, TestOrderLine[]]>();
-
+    readonly customFields = this.serverConfigService.getCustomFieldsFor('ShippingMethod');
     readonly filters = this.dataTableService
         .createFilterCollection<ShippingMethodFilterParameter>()
         .addDateFilters()
@@ -64,6 +52,7 @@ export class ShippingMethodListComponent
             label: _('common.description'),
             filterField: 'description',
         })
+        .addCustomFieldFilters(this.customFields)
         .connectToRoute(this.route);
 
     readonly sorts = this.dataTableService
@@ -74,16 +63,26 @@ export class ShippingMethodListComponent
         .addSort({ name: 'name' })
         .addSort({ name: 'code' })
         .addSort({ name: 'description' })
+        .addCustomFieldSorts(this.customFields)
         .connectToRoute(this.route);
 
     constructor(
         router: Router,
         route: ActivatedRoute,
+        navBuilderService: NavBuilderService,
         private dataService: DataService,
         private serverConfigService: ServerConfigService,
         private dataTableService: DataTableService,
     ) {
         super(router, route);
+        navBuilderService.addActionBarItem({
+            id: 'create-shipping-method',
+            label: _('settings.create-new-shipping-method'),
+            locationId: 'shipping-method-list',
+            icon: 'plus',
+            routerLink: ['./create'],
+            requiresPermission: ['CreateSettings', 'CreateShippingMethod'],
+        });
         super.setQueryFn(
             (...args: any[]) =>
                 this.dataService.shippingMethod.getShippingMethods(...args).refetchOnChannelChange(),
@@ -106,20 +105,6 @@ export class ShippingMethodListComponent
 
     ngOnInit() {
         super.ngOnInit();
-        this.testResult$ = this.fetchTestResult$.pipe(
-            switchMap(([address, lines]) => {
-                const input: TestEligibleShippingMethodsInput = {
-                    shippingAddress: { ...address, streetLine1: 'test' },
-                    lines: lines.map(l => ({ productVariantId: l.id, quantity: l.quantity })),
-                };
-                return this.dataService.shippingMethod
-                    .testEligibleShippingMethods(input)
-                    .mapSingle(result => result.testEligibleShippingMethods);
-            }),
-        );
-        this.activeChannel$ = this.dataService.settings
-            .getActiveChannel()
-            .mapStream(data => data.activeChannel);
         this.availableLanguages$ = this.serverConfigService.getAvailableLanguages();
         this.contentLanguage$ = this.dataService.client
             .uiState()
@@ -128,25 +113,6 @@ export class ShippingMethodListComponent
         super.refreshListOnChanges(this.contentLanguage$, this.filters.valueChanges, this.sorts.valueChanges);
     }
 
-    setTestOrderLines(event: TestOrderLine[]) {
-        this.testOrderLines = event;
-        this.testDataUpdated = true;
-    }
-
-    setTestAddress(event: TestAddress) {
-        this.testAddress = event;
-        this.testDataUpdated = true;
-    }
-
-    allTestDataPresent(): boolean {
-        return !!(this.testAddress && this.testOrderLines && this.testOrderLines.length);
-    }
-
-    runTest() {
-        this.fetchTestResult$.next([this.testAddress, this.testOrderLines]);
-        this.testDataUpdated = false;
-    }
-
     setLanguage(code: LanguageCode) {
         this.dataService.client.setContentLanguage(code).subscribe();
     }

+ 57 - 73
packages/admin-ui/src/lib/settings/src/components/tax-category-list/tax-category-list.component.html

@@ -1,74 +1,58 @@
-<vdr-page-header>
-    <vdr-page-title>
-        <vdr-action-bar-items locationId="tax-category-list"></vdr-action-bar-items>
-        <a
-            class="btn btn-primary"
-            [routerLink]="['./create']"
-            *vdrIfPermissions="['CreateSettings', 'CreateTaxCategory']"
-        >
-            <clr-icon shape="plus"></clr-icon>
-            {{ 'settings.create-new-tax-category' | translate }}
-        </a>
-    </vdr-page-title>
-</vdr-page-header>
-<vdr-page-body>
-    <vdr-data-table-2
-        class="mt-2"
-        id="tax-category-list"
-        [items]="items$ | async"
-        [itemsPerPage]="itemsPerPage$ | async"
-        [totalItems]="totalItems$ | async"
-        [currentPage]="currentPage$ | async"
-        [filters]="filters"
-        (pageChange)="setPageNumber($event)"
-        (itemsPerPageChange)="setItemsPerPage($event)"
+<vdr-data-table-2
+    class="mt-2"
+    id="tax-category-list"
+    [items]="items$ | async"
+    [itemsPerPage]="itemsPerPage$ | async"
+    [totalItems]="totalItems$ | async"
+    [currentPage]="currentPage$ | async"
+    [filters]="filters"
+    (pageChange)="setPageNumber($event)"
+    (itemsPerPageChange)="setItemsPerPage($event)"
+>
+    <vdr-bulk-action-menu
+        locationId="tax-category-list"
+        [hostComponent]="this"
+        [selectionManager]="selectionManager"
+    />
+    <vdr-dt2-search
+        [searchTermControl]="searchTermControl"
+        [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
+    />
+    <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
+        <ng-template let-taxCategory="item">
+            {{ taxCategory.id }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.created-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('createdAt')"
     >
-        <vdr-bulk-action-menu
-            locationId="tax-category-list"
-            [hostComponent]="this"
-            [selectionManager]="selectionManager"
-        />
-        <vdr-dt2-search
-            [searchTermControl]="searchTermControl"
-            [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
-        />
-        <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
-            <ng-template let-taxCategory="item">
-                {{ taxCategory.id }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.created-at' | translate"
-            [hiddenByDefault]="true"
-            [sort]="sorts.get('createdAt')"
-        >
-            <ng-template let-taxCategory="item">
-                {{ taxCategory.createdAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.updated-at' | translate"
-            [hiddenByDefault]="true"
-            [sort]="sorts.get('updatedAt')"
-        >
-            <ng-template let-taxCategory="item">
-                {{ taxCategory.updatedAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'common.name' | translate" [optional]="false" [sort]="sorts.get('name')">
-            <ng-template let-taxCategory="item">
-                <a class="button-ghost" [routerLink]="['./', taxCategory.id]"
-                    ><span>{{ taxCategory.name }}</span>
-                    <clr-icon shape="arrow right"></clr-icon>
-                </a>
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'common.default-tax-category' | translate">
-            <ng-template let-taxCategory="item">
-                <vdr-chip *ngIf="taxCategory.isDefault">{{
-                    'common.default-tax-category' | translate
-                }}</vdr-chip>
-            </ng-template>
-        </vdr-dt2-column>
-    </vdr-data-table-2>
-</vdr-page-body>
+        <ng-template let-taxCategory="item">
+            {{ taxCategory.createdAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.updated-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('updatedAt')"
+    >
+        <ng-template let-taxCategory="item">
+            {{ taxCategory.updatedAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.name' | translate" [optional]="false" [sort]="sorts.get('name')">
+        <ng-template let-taxCategory="item">
+            <a class="button-ghost" [routerLink]="['./', taxCategory.id]"
+                ><span>{{ taxCategory.name }}</span>
+                <clr-icon shape="arrow right"></clr-icon>
+            </a>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.default-tax-category' | translate">
+        <ng-template let-taxCategory="item">
+            <vdr-chip *ngIf="taxCategory.isDefault">{{ 'common.default-tax-category' | translate }}</vdr-chip>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-custom-field-column *ngFor="let customField of customFields" [customField]="customField" />
+</vdr-data-table-2>

+ 15 - 0
packages/admin-ui/src/lib/settings/src/components/tax-category-list/tax-category-list.component.ts

@@ -7,6 +7,8 @@ import {
     DataTableService,
     GetTaxCategoriesQuery,
     ItemOf,
+    NavBuilderService,
+    ServerConfigService,
     TaxCategoryFilterParameter,
     TaxCategorySortParameter,
 } from '@vendure/admin-ui/core';
@@ -21,6 +23,7 @@ export class TaxCategoryListComponent
     extends BaseListComponent<GetTaxCategoriesQuery, ItemOf<GetTaxCategoriesQuery, 'taxCategories'>>
     implements OnInit
 {
+    readonly customFields = this.serverConfigService.getCustomFieldsFor('TaxCategory');
     readonly filters = this.dataTableService
         .createFilterCollection<TaxCategoryFilterParameter>()
         .addDateFilters()
@@ -30,6 +33,7 @@ export class TaxCategoryListComponent
             label: _('common.name'),
             filterField: 'name',
         })
+        .addCustomFieldFilters(this.customFields)
         .connectToRoute(this.route);
 
     readonly sorts = this.dataTableService
@@ -38,15 +42,26 @@ export class TaxCategoryListComponent
         .addSort({ name: 'createdAt' })
         .addSort({ name: 'updatedAt' })
         .addSort({ name: 'name' })
+        .addCustomFieldSorts(this.customFields)
         .connectToRoute(this.route);
 
     constructor(
         route: ActivatedRoute,
         router: Router,
+        navBuilderService: NavBuilderService,
         private dataService: DataService,
         private dataTableService: DataTableService,
+        private serverConfigService: ServerConfigService,
     ) {
         super(router, route);
+        navBuilderService.addActionBarItem({
+            id: 'create-tax-category',
+            label: _('settings.create-new-tax-category'),
+            locationId: 'tax-category-list',
+            icon: 'plus',
+            routerLink: ['./create'],
+            requiresPermission: ['CreateSettings', 'CreateTaxCategory'],
+        });
         super.setQueryFn(
             (...args: any[]) => this.dataService.settings.getTaxCategories(...args),
             data => data.taxCategories,

+ 75 - 85
packages/admin-ui/src/lib/settings/src/components/tax-rate-list/tax-rate-list.component.html

@@ -1,86 +1,76 @@
-<vdr-page-header>
-    <vdr-page-title>
-        <vdr-action-bar-items locationId="tax-rate-list"></vdr-action-bar-items>
-        <a
-            class="btn btn-primary"
-            [routerLink]="['./create']"
-            *vdrIfPermissions="['CreateSettings', 'CreateTaxRate']"
-        >
-            <clr-icon shape="plus"></clr-icon>
-            {{ 'settings.create-new-tax-rate' | translate }}
-        </a>
-    </vdr-page-title>
-</vdr-page-header>
-<vdr-page-body>
-    <vdr-data-table-2
-        class="mt-2"
-        id="tax-rate-list"
-        [items]="items$ | async"
-        [itemsPerPage]="itemsPerPage$ | async"
-        [totalItems]="totalItems$ | async"
-        [currentPage]="currentPage$ | async"
-        [filters]="filters"
-        (pageChange)="setPageNumber($event)"
-        (itemsPerPageChange)="setItemsPerPage($event)"
+<vdr-data-table-2
+    class="mt-2"
+    id="tax-rate-list"
+    [items]="items$ | async"
+    [itemsPerPage]="itemsPerPage$ | async"
+    [totalItems]="totalItems$ | async"
+    [currentPage]="currentPage$ | async"
+    [filters]="filters"
+    (pageChange)="setPageNumber($event)"
+    (itemsPerPageChange)="setItemsPerPage($event)"
+>
+    <vdr-bulk-action-menu
+        locationId="tax-rate-list"
+        [hostComponent]="this"
+        [selectionManager]="selectionManager"
+    />
+    <vdr-dt2-search
+        [searchTermControl]="searchTermControl"
+        [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
+    />
+    <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
+        <ng-template let-taxRate="item">
+            {{ taxRate.id }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.created-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('createdAt')"
     >
-        <vdr-bulk-action-menu
-            locationId="tax-rate-list"
-            [hostComponent]="this"
-            [selectionManager]="selectionManager"
-        />
-        <vdr-dt2-search
-            [searchTermControl]="searchTermControl"
-            [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
-        />
-        <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
-            <ng-template let-taxRate="item">
-                {{ taxRate.id }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.created-at' | translate"
-            [hiddenByDefault]="true"
-            [sort]="sorts.get('createdAt')"
-        >
-            <ng-template let-taxRate="item">
-                {{ taxRate.createdAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column
-            [heading]="'common.updated-at' | translate"
-            [hiddenByDefault]="true"
-            [sort]="sorts.get('updatedAt')"
-        >
-            <ng-template let-taxRate="item">
-                {{ taxRate.updatedAt | localeDate : 'short' }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'common.name' | translate" [optional]="false" [sort]="sorts.get('name')">
-            <ng-template let-taxRate="item">
-                <a class="button-ghost" [routerLink]="['./', taxRate.id]"
-                    ><span>{{ taxRate.name }}</span>
-                    <clr-icon shape="arrow right"></clr-icon>
-                </a>
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'settings.tax-category' | translate">
-            <ng-template let-taxRate="item">
-                {{ taxRate.category.name }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'settings.zone' | translate">
-            <ng-template let-taxRate="item">
-                {{ taxRate.zone.name }}
-            </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'settings.tax-rate' | translate" [sort]="sorts.get('value')">
-            <ng-template let-taxRate="item"> {{ taxRate.value }}% </ng-template>
-        </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'common.enabled' | translate">
-            <ng-template let-taxRate="item">
-                <vdr-chip  *ngIf="taxRate.enabled" colorType="success">{{ 'common.enabled' | translate }}</vdr-chip>
-                <vdr-chip  *ngIf="!taxRate.enabled" colorType="warning">{{ 'common.disabled' | translate }}</vdr-chip>
-            </ng-template>
-        </vdr-dt2-column>
-    </vdr-data-table-2>
-</vdr-page-body>
+        <ng-template let-taxRate="item">
+            {{ taxRate.createdAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column
+        [heading]="'common.updated-at' | translate"
+        [hiddenByDefault]="true"
+        [sort]="sorts.get('updatedAt')"
+    >
+        <ng-template let-taxRate="item">
+            {{ taxRate.updatedAt | localeDate : 'short' }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.name' | translate" [optional]="false" [sort]="sorts.get('name')">
+        <ng-template let-taxRate="item">
+            <a class="button-ghost" [routerLink]="['./', taxRate.id]"
+                ><span>{{ taxRate.name }}</span>
+                <clr-icon shape="arrow right"></clr-icon>
+            </a>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'settings.tax-category' | translate">
+        <ng-template let-taxRate="item">
+            {{ taxRate.category.name }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'settings.zone' | translate">
+        <ng-template let-taxRate="item">
+            {{ taxRate.zone.name }}
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'settings.tax-rate' | translate" [sort]="sorts.get('value')">
+        <ng-template let-taxRate="item"> {{ taxRate.value }}% </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-column [heading]="'common.enabled' | translate">
+        <ng-template let-taxRate="item">
+            <vdr-chip *ngIf="taxRate.enabled" colorType="success">{{
+                'common.enabled' | translate
+            }}</vdr-chip>
+            <vdr-chip *ngIf="!taxRate.enabled" colorType="warning">{{
+                'common.disabled' | translate
+            }}</vdr-chip>
+        </ng-template>
+    </vdr-dt2-column>
+    <vdr-dt2-custom-field-column *ngFor="let customField of customFields" [customField]="customField" />
+</vdr-data-table-2>

+ 15 - 0
packages/admin-ui/src/lib/settings/src/components/tax-rate-list/tax-rate-list.component.ts

@@ -7,6 +7,8 @@ import {
     DataTableService,
     GetTaxRateListQuery,
     ItemOf,
+    NavBuilderService,
+    ServerConfigService,
     TaxRateFilterParameter,
     TaxRateSortParameter,
 } from '@vendure/admin-ui/core';
@@ -21,6 +23,7 @@ export class TaxRateListComponent
     extends BaseListComponent<GetTaxRateListQuery, ItemOf<GetTaxRateListQuery, 'taxRates'>>
     implements OnInit
 {
+    readonly customFields = this.serverConfigService.getCustomFieldsFor('TaxRate');
     readonly filters = this.dataTableService
         .createFilterCollection<TaxRateFilterParameter>()
         .addDateFilters()
@@ -42,6 +45,7 @@ export class TaxRateListComponent
             label: _('common.value'),
             filterField: 'value',
         })
+        .addCustomFieldFilters(this.customFields)
         .connectToRoute(this.route);
 
     readonly sorts = this.dataTableService
@@ -51,15 +55,26 @@ export class TaxRateListComponent
         .addSort({ name: 'updatedAt' })
         .addSort({ name: 'name' })
         .addSort({ name: 'value' })
+        .addCustomFieldSorts(this.customFields)
         .connectToRoute(this.route);
 
     constructor(
         router: Router,
         route: ActivatedRoute,
+        navBuilderService: NavBuilderService,
         private dataService: DataService,
         private dataTableService: DataTableService,
+        private serverConfigService: ServerConfigService,
     ) {
         super(router, route);
+        navBuilderService.addActionBarItem({
+            id: 'create-tax-rate',
+            label: _('settings.create-new-tax-rate'),
+            locationId: 'facet-list',
+            icon: 'plus',
+            routerLink: ['./create'],
+            requiresPermission: ['CreateSettings', 'CreateTaxRate'],
+        });
         super.setQueryFn(
             (...args: any[]) => this.dataService.settings.getTaxRates(...args),
             data => data.taxRates,

+ 17 - 0
packages/admin-ui/src/lib/settings/src/components/test-shipping-methods/test-shipping-methods.component.html

@@ -0,0 +1,17 @@
+<vdr-page-block>
+    <div class="test-wrapper">
+        <div class="">
+            <vdr-test-order-builder (orderLinesChange)="setTestOrderLines($event)"></vdr-test-order-builder>
+        </div>
+        <div class="">
+            <vdr-test-address-form (addressChange)="setTestAddress($event)"></vdr-test-address-form>
+            <vdr-shipping-eligibility-test-result
+                [currencyCode]="(activeChannel$ | async)?.currencyCode"
+                [okToRun]="allTestDataPresent()"
+                [testDataUpdated]="testDataUpdated"
+                [testResult]="testResult$ | async"
+                (runTest)="runTest()"
+            ></vdr-shipping-eligibility-test-result>
+        </div>
+    </div>
+</vdr-page-block>

+ 9 - 0
packages/admin-ui/src/lib/settings/src/components/test-shipping-methods/test-shipping-methods.component.scss

@@ -0,0 +1,9 @@
+.test-wrapper {
+    display: flex;
+    width: 100%;
+    gap: var(--space-unit);
+
+    > div{
+        flex: 1;
+    }
+}

+ 63 - 0
packages/admin-ui/src/lib/settings/src/components/test-shipping-methods/test-shipping-methods.component.ts

@@ -0,0 +1,63 @@
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import {
+    DataService,
+    GetActiveChannelQuery,
+    ShippingMethodQuote,
+    TestEligibleShippingMethodsInput,
+} from '@vendure/admin-ui/core';
+import { TestAddress, TestOrderLine } from '@vendure/admin-ui/settings';
+import { Observable, Subject } from 'rxjs';
+import { switchMap } from 'rxjs/operators';
+
+@Component({
+    selector: 'vdr-test-shipping-methods',
+    templateUrl: './test-shipping-methods.component.html',
+    styleUrls: ['./test-shipping-methods.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class TestShippingMethodsComponent implements OnInit {
+    activeChannel$: Observable<GetActiveChannelQuery['activeChannel']>;
+    testAddress: TestAddress;
+    testOrderLines: TestOrderLine[];
+    testDataUpdated = false;
+    testResult$: Observable<ShippingMethodQuote[] | undefined>;
+    private fetchTestResult$ = new Subject<[TestAddress, TestOrderLine[]]>();
+
+    constructor(private dataService: DataService) {}
+
+    ngOnInit() {
+        this.activeChannel$ = this.dataService.settings
+            .getActiveChannel()
+            .mapStream(data => data.activeChannel);
+        this.testResult$ = this.fetchTestResult$.pipe(
+            switchMap(([address, lines]) => {
+                const input: TestEligibleShippingMethodsInput = {
+                    shippingAddress: { ...address, streetLine1: 'test' },
+                    lines: lines.map(l => ({ productVariantId: l.id, quantity: l.quantity })),
+                };
+                return this.dataService.shippingMethod
+                    .testEligibleShippingMethods(input)
+                    .mapSingle(result => result.testEligibleShippingMethods);
+            }),
+        );
+    }
+
+    setTestOrderLines(event: TestOrderLine[]) {
+        this.testOrderLines = event;
+        this.testDataUpdated = true;
+    }
+
+    setTestAddress(event: TestAddress) {
+        this.testAddress = event;
+        this.testDataUpdated = true;
+    }
+
+    allTestDataPresent(): boolean {
+        return !!(this.testAddress && this.testOrderLines && this.testOrderLines.length);
+    }
+
+    runTest() {
+        this.fetchTestResult$.next([this.testAddress, this.testOrderLines]);
+        this.testDataUpdated = false;
+    }
+}

+ 96 - 109
packages/admin-ui/src/lib/settings/src/components/zone-list/zone-list.component.html

@@ -1,115 +1,102 @@
-<vdr-page-header>
-    <vdr-page-title>
-        <vdr-action-bar-items locationId="zone-list"></vdr-action-bar-items>
-        <button
-            class="btn btn-primary"
-            *vdrIfPermissions="['CreateSettings', 'CreateZone']"
-            (click)="create()"
-        >
-            <clr-icon shape="plus"></clr-icon>
-            {{ 'settings.create-new-zone' | translate }}
-        </button>
-    </vdr-page-title>
-</vdr-page-header>
-<vdr-page-body>
+<vdr-page-block>
     <vdr-language-selector
-        class="ml-4 mt-2"
         [availableLanguageCodes]="availableLanguages$ | async"
         [currentLanguageCode]="contentLanguage$ | async"
         (languageCodeChange)="setLanguage($event)"
     />
-    <vdr-split-view [rightPanelOpen]="activeZone$ | async" (closeClicked)="closeMembers()">
-        <ng-template vdrSplitViewLeft>
-            <vdr-data-table-2
-                class="mt-2"
-                id="zone-list"
-                [items]="items$ | async"
-                [itemsPerPage]="itemsPerPage$ | async"
-                [totalItems]="totalItems$ | async"
-                [currentPage]="currentPage$ | async"
-                [filters]="filters"
-                [activeIndex]="activeIndex$ | async"
-                (pageChange)="setPageNumber($event)"
-                (itemsPerPageChange)="setItemsPerPage($event)"
+</vdr-page-block>
+<vdr-split-view [rightPanelOpen]="activeZone$ | async" (closeClicked)="closeMembers()">
+    <ng-template vdrSplitViewLeft>
+        <vdr-data-table-2
+            class="mt-2"
+            id="zone-list"
+            [items]="items$ | async"
+            [itemsPerPage]="itemsPerPage$ | async"
+            [totalItems]="totalItems$ | async"
+            [currentPage]="currentPage$ | async"
+            [filters]="filters"
+            [activeIndex]="activeIndex$ | async"
+            (pageChange)="setPageNumber($event)"
+            (itemsPerPageChange)="setItemsPerPage($event)"
+        >
+            <vdr-bulk-action-menu
+                locationId="zone-list"
+                [hostComponent]="this"
+                [selectionManager]="selectionManager"
+            />
+            <vdr-dt2-search
+                [searchTermControl]="searchTermControl"
+                [searchTermPlaceholder]="'common.search-by-name' | translate"
+            />
+            <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
+                <ng-template let-customerGroup="item">
+                    {{ customerGroup.id }}
+                </ng-template>
+            </vdr-dt2-column>
+            <vdr-dt2-column
+                [heading]="'common.created-at' | translate"
+                [hiddenByDefault]="true"
+                [sort]="sorts.get('createdAt')"
+            >
+                <ng-template let-customerGroup="item">
+                    {{ customerGroup.createdAt | localeDate : 'short' }}
+                </ng-template>
+            </vdr-dt2-column>
+            <vdr-dt2-column
+                [heading]="'common.updated-at' | translate"
+                [hiddenByDefault]="true"
+                [sort]="sorts.get('updatedAt')"
+            >
+                <ng-template let-customerGroup="item">
+                    {{ customerGroup.updatedAt | localeDate : 'short' }}
+                </ng-template>
+            </vdr-dt2-column>
+            <vdr-dt2-column
+                [heading]="'common.name' | translate"
+                [optional]="false"
+                [sort]="sorts.get('name')"
             >
-                <vdr-bulk-action-menu
-                    locationId="zone-list"
-                    [hostComponent]="this"
-                    [selectionManager]="selectionManager"
-                />
-                <vdr-dt2-search
-                    [searchTermControl]="searchTermControl"
-                    [searchTermPlaceholder]="'common.search-by-name' | translate"
-                />
-                <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
-                    <ng-template let-customerGroup="item">
-                        {{ customerGroup.id }}
-                    </ng-template>
-                </vdr-dt2-column>
-                <vdr-dt2-column
-                    [heading]="'common.created-at' | translate"
-                    [hiddenByDefault]="true"
-                    [sort]="sorts.get('createdAt')"
-                >
-                    <ng-template let-customerGroup="item">
-                        {{ customerGroup.createdAt | localeDate : 'short' }}
-                    </ng-template>
-                </vdr-dt2-column>
-                <vdr-dt2-column
-                    [heading]="'common.updated-at' | translate"
-                    [hiddenByDefault]="true"
-                    [sort]="sorts.get('updatedAt')"
-                >
-                    <ng-template let-customerGroup="item">
-                        {{ customerGroup.updatedAt | localeDate : 'short' }}
-                    </ng-template>
-                </vdr-dt2-column>
-                <vdr-dt2-column
-                    [heading]="'common.name' | translate"
-                    [optional]="false"
-                    [sort]="sorts.get('name')"
-                >
-                    <ng-template let-customerGroup="item">
-                        <a class="button-ghost" [routerLink]="['./', customerGroup.id]"
-                            ><span>{{ customerGroup.name }}</span>
-                            <clr-icon shape="arrow right"></clr-icon>
-                        </a>
-                    </ng-template>
-                </vdr-dt2-column>
-                <vdr-dt2-column
-                    [heading]="'common.view-contents' | translate"
-                    [optional]="false"
-                >
-                    <ng-template let-customerGroup="item">
-                        <a
-                            class="button-small bg-weight-150"
-                            [routerLink]="['./', { contents: customerGroup.id }]"
-                            queryParamsHandling="preserve"
-                        >
-                            <span>{{ 'settings.view-zone-members' | translate }}</span>
-                            <clr-icon shape="file-group"></clr-icon>
-                        </a>
-                    </ng-template>
-                </vdr-dt2-column>
-            </vdr-data-table-2>
-        </ng-template>
-        <ng-template vdrSplitViewRight [splitViewTitle]="(activeZone$ | async)?.name">
-            <ng-container *ngIf="activeZone$ | async as activeZone">
-                <button class="button-ghost ml-4" (click)="addToZone(activeZone)">
-                    <clr-icon shape="plus"></clr-icon>
-                    <span>{{
-                        'settings.add-countries-to-zone' | translate : { zoneName: activeZone.name }
-                    }}</span>
-                </button>
-                <vdr-zone-member-list
-                    *ngIf="activeZone$ | async as activeZone"
-                    locationId="zone-members-list"
-                    [members]="activeZone.members"
-                    [selectedMemberIds]="selectedMemberIds"
-                    [activeZone]="activeZone"
-                    (selectionChange)="selectedMemberIds = $event"
-                />
-            </ng-container>
-        </ng-template>
-    </vdr-split-view>
-</vdr-page-body>
+                <ng-template let-customerGroup="item">
+                    <a class="button-ghost" [routerLink]="['./', customerGroup.id]"
+                        ><span>{{ customerGroup.name }}</span>
+                        <clr-icon shape="arrow right"></clr-icon>
+                    </a>
+                </ng-template>
+            </vdr-dt2-column>
+            <vdr-dt2-column [heading]="'common.view-contents' | translate" [optional]="false">
+                <ng-template let-customerGroup="item">
+                    <a
+                        class="button-small bg-weight-150"
+                        [routerLink]="['./', { contents: customerGroup.id }]"
+                        queryParamsHandling="preserve"
+                    >
+                        <span>{{ 'settings.view-zone-members' | translate }}</span>
+                        <clr-icon shape="file-group"></clr-icon>
+                    </a>
+                </ng-template>
+            </vdr-dt2-column>
+            <vdr-dt2-custom-field-column
+                *ngFor="let customField of customFields"
+                [customField]="customField"
+            />
+        </vdr-data-table-2>
+    </ng-template>
+    <ng-template vdrSplitViewRight [splitViewTitle]="(activeZone$ | async)?.name">
+        <ng-container *ngIf="activeZone$ | async as activeZone">
+            <button class="button-ghost ml-4" (click)="addToZone(activeZone)">
+                <clr-icon shape="plus"></clr-icon>
+                <span>{{
+                    'settings.add-countries-to-zone' | translate : { zoneName: activeZone.name }
+                }}</span>
+            </button>
+            <vdr-zone-member-list
+                *ngIf="activeZone$ | async as activeZone"
+                locationId="zone-members-list"
+                [members]="activeZone.members"
+                [selectedMemberIds]="selectedMemberIds"
+                [activeZone]="activeZone"
+                (selectionChange)="selectedMemberIds = $event"
+            />
+        </ng-container>
+    </ng-template>
+</vdr-split-view>

+ 13 - 1
packages/admin-ui/src/lib/settings/src/components/zone-list/zone-list.component.ts

@@ -10,6 +10,7 @@ import {
     LanguageCode,
     LogicalOperator,
     ModalService,
+    NavBuilderService,
     NotificationService,
     ServerConfigService,
     ZoneFilterParameter,
@@ -37,7 +38,7 @@ export class ZoneListComponent
     availableLanguages$: Observable<LanguageCode[]>;
     contentLanguage$: Observable<LanguageCode>;
     selectedMemberIds: string[] = [];
-
+    readonly customFields = this.serverConfigService.getCustomFieldsFor('Zone');
     readonly filters = this.dataTableService
         .createFilterCollection<ZoneFilterParameter>()
         .addDateFilters()
@@ -47,6 +48,7 @@ export class ZoneListComponent
             label: _('common.name'),
             filterField: 'name',
         })
+        .addCustomFieldFilters(this.customFields)
         .connectToRoute(this.route);
 
     readonly sorts = this.dataTableService
@@ -55,11 +57,13 @@ export class ZoneListComponent
         .addSort({ name: 'createdAt' })
         .addSort({ name: 'updatedAt' })
         .addSort({ name: 'name' })
+        .addCustomFieldSorts(this.customFields)
         .connectToRoute(this.route);
 
     constructor(
         route: ActivatedRoute,
         router: Router,
+        navBuilderService: NavBuilderService,
         private dataService: DataService,
         private notificationService: NotificationService,
         private modalService: ModalService,
@@ -67,6 +71,14 @@ export class ZoneListComponent
         private dataTableService: DataTableService,
     ) {
         super(router, route);
+        navBuilderService.addActionBarItem({
+            id: 'create-zone',
+            label: _('settings.create-new-zone'),
+            locationId: 'zone-list',
+            icon: 'plus',
+            routerLink: ['./create'],
+            requiresPermission: ['CreateSettings', 'CreateZone'],
+        });
         super.setQueryFn(
             (...args: any[]) => this.dataService.settings.getZones(...args).refetchOnChannelChange(),
             data => data.zones,

+ 83 - 5
packages/admin-ui/src/lib/settings/src/settings.module.ts

@@ -1,6 +1,7 @@
 import { NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-import { BulkActionRegistryService, SharedModule } from '@vendure/admin-ui/core';
+import { RouterModule, ROUTES } from '@angular/router';
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
+import { BulkActionRegistryService, PageService, 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';
@@ -43,10 +44,19 @@ import { ZoneMemberControlsDirective } from './components/zone-member-list/zone-
 import { removeZoneMembersBulkAction } from './components/zone-member-list/zone-member-list-bulk-actions';
 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';
+import { createRoutes } from './settings.routes';
+import { TestShippingMethodsComponent } from './components/test-shipping-methods/test-shipping-methods.component';
 
 @NgModule({
-    imports: [SharedModule, RouterModule.forChild(settingsRoutes)],
+    imports: [SharedModule, RouterModule.forChild([])],
+    providers: [
+        {
+            provide: ROUTES,
+            useFactory: (pageService: PageService) => createRoutes(pageService),
+            multi: true,
+            deps: [PageService],
+        },
+    ],
     declarations: [
         TaxCategoryListComponent,
         TaxCategoryDetailComponent,
@@ -79,10 +89,11 @@ import { settingsRoutes } from './settings.routes';
         ZoneMemberControlsDirective,
         ZoneDetailDialogComponent,
         ProfileComponent,
+        TestShippingMethodsComponent,
     ],
 })
 export class SettingsModule {
-    constructor(private bulkActionRegistryService: BulkActionRegistryService) {
+    constructor(private bulkActionRegistryService: BulkActionRegistryService, pageService: PageService) {
         bulkActionRegistryService.registerBulkAction(deleteSellersBulkAction);
         bulkActionRegistryService.registerBulkAction(deleteChannelsBulkAction);
         bulkActionRegistryService.registerBulkAction(deleteAdministratorsBulkAction);
@@ -93,5 +104,72 @@ export class SettingsModule {
         bulkActionRegistryService.registerBulkAction(deleteCountriesBulkAction);
         bulkActionRegistryService.registerBulkAction(deleteZonesBulkAction);
         bulkActionRegistryService.registerBulkAction(removeZoneMembersBulkAction);
+
+        pageService.registerPageTab({
+            location: 'seller-list',
+            tab: _('breadcrumb.sellers'),
+            route: '',
+            component: SellerListComponent,
+        });
+        pageService.registerPageTab({
+            location: 'channel-list',
+            tab: _('breadcrumb.channels'),
+            route: '',
+            component: ChannelListComponent,
+        });
+        pageService.registerPageTab({
+            location: 'administrator-list',
+            tab: _('breadcrumb.administrators'),
+            route: '',
+            component: AdministratorListComponent,
+        });
+        pageService.registerPageTab({
+            location: 'role-list',
+            tab: _('breadcrumb.roles'),
+            route: '',
+            component: RoleListComponent,
+        });
+        pageService.registerPageTab({
+            location: 'shipping-method-list',
+            tab: _('breadcrumb.shipping-methods'),
+            route: '',
+            component: ShippingMethodListComponent,
+        });
+        pageService.registerPageTab({
+            location: 'shipping-method-list',
+            tab: _('settings.test-shipping-methods'),
+            route: 'test',
+            component: TestShippingMethodsComponent,
+        });
+        pageService.registerPageTab({
+            location: 'payment-method-list',
+            tab: _('breadcrumb.payment-methods'),
+            route: '',
+            component: PaymentMethodListComponent,
+        });
+        pageService.registerPageTab({
+            location: 'tax-category-list',
+            tab: _('breadcrumb.tax-categories'),
+            route: '',
+            component: TaxCategoryListComponent,
+        });
+        pageService.registerPageTab({
+            location: 'tax-rate-list',
+            tab: _('breadcrumb.tax-rates'),
+            route: '',
+            component: TaxRateListComponent,
+        });
+        pageService.registerPageTab({
+            location: 'country-list',
+            tab: _('breadcrumb.countries'),
+            route: '',
+            component: CountryListComponent,
+        });
+        pageService.registerPageTab({
+            location: 'zone-list',
+            tab: _('breadcrumb.zones'),
+            route: '',
+            component: ZoneListComponent,
+        });
     }
 }

+ 33 - 11
packages/admin-ui/src/lib/settings/src/settings.routes.ts

@@ -7,6 +7,8 @@ import {
     CountryFragment,
     createResolveData,
     detailBreadcrumb,
+    PageComponent,
+    PageService,
     PaymentMethodFragment,
     Role,
     Seller,
@@ -48,7 +50,7 @@ import { ShippingMethodResolver } from './providers/routing/shipping-method-reso
 import { TaxCategoryResolver } from './providers/routing/tax-category-resolver';
 import { TaxRateResolver } from './providers/routing/tax-rate-resolver';
 
-export const settingsRoutes: Route[] = [
+export const createRoutes = (pageService: PageService): Route[] => [
     {
         path: 'profile',
         component: ProfileComponent,
@@ -60,10 +62,12 @@ export const settingsRoutes: Route[] = [
     },
     {
         path: 'administrators',
-        component: AdministratorListComponent,
+        component: PageComponent,
         data: {
+            locationId: 'administrator-list',
             breadcrumb: _('breadcrumb.administrators'),
         },
+        children: pageService.getPageTabRoutes('administrator-list'),
     },
     {
         path: 'administrators/:id',
@@ -74,10 +78,12 @@ export const settingsRoutes: Route[] = [
     },
     {
         path: 'channels',
-        component: ChannelListComponent,
+        component: PageComponent,
         data: {
+            locationId: 'channel-list',
             breadcrumb: _('breadcrumb.channels'),
         },
+        children: pageService.getPageTabRoutes('channel-list'),
     },
     {
         path: 'channels/:id',
@@ -88,10 +94,12 @@ export const settingsRoutes: Route[] = [
     },
     {
         path: 'sellers',
-        component: SellerListComponent,
+        component: PageComponent,
         data: {
+            locationId: 'seller-list',
             breadcrumb: _('breadcrumb.sellers'),
         },
+        children: pageService.getPageTabRoutes('seller-list'),
     },
     {
         path: 'sellers/:id',
@@ -102,10 +110,12 @@ export const settingsRoutes: Route[] = [
     },
     {
         path: 'roles',
-        component: RoleListComponent,
+        component: PageComponent,
         data: {
+            locationId: 'role-list',
             breadcrumb: _('breadcrumb.roles'),
         },
+        children: pageService.getPageTabRoutes('role-list'),
     },
     {
         path: 'roles/:id',
@@ -116,10 +126,12 @@ export const settingsRoutes: Route[] = [
     },
     {
         path: 'tax-categories',
-        component: TaxCategoryListComponent,
+        component: PageComponent,
         data: {
+            locationId: 'tax-category-list',
             breadcrumb: _('breadcrumb.tax-categories'),
         },
+        children: pageService.getPageTabRoutes('tax-category-list'),
     },
     {
         path: 'tax-categories/:id',
@@ -132,10 +144,12 @@ export const settingsRoutes: Route[] = [
     },
     {
         path: 'tax-rates',
-        component: TaxRateListComponent,
+        component: PageComponent,
         data: {
+            locationId: 'tax-rate-list',
             breadcrumb: _('breadcrumb.tax-rates'),
         },
+        children: pageService.getPageTabRoutes('tax-rate-list'),
     },
     {
         path: 'tax-rates/:id',
@@ -148,10 +162,12 @@ export const settingsRoutes: Route[] = [
     },
     {
         path: 'countries',
-        component: CountryListComponent,
+        component: PageComponent,
         data: {
+            locationId: 'country-list',
             breadcrumb: _('breadcrumb.countries'),
         },
+        children: pageService.getPageTabRoutes('country-list'),
     },
     {
         path: 'countries/:id',
@@ -164,17 +180,21 @@ export const settingsRoutes: Route[] = [
     },
     {
         path: 'zones',
-        component: ZoneListComponent,
+        component: PageComponent,
         data: {
+            locationId: 'zone-list',
             breadcrumb: _('breadcrumb.zones'),
         },
+        children: pageService.getPageTabRoutes('zone-list'),
     },
     {
         path: 'shipping-methods',
-        component: ShippingMethodListComponent,
+        component: PageComponent,
         data: {
+            locationId: 'shipping-method-list',
             breadcrumb: _('breadcrumb.shipping-methods'),
         },
+        children: pageService.getPageTabRoutes('shipping-method-list'),
     },
     {
         path: 'shipping-methods/:id',
@@ -187,10 +207,12 @@ export const settingsRoutes: Route[] = [
     },
     {
         path: 'payment-methods',
-        component: PaymentMethodListComponent,
+        component: PageComponent,
         data: {
+            locationId: 'payment-method-list',
             breadcrumb: _('breadcrumb.payment-methods'),
         },
+        children: pageService.getPageTabRoutes('payment-method-list'),
     },
     {
         path: 'payment-methods/:id',