Add basic filter dialog implementation
Signed-off-by: trivernis <trivernis@protonmail.com>pull/4/head
parent
4a0a946deb
commit
6cb91bf263
@ -0,0 +1,22 @@
|
|||||||
|
<h1 mat-dialog-title>Filters</h1>
|
||||||
|
<div mat-dialog-content>
|
||||||
|
<mat-list>
|
||||||
|
<mat-list-item class="filter-list-item" *ngFor="let expression of filters">
|
||||||
|
<app-tag-filter-list-item [expression]="expression"></app-tag-filter-list-item>
|
||||||
|
</mat-list-item>
|
||||||
|
</mat-list>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<mat-form-field class="tag-input">
|
||||||
|
<mat-label>Enter tags to filter for</mat-label>
|
||||||
|
<input #tagInput matInput [formControl]="formControl" [matAutocomplete]="auto" (keydown.enter)="this.addFilterByInput()">
|
||||||
|
<mat-autocomplete #auto (optionSelected)="addFilterByAutocomplete($event)">
|
||||||
|
<mat-option *ngFor="let tag of suggestionTags | async" [value]="tag">
|
||||||
|
{{tag}}
|
||||||
|
</mat-option>
|
||||||
|
</mat-autocomplete>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-actions" mat-dialog-actions>
|
||||||
|
<button mat-flat-button color="primary" (click)="confirmFilter()">Filter</button>
|
||||||
|
<button mat-stroked-button color="accent" (click)="cancelFilter()">Cancel</button>
|
||||||
|
</div>
|
@ -0,0 +1,22 @@
|
|||||||
|
.dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-list-item.filter-list-item {
|
||||||
|
height: 100%;
|
||||||
|
padding: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
app-tag-filter-list-item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FilterDialogComponent } from './filter-dialog.component';
|
||||||
|
|
||||||
|
describe('FilterDialogComponent', () => {
|
||||||
|
let component: FilterDialogComponent;
|
||||||
|
let fixture: ComponentFixture<FilterDialogComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ FilterDialogComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(FilterDialogComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,108 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
HostListener,
|
||||||
|
Inject,
|
||||||
|
ViewChild
|
||||||
|
} from '@angular/core';
|
||||||
|
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||||
|
import {SortDialogComponent} from "../sort-dialog/sort-dialog.component";
|
||||||
|
import {
|
||||||
|
FilterExpression, OrFilterExpression,
|
||||||
|
SingleFilterExpression
|
||||||
|
} from "../../../models/FilterExpression";
|
||||||
|
import {Observable} from "rxjs";
|
||||||
|
import {FormControl} from "@angular/forms";
|
||||||
|
import {last, map, startWith} from "rxjs/operators";
|
||||||
|
import {MatAutocompleteSelectedEvent} from "@angular/material/autocomplete";
|
||||||
|
import {TagQuery} from "../../../models/TagQuery";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-filter-dialog',
|
||||||
|
templateUrl: './filter-dialog.component.html',
|
||||||
|
styleUrls: ['./filter-dialog.component.scss']
|
||||||
|
})
|
||||||
|
export class FilterDialogComponent {
|
||||||
|
|
||||||
|
public filters: FilterExpression[];
|
||||||
|
public suggestionTags: Observable<string[]>;
|
||||||
|
public validTags: string[] = [];
|
||||||
|
public formControl = new FormControl();
|
||||||
|
public mode: "AND" | "OR" = "AND";
|
||||||
|
@ViewChild("tagInput") tagInput!: ElementRef<HTMLInputElement>;
|
||||||
|
|
||||||
|
constructor(public dialogRef: MatDialogRef<SortDialogComponent>, @Inject(
|
||||||
|
MAT_DIALOG_DATA) data: any) {
|
||||||
|
this.filters = data.filterEntries;
|
||||||
|
this.validTags = data.validTags;
|
||||||
|
|
||||||
|
this.suggestionTags = this.formControl.valueChanges.pipe(startWith(null),
|
||||||
|
map(
|
||||||
|
(tag: string | null) => tag ? this.filterSuggestionTag(
|
||||||
|
tag) : this.validTags.slice(0, 20)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public cancelFilter(): void {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public confirmFilter(): void {
|
||||||
|
this.dialogRef.close(this.filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
private filterSuggestionTag(tag: string) {
|
||||||
|
const negated = tag.startsWith("-");
|
||||||
|
const normalizedTag = tag.replace(/^-/, "");
|
||||||
|
|
||||||
|
return this.validTags.filter(
|
||||||
|
t => t.includes(normalizedTag) && this.filters.findIndex(
|
||||||
|
f => f.eq(t)) < 0)
|
||||||
|
.map(t => negated ? "-" + t : t)
|
||||||
|
.slice(0, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
public addFilterByAutocomplete(event: MatAutocompleteSelectedEvent): void {
|
||||||
|
this.addFilter(event.option.value);
|
||||||
|
this.formControl.setValue(null);
|
||||||
|
this.tagInput.nativeElement.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public addFilterByInput(): void {
|
||||||
|
this.addFilter(this.formControl.value);
|
||||||
|
this.formControl.setValue(null);
|
||||||
|
this.tagInput.nativeElement.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public addFilter(tag: string) {
|
||||||
|
const query = TagQuery.fromString(tag);
|
||||||
|
|
||||||
|
if (this.mode === "AND") {
|
||||||
|
this.filters.push(new SingleFilterExpression(query));
|
||||||
|
tag = tag.replace(/^-/g, '');
|
||||||
|
|
||||||
|
if (this.filters.filter(t => t.partiallyEq(tag)).length > 1) {
|
||||||
|
const index = this.filters.findIndex(t => t.partiallyEq(tag));
|
||||||
|
this.filters.splice(index, 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let queryList = this.filters.pop()?.queryList() ?? [];
|
||||||
|
|
||||||
|
queryList.push(query);
|
||||||
|
this.filters.push(new OrFilterExpression(queryList));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener("window:keydown", ["$event"])
|
||||||
|
private async handleKeydownEvent(event: KeyboardEvent) {
|
||||||
|
if (event.key === "Shift") {
|
||||||
|
this.mode = "OR";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener("window:keyup", ["$event"])
|
||||||
|
private async handleKeyupEvent(event: KeyboardEvent) {
|
||||||
|
if (event.key === "Shift") {
|
||||||
|
this.mode = "AND";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
<span *ngIf="expression.filter_type === 'Query'">
|
||||||
|
{{expression.getDisplayName()}}
|
||||||
|
<button mat-button class="remove-button">
|
||||||
|
<mat-icon>remove</mat-icon>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<div *ngIf="expression.filter_type === 'OrExpression'">
|
||||||
|
<mat-list>
|
||||||
|
<mat-list-item class="or-filter-list-item" *ngFor="let query of expression.queryList()">
|
||||||
|
{{query.getNormalizedTag()}}
|
||||||
|
<button mat-button class="remove-button">
|
||||||
|
<mat-icon>remove</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-list-item>
|
||||||
|
</mat-list>
|
||||||
|
</div>
|
@ -0,0 +1,16 @@
|
|||||||
|
.remove-button {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(0.5em - 15px);
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-list {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-list-item.or-filter-list-item {
|
||||||
|
padding: 0.5em 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { TagFilterListItemComponent } from './tag-filter-list-item.component';
|
||||||
|
|
||||||
|
describe('TagFilterListItemComponent', () => {
|
||||||
|
let component: TagFilterListItemComponent;
|
||||||
|
let fixture: ComponentFixture<TagFilterListItemComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ TagFilterListItemComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(TagFilterListItemComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,18 @@
|
|||||||
|
import {Component, Input, OnInit} from '@angular/core';
|
||||||
|
import {FilterExpression} from "../../../../models/FilterExpression";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-tag-filter-list-item',
|
||||||
|
templateUrl: './tag-filter-list-item.component.html',
|
||||||
|
styleUrls: ['./tag-filter-list-item.component.scss']
|
||||||
|
})
|
||||||
|
export class TagFilterListItemComponent {
|
||||||
|
|
||||||
|
@Input() expression!: FilterExpression;
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue