Browse Source

fix(admin-ui): Fix multiple same filter types

Michael Bromley 2 years ago
parent
commit
f29aae9781

+ 1 - 0
packages/admin-ui/package.json

@@ -48,6 +48,7 @@
         "core-js": "^3.29.0",
         "core-js": "^3.29.0",
         "dayjs": "^1.10.4",
         "dayjs": "^1.10.4",
         "graphql": "16.6.0",
         "graphql": "16.6.0",
+        "just-extend": "^6.2.0",
         "messageformat": "2.3.0",
         "messageformat": "2.3.0",
         "ngx-pagination": "^6.0.3",
         "ngx-pagination": "^6.0.3",
         "ngx-translate-messageformat-compiler": "^6.2.0",
         "ngx-translate-messageformat-compiler": "^6.2.0",

+ 0 - 1
packages/admin-ui/src/lib/core/src/common/base-list.component.ts

@@ -1,6 +1,5 @@
 import { Directive, OnDestroy, OnInit } from '@angular/core';
 import { Directive, OnDestroy, OnInit } from '@angular/core';
 import { ActivatedRoute, QueryParamsHandling, Router } from '@angular/router';
 import { ActivatedRoute, QueryParamsHandling, Router } from '@angular/router';
-import { PaginatedList } from '@vendure/common/lib/shared-types';
 import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
 import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
 import { distinctUntilChanged, map, shareReplay, takeUntil } from 'rxjs/operators';
 import { distinctUntilChanged, map, shareReplay, takeUntil } from 'rxjs/operators';
 
 

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

@@ -1,82 +1,197 @@
 import { ActivatedRoute, Router } from '@angular/router';
 import { ActivatedRoute, Router } from '@angular/router';
+import { assertNever } from '@vendure/common/lib/shared-utils';
 import { Subject } from 'rxjs';
 import { Subject } from 'rxjs';
-import { DataTableFilter, DataTableFilterType } from './data-table-filter';
+import extend from 'just-extend';
+import {
+    DataTableFilter,
+    DataTableFilterBooleanType,
+    DataTableFilterDateRangeType,
+    DataTableFilterNumberType,
+    DataTableFilterOptions,
+    DataTableFilterSelectType,
+    DataTableFilterTextType,
+    DataTableFilterType,
+    DataTableFilterValue,
+} from './data-table-filter';
+
+export class FilterWithValue<Type extends DataTableFilterType = DataTableFilterType> {
+    constructor(
+        public readonly filter: DataTableFilter<any, Type>,
+        public value: DataTableFilterValue<Type>,
+        private onUpdate?: (value: DataTableFilterValue<Type>) => void,
+    ) {}
+
+    updateValue(value: DataTableFilterValue<Type>) {
+        this.value = value;
+        if (this.onUpdate) {
+            this.onUpdate(value);
+        }
+    }
+
+    isText(): this is FilterWithValue<DataTableFilterTextType> {
+        return this.filter.type.kind === 'text';
+    }
+
+    isNumber(): this is FilterWithValue<DataTableFilterNumberType> {
+        return this.filter.type.kind === 'number';
+    }
+
+    isBoolean(): this is FilterWithValue<DataTableFilterBooleanType> {
+        return this.filter.type.kind === 'boolean';
+    }
+
+    isSelect(): this is FilterWithValue<DataTableFilterSelectType> {
+        return this.filter.type.kind === 'select';
+    }
+
+    isDateRange(): this is FilterWithValue<DataTableFilterDateRangeType> {
+        return this.filter.type.kind === 'dateRange';
+    }
+}
 
 
 export class DataTableFilterCollection<FilterInput extends Record<string, any> = Record<string, any>> {
 export class DataTableFilterCollection<FilterInput extends Record<string, any> = Record<string, any>> {
-    private readonly filters: Array<DataTableFilter<FilterInput, any>> = [];
-    private valueChanges$ = new Subject<Array<{ id: string; value: any }>>();
-    private connectedToRouter = false;
-    valueChanges = this.valueChanges$.asObservable();
-    private readonly filtersQueryParamName = 'filters';
+    readonly #filters: Array<DataTableFilter<FilterInput, any>> = [];
+    #activeFilters: FilterWithValue[] = [];
+    #valueChanges$ = new Subject<FilterWithValue[]>();
+    #connectedToRouter = false;
+    valueChanges = this.#valueChanges$.asObservable();
+    readonly #filtersQueryParamName = 'filters';
 
 
     constructor(private router: Router) {}
     constructor(private router: Router) {}
 
 
     get length(): number {
     get length(): number {
-        return this.filters.length;
+        return this.#filters.length;
+    }
+
+    get activeFilters(): FilterWithValue[] {
+        return this.#activeFilters;
     }
     }
 
 
     addFilter<FilterType extends DataTableFilterType>(
     addFilter<FilterType extends DataTableFilterType>(
-        config: ConstructorParameters<typeof DataTableFilter<FilterInput, FilterType>>[0],
+        config: DataTableFilterOptions<FilterInput, FilterType>,
     ): DataTableFilterCollection<FilterInput> {
     ): DataTableFilterCollection<FilterInput> {
-        if (this.connectedToRouter) {
+        if (this.#connectedToRouter) {
             throw new Error(
             throw new Error(
                 'Cannot add filter after connecting to router. Make sure to call addFilter() before connectToRoute()',
                 'Cannot add filter after connecting to router. Make sure to call addFilter() before connectToRoute()',
             );
             );
         }
         }
-        this.filters.push(new DataTableFilter(config, () => this.onSetValue()));
+        this.#filters.push(
+            new DataTableFilter(config, (filter, value) => this.onActivateFilter(filter, value)),
+        );
         return this;
         return this;
     }
     }
 
 
-    getFilter(id: string): DataTableFilter<FilterInput> | undefined {
-        return this.filters.find(f => f.name === id);
+    getFilter(name: string): DataTableFilter<FilterInput> | undefined {
+        return this.#filters.find(f => f.name === name);
     }
     }
 
 
     getFilters(): Array<DataTableFilter<FilterInput>> {
     getFilters(): Array<DataTableFilter<FilterInput>> {
-        return this.filters;
+        return this.#filters;
     }
     }
 
 
-    getActiveFilters(): Array<DataTableFilter<FilterInput>> {
-        return this.filters.filter(f => f.value !== undefined);
+    removeActiveFilterAtIndex(index: number) {
+        this.#activeFilters.splice(index, 1);
+        this.#valueChanges$.next(this.#activeFilters);
     }
     }
 
 
     createFilterInput(): FilterInput {
     createFilterInput(): FilterInput {
-        return this.getActiveFilters().reduce(
-            (acc, f) => ({ ...acc, ...(f.value != null ? f.toFilterInput(f.value) : {}) }),
-            {} as FilterInput,
-        );
+        return this.#activeFilters.reduce((acc, { filter, value }) => {
+            const newValue = value != null ? filter.toFilterInput(value) : {};
+            const result = extend(true, acc, newValue);
+            return result as FilterInput;
+        }, {} as FilterInput);
     }
     }
 
 
     connectToRoute(route: ActivatedRoute) {
     connectToRoute(route: ActivatedRoute) {
         this.valueChanges.subscribe(value => {
         this.valueChanges.subscribe(value => {
             this.router.navigate(['./'], {
             this.router.navigate(['./'], {
-                queryParams: { [this.filtersQueryParamName]: this.serialize() },
+                queryParams: { [this.#filtersQueryParamName]: this.serialize() },
                 relativeTo: route,
                 relativeTo: route,
                 queryParamsHandling: 'merge',
                 queryParamsHandling: 'merge',
             });
             });
         });
         });
-        const filterQueryParams = (route.snapshot.queryParamMap.get(this.filtersQueryParamName) ?? '')
+        const filterQueryParams = (route.snapshot.queryParamMap.get(this.#filtersQueryParamName) ?? '')
             .split(';')
             .split(';')
             .map(value => value.split(':'))
             .map(value => value.split(':'))
-            .map(([id, value]) => ({ id, value }));
-        for (const { id, value } of filterQueryParams) {
-            const filter = this.getFilter(id);
+            .map(([name, value]) => ({ name, value }));
+        for (const { name, value } of filterQueryParams) {
+            const filter = this.getFilter(name);
             if (filter) {
             if (filter) {
-                filter.deserializeValue(value);
+                const val = this.deserializeValue(filter, value);
+                this.#activeFilters.push(this.createFacetWithValue(filter, val));
             }
             }
         }
         }
-        this.connectedToRouter = true;
+        this.#connectedToRouter = true;
         return this;
         return this;
     }
     }
 
 
+    serializeValue<Type extends DataTableFilterType>(
+        filterWithValue: FilterWithValue<Type>,
+    ): string | undefined {
+        const valueAsType = <T extends DataTableFilter<any, any>>(
+            _filter: T,
+            _value: DataTableFilterValue<any>,
+        ): T extends DataTableFilter<any, infer R> ? DataTableFilterValue<R> : any => _value;
+
+        if (filterWithValue.isText()) {
+            const val = filterWithValue.value;
+            return `${val?.operator},${val?.term}`;
+        } else if (filterWithValue.isNumber()) {
+            const val = filterWithValue.value;
+            return `${val.operator},${val.amount}`;
+        } else if (filterWithValue.isSelect()) {
+            const val = filterWithValue.value;
+            return val.join(',');
+        } else if (filterWithValue.isBoolean()) {
+            const val = filterWithValue.value;
+            return val ? '1' : '0';
+        } else if (filterWithValue.isDateRange()) {
+            const val = filterWithValue.value;
+            const start = val.start ? new Date(val.start).getTime() : '';
+            const end = val.end ? new Date(val.end).getTime() : '';
+            return `${start},${end}`;
+        }
+    }
+
+    deserializeValue(filter: DataTableFilter, value: string): any {
+        switch (filter.type.kind) {
+            case 'text': {
+                const [operator, term] = value.split(',');
+                return { operator, term };
+            }
+            case 'number': {
+                const [operator, amount] = value.split(',');
+                return { operator, amount };
+            }
+            case 'select':
+                return value.split(',');
+            case 'boolean':
+                return value === '1';
+            case 'dateRange':
+                const [startTimestamp, endTimestamp] = value.split(',');
+                const start = startTimestamp ? new Date(Number(startTimestamp)).toISOString() : '';
+                const end = endTimestamp ? new Date(Number(endTimestamp)).toISOString() : '';
+                return { start, end };
+            default:
+                assertNever(filter.type);
+        }
+    }
+
     private serialize(): string {
     private serialize(): string {
-        return this.getActiveFilters()
-            .map(f => `${f.name}:${f.serializeValue()}`)
+        return this.#activeFilters
+            .map(
+                (filterWithValue, i) =>
+                    `${filterWithValue.filter.name}:${this.serializeValue(filterWithValue)}`,
+            )
             .join(';');
             .join(';');
     }
     }
 
 
-    private onSetValue() {
-        this.valueChanges$.next(
-            this.filters.filter(f => f.value !== undefined).map(f => ({ id: f.name, value: f.value })),
-        );
+    private onActivateFilter(filter: DataTableFilter<any, any>, value: DataTableFilterValue<any>) {
+        this.#activeFilters.push(this.createFacetWithValue(filter, value));
+        this.#valueChanges$.next(this.#activeFilters);
+    }
+
+    private createFacetWithValue(filter: DataTableFilter<any, any>, value: DataTableFilterValue<any>) {
+        return new FilterWithValue(filter, value, v => this.#valueChanges$.next(v));
     }
     }
 }
 }

+ 27 - 88
packages/admin-ui/src/lib/core/src/providers/data-table/data-table-filter.ts

@@ -1,5 +1,5 @@
-import { DateOperators } from '@vendure/admin-ui/core';
 import { assertNever } from '@vendure/common/lib/shared-utils';
 import { assertNever } from '@vendure/common/lib/shared-utils';
+import { DateOperators } from '../../common/generated-types';
 
 
 export interface DataTableFilterTextType {
 export interface DataTableFilterTextType {
     kind: 'text';
     kind: 'text';
@@ -15,6 +15,11 @@ export interface DataTableFilterBooleanType {
     kind: 'boolean';
     kind: 'boolean';
 }
 }
 
 
+export interface DataTableFilterNumberType {
+    kind: 'number';
+    inputType?: 'number' | 'currency';
+}
+
 export interface DataTableFilterDateRangeType {
 export interface DataTableFilterDateRangeType {
     kind: 'dateRange';
     kind: 'dateRange';
 }
 }
@@ -24,12 +29,14 @@ export type KindValueMap = {
     select: string[];
     select: string[];
     boolean: boolean;
     boolean: boolean;
     dateRange: { start?: string; end?: string; dateOperators: DateOperators };
     dateRange: { start?: string; end?: string; dateOperators: DateOperators };
+    number: { operator: 'eq' | 'gt' | 'lt'; amount: string };
 };
 };
 export type DataTableFilterType =
 export type DataTableFilterType =
     | DataTableFilterTextType
     | DataTableFilterTextType
     | DataTableFilterSelectType
     | DataTableFilterSelectType
     | DataTableFilterBooleanType
     | DataTableFilterBooleanType
-    | DataTableFilterDateRangeType;
+    | DataTableFilterDateRangeType
+    | DataTableFilterNumberType;
 
 
 export interface DataTableFilterOptions<
 export interface DataTableFilterOptions<
     FilterInput extends Record<string, any> = any,
     FilterInput extends Record<string, any> = any,
@@ -38,22 +45,22 @@ export interface DataTableFilterOptions<
     readonly name: string;
     readonly name: string;
     readonly type: Type;
     readonly type: Type;
     readonly label: string;
     readonly label: string;
-    readonly toFilterInput: (value: KindValueMap[Type['kind']]) => Partial<FilterInput>;
+    readonly toFilterInput: (value: DataTableFilterValue<Type>) => Partial<FilterInput>;
 }
 }
 
 
+export type DataTableFilterValue<Type extends DataTableFilterType> = KindValueMap[Type['kind']];
+
 export class DataTableFilter<
 export class DataTableFilter<
     FilterInput extends Record<string, any> = any,
     FilterInput extends Record<string, any> = any,
     Type extends DataTableFilterType = DataTableFilterType,
     Type extends DataTableFilterType = DataTableFilterType,
 > {
 > {
     constructor(
     constructor(
         private readonly options: DataTableFilterOptions<FilterInput, Type>,
         private readonly options: DataTableFilterOptions<FilterInput, Type>,
-        private onSetValue?: (value: KindValueMap[Type['kind']] | undefined) => void,
+        private onActivate?: (
+            filter: DataTableFilter<FilterInput, Type>,
+            value: DataTableFilterValue<Type> | undefined,
+        ) => void,
     ) {}
     ) {}
-    private _value: any | undefined;
-
-    get value(): KindValueMap[Type['kind']] | undefined {
-        return this._value;
-    }
 
 
     get name(): string {
     get name(): string {
         return this.options.name;
         return this.options.name;
@@ -67,74 +74,13 @@ export class DataTableFilter<
         return this.options.label;
         return this.options.label;
     }
     }
 
 
-    toFilterInput(value: KindValueMap[Type['kind']]): Partial<FilterInput> {
+    toFilterInput(value: DataTableFilterValue<Type>): Partial<FilterInput> {
         return this.options.toFilterInput(value);
         return this.options.toFilterInput(value);
     }
     }
 
 
-    setValue(value: KindValueMap[Type['kind']]): void {
-        this._value = value;
-        if (this.onSetValue) {
-            this.onSetValue(value);
-        }
-    }
-
-    clearValue(): void {
-        this._value = undefined;
-        if (this.onSetValue) {
-            this.onSetValue(undefined);
-        }
-    }
-
-    serializeValue(): string | undefined {
-        if (this.value === undefined) {
-            return undefined;
-        }
-        const kind = this.type.kind;
-        switch (kind) {
-            case 'text': {
-                const value = this.getValueForKind(kind);
-                return `${value?.operator},${value?.term}`;
-            }
-            case 'select': {
-                const value = this.getValueForKind(kind);
-                return value?.join(',');
-            }
-            case 'boolean': {
-                const value = this.getValueForKind(kind);
-                return value ? '1' : '0';
-            }
-            case 'dateRange': {
-                const value = this.getValueForKind(kind);
-                const start = value?.start ? new Date(value.start).getTime() : '';
-                const end = value?.end ? new Date(value.end).getTime() : '';
-                return `${start},${end}`;
-            }
-            default:
-                assertNever(this.type);
-        }
-    }
-
-    deserializeValue(value: string): void {
-        switch (this.type.kind) {
-            case 'text': {
-                const [operator, term] = value.split(',');
-                this._value = { operator, term };
-                break;
-            }
-            case 'select':
-                this._value = value.split(',');
-                break;
-            case 'boolean':
-                this._value = value === '1';
-                break;
-            case 'dateRange':
-                const [startTimestamp, endTimestamp] = value.split(',');
-                const start = startTimestamp ? new Date(Number(startTimestamp)).toISOString() : '';
-                const end = endTimestamp ? new Date(Number(endTimestamp)).toISOString() : '';
-                this._value = { start, end };
-                break;
-            default:
-                assertNever(this.type);
+    activate(value: DataTableFilterValue<Type>) {
+        if (this.onActivate) {
+            this.onActivate(this, value);
         }
         }
     }
     }
 
 
@@ -142,6 +88,10 @@ export class DataTableFilter<
         return this.type.kind === 'text';
         return this.type.kind === 'text';
     }
     }
 
 
+    isNumber(): this is DataTableFilter<FilterInput, DataTableFilterNumberType> {
+        return this.type.kind === 'number';
+    }
+
     isBoolean(): this is DataTableFilter<FilterInput, DataTableFilterBooleanType> {
     isBoolean(): this is DataTableFilter<FilterInput, DataTableFilterBooleanType> {
         return this.type.kind === 'boolean';
         return this.type.kind === 'boolean';
     }
     }
@@ -154,18 +104,7 @@ export class DataTableFilter<
         return this.type.kind === 'dateRange';
         return this.type.kind === 'dateRange';
     }
     }
 
 
