Browse Source

feat(admin-ui): Add sorting to data table component

Michael Bromley 2 years ago
parent
commit
c31a4066ab
20 changed files with 419 additions and 204 deletions
  1. 82 102
      packages/admin-ui/src/lib/catalog/src/components/facet-list/facet-list.component.html
  2. 5 5
      packages/admin-ui/src/lib/catalog/src/components/facet-list/facet-list.component.scss
  3. 48 12
      packages/admin-ui/src/lib/catalog/src/components/facet-list/facet-list.component.ts
  4. 0 1
      packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.html
  5. 4 4
      packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.ts
  6. 9 19
      packages/admin-ui/src/lib/core/src/providers/data-table/data-table-filter-collection.ts
  7. 3 3
      packages/admin-ui/src/lib/core/src/providers/data-table/data-table-filter.ts
  8. 89 0
      packages/admin-ui/src/lib/core/src/providers/data-table/data-table-sort-collection.ts
  9. 46 0
      packages/admin-ui/src/lib/core/src/providers/data-table/data-table-sort.ts
  10. 7 2
      packages/admin-ui/src/lib/core/src/providers/data-table/data-table.service.ts
  11. 6 0
      packages/admin-ui/src/lib/core/src/shared/components/bulk-action-menu/bulk-action-menu.component.ts
  12. 2 0
      packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-column.component.ts
  13. 26 18
      packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table2.component.html
  14. 24 0
      packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table2.component.scss
  15. 13 7
      packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table2.component.ts
  16. 1 1
      packages/admin-ui/src/lib/core/src/shared/components/data-table-filter-label/data-table-filter-label.component.ts
  17. 3 6
      packages/admin-ui/src/lib/core/src/shared/components/data-table-filters/data-table-filters.component.ts
  18. 4 4
      packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.html
  19. 46 20
      packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.ts
  20. 1 0
      packages/admin-ui/src/lib/static/styles/styles.scss

+ 82 - 102
packages/admin-ui/src/lib/catalog/src/components/facet-list/facet-list.component.html

@@ -1,23 +1,5 @@
-<vdr-action-bar>
-    <vdr-ab-left>
-        <div class="">
-            <input
-                type="text"
-                name="searchTerm"
-                [formControl]="filterTermControl"
-                [placeholder]="'catalog.filter-by-name' | translate"
-                class="clr-input search-input"
-            />
-            <div>
-                <vdr-language-selector
-                    [availableLanguageCodes]="availableLanguages$ | async"
-                    [currentLanguageCode]="contentLanguage$ | async"
-                    (languageCodeChange)="setLanguage($event)"
-                ></vdr-language-selector>
-            </div>
-        </div>
-    </vdr-ab-left>
-    <vdr-ab-right>
+<vdr-page-header>
+    <vdr-page-title>
         <vdr-action-bar-items locationId="facet-list"></vdr-action-bar-items>
         <a
             class="btn btn-primary"
