Przeglądaj źródła

feat(admin-ui): First pass updated list views

Michael Bromley 2 lat temu
rodzic
commit
22726a6869
63 zmienionych plików z 2562 dodań i 1143 usunięć
  1. 39 39
      packages/admin-ui/i18n-coverage.json
  2. 8 5
      packages/admin-ui/src/lib/catalog/src/components/assign-products-to-channel-dialog/assign-products-to-channel-dialog.component.ts
  3. 3 3
      packages/admin-ui/src/lib/catalog/src/components/assign-to-channel-dialog/assign-to-channel-dialog.component.ts
  4. 10 0
      packages/admin-ui/src/lib/catalog/src/components/facet-list/facet-list.component.html
  5. 17 16
      packages/admin-ui/src/lib/catalog/src/components/facet-list/facet-list.component.ts
  6. 1 3
      packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.ts
  7. 10 8
      packages/admin-ui/src/lib/catalog/src/providers/product-detail/product-detail.service.ts
  8. 2 0
      packages/admin-ui/src/lib/core/src/common/component-registry-types.ts
  9. 93 6
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  10. 2 0
      packages/admin-ui/src/lib/core/src/common/introspection-result.ts
  11. 12 7
      packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts
  12. 20 6
      packages/admin-ui/src/lib/core/src/data/providers/settings-data.service.ts
  13. 9 5
      packages/admin-ui/src/lib/core/src/providers/data-table/data-table-filter-collection.ts
  14. 73 13
      packages/admin-ui/src/lib/core/src/providers/data-table/data-table-filter.ts
  15. 4 4
      packages/admin-ui/src/lib/core/src/providers/data-table/data-table-sort-collection.ts
  16. 7 7
      packages/admin-ui/src/lib/core/src/providers/data-table/data-table-sort.ts
  17. 16 1
      packages/admin-ui/src/lib/core/src/public_api.ts
  18. 23 8
      packages/admin-ui/src/lib/core/src/shared/components/data-table-filter-label/data-table-filter-label.component.html
  19. 55 31
      packages/admin-ui/src/lib/core/src/shared/components/data-table-filters/data-table-filters.component.ts
  20. 67 63
      packages/admin-ui/src/lib/customer/src/components/customer-list/customer-list.component.html
  21. 63 12
      packages/admin-ui/src/lib/customer/src/components/customer-list/customer-list.component.ts
  22. 83 78
      packages/admin-ui/src/lib/marketing/src/components/promotion-list/promotion-list.component.html
  23. 94 19
      packages/admin-ui/src/lib/marketing/src/components/promotion-list/promotion-list.component.ts
  24. 7 2
      packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.html
  25. 21 46
      packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.ts
  26. 61 60
      packages/admin-ui/src/lib/settings/src/components/administrator-list/administrator-list.component.html
  27. 72 20
      packages/admin-ui/src/lib/settings/src/components/administrator-list/administrator-list.component.ts
  28. 1 1
      packages/admin-ui/src/lib/settings/src/components/channel-detail/channel-detail.component.ts
  29. 64 46
      packages/admin-ui/src/lib/settings/src/components/channel-list/channel-list.component.html
  30. 97 11
      packages/admin-ui/src/lib/settings/src/components/channel-list/channel-list.component.ts
  31. 64 75
      packages/admin-ui/src/lib/settings/src/components/country-list/country-list.component.html
  32. 89 46
      packages/admin-ui/src/lib/settings/src/components/country-list/country-list.component.ts
  33. 74 10
      packages/admin-ui/src/lib/settings/src/components/payment-method-list/payment-method-list.component.html
  34. 95 7
      packages/admin-ui/src/lib/settings/src/components/payment-method-list/payment-method-list.component.ts
  35. 104 77
      packages/admin-ui/src/lib/settings/src/components/role-list/role-list.component.html
  36. 7 0
      packages/admin-ui/src/lib/settings/src/components/role-list/role-list.component.scss
  37. 75 7
      packages/admin-ui/src/lib/settings/src/components/role-list/role-list.component.ts
  38. 58 43
      packages/admin-ui/src/lib/settings/src/components/seller-list/seller-list.component.html
  39. 84 11
      packages/admin-ui/src/lib/settings/src/components/seller-list/seller-list.component.ts
  40. 95 83
      packages/admin-ui/src/lib/settings/src/components/shipping-method-list/shipping-method-list.component.html
  41. 80 4
      packages/admin-ui/src/lib/settings/src/components/shipping-method-list/shipping-method-list.component.ts
  42. 65 45
      packages/admin-ui/src/lib/settings/src/components/tax-category-list/tax-category-list.component.html
  43. 85 10
      packages/admin-ui/src/lib/settings/src/components/tax-category-list/tax-category-list.component.ts
  44. 1 1
      packages/admin-ui/src/lib/settings/src/components/tax-rate-detail/tax-rate-detail.component.ts
  45. 77 54
      packages/admin-ui/src/lib/settings/src/components/tax-rate-list/tax-rate-list.component.html
  46. 88 7
      packages/admin-ui/src/lib/settings/src/components/tax-rate-list/tax-rate-list.component.ts
  47. 8 3
      packages/admin-ui/src/lib/static/i18n-messages/cs.json
  48. 8 3
      packages/admin-ui/src/lib/static/i18n-messages/de.json
  49. 9 4
      packages/admin-ui/src/lib/static/i18n-messages/en.json
  50. 8 3
      packages/admin-ui/src/lib/static/i18n-messages/es.json
  51. 8 3
      packages/admin-ui/src/lib/static/i18n-messages/fr.json
  52. 8 3
      packages/admin-ui/src/lib/static/i18n-messages/it.json
  53. 8 3
      packages/admin-ui/src/lib/static/i18n-messages/pl.json
  54. 8 3
      packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json
  55. 8 3
      packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json
  56. 8 3
      packages/admin-ui/src/lib/static/i18n-messages/ru.json
  57. 8 3
      packages/admin-ui/src/lib/static/i18n-messages/uk.json
  58. 8 3
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json
  59. 8 3
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json
  60. 34 0
      packages/admin-ui/src/lib/static/styles/global/_badges.scss
  61. 140 0
      packages/admin-ui/src/lib/static/styles/global/_clarity.scss
  62. 107 100
      packages/admin-ui/src/lib/system/src/components/job-list/job-list.component.html
  63. 1 3
      packages/admin-ui/src/lib/system/src/components/job-list/job-list.component.ts

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

