|
|
@@ -1,82 +1,197 @@
|
|
|
import { ActivatedRoute, Router } from '@angular/router';
|
|
|
+import { assertNever } from '@vendure/common/lib/shared-utils';
|
|
|
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>> {
|
|
|
- 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) {}
|
|
|
|
|
|
get length(): number {
|
|
|
- return this.filters.length;
|
|
|
+ return this.#filters.length;
|
|
|
+ }
|
|
|
+
|
|
|
+ get activeFilters(): FilterWithValue[] {
|
|
|
+ return this.#activeFilters;
|
|
|
}
|
|
|
|
|
|
addFilter<FilterType extends DataTableFilterType>(
|
|
|
- config: ConstructorParameters<typeof DataTableFilter<FilterInput, FilterType>>[0],
|
|
|
+ config: DataTableFilterOptions<FilterInput, FilterType>,
|
|
|
): DataTableFilterCollection<FilterInput> {
|
|
|
- if (this.connectedToRouter) {
|
|
|
+ if (this.#connectedToRouter) {
|
|
|
throw new Error(
|
|
|
'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;
|
|
|
}
|
|
|
|
|
|
- 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>> {
|
|
|
- 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 {
|
|
|
- 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) {
|
|
|
this.valueChanges.subscribe(value => {
|
|
|
this.router.navigate(['./'], {
|
|
|
- queryParams: { [this.filtersQueryParamName]: this.serialize() },
|
|
|
+ queryParams: { [this.#filtersQueryParamName]: this.serialize() },
|
|
|
relativeTo: route,
|
|
|
queryParamsHandling: 'merge',
|
|
|
});
|
|
|
});
|
|
|
- const filterQueryParams = (route.snapshot.queryParamMap.get(this.filtersQueryParamName) ?? '')
|
|
|
+ const filterQueryParams = (route.snapshot.queryParamMap.get(this.#filtersQueryParamName) ?? '')
|
|
|
.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) {
|
|
|
- filter.deserializeValue(value);
|
|
|
+ const val = this.deserializeValue(filter, value);
|
|
|
+ this.#activeFilters.push(this.createFacetWithValue(filter, val));
|
|
|
}
|
|
|
}
|
|
|
- this.connectedToRouter = true;
|
|
|
+ this.#connectedToRouter = true;
|
|
|
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 {
|
|
|
- return this.getActiveFilters()
|
|
|
- .map(f => `${f.name}:${f.serializeValue()}`)
|
|
|
+ return this.#activeFilters
|
|
|
+ .map(
|
|
|
+ (filterWithValue, i) =>
|
|
|
+ `${filterWithValue.filter.name}:${this.serializeValue(filterWithValue)}`,
|
|
|
+ )
|
|
|
.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));
|
|
|
}
|
|
|
}
|