@@ -27,87 +9,85 @@
             <clr-icon shape="plus"></clr-icon>
             {{ 'catalog.create-new-facet' | 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)"
-    [selectionManager]="selectionManager"
->
-    <vdr-bulk-action-menu
-        locationId="facet-list"
-        [hostComponent]="this"
-        [selectionManager]="selectionManager"
-    ></vdr-bulk-action-menu>
-    <vdr-dt-column>{{ 'common.code' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'common.name' | translate }}</vdr-dt-column>
-    <vdr-dt-column [expand]="true">{{ 'catalog.values' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'catalog.visibility' | translate }}</vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <vdr-dt-column></vdr-dt-column>
-    <ng-template let-facet="item">
-        <td class="left align-middle" [class.private]="facet.isPrivate">{{ facet.code }}</td>
-        <td class="left align-middle" [class.private]="facet.isPrivate">{{ facet.name }}</td>
-        <td class="left align-middle" [class.private]="facet.isPrivate">
-            <vdr-facet-value-chip
-                *ngFor="let value of facet.values | slice: 0:displayLimit[facet.id] || 3"
-                [facetValue]="value"
-                [removable]="false"
-                [displayFacetName]="false"
-            ></vdr-facet-value-chip>
-            <button
-                class="btn btn-sm btn-secondary btn-icon"
-                *ngIf="facet.values.length > initialLimit"
-                (click)="toggleDisplayLimit(facet)"
+    </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-page-body>
+        <vdr-data-table-2
+            class="mt-2"
+            id="facet-list"
+            [items]="items$ | async"
+            [itemsPerPage]="itemsPerPage$ | async"
+            [totalItems]="totalItems$ | async"
+            [currentPage]="currentPage$ | async"
+            [filters]="filters"
+            (pageChange)="setPageNumber($event)"
+            (itemsPerPageChange)="setItemsPerPage($event)"
+        >
+            <vdr-bulk-action-menu
+                locationId="facet-list"
+                [hostComponent]="this"
+                [selectionManager]="selectionManager"
+            ></vdr-bulk-action-menu>
+            <vdr-dt2-search
+                [searchTermControl]="searchTermControl"
+                [searchTermPlaceholder]="'catalog.filter-by-name' | translate"
+            ></vdr-dt2-search>
+            <vdr-dt2-column
+                [heading]="'common.name' | translate"
+                [optional]="false"
+                [sort]="sorts.get('name')"
             >
-                <ng-container *ngIf="(displayLimit[facet.id] || 0) < facet.values.length; else collapse">
-                    <clr-icon shape="plus"></clr-icon>
-                    {{ facet.values.length - initialLimit }}
-                </ng-container>
-                <ng-template #collapse>
-                    <clr-icon shape="minus"></clr-icon>
+                <ng-template let-facet="item">
+                    <a class="button-ghost" [routerLink]="['./', facet.id]"
+                        ><span>{{ facet.name }}</span>
+                        <clr-icon shape="arrow right"></clr-icon>
+                    </a>
+                </ng-template>
+            </vdr-dt2-column>
+            <vdr-dt2-column [heading]="'common.code' | translate">
+                <ng-template let-facet="item">
+                    {{ facet.code }}
+                </ng-template>
+            </vdr-dt2-column>
+            <vdr-dt2-column [heading]="'common.visibility' | translate">
+                <ng-template let-facet="item">
+                    <div class="badge warning" *ngIf="facet.isPrivate">{{ 'common.private' }}</div>
+                    <div class="badge success" *ngIf="!facet.isPrivate">{{ 'common.public' }}</div>
+                </ng-template>
+            </vdr-dt2-column>
+            <vdr-dt2-column [heading]="'catalog.values' | translate">
+                <ng-template let-facet="item">
+                    <div class="facet-values-list">
+                        <vdr-facet-value-chip
+                            *ngFor="let value of facet.values | slice : 0 : displayLimit[facet.id] || 3"
+                            [facetValue]="value"
+                            [removable]="false"
+                            [displayFacetName]="false"
+                        ></vdr-facet-value-chip>
+                        <button
+                            class="button-ghost"
+                            *ngIf="facet.values.length > initialLimit"
+                            (click)="toggleDisplayLimit(facet)"
+                        >
+                            <ng-container
+                                *ngIf="(displayLimit[facet.id] || 0) < facet.values.length; else collapse"
+                            >
+                                <clr-icon shape="plus"></clr-icon>
+                                {{ facet.values.length - initialLimit }}
+                            </ng-container>
+                            <ng-template #collapse>
+                                <clr-icon shape="minus"></clr-icon>
+                            </ng-template>
+                        </button>
+                    </div>
                 </ng-template>
-            </button>
-        </td>
-        <td class="left align-middle" [class.private]="facet.isPrivate">
-            <vdr-chip>
-                <ng-container *ngIf="!facet.isPrivate; else private">{{
-                    'catalog.public' | translate
-                }}</ng-container>
-                <ng-template #private>{{ 'catalog.private' | translate }}</ng-template>
-            </vdr-chip>
-        </td>
-        <td class="right align-middle" [class.private]="facet.isPrivate">
-            <vdr-table-row-action
-                iconShape="edit"
-                [label]="'common.edit' | translate"
-                [linkTo]="['./', facet.id]"
-            ></vdr-table-row-action>
-        </td>
-        <td class="right align-middle" [class.private]="facet.isPrivate">
-            <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)="deleteFacet(facet.id)"
-                        [disabled]="!(['DeleteCatalog', 'DeleteFacet'] | 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-dt2-column>
+        </vdr-data-table-2>
+    </vdr-page-body>
+</vdr-page-body>

+ 5 - 5
packages/admin-ui/src/lib/catalog/src/components/facet-list/facet-list.component.scss

@@ -1,6 +1,6 @@
-
-td {
-    &.private {
-        background-color: var(--color-component-bg-200);
-    }
+.facet-values-list {
+    max-width: 500px;
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
 }

+ 48 - 12
packages/admin-ui/src/lib/catalog/src/components/facet-list/facet-list.component.ts

@@ -6,17 +6,22 @@ import {
     BaseListComponent,
     DataService,
     DeletionResult,
+    FacetFilterParameter,
+    FacetSortParameter,
     GetFacetListQuery,
+    getOrderStateTranslationToken,
     ItemOf,
     LanguageCode,
     ModalService,
     NotificationService,
+    OrderFilterParameter,
     SelectionManager,
     ServerConfigService,
 } from '@vendure/admin-ui/core';
 import { SortOrder } from '@vendure/common/lib/generated-types';
-import { EMPTY, Observable } from 'rxjs';
+import { EMPTY, merge, Observable } from 'rxjs';
 import { debounceTime, filter, map, switchMap, takeUntil, tap } from 'rxjs/operators';
+import { DataTableService } from '../../../../core/src/providers/data-table/data-table.service';
 
 @Component({
     selector: 'vdr-facet-list',
@@ -27,18 +32,50 @@ export class FacetListComponent
     extends BaseListComponent<GetFacetListQuery, ItemOf<GetFacetListQuery, 'facets'>>
     implements OnInit
 {
-    filterTermControl = new UntypedFormControl('');
+    searchTermControl = new UntypedFormControl('');
     availableLanguages$: Observable<LanguageCode[]>;
     contentLanguage$: Observable<LanguageCode>;
     readonly initialLimit = 3;
     displayLimit: { [id: string]: number } = {};
     selectionManager: SelectionManager<ItemOf<GetFacetListQuery, 'facets'>>;
 
+    readonly filters = this.dataTableService
+        .createFilterCollection<FacetFilterParameter>()
+        .addFilter({
+            name: 'createdAt',
+            type: { kind: 'dateRange' },
+            label: _('common.created-at'),
+            toFilterInput: value => ({
+                createdAt: value.dateOperators,
+            }),
+        })
+        .addFilter({
+            name: 'visibility',
+            type: { kind: 'boolean' },
+            label: _('common.visibility'),
+            toFilterInput: value => ({
+                isPrivate: { eq: value },
+            }),
+        })
+        .connectToRoute(this.route);
+
+    readonly sorts = this.dataTableService
+        .createSortCollection<FacetSortParameter>()
+        .defaultSort('createdAt', 'DESC')
+        .addSort({
+            name: 'name',
+        })
+        .addSort({
+            name: 'code',
+        })
+        .connectToRoute(this.route);
+
     constructor(
         private dataService: DataService,
         private modalService: ModalService,
         private notificationService: NotificationService,
         private serverConfigService: ServerConfigService,
+        private dataTableService: DataTableService,
         router: Router,
         route: ActivatedRoute,
     ) {
@@ -52,12 +89,11 @@ export class FacetListComponent
                     take,
                     filter: {
                         name: {
-                            contains: this.filterTermControl.value,
+                            contains: this.searchTermControl.value,
                         },
+                        ...this.filters.createFilterInput(),
                     },
-                    sort: {
-                        createdAt: SortOrder.DESC,
-                    },
+                    sort: this.sorts.createSortInput(),
                 },
             }),
         );
@@ -75,12 +111,12 @@ export class FacetListComponent
             .uiState()
             .mapStream(({ uiState }) => uiState.contentLanguage)
             .pipe(tap(() => this.refresh()));
-        this.filterTermControl.valueChanges
-            .pipe(
-                filter(value => 2 <= value.length || value.length === 0),
-                debounceTime(250),
-                takeUntil(this.destroy$),
-            )
+        const searchTerm$ = this.searchTermControl.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());
     }
 

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

@@ -33,7 +33,6 @@
         [totalItems]="totalItems$ | async"
         [currentPage]="currentPage$ | async"
         [filters]="filters"
-        [selectionManager]="selectionManager"
         (pageChange)="setPageNumber($event)"
         (itemsPerPageChange)="setItemsPerPage($event)"
     >

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

@@ -21,7 +21,7 @@ import {
 } from '@vendure/admin-ui/core';
 import { EMPTY, Observable } from 'rxjs';
 import { delay, map, switchMap, take, takeUntil, tap, withLatestFrom } from 'rxjs/operators';
-import { DataTableFilterService } from '../../../../core/src/providers/data-table-filter/data-table-filter.service';
+import { DataTableService } from '../../../../core/src/providers/data-table/data-table.service';
 
 export type SearchItem = ItemOf<SearchProductsQuery, 'search'>;
 
@@ -44,9 +44,9 @@ export class ProductListComponent
     pendingSearchIndexUpdates = 0;
     selectionManager: SelectionManager<SearchItem>;
     readonly filters = this.dataTableFilterService