@@ -1,70 +1,70 @@
 {
-  "generatedOn": "2023-04-25T20:32:51.405Z",
-  "lastCommit": "f03d16161ebfce6b6897d4f004587abd98950446",
+  "generatedOn": "2023-04-28T12:40:43.278Z",
+  "lastCommit": "f29aae9781b79ffa3c257d93639b844bf32fe6fa",
   "translationStatus": {
     "cs": {
-      "tokenCount": 705,
-      "translatedCount": 585,
-      "percentage": 83
+      "tokenCount": 710,
+      "translatedCount": 584,
+      "percentage": 82
     },
     "de": {
-      "tokenCount": 705,
-      "translatedCount": 568,
-      "percentage": 81
+      "tokenCount": 710,
+      "translatedCount": 567,
+      "percentage": 80
     },
     "en": {
-      "tokenCount": 705,
-      "translatedCount": 705,
-      "percentage": 100
+      "tokenCount": 710,
+      "translatedCount": 702,
+      "percentage": 99
     },
     "es": {
-      "tokenCount": 705,
-      "translatedCount": 616,
-      "percentage": 87
+      "tokenCount": 710,
+      "translatedCount": 613,
+      "percentage": 86
     },
     "fr": {
-      "tokenCount": 705,
-      "translatedCount": 606,
-      "percentage": 86
+      "tokenCount": 710,
+      "translatedCount": 605,
+      "percentage": 85
     },
     "it": {
-      "tokenCount": 705,
-      "translatedCount": 614,
-      "percentage": 87
+      "tokenCount": 710,
+      "translatedCount": 611,
+      "percentage": 86
     },
     "pl": {
-      "tokenCount": 705,
-      "translatedCount": 408,
-      "percentage": 58
+      "tokenCount": 710,
+      "translatedCount": 407,
+      "percentage": 57
     },
     "pt_BR": {
-      "tokenCount": 705,
-      "translatedCount": 583,
-      "percentage": 83
+      "tokenCount": 710,
+      "translatedCount": 582,
+      "percentage": 82
     },
     "pt_PT": {
-      "tokenCount": 705,
-      "translatedCount": 627,
-      "percentage": 89
+      "tokenCount": 710,
+      "translatedCount": 624,
+      "percentage": 88
     },
     "ru": {
-      "tokenCount": 705,
-      "translatedCount": 613,
-      "percentage": 87
+      "tokenCount": 710,
+      "translatedCount": 610,
+      "percentage": 86
     },
     "uk": {
-      "tokenCount": 705,
-      "translatedCount": 613,
-      "percentage": 87
+      "tokenCount": 710,
+      "translatedCount": 610,
+      "percentage": 86
     },
     "zh_Hans": {
-      "tokenCount": 705,
-      "translatedCount": 553,
+      "tokenCount": 710,
+      "translatedCount": 552,
       "percentage": 78
     },
     "zh_Hant": {
-      "tokenCount": 705,
-      "translatedCount": 388,
+      "tokenCount": 710,
+      "translatedCount": 387,
       "percentage": 55
     }
   }

+ 8 - 5
packages/admin-ui/src/lib/catalog/src/components/assign-products-to-channel-dialog/assign-products-to-channel-dialog.component.ts

@@ -5,13 +5,14 @@ import {
     DataService,
     Dialog,
     GetChannelsQuery,
+    ItemOf,
     NotificationService,
     ProductVariantFragment,
 } from '@vendure/admin-ui/core';
 import { combineLatest, from, Observable } from 'rxjs';
 import { map, startWith } from 'rxjs/operators';
 
-type Channel = GetChannelsQuery['channels'][number];
+type Channel = ItemOf<GetChannelsQuery, 'channels'>;
 
 @Component({
     selector: 'vdr-assign-products-to-channel-dialog',
@@ -47,8 +48,8 @@ export class AssignProductsToChannelDialogComponent implements OnInit, Dialog<an
 
         combineLatest(activeChannelId$, allChannels$).subscribe(([activeChannelId, channels]) => {
             // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-            this.currentChannel = channels.find(c => c.id === activeChannelId)!;
-            this.availableChannels = channels;
+            this.currentChannel = channels.items.find(c => c.id === activeChannelId)!;
+            this.availableChannels = channels.items;
         });
 
         this.selectedChannelIdControl.valueChanges.subscribe(ids => {
@@ -59,12 +60,14 @@ export class AssignProductsToChannelDialogComponent implements OnInit, Dialog<an
             from(this.getTopVariants(10)),
             this.priceFactorControl.valueChanges.pipe(startWith(1)),
         ).pipe(
-            map(([variants, factor]) => variants.map(v => ({
+            map(([variants, factor]) =>
+                variants.map(v => ({
                     id: v.id,
                     name: v.name,
                     price: v.price,
                     pricePreview: v.price * +factor,
-                }))),
+                })),
+            ),
         );
     }
 

+ 3 - 3
packages/admin-ui/src/lib/catalog/src/components/assign-to-channel-dialog/assign-to-channel-dialog.component.ts

@@ -3,7 +3,7 @@ import { UntypedFormControl } from '@angular/forms';
 import { DataService, Dialog, GetChannelsQuery, ItemOf, NotificationService } from '@vendure/admin-ui/core';
 import { combineLatest } from 'rxjs';
 
-type Channel = GetChannelsQuery['channels'][number];
+type Channel = ItemOf<GetChannelsQuery, 'channels'>;
 
 @Component({
     selector: 'vdr-assign-to-channel-dialog',
@@ -30,8 +30,8 @@ export class AssignToChannelDialogComponent implements OnInit, Dialog<Channel> {
 
         combineLatest(activeChannelId$, allChannels$).subscribe(([activeChannelId, channels]) => {
             // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-            this.currentChannel = channels.find(c => c.id === activeChannelId)!;
-            this.availableChannels = channels;
+            this.currentChannel = channels.items.find(c => c.id === activeChannelId)!;
+            this.availableChannels = channels.items;
         });
 
         this.selectedChannelIdControl.valueChanges.subscribe(ids => {

+ 10 - 0
packages/admin-ui/src/lib/catalog/src/components/facet-list/facet-list.component.html

@@ -38,6 +38,16 @@
                 [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-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"

+ 17 - 16
packages/admin-ui/src/lib/catalog/src/components/facet-list/facet-list.component.ts

@@ -37,7 +37,11 @@ export class FacetListComponent
     contentLanguage$: Observable<LanguageCode>;
     readonly initialLimit = 3;
     displayLimit: { [id: string]: number } = {};
-    selectionManager: SelectionManager<ItemOf<GetFacetListQuery, 'facets'>>;
+    selectionManager = new SelectionManager<ItemOf<GetFacetListQuery, 'facets'>>({
+        multiSelect: true,
+        itemsAreEqual: (a, b) => a.id === b.id,
+        additiveMode: true,
+    });
 
     readonly filters = this.dataTableService
         .createFilterCollection<FacetFilterParameter>()
@@ -45,16 +49,20 @@ export class FacetListComponent
             name: 'createdAt',
             type: { kind: 'dateRange' },
             label: _('common.created-at'),
-            toFilterInput: value => ({
-                createdAt: value.dateOperators,
-            }),
+            filterField: 'createdAt',
+        })
+        .addFilter({
+            name: 'updatedAt',
+            type: { kind: 'dateRange' },
+            label: _('common.updated-at'),
+            filterField: 'updatedAt',
         })
         .addFilter({
             name: 'visibility',
             type: { kind: 'boolean' },
             label: _('common.visibility'),
             toFilterInput: value => ({
-                isPrivate: { eq: value },
+                isPrivate: { eq: !value },
             }),
         })
         .connectToRoute(this.route);
@@ -62,12 +70,10 @@ export class FacetListComponent
     readonly sorts = this.dataTableService
         .createSortCollection<FacetSortParameter>()
         .defaultSort('createdAt', 'DESC')
-        .addSort({
-            name: 'name',
-        })
-        .addSort({
-            name: 'code',
-        })
+        .addSort({ name: 'createdAt' })
+        .addSort({ name: 'updatedAt' })
+        .addSort({ name: 'name' })
+        .addSort({ name: 'code' })
         .connectToRoute(this.route);
 
     constructor(
@@ -97,11 +103,6 @@ export class FacetListComponent
                 },
             }),
         );
-        this.selectionManager = new SelectionManager({
-            multiSelect: true,
-            itemsAreEqual: (a, b) => a.id === b.id,
-            additiveMode: true,
-        });
     }
 
     ngOnInit() {

+ 1 - 3
packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.ts

@@ -49,9 +49,7 @@ export class ProductListComponent
             name: 'collectionSlug',
             type: { kind: 'text' },
             label: _('catalog.collection-slug'),
-            toFilterInput: value => ({
-                collectionSlug: value.term,
-            }),
+            filterField: 'collectionSlug',
         })
         .connectToRoute(this.route);
 

+ 10 - 8
packages/admin-ui/src/lib/catalog/src/providers/product-detail/product-detail.service.ts

@@ -36,7 +36,7 @@ export class ProductDetailService {
     getTaxCategories() {
         return this.dataService.settings
             .getTaxCategories()
-            .mapSingle(data => data.taxCategories)
+            .mapSingle(data => data.taxCategories.items)
             .pipe(shareReplay(1));
     }
 
@@ -53,15 +53,15 @@ export class ProductDetailService {
             mergeMap(([{ createProduct }, optionGroups]) => {
                 const addOptionsToProduct$ = optionGroups.length
                     ? forkJoin(
-                          optionGroups.map(optionGroup => this.dataService.product.addOptionGroupToProduct({
+                          optionGroups.map(optionGroup =>
+                              this.dataService.product.addOptionGroupToProduct({
                                   productId: createProduct.id,
                                   optionGroupId: optionGroup.id,
-                              })),
+                              }),
+                          ),
                       )
                     : of([]);
-                return addOptionsToProduct$.pipe(
-                    map(() => ({ createProduct, optionGroups })),
-                );
+                return addOptionsToProduct$.pipe(map(() => ({ createProduct, optionGroups })));
             }),
             mergeMap(({ createProduct, optionGroups }) => {
                 const variants = createVariantsConfig.variants.map(v => {
@@ -90,7 +90,8 @@ export class ProductDetailService {
     createProductOptionGroups(groups: Array<{ name: string; values: string[] }>, languageCode: LanguageCode) {
         return groups.length
             ? forkJoin(
-                  groups.map(c => this.dataService.product
+                  groups.map(c =>
+                      this.dataService.product
                           .createProductOptionGroups({
                               code: normalizeString(c.name, '-'),
                               translations: [{ languageCode, name: c.name }],
@@ -99,7 +100,8 @@ export class ProductDetailService {
                                   translations: [{ languageCode, name: v }],
                               })),
                           })
-                          .pipe(map(data => data.createProductOptionGroup))),
+                          .pipe(map(data => data.createProductOptionGroup)),
+                  ),
               )
             : of([]);
     }

+ 2 - 0
packages/admin-ui/src/lib/core/src/common/component-registry-types.ts

@@ -77,6 +77,8 @@ export type ActionBarLocationId =
     | 'promotion-list'
     | 'role-detail'
     | 'role-list'
+    | 'seller-detail'
+    | 'seller-list'
     | 'shipping-method-detail'
     | 'shipping-method-list'
     | 'tax-category-detail'

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

@@ -379,6 +379,45 @@ export type ChannelDefaultLanguageError = ErrorResult & {
   message: Scalars['String'];
 };
 
+export type ChannelFilterParameter = {
+  code?: InputMaybe<StringOperators>;
+  createdAt?: InputMaybe<DateOperators>;
+  currencyCode?: InputMaybe<StringOperators>;
+  defaultCurrencyCode?: InputMaybe<StringOperators>;
+  defaultLanguageCode?: InputMaybe<StringOperators>;
+  id?: InputMaybe<IdOperators>;
+  pricesIncludeTax?: InputMaybe<BooleanOperators>;
+  token?: InputMaybe<StringOperators>;
+  updatedAt?: InputMaybe<DateOperators>;
+};
+
+export type ChannelList = PaginatedList & {
+  __typename?: 'ChannelList';
+  items: Array<Channel>;
+  totalItems: Scalars['Int'];
+};
+
+export type ChannelListOptions = {
+  /** Allows the results to be filtered */
+  filter?: InputMaybe<ChannelFilterParameter>;
+  /** Specifies whether multiple "filter" arguments should be combines with a logical AND or OR operation. Defaults to AND. */
+  filterOperator?: InputMaybe<LogicalOperator>;
+  /** Skips the first n results, for use in pagination */
+  skip?: InputMaybe<Scalars['Int']>;
+  /** Specifies which properties to sort the results by */
+  sort?: InputMaybe<ChannelSortParameter>;
+  /** Takes n results, for use in pagination */
+  take?: InputMaybe<Scalars['Int']>;
+};
+
+export type ChannelSortParameter = {
+  code?: InputMaybe<SortOrder>;
+  createdAt?: InputMaybe<SortOrder>;
+  id?: InputMaybe<SortOrder>;
+  token?: InputMaybe<SortOrder>;
+  updatedAt?: InputMaybe<SortOrder>;
+};
+
 export type Collection = Node & {
   __typename?: 'Collection';
   assets: Array<Asset>;
@@ -4679,7 +4718,7 @@ export type Query = {
   /** Get a list of Assets */
   assets: AssetList;
   channel?: Maybe<Channel>;
-  channels: Array<Channel>;
+  channels: ChannelList;
   /** Get a Collection either by id or slug. If neither id nor slug is specified, an error will result. */
   collection?: Maybe<Collection>;
   collectionFilters: Array<ConfigurableOperationDefinition>;
@@ -4742,7 +4781,7 @@ export type Query = {
   stockLocations: StockLocationList;
   tag: Tag;
   tags: TagList;
-  taxCategories: Array<TaxCategory>;
+  taxCategories: TaxCategoryList;
   taxCategory?: Maybe<TaxCategory>;
   taxRate?: Maybe<TaxRate>;
   taxRates: TaxRateList;
@@ -4780,6 +4819,11 @@ export type QueryChannelArgs = {
 };
 
 
+export type QueryChannelsArgs = {
+  options?: InputMaybe<ChannelListOptions>;
+};
+
+
 export type QueryCollectionArgs = {
   id?: InputMaybe<Scalars['ID']>;
   slug?: InputMaybe<Scalars['String']>;
@@ -4994,6 +5038,11 @@ export type QueryTagsArgs = {
 };
 
 
+export type QueryTaxCategoriesArgs = {
+  options?: InputMaybe<TaxCategoryListOptions>;
+};
+
+
 export type QueryTaxCategoryArgs = {
   id: Scalars['ID'];
 };
@@ -5706,6 +5755,40 @@ export type TaxCategory = Node & {
   updatedAt: Scalars['DateTime'];
 };
 
+export type TaxCategoryFilterParameter = {
+  createdAt?: InputMaybe<DateOperators>;
+  id?: InputMaybe<IdOperators>;
+  isDefault?: InputMaybe<BooleanOperators>;
+  name?: InputMaybe<StringOperators>;
+  updatedAt?: InputMaybe<DateOperators>;
+};
+
+export type TaxCategoryList = PaginatedList & {
+  __typename?: 'TaxCategoryList';
+  items: Array<TaxCategory>;
+  totalItems: Scalars['Int'];
+};
+
+export type TaxCategoryListOptions = {
+  /** Allows the results to be filtered */
+  filter?: InputMaybe<TaxCategoryFilterParameter>;
+  /** Specifies whether multiple "filter" arguments should be combines with a logical AND or OR operation. Defaults to AND. */
+  filterOperator?: InputMaybe<LogicalOperator>;
+  /** Skips the first n results, for use in pagination */
+  skip?: InputMaybe<Scalars['Int']>;
+  /** Specifies which properties to sort the results by */
+  sort?: InputMaybe<TaxCategorySortParameter>;
+  /** Takes n results, for use in pagination */
+  take?: InputMaybe<Scalars['Int']>;
+};
+
+export type TaxCategorySortParameter = {
+  createdAt?: InputMaybe<SortOrder>;
+  id?: InputMaybe<SortOrder>;
+  name?: InputMaybe<SortOrder>;
+  updatedAt?: InputMaybe<SortOrder>;
+};
+
 export type TaxLine = {
   __typename?: 'TaxLine';
   description: Scalars['String'];
@@ -7395,10 +7478,12 @@ export type RemoveMembersFromZoneMutation = { removeMembersFromZone: { __typenam
 
 export type TaxCategoryFragment = { __typename?: 'TaxCategory', id: string, createdAt: any, updatedAt: any, name: string, isDefault: boolean };
 
-export type GetTaxCategoriesQueryVariables = Exact<{ [key: string]: never; }>;
+export type GetTaxCategoriesQueryVariables = Exact<{
+  options?: InputMaybe<TaxCategoryListOptions>;
+}>;
 
 
-export type GetTaxCategoriesQuery = { taxCategories: Array<{ __typename?: 'TaxCategory', id: string, createdAt: any, updatedAt: any, name: string, isDefault: boolean }> };
+export type GetTaxCategoriesQuery = { taxCategories: { __typename?: 'TaxCategoryList', totalItems: number, items: Array<{ __typename?: 'TaxCategory', id: string, createdAt: any, updatedAt: any, name: string, isDefault: boolean }> } };
 
 export type GetTaxCategoryQueryVariables = Exact<{
   id: Scalars['ID'];
@@ -7476,10 +7561,12 @@ export type ChannelFragment = { __typename?: 'Channel', id: string, createdAt: a
 
 export type SellerFragment = { __typename?: 'Seller', id: string, createdAt: any, updatedAt: any, name: string };
 
-export type GetChannelsQueryVariables = Exact<{ [key: string]: never; }>;
+export type GetChannelsQueryVariables = Exact<{
+  options?: InputMaybe<ChannelListOptions>;
+}>;
 
 
-export type GetChannelsQuery = { channels: Array<{ __typename?: 'Channel', id: string, createdAt: any, updatedAt: any, code: string, token: string, pricesIncludeTax: boolean, currencyCode: CurrencyCode, defaultLanguageCode: LanguageCode, defaultShippingZone?: { __typename?: 'Zone', id: string, name: string } | null, defaultTaxZone?: { __typename?: 'Zone', id: string, name: string } | null, seller?: { __typename?: 'Seller', id: string, name: string } | null }> };
+export type GetChannelsQuery = { channels: { __typename?: 'ChannelList', totalItems: number, items: Array<{ __typename?: 'Channel', id: string, createdAt: any, updatedAt: any, code: string, token: string, pricesIncludeTax: boolean, currencyCode: CurrencyCode, defaultLanguageCode: LanguageCode, defaultShippingZone?: { __typename?: 'Zone', id: string, name: string } | null, defaultTaxZone?: { __typename?: 'Zone', id: string, name: string } | null, seller?: { __typename?: 'Seller', id: string, name: string } | null }> } };
 
 export type GetChannelQueryVariables = Exact<{
   id: Scalars['ID'];

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

@@ -166,6 +166,7 @@ const result: PossibleTypesResultData = {
         PaginatedList: [
             'AdministratorList',
             'AssetList',
+            'ChannelList',
             'CollectionList',
             'CountryList',
             'CustomerGroupList',
@@ -185,6 +186,7 @@ const result: PossibleTypesResultData = {
             'ShippingMethodList',
             'StockLocationList',
             'TagList',
+            'TaxCategoryList',
             'TaxRateList',
         ],
         RefundOrderResult: [

+ 12 - 7
packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts

@@ -180,9 +180,12 @@ export const TAX_CATEGORY_FRAGMENT = gql`
 `;
 
 export const GET_TAX_CATEGORIES = gql`
-    query GetTaxCategories {
-        taxCategories {
-            ...TaxCategory
+    query GetTaxCategories($options: TaxCategoryListOptions) {
+        taxCategories(options: $options) {
+            items {
+                ...TaxCategory
+            }
+            totalItems
         }
     }
     ${TAX_CATEGORY_FRAGMENT}
@@ -353,11 +356,13 @@ export const SELLER_FRAGMENT = gql`
     }
 `;
 
-// TODO v2: change this to paginated list
 export const GET_CHANNELS = gql`
-    query GetChannels {
-        channels {
-            ...Channel
+    query GetChannels($options: ChannelListOptions) {
+        channels(options: $options) {
+            items {
+                ...Channel
+            }
+            totalItems
         }
     }
     ${CHANNEL_FRAGMENT}

+ 20 - 6
packages/admin-ui/src/lib/core/src/data/providers/settings-data.service.ts

@@ -2,7 +2,13 @@ import { FetchPolicy, WatchQueryFetchPolicy } from '@apollo/client/core';
 import { pick } from '@vendure/common/lib/pick';
 
 import * as Codegen from '../../common/generated-types';
-import { JobListOptions, JobState, SellerListOptions } from '../../common/generated-types';
+import {
+    ChannelListOptions,
+    JobListOptions,
+    JobState,
+    SellerListOptions,
+    TaxCategoryListOptions,
+} from '../../common/generated-types';
 import {
     ADD_MEMBERS_TO_ZONE,
     CANCEL_JOB,
@@ -168,8 +174,13 @@ export class SettingsDataService {
         });
     }
 
-    getTaxCategories() {
-        return this.baseDataService.query<Codegen.GetTaxCategoriesQuery>(GET_TAX_CATEGORIES);
+    getTaxCategories(options: TaxCategoryListOptions = {}) {
+        return this.baseDataService.query<
+            Codegen.GetTaxCategoriesQuery,
+            Codegen.GetTaxCategoriesQueryVariables
+        >(GET_TAX_CATEGORIES, {
+            options,
+        });
     }
 
     getTaxCategory(id: string) {
@@ -273,8 +284,11 @@ export class SettingsDataService {
         });
     }
 
-    getChannels() {
-        return this.baseDataService.query<Codegen.GetChannelsQuery>(GET_CHANNELS);
+    getChannels(options: ChannelListOptions = {}) {
+        return this.baseDataService.query<Codegen.GetChannelsQuery, Codegen.GetChannelsQueryVariables>(
+            GET_CHANNELS,
+            { options },
+        );
     }
 
     getChannel(id: string) {
@@ -286,7 +300,7 @@ export class SettingsDataService {
         );
     }
 
-    getSellers(options?: SellerListOptions) {
+    getSellerList(options?: SellerListOptions) {
         return this.baseDataService.query<Codegen.GetSellersQuery, Codegen.GetSellersQueryVariables>(
             GET_SELLERS,
             { options },

+ 9 - 5
packages/admin-ui/src/lib/core/src/providers/data-table/data-table-filter-collection.ts

@@ -2,6 +2,7 @@ import { ActivatedRoute, Router } from '@angular/router';
 import { assertNever } from '@vendure/common/lib/shared-utils';
 import { Subject } from 'rxjs';
 import extend from 'just-extend';
+import { NumberOperators, StringOperators } from '../../common/generated-types';
 import {
     DataTableFilter,
     DataTableFilterBooleanType,
@@ -153,15 +154,15 @@ export class DataTableFilterCollection<FilterInput extends Record<string, any> =
         }
     }
 
-    deserializeValue(filter: DataTableFilter, value: string): any {
+    deserializeValue(filter: DataTableFilter, value: string): DataTableFilterValue<DataTableFilterType> {
         switch (filter.type.kind) {
             case 'text': {
-                const [operator, term] = value.split(',');
+                const [operator, term] = value.split(',') as [keyof StringOperators, string];
                 return { operator, term };
             }
             case 'number': {
-                const [operator, amount] = value.split(',');
-                return { operator, amount };
+                const [operator, amount] = value.split(',') as [keyof NumberOperators, string];
+                return { operator, amount: +amount };
             }
             case 'select':
                 return value.split(',');
@@ -191,7 +192,10 @@ export class DataTableFilterCollection<FilterInput extends Record<string, any> =
         this.#valueChanges$.next(this.#activeFilters);
     }
 
-    private createFacetWithValue(filter: DataTableFilter<any, any>, value: DataTableFilterValue<any>) {
+    private createFacetWithValue(
+        filter: DataTableFilter<any, any>,
+        value: DataTableFilterValue<DataTableFilterType>,
+    ) {
         return new FilterWithValue(filter, value, v => this.#valueChanges$.next(v));
     }
 }

+ 73 - 13
packages/admin-ui/src/lib/core/src/providers/data-table/data-table-filter.ts

@@ -1,5 +1,10 @@
 import { assertNever } from '@vendure/common/lib/shared-utils';
-import { DateOperators } from '../../common/generated-types';
+import {
+    BooleanOperators,
+    DateOperators,
+    NumberOperators,
+    StringOperators,
+} from '../../common/generated-types';
 
 export interface DataTableFilterTextType {
     kind: 'text';
@@ -25,11 +30,17 @@ export interface DataTableFilterDateRangeType {
 }
 
 export type KindValueMap = {
-    text: { operator: 'contains' | 'eq' | 'notContains' | 'notEq' | 'regex'; term: string };
-    select: string[];
-    boolean: boolean;
-    dateRange: { start?: string; end?: string; dateOperators: DateOperators };
-    number: { operator: 'eq' | 'gt' | 'lt'; amount: string };
+    text: {
+        raw: {
+            operator: keyof StringOperators;
+            term: string;
+        };
+        input: StringOperators;
+    };
+    select: { raw: string[]; input: StringOperators };
+    boolean: { raw: boolean; input: BooleanOperators };
+    dateRange: { raw: { start?: string; end?: string }; input: DateOperators };
+    number: { raw: { operator: keyof NumberOperators; amount: number }; input: NumberOperators };
 };
 export type DataTableFilterType =
     | DataTableFilterTextType
@@ -45,10 +56,12 @@ export interface DataTableFilterOptions<
     readonly name: string;
     readonly type: Type;
     readonly label: string;
-    readonly toFilterInput: (value: DataTableFilterValue<Type>) => Partial<FilterInput>;
+    readonly filterField?: keyof FilterInput;
+    readonly toFilterInput?: (value: DataTableFilterValue<Type>) => Partial<FilterInput>;
 }
 
-export type DataTableFilterValue<Type extends DataTableFilterType> = KindValueMap[Type['kind']];
+export type DataTableFilterValue<Type extends DataTableFilterType> = KindValueMap[Type['kind']]['raw'];
+export type DataTableFilterOperator<Type extends DataTableFilterType> = KindValueMap[Type['kind']]['input'];
 
 export class DataTableFilter<
     FilterInput extends Record<string, any> = any,
@@ -74,8 +87,59 @@ export class DataTableFilter<
         return this.options.label;
     }
 
+    getFilterOperator(value: any): DataTableFilterOperator<Type> {
+        const type = this.options.type;
+        switch (type.kind) {
+            case 'boolean':
+                return {
+                    eq: !!value,
+                };
+            case 'dateRange': {
+                let dateOperators: DateOperators;
+                const start = value.start ?? undefined;
+                const end = value.end ?? undefined;
+                if (start && end) {
+                    dateOperators = {
+                        between: { start, end },
+                    };
+                } else if (start) {
+                    dateOperators = {
+                        after: start,
+                    };
+                } else {
+                    dateOperators = {
+                        before: end,
+                    };
+                }
+                return dateOperators;
+            }
+            case 'number':
+                return {
+                    [value.operator]: Number(value.amount),
+                };
+
+            case 'select':
+                return { in: value };
+            case 'text':
+                return {
+                    [value.operator]: value.term,
+                };
+            default:
+                assertNever(type);
+        }
+    }
+
     toFilterInput(value: DataTableFilterValue<Type>): Partial<FilterInput> {
-        return this.options.toFilterInput(value);
+        if (this.options.toFilterInput) {
+            return this.options.toFilterInput(value);
+        }
+        if (this.options.filterField) {
+            return { [this.options.filterField]: this.getFilterOperator(value) } as Partial<FilterInput>;
+        } else {
+            throw new Error(
+                `Either "filterField" or "toFilterInput" must be provided (for filter "${this.name}"))`,
+            );
+        }
     }
 
     activate(value: DataTableFilterValue<Type>) {
@@ -103,8 +167,4 @@ export class DataTableFilter<
     isDateRange(): this is DataTableFilter<FilterInput, DataTableFilterDateRangeType> {
         return this.type.kind === 'dateRange';
     }
-
-    // private getValueForKind<Kind extends Type['kind']>(kind: Kind): KindValueMap[Kind] | undefined {
-    //     return this.value as any;
-    // }
 }

+ 4 - 4
packages/admin-ui/src/lib/core/src/providers/data-table/data-table-sort-collection.ts

@@ -1,17 +1,17 @@
 import { ActivatedRoute, Router } from '@angular/router';
 import { Subject } from 'rxjs';
-import { DataTableSort, DataTableSortOptions, SortOrder } from './data-table-sort';
+import { DataTableSort, DataTableSortOptions, DataTableSortOrder } from './data-table-sort';
 
 export class DataTableSortCollection<
     SortInput extends Record<string, 'ASC' | 'DESC'>,
     Names extends [...Array<keyof SortInput>] = [],
 > {
     readonly #sorts: Array<DataTableSort<SortInput>> = [];
-    #valueChanges$ = new Subject<Array<{ name: string; sortOrder: SortOrder | undefined }>>();
+    #valueChanges$ = new Subject<Array<{ name: string; sortOrder: DataTableSortOrder | undefined }>>();
     #connectedToRouter = false;
     valueChanges = this.#valueChanges$.asObservable();
     readonly #sortQueryParamName = 'sort';
-    #defaultSort: { name: keyof SortInput; sortOrder: SortOrder } | undefined;
+    #defaultSort: { name: keyof SortInput; sortOrder: DataTableSortOrder } | undefined;
 
     constructor(private router: Router) {}
 
@@ -31,7 +31,7 @@ export class DataTableSortCollection<
         return this as unknown as DataTableSortCollection<SortInput, [...Names, Name]>;
     }
 
-    defaultSort(name: keyof SortInput, sortOrder: SortOrder) {
+    defaultSort(name: keyof SortInput, sortOrder: DataTableSortOrder) {
         this.#defaultSort = { name, sortOrder };
         return this;
     }

+ 7 - 7
packages/admin-ui/src/lib/core/src/providers/data-table/data-table-sort.ts

@@ -1,22 +1,22 @@
 import { DataTableFilterOptions, KindValueMap } from './data-table-filter';
 
-export type SortOrder = 'ASC' | 'DESC';
+export type DataTableSortOrder = 'ASC' | 'DESC';
 
 export interface DataTableSortOptions<
-    SortInput extends Record<string, SortOrder>,
+    SortInput extends Record<string, DataTableSortOrder>,
     Name extends keyof SortInput,
 > {
     name: Name;
 }
 
-export class DataTableSort<SortInput extends Record<string, SortOrder>> {
+export class DataTableSort<SortInput extends Record<string, DataTableSortOrder>> {
     constructor(
         private readonly options: DataTableSortOptions<SortInput, any>,
-        private onSetValue?: (name: keyof SortInput, state: SortOrder | undefined) => void,
+        private onSetValue?: (name: keyof SortInput, state: DataTableSortOrder | undefined) => void,
     ) {}
-    #sortOrder: SortOrder | undefined;
+    #sortOrder: DataTableSortOrder | undefined;
 
-    get sortOrder(): SortOrder | undefined {
+    get sortOrder(): DataTableSortOrder | undefined {
         return this.#sortOrder;
     }
 
@@ -37,7 +37,7 @@ export class DataTableSort<SortInput extends Record<string, SortOrder>> {
         }
     }
 
-    setSortOrder(sortOrder: SortOrder | undefined): void {
+    setSortOrder(sortOrder: DataTableSortOrder | undefined): void {
         this.#sortOrder = sortOrder;
         if (this.onSetValue) {
             this.onSetValue(this.name, this.#sortOrder);

+ 16 - 1
packages/admin-ui/src/lib/core/src/public_api.ts

@@ -28,7 +28,6 @@ export * from './components/channel-switcher/channel-switcher.component';
 export * from './components/main-nav/main-nav.component';
 export * from './components/notification/notification.component';
 export * from './components/overlay-host/overlay-host.component';
-export * from './shared/components/page-title/page-title.component';
 export * from './components/settings-nav/settings-nav.component';
 export * from './components/theme-switcher/theme-switcher.component';
 export * from './components/ui-language-switcher-dialog/ui-language-switcher-dialog.component';
@@ -87,6 +86,11 @@ export * from './providers/custom-history-entry-component/history-entry-componen
 export * from './providers/dashboard-widget/dashboard-widget-types';
 export * from './providers/dashboard-widget/dashboard-widget.service';
 export * from './providers/dashboard-widget/register-dashboard-widget';
+export * from './providers/data-table/data-table-filter-collection';
+export * from './providers/data-table/data-table-filter';
+export * from './providers/data-table/data-table-sort-collection';
+export * from './providers/data-table/data-table-sort';
+export * from './providers/data-table/data-table.service';
 export * from './providers/guard/auth.guard';
 export * from './providers/health-check/health-check.service';
 export * from './providers/i18n/custom-http-loader';
@@ -124,6 +128,12 @@ export * from './shared/components/custom-field-control/custom-field-control.com
 export * from './shared/components/customer-label/customer-label.component';
 export * from './shared/components/data-table/data-table-column.component';
 export * from './shared/components/data-table/data-table.component';
+export * from './shared/components/data-table-2/data-table-column.component';
+export * from './shared/components/data-table-2/data-table-search.component';
+export * from './shared/components/data-table-2/data-table2.component';
+export * from './shared/components/data-table-column-picker/data-table-column-picker.component';
+export * from './shared/components/data-table-filter-label/data-table-filter-label.component';
+export * from './shared/components/data-table-filters/data-table-filters.component';
 export * from './shared/components/datetime-picker/constants';
 export * from './shared/components/datetime-picker/datetime-picker.component';
 export * from './shared/components/datetime-picker/datetime-picker.service';
@@ -158,6 +168,11 @@ export * from './shared/components/modal-dialog/dialog-title.directive';
 export * from './shared/components/modal-dialog/modal-dialog.component';
 export * from './shared/components/object-tree/object-tree.component';
 export * from './shared/components/order-state-label/order-state-label.component';
+export * from './shared/components/page-body/page-body.component';
+export * from './shared/components/page-header/page-header.component';
+export * from './shared/components/page-header-description/page-header-description.component';
+export * from './shared/components/page-header-tabs/page-header-tabs.component';
+export * from './shared/components/page-title/page-title.component';
 export * from './shared/components/pagination-controls/pagination-controls.component';
 export * from './shared/components/product-multi-selector-dialog/product-multi-selector-dialog.component';
 export * from './shared/components/product-search-input/product-search-input.component';

+ 23 - 8
packages/admin-ui/src/lib/core/src/shared/components/data-table-filter-label/data-table-filter-label.component.html

@@ -7,12 +7,18 @@
         <span *ngIf="filterWithValue.value?.operator === 'contains'">{{
             'common.operator-contains' | translate
         }}</span>
-        <span *ngIf="filterWithValue.value?.operator === 'eq'">{{ 'common.operator-eq' | translate }}</span>
+        <span *ngIf="filterWithValue.value?.operator === 'eq'">{{
+            'common.operator-eq' | translate
+        }}</span>
         <span *ngIf="filterWithValue.value?.operator === 'notContains'">{{
             'common.operator-notContains' | translate
         }}</span>
-        <span *ngIf="filterWithValue.value?.operator === 'notEq'">{{ 'common.operator-not-eq' | translate }}</span>
-        <span *ngIf="filterWithValue.value?.operator === 'regex'">{{ 'common.operator-regex' | translate }}</span>
+        <span *ngIf="filterWithValue.value?.operator === 'notEq'">{{
+            'common.operator-not-eq' | translate
+        }}</span>
+        <span *ngIf="filterWithValue.value?.operator === 'regex'">{{
+            'common.operator-regex' | translate
+        }}</span>
         <span> "{{ filterWithValue.value?.term }}"</span>
     </ng-container>
     <ng-container *ngIf="filterWithValue.isBoolean()">
@@ -21,16 +27,25 @@
     </ng-container>
     <ng-container *ngIf="filterWithValue.isDateRange()">
         <span *ngIf="filterWithValue.value?.start && filterWithValue.value?.end">
-            {{ filterWithValue.value?.start | localeDate : 'shortDate' }} - {{ filterWithValue.value?.end | localeDate : 'shortDate' }}
+            {{ filterWithValue.value?.start | localeDate : 'shortDate' }} -
+            {{ filterWithValue.value?.end | localeDate : 'shortDate' }}
+        </span>
+        <span *ngIf="filterWithValue.value?.start && !filterWithValue.value?.end">
+            > {{ filterWithValue.value?.start | localeDate : 'shortDate' }}
+        </span>
+        <span *ngIf="filterWithValue.value?.end && !filterWithValue.value?.start">
+            < {{ filterWithValue.value?.end | localeDate : 'shortDate' }}
         </span>
-        <span *ngIf="filterWithValue.value?.start && !filterWithValue.value?.end"> > {{ filterWithValue.value?.start | localeDate : 'shortDate' }} </span>
-        <span *ngIf="filterWithValue.value?.end && !filterWithValue.value?.start"> < {{ filterWithValue.value?.end | localeDate : 'shortDate' }} </span>
     </ng-container>
     <ng-container *ngIf="filterWithValue.isNumber()">
         <span *ngIf="filterWithValue.value?.operator === 'eq'"> = </span>
         <span *ngIf="filterWithValue.value?.operator === 'gt'"> > </span>
         <span *ngIf="filterWithValue.value?.operator === 'lt'"> < </span>
-        <span *ngIf="$any(filterWithValue.filter.type).inputType === 'currency'">{{ +filterWithValue.value?.amount | localeCurrency }}</span>
-        <span *ngIf="$any(filterWithValue.filter.type).inputType !== 'currency'">{{ +filterWithValue.value?.amount }}</span>
+        <span *ngIf="$any(filterWithValue.filter.type).inputType === 'currency'">{{
+            +filterWithValue.value?.amount | localeCurrency
+        }}</span>
+        <span *ngIf="$any(filterWithValue.filter.type).inputType !== 'currency'">{{
+            +filterWithValue.value?.amount
+        }}</span>
     </ng-container>
 </div>

+ 55 - 31
packages/admin-ui/src/lib/core/src/shared/components/data-table-filters/data-table-filters.component.ts

@@ -1,15 +1,19 @@
 import { AfterViewInit, ChangeDetectionStrategy, Component, Input, OnInit, ViewChild } from '@angular/core';
-import { AbstractControl, FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
-import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import { DateOperators } from '@vendure/admin-ui/core';
+import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms';
+import {
+    DataTableFilterType,
+    DataTableFilterValue,
+    DateOperators,
+    KindValueMap,
+} from '@vendure/admin-ui/core';
 import { assertNever } from '@vendure/common/lib/shared-utils';
-import { DropdownComponent } from '../dropdown/dropdown.component';
-import { I18nService } from '../../../providers/i18n/i18n.service';
-import { DataTableFilter, DataTableFilterSelectType } from '../../../providers/data-table/data-table-filter';
+import { DataTableFilter } from '../../../providers/data-table/data-table-filter';
 import {
     DataTableFilterCollection,
     FilterWithValue,
 } from '../../../providers/data-table/data-table-filter-collection';
+import { I18nService } from '../../../providers/i18n/i18n.service';
+import { DropdownComponent } from '../dropdown/dropdown.component';
 
 @Component({
     selector: 'vdr-data-table-filters',
@@ -103,33 +107,53 @@ export class DataTableFiltersComponent implements AfterViewInit, OnInit {
         if (!this.selectedFilter) {
             return;
         }
-        let value = this.formControl.value;
+        let value: any;
         const type = this.selectedFilter?.type;
-        if (type.kind === 'select' && Array.isArray(value)) {
-            value = value.map((o, i) => (o ? type.options[i].value : undefined)).filter(v => !!v);
-        }
-        if (type.kind === 'dateRange') {
-            let dateOperators: DateOperators;
-            const start = value.start ?? undefined;
-            const end = value.end ?? undefined;
-            if (start && end) {
-                dateOperators = {
-                    between: { start, end },
-                };
-            } else if (start) {
-                dateOperators = {
-                    after: start,
-                };
-            } else {
-                dateOperators = {
-                    before: end,
-                };
+        switch (type.kind) {
+            case 'boolean':
+                value = !!this.formControl.value as KindValueMap[typeof type.kind]['raw'];
+                break;
+            case 'dateRange': {
+                let dateOperators: DateOperators;
+                const start = this.formControl.value.start ?? undefined;
+                const end = this.formControl.value.end ?? undefined;
+                if (start && end) {
+                    dateOperators = {
+                        between: { start, end },
+                    };
+                } else if (start) {
+                    dateOperators = {
+                        after: start,
+                    };
+                } else {
+                    dateOperators = {
+                        before: end,
+                    };
+                }
+                value = { start, end } as KindValueMap[typeof type.kind]['raw'];
+                break;
             }
-            value = {
-                start,
-                end,
-                dateOperators,
-            };
+            case 'number':
+                value = {
+                    amount: Number(this.formControl.value.amount),
+                    operator: this.formControl.value.operator,
+                } as KindValueMap[typeof type.kind]['raw'];
+                break;
+
+            case 'select':
+                const options = this.formControl.value
+                    .map((v, i) => (v ? type.options[i].value : undefined))
+                    .filter(v => !!v);
+                value = options as KindValueMap[typeof type.kind]['raw'];
+                break;
+            case 'text':
+                value = {
+                    operator: this.formControl.value.operator,
+                    term: this.formControl.value.term,
+                } as KindValueMap[typeof type.kind]['raw'];
+                break;
+            default:
+                assertNever(type);
         }
         if (this.state === 'new') {
             this.selectedFilter.activate(value);

+ 67 - 63
packages/admin-ui/src/lib/customer/src/components/customer-list/customer-list.component.html

@@ -1,69 +1,73 @@
-<vdr-action-bar>
-    <vdr-ab-left>
-        <input
-            type="text"
-            name="emailSearchTerm"
-            [formControl]="searchTerm"
-            [placeholder]="'customer.search-customers-by-email-last-name-postal-code' | translate"
-            class="search-input"
-        />
-    </vdr-ab-left>
-    <vdr-ab-right>
+<vdr-page-header>
+    <vdr-page-title>
         <vdr-action-bar-items locationId="customer-list"></vdr-action-bar-items>
         <a class="btn btn-primary" [routerLink]="['./create']" *vdrIfPermissions="'CreateCustomer'">
             <clr-icon shape="plus"></clr-icon>
             {{ 'customer.create-new-customer' | translate }}
         </a>
-    </vdr-ab-right>
-</vdr-action-bar>
+    </vdr-page-title>
+</vdr-page-header>
 
-<vdr-data-table
-    [items]="items$ | async"
-    [itemsPerPage]="itemsPerPage$ | async"
-    [totalItems]="totalItems$ | async"
-    [currentPage]="currentPage$ | async"
-    (pageChange)="setPageNumber($event)"
-    (itemsPerPageChange)="setItemsPerPage($event)"
->
-    <vdr-dt-column [expand]="true">{{ 'customer.name' | translate }}</vdr-dt-column>
-    <vdr-dt-column [expand]="true">{{ 'customer.email-address' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'customer.customer-type' | translate }}</vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <ng-template let-customer="item">
-        <td class="left align-middle">
-            {{ customer.title }} {{ customer.firstName }} {{ customer.lastName }}
-        </td>
-        <td class="left align-middle">{{ customer.emailAddress }}</td>
-        <td class="left align-middle">
-            <vdr-customer-status-label [customer]="customer"></vdr-customer-status-label>
-        </td>
-        <td class="right align-middle">
-            <vdr-table-row-action
-                iconShape="edit"
-                [label]="'common.edit' | translate"
-                [linkTo]="['./', customer.id]"
-            ></vdr-table-row-action>
-        </td>
-        <td>
-            <vdr-dropdown>
-                <button type="button" class="btn btn-link btn-sm" vdrDropdownTrigger>
-                    {{ 'common.actions' | translate }}
-                    <clr-icon shape="caret down"></clr-icon>
-                </button>
-                <vdr-dropdown-menu vdrPosition="bottom-right">
-                    <button
-                        type="button"
-                        class="delete-button"
-                        (click)="deleteCustomer(customer)"
-                        [disabled]="!('DeleteCustomer' | hasPermission)"
-                        vdrDropdownItem
-                    >
-                        <clr-icon shape="trash" class="is-danger"></clr-icon>
-                        {{ 'common.delete' | translate }}
-                    </button>
-                </vdr-dropdown-menu>
-            </vdr-dropdown>
-        </td>
-    </ng-template>
-</vdr-data-table>
+<vdr-page-body>
+    <vdr-data-table-2
+        class="mt-2"
+        id="customer-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]="searchTerm"
+            [searchTermPlaceholder]="'customer.search-customers-by-email-last-name-postal-code' | translate"
+        ></vdr-dt2-search>
+        <vdr-dt2-column
+            [heading]="'common.created-at' | translate"
+            [hiddenByDefault]="true"
+            [sort]="sorts.get('createdAt')"
+        >
+            <ng-template let-customer="item">
+                {{ customer.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-customer="item">
+                {{ customer.updatedAt | localeDate : 'short' }}
+            </ng-template>
+        </vdr-dt2-column>
+        <vdr-dt2-column
+            [heading]="'customer.name' | translate"
+            [optional]="false"
+            [sort]="sorts.get('lastName')"
+        >
+            <ng-template let-customer="item">
+                <a class="button-ghost" [routerLink]="['./', customer.id]"
+                    ><span> {{ customer.title }} {{ customer.firstName }} {{ customer.lastName }} </span>
+                    <clr-icon shape="arrow right"></clr-icon>
+                </a>
+            </ng-template>
+        </vdr-dt2-column>
+        <vdr-dt2-column [heading]="'common.status' | translate">
+            <ng-template let-customer="item">
+                <vdr-customer-status-label [customer]="customer" />
+            </ng-template>
+        </vdr-dt2-column>
+        <vdr-dt2-column [heading]="'customer.email-address' | translate" [sort]="sorts.get('emailAddress')">
+            <ng-template let-customer="item">
+                {{ customer.emailAddress }}
+            </ng-template>
+        </vdr-dt2-column>
+    </vdr-data-table-2>
+</vdr-page-body>

+ 63 - 12
packages/admin-ui/src/lib/customer/src/components/customer-list/customer-list.component.ts

@@ -4,16 +4,19 @@ import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
     BaseListComponent,
+    CustomerFilterParameter,
+    CustomerSortParameter,
     DataService,
     GetCustomerListQuery,
     ItemOf,
     LogicalOperator,
     ModalService,
     NotificationService,
+    SelectionManager,
 } from '@vendure/admin-ui/core';
-import { SortOrder } from '@vendure/common/lib/generated-types';
-import { EMPTY } from 'rxjs';
+import { EMPTY, merge } from 'rxjs';
 import { debounceTime, filter, switchMap, takeUntil } from 'rxjs/operators';
+import { DataTableService } from '../../../../core/src/providers/data-table/data-table.service';
 
 @Component({
     selector: 'vdr-customer-list',
@@ -25,12 +28,61 @@ export class CustomerListComponent
     implements OnInit
 {
     searchTerm = new UntypedFormControl('');
+    selectionManager = new SelectionManager<ItemOf<GetCustomerListQuery, 'customers'>>({
+        multiSelect: true,
+        itemsAreEqual: (a, b) => a.id === b.id,
+        additiveMode: true,
+    });
+    readonly filters = this.dataTableService
+        .createFilterCollection<CustomerFilterParameter>()
+        .addFilter({
+            name: 'createdAt',
+            type: { kind: 'dateRange' },
+            label: _('common.created-at'),
+            filterField: 'createdAt',
+        })
+        .addFilter({
+            name: 'updatedAt',
+            type: { kind: 'dateRange' },
+            label: _('common.updated-at'),
+            filterField: 'updatedAt',
+        })
+        .addFilter({
+            name: 'firstName',
+            type: { kind: 'text' },
+            label: _('customer.first-name'),
+            filterField: 'firstName',
+        })
+        .addFilter({
+            name: 'lastName',
+            type: { kind: 'text' },
+            label: _('customer.last-name'),
+            filterField: 'lastName',
+        })
+        .addFilter({
+            name: 'emailAddress',
+            type: { kind: 'text' },
+            label: _('customer.email-address'),
+            filterField: 'emailAddress',
+        })
+        .connectToRoute(this.route);
+
+    readonly sorts = this.dataTableService
+        .createSortCollection<CustomerSortParameter>()
+        .defaultSort('createdAt', 'DESC')
+        .addSort({ name: 'createdAt' })
+        .addSort({ name: 'updatedAt' })
+        .addSort({ name: 'lastName' })
+        .addSort({ name: 'emailAddress' })
+        .connectToRoute(this.route);
+
     constructor(
         private dataService: DataService,
         router: Router,
         route: ActivatedRoute,
         private modalService: ModalService,
         private notificationService: NotificationService,
+        private dataTableService: DataTableService,
     ) {
         super(router, route);
         super.setQueryFn(
@@ -50,11 +102,10 @@ export class CustomerListComponent
                         postalCode: {
                             contains: this.searchTerm.value,
                         },
+                        ...this.filters.createFilterInput(),
                     },
-                    filterOperator: LogicalOperator.OR,
-                    sort: {
-                        createdAt: SortOrder.DESC,
-                    },
+                    filterOperator: this.searchTerm.value ? LogicalOperator.OR : LogicalOperator.AND,
+                    sort: this.sorts.createSortInput(),
                 },
             }),
         );
@@ -62,12 +113,12 @@ export class CustomerListComponent
 
     ngOnInit() {
         super.ngOnInit();
-        this.searchTerm.valueChanges
-            .pipe(
-                filter(value => 2 < value.length || value.length === 0),
-                debounceTime(250),
-                takeUntil(this.destroy$),
-            )
+        const searchTerm$ = this.searchTerm.valueChanges.pipe(
+            filter(value => 2 < value.length || value.length === 0),
+            debounceTime(250),
+        );
+        merge(searchTerm$, this.filters.valueChanges, this.sorts.valueChanges)
+            .pipe(takeUntil(this.destroy$))
             .subscribe(() => this.refresh());
     }
 

+ 83 - 78
packages/admin-ui/src/lib/marketing/src/components/promotion-list/promotion-list.component.html

@@ -1,84 +1,89 @@
-<vdr-action-bar>
-    <vdr-ab-left>
-        <form class="search-form" [formGroup]="searchForm">
-            <input
-                type="text"
-                formControlName="name"
-                [placeholder]="'marketing.search-by-name' | translate"
-                class="search-input"
-            />
-            <input
-                type="text"
-                formControlName="couponCode"
-                [placeholder]="'marketing.search-by-coupon-code' | translate"
-                class="search-input"
-            />
-        </form>
-    </vdr-ab-left>
-    <vdr-ab-right>
+<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']">
+        <a class="btn btn-primary" *vdrIfPermissions="'CreatePromotion'" [routerLink]="['./create']">
             <clr-icon shape="plus"></clr-icon>
             {{ 'marketing.create-new-promotion' | translate }}
         </a>
-    </vdr-ab-right>
-</vdr-action-bar>
+    </vdr-page-title>
+</vdr-page-header>
 
-<vdr-data-table
-    [items]="items$ | async"
-    [itemsPerPage]="itemsPerPage$ | async"
-    [totalItems]="totalItems$ | async"
-    [currentPage]="currentPage$ | async"
-    (pageChange)="setPageNumber($event)"
-    (itemsPerPageChange)="setItemsPerPage($event)"
->
-    <vdr-dt-column>{{ 'common.name' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'marketing.coupon-code' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'marketing.starts-at' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'marketing.ends-at' | translate }}</vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <ng-template let-promotion="item">
-        <td class="left align-middle">{{ promotion.name }}</td>
-        <td class="left align-middle">
-            <vdr-chip *ngIf="promotion.couponCode">
+<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-bulk-action-menu
+            locationId="facet-list"
+            [hostComponent]="this"
+            [selectionManager]="selectionManager"
+        />
+        <vdr-dt2-search
+            [searchTermControl]="searchTerm"
+            [searchTermPlaceholder]="'marketing.search-by-name-or-coupon-code' | translate"
+        />
+        <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">
+                <div class="badge success" *ngIf="promotion.enabled">{{ 'common.enabled' }}</div>
+                <div class="badge warning" *ngIf="!promotion.enabled">{{ 'common.disabled' }}</div>
+            </ng-template>
+        </vdr-dt2-column>
+        <vdr-dt2-column [heading]="'marketing.coupon-code' | translate" [sort]="sorts.get('couponCode')">
+            <ng-template let-promotion="item">
                 {{ promotion.couponCode }}
-            </vdr-chip>
-        </td>
-        <td class="left align-middle">{{ promotion.startsAt | localeDate: 'longDate' }}</td>
-        <td class="left align-middle">{{ promotion.endsAt | localeDate: 'longDate' }}</td>
-        <td class="align-middle">
-            <vdr-chip *ngIf="!promotion.enabled">{{ 'common.disabled' | translate }}</vdr-chip>
-        </td>
-        <td class="right align-middle">
-            <vdr-table-row-action
-                iconShape="edit"
-                [label]="'common.edit' | translate"
-                [linkTo]="['./', promotion.id]"
-            ></vdr-table-row-action>
-        </td>
-        <td class="right align-middle">
-            <vdr-dropdown>
-                <button type="button" class="btn btn-link btn-sm" vdrDropdownTrigger>
-                    {{ 'common.actions' | translate }}
-                    <clr-icon shape="caret down"></clr-icon>
-                </button>
-                <vdr-dropdown-menu vdrPosition="bottom-right">
-                    <button
-                        type="button"
-                        class="delete-button"
-                        (click)="deletePromotion(promotion.id)"
-                        [disabled]="!('DeletePromotion' | hasPermission)"
-                        vdrDropdownItem
-                    >
-                        <clr-icon shape="trash" class="is-danger"></clr-icon>
-                        {{ 'common.delete' | translate }}
-                    </button>
-                </vdr-dropdown-menu>
-            </vdr-dropdown>
-        </td>
-    </ng-template>
-</vdr-data-table>
+            </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>

+ 94 - 19
packages/admin-ui/src/lib/marketing/src/components/promotion-list/promotion-list.component.ts

@@ -1,5 +1,5 @@
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
-import { UntypedFormControl, UntypedFormGroup } from '@angular/forms';
+import { FormControl } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
@@ -7,13 +7,17 @@ import {
     DataService,
     GetPromotionListQuery,
     ItemOf,
+    LogicalOperator,
     ModalService,
     NotificationService,
     PromotionFilterParameter,
     PromotionListOptions,
+    PromotionSortParameter,
+    SelectionManager,
 } from '@vendure/admin-ui/core';
 import { EMPTY, merge } from 'rxjs';
 import { debounceTime, switchMap, takeUntil } from 'rxjs/operators';
+import { DataTableService } from '../../../../core/src/providers/data-table/data-table.service';
 
 export type PromotionSearchForm = {
     name: string;
@@ -30,35 +34,105 @@ export class PromotionListComponent
     extends BaseListComponent<GetPromotionListQuery, ItemOf<GetPromotionListQuery, 'promotions'>>
     implements OnInit
 {
-    searchForm = new UntypedFormGroup({
-        name: new UntypedFormControl(''),
-        couponCode: new UntypedFormControl(''),
+    searchTerm = new FormControl('');
+    selectionManager = new SelectionManager<ItemOf<GetPromotionListQuery, 'promotions'>>({
+        multiSelect: true,
+        itemsAreEqual: (a, b) => a.id === b.id,
+        additiveMode: true,
     });
 
+    readonly filters = this.dataTableService
+        .createFilterCollection<PromotionFilterParameter>()
+        .addFilter({
+            name: 'createdAt',
+            type: { kind: 'dateRange' },
+            label: _('common.created-at'),
+            filterField: 'createdAt',
+        })
+        .addFilter({
+            name: 'updatedAt',
+            type: { kind: 'dateRange' },
+            label: _('common.updated-at'),
+            filterField: 'updatedAt',
+        })
+        .addFilter({
+            name: 'startsAt',
+            type: { kind: 'dateRange' },
+            label: _('marketing.starts-at'),
+            filterField: 'startsAt',
+        })
+        .addFilter({
+            name: 'endsAt',
+            type: { kind: 'dateRange' },
+            label: _('marketing.ends-at'),
+            filterField: 'endsAt',
+        })
+        .addFilter({
+            name: 'enabled',
+            type: { kind: 'boolean' },
+            label: _('common.enabled'),
+            filterField: 'enabled',
+        })
+        .addFilter({
+            name: 'name',
+            type: { kind: 'text' },
+            label: _('common.name'),
+            filterField: 'name',
+        })
+        .addFilter({
+            name: 'couponCode',
+            type: { kind: 'text' },
+            label: _('marketing.coupon-code'),
+            filterField: 'couponCode',
+        })
+        .addFilter({
+            name: 'desc',
+            type: { kind: 'text' },
+            label: _('common.description'),
+            filterField: 'description',
+        })
+        .addFilter({
+            name: 'usageLimit',
+            type: { kind: 'number' },
+            label: _('marketing.per-customer-limit'),
+            filterField: 'perCustomerUsageLimit',
+        })
+        .connectToRoute(this.route);
+
+    readonly sorts = this.dataTableService
+        .createSortCollection<PromotionSortParameter>()
+        .defaultSort('createdAt', 'DESC')
+        .addSort({ name: 'createdAt' })
+        .addSort({ name: 'updatedAt' })
+        .addSort({ name: 'startsAt' })
+        .addSort({ name: 'endsAt' })
+        .addSort({ name: 'name' })
+        .addSort({ name: 'couponCode' })
+        .addSort({ name: 'perCustomerUsageLimit' })
+        .connectToRoute(this.route);
+
     constructor(
         private dataService: DataService,
         router: Router,
         route: ActivatedRoute,
         private notificationService: NotificationService,
         private modalService: ModalService,
+        private dataTableService: DataTableService,
     ) {
         super(router, route);
         super.setQueryFn(
             (...args: any[]) => this.dataService.promotion.getPromotions(...args).refetchOnChannelChange(),
             data => data.promotions,
-            (skip, take) => this.createQueryOptions(skip, take, this.searchForm.value),
+            (skip, take) => this.createQueryOptions(skip, take, this.searchTerm.value),
         );
     }
 
     ngOnInit(): void {
         super.ngOnInit();
-
-        merge(this.searchForm.valueChanges.pipe(debounceTime(250)), this.route.queryParamMap)
+        const searchTerm$ = this.searchTerm.valueChanges.pipe(debounceTime(250));
+        merge(searchTerm$, this.filters.valueChanges, this.sorts.valueChanges)
             .pipe(takeUntil(this.destroy$))
             .subscribe(val => {
-                if (!val.params) {
-                    this.setPageNumber(1);
-                }
                 this.refresh();
             });
     }
@@ -95,16 +169,15 @@ export class PromotionListComponent
     private createQueryOptions(
         skip: number,
         take: number,
-        searchForm: PromotionSearchForm,
+        searchTerm: string | null,
     ): { options: PromotionListOptions } {
-        const filter: PromotionFilterParameter = {};
-
-        if (searchForm.couponCode) {
-            filter.couponCode = { contains: searchForm.couponCode };
-        }
-
-        if (searchForm.name) {
-            filter.name = { contains: searchForm.name };
+        const filter = this.filters.createFilterInput();
+        const sort = this.sorts.createSortInput();
+        let filterOperator = LogicalOperator.AND;
+        if (searchTerm) {
+            filter.couponCode = { contains: searchTerm };
+            filter.name = { contains: searchTerm };
+            filterOperator = LogicalOperator.OR;
         }
 
         return {
@@ -112,6 +185,8 @@ export class PromotionListComponent
                 skip,
                 take,
                 filter,
+                filterOperator,
+                sort,
             },
         };
     }

+ 7 - 2
packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.html

@@ -8,14 +8,14 @@
             </a>
         </ng-container>
     </vdr-page-title>
-    <vdr-page-header-description>Description of the current page (if applicable)</vdr-page-header-description>
+ <!--   <vdr-page-header-description>Description of the current page (if applicable)</vdr-page-header-description>
     <vdr-page-header-tabs
         [tabs]="[
             { id: 'tab1', label: 'Tab 1' },
             { id: 'tab2', label: 'Tab 2' },
             { id: 'tab3', label: 'Tab 3' }
         ]"
-    ></vdr-page-header-tabs>
+    ></vdr-page-header-tabs>-->
 </vdr-page-header>
 <vdr-page-body>
     <vdr-data-table-2
@@ -64,6 +64,11 @@
                 {{ order.totalWithTax | localeCurrency : order.currencyCode }}
             </ng-template>
         </vdr-dt2-column>
+        <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.updated-at' | translate">
             <ng-template let-order="item">
                 {{ order.updatedAt | timeAgo }}

+ 21 - 46
packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.ts

@@ -36,35 +36,32 @@ export class OrderListComponent
 {
     searchControl = new UntypedFormControl('');
     orderStates = this.serverConfigService.getOrderProcessStates().map(item => item.name);
+
     readonly filters = this.dataTableService
         .createFilterCollection<OrderFilterParameter>()
         .addFilter({
             name: 'createdAt',
             type: { kind: 'dateRange' },
             label: _('common.created-at'),
-            toFilterInput: value => ({
-                createdAt: value.dateOperators,
-            }),
+            filterField: 'createdAt',
+        })
+        .addFilter({
+            name: 'updatedAt',
+            type: { kind: 'dateRange' },
+            label: _('common.updated-at'),
+            filterField: 'updatedAt',
         })
         .addFilter({
             name: 'active',
             type: { kind: 'boolean' },
             label: _('order.filter-is-active'),
-            toFilterInput: value => ({
-                active: {
-                    eq: value,
-                },
-            }),
+            filterField: 'active',
         })
         .addFilter({
             name: 'totalWithTax',
             type: { kind: 'number', inputType: 'currency', currencyCode: 'USD' },
             label: _('order.total'),
-            toFilterInput: value => ({
-                totalWithTax: {
-                    [value.operator]: +value.amount,
-                },
-            }),
+            filterField: 'totalWithTax',
         })
         .addFilter({
             name: 'state',
@@ -73,59 +70,37 @@ export class OrderListComponent
                 options: this.orderStates.map(s => ({ value: s, label: getOrderStateTranslationToken(s) })),
             },
             label: _('order.state'),
-            toFilterInput: value => ({
-                state: {
-                    in: value,
-                },
-            }),
+            filterField: 'state',
         })
         .addFilter({
             name: 'orderPlacedAt',
-            type: {
-                kind: 'dateRange',
-            },
+            type: { kind: 'dateRange' },
             label: _('order.placed-at'),
-            toFilterInput: value => ({
-                orderPlacedAt: value.dateOperators,
-            }),
+            filterField: 'orderPlacedAt',
         })
         .addFilter({
             name: 'customerLastName',
             type: { kind: 'text' },
             label: _('customer.last-name'),
-            toFilterInput: value => ({
-                customerLastName: {
-                    [value.operator]: value.term,
-                },
-            }),
+            filterField: 'customerLastName',
         })
         .addFilter({
             name: 'transactionId',
             type: { kind: 'text' },
             label: _('order.transaction-id'),
-            toFilterInput: value => ({
-                transactionId: {
-                    [value.operator]: value.term,
-                },
-            }),
+            filterField: 'transactionId',
         })
         .connectToRoute(this.route);
 
     readonly sorts = this.dataTableService
         .createSortCollection<OrderSortParameter>()
         .defaultSort('updatedAt', 'DESC')
-        .addSort({
-            name: 'orderPlacedAt',
-        })
-        .addSort({
-            name: 'customerLastName',
-        })
-        .addSort({
-            name: 'state',
-        })
-        .addSort({
-            name: 'totalWithTax',
-        })
+        .addSort({ name: 'createdAt' })
+        .addSort({ name: 'updatedAt' })
+        .addSort({ name: 'orderPlacedAt' })
+        .addSort({ name: 'customerLastName' })
+        .addSort({ name: 'state' })
+        .addSort({ name: 'totalWithTax' })
         .connectToRoute(this.route);
 
     canCreateDraftOrder = false;

+ 61 - 60
packages/admin-ui/src/lib/settings/src/components/administrator-list/administrator-list.component.html

@@ -1,65 +1,66 @@
-<vdr-action-bar>
-    <vdr-ab-left>
-        <input
-        type="text"
-        name="searchTerm"
-        [formControl]="searchControl"
-        [placeholder]="'admin.search-administrator' | translate"
-        class="search-input"
-    />
-    </vdr-ab-left>
-    <vdr-ab-right>
+<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-ab-right>
-</vdr-action-bar>
-
-<vdr-data-table
-    [items]="items$ | async"
-    [itemsPerPage]="itemsPerPage$ | async"
-    [totalItems]="totalItems$ | async"
-    [currentPage]="currentPage$ | async"
-    (pageChange)="setPageNumber($event)"
-    (itemsPerPageChange)="setItemsPerPage($event)"
->
-    <vdr-dt-column>{{ 'settings.first-name' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'settings.last-name' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'settings.email-address' | translate }}</vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <ng-template let-administrator="item">
-        <td class="left align-middle">{{ administrator.firstName }}</td>
-        <td class="left align-middle">{{ administrator.lastName }}</td>
-        <td class="left align-middle">{{ administrator.emailAddress }}</td>
-        <td class="right align-middle">
-            <vdr-table-row-action
-                iconShape="edit"
-                [label]="'common.edit' | translate"
-                [linkTo]="['./', administrator.id]"
-            ></vdr-table-row-action>
-        </td>
-        <td>
-            <vdr-dropdown>
-                <button type="button" class="btn btn-link btn-sm" vdrDropdownTrigger>
-                    {{ 'common.actions' | translate }}
-                    <clr-icon shape="caret down"></clr-icon>
-                </button>
-                <vdr-dropdown-menu vdrPosition="bottom-right">
-                    <button
-                        type="button"
-                        class="delete-button"
-                        (click)="deleteAdministrator(administrator)"
-                        [disabled]="!('DeleteAdministrator' | hasPermission)"
-                        vdrDropdownItem
-                    >
-                        <clr-icon shape="trash" class="is-danger"></clr-icon>
-                        {{ 'common.delete' | translate }}
-                    </button>
-                </vdr-dropdown-menu>
-            </vdr-dropdown>
-        </td>
-    </ng-template>
-</vdr-data-table>
+    </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-bulk-action-menu
+            locationId="administrator-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-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.emailAddress' | translate"
+            [sort]="sorts.get('emailAddress')"
+        >
+            <ng-template let-administrator="item">
+                {{ administrator.emailAddress }}
+            </ng-template>
+        </vdr-dt2-column>
+    </vdr-data-table-2>
+</vdr-page-body>

+ 72 - 20
packages/admin-ui/src/lib/settings/src/components/administrator-list/administrator-list.component.ts

@@ -3,54 +3,110 @@ import { FormControl } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
+    AdministratorFilterParameter,
+    AdministratorSortParameter,
     BaseListComponent,
     DataService,
     GetAdministratorsQuery,
+    GetFacetListQuery,
     ItemOf,
     LogicalOperator,
     ModalService,
     NotificationService,
+    SelectionManager,
+    SellerFilterParameter,
+    SellerSortParameter,
     SortOrder,
 } from '@vendure/admin-ui/core';
 import { EMPTY, merge } from 'rxjs';
 import { debounceTime, filter, switchMap, takeUntil } from 'rxjs/operators';
+import { DataTableService } from '../../../../core/src/providers/data-table/data-table.service';
 
 @Component({
     selector: 'vdr-administrator-list',
     templateUrl: './administrator-list.component.html',
     styleUrls: ['./administrator-list.component.scss'],
 })
-export class AdministratorListComponent extends BaseListComponent<
-    GetAdministratorsQuery,
-    ItemOf<GetAdministratorsQuery, 'administrators'>
-> implements OnInit {
-    searchControl = new FormControl('');
+export class AdministratorListComponent
+    extends BaseListComponent<GetAdministratorsQuery, ItemOf<GetAdministratorsQuery, 'administrators'>>
+    implements OnInit
+{
+    searchTermControl = new FormControl('');
+    selectionManager = new SelectionManager<ItemOf<GetFacetListQuery, 'facets'>>({
+        multiSelect: true,
+        itemsAreEqual: (a, b) => a.id === b.id,
+        additiveMode: true,
+    });
+    readonly filters = this.dataTableService
+        .createFilterCollection<AdministratorFilterParameter>()
+        .addFilter({
+            name: 'createdAt',
+            type: { kind: 'dateRange' },
+            label: _('common.created-at'),
+            filterField: 'createdAt',
+        })
+        .addFilter({
+            name: 'updatedAt',
+            type: { kind: 'dateRange' },
+            label: _('common.updated-at'),
+            filterField: 'updatedAt',
+        })
+        .addFilter({
+            name: 'firstName',
+            type: { kind: 'text' },
+            label: _('settings.first-name'),
+            filterField: 'firstName',
+        })
+        .addFilter({
+            name: 'lastName',
+            type: { kind: 'text' },
+            label: _('settings.last-name'),
+            filterField: 'lastName',
+        })
+        .addFilter({
+            name: 'emailAddress',
+            type: { kind: 'text' },
+            label: _('settings.email-address'),
+            filterField: 'emailAddress',
+        })
+        .connectToRoute(this.route);
+
+    readonly sorts = this.dataTableService
+        .createSortCollection<AdministratorSortParameter>()
+        .defaultSort('createdAt', 'DESC')
+        .addSort({ name: 'createdAt' })
+        .addSort({ name: 'updatedAt' })
+        .addSort({ name: 'lastName' })
+        .addSort({ name: 'emailAddress' })
+        .connectToRoute(this.route);
+
     constructor(
         private dataService: DataService,
         router: Router,
         route: ActivatedRoute,
         private modalService: ModalService,
         private notificationService: NotificationService,
+        private dataTableService: DataTableService,
     ) {
         super(router, route);
         super.setQueryFn(
             (...args: any[]) => this.dataService.administrator.getAdministrators(...args),
-            (data) => data.administrators,
-            (skip, take) => this.createSearchQuery(skip, take, this.searchControl.value)
+            data => data.administrators,
+            (skip, take) => this.createSearchQuery(skip, take, this.searchTermControl.value),
         );
     }
 
     ngOnInit() {
         super.ngOnInit();
-        const searchTerms$ = merge(this.searchControl.valueChanges).pipe(
+        const searchTerms$ = merge(this.searchTermControl.valueChanges).pipe(
             filter(value => (value && 2 < value.length) || value?.length === 0),
             debounceTime(250),
         );
-        merge(searchTerms$, this.route.queryParamMap)
-        .pipe(takeUntil(this.destroy$))
-        .subscribe(val => {
-            this.refresh();
-        });
+        merge(searchTerms$, this.filters.valueChanges, this.sorts.valueChanges)
+            .pipe(takeUntil(this.destroy$))
+            .subscribe(val => {
+                this.refresh();
+            });
     }
 
     deleteAdministrator(administrator: ItemOf<GetAdministratorsQuery, 'administrators'>) {
@@ -83,10 +139,7 @@ export class AdministratorListComponent extends BaseListComponent<
             );
     }
 
-    createSearchQuery(
-        skip: number,
-        take: number,
-        searchTerm: string | null) {
+    createSearchQuery(skip: number, take: number, searchTerm: string | null) {
         let _filter = {};
         let filterOperator: LogicalOperator = LogicalOperator.AND;
 
@@ -110,10 +163,9 @@ export class AdministratorListComponent extends BaseListComponent<
                 take,
                 filter: {
                     ...(_filter ?? {}),
+                    ...this.filters.createFilterInput(),
                 },
-                sort: {
-                    updatedAt: SortOrder.DESC,
-                },
+                sort: this.sorts.createSortInput(),
                 filterOperator,
             },
         };

+ 1 - 1
packages/admin-ui/src/lib/settings/src/components/channel-detail/channel-detail.component.ts

@@ -69,7 +69,7 @@ export class ChannelDetailComponent
         this.init();
         this.zones$ = this.dataService.settings.getZones().mapSingle(data => data.zones);
         // TODO: make this lazy-loaded autocomplete
-        this.sellers$ = this.dataService.settings.getSellers().mapSingle(data => data.sellers.items);
+        this.sellers$ = this.dataService.settings.getSellerList().mapSingle(data => data.sellers.items);
         this.availableLanguageCodes$ = this.serverConfigService.getAvailableLanguages();
     }
 

+ 64 - 46
packages/admin-ui/src/lib/settings/src/components/channel-list/channel-list.component.html

@@ -1,48 +1,66 @@
-<vdr-action-bar>
-    <vdr-ab-right>
+<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-ab-right>
-</vdr-action-bar>
-
-<vdr-data-table [items]="channels$ | async">
-    <vdr-dt-column>{{ 'common.code' | translate }}</vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <ng-template let-channel="item">
-        <td class="left align-middle">
-            <vdr-channel-badge [channelCode]="channel.code"></vdr-channel-badge>
-            {{ channel.code | channelCodeToLabel | translate }}
-        </td>
-        <td class="right align-middle">
-            <vdr-table-row-action
-                iconShape="edit"
-                [label]="'common.edit' | translate"
-                [linkTo]="['./', channel.id]"
-            ></vdr-table-row-action>
-        </td>
-        <td class="right align-middle">
-            <vdr-dropdown>
-                <button type="button" class="btn btn-link btn-sm" vdrDropdownTrigger [disabled]="isDefaultChannel(channel.code)">
-                    {{ 'common.actions' | translate }}
-                    <clr-icon shape="caret down"></clr-icon>
-                </button>
-                <vdr-dropdown-menu vdrPosition="bottom-right">
-                    <button
-                        type="button"
-                        class="delete-button"
-                        (click)="deleteChannel(channel.id)"
-                        [disabled]="!(['SuperAdmin', 'DeleteChannel'] | hasPermission)"
-                        vdrDropdownItem
-                    >
-                        <clr-icon shape="trash" class="is-danger"></clr-icon>
-                        {{ 'common.delete' | translate }}
-                    </button>
-                </vdr-dropdown-menu>
-            </vdr-dropdown>
-        </td>
-    </ng-template>
-</vdr-data-table>
+                  <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-bulk-action-menu
+            locationId="channel-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-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>

+ 97 - 11
packages/admin-ui/src/lib/settings/src/components/channel-list/channel-list.component.ts

@@ -1,9 +1,23 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { FormControl } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import { ChannelFragment, DataService, ModalService, NotificationService } from '@vendure/admin-ui/core';
+import {
+    BaseListComponent,
+    ChannelFilterParameter,
+    ChannelSortParameter,
+    DataService,
+    GetChannelsQuery,
+    GetFacetListQuery,
+    ItemOf,
+    ModalService,
+    NotificationService,
+    SelectionManager,
+} from '@vendure/admin-ui/core';
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
-import { EMPTY, Observable, Subject } from 'rxjs';
-import { mergeMap, startWith, switchMap } from 'rxjs/operators';
+import { EMPTY, merge } from 'rxjs';
+import { debounceTime, filter, mergeMap, switchMap, takeUntil } from 'rxjs/operators';
+import { DataTableService } from '../../../../core/src/providers/data-table/data-table.service';
 
 @Component({
     selector: 'vdr-channel-list',
@@ -11,19 +25,91 @@ import { mergeMap, startWith, switchMap } from 'rxjs/operators';
     styleUrls: ['./channel-list.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class ChannelListComponent {
-    channels$: Observable<ChannelFragment[]>;
-    private refresh$ = new Subject();
+export class ChannelListComponent
+    extends BaseListComponent<GetChannelsQuery, ItemOf<GetChannelsQuery, 'channels'>>
+    implements OnInit
+{
+    searchTermControl = new FormControl('');
+    selectionManager = new SelectionManager<ItemOf<GetFacetListQuery, 'facets'>>({
+        multiSelect: true,
+        itemsAreEqual: (a, b) => a.id === b.id,
+        additiveMode: true,
+    });
+
+    readonly filters = this.dataTableService
+        .createFilterCollection<ChannelFilterParameter>()
+        .addFilter({
+            name: 'createdAt',
+            type: { kind: 'dateRange' },
+            label: _('common.created-at'),
+            filterField: 'createdAt',
+        })
+        .addFilter({
+            name: 'updatedAt',
+            type: { kind: 'dateRange' },
+            label: _('common.updated-at'),
+            filterField: 'updatedAt',
+        })
+        .addFilter({
+            name: 'code',
+            type: { kind: 'text' },
+            label: _('common.code'),
+            filterField: 'code',
+        })
+        .addFilter({
+            name: 'token',
+            type: { kind: 'text' },
+            label: _('settings.channel-token'),
+            filterField: 'token',
+        })
+        .connectToRoute(this.route);
+
+    readonly sorts = this.dataTableService
+        .createSortCollection<ChannelSortParameter>()
+        .defaultSort('createdAt', 'DESC')
+        .addSort({ name: 'createdAt' })
+        .addSort({ name: 'updatedAt' })
+        .addSort({ name: 'code' })
+        .addSort({ name: 'token' })
+        .connectToRoute(this.route);
 
     constructor(
         private dataService: DataService,
         private modalService: ModalService,
         private notificationService: NotificationService,
+        route: ActivatedRoute,
+        router: Router,
+        private dataTableService: DataTableService,
     ) {
-        this.channels$ = this.refresh$.pipe(
-            startWith(1),
-            switchMap(() => this.dataService.settings.getChannels().mapStream(data => data.channels)),
+        super(router, route);
+        super.setQueryFn(
+            (...args: any[]) => this.dataService.settings.getChannels(...args).refetchOnChannelChange(),
+            data => data.channels,
+            (skip, take) => ({
+                options: {
+                    skip,
+                    take,
+                    filter: {
+                        code: {
+                            contains: this.searchTermControl.value,
+                        },
+                        ...this.filters.createFilterInput(),
+                    },
+                    sort: this.sorts.createSortInput(),
+                },
+            }),
+        );
+    }
+
+    ngOnInit() {
+        super.ngOnInit();
+        const searchTerm$ = this.searchTermControl.valueChanges.pipe(
+            filter(value => value != null && (2 <= value.length || value.length === 0)),
+            debounceTime(250),
         );
+        merge(searchTerm$, this.filters.valueChanges, this.sorts.valueChanges)
+            .pipe(takeUntil(this.destroy$))
+            .subscribe(() => this.refresh());
     }
 
     isDefaultChannel(channelCode: string): boolean {
@@ -50,7 +136,7 @@ export class ChannelListComponent {
                     this.notificationService.success(_('common.notify-delete-success'), {
                         entity: 'Channel',
                     });
-                    this.refresh$.next(1);
+                    this.refresh();
                 },
                 err => {
                     this.notificationService.error(_('common.notify-delete-error'), {

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

@@ -1,22 +1,5 @@
-<vdr-action-bar>
-    <vdr-ab-left>
-        <input
-            type="text"
-            name="searchTerm"
-            [formControl]="searchTerm"
-            [placeholder]="'settings.search-country-by-name' | translate"
-            class="search-input"
-        />
-        <div>
-            <vdr-language-selector
-                [availableLanguageCodes]="availableLanguages$ | async"
-                [currentLanguageCode]="contentLanguage$ | async"
-                (languageCodeChange)="setLanguage($event)"
-            ></vdr-language-selector>
-        </div>
-    </vdr-ab-left>
-
-    <vdr-ab-right>
+<vdr-page-header>
+    <vdr-page-title>
         <vdr-action-bar-items locationId="country-list"></vdr-action-bar-items>
         <a
             class="btn btn-primary"
@@ -26,59 +9,65 @@
             <clr-icon shape="plus"></clr-icon>
             {{ 'settings.create-new-country' | translate }}
         </a>
-    </vdr-ab-right>
-</vdr-action-bar>
-
-<vdr-data-table [items]="countriesWithZones$ | async">
-    <vdr-dt-column>{{ 'common.code' | translate }}</vdr-dt-column>
-    <vdr-dt-column [expand]="true">{{ 'common.name' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'settings.zone' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'common.enabled' | translate }}</vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <ng-template let-country="item">
-        <td class="left align-middle">{{ country.code }}</td>
-        <td class="left align-middle">{{ country.name }}</td>
-        <td class="left align-middle">
-            <a
-                [routerLink]="['/settings', 'zones', { contents: zone.id }]"
-                *ngFor="let zone of country.zones"
-            >
-                <vdr-chip [colorFrom]="zone.name">{{ zone.name }}</vdr-chip>
-            </a>
-        </td>
-        <td class="left align-middle">
-            <clr-icon
-                [class.is-success]="country.enabled"
-                [attr.shape]="country.enabled ? 'check' : 'times'"
-            ></clr-icon>
-        </td>
-        <td class="right align-middle">
-            <vdr-table-row-action
-                iconShape="edit"
-                [label]="'common.edit' | translate"
-                [linkTo]="['./', country.id]"
-            ></vdr-table-row-action>
-        </td>
-        <td class="right align-middle">
-            <vdr-dropdown>
-                <button type="button" class="btn btn-link btn-sm" vdrDropdownTrigger>
-                    {{ 'common.actions' | translate }}
-                    <clr-icon shape="caret down"></clr-icon>
-                </button>
-                <vdr-dropdown-menu vdrPosition="bottom-right">
-                    <button
-                        type="button"
-                        class="delete-button"
-                        (click)="deleteCountry(country.id)"
-                        vdrDropdownItem
-                        [disabled]="!(['DeleteSettings', 'DeleteCountry'] | hasPermission)"
-                    >
-                        <clr-icon shape="trash" class="is-danger"></clr-icon>
-                        {{ 'common.delete' | translate }}
-                    </button>
-                </vdr-dropdown-menu>
-            </vdr-dropdown>
-        </td>
-    </ng-template>
-</vdr-data-table>
+    </vdr-page-title>
+</vdr-page-header>
+<vdr-page-body>
+    <vdr-language-selector
+        [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-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.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">
+                <div class="badge success" *ngIf="country.enabled">{{ 'common.enabled' }}</div>
+                <div class="badge warning" *ngIf="!country.enabled">{{ 'common.disabled' }}</div>
+            </ng-template>
+        </vdr-dt2-column>
+    </vdr-data-table-2>
+</vdr-page-body>

+ 89 - 46
packages/admin-ui/src/lib/settings/src/components/country-list/country-list.component.ts

@@ -1,20 +1,27 @@
-import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
-import { UntypedFormControl } from '@angular/forms';
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { FormControl } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
+    BaseListComponent,
+    CountryFilterParameter,
+    CountrySortParameter,
     DataService,
+    DataTableService,
     DeletionResult,
     GetCountryListQuery,
-    GetZonesQuery,
+    GetFacetListQuery,
     ItemOf,
     LanguageCode,
     ModalService,
     NotificationService,
+    SelectionManager,
+    SellerFilterParameter,
+    SellerSortParameter,
     ServerConfigService,
-    ZoneFragment,
 } from '@vendure/admin-ui/core';
-import { combineLatest, EMPTY, Observable, Subject } from 'rxjs';
-import { map, startWith, switchMap, tap } from 'rxjs/operators';
+import { EMPTY, merge, Observable } from 'rxjs';
+import { debounceTime, filter, switchMap, takeUntil } from 'rxjs/operators';
 
 @Component({
     selector: 'vdr-country-list',
@@ -22,58 +29,98 @@ import { map, startWith, switchMap, tap } from 'rxjs/operators';
     styleUrls: ['./country-list.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class CountryListComponent implements OnInit, OnDestroy {
-    searchTerm = new UntypedFormControl('');
-    countriesWithZones$: Observable<
-        Array<ItemOf<GetCountryListQuery, 'countries'> & { zones: GetZonesQuery['zones'] }>
-    >;
-    zones$: Observable<GetZonesQuery['zones']>;
+export class CountryListComponent
+    extends BaseListComponent<GetCountryListQuery, ItemOf<GetCountryListQuery, 'countries'>>
+    implements OnInit
+{
+    searchTermControl = new FormControl('');
+    selectionManager = new SelectionManager<ItemOf<GetFacetListQuery, 'facets'>>({
+        multiSelect: true,
+        itemsAreEqual: (a, b) => a.id === b.id,
+        additiveMode: true,
+    });
     availableLanguages$: Observable<LanguageCode[]>;
     contentLanguage$: Observable<LanguageCode>;
 
-    private countries: GetCountryListQuery['countries']['items'] = [];
-    private destroy$ = new Subject<void>();
-    private refresh$ = new Subject<void>();
+    readonly filters = this.dataTableService
+        .createFilterCollection<CountryFilterParameter>()
+        .addFilter({
+            name: 'createdAt',
+            type: { kind: 'dateRange' },
+            label: _('common.created-at'),
+            filterField: 'createdAt',
+        })
+        .addFilter({
+            name: 'updatedAt',
+            type: { kind: 'dateRange' },
+            label: _('common.updated-at'),
+            filterField: 'updatedAt',
+        })
+        .addFilter({
+            name: 'name',
+            type: { kind: 'text' },
+            label: _('common.name'),
+            filterField: 'name',
+        })
+        .addFilter({
+            name: 'enabled',
+            type: { kind: 'boolean' },
+            label: _('common.enabled'),
+            filterField: 'enabled',
+        })
+        .connectToRoute(this.route);
+
+    readonly sorts = this.dataTableService
+        .createSortCollection<CountrySortParameter>()
+        .defaultSort('createdAt', 'DESC')
+        .addSort({ name: 'createdAt' })
+        .addSort({ name: 'updatedAt' })
+        .addSort({ name: 'name' })
+        .connectToRoute(this.route);
 
     constructor(
         private dataService: DataService,
         private notificationService: NotificationService,
         private modalService: ModalService,
         private serverConfigService: ServerConfigService,
-    ) {}
+        route: ActivatedRoute,
+        router: Router,
+        private dataTableService: DataTableService,
+    ) {
+        super(router, route);
+        super.setQueryFn(
+            (...args: any[]) => this.dataService.settings.getCountries(...args).refetchOnChannelChange(),
+            data => data.countries,
+            (skip, take) => ({
+                options: {
+                    skip,
+                    take,
+                    filter: {
+                        name: {
+                            contains: this.searchTermControl.value,
+                        },
+                        ...this.filters.createFilterInput(),
+                    },
+                    sort: this.sorts.createSortInput(),
+                },
+            }),
+        );
+    }
 
     ngOnInit() {
+        super.ngOnInit();
         this.contentLanguage$ = this.dataService.client
             .uiState()
             .mapStream(({ uiState }) => uiState.contentLanguage);
-
-        const countries$ = combineLatest(
-            this.contentLanguage$,
-            this.searchTerm.valueChanges.pipe(startWith(null)),
-        ).pipe(
-            map(([__, term]) => term),
-            switchMap(term => this.dataService.settings.getCountries(999, 0, term).single$),
-            tap(data => {
-                this.countries = data.countries.items;
-            }),
-            map(data => data.countries.items),
-        );
-
-        this.zones$ = this.dataService.settings.getZones().mapStream(data => data.zones);
-
-        this.countriesWithZones$ = combineLatest(countries$, this.zones$).pipe(
-            map(([countries, zones]) => countries.map(country => ({
-                    ...country,
-                    zones: zones.filter(z => !!z.members.find(c => c.id === country.id)),
-                }))),
-        );
-
         this.availableLanguages$ = this.serverConfigService.getAvailableLanguages();
-    }
 
-    ngOnDestroy() {
-        this.destroy$.next(undefined);
-        this.destroy$.complete();
+        const searchTerm$ = this.searchTermControl.valueChanges.pipe(
+            filter(value => value != null && (2 <= value.length || value.length === 0)),
+            debounceTime(250),
+        );
+        merge(searchTerm$, this.filters.valueChanges, this.sorts.valueChanges)
+            .pipe(takeUntil(this.destroy$))
+            .subscribe(() => this.refresh());
     }
 
     setLanguage(code: LanguageCode) {
@@ -112,8 +159,4 @@ export class CountryListComponent implements OnInit, OnDestroy {
                 },
             );
     }
-
-    private isZone(input: ZoneFragment | { name: string } | string): input is ZoneFragment {
-        return input.hasOwnProperty('id');
-    }
 }

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

@@ -1,13 +1,3 @@
-<vdr-action-bar>
-    <vdr-ab-right>
-        <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-ab-right>
-</vdr-action-bar>
-
 <vdr-data-table
     [items]="items$ | async"
     [itemsPerPage]="itemsPerPage$ | async"
@@ -52,3 +42,77 @@
         </td>
     </ng-template>
 </vdr-data-table>
+
+<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-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')"
+        >
+            <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">
+                <div class="badge success" *ngIf="paymentMethod.enabled">{{ 'common.enabled' }}</div>
+                <div class="badge warning" *ngIf="!paymentMethod.enabled">{{ 'common.disabled' }}</div>
+            </ng-template>
+        </vdr-dt2-column>
+    </vdr-data-table-2>
+</vdr-page-body>

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

@@ -1,17 +1,23 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { FormControl } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
     BaseListComponent,
     DataService,
+    DataTableService,
     DeletionResult,
+    GetFacetListQuery,
     GetPaymentMethodListQuery,
     ItemOf,
     ModalService,
     NotificationService,
+    PaymentMethodFilterParameter,
+    PaymentMethodSortParameter,
+    SelectionManager,
 } from '@vendure/admin-ui/core';
-import { EMPTY } from 'rxjs';
-import { map, switchMap } from 'rxjs/operators';
+import { EMPTY, merge } from 'rxjs';
+import { debounceTime, filter, map, switchMap, takeUntil } from 'rxjs/operators';
 
 @Component({
     selector: 'vdr-payment-method-list',
@@ -19,22 +25,104 @@ import { map, switchMap } from 'rxjs/operators';
     styleUrls: ['./payment-method-list.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class PaymentMethodListComponent extends BaseListComponent<
-    GetPaymentMethodListQuery,
-    ItemOf<GetPaymentMethodListQuery, 'paymentMethods'>
-> {
+export class PaymentMethodListComponent
+    extends BaseListComponent<GetPaymentMethodListQuery, ItemOf<GetPaymentMethodListQuery, 'paymentMethods'>>
+    implements OnInit
+{
+    searchTermControl = new FormControl('');
+    selectionManager = new SelectionManager<ItemOf<GetFacetListQuery, 'facets'>>({
+        multiSelect: true,
+        itemsAreEqual: (a, b) => a.id === b.id,
+        additiveMode: true,
+    });
+
+    readonly filters = this.dataTableService
+        .createFilterCollection<PaymentMethodFilterParameter>()
+        .addFilter({
+            name: 'createdAt',
+            type: { kind: 'dateRange' },
+            label: _('common.created-at'),
+            filterField: 'createdAt',
+        })
+        .addFilter({
+            name: 'updatedAt',
+            type: { kind: 'dateRange' },
+            label: _('common.updated-at'),
+            filterField: 'updatedAt',
+        })
+        .addFilter({
+            name: 'name',
+            type: { kind: 'text' },
+            label: _('common.name'),
+            filterField: 'name',
+        })
+        .addFilter({
+            name: 'code',
+            type: { kind: 'text' },
+            label: _('common.code'),
+            filterField: 'code',
+        })
+        .addFilter({
+            name: 'enabled',
+            type: { kind: 'boolean' },
+            label: _('common.enabled'),
+            filterField: 'enabled',
+        })
+        .addFilter({
+            name: 'description',
+            type: { kind: 'text' },
+            label: _('common.description'),
+            filterField: 'description',
+        })
+        .connectToRoute(this.route);
+
+    readonly sorts = this.dataTableService
+        .createSortCollection<PaymentMethodSortParameter>()
+        .defaultSort('createdAt', 'DESC')
+        .addSort({ name: 'createdAt' })
+        .addSort({ name: 'updatedAt' })
+        .addSort({ name: 'name' })
+        .addSort({ name: 'code' })
+        .addSort({ name: 'description' })
+        .connectToRoute(this.route);
+
     constructor(
         private dataService: DataService,
         router: Router,
         route: ActivatedRoute,
         private modalService: ModalService,
         private notificationService: NotificationService,
+        private dataTableService: DataTableService,
     ) {
         super(router, route);
         super.setQueryFn(
             (...args: any[]) => this.dataService.settings.getPaymentMethods(...args).refetchOnChannelChange(),
             data => data.paymentMethods,
+            (skip, take) => ({
+                options: {
+                    skip,
+                    take,
+                    filter: {
+                        name: {
+                            contains: this.searchTermControl.value,
+                        },
+                        ...this.filters.createFilterInput(),
+                    },
+                    sort: this.sorts.createSortInput(),
+                },
+            }),
+        );
+    }
+
+    ngOnInit() {
+        super.ngOnInit();
+        const searchTerm$ = this.searchTermControl.valueChanges.pipe(
+            filter(value => value != null && (2 <= value.length || value.length === 0)),
+            debounceTime(250),
         );
+        merge(searchTerm$, this.filters.valueChanges, this.sorts.valueChanges)
+            .pipe(takeUntil(this.destroy$))
+            .subscribe(() => this.refresh());
     }
 
     deletePaymentMethod(paymentMethodId: string) {

+ 104 - 77
packages/admin-ui/src/lib/settings/src/components/role-list/role-list.component.html

@@ -1,86 +1,113 @@
-<vdr-action-bar>
-    <vdr-ab-right>
+<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-ab-right>
-</vdr-action-bar>
-
-<vdr-data-table
-    [items]="visibleRoles$ | async"
-    [itemsPerPage]="itemsPerPage$ | async"
-    [totalItems]="totalItems$ | async"
-    [currentPage]="currentPage$ | async"
-    (pageChange)="setPageNumber($event)"
-    (itemsPerPageChange)="setItemsPerPage($event)"
->
-    <vdr-dt-column>{{ 'common.description' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'common.code' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'settings.channel' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'settings.permissions' | translate }}</vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <ng-template let-role="item">
-        <td class="left align-middle">{{ role.description }}</td>
-        <td class="left align-middle"><span *ngIf="!isDefaultRole(role)">{{ role.code }}</span></td>
-        <td class="left align-middle">
-            <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>
-        </td>
-        <td class="left align-middle">
-            <ng-container *ngIf="!isDefaultRole(role); else defaultRole">
-                <vdr-chip *ngFor="let permission of role.permissions |  slice: 0:displayLimit[role.id] || 3">{{ permission }}</vdr-chip>
-                <button
-                    class="btn btn-sm btn-secondary btn-icon"
-                    *ngIf="role.permissions.length > initialLimit"
-                    (click)="toggleDisplayLimit(role)"
-                >
-                    <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>
-            </ng-container>
-            <ng-template #defaultRole>
-                <span class="default-role-label">{{ 'settings.default-role-label' | translate }}</span>
+    </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-bulk-action-menu
+            locationId="role-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-role="item">
+                {{ role.createdAt | localeDate : 'short' }}
             </ng-template>
-        </td>
-        <td class="right align-middle">
-            <vdr-table-row-action
-                *ngIf="!isDefaultRole(role)"
-                iconShape="edit"
-                [label]="'common.edit' | translate"
-                [linkTo]="['./', role.id]"
-            ></vdr-table-row-action>
-        </td>
-        <td class="right align-middle">
-            <vdr-dropdown>
-                <button type="button" class="btn btn-link btn-sm" vdrDropdownTrigger [disabled]="isDefaultRole(role)">
-                    {{ 'common.actions' | translate }}
-                    <clr-icon shape="caret down"></clr-icon>
-                </button>
-                <vdr-dropdown-menu vdrPosition="bottom-right">
+        </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>
+        </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
-                        type="button"
-                        class="delete-button"
-                        (click)="deleteRole(role.id)"
-                        [disabled]="!('SuperAdmin' | hasPermission)"
-                        vdrDropdownItem
+                        class="button-ghost"
+                        *ngIf="role.permissions.length > initialLimit"
+                        (click)="toggleDisplayLimit(role)"
                     >
-                        <clr-icon shape="trash" class="is-danger"></clr-icon>
-                        {{ 'common.delete' | translate }}
+                        <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>
-                </vdr-dropdown-menu>
-            </vdr-dropdown>
-        </td>
-    </ng-template>
-</vdr-data-table>
+              </div>
+                </ng-container>
+                <ng-template #defaultRole>
+                    <span class="default-role-label">{{ 'settings.default-role-label' | translate }}</span>
+                </ng-template>
+            </ng-template>
+        </vdr-dt2-column>
+    </vdr-data-table-2>
+</vdr-page-body>

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

@@ -2,3 +2,10 @@
 .default-role-label {
     color: var(--color-grey-400);
 }
+
+.permissions-list {
+    max-width: 500px;
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
+}

+ 75 - 7
packages/admin-ui/src/lib/settings/src/components/role-list/role-list.component.ts

@@ -1,14 +1,24 @@
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { FormControl } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import { BaseListComponent, GetRolesQuery, ItemOf } from '@vendure/admin-ui/core';
-import { Role } from '@vendure/admin-ui/core';
-import { NotificationService } from '@vendure/admin-ui/core';
-import { DataService } from '@vendure/admin-ui/core';
-import { ModalService } from '@vendure/admin-ui/core';
+import {
+    BaseListComponent,
+    DataService,
+    DataTableService,
+    GetFacetListQuery,
+    GetRolesQuery,
+    ItemOf,
+    ModalService,
+    NotificationService,
+    Role,
+    RoleFilterParameter,
+    RoleSortParameter,
+    SelectionManager,
+} from '@vendure/admin-ui/core';
 import { CUSTOMER_ROLE_CODE, SUPER_ADMIN_ROLE_CODE } from '@vendure/common/lib/shared-constants';
-import { EMPTY, Observable } from 'rxjs';
-import { map, switchMap } from 'rxjs/operators';
+import { EMPTY, merge, Observable } from 'rxjs';
+import { debounceTime, filter, map, switchMap, takeUntil } from 'rxjs/operators';
 
 @Component({
     selector: 'vdr-role-list',
@@ -23,6 +33,43 @@ export class RoleListComponent
     readonly initialLimit = 3;
     displayLimit: { [id: string]: number } = {};
     visibleRoles$: Observable<Array<ItemOf<GetRolesQuery, 'roles'>>>;
+    searchTermControl = new FormControl('');
+    selectionManager = new SelectionManager<ItemOf<GetFacetListQuery, 'facets'>>({
+        multiSelect: true,
+        itemsAreEqual: (a, b) => a.id === b.id,
+        additiveMode: true,
+    });
+
+    readonly filters = this.dataTableService
+        .createFilterCollection<RoleFilterParameter>()
+        .addFilter({
+            name: 'createdAt',
+            type: { kind: 'dateRange' },
+            label: _('common.created-at'),
+            filterField: 'createdAt',
+        })
+        .addFilter({
+            name: 'updatedAt',
+            type: { kind: 'dateRange' },
+            label: _('common.updated-at'),
+            filterField: 'updatedAt',
+        })
+        .addFilter({
+            name: 'code',
+            type: { kind: 'text' },
+            label: _('common.code'),
+            filterField: 'code',
+        })
+        .connectToRoute(this.route);
+
+    readonly sorts = this.dataTableService
+        .createSortCollection<RoleSortParameter>()
+        .defaultSort('createdAt', 'DESC')
+        .addSort({ name: 'createdAt' })
+        .addSort({ name: 'updatedAt' })
+        .addSort({ name: 'code' })
+        .addSort({ name: 'description' })
+        .connectToRoute(this.route);
 
     constructor(
         private modalService: ModalService,
@@ -30,11 +77,25 @@ export class RoleListComponent
         private dataService: DataService,
         router: Router,
         route: ActivatedRoute,
+        private dataTableService: DataTableService,
     ) {
         super(router, route);
         super.setQueryFn(
             (...args: any[]) => this.dataService.administrator.getRoles(...args),
             data => data.roles,
+            (skip, take) => ({
+                options: {
+                    skip,
+                    take,
+                    filter: {
+                        code: {
+                            contains: this.searchTermControl.value,
+                        },
+                        ...this.filters.createFilterInput(),
+                    },
+                    sort: this.sorts.createSortInput(),
+                },
+            }),
         );
     }
 
@@ -43,6 +104,13 @@ export class RoleListComponent
         this.visibleRoles$ = this.items$.pipe(
             map(roles => roles.filter(role => role.code !== CUSTOMER_ROLE_CODE)),
         );
+        const searchTerm$ = this.searchTermControl.valueChanges.pipe(
+            filter(value => value != null && (2 <= value.length || value.length === 0)),
+            debounceTime(250),
+        );
+        merge(searchTerm$, this.filters.valueChanges, this.sorts.valueChanges)
+            .pipe(takeUntil(this.destroy$))
+            .subscribe(() => this.refresh());
     }
 
     toggleDisplayLimit(role: ItemOf<GetRolesQuery, 'roles'>) {

+ 58 - 43
packages/admin-ui/src/lib/settings/src/components/seller-list/seller-list.component.html

@@ -1,47 +1,62 @@
-<vdr-action-bar>
-    <vdr-ab-right>
+<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']">
+        <a
+            class="btn btn-primary"
+            [routerLink]="['./create']"
+            *vdrIfPermissions="['SuperAdmin', 'CreateSeller']"
+        >
             <clr-icon shape="plus"></clr-icon>
             {{ 'settings.create-new-seller' | translate }}
         </a>
-    </vdr-ab-right>
-</vdr-action-bar>
-
-<vdr-data-table [items]="sellers$ | async">
-    <vdr-dt-column></vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <ng-template let-seller="item">
-        <td class="left align-middle">
-            {{ seller.name }}
-        </td>
-        <td class="right align-middle">
-            <vdr-table-row-action
-                iconShape="edit"
-                [label]="'common.edit' | translate"
-                [linkTo]="['./', seller.id]"
-            ></vdr-table-row-action>
-        </td>
-        <td class="right align-middle">
-            <vdr-dropdown>
-                <button type="button" class="btn btn-link btn-sm" vdrDropdownTrigger>
-                    {{ 'common.actions' | translate }}
-                    <clr-icon shape="caret down"></clr-icon>
-                </button>
-                <vdr-dropdown-menu vdrPosition="bottom-right">
-                    <button
-                        type="button"
-                        class="delete-button"
-                        (click)="deleteSeller(seller.id)"
-                        [disabled]="!(['SuperAdmin', 'DeleteSeller'] | hasPermission)"
-                        vdrDropdownItem
-                    >
-                        <clr-icon shape="trash" class="is-danger"></clr-icon>
-                        {{ 'common.delete' | translate }}
-                    </button>
-                </vdr-dropdown-menu>
-            </vdr-dropdown>
-        </td>
-    </ng-template>
-</vdr-data-table>
+    </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-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.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>

+ 84 - 11
packages/admin-ui/src/lib/settings/src/components/seller-list/seller-list.component.ts

@@ -1,8 +1,22 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { FormControl } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import { DataService, ModalService, NotificationService, SellerFragment } from '@vendure/admin-ui/core';
-import { EMPTY, Observable, Subject } from 'rxjs';
-import { mergeMap, startWith, switchMap } from 'rxjs/operators';
+import {
+    BaseListComponent,
+    DataService,
+    DataTableService,
+    GetFacetListQuery,
+    GetSellersQuery,
+    ItemOf,
+    ModalService,
+    NotificationService,
+    SelectionManager,
+    SellerFilterParameter,
+    SellerSortParameter,
+} from '@vendure/admin-ui/core';
+import { EMPTY, merge, switchMap } from 'rxjs';
+import { debounceTime, filter, takeUntil } from 'rxjs/operators';
 
 @Component({
     selector: 'vdr-seller-list',
@@ -10,19 +24,78 @@ import { mergeMap, startWith, switchMap } from 'rxjs/operators';
     styleUrls: ['./seller-list.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class SellerListComponent {
-    sellers$: Observable<SellerFragment[]>;
-    private refresh$ = new Subject();
+export class SellerListComponent
+    extends BaseListComponent<GetSellersQuery, ItemOf<GetSellersQuery, 'sellers'>>
+    implements OnInit
+{
+    searchTermControl = new FormControl('');
+    selectionManager = new SelectionManager<ItemOf<GetFacetListQuery, 'facets'>>({
+        multiSelect: true,
+        itemsAreEqual: (a, b) => a.id === b.id,
+        additiveMode: true,
+    });
+
+    readonly filters = this.dataTableService
+        .createFilterCollection<SellerFilterParameter>()
+        .addFilter({
+            name: 'createdAt',
+            type: { kind: 'dateRange' },
+            label: _('common.created-at'),
+            filterField: 'createdAt',
+        })
+        .addFilter({
+            name: 'updatedAt',
+            type: { kind: 'dateRange' },
+            label: _('common.updated-at'),
+            filterField: 'updatedAt',
+        })
+        .connectToRoute(this.route);
+
+    readonly sorts = this.dataTableService
+        .createSortCollection<SellerSortParameter>()
+        .defaultSort('createdAt', 'DESC')
+        .addSort({ name: 'createdAt' })
+        .addSort({ name: 'updatedAt' })
+        .addSort({ name: 'name' })
+        .connectToRoute(this.route);
 
     constructor(
         private dataService: DataService,
         private modalService: ModalService,
         private notificationService: NotificationService,
+        route: ActivatedRoute,
+        router: Router,
+        private dataTableService: DataTableService,
     ) {
-        this.sellers$ = this.refresh$.pipe(
-            startWith(1),
-            switchMap(() => this.dataService.settings.getSellers().mapStream(data => data.sellers.items)),
+        super(router, route);
+        super.setQueryFn(
+            (...args: any[]) => this.dataService.settings.getSellerList(...args).refetchOnChannelChange(),
+            data => data.sellers,
+            (skip, take) => ({
+                options: {
+                    skip,
+                    take,
+                    filter: {
+                        name: {
+                            contains: this.searchTermControl.value,
+                        },
+                        ...this.filters.createFilterInput(),
+                    },
+                    sort: this.sorts.createSortInput(),
+                },
+            }),
+        );
+    }
+
+    ngOnInit() {
+        super.ngOnInit();
+        const searchTerm$ = this.searchTermControl.valueChanges.pipe(
+            filter(value => value != null && (2 <= value.length || value.length === 0)),
+            debounceTime(250),
         );
+        merge(searchTerm$, this.filters.valueChanges, this.sorts.valueChanges)
+            .pipe(takeUntil(this.destroy$))
+            .subscribe(() => this.refresh());
     }
 
     deleteSeller(id: string) {
@@ -40,7 +113,7 @@ export class SellerListComponent {
                     this.notificationService.success(_('common.notify-delete-success'), {
                         entity: 'Seller',
                     });
-                    this.refresh$.next(1);
+                    this.refresh();
                 },
                 err => {
                     this.notificationService.error(_('common.notify-delete-error'), {

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

@@ -1,12 +1,5 @@
-<vdr-action-bar>
-    <vdr-ab-left>
-        <vdr-language-selector
-            [availableLanguageCodes]="availableLanguages$ | async"
-            [currentLanguageCode]="contentLanguage$ | async"
-            (languageCodeChange)="setLanguage($event)"
-        ></vdr-language-selector>
-    </vdr-ab-left>
-    <vdr-ab-right>
+<vdr-page-header>
+    <vdr-page-title>
         <vdr-action-bar-items locationId="shipping-method-list"></vdr-action-bar-items>
         <a
             class="btn btn-primary"
@@ -16,79 +9,98 @@
             <clr-icon shape="plus"></clr-icon>
             {{ 'settings.create-new-shipping-method' | translate }}
         </a>
-    </vdr-ab-right>
-</vdr-action-bar>
-
-<vdr-data-table
-    [items]="items$ | async"
-    [itemsPerPage]="itemsPerPage$ | async"
-    [totalItems]="totalItems$ | async"
-    [currentPage]="currentPage$ | async"
-    (pageChange)="setPageNumber($event)"
-    (itemsPerPageChange)="setItemsPerPage($event)"
->
-    <vdr-dt-column>{{ 'common.code' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'common.name' | translate }}</vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <ng-template let-shippingMethod="item">
-        <td class="left align-middle">{{ shippingMethod.code }}</td>
-        <td class="left align-middle">{{ shippingMethod.name }}</td>
-        <td class="right align-middle">
-            <vdr-table-row-action
-                iconShape="edit"
-                [label]="'common.edit' | translate"
-                [linkTo]="['./', shippingMethod.id]"
-            ></vdr-table-row-action>
-        </td>
-        <td class="right align-middle">
-            <vdr-dropdown>
-                <button type="button" class="btn btn-link btn-sm" vdrDropdownTrigger>
-                    {{ 'common.actions' | translate }}
-                    <clr-icon shape="caret down"></clr-icon>
-                </button>
-                <vdr-dropdown-menu vdrPosition="bottom-right">
-                    <button
-                        type="button"
-                        class="delete-button"
-                        (click)="deleteShippingMethod(shippingMethod.id)"
-                        [disabled]="!(['DeleteSettings', 'DeleteShippingMethod'] | hasPermission)"
-                        vdrDropdownItem
-                    >
-                        <clr-icon shape="trash" class="is-danger"></clr-icon>
-                        {{ 'common.delete' | translate }}
-                    </button>
-                </vdr-dropdown-menu>
-            </vdr-dropdown>
-        </td>
-    </ng-template>
-</vdr-data-table>
+    </vdr-page-title>
+</vdr-page-header>
+<vdr-page-body>
+    <vdr-language-selector
+        [availableLanguageCodes]="availableLanguages$ | async"
+        [currentLanguageCode]="contentLanguage$ | async"
+        (languageCodeChange)="setLanguage($event)"
+    ></vdr-language-selector>
+    <vdr-data-table-2
+        class="mt-2"
+        id="shippingMethod-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="shippingMethod-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-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 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>
-                </div>
-            </clr-accordion-content>
-        </clr-accordion-panel>
-    </clr-accordion>
-</div>
+                </clr-accordion-content>
+            </clr-accordion-panel>
+        </clr-accordion>
+    </div>
+</vdr-page-body>

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

@@ -1,21 +1,27 @@
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { FormControl } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
     BaseListComponent,
     DataService,
+    DataTableService,
     GetActiveChannelQuery,
+    GetFacetListQuery,
     GetShippingMethodListQuery,
     ItemOf,
     LanguageCode,
     ModalService,
     NotificationService,
+    SelectionManager,
     ServerConfigService,
+    ShippingMethodFilterParameter,
     ShippingMethodQuote,
+    ShippingMethodSortParameter,
     TestEligibleShippingMethodsInput,
 } from '@vendure/admin-ui/core';
-import { EMPTY, Observable, Subject } from 'rxjs';
-import { switchMap, tap } from 'rxjs/operators';
+import { EMPTY, merge, Observable, Subject } from 'rxjs';
+import { debounceTime, filter, switchMap, takeUntil } from 'rxjs/operators';
 
 import { TestAddress } from '../test-address-form/test-address-form.component';
 import { TestOrderLine } from '../test-order-builder/test-order-builder.component';
@@ -41,6 +47,55 @@ export class ShippingMethodListComponent
     availableLanguages$: Observable<LanguageCode[]>;
     contentLanguage$: Observable<LanguageCode>;
     private fetchTestResult$ = new Subject<[TestAddress, TestOrderLine[]]>();
+    searchTermControl = new FormControl('');
+    selectionManager = new SelectionManager<ItemOf<GetFacetListQuery, 'facets'>>({
+        multiSelect: true,
+        itemsAreEqual: (a, b) => a.id === b.id,
+        additiveMode: true,
+    });
+    readonly filters = this.dataTableService
+        .createFilterCollection<ShippingMethodFilterParameter>()
+        .addFilter({
+            name: 'createdAt',
+            type: { kind: 'dateRange' },
+            label: _('common.created-at'),
+            filterField: 'createdAt',
+        })
+        .addFilter({
+            name: 'updatedAt',
+            type: { kind: 'dateRange' },
+            label: _('common.updated-at'),
+            filterField: 'updatedAt',
+        })
+        .addFilter({
+            name: 'name',
+            type: { kind: 'text' },
+            label: _('common.name'),
+            filterField: 'name',
+        })
+        .addFilter({
+            name: 'code',
+            type: { kind: 'text' },
+            label: _('common.code'),
+            filterField: 'code',
+        })
+        .addFilter({
+            name: 'description',
+            type: { kind: 'text' },
+            label: _('common.description'),
+            filterField: 'description',
+        })
+        .connectToRoute(this.route);
+
+    readonly sorts = this.dataTableService
+        .createSortCollection<ShippingMethodSortParameter>()
+        .defaultSort('createdAt', 'DESC')
+        .addSort({ name: 'createdAt' })
+        .addSort({ name: 'updatedAt' })
+        .addSort({ name: 'name' })
+        .addSort({ name: 'code' })
+        .addSort({ name: 'description' })
+        .connectToRoute(this.route);
 
     constructor(
         private modalService: ModalService,
@@ -49,12 +104,26 @@ export class ShippingMethodListComponent
         private serverConfigService: ServerConfigService,
         router: Router,
         route: ActivatedRoute,
+        private dataTableService: DataTableService,
     ) {
         super(router, route);
         super.setQueryFn(
             (...args: any[]) =>
                 this.dataService.shippingMethod.getShippingMethods(...args).refetchOnChannelChange(),
             data => data.shippingMethods,
+            (skip, take) => ({
+                options: {
+                    skip,
+                    take,
+                    filter: {
+                        name: {
+                            contains: this.searchTermControl.value,
+                        },
+                        ...this.filters.createFilterInput(),
+                    },
+                    sort: this.sorts.createSortInput(),
+                },
+            }),
         );
     }
 
@@ -75,10 +144,17 @@ export class ShippingMethodListComponent
             .getActiveChannel()
             .mapStream(data => data.activeChannel);
         this.availableLanguages$ = this.serverConfigService.getAvailableLanguages();
+
+        const searchTerm$ = this.searchTermControl.valueChanges.pipe(
+            filter(value => value != null && (2 <= value.length || value.length === 0)),
+            debounceTime(250),
+        );
         this.contentLanguage$ = this.dataService.client
             .uiState()
-            .mapStream(({ uiState }) => uiState.contentLanguage)
-            .pipe(tap(() => this.refresh()));
+            .mapStream(({ uiState }) => uiState.contentLanguage);
+        merge(searchTerm$, this.contentLanguage$, this.filters.valueChanges, this.sorts.valueChanges)
+            .pipe(takeUntil(this.destroy$))
+            .subscribe(() => this.refresh());
     }
 
     deleteShippingMethod(id: string) {

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

@@ -1,49 +1,69 @@
-<vdr-action-bar>
-    <vdr-ab-right>
+<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']">
+        <a
+            class="btn btn-primary"
+            [routerLink]="['./create']"
+            *vdrIfPermissions="['CreateSettings', 'CreateTaxCategory']"
+        >
             <clr-icon shape="plus"></clr-icon>
             {{ 'settings.create-new-tax-category' | translate }}
         </a>
-    </vdr-ab-right>
-</vdr-action-bar>
-
-<vdr-data-table [items]="taxCategories$ | async">
-    <vdr-dt-column>{{ 'common.name' | translate }}</vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <ng-template let-taxCategory="item">
-        <td class="left align-middle">{{ taxCategory.name }}</td>
-        <td class="left align-middle">
-            <vdr-chip *ngIf="taxCategory.isDefault">{{ 'common.default-tax-category' | translate }}</vdr-chip>
-        </td>
-        <td class="right align-middle">
-            <vdr-table-row-action
-                iconShape="edit"
-                [label]="'common.edit' | translate"
-                [linkTo]="['./', taxCategory.id]"
-            ></vdr-table-row-action>
-        </td>
-        <td class="right align-middle">
-            <vdr-dropdown>
-                <button type="button" class="btn btn-link btn-sm" vdrDropdownTrigger>
-                    {{ 'common.actions' | translate }}
-                    <clr-icon shape="caret down"></clr-icon>
-                </button>
-                <vdr-dropdown-menu vdrPosition="bottom-right">
-                    <button
-                        type="button"
-                        class="delete-button"
-                        (click)="deleteTaxCategory(taxCategory)"
-                        [disabled]="!(['DeleteSettings', 'DeleteTaxCategory'] | hasPermission)"
-                        vdrDropdownItem
-                    >
-                        <clr-icon shape="trash" class="is-danger"></clr-icon>
-                        {{ 'common.delete' | translate }}
-                    </button>
-                </vdr-dropdown-menu>
-            </vdr-dropdown>
-        </td>
-    </ng-template>
-</vdr-data-table>
+    </vdr-page-title>
+</vdr-page-header>
+<vdr-page-body>
+    <vdr-data-table-2
+        class="mt-2"
+        id="taxCategory-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-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-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>

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

@@ -1,16 +1,24 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { FormControl } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
+    BaseListComponent,
     DataService,
+    DataTableService,
     DeletionResult,
+    GetFacetListQuery,
     GetTaxCategoriesQuery,
+    ItemOf,
     ModalService,
     NotificationService,
-    QueryResult,
+    SelectionManager,
+    TaxCategoryFilterParameter,
     TaxCategoryFragment,
+    TaxCategorySortParameter,
 } from '@vendure/admin-ui/core';
-import { EMPTY, Observable } from 'rxjs';
-import { map, switchMap } from 'rxjs/operators';
+import { EMPTY, merge } from 'rxjs';
+import { debounceTime, filter, map, switchMap, takeUntil } from 'rxjs/operators';
 
 @Component({
     selector: 'vdr-tax-list',
@@ -18,17 +26,84 @@ import { map, switchMap } from 'rxjs/operators';
     styleUrls: ['./tax-category-list.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class TaxCategoryListComponent {
-    taxCategories$: Observable<TaxCategoryFragment[]>;
-    private queryResult: QueryResult<GetTaxCategoriesQuery>;
+export class TaxCategoryListComponent
+    extends BaseListComponent<GetTaxCategoriesQuery, ItemOf<GetTaxCategoriesQuery, 'taxCategories'>>
+    implements OnInit
+{
+    searchTermControl = new FormControl('');
+    selectionManager = new SelectionManager<ItemOf<GetFacetListQuery, 'facets'>>({
+        multiSelect: true,
+        itemsAreEqual: (a, b) => a.id === b.id,
+        additiveMode: true,
+    });
+
+    readonly filters = this.dataTableService
+        .createFilterCollection<TaxCategoryFilterParameter>()
+        .addFilter({
+            name: 'createdAt',
+            type: { kind: 'dateRange' },
+            label: _('common.created-at'),
+            filterField: 'createdAt',
+        })
+        .addFilter({
+            name: 'updatedAt',
+            type: { kind: 'dateRange' },
+            label: _('common.updated-at'),
+            filterField: 'updatedAt',
+        })
+        .addFilter({
+            name: 'name',
+            type: { kind: 'text' },
+            label: _('common.name'),
+            filterField: 'name',
+        })
+        .connectToRoute(this.route);
+
+    readonly sorts = this.dataTableService
+        .createSortCollection<TaxCategorySortParameter>()
+        .defaultSort('createdAt', 'DESC')
+        .addSort({ name: 'createdAt' })
+        .addSort({ name: 'updatedAt' })
+        .addSort({ name: 'name' })
+        .connectToRoute(this.route);
 
     constructor(
         private dataService: DataService,
         private notificationService: NotificationService,
         private modalService: ModalService,
+        route: ActivatedRoute,
+        router: Router,
+        private dataTableService: DataTableService,
     ) {
-        this.queryResult = this.dataService.settings.getTaxCategories();
-        this.taxCategories$ = this.queryResult.mapStream(data => data.taxCategories);
+        super(router, route);
+        super.setQueryFn(
+            (...args: any[]) => this.dataService.settings.getTaxCategories(...args),
+            data => data.taxCategories,
+            (skip, take) => ({
+                options: {
+                    skip,
+                    take,
+                    filter: {
+                        name: {
+                            contains: this.searchTermControl.value,
+                        },
+                        ...this.filters.createFilterInput(),
+                    },
+                    sort: this.sorts.createSortInput(),
+                },
+            }),
+        );
+    }
+
+    ngOnInit() {
+        super.ngOnInit();
+        const searchTerm$ = this.searchTermControl.valueChanges.pipe(
+            filter(value => value != null && (2 <= value.length || value.length === 0)),
+            debounceTime(250),
+        );
+        merge(searchTerm$, this.filters.valueChanges, this.sorts.valueChanges)
+            .pipe(takeUntil(this.destroy$))
+            .subscribe(() => this.refresh());
     }
 
     deleteTaxCategory(taxCategory: TaxCategoryFragment) {
@@ -51,7 +126,7 @@ export class TaxCategoryListComponent {
                         this.notificationService.success(_('common.notify-delete-success'), {
                             entity: 'TaxRate',
                         });
-                        this.queryResult.ref.refetch();
+                        this.refresh();
                     } else {
                         this.notificationService.error(res.message || _('common.notify-delete-error'), {
                             entity: 'TaxRate',

+ 1 - 1
packages/admin-ui/src/lib/settings/src/components/tax-rate-detail/tax-rate-detail.component.ts

@@ -66,7 +66,7 @@ export class TaxRateDetailComponent
         this.init();
         this.taxCategories$ = this.dataService.settings
             .getTaxCategories()
-            .mapSingle(data => data.taxCategories);
+            .mapSingle(data => data.taxCategories.items);
         this.zones$ = this.dataService.settings.getZones().mapSingle(data => data.zones);
     }
 

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

@@ -1,58 +1,81 @@
-<vdr-action-bar>
-    <vdr-ab-right>
+<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']">
+        <a
+            class="btn btn-primary"
+            [routerLink]="['./create']"
+            *vdrIfPermissions="['CreateSettings', 'CreateTaxRate']"
+        >
             <clr-icon shape="plus"></clr-icon>
             {{ 'settings.create-new-tax-rate' | translate }}
         </a>
-    </vdr-ab-right>
-</vdr-action-bar>
-
-<vdr-data-table
-    [items]="items$ | async"
-    [itemsPerPage]="itemsPerPage$ | async"
-    [totalItems]="totalItems$ | async"
-    [currentPage]="currentPage$ | async"
-    (pageChange)="setPageNumber($event)"
-    (itemsPerPageChange)="setItemsPerPage($event)"
->
-    <vdr-dt-column>{{ 'common.name' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'settings.tax-category' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'settings.zone' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'settings.tax-rate' | translate }}</vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <ng-template let-taxRate="item">
-        <td class="left align-middle">{{ taxRate.name }}</td>
-        <td class="left align-middle">{{ taxRate.category.name }}</td>
-        <td class="left align-middle">{{ taxRate.zone.name }}</td>
-        <td class="left align-middle">{{ taxRate.value }}%</td>
-        <td class="right align-middle">
-            <vdr-table-row-action
-                iconShape="edit"
-                [label]="'common.edit' | translate"
-                [linkTo]="['./', taxRate.id]"
-            ></vdr-table-row-action>
-        </td>
-        <td class="right align-middle">
-            <vdr-dropdown>
-                <button type="button" class="btn btn-link btn-sm" vdrDropdownTrigger>
-                    {{ 'common.actions' | translate }}
-                    <clr-icon shape="caret down"></clr-icon>
-                </button>
-                <vdr-dropdown-menu vdrPosition="bottom-right">
-                    <button
-                        type="button"
-                        class="delete-button"
-                        (click)="deleteTaxRate(taxRate)"
-                        [disabled]="!(['DeleteSettings', 'DeleteTaxRate'] | hasPermission)"
-                        vdrDropdownItem
-                    >
-                        <clr-icon shape="trash" class="is-danger"></clr-icon>
-                        {{ 'common.delete' | translate }}
-                    </button>
-                </vdr-dropdown-menu>
-            </vdr-dropdown>
-        </td>
-    </ng-template>
-</vdr-data-table>
+    </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-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.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">
+                <div class="badge success" *ngIf="taxRate.enabled">{{ 'common.enabled' }}</div>
+                <div class="badge warning" *ngIf="!taxRate.enabled">{{ 'common.disabled' }}</div>
+            </ng-template>
+        </vdr-dt2-column>
+    </vdr-data-table-2>
+</vdr-page-body>

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

@@ -1,17 +1,23 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { FormControl } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
     BaseListComponent,
     DataService,
+    DataTableService,
     DeletionResult,
+    GetFacetListQuery,
     GetTaxRateListQuery,
     ItemOf,
     ModalService,
     NotificationService,
+    SelectionManager,
+    TaxRateFilterParameter,
+    TaxRateSortParameter,
 } from '@vendure/admin-ui/core';
-import { EMPTY } from 'rxjs';
-import { map, switchMap } from 'rxjs/operators';
+import { EMPTY, merge } from 'rxjs';
+import { debounceTime, filter, map, switchMap, takeUntil } from 'rxjs/operators';
 
 @Component({
     selector: 'vdr-tax-rate-list',
@@ -19,22 +25,97 @@ import { map, switchMap } from 'rxjs/operators';
     styleUrls: ['./tax-rate-list.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class TaxRateListComponent extends BaseListComponent<
-    GetTaxRateListQuery,
-    ItemOf<GetTaxRateListQuery, 'taxRates'>
-> {
+export class TaxRateListComponent
+    extends BaseListComponent<GetTaxRateListQuery, ItemOf<GetTaxRateListQuery, 'taxRates'>>
+    implements OnInit
+{
+    searchTermControl = new FormControl('');
+    selectionManager = new SelectionManager<ItemOf<GetFacetListQuery, 'facets'>>({
+        multiSelect: true,
+        itemsAreEqual: (a, b) => a.id === b.id,
+        additiveMode: true,
+    });
+
+    readonly filters = this.dataTableService
+        .createFilterCollection<TaxRateFilterParameter>()
+        .addFilter({
+            name: 'createdAt',
+            type: { kind: 'dateRange' },
+            label: _('common.created-at'),
+            filterField: 'createdAt',
+        })
+        .addFilter({
+            name: 'updatedAt',
+            type: { kind: 'dateRange' },
+            label: _('common.updated-at'),
+            filterField: 'updatedAt',
+        })
+        .addFilter({
+            name: 'name',
+            type: { kind: 'text' },
+            label: _('common.name'),
+            filterField: 'name',
+        })
+        .addFilter({
+            name: 'enabled',
+            type: { kind: 'boolean' },
+            label: _('common.enabled'),
+            filterField: 'enabled',
+        })
+        .addFilter({
+            name: 'value',
+            type: { kind: 'number' },
+            label: _('common.value'),
+            filterField: 'value',
+        })
+        .connectToRoute(this.route);
+
+    readonly sorts = this.dataTableService
+        .createSortCollection<TaxRateSortParameter>()
+        .defaultSort('createdAt', 'DESC')
+        .addSort({ name: 'createdAt' })
+        .addSort({ name: 'updatedAt' })
+        .addSort({ name: 'name' })
+        .addSort({ name: 'value' })
+        .connectToRoute(this.route);
+
     constructor(
         private modalService: ModalService,
         private notificationService: NotificationService,
         private dataService: DataService,
         router: Router,
         route: ActivatedRoute,
+        private dataTableService: DataTableService,
     ) {
         super(router, route);
         super.setQueryFn(
             (...args: any[]) => this.dataService.settings.getTaxRates(...args),
             data => data.taxRates,
+            (skip, take) => ({
+                options: {
+                    skip,
+                    take,
+                    filter: {
+                        name: {
+                            contains: this.searchTermControl.value,
+                        },
+                        ...this.filters.createFilterInput(),
+                    },
+                    sort: this.sorts.createSortInput(),
+                },
+            }),
+        );
+    }
+
+    ngOnInit() {
+        super.ngOnInit();
+        const searchTerm$ = this.searchTermControl.valueChanges.pipe(
+            filter(value => value != null && (2 <= value.length || value.length === 0)),
+            debounceTime(250),
         );
+        merge(searchTerm$, this.filters.valueChanges, this.sorts.valueChanges)
+            .pipe(takeUntil(this.destroy$))
+            .subscribe(() => this.refresh());
     }
 
     deleteTaxRate(taxRate: ItemOf<GetTaxRateListQuery, 'taxRates'>) {

+ 8 - 3
packages/admin-ui/src/lib/static/i18n-messages/cs.json

@@ -71,6 +71,7 @@
     "auto-update-product-variant-name": "Automaticky aktualizovat jména variant",
     "channel-price-preview": "Náhled ceny v kanálu",
     "collection-contents": "Obsah kolekce",
+    "collection-slug": "",
     "confirm-bulk-delete-collections": "",
     "confirm-bulk-delete-facets": "",
     "confirm-bulk-delete-products": "",
@@ -121,6 +122,7 @@
     "move-down": "Posunout dolů",
     "move-to": "Posunout",
     "move-up": "Posunout nahoru",
+    "name": "",
     "no-channel-selected": "Žádný kanál nevybrán",
     "no-featured-asset": "Žádné zvýrazněné médium",
     "no-selection": "Žádný výběr",
@@ -238,6 +240,7 @@
     "force-remove": "",
     "general": "",
     "guest": "Host",
+    "image": "",
     "items-per-page-option": "{ count } na stránku",
     "items-selected-count": "",
     "keep-editing": "",
@@ -273,6 +276,8 @@
     "open": "Otevřít",
     "operator-contains": "",
     "operator-eq": "",
+    "operator-gt": "",
+    "operator-lt": "",
     "operator-not-contains": "",
     "operator-not-eq": "",
     "operator-notContains": "",
@@ -299,6 +304,7 @@
     "set-language": "",
     "short-date": "",
     "start-date": "",
+    "status": "",
     "tags": "",
     "theme": "Motiv",
     "there-are-unsaved-changes": "Provedené změny nebyly uloženy. Přechod na jinou stránku způsobí ztrátu těchto změn.",
@@ -309,6 +315,7 @@
     "username": "Uživatelské jméno",
     "view-next-month": "Další měsíc",
     "view-previous-month": "Předchozí měsíc",
+    "visibility": "",
     "with-selected": "S vybranými..."
   },
   "customer": {
@@ -330,7 +337,6 @@
     "create-new-customer-group": "Create new customer group",
     "customer-groups": "Skupiny zákazníka",
     "customer-history": "Historie zákazníka",
-    "customer-type": "Typ zákazníka",
     "default-billing-address": "Výchozí fakturační",
     "default-shipping-address": "Výchozí dodací",
     "email-address": "E-mailová adresa",
@@ -453,8 +459,7 @@
     "create-new-promotion": "Nová propagace",
     "ends-at": "Končí",
     "per-customer-limit": "Limit za zákazníka",
-    "search-by-coupon-code": "",
-    "search-by-name": "",
+    "search-by-name-or-coupon-code": "",
     "starts-at": "Začíná"
   },
   "nav": {

+ 8 - 3
packages/admin-ui/src/lib/static/i18n-messages/de.json

@@ -71,6 +71,7 @@
     "auto-update-product-variant-name": "Automatisch Namen der Produktvariante aktualisieren",
     "channel-price-preview": "Kanal-Preisvorschau",
     "collection-contents": "Inhalt der Sammlung",
+    "collection-slug": "",
     "confirm-bulk-delete-collections": "",
     "confirm-bulk-delete-facets": "",
     "confirm-bulk-delete-products": "",
@@ -121,6 +122,7 @@
     "move-down": "Nach unten bewegen",
     "move-to": "Verschieben nach",
     "move-up": "Nach oben bewegen",
+    "name": "",
     "no-channel-selected": "Kein Kanal ausgewählt",
     "no-featured-asset": "Kein \"Featured Asset\"",
     "no-selection": "Keine Auswahl",
@@ -238,6 +240,7 @@
     "force-remove": "",
     "general": "",
     "guest": "Gast",
+    "image": "",
     "items-per-page-option": "{ count } pro Seite",
     "items-selected-count": "",
     "keep-editing": "",
@@ -273,6 +276,8 @@
     "open": "Öffnen",
     "operator-contains": "",
     "operator-eq": "",
+    "operator-gt": "",
+    "operator-lt": "",
     "operator-not-contains": "",
     "operator-not-eq": "",
     "operator-notContains": "",
@@ -299,6 +304,7 @@
     "set-language": "",
     "short-date": "",
     "start-date": "",
+    "status": "",
     "tags": "Tags",
     "theme": "Theme",
     "there-are-unsaved-changes": "Es gibt ungespeicherte Änderungen. Wenn Sie wechseln, gehen diese Änderungen verloren.",
@@ -309,6 +315,7 @@
     "username": "Benutzername",
     "view-next-month": "Nächsten Monat anzeigen",
     "view-previous-month": "Vorherigen Monat anzeigen",
+    "visibility": "",
     "with-selected": "Auswahl..."
   },
   "customer": {
@@ -330,7 +337,6 @@
     "create-new-customer-group": "Neue Kundengruppe anlegen",
     "customer-groups": "Kundengruppen",
     "customer-history": "Kundenhistorie",
-    "customer-type": "Kundentyp",
     "default-billing-address": "Standardrechnungsadresse",
     "default-shipping-address": "Standardversand",
     "email-address": "E-Mail-Adresse",
@@ -453,8 +459,7 @@
     "create-new-promotion": "Neue Werbeaktion erstellen",
     "ends-at": "Endet am",
     "per-customer-limit": "Begrenzung pro Kunde",
-    "search-by-coupon-code": "",
-    "search-by-name": "",
+    "search-by-name-or-coupon-code": "",
     "starts-at": "Beginnt am"
   },
   "nav": {

+ 9 - 4
packages/admin-ui/src/lib/static/i18n-messages/en.json

@@ -71,6 +71,7 @@
     "auto-update-product-variant-name": "Automatically update the names of ProductVariants",
     "channel-price-preview": "Channel price preview",
     "collection-contents": "Collection contents",
+    "collection-slug": "Collection slug",
     "confirm-bulk-delete-collections": "Delete {count} collections?",
     "confirm-bulk-delete-facets": "Delete {count} facets?",
     "confirm-bulk-delete-products": "Delete {count} products?",
@@ -121,6 +122,7 @@
     "move-down": "Move down",
     "move-to": "Move to",
     "move-up": "Move up",
+    "name": "Name",
     "no-channel-selected": "No channel selected",
     "no-featured-asset": "No featured asset",
     "no-selection": "No selection",
@@ -238,6 +240,7 @@
     "force-remove": "Force remove",
     "general": "General",
     "guest": "Guest",
+    "image": "Image",
     "items-per-page-option": "{ count } per page",
     "items-selected-count": "{ count } {count, plural, one {item} other {items}} selected",
     "keep-editing": "Keep editing",
@@ -273,6 +276,8 @@
     "open": "Open",
     "operator-contains": "contains",
     "operator-eq": "equals",
+    "operator-gt": "greater than",
+    "operator-lt": "less than",
     "operator-not-contains": "does not contain",
     "operator-not-eq": "does not equal",
     "operator-notContains": "does not contain",
@@ -299,6 +304,7 @@
     "set-language": "Set language",
     "short-date": "Short date",
     "start-date": "Start date",
+    "status": "Status",
     "tags": "Tags",
     "theme": "Theme",
     "there-are-unsaved-changes": "There are unsaved changes. Navigating away will cause these changes to be lost.",
@@ -309,6 +315,7 @@
     "username": "Username",
     "view-next-month": "View next month",
     "view-previous-month": "View previous month",
+    "visibility": "Visibility",
     "with-selected": "With {count} selected..."
   },
   "customer": {
@@ -330,7 +337,6 @@
     "create-new-customer-group": "Create new customer group",
     "customer-groups": "Customer groups",
     "customer-history": "Customer history",
-    "customer-type": "Customer type",
     "default-billing-address": "Default billing",
     "default-shipping-address": "Default shipping",
     "email-address": "Email address",
@@ -453,8 +459,7 @@
     "create-new-promotion": "Create new promotion",
     "ends-at": "Ends at",
     "per-customer-limit": "Per-customer limit",
-    "search-by-coupon-code": "Search by coupon code",
-    "search-by-name": "Search by name",
+    "search-by-name-or-coupon-code": "Search by name or coupon code",
     "starts-at": "Starts at"
   },
   "nav": {
@@ -736,4 +741,4 @@
     "job-result": "Job result",
     "job-state": "Job state"
   }
-}
+}

+ 8 - 3
packages/admin-ui/src/lib/static/i18n-messages/es.json

@@ -71,6 +71,7 @@
     "auto-update-product-variant-name": "Actualiza los nombres de las variantes de producto automáticamente",
     "channel-price-preview": "Vista previa de precio para el canal de ventas",
     "collection-contents": "Contenidos de la colección",
+    "collection-slug": "",
     "confirm-bulk-delete-collections": "",
     "confirm-bulk-delete-facets": "",
     "confirm-bulk-delete-products": "",
@@ -121,6 +122,7 @@
     "move-down": "Mover abajo",
     "move-to": "Mover a",
     "move-up": "Mover arriba",
+    "name": "",
     "no-channel-selected": "Ninún canal seleccionado",
     "no-featured-asset": "Sin recurso destacado",
     "no-selection": "Sin selección",
@@ -238,6 +240,7 @@
     "force-remove": "",
     "general": "",
     "guest": "Invitado",
+    "image": "",
     "items-per-page-option": "{ count } por página",
     "items-selected-count": "",
     "keep-editing": "",
@@ -273,6 +276,8 @@
     "open": "Abrir",
     "operator-contains": "",
     "operator-eq": "",
+    "operator-gt": "",
+    "operator-lt": "",
     "operator-not-contains": "",
     "operator-not-eq": "",
     "operator-notContains": "",
@@ -299,6 +304,7 @@
     "set-language": "",
     "short-date": "",
     "start-date": "",
+    "status": "",
     "tags": "Etiquetas",
     "theme": "Tema",
     "there-are-unsaved-changes": "Hay cambios sin guardar. Si sale de este sitio sus cambios se perderán.",
@@ -309,6 +315,7 @@
     "username": "Nombre de usuario",
     "view-next-month": "Ver próximo mes",
     "view-previous-month": "Ver mes anterior",
+    "visibility": "",
     "with-selected": "Seleccionados"
   },
   "customer": {
@@ -330,7 +337,6 @@
     "create-new-customer-group": "Crear nuevo grupo de clientes",
     "customer-groups": "Grupos de clientes",
     "customer-history": "Historial del cliente",
-    "customer-type": "Tipo de cliente",
     "default-billing-address": "Facturación (Por defecto)",
     "default-shipping-address": "Envío (Por defecto)",
     "email-address": "Dirección de correo electrónico",
@@ -453,8 +459,7 @@
     "create-new-promotion": "Crear nueva promoción",
     "ends-at": "Finaliza el",
     "per-customer-limit": "Límite por cliente",
-    "search-by-coupon-code": "Buscar por código de descuento",
-    "search-by-name": "Buscar por nombre",
+    "search-by-name-or-coupon-code": "",
     "starts-at": "Comienza el"
   },
   "nav": {

+ 8 - 3
packages/admin-ui/src/lib/static/i18n-messages/fr.json

@@ -71,6 +71,7 @@
     "auto-update-product-variant-name": "Mettre à jour automatiquement les noms de variations du produit ",
     "channel-price-preview": "Prévisualisation du prix du canal",
     "collection-contents": "Contenu de la Collection",
+    "collection-slug": "",
     "confirm-bulk-delete-collections": "",
     "confirm-bulk-delete-facets": "",
     "confirm-bulk-delete-products": "",
@@ -121,6 +122,7 @@
     "move-down": "Déplacer vers le bas",
     "move-to": "Déplacer à",
     "move-up": "Déplacer vers le haut",
+    "name": "",
     "no-channel-selected": "Pas de canal sélectionné",
     "no-featured-asset": "Pas de fichier vedette",
     "no-selection": "Pas de sélection",
@@ -238,6 +240,7 @@
     "force-remove": "",
     "general": "",
     "guest": "Invité",
+    "image": "",
     "items-per-page-option": "{ count } par page",
     "items-selected-count": "",
     "keep-editing": "",
@@ -273,6 +276,8 @@
     "open": "Ouvert",
     "operator-contains": "",
     "operator-eq": "",
+    "operator-gt": "",
+    "operator-lt": "",
     "operator-not-contains": "",
     "operator-not-eq": "",
     "operator-notContains": "",
@@ -299,6 +304,7 @@
     "set-language": "",
     "short-date": "",
     "start-date": "",
+    "status": "",
     "tags": "Mots-clés",
     "theme": "Thème",
     "there-are-unsaved-changes": "Il y a des changements non enregistrés. Naviguer ailleurs fera perdre ces changements.",
@@ -309,6 +315,7 @@
     "username": "Nom d'utilisateur",
     "view-next-month": "Voir le mois suivant",
     "view-previous-month": "Voir le mois précédent",
+    "visibility": "",
     "with-selected": "Avec la selection..."
   },
   "customer": {
@@ -330,7 +337,6 @@
     "create-new-customer-group": "Creer un nouveau groupe de clients",
     "customer-groups": "Groupes de client",
     "customer-history": "Historique du client",
-    "customer-type": "Type de client",
     "default-billing-address": "Adresse de facturation par défaut",
     "default-shipping-address": "Adresse de livraison par défaut",
     "email-address": "Adresse email",
@@ -453,8 +459,7 @@
     "create-new-promotion": "Creer nouvelle promotion",
     "ends-at": "Termine au",
     "per-customer-limit": "Limite par client",
-    "search-by-coupon-code": "",
-    "search-by-name": "",
+    "search-by-name-or-coupon-code": "",
     "starts-at": "Débute au"
   },
   "nav": {

+ 8 - 3
packages/admin-ui/src/lib/static/i18n-messages/it.json

@@ -71,6 +71,7 @@
     "auto-update-product-variant-name": "Aggiorna automaticamente i nomi delle Varianti",
     "channel-price-preview": "Anteprima prezzo canale",
     "collection-contents": "Contenuti della Collezione",
+    "collection-slug": "",
     "confirm-bulk-delete-collections": "",
     "confirm-bulk-delete-facets": "",
     "confirm-bulk-delete-products": "",
@@ -121,6 +122,7 @@
     "move-down": "Sposta in basso",
     "move-to": "Sposta in",
     "move-up": "Sposta in alto",
+    "name": "",
     "no-channel-selected": "Nessun canale selezionato",
     "no-featured-asset": "Nessun media in evidenza",
     "no-selection": "Nessuna selezione",
@@ -238,6 +240,7 @@
     "force-remove": "",
     "general": "",
     "guest": "Ospite",
+    "image": "",
     "items-per-page-option": "{ count } per pagina",
     "items-selected-count": "",
     "keep-editing": "",
@@ -273,6 +276,8 @@
     "open": "Apri",
     "operator-contains": "",
     "operator-eq": "",
+    "operator-gt": "",
+    "operator-lt": "",
     "operator-not-contains": "",
     "operator-not-eq": "",
     "operator-notContains": "",
@@ -299,6 +304,7 @@
     "set-language": "",
     "short-date": "",
     "start-date": "",
+    "status": "",
     "tags": "Tag",
     "theme": "Tema",
     "there-are-unsaved-changes": "Ci sono modifiche non salvate. Lasciando questa pagina le modifiche andranno perse.",
@@ -309,6 +315,7 @@
     "username": "Username",
     "view-next-month": "Vedi mese successivo",
     "view-previous-month": "Vedi mese precedente",
+    "visibility": "",
     "with-selected": "Con selezionato..."
   },
   "customer": {
@@ -330,7 +337,6 @@
     "create-new-customer-group": "Crea nuovo gruppo di clienti",
     "customer-groups": "Gruppi di clienti",
     "customer-history": "Storico cliente",
-    "customer-type": "Tipologia cliente",
     "default-billing-address": "Indirizzo di fatturazione principale",
     "default-shipping-address": "Indirizzo di spedizione principale",
     "email-address": "Indirizzo email",
@@ -453,8 +459,7 @@
     "create-new-promotion": "Crea nuova promozione",
     "ends-at": "Finisce a",
     "per-customer-limit": "Limiti per cliente",
-    "search-by-coupon-code": "Cerca per codice coupon",
-    "search-by-name": "Cerca per nome",
+    "search-by-name-or-coupon-code": "",
     "starts-at": "Inizia a"
   },
   "nav": {

+ 8 - 3
packages/admin-ui/src/lib/static/i18n-messages/pl.json

@@ -71,6 +71,7 @@
     "auto-update-product-variant-name": "",
     "channel-price-preview": "Podgląd cen kanału",
     "collection-contents": "Zawartość kolekcji",
+    "collection-slug": "",
     "confirm-bulk-delete-collections": "",
     "confirm-bulk-delete-facets": "",
     "confirm-bulk-delete-products": "",
@@ -121,6 +122,7 @@
     "move-down": "Przesuń w dół",
     "move-to": "Przesuń do",
     "move-up": "Przesuń w górę",
+    "name": "",
     "no-channel-selected": "Brak zaznaczonego kanału",
     "no-featured-asset": "Brak polecanego zasobu",
     "no-selection": "Brak zaznaczenia",
@@ -238,6 +240,7 @@
     "force-remove": "",
     "general": "",
     "guest": "Gość",
+    "image": "",
     "items-per-page-option": "{ count } na stronę",
     "items-selected-count": "",
     "keep-editing": "",
@@ -273,6 +276,8 @@
     "open": "Otwórz",
     "operator-contains": "",
     "operator-eq": "",
+    "operator-gt": "",
+    "operator-lt": "",
     "operator-not-contains": "",
     "operator-not-eq": "",
     "operator-notContains": "",
@@ -299,6 +304,7 @@
     "set-language": "",
     "short-date": "",
     "start-date": "",
+    "status": "",
     "tags": "",
     "theme": "",
     "there-are-unsaved-changes": "Są nie zapisane zmiany. Nawigacja do innej lokalizacji spowoduje utrate zmian.",
@@ -309,6 +315,7 @@
     "username": "Nazwa użytkownika",
     "view-next-month": "Wyświetl następny miesiąc",
     "view-previous-month": "Wyświetl poprzedni miesiąc",
+    "visibility": "",
     "with-selected": ""
   },
   "customer": {
@@ -330,7 +337,6 @@
     "create-new-customer-group": "",
     "customer-groups": "",
     "customer-history": "",
-    "customer-type": "Typ klienta",
     "default-billing-address": "Domyślny adres rozliczeniowy",
     "default-shipping-address": "Domyślny adres wysyłki",
     "email-address": "Email",
@@ -453,8 +459,7 @@
     "create-new-promotion": "Utwórz nową promocje",
     "ends-at": "Kończy się",
     "per-customer-limit": "Limit klientów",
-    "search-by-coupon-code": "",
-    "search-by-name": "",
+    "search-by-name-or-coupon-code": "",
     "starts-at": "Zaczyna się"
   },
   "nav": {

+ 8 - 3
packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json

@@ -71,6 +71,7 @@
     "auto-update-product-variant-name": "Atualizar automaticamente os nomes das variações do produto",
     "channel-price-preview": "Visualizar preço do canal",
     "collection-contents": "Conteúdo da categoria",
+    "collection-slug": "",
     "confirm-bulk-delete-collections": "",
     "confirm-bulk-delete-facets": "",
     "confirm-bulk-delete-products": "",
@@ -121,6 +122,7 @@
     "move-down": "Mover para baixo",
     "move-to": "Mover para",
     "move-up": "Mover para cima",
+    "name": "",
     "no-channel-selected": "Nenhum canal selecionado",
     "no-featured-asset": "Nenhum recurso em destaque",
     "no-selection": "Nenhuma seleção",
@@ -238,6 +240,7 @@
     "force-remove": "",
     "general": "",
     "guest": "Convidado",
+    "image": "",
     "items-per-page-option": "{ count } por página",
     "items-selected-count": "",
     "keep-editing": "",
@@ -273,6 +276,8 @@
     "open": "Aberto",
     "operator-contains": "",
     "operator-eq": "",
+    "operator-gt": "",
+    "operator-lt": "",
     "operator-not-contains": "",
     "operator-not-eq": "",
     "operator-notContains": "",
@@ -299,6 +304,7 @@
     "set-language": "",
     "short-date": "",
     "start-date": "",
+    "status": "",
     "tags": "",
     "theme": "Tema",
     "there-are-unsaved-changes": "Há alterações não salvas. Navegar para outra página fará com que essas alterações sejam perdidas.",
@@ -309,6 +315,7 @@
     "username": "Nome do usuário",
     "view-next-month": "Visualizar próximo mês",
     "view-previous-month": "Visualizar mês anterior",
+    "visibility": "",
     "with-selected": "Com selecionado..."
   },
   "customer": {
@@ -330,7 +337,6 @@
     "create-new-customer-group": "Criar novo grupo de cliente",
     "customer-groups": "Grupos de cliente",
     "customer-history": "Histórico de cliente",
-    "customer-type": "Tipo de cliente",
     "default-billing-address": "Endereço de cobrança padrão",
     "default-shipping-address": "Endereço de entrega padrão",
     "email-address": "Email",
@@ -453,8 +459,7 @@
     "create-new-promotion": "Criar nova promoção",
     "ends-at": "Termina em",
     "per-customer-limit": "Limite por cliente",
-    "search-by-coupon-code": "",
-    "search-by-name": "",
+    "search-by-name-or-coupon-code": "",
     "starts-at": "Começa em"
   },
   "nav": {

+ 8 - 3
packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json

@@ -71,6 +71,7 @@
     "auto-update-product-variant-name": "Actualizar automaticamente os nomes das variantes do produto",
     "channel-price-preview": "Visualizar preço do canal",
     "collection-contents": "Conteúdo da categoria",
+    "collection-slug": "",
     "confirm-bulk-delete-collections": "",
     "confirm-bulk-delete-facets": "",
     "confirm-bulk-delete-products": "",
@@ -121,6 +122,7 @@
     "move-down": "Mover para baixo",
     "move-to": "Mover para",
     "move-up": "Mover para cima",
+    "name": "",
     "no-channel-selected": "Nenhum canal seleccionado",
     "no-featured-asset": "Nenhum recurso em destaque",
     "no-selection": "Nenhuma imagem seleccionada",
@@ -238,6 +240,7 @@
     "force-remove": "",
     "general": "Geral",
     "guest": "Convidado",
+    "image": "",
     "items-per-page-option": "{ count } por página",
     "items-selected-count": "",
     "keep-editing": "",
@@ -273,6 +276,8 @@
     "open": "Visualizar",
     "operator-contains": "",
     "operator-eq": "",
+    "operator-gt": "",
+    "operator-lt": "",
     "operator-not-contains": "",
     "operator-not-eq": "",
     "operator-notContains": "",
@@ -299,6 +304,7 @@
     "set-language": "Definir idioma",
     "short-date": "Data abreviada",
     "start-date": "",
+    "status": "",
     "tags": "Tags",
     "theme": "Tema",
     "there-are-unsaved-changes": "Há alterações por guardar. Navegar para outra página fará com que essas alterações sejam perdidas.",
@@ -309,6 +315,7 @@
     "username": "Nome do utilizador",
     "view-next-month": "Visualizar próximo mês",
     "view-previous-month": "Visualizar mês anterior",
+    "visibility": "",
     "with-selected": "Com seleccionado..."
   },
   "customer": {
@@ -330,7 +337,6 @@
     "create-new-customer-group": "Criar novo grupo de cliente",
     "customer-groups": "Grupos de cliente",
     "customer-history": "Histórico de cliente",
-    "customer-type": "Tipo de cliente",
     "default-billing-address": "Morada de cobrança padrão",
     "default-shipping-address": "Morada de entrega padrão",
     "email-address": "E-mail",
@@ -453,8 +459,7 @@
     "create-new-promotion": "Criar nova promoção",
     "ends-at": "Válido até",
     "per-customer-limit": "Limite por cliente",
-    "search-by-coupon-code": "Pesquisar pelo código",
-    "search-by-name": "Pesquisar pelo nome",
+    "search-by-name-or-coupon-code": "",
     "starts-at": "Válido a partir"
   },
   "nav": {

+ 8 - 3
packages/admin-ui/src/lib/static/i18n-messages/ru.json

@@ -71,6 +71,7 @@
     "auto-update-product-variant-name": "Автоматически обновлять названия вариантов товара",
     "channel-price-preview": "Предварительный просмотр цен канала",
     "collection-contents": "Содержание коллекции",
+    "collection-slug": "",
     "confirm-bulk-delete-collections": "",
     "confirm-bulk-delete-facets": "",
     "confirm-bulk-delete-products": "",
@@ -121,6 +122,7 @@
     "move-down": "Двигать вниз",
     "move-to": "Двигать к",
     "move-up": "Двигать вверх",
+    "name": "",
     "no-channel-selected": "Канал не выбран",
     "no-featured-asset": "Нет избранного медиа-объекта",
     "no-selection": "Не выбрано",
@@ -238,6 +240,7 @@
     "force-remove": "",
     "general": "",
     "guest": "Гость",
+    "image": "",
     "items-per-page-option": "{ count } на странице",
     "items-selected-count": "",
     "keep-editing": "",
@@ -273,6 +276,8 @@
     "open": "Открыть",
     "operator-contains": "",
     "operator-eq": "",
+    "operator-gt": "",
+    "operator-lt": "",
     "operator-not-contains": "",
     "operator-not-eq": "",
     "operator-notContains": "",
@@ -299,6 +304,7 @@
     "set-language": "",
     "short-date": "",
     "start-date": "",
+    "status": "",
     "tags": "Теги",
     "theme": "Тема",
     "there-are-unsaved-changes": "Есть несохраненные изменения. Если вы выйдете, эти изменения будут потеряны.",
@@ -309,6 +315,7 @@
     "username": "Имя пользователя",
     "view-next-month": "Посмотреть следующий месяц",
     "view-previous-month": "Посмотреть предыдущий месяц",
+    "visibility": "",
     "with-selected": "С выбранным..."
   },
   "customer": {
@@ -330,7 +337,6 @@
     "create-new-customer-group": "Создать новую группу клиентов",
     "customer-groups": "Группы клиентов",
     "customer-history": "История изменения клиентов",
-    "customer-type": "Тип клиента",
     "default-billing-address": "Биллинг по умолчанию",
     "default-shipping-address": "Доставка по умолчанию",
     "email-address": "Адрес электронной почты",
@@ -453,8 +459,7 @@
     "create-new-promotion": "Создать новую акцию",
     "ends-at": "Заканчивается в",
     "per-customer-limit": "Лимит на клиента",
-    "search-by-coupon-code": "Поиск по коду купона",
-    "search-by-name": "Поиск по имени",
+    "search-by-name-or-coupon-code": "",
     "starts-at": "Начинается в"
   },
   "nav": {

+ 8 - 3
packages/admin-ui/src/lib/static/i18n-messages/uk.json

@@ -71,6 +71,7 @@
     "auto-update-product-variant-name": "Автоматично оновлювати назви варіантів товару",
     "channel-price-preview": "Попередній перегляд цін каналу",
     "collection-contents": "Зміст колекції",
+    "collection-slug": "",
     "confirm-bulk-delete-collections": "",
     "confirm-bulk-delete-facets": "",
     "confirm-bulk-delete-products": "",
@@ -121,6 +122,7 @@
     "move-down": "Рухати вниз",
     "move-to": "Рухати до",
     "move-up": "Рухати вгору",
+    "name": "",
     "no-channel-selected": "Канал не вибрано",
     "no-featured-asset": "Немає обраного медіа-об'єкта",
     "no-selection": "Не вибрано",
@@ -238,6 +240,7 @@
     "force-remove": "",
     "general": "",
     "guest": "Гість",
+    "image": "",
     "items-per-page-option": "{ count } на сторінці",
     "items-selected-count": "",
     "keep-editing": "",
@@ -273,6 +276,8 @@
     "open": "Відкрити",
     "operator-contains": "",
     "operator-eq": "",
+    "operator-gt": "",
+    "operator-lt": "",
     "operator-not-contains": "",
     "operator-not-eq": "",
     "operator-notContains": "",
@@ -299,6 +304,7 @@
     "set-language": "",
     "short-date": "",
     "start-date": "",
+    "status": "",
     "tags": "Теги",
     "theme": "Тема",
     "there-are-unsaved-changes": "Є незбережені зміни. Якщо ви вийдете, ці зміни будуть втрачені.",
@@ -309,6 +315,7 @@
     "username": "Ім'я користувача",
     "view-next-month": "Переглянути наступний місяць",
     "view-previous-month": "Переглянути попередній місяць",
+    "visibility": "",
     "with-selected": "З вибраним..."
   },
   "customer": {
@@ -330,7 +337,6 @@
     "create-new-customer-group": "Створити нову групу клієнтів",
     "customer-groups": "Групи клієнтів",
     "customer-history": "Історія зміни клієнтів",
-    "customer-type": "Тип клієнта",
     "default-billing-address": "Білінг за замовчуванням",
     "default-shipping-address": "Доставка за замовчуванням",
     "email-address": "Адреса електронної пошти",
@@ -453,8 +459,7 @@
     "create-new-promotion": "Створити нову акцію",
     "ends-at": "Закінчується в",
     "per-customer-limit": "Ліміт на клієнта",
-    "search-by-coupon-code": "Пошук по коду купона",
-    "search-by-name": "Пошук по імені",
+    "search-by-name-or-coupon-code": "",
     "starts-at": "Починається в"
   },
   "nav": {

+ 8 - 3
packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json

@@ -71,6 +71,7 @@
     "auto-update-product-variant-name": "自动更新不同商品变体名称",
     "channel-price-preview": "渠道价格预览",
     "collection-contents": "系列产品",
+    "collection-slug": "",
     "confirm-bulk-delete-collections": "",
     "confirm-bulk-delete-facets": "",
     "confirm-bulk-delete-products": "",
@@ -121,6 +122,7 @@
     "move-down": "向下移",
     "move-to": "移至",
     "move-up": "向上移",
+    "name": "",
     "no-channel-selected": "未选择销售渠道",
     "no-featured-asset": "无特征图片",
     "no-selection": "尚未选择",
@@ -238,6 +240,7 @@
     "force-remove": "",
     "general": "",
     "guest": "游客",
+    "image": "",
     "items-per-page-option": "每页显示 { count } 条",
     "items-selected-count": "",
     "keep-editing": "",
@@ -273,6 +276,8 @@
     "open": "详情",
     "operator-contains": "",
     "operator-eq": "",
+    "operator-gt": "",
+    "operator-lt": "",
     "operator-not-contains": "",
     "operator-not-eq": "",
     "operator-notContains": "",
@@ -299,6 +304,7 @@
     "set-language": "",
     "short-date": "",
     "start-date": "",
+    "status": "",
     "tags": "标签",
     "theme": "主题",
     "there-are-unsaved-changes": "修改尚未被保存,现在离开会导致您的修改会被删除",
@@ -309,6 +315,7 @@
     "username": "用户名",
     "view-next-month": "查看下个月",
     "view-previous-month": "查看下个月",
+    "visibility": "",
     "with-selected": "从已选中..."
   },
   "customer": {
@@ -330,7 +337,6 @@
     "create-new-customer-group": "确认添加",
     "customer-groups": "客户分组",
     "customer-history": "客户记录",
-    "customer-type": "客户验证类型",
     "default-billing-address": "默认账单地址",
     "default-shipping-address": "默认邮寄地址",
     "email-address": "电子邮件地址",
@@ -453,8 +459,7 @@
     "create-new-promotion": "添加促销产品",
     "ends-at": "有效起始时间",
     "per-customer-limit": "每人限领数",
-    "search-by-coupon-code": "",
-    "search-by-name": "",
+    "search-by-name-or-coupon-code": "",
     "starts-at": "有效结束时间"
   },
   "nav": {

+ 8 - 3
packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json

@@ -71,6 +71,7 @@
     "auto-update-product-variant-name": "",
     "channel-price-preview": "渠道價格覽",
     "collection-contents": "系列產品",
+    "collection-slug": "",
     "confirm-bulk-delete-collections": "",
     "confirm-bulk-delete-facets": "",
     "confirm-bulk-delete-products": "",
@@ -121,6 +122,7 @@
     "move-down": "下移",
     "move-to": "移至",
     "move-up": "上移",
+    "name": "",
     "no-channel-selected": "並未選擇渠道",
     "no-featured-asset": "並無特徵圖片",
     "no-selection": "尚未選擇",
@@ -238,6 +240,7 @@
     "force-remove": "",
     "general": "",
     "guest": "游客",
+    "image": "",
     "items-per-page-option": "每页顯示 { count } 條",
     "items-selected-count": "",
     "keep-editing": "",
@@ -273,6 +276,8 @@
     "open": "詳情",
     "operator-contains": "",
     "operator-eq": "",
+    "operator-gt": "",
+    "operator-lt": "",
     "operator-not-contains": "",
     "operator-not-eq": "",
     "operator-notContains": "",
@@ -299,6 +304,7 @@
     "set-language": "",
     "short-date": "",
     "start-date": "",
+    "status": "",
     "tags": "",
     "theme": "",
     "there-are-unsaved-changes": "變更尚未被儲存,離開會失去所有變更",
@@ -309,6 +315,7 @@
     "username": "用户名",
     "view-next-month": "查看下個月",
     "view-previous-month": "查看上個月",
+    "visibility": "",
     "with-selected": ""
   },
   "customer": {
@@ -330,7 +337,6 @@
     "create-new-customer-group": "",
     "customer-groups": "",
     "customer-history": "",
-    "customer-type": "客户驗證類型",
     "default-billing-address": "默認賬單地址",
     "default-shipping-address": "默認郵寄地址",
     "email-address": "電子郵件地址",
@@ -453,8 +459,7 @@
     "create-new-promotion": "新增促销產品",
     "ends-at": "結束時間",
     "per-customer-limit": "領取上限",
-    "search-by-coupon-code": "",
-    "search-by-name": "",
+    "search-by-name-or-coupon-code": "",
     "starts-at": "開始時間"
   },
   "nav": {

+ 34 - 0
packages/admin-ui/src/lib/static/styles/global/_badges.scss

@@ -0,0 +1,34 @@
+.badge {
+    display: inline-block;
+    font-size: 12px;
+    font-weight: 600;
+    line-height: 12px;
+    text-align: center;
+    white-space: nowrap;
+    vertical-align: baseline;
+    border-radius: var(--border-radius-lg);
+    padding: calc(var(--space-unit) * 1) calc(var(--space-unit) * 1.5);
+    color: var(--color-weight-700);
+    background-color: var(--color-weight-100);
+
+    &.primary {
+        color: var(--color-primary-800);
+        background-color: var(--color-primary-125);
+    }
+    &.accent {
+        color: var(--color-accent-800);
+        background-color: var(--color-accent-125);
+    }
+    &.success {
+        color: var(--color-success-800);
+        background-color: var(--color-success-125);
+    }
+    &.warning {
+        color: var(--color-warning-800);
+        background-color: var(--color-warning-125);
+    }
+    &.error {
+        color: var(--color-error-800);
+        background-color: var(--color-error-125);
+    }
+}

+ 140 - 0
packages/admin-ui/src/lib/static/styles/global/_clarity.scss

@@ -0,0 +1,140 @@
+
+// Clarity Internal Dependencies
+@import '@clr/ui/src/utils/normalize'; // TODO: upgrade to latest normalize, once updated clr-ui can import core as is.
+
+@import '@clr/ui/src/utils/mixins';
+@import '@clr/ui/src/utils/variables/variables';
+@import '@clr/ui/src/utils/variables/properties';
+
+// Layout/Grid
+@import '@clr/ui/src/layout/grid/grid';
+
+@import '@clr/ui/src/typography/typography';
+
+// Component variables
+@import '@clr/ui/src/utils/variables.clarity';
+
+//Reboot
+@import '@clr/ui/src/utils/reboot.clarity'; // depends on variables.clarity, mixins.clarity, color.clarity, helpers.clarity
+@import '@clr/ui/src/utils/a11y';
+
+//Icons & Images
+@import '@clr/ui/src/image/icons.clarity'; // depends on variables.clarity
+@import '@clr/ui/src/image/images.clarity'; // depends on variables.clarity, mixins.clarity, icons.clarity
+
+//Popover
+@import '@clr/ui/src/popover/common/popover.clarity';
+
+// Smart Popover
+@import './popover/popover-popover.clarity';
+
+//Buttons
+//@import '@clr/ui/src/button/buttons.clarity'; // depends on variables.clarity, mixins.clarity, color.clarity
+//@import '@clr/ui/src/button/button-group/button-group.clarity'; // depends on variables.clarity, mixins.clarity
+//@import '@clr/ui/src/utils/close.clarity'; //depends on variables.clarity, mixins.clarity
+
+//Alerts
+//depends on variables.clarity, mixins.clarity, color.clarity, icons.clarity, buttons.clarity
+//@import '@clr/ui/src/emphasis/alert/alert.clarity';
+
+//Cards
+//depends on variables.clarity, mixins.clarity, color.clarity, helpers.clarity, list-group.clarity, buttons.clarity
+@import '@clr/ui/src/layout/card.clarity';
+
+//Dropdowns
+//depends on variables.clarity, mixins.clarity, color.clarity, helpers.clarity, layers.clarity
+@import '@clr/ui/src/popover/dropdown/dropdown.clarity';
+
+//Badges
+// depends on variables.clarity, mixins.clarity, color.clarity, helpers.clarity, typography.clarity
+//@import '@clr/ui/src/emphasis/badges.clarity';
+
+//Labels
+// depends on variables.clarity, mixins.clarity, color.clarity, helpers.clarity, typography.clarity, badges.clarity
+@import '@clr/ui/src/emphasis/labels.clarity';
+
+//Login
+//depends on variables.clarity, mixins.clarity, helpers.clarity, color.clarity, icons.clarity
+@import '@clr/ui/src/layout/login.clarity';
+
+//Layout
+//depends on variables.clarity, mixins.clarity, helpers.clarity, color.clarity, layers.clarity
+@import '@clr/ui/src/layout/main-container/layout.clarity';
+
+//Modal
+//depends on variables.clarity, mixins.clarity, helpers.clarity, color.clarity, layers.clarity
+@import '@clr/ui/src/modal/modal.clarity';
+
+//Nav
+@import '@clr/ui/src/layout/nav/header.clarity'; // depends on variables.clarity, mixins.clarity, helpers.clarity, color.clarity, layers.clarity
+@import '@clr/ui/src/layout/nav/links.clarity'; // depends on variables.clarity, mixins.clarity, helpers.clarity, color.clarity
+@import '@clr/ui/src/layout/nav/nav.clarity'; // depends on variables.clarity, mixins.clarity, helpers.clarity, color.clarity, layers.clarity
+@import '@clr/ui/src/layout/nav/subnav.clarity'; // depends on variables.clarity, mixins.clarity, helpers.clarity, color.clarity, layers.clarity
+@import '@clr/ui/src/layout/vertical-nav/vertical-nav.clarity'; // depends on variables.clarity, mixins.clarity, helpers.clarity, color.clarity
+@import '@clr/ui/src/layout/nav/responsive-nav.clarity'; // depends on variables.clarity, mixins.clarity, helpers.clarity, color.clarity, layers.clarity
+
+//Progress Bars
+//depends on variables.clarity, helpers.clarity, color.clarity, cards.clarity
+@import '@clr/ui/src/progress/progress-bars/progress-bars.clarity';
+
+//Spinners
+//depends on variables.clarity, mixins.clarity, color.clarity, helpers.clarity, icons.clarity
+@import '@clr/ui/src/progress/spinner/spinner.clarity';
+
+//Tables
+//depends on variables.clarity, mixins.clarity, helpers.clarity, typography.clarity
+@import '@clr/ui/src/data/tables.clarity';
+
+//Tooltips
+// depends on variables.clarity, mixins.clarity, color.clarity, helpers.clarity, layers.clarity
+@import '@clr/ui/src/popover/tooltip/tooltips.clarity';
+
+//Forms
+@import '@clr/ui/src/forms/styles/mixins.forms';
+@import '@clr/ui/src/forms/styles/properties.forms';
+@import '@clr/ui/src/forms/styles/containers.clarity';
+@import '@clr/ui/src/forms/styles/form.clarity';
+@import '@clr/ui/src/forms/styles/checkbox.clarity';
+@import '@clr/ui/src/forms/styles/file.clarity';
+@import '@clr/ui/src/forms/styles/input.clarity';
+@import '@clr/ui/src/forms/styles/input-group.clarity';
+@import '@clr/ui/src/forms/styles/radio.clarity';
+@import '@clr/ui/src/forms/styles/select.clarity';
+@import '@clr/ui/src/forms/styles/textarea.clarity';
+@import '@clr/ui/src/forms/styles/toggles.clarity'; // depends on variables.clarity, mixins.clarity, color.clarity, helpers.clarity
+@import '@clr/ui/src/forms/styles/range.clarity';
+@import '@clr/ui/src/forms/styles/datalist.clarity';
+@import '@clr/ui/src/forms/datepicker/datepicker.clarity';
+@import '@clr/ui/src/forms/combobox/combobox.clarity';
+
+//Stack View
+//depends on variables.clarity, mixins.clarity, color.clarity, helpers.clarity, forms.clarity
+@import '@clr/ui/src/data/stack-view/stack-view.clarity';
+
+//Tree View
+//depends on variables.clarity, mixins.clarity, helpers.clarity, forms.clarity
+@import '@clr/ui/src/data/tree-view/tree-view.clarity';
+
+//Datagrid
+//depends on variables.clarity, mixins.clarity, helpers.clarity, layers, icons.clarity, tables.clarity, forms.clarity
+@import '@clr/ui/src/data/datagrid/datagrid.clarity';
+
+//Animations
+// no dependencies on other clarity scss
+@import '@clr/ui/src/utils/animations/animations.clarity';
+
+//Tabs
+@import '@clr/ui/src/layout/tabs/tabs.clarity'; // no dependencies on other clarity scss
+
+//Wizards
+// depends on variables.clarity, mixins.clarity, helpers.clarity, color.clarity, layers.clarity
+//@import '@clr/ui/src/wizard/wizard.clarity';
+
+// Signposts
+//@import '@clr/ui/src/popover/signpost/signposts.clarity';
+
+// Stepper
+@import '@clr/ui/src/accordion/accordion.clarity';
+
+// Timeline
+//@import '@clr/ui/src/timeline/timeline.clarity';

+ 107 - 100
packages/admin-ui/src/lib/system/src/components/job-list/job-list.component.html

@@ -1,8 +1,11 @@
-<vdr-action-bar>
-    <vdr-ab-left>
-        <clr-checkbox-container>
+<vdr-page-header>
+    <vdr-page-title><vdr-action-bar-items locationId="job-list"></vdr-action-bar-items></vdr-page-title>
+</vdr-page-header>
+<vdr-page-body>
+    <div class="flex ml-4 mt-2">
+        <clr-checkbox-container class="mr-4">
             <clr-checkbox-wrapper>
-                <input type="checkbox" clrCheckbox [formControl]="liveUpdate" name="live-update"/>
+                <input type="checkbox" clrCheckbox [formControl]="liveUpdate" name="live-update" />
                 <label>{{ 'common.live-update' | translate }}</label>
             </clr-checkbox-wrapper>
             <clr-checkbox-wrapper>
@@ -16,8 +19,6 @@
                 <label>{{ 'system.hide-settled-jobs' | translate }}</label>
             </clr-checkbox-wrapper>
         </clr-checkbox-container>
-    </vdr-ab-left>
-    <vdr-ab-right>
         <ng-select
             [addTag]="false"
             [items]="queues$ | async"
@@ -39,100 +40,106 @@
                 </ng-template>
             </ng-template>
         </ng-select>
-        <vdr-action-bar-items locationId="job-list"></vdr-action-bar-items>
-    </vdr-ab-right>
-</vdr-action-bar>
-
-<vdr-data-table
-    [items]="items$ | async"
-    [itemsPerPage]="itemsPerPage$ | async"
-    [totalItems]="totalItems$ | async"
-    [currentPage]="currentPage$ | async"
-    (pageChange)="setPageNumber($event)"
-    (itemsPerPageChange)="setItemsPerPage($event)"
->
-    <vdr-dt-column></vdr-dt-column>
-    <vdr-dt-column>{{ 'system.job-queue-name' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'common.created-at' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'system.job-state' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'system.job-duration' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'system.job-result' | translate }}</vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <ng-template let-job="item">
-        <td class="left align-middle">
-            <vdr-entity-info [entity]="job"></vdr-entity-info>
-        </td>
-        <td class="left align-middle">
-            <vdr-dropdown *ngIf="job.data">
-                <button
-                    class="btn btn-link btn-icon"
-                    vdrDropdownTrigger
-                    [title]="'system.job-data' | translate"
-                >
-                    <clr-icon shape="details"></clr-icon>
-                </button>
-                <vdr-dropdown-menu>
-                    <div class="result-detail">
-                        <vdr-object-tree [value]="job.data"></vdr-object-tree>
-                    </div>
-                </vdr-dropdown-menu>
-            </vdr-dropdown>
-            <vdr-chip [colorFrom]="job.queueName">{{ job.queueName }}</vdr-chip>
-        </td>
-
-        <td class="left align-middle">{{ job.createdAt | timeAgo }}</td>
-        <td class="left align-middle">
-            <vdr-job-state-label [job]="job"></vdr-job-state-label>
-            <div *ngIf="job.state === 'FAILED'" class="retry-info">
-                after {{ job.attempts }} attempts
-            </div>
-            <div *ngIf="job.state === 'RUNNING' || job.state === 'RETRYING'"  class="retry-info">
-                attempting {{ job.attempts + 1 }} of {{ job.retries }}
-            </div>
-        </td>
-        <td class="left align-middle">{{ job.duration | duration }}</td>
-        <td class="left align-middle">
-            <vdr-dropdown *ngIf="hasResult(job)">
-                <button class="btn btn-link btn-sm details-button" vdrDropdownTrigger>
-                    <clr-icon shape="details"></clr-icon>
-                    {{ 'system.job-result' | translate }}
-                </button>
-                <vdr-dropdown-menu>
-                    <div class="result-detail">
-                        <vdr-object-tree [value]="job.result"></vdr-object-tree>
-                    </div>
-                </vdr-dropdown-menu>
-            </vdr-dropdown>
-            <vdr-dropdown *ngIf="job.error">
-                <button class="btn btn-link btn-sm details-button" vdrDropdownTrigger>
-                    <clr-icon shape="exclamation-circle"></clr-icon>
-                    {{ 'system.job-error' | translate }}
-                </button>
-                <vdr-dropdown-menu>
-                    <div class="result-detail">
-                        {{ job.error }}
-                    </div>
-                </vdr-dropdown-menu>
-            </vdr-dropdown>
-        </td>
-        <td class="right align-middle">
-            <vdr-dropdown *ngIf="!job.isSettled && job.state !== 'FAILED'">
-                <button class="icon-button" vdrDropdownTrigger>
-                    <clr-icon shape="ellipsis-vertical"></clr-icon>
-                </button>
-                <vdr-dropdown-menu vdrPosition="bottom-right">
+    </div>
+    <vdr-data-table-2
+        class="mt-2"
+        id="job-list"
+        [items]="items$ | async"
+        [itemsPerPage]="itemsPerPage$ | async"
+        [totalItems]="totalItems$ | async"
+        [currentPage]="currentPage$ | async"
+        (pageChange)="setPageNumber($event)"
+        (itemsPerPageChange)="setItemsPerPage($event)"
+    >
+        <vdr-dt2-column [heading]="'common.id' | translate">
+            <ng-template let-job="item">
+                {{ job.id }}
+            </ng-template>
+        </vdr-dt2-column>
+        <vdr-dt2-column [heading]="'common.created-at' | translate">
+            <ng-template let-job="item">
+                {{ job.createdAt | timeAgo }}
+            </ng-template>
+        </vdr-dt2-column>
+        <vdr-dt2-column [heading]="'system.job-queue-name' | translate" [optional]="false">
+            <ng-template let-job="item">
+                <vdr-chip [colorFrom]="job.queueName">{{ job.queueName }}</vdr-chip>
+            </ng-template>
+        </vdr-dt2-column>
+        <vdr-dt2-column [heading]="'system.job-data' | translate" [optional]="false">
+            <ng-template let-job="item">
+                <vdr-dropdown *ngIf="job.data">
                     <button
-                        type="button"
-                        class="delete-button"
-                        (click)="cancelJob(job.id)"
-                        [disabled]="!(['DeleteSettings', 'DeleteSystem'] | hasPermission)"
-                        vdrDropdownItem
+                        class="btn btn-link btn-icon"
+                        vdrDropdownTrigger
+                        [title]="'system.job-data' | translate"
                     >
-                        <clr-icon shape="ban" class="is-danger"></clr-icon>
-                        {{ 'common.cancel' | translate }}
+                        <clr-icon shape="details"></clr-icon>
+                    </button>
+                    <vdr-dropdown-menu>
+                        <div class="result-detail">
+                            <vdr-object-tree [value]="job.data"></vdr-object-tree>
+                        </div>
+                    </vdr-dropdown-menu>
+                </vdr-dropdown>
+            </ng-template>
+        </vdr-dt2-column>
+        <vdr-dt2-column [heading]="'system.job-state' | translate">
+            <ng-template let-job="item">
+                <vdr-job-state-label [job]="job"></vdr-job-state-label>
+                <div *ngIf="job.state === 'FAILED'" class="retry-info">after {{ job.attempts }} attempts</div>
+                <div *ngIf="job.state === 'RUNNING' || job.state === 'RETRYING'" class="retry-info">
+                    attempting {{ job.attempts + 1 }} of {{ job.retries }}
+                </div>
+            </ng-template>
+        </vdr-dt2-column>
+        <vdr-dt2-column [heading]="'system.job-duration' | translate">
+            <ng-template let-job="item">
+                {{ job.duration | duration }}
+            </ng-template>
+        </vdr-dt2-column>
+        <vdr-dt2-column [heading]="'system.job-result' | translate">
+            <ng-template let-job="item">
+                <vdr-dropdown *ngIf="hasResult(job)">
+                    <button class="btn btn-link btn-sm details-button" vdrDropdownTrigger>
+                        <clr-icon shape="details"></clr-icon>
+                        {{ 'system.job-result' | translate }}
+                    </button>
+                    <vdr-dropdown-menu>
+                        <div class="result-detail">
+                            <vdr-object-tree [value]="job.result"></vdr-object-tree>
+                        </div>
+                    </vdr-dropdown-menu>
+                </vdr-dropdown>
+                <vdr-dropdown *ngIf="job.error">
+                    <button class="btn btn-link btn-sm details-button" vdrDropdownTrigger>
+                        <clr-icon shape="exclamation-circle"></clr-icon>
+                        {{ 'system.job-error' | translate }}
                     </button>
-                </vdr-dropdown-menu>
-            </vdr-dropdown>
-        </td>
-    </ng-template>
-</vdr-data-table>
+                    <vdr-dropdown-menu>
+                        <div class="result-detail">
+                            {{ job.error }}
+                        </div>
+                    </vdr-dropdown-menu>
+                </vdr-dropdown>
+                <vdr-dropdown *ngIf="!job.isSettled && job.state !== 'FAILED'">
+                    <button class="icon-button" vdrDropdownTrigger>
+                        <clr-icon shape="ellipsis-vertical"></clr-icon>
+                    </button>
+                    <vdr-dropdown-menu vdrPosition="bottom-right">
+                        <button
+                            type="button"
+                            class="delete-button"
+                            (click)="cancelJob(job.id)"
+                            [disabled]="!(['DeleteSettings', 'DeleteSystem'] | hasPermission)"
+                            vdrDropdownItem
+                        >
+                            <clr-icon shape="ban" class="is-danger"></clr-icon>
+                            {{ 'common.cancel' | translate }}
+                        </button>
+                    </vdr-dropdown-menu>
+                </vdr-dropdown>
+            </ng-template>
+        </vdr-dt2-column>
+    </vdr-data-table-2>
+</vdr-page-body>

+ 1 - 3
packages/admin-ui/src/lib/system/src/components/job-list/job-list.component.ts

@@ -74,9 +74,7 @@ export class JobListComponent
         this.queues$ = this.dataService.settings
             .getJobQueues()
             .mapStream(res => res.jobQueues)
-            .pipe(
-                map(queues => [{ name: 'all', running: true }, ...queues]),
-            );
+            .pipe(map(queues => [{ name: 'all', running: true }, ...queues]));
     }
 
     hasResult(job: ItemOf<GetAllJobsQuery, 'jobs'>): boolean {