1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-28 12:45:45 +01:00

[SM-497] Add filtering capabilities to the table datasource (#4717)

* Add filtering capabilities to the table datasource
This commit is contained in:
Oscar Hinton 2023-02-14 11:38:53 +01:00 committed by GitHub
parent c3dfb7735f
commit 42ba475c37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 104 additions and 7 deletions

View File

@ -24,6 +24,11 @@ export class ProjectsListComponent implements OnDestroy {
}
private _projects: ProjectListView[];
@Input()
set search(search: string) {
this.dataSource.filter = search;
}
@Output() editProjectEvent = new EventEmitter<string>();
@Output() deleteProjectEvent = new EventEmitter<ProjectListView[]>();
@Output() onProjectCheckedEvent = new EventEmitter<string[]>();

View File

@ -1,5 +1,5 @@
<sm-header>
<input bitInput [placeholder]="'searchProjects' | i18n" />
<input bitInput [placeholder]="'searchProjects' | i18n" [(ngModel)]="search" />
<sm-new-menu></sm-new-menu>
</sm-header>
<sm-projects-list
@ -7,5 +7,6 @@
(editProjectEvent)="openEditProject($event)"
(deleteProjectEvent)="openDeleteProjectDialog($event)"
[projects]="projects$ | async"
[search]="search"
>
</sm-projects-list>

View File

@ -21,7 +21,8 @@ import { ProjectService } from "../project.service";
templateUrl: "./projects.component.html",
})
export class ProjectsComponent implements OnInit {
projects$: Observable<ProjectListView[]>;
protected projects$: Observable<ProjectListView[]>;
protected search: string;
private organizationId: string;

View File

@ -15,6 +15,7 @@ export type Sort = {
export class TableDataSource<T> extends DataSource<T> {
private readonly _data: BehaviorSubject<T[]>;
private readonly _sort: BehaviorSubject<Sort>;
private readonly _filter = new BehaviorSubject<string>("");
private readonly _renderData = new BehaviorSubject<T[]>([]);
private _renderChangesSubscription: Subscription | null = null;
@ -41,6 +42,14 @@ export class TableDataSource<T> extends DataSource<T> {
return this._sort.value;
}
get filter() {
return this._filter.value;
}
set filter(filter: string) {
this._filter.next(filter);
}
connect(): Observable<readonly T[]> {
if (!this._renderChangesSubscription) {
this.updateChangeSubscription();
@ -55,20 +64,32 @@ export class TableDataSource<T> extends DataSource<T> {
}
private updateChangeSubscription() {
const orderedData = combineLatest([this._data, this._sort]).pipe(
map(([data]) => this.orderData(data))
const filteredData = combineLatest([this._data, this._filter]).pipe(
map(([data, filter]) => this.filterData(data, filter))
);
const orderedData = combineLatest([filteredData, this._sort]).pipe(
map(([data, sort]) => this.orderData(data, sort))
);
this._renderChangesSubscription?.unsubscribe();
this._renderChangesSubscription = orderedData.subscribe((data) => this._renderData.next(data));
}
private orderData(data: T[]): T[] {
if (!this.sort) {
private filterData(data: T[], filter: string): T[] {
if (filter == null || filter == "") {
return data;
}
return this.sortData(data, this.sort);
return data.filter((obj) => this.filterPredicate(obj, filter));
}
private orderData(data: T[], sort: Sort): T[] {
if (!sort) {
return data;
}
return this.sortData(data, sort);
}
/**
@ -161,4 +182,38 @@ export class TableDataSource<T> extends DataSource<T> {
return comparatorResult * (direction === "asc" ? 1 : -1);
});
}
/**
* Copied from https://github.com/angular/components/blob/main/src/material/table/table-data-source.ts
* License: MIT
* Copyright (c) 2022 Google LLC.
*
* Checks if a data object matches the data source's filter string. By default, each data object
* is converted to a string of its properties and returns true if the filter has
* at least one occurrence in that string. By default, the filter string has its whitespace
* trimmed and the match is case-insensitive. May be overridden for a custom implementation of
* filter matching.
* @param data Data object used to check against the filter.
* @param filter Filter string that has been set on the data source.
* @returns Whether the filter matches against the data
*/
protected filterPredicate(data: T, filter: string): boolean {
// Transform the data into a lowercase string of all property values.
const dataStr = Object.keys(data as unknown as Record<string, any>)
.reduce((currentTerm: string, key: string) => {
// Use an obscure Unicode character to delimit the words in the concatenated string.
// This avoids matches where the values of two columns combined will match the user's query
// (e.g. `Flute` and `Stop` will match `Test`). The character is intended to be something
// that has a very low chance of being typed in by somebody in a text field. This one in
// particular is "White up-pointing triangle with dot" from
// https://en.wikipedia.org/wiki/List_of_Unicode_characters
return currentTerm + (data as unknown as Record<string, any>)[key] + "◬";
}, "")
.toLowerCase();
// Transform the filter by converting it to lowercase and removing whitespace.
const transformedFilter = filter.trim().toLowerCase();
return dataStr.indexOf(transformedFilter) != -1;
}
}

View File

@ -1,6 +1,8 @@
import { ScrollingModule } from "@angular/cdk/scrolling";
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { countries } from "../form/countries";
import { TableDataSource } from "./table-data-source";
import { TableModule } from "./table.module";
@ -133,3 +135,36 @@ const ScrollableTemplate: Story = (args) => ({
});
export const Scrollable = ScrollableTemplate.bind({});
const data3 = new TableDataSource<{ value: string; name: string }>();
// Chromatic has a max page size, lowering the number of entries to ensure we don't hit it
data3.data = countries.slice(0, 100);
const FilterableTemplate: Story = (args) => ({
props: {
dataSource: data3,
sortFn: (a: any, b: any) => a.id - b.id,
},
template: `
<input type="search" placeholder="Search" (input)="dataSource.filter = $event.target.value" />
<cdk-virtual-scroll-viewport scrollWindow itemSize="47">
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr>
<th bitCell bitSortable="name" default>Name</th>
<th bitCell bitSortable="value" width="120px">Value</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *cdkVirtualFor="let r of rows$">
<td bitCell>{{ r.name }}</td>
<td bitCell>{{ r.value }}</td>
</tr>
</ng-template>
</bit-table>
</cdk-virtual-scroll-viewport>
`,
});
export const Filterable = FilterableTemplate.bind({});