src/app/graphs/label-search/label-search.component.ts
This component is responsible for the display of the search bar above the graph. It allows for filtering issues by multiple labels and text fragments.
selector | app-label-search |
styleUrls | ./label-search.component.scss |
templateUrl | ./label-search.component.html |
Properties |
Methods |
|
constructor(labelStore: LabelStoreService, ss: StateService)
|
|||||||||
Parameters :
|
emitSelectedLabels |
emitSelectedLabels()
|
Emit value representing label and text fragments in the search bar via this.filterSelection$
Returns :
void
|
Private loadLabels |
loadLabels()
|
Load all labels from backend that match the currently typed in ng-select element
Returns :
void
|
trackByFn | ||||||
trackByFn(item: FilterLabel)
|
||||||
Parameters :
Returns :
string
|
Public filterSelection$ |
Default value : new BehaviorSubject<FilterSelection>({
labels: [],
texts: []
})
|
labels$ |
Type : Observable<FilterLabel[]>
|
labelsInput$ |
Default value : new Subject<string>()
|
labelsLoading |
Default value : false
|
selectedLabels |
Type : FilterElement[]
|
Default value : []
|
import {Component, OnInit} from '@angular/core';
import {concat, of, Subject, Observable, BehaviorSubject} from 'rxjs';
import {catchError, distinctUntilChanged, switchMap, tap} from 'rxjs/operators';
import {FilterLabel, isFilterLabel, LabelStoreService} from '../../data/label/label-store.service';
import {StateService} from '../../state.service';
/**
* This component is responsible for the display of the search bar above the graph.
* It allows for filtering issues by multiple labels and text fragments.
*/
@Component({
selector: 'app-label-search',
templateUrl: './label-search.component.html',
styleUrls: ['./label-search.component.scss']
})
export class LabelSearchComponent implements OnInit {
public filterSelection$ = new BehaviorSubject<FilterSelection>({
labels: [],
texts: []
});
labels$: Observable<FilterLabel[]>;
labelsLoading = false;
labelsInput$ = new Subject<string>();
selectedLabels: FilterElement[] = [];
constructor(private labelStore: LabelStoreService, private ss: StateService) {}
ngOnInit() {
this.loadLabels();
}
trackByFn(item: FilterLabel): string {
return item.id;
}
/**
* Emit value representing label and text fragments in the search bar via this.filterSelection$
*/
emitSelectedLabels(): void {
const selection: FilterSelection = {texts: [], labels: []};
// find out which elements in search bar correspond to an existing label on the backend and which to a text fragment
selection.texts = this.selectedLabels.filter((item) => !isFilterLabel(item)).map((item) => item.name);
selection.labels = this.selectedLabels.filter((label) => isFilterLabel(label)) as FilterLabel[];
this.filterSelection$.next(selection);
}
/**
* Load all labels from backend that match the currently typed in ng-select element
*/
private loadLabels() {
this.labels$ = concat(
of([]), // default items
this.labelsInput$.pipe(
distinctUntilChanged(),
tap(() => (this.labelsLoading = true)),
switchMap((term) =>
this.labelStore.getMatchingLabels(this.ss.state.project.node.id, term).pipe(
catchError(() => of([])), // empty list on error
tap(() => (this.labelsLoading = false))
)
)
)
);
}
}
/**
* The bar can contain elements standing for labels and elements for text fragments.
*/
type FilterElement = TextFragment | FilterLabel;
interface TextFragment {
name: string;
}
export interface FilterSelection {
texts: string[];
labels: FilterLabel[];
}
<ng-select
class="label-select"
[items]="labels$ | async"
bindLabel="name"
[addTag]="true"
[multiple]="true"
[hideSelected]="true"
[trackByFn]="trackByFn"
[loading]="labelsLoading"
typeToSearchText="Search by label or text"
addTagText="Search text in issue title & body"
[typeahead]="labelsInput$"
[(ngModel)]="selectedLabels"
ngDefaultControl
(change)="emitSelectedLabels()"
>
</ng-select>
./label-search.component.scss
.ng-select.label-select ::ng-deep .ng-select-container {
min-height: 0;
}
.ng-select.label-select ::ng-deep .ng-value-container {
border-top: 7px solid transparent;
}