-        .createConfig<SearchInput>()
+        .createFilterCollection<SearchInput>()
         .addFilter({
-            id: 'collectionSlug',
+            name: 'collectionSlug',
             type: { kind: 'text' },
             label: _('catalog.collection-slug'),
             toFilterInput: value => ({
@@ -64,7 +64,7 @@ export class ProductListComponent
         private notificationService: NotificationService,
         private jobQueueService: JobQueueService,
         private serverConfigService: ServerConfigService,
-        private dataTableFilterService: DataTableFilterService,
+        private dataTableFilterService: DataTableService,
         router: Router,
         route: ActivatedRoute,
     ) {

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

@@ -28,7 +28,7 @@ export class DataTableFilterCollection<FilterInput extends Record<string, any> =
     }
 
     getFilter(id: string): DataTableFilter<FilterInput> | undefined {
-        return this.filters.find(f => f.id === id);
+        return this.filters.find(f => f.name === id);
     }
 
     getFilters(): Array<DataTableFilter<FilterInput>> {
@@ -41,27 +41,11 @@ export class DataTableFilterCollection<FilterInput extends Record<string, any> =
 
     createFilterInput(): FilterInput {
         return this.getActiveFilters().reduce(
-            (acc, f) => ({ ...acc, ...(f.value ? f.toFilterInput(f.value) : {}) }),
+            (acc, f) => ({ ...acc, ...(f.value != null ? f.toFilterInput(f.value) : {}) }),
             {} as FilterInput,
         );
     }
 
-    serialize(): string {
-        return this.getActiveFilters()
-            .map(f => `${f.id}:${f.serializeValue()}`)
-            .join(';');
-    }
-
-    parseQueryString(queryString: string): void {
-        const params = new URLSearchParams(queryString);
-        this.filters.forEach(f => {
-            const value = params.get(f.id);
-            if (value !== null) {
-                f.setValue(value);
-            }
-        });
-    }
-
     connectToRoute(route: ActivatedRoute) {
         this.valueChanges.subscribe(value => {
             this.router.navigate(['./'], {
@@ -84,9 +68,15 @@ export class DataTableFilterCollection<FilterInput extends Record<string, any> =
         return this;
     }
 
+    private serialize(): string {
+        return this.getActiveFilters()
+            .map(f => `${f.name}:${f.serializeValue()}`)
+            .join(';');
+    }
+
     private onSetValue() {
         this.valueChanges$.next(
-            this.filters.filter(f => f.value !== undefined).map(f => ({ id: f.id, value: f.value })),
+            this.filters.filter(f => f.value !== undefined).map(f => ({ id: f.name, value: f.value })),
         );
     }
 }

+ 3 - 3
packages/admin-ui/src/lib/core/src/providers/data-table-filter/data-table-filter.ts → packages/admin-ui/src/lib/core/src/providers/data-table/data-table-filter.ts

@@ -35,7 +35,7 @@ export interface DataTableFilterOptions<
     FilterInput extends Record<string, any> = any,
     Type extends DataTableFilterType = DataTableFilterType,
 > {
-    readonly id: string;
+    readonly name: string;
     readonly type: Type;
     readonly label: string;
     readonly toFilterInput: (value: KindValueMap[Type['kind']]) => Partial<FilterInput>;
@@ -55,8 +55,8 @@ export class DataTableFilter<
         return this._value;
     }
 
-    get id(): string {
-        return this.options.id;
+    get name(): string {
+        return this.options.name;
     }
 
     get type(): Type {

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

@@ -0,0 +1,89 @@
+import { ActivatedRoute, Router } from '@angular/router';
+import { Subject } from 'rxjs';
+import { DataTableSort, DataTableSortOptions, SortOrder } 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 }>>();
+    #connectedToRouter = false;
+    valueChanges = this.#valueChanges$.asObservable();
+    readonly #sortQueryParamName = 'sort';
+    #defaultSort: { name: keyof SortInput; sortOrder: SortOrder } | undefined;
+
+    constructor(private router: Router) {}
+
+    get length(): number {
+        return this.#sorts.length;
+    }
+
+    addSort<Name extends keyof SortInput>(
+        config: DataTableSortOptions<SortInput, Name>,
+    ): DataTableSortCollection<SortInput, [...Names, Name]> {
+        if (this.#connectedToRouter) {
+            throw new Error(
+                'Cannot add sort after connecting to router. Make sure to call addSort() before connectToRoute()',
+            );
+        }
+        this.#sorts.push(new DataTableSort<SortInput>(config, () => this.onSetValue()));
+        return this as unknown as DataTableSortCollection<SortInput, [...Names, Name]>;
+    }
+
+    defaultSort(name: keyof SortInput, sortOrder: SortOrder) {
+        this.#defaultSort = { name, sortOrder };
+        return this;
+    }
+
+    get(name: Names[number]): DataTableSort<SortInput> | undefined {
+        return this.#sorts.find(s => s.name === name);
+    }
+
+    connectToRoute(route: ActivatedRoute) {
+        this.valueChanges.subscribe(value => {
+            this.router.navigate(['./'], {
+                queryParams: { [this.#sortQueryParamName]: this.serialize() },
+                relativeTo: route,
+                queryParamsHandling: 'merge',
+            });
+        });
+        const filterQueryParams = (route.snapshot.queryParamMap.get(this.#sortQueryParamName) ?? '')
+            .split(';')
+            .map(value => value.split(':'))
+            .map(([name, value]) => ({ name, value }));
+        for (const { name, value } of filterQueryParams) {
+            const sort = this.get(name);
+            if (sort) {
+                sort.setSortOrder(value as any);
+            }
+        }
+        this.#connectedToRouter = true;
+        return this;
+    }
+
+    createSortInput(): SortInput {
+        const activeSorts = this.#sorts.filter(s => s.sortOrder !== undefined);
+        let sortInput = {} as SortInput;
+        if (activeSorts.length === 0 && this.#defaultSort) {
+            return { [this.#defaultSort.name]: this.#defaultSort.sortOrder } as SortInput;
+        }
+        for (const sort of activeSorts) {
+            sortInput = { ...sortInput, [sort.name]: sort.sortOrder };
+        }
+        return sortInput;
+    }
+
+    private serialize(): string {
+        const activeSorts = this.#sorts.filter(s => s.sortOrder !== undefined);
+        return activeSorts.map(s => `${s.name as string}:${s.sortOrder}`).join(';');
+    }
+
+    private onSetValue() {
+        this.#valueChanges$.next(
+            this.#sorts
+                .filter(f => f.sortOrder !== undefined)
+                .map(s => ({ name: s.name as any, sortOrder: s.sortOrder })),
+        );
+    }
+}

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

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

+ 7 - 2
packages/admin-ui/src/lib/core/src/providers/data-table-filter/data-table-filter.service.ts → packages/admin-ui/src/lib/core/src/providers/data-table/data-table.service.ts

@@ -2,14 +2,19 @@ import { Injectable } from '@angular/core';
 import { Router } from '@angular/router';
 
 import { DataTableFilterCollection } from './data-table-filter-collection';
+import { DataTableSortCollection } from './data-table-sort-collection';
 
 @Injectable({
     providedIn: 'root',
 })
-export class DataTableFilterService {
+export class DataTableService {
     constructor(private router: Router) {}
 
-    createConfig<FilterInput extends Record<string, any>>() {
+    createFilterCollection<FilterInput extends Record<string, any>>() {
         return new DataTableFilterCollection<FilterInput>(this.router);
     }
+
+    createSortCollection<SortInput extends Record<string, any>>() {
+        return new DataTableSortCollection<SortInput>(this.router);
+    }
 }

+ 6 - 0
packages/admin-ui/src/lib/core/src/shared/components/bulk-action-menu/bulk-action-menu.component.ts

@@ -36,6 +36,7 @@ export class BulkActionMenuComponent<T = any> implements OnInit, OnDestroy {
     userPermissions: string[] = [];
 
     private subscription: Subscription;
+    private onClearSelectionFns: Array<() => void> = [];
 
     constructor(
         private bulkActionRegistryService: BulkActionRegistryService,
@@ -113,5 +114,10 @@ export class BulkActionMenuComponent<T = any> implements OnInit, OnDestroy {
     clearSelection() {
         this.selectionManager.clearSelection();
         this.changeDetectorRef.markForCheck();
+        this.onClearSelectionFns.forEach(fn => fn());
+    }
+
+    onClearSelection(callback: () => void) {
+        this.onClearSelectionFns.push(callback);
     }
 }

+ 2 - 0
packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table-column.component.ts

@@ -1,4 +1,5 @@
 import { Component, ContentChild, Input, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { DataTableSort } from '../../../providers/data-table/data-table-sort';
 
 @Component({
     selector: 'vdr-dt2-column',
@@ -12,6 +13,7 @@ export class DataTable2ColumnComponent<T> implements OnInit {
     @Input() expand = false;
     @Input() heading: string;
     @Input() align: 'left' | 'right' | 'center' = 'left';
+    @Input() sort: DataTableSort<any>;
     @Input() optional = true;
     @Input() hiddenByDefault = false;
     #visible = true;

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

@@ -12,13 +12,19 @@
                     (change)="onToggleAllClick()"
                 />
             </th>
-            <th
-                *ngFor="let column of visibleColumns; last as isLast"
-                [class.expand]="column.expand"
-            >
+            <th *ngFor="let column of visibleColumns; last as isLast" [class.expand]="column.expand">
                 <div class="cell-content" [ngClass]="column.align">
-                    {{ column.heading }}
+                    <span>{{ column.heading }}</span>
+                    <div *ngIf="column.sort as sort" class="sort-toggle">
+                        <button (click)="sort.toggleSortOrder()" [class.active]="sort.sortOrder">
+                            <clr-icon *ngIf="!sort.sortOrder" shape="two-way-arrows left"></clr-icon>
+                            <clr-icon *ngIf="sort.sortOrder === 'ASC'" shape="arrow up"></clr-icon>
+                            <clr-icon *ngIf="sort.sortOrder === 'DESC'" shape="arrow down"></clr-icon>
+                        </button>
+                        <div class="sort-label" *ngIf="sort.sortOrder">{{ sort.sortOrder }}</div>
+                    </div>
                 </div>
+
                 <div *ngIf="isLast" class="column-picker">
                     <vdr-data-table-colum-picker [columns]="columns?.toArray()"></vdr-data-table-colum-picker>
                 </div>
@@ -32,19 +38,21 @@
                 </div>-->
                 <ng-container *ngTemplateOutlet="searchComponent?.template"></ng-container>
                 <ng-container *ngTemplateOutlet="customSearchTemplate"></ng-container>
-                <div class="filters">
-                    <vdr-data-table-filters
-                        *ngFor="let activeFilter of filters.getActiveFilters()"
-                        [filter]="activeFilter"
-                        [filters]="filters"
-                        class="mt-1"
-                    ></vdr-data-table-filters>
-                    <vdr-data-table-filters
-                        *ngIf="filters.length"
-                        [filters]="filters"
-                        class="mt-1"
-                    ></vdr-data-table-filters>
-                </div>
+                <ng-container *ngIf="filters">
+                    <div class="filters">
+                        <vdr-data-table-filters
+                            *ngFor="let activeFilter of filters.getActiveFilters()"
+                            [filter]="activeFilter"
+                            [filters]="filters"
+                            class="mt-1"
+                        ></vdr-data-table-filters>
+                        <vdr-data-table-filters
+                            *ngIf="filters.length"
+                            [filters]="filters"
+                            class="mt-1"
+                        ></vdr-data-table-filters>
+                    </div>
+                </ng-container>
             </th>
         </tr>
     </thead>

+ 24 - 0
packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table2.component.scss

@@ -36,6 +36,30 @@ table.no-select {
      position: relative;
 }
 
+.sort-toggle {
+    display: flex;
+    align-items: center;
+    margin-left: calc(var(--space-unit) * 0.5);
+    button {
+        border: 0;
+        border-radius: var(--border-radius-lg);
+        color: var(--color-weight-500);
+        padding: 0 2px;
+        cursor: pointer;
+        background-color: transparent;
+        &.active {
+            color: var(--color-primary-700);
+        }
+    }
+    .sort-label {
+        margin-left: calc(var(--space-unit) * 0.5);
+        font-size: 10px;
+        color: var(--color-primary-600);
+        font-weight: 400;
+    }
+
+}
+
 .filter-row {
     font-size: var(--font-size-base);
     font-weight: 400;

+ 13 - 7
packages/admin-ui/src/lib/core/src/shared/components/data-table-2/data-table2.component.ts

@@ -20,7 +20,7 @@ import { BulkActionMenuComponent, LocalStorageService } from '@vendure/admin-ui/
 import { PaginationService } from 'ngx-pagination';
 import { Subscription } from 'rxjs';
 import { SelectionManager } from '../../../common/utilities/selection-manager';
-import { DataTableFilterCollection } from '../../../providers/data-table-filter/data-table-filter-collection';
+import { DataTableFilterCollection } from '../../../providers/data-table/data-table-filter-collection';
 
 import { DataTable2ColumnComponent } from './data-table-column.component';
 import { DataTable2SearchComponent } from './data-table-search.component';
@@ -98,7 +98,6 @@ export class DataTable2Component<T> implements AfterContentInit, OnChanges, OnIn
     @Input() currentPage: number;
     @Input() totalItems: number;
     @Input() emptyStateLabel: string;
-    @Input() selectionManager?: SelectionManager<T>;
     @Input() filters: DataTableFilterCollection;
     @Output() pageChange = new EventEmitter<number>();
     @Output() itemsPerPageChange = new EventEmitter<number>();
@@ -121,6 +120,10 @@ export class DataTable2Component<T> implements AfterContentInit, OnChanges, OnIn
         private localStorageService: LocalStorageService,
     ) {}
 
+    get selectionManager() {
+        return this.bulkActionMenuComponent?.selectionManager;
+    }
+
     get visibleColumns() {
         return this.columns?.filter(c => c.visible) ?? [];
     }
@@ -140,11 +143,6 @@ export class DataTable2Component<T> implements AfterContentInit, OnChanges, OnIn
     };
 
     ngOnInit() {
-        if (this.selectionManager) {
-            document.addEventListener('keydown', this.shiftDownHandler, { passive: true });
-            document.addEventListener('keyup', this.shiftUpHandler, { passive: true });
-        }
-
         this.subscription = this.selectionManager?.selectionChanges$.subscribe(() =>
             this.changeDetectorRef.markForCheck(),
         );
@@ -189,6 +187,14 @@ export class DataTable2Component<T> implements AfterContentInit, OnChanges, OnIn
             }
             column.onColumnChange(updateColumnVisibility);
         });
+
+        if (this.selectionManager) {
+            document.addEventListener('keydown', this.shiftDownHandler, { passive: true });
+            document.addEventListener('keyup', this.shiftUpHandler, { passive: true });
+            this.bulkActionMenuComponent.onClearSelection(() => {
+                this.changeDetectorRef.markForCheck();
+            });
+        }
     }
 
     trackByFn(index: number, item: any) {

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/components/data-table-filter-label/data-table-filter-label.component.ts

@@ -1,5 +1,5 @@
 import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
-import { DataTableFilter } from '../../../providers/data-table-filter/data-table-filter';
+import { DataTableFilter } from '../../../providers/data-table/data-table-filter';
 
 @Component({
     selector: 'vdr-data-table-filter-label',

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

@@ -5,11 +5,8 @@ import { DateOperators } 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-filter/data-table-filter';
-import { DataTableFilterCollection } from '../../../providers/data-table-filter/data-table-filter-collection';
+import { DataTableFilter, DataTableFilterSelectType } from '../../../providers/data-table/data-table-filter';
+import { DataTableFilterCollection } from '../../../providers/data-table/data-table-filter-collection';
 
 @Component({
     selector: 'vdr-data-table-filters',
@@ -29,7 +26,7 @@ export class DataTableFiltersComponent implements AfterViewInit, OnInit {
 
     ngOnInit() {
         if (this.filter) {
-            const filterConfig = this.filters.getFilter(this.filter?.id);
+            const filterConfig = this.filters.getFilter(this.filter?.name);
             if (filterConfig) {
                 this.selectFilter(filterConfig);
                 this.state = 'active';

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

@@ -41,7 +41,7 @@
                 </a>
             </ng-template>
         </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'order.customer' | translate">
+        <vdr-dt2-column [heading]="'order.customer' | translate" [sort]="sorts.get('customerLastName')">
             <ng-template let-order="item">
                 <vdr-customer-label
                     [customer]="order.customer"
@@ -54,12 +54,12 @@
                 <vdr-chip>{{ order.type }}</vdr-chip>
             </ng-template>
         </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'order.state' | translate">
+        <vdr-dt2-column [heading]="'order.state' | translate" [sort]="sorts.get('state')">
             <ng-template let-order="item">
                 <vdr-order-state-label [state]="order.state"></vdr-order-state-label>
             </ng-template>
         </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'order.total' | translate">
+        <vdr-dt2-column [heading]="'order.total' | translate" [sort]="sorts.get('totalWithTax')">
             <ng-template let-order="item">
                 {{ order.totalWithTax | localeCurrency : order.currencyCode }}
             </ng-template>
@@ -69,7 +69,7 @@
                 {{ order.updatedAt | timeAgo }}
             </ng-template>
         </vdr-dt2-column>
-        <vdr-dt2-column [heading]="'order.placed-at' | translate">
+        <vdr-dt2-column [heading]="'order.placed-at' | translate" [sort]="sorts.get('orderPlacedAt')">
             <ng-template let-order="item">
                 {{ order.orderPlacedAt | localeDate : 'short' }}
             </ng-template>

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

@@ -6,6 +6,7 @@ import {
     BaseListComponent,
     ChannelService,
     DataService,
+    FacetSortParameter,
     GetOrderListQuery,
     getOrderStateTranslationToken,
     ItemOf,
@@ -13,6 +14,7 @@ import {
     LogicalOperator,
     OrderFilterParameter,
     OrderListOptions,
+    OrderSortParameter,
     OrderType,
     ServerConfigService,
     SortOrder,
@@ -20,7 +22,7 @@ import {
 import { Order } from '@vendure/common/lib/generated-types';
 import { merge } from 'rxjs';
 import { debounceTime, filter, takeUntil, tap } from 'rxjs/operators';
-import { DataTableFilterService } from '../../../../core/src/providers/data-table-filter/data-table-filter.service';
+import { DataTableService } from '../../../../core/src/providers/data-table/data-table.service';
 
 @Component({
     selector: 'vdr-order-list',
@@ -34,10 +36,18 @@ export class OrderListComponent
 {
     searchControl = new UntypedFormControl('');
     orderStates = this.serverConfigService.getOrderProcessStates().map(item => item.name);
-    readonly filters = this.dataTableFilterService
-        .createConfig<OrderFilterParameter>()
+    readonly filters = this.dataTableService
+        .createFilterCollection<OrderFilterParameter>()
         .addFilter({
-            id: 'active',
+            name: 'createdAt',
+            type: { kind: 'dateRange' },
+            label: _('common.created-at'),
+            toFilterInput: value => ({
+                createdAt: value.dateOperators,
+            }),
+        })
+        .addFilter({
+            name: 'active',
             type: { kind: 'boolean' },
             label: _('order.filter-is-active'),
             toFilterInput: value => ({
@@ -47,7 +57,7 @@ export class OrderListComponent
             }),
         })
         .addFilter({
-            id: 'state',
+            name: 'state',
             type: {
                 kind: 'select',
                 options: this.orderStates.map(s => ({ value: s, label: getOrderStateTranslationToken(s) })),
@@ -60,7 +70,7 @@ export class OrderListComponent
             }),
         })
         .addFilter({
-            id: 'orderPlacedAt',
+            name: 'orderPlacedAt',
             type: {
                 kind: 'dateRange',
             },
@@ -70,7 +80,7 @@ export class OrderListComponent
             }),
         })
         .addFilter({
-            id: 'customerLastName',
+            name: 'customerLastName',
             type: { kind: 'text' },
             label: _('customer.last-name'),
             toFilterInput: value => ({
@@ -79,6 +89,33 @@ export class OrderListComponent
                 },
             }),
         })
+        .addFilter({
+            name: 'transactionId',
+            type: { kind: 'text' },
+            label: _('order.transaction-id'),
+            toFilterInput: value => ({
+                transactionId: {
+                    [value.operator]: value.term,
+                },
+            }),
+        })
+        .connectToRoute(this.route);
+
+    readonly sorts = this.dataTableService
+        .createSortCollection<OrderSortParameter>()
+        .defaultSort('updatedAt', 'DESC')
+        .addSort({
+            name: 'orderPlacedAt',
+        })
+        .addSort({
+            name: 'customerLastName',
+        })
+        .addSort({
+            name: 'state',
+        })
+        .addSort({
+            name: 'totalWithTax',
+        })
         .connectToRoute(this.route);
 
     canCreateDraftOrder = false;
@@ -89,7 +126,7 @@ export class OrderListComponent
         private dataService: DataService,
         private localStorageService: LocalStorageService,
         private channelService: ChannelService,
-        private dataTableFilterService: DataTableFilterService,
+        private dataTableService: DataTableService,
         router: Router,
         route: ActivatedRoute,
     ) {
@@ -135,7 +172,6 @@ export class OrderListComponent
         take: number,
         searchTerm: string,
     ): { options: OrderListOptions } {
-        let filterOperator: LogicalOperator = LogicalOperator.AND;
         let filterInput = this.filters.createFilterInput();
         if (this.activeChannelIsDefaultChannel) {
             filterInput = {
@@ -147,17 +183,10 @@ export class OrderListComponent
         }
         if (searchTerm) {
             filterInput = {
-                customerLastName: {
-                    contains: searchTerm,
-                },
-                transactionId: {
-                    contains: searchTerm,
-                },
                 code: {
                     contains: searchTerm,
                 },
             };
-            filterOperator = LogicalOperator.OR;
         }
         return {
             options: {
@@ -166,10 +195,7 @@ export class OrderListComponent
                 filter: {
                     ...(filterInput ?? {}),
                 },
-                sort: {
-                    updatedAt: SortOrder.DESC,
-                },
-                filterOperator,
+                sort: this.sorts.createSortInput(),
             },
         };
     }

+ 1 - 0
packages/admin-ui/src/lib/static/styles/styles.scss

@@ -3,6 +3,7 @@
 @import "@clr/icons/clr-icons.min.css";
 @import "@ng-select/ng-select/themes/default.theme.css";
 @import '@angular/cdk/overlay-prebuilt.css';
+@import "global/badges";
 @import "global/buttons";
 @import "global/forms";
 @import "global/overrides";