-    private getValueForKind<Kind extends Type['kind']>(kind: Kind): KindValueMap[Kind] | undefined {
-        switch (kind) {
-            case 'text':
-                return this.value as any;
-            case 'select':
-                return this.value as any;
-            case 'boolean':
-                return this.value as any;
-            case 'dateRange':
-                return this.value as any;
-            default:
-                assertNever(kind);
-        }
-    }
+    // private getValueForKind<Kind extends Type['kind']>(kind: Kind): KindValueMap[Kind] | undefined {
+    //     return this.value as any;
+    // }
 }
 }

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

@@ -41,8 +41,8 @@
                 <ng-container *ngIf="filters">
                 <ng-container *ngIf="filters">
                     <div class="filters">
                     <div class="filters">
                         <vdr-data-table-filters
                         <vdr-data-table-filters
-                            *ngFor="let activeFilter of filters.getActiveFilters()"
-                            [filter]="activeFilter"
+                            *ngFor="let activeFilter of filters.activeFilters"
+                            [filterWithValue]="activeFilter"
                             [filters]="filters"
                             [filters]="filters"
                             class="mt-1"
                             class="mt-1"
                         ></vdr-data-table-filters>
                         ></vdr-data-table-filters>

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

@@ -1,29 +1,36 @@
-<span>{{ filter.label | translate }}:</span>
+<span>{{ filterWithValue.filter.label | translate }}:</span>
 <div>
 <div>
-    <ng-container *ngIf="filter.isSelect()">
-        {{ filter.value?.join(', ') }}
+    <ng-container *ngIf="filterWithValue.isSelect()">
+        {{ filterWithValue.value?.join(', ') }}
     </ng-container>
     </ng-container>
-    <ng-container *ngIf="filter.isText()">
-        <span *ngIf="filter.value?.operator === 'contains'">{{
+    <ng-container *ngIf="filterWithValue.isText()">
+        <span *ngIf="filterWithValue.value?.operator === 'contains'">{{
             'common.operator-contains' | translate
             'common.operator-contains' | translate
         }}</span>
         }}</span>
-        <span *ngIf="filter.value?.operator === 'eq'">{{ 'common.operator-eq' | translate }}</span>
-        <span *ngIf="filter.value?.operator === 'notContains'">{{
+        <span *ngIf="filterWithValue.value?.operator === 'eq'">{{ 'common.operator-eq' | translate }}</span>
+        <span *ngIf="filterWithValue.value?.operator === 'notContains'">{{
             'common.operator-notContains' | translate
             'common.operator-notContains' | translate
         }}</span>
         }}</span>
-        <span *ngIf="filter.value?.operator === 'notEq'">{{ 'common.operator-not-eq' | translate }}</span>
-        <span *ngIf="filter.value?.operator === 'regex'">{{ 'common.operator-regex' | translate }}</span>
-        <span> "{{ filter.value?.term }}"</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>
-    <ng-container *ngIf="filter.isBoolean()">
-        <span *ngIf="filter?.value">{{ 'common.boolean-true' | translate }}</span>
-        <span *ngIf="!filter?.value">{{ 'common.boolean-false' | translate }}</span>
+    <ng-container *ngIf="filterWithValue.isBoolean()">
+        <span *ngIf="filterWithValue?.value">{{ 'common.boolean-true' | translate }}</span>
+        <span *ngIf="!filterWithValue?.value">{{ 'common.boolean-false' | translate }}</span>
     </ng-container>
     </ng-container>
-    <ng-container *ngIf="filter.isDateRange()">
-        <span *ngIf="filter.value?.start && filter.value?.end">
-            {{ filter.value?.start | localeDate : 'shortDate' }} - {{ filter.value?.end | localeDate : 'shortDate' }}
+    <ng-container *ngIf="filterWithValue.isDateRange()">
+        <span *ngIf="filterWithValue.value?.start && filterWithValue.value?.end">
+            {{ filterWithValue.value?.start | localeDate : 'shortDate' }} - {{ filterWithValue.value?.end | localeDate : 'shortDate' }}
         </span>
         </span>
-        <span *ngIf="filter.value?.start && !filter.value?.end"> > {{ filter.value?.start | localeDate : 'shortDate' }} </span>
-        <span *ngIf="filter.value?.end && !filter.value?.start"> < {{ filter.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>
     </ng-container>
     </ng-container>
 </div>
 </div>

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

@@ -1,5 +1,6 @@
 import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
 import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
 import { DataTableFilter } from '../../../providers/data-table/data-table-filter';
 import { DataTableFilter } from '../../../providers/data-table/data-table-filter';
+import { FilterWithValue } from '../../../providers/data-table/data-table-filter-collection';
 
 
 @Component({
 @Component({
     selector: 'vdr-data-table-filter-label',
     selector: 'vdr-data-table-filter-label',
@@ -8,5 +9,5 @@ import { DataTableFilter } from '../../../providers/data-table/data-table-filter
     changeDetection: ChangeDetectionStrategy.Default,
     changeDetection: ChangeDetectionStrategy.Default,
 })
 })
 export class DataTableFilterLabelComponent {
 export class DataTableFilterLabelComponent {
-    @Input() filter: DataTableFilter;
+    @Input() filterWithValue: FilterWithValue;
 }
 }

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

@@ -7,7 +7,7 @@
         <button vdrDropdownTrigger class="">
         <button vdrDropdownTrigger class="">
             <span *ngIf="state === 'new'">{{ 'common.add-filter' | translate }}</span>
             <span *ngIf="state === 'new'">{{ 'common.add-filter' | translate }}</span>
             <span *ngIf="state === 'active'">
             <span *ngIf="state === 'active'">
-                <vdr-data-table-filter-label [filter]="filter"></vdr-data-table-filter-label>
+                <vdr-data-table-filter-label [filterWithValue]="filterWithValue"></vdr-data-table-filter-label>
             </span>
             </span>
             <clr-icon shape="ellipsis-vertical" size="12"></clr-icon>
             <clr-icon shape="ellipsis-vertical" size="12"></clr-icon>
         </button>
         </button>
@@ -51,6 +51,24 @@
                         <input type="text" formControlName="term" />
                         <input type="text" formControlName="term" />
                     </div>
                     </div>
                 </div>
                 </div>
+                <div *ngSwitchCase="'number'">
+                    <div [formGroup]="formControl">
+                        <select clrSelect name="options" formControlName="operator" class="mb-1">
+                            <option value="eq">{{ 'common.operator-eq' | translate }}</option>
+                            <option value="gt">{{ 'common.operator-gt' | translate }}</option>
+                            <option value="lt">{{ 'common.operator-lt' | translate }}</option>
+                        </select>
+                        <input
+                            *ngIf="$any(selectedFilter.type).inputType !== 'currency'"
+                            type="text"
+                            formControlName="amount"
+                        />
+                        <vdr-currency-input
+                            *ngIf="$any(selectedFilter.type).inputType === 'currency'"
+                            formControlName="amount"
+                        />
+                    </div>
+                </div>
                 <div *ngSwitchCase="'dateRange'">
                 <div *ngSwitchCase="'dateRange'">
                     <div [formGroup]="formControl">
                     <div [formGroup]="formControl">
                         <label>
                         <label>

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

@@ -6,7 +6,10 @@ import { assertNever } from '@vendure/common/lib/shared-utils';
 import { DropdownComponent } from '../dropdown/dropdown.component';
 import { DropdownComponent } from '../dropdown/dropdown.component';
 import { I18nService } from '../../../providers/i18n/i18n.service';
 import { I18nService } from '../../../providers/i18n/i18n.service';
 import { DataTableFilter, DataTableFilterSelectType } from '../../../providers/data-table/data-table-filter';
 import { DataTableFilter, DataTableFilterSelectType } from '../../../providers/data-table/data-table-filter';
-import { DataTableFilterCollection } from '../../../providers/data-table/data-table-filter-collection';
+import {
+    DataTableFilterCollection,
+    FilterWithValue,
+} from '../../../providers/data-table/data-table-filter-collection';
 
 
 @Component({
 @Component({
     selector: 'vdr-data-table-filters',
     selector: 'vdr-data-table-filters',
@@ -16,7 +19,7 @@ import { DataTableFilterCollection } from '../../../providers/data-table/data-ta
 })
 })
 export class DataTableFiltersComponent implements AfterViewInit, OnInit {
 export class DataTableFiltersComponent implements AfterViewInit, OnInit {
     @Input() filters: DataTableFilterCollection;
     @Input() filters: DataTableFilterCollection;
-    @Input() filter?: DataTableFilter;
+    @Input() filterWithValue?: FilterWithValue;
     @ViewChild('dropdown', { static: true }) dropdown: DropdownComponent;
     @ViewChild('dropdown', { static: true }) dropdown: DropdownComponent;
     protected state: 'new' | 'active' = 'new';
     protected state: 'new' | 'active' = 'new';
     protected formControl: AbstractControl;
     protected formControl: AbstractControl;
@@ -25,12 +28,10 @@ export class DataTableFiltersComponent implements AfterViewInit, OnInit {
     constructor(private i18nService: I18nService) {}
     constructor(private i18nService: I18nService) {}
 
 
     ngOnInit() {
     ngOnInit() {
-        if (this.filter) {
-            const filterConfig = this.filters.getFilter(this.filter?.name);
-            if (filterConfig) {
-                this.selectFilter(filterConfig);
-                this.state = 'active';
-            }
+        if (this.filterWithValue) {
+            const { filter, value } = this.filterWithValue;
+            this.selectFilter(filter, value);
+            this.state = 'active';
         }
         }
     }
     }
 
 
@@ -42,13 +43,13 @@ export class DataTableFiltersComponent implements AfterViewInit, OnInit {
         });
         });
     }
     }
 
 
-    selectFilter(filter: DataTableFilter) {
+    selectFilter(filter: DataTableFilter, value?: any) {
         this.selectedFilter = filter;
         this.selectedFilter = filter;
         if (filter.isText()) {
         if (filter.isText()) {
             this.formControl = new FormGroup(
             this.formControl = new FormGroup(
                 {
                 {
-                    operator: new FormControl(filter.value?.operator ?? 'contains'),
-                    term: new FormControl(filter.value?.term ?? ''),
+                    operator: new FormControl(value?.operator ?? 'contains'),
+                    term: new FormControl(value?.term ?? ''),
                 },
                 },
                 control => {
                 control => {
                     if (!control.value.term) {
                     if (!control.value.term) {
@@ -57,25 +58,39 @@ export class DataTableFiltersComponent implements AfterViewInit, OnInit {
                     return null;
                     return null;
                 },
                 },
             );
             );
+        }
+        if (filter.isNumber()) {
+            this.formControl = new FormGroup(
+                {
+                    operator: new FormControl(value?.operator ?? 'gt'),
+                    amount: new FormControl(value?.amount ?? ''),
+                },
+                control => {
+                    if (!control.value.amount) {
+                        return { noSelection: true };
+                    }
+                    return null;
+                },
+            );
         } else if (filter.isSelect()) {
         } else if (filter.isSelect()) {
             this.formControl = new FormArray(
             this.formControl = new FormArray(
-                filter.type.options.map(o => new FormControl(filter.value?.includes(o.value) ?? false)),
+                filter.type.options.map(o => new FormControl(value?.includes(o.value) ?? false)),
                 control => (control.value.some(Boolean) ? null : { noSelection: true }),
                 control => (control.value.some(Boolean) ? null : { noSelection: true }),
             );
             );
         } else if (filter.isBoolean()) {
         } else if (filter.isBoolean()) {
-            this.formControl = new FormControl(filter.value ?? false);
+            this.formControl = new FormControl(value ?? false);
         } else if (filter.isDateRange()) {
         } else if (filter.isDateRange()) {
             this.formControl = new FormGroup(
             this.formControl = new FormGroup(
                 {
                 {
-                    start: new FormControl(filter.value?.start ?? null),
-                    end: new FormControl(filter.value?.end ?? null),
+                    start: new FormControl(value?.start ?? null),
+                    end: new FormControl(value?.end ?? null),
                 },
                 },
                 control => {
                 control => {
-                    const value = control.value;
-                    if (value.start && value.end && value.start > value.end) {
+                    const val = control.value;
+                    if (val.start && val.end && val.start > val.end) {
                         return { invalidRange: true };
                         return { invalidRange: true };
                     }
                     }
-                    if (!value.start && !value.end) {
+                    if (!val.start && !val.end) {
                         return { noSelection: true };
                         return { noSelection: true };
                     }
                     }
                     return null;
                     return null;
@@ -116,13 +131,18 @@ export class DataTableFiltersComponent implements AfterViewInit, OnInit {
                 dateOperators,
                 dateOperators,
             };
             };
         }
         }
-        this.selectedFilter.setValue(value);
+        if (this.state === 'new') {
+            this.selectedFilter.activate(value);
+        } else {
+            this.filterWithValue?.updateValue(value);
+        }
         this.dropdown.toggleOpen();
         this.dropdown.toggleOpen();
     }
     }
 
 
     deactivate() {
     deactivate() {
-        if (this.filter) {
-            this.filter.clearValue();
+        if (this.filterWithValue) {
+            const index = this.filters.activeFilters.indexOf(this.filterWithValue);
+            this.filters.removeActiveFilterAtIndex(index);
         }
         }
     }
     }
 }
 }

+ 2 - 2
packages/admin-ui/src/lib/core/src/shared/pipes/locale-currency.pipe.ts

@@ -27,13 +27,13 @@ export class LocaleCurrencyPipe extends LocaleBasePipe implements PipeTransform
 
 
     transform(value: unknown, ...args: unknown[]): string | unknown {
     transform(value: unknown, ...args: unknown[]): string | unknown {
         const [currencyCode, locale] = args;
         const [currencyCode, locale] = args;
-        if (typeof value === 'number' && typeof currencyCode === 'string') {
+        if (typeof value === 'number') {
             const activeLocale = this.getActiveLocale(locale);
             const activeLocale = this.getActiveLocale(locale);
             const majorUnits = value / 100;
             const majorUnits = value / 100;
             try {
             try {
                 return new Intl.NumberFormat(activeLocale, {
                 return new Intl.NumberFormat(activeLocale, {
                     style: 'currency',
                     style: 'currency',
-                    currency: currencyCode,
+                    currency: currencyCode as any,
                 }).format(majorUnits);
                 }).format(majorUnits);
             } catch (e: any) {
             } catch (e: any) {
                 return majorUnits.toFixed(2);
                 return majorUnits.toFixed(2);

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

@@ -56,6 +56,16 @@ export class OrderListComponent
                 },
                 },
             }),
             }),
         })
         })
+        .addFilter({
+            name: 'totalWithTax',
+            type: { kind: 'number', inputType: 'currency', currencyCode: 'USD' },
+            label: _('order.total'),
+            toFilterInput: value => ({
+                totalWithTax: {
+                    [value.operator]: +value.amount,
+                },
+            }),
+        })
         .addFilter({
         .addFilter({
             name: 'state',
             name: 'state',
             type: {
             type: {

+ 5 - 0
yarn.lock

@@ -12211,6 +12211,11 @@ just-diff@^6.0.0:
   resolved "https://registry.yarnpkg.com/just-diff/-/just-diff-6.0.2.tgz#03b65908543ac0521caf6d8eb85035f7d27ea285"
   resolved "https://registry.yarnpkg.com/just-diff/-/just-diff-6.0.2.tgz#03b65908543ac0521caf6d8eb85035f7d27ea285"
   integrity sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==
   integrity sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==
 
 
+just-extend@^6.2.0:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-6.2.0.tgz#b816abfb3d67ee860482e7401564672558163947"
+  integrity sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==
+
 jwa@^2.0.0:
 jwa@^2.0.0:
   version "2.0.0"
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc"
   resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc"