Add basic filter dialog implementation

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/4/head
trivernis 3 years ago
parent 4a0a946deb
commit 6cb91bf263

@ -54,6 +54,8 @@ import { ImportTabSidebarComponent } from './pages/home/import-tab/import-tab-si
import { NativeFileSelectComponent } from './components/inputs/native-file-select/native-file-select.component';
import { FilesystemImportComponent } from './pages/home/import-tab/import-tab-sidebar/filesystem-import/filesystem-import.component';
import {MatCheckboxModule} from "@angular/material/checkbox";
import { FilterDialogComponent } from './components/file-search/filter-dialog/filter-dialog.component';
import { TagFilterListItemComponent } from './components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component';
@NgModule({
declarations: [
@ -78,6 +80,8 @@ import {MatCheckboxModule} from "@angular/material/checkbox";
ImportTabSidebarComponent,
NativeFileSelectComponent,
FilesystemImportComponent,
FilterDialogComponent,
TagFilterListItemComponent,
],
imports: [
BrowserModule,

@ -16,6 +16,9 @@
[formControl]="formControl"
[matAutocomplete]="auto"
matInput/>
<button mat-button class="filter-dialog-button" (click)="openFilterDialog()">
<mat-icon>filter_alt</mat-icon>
</button>
<mat-autocomplete #auto (optionSelected)="addSearchTagByAutocomplete($event)">
<mat-option *ngFor="let tag of suggestionTags | async" [value]="tag">
{{tag}}

@ -54,3 +54,9 @@
cursor: pointer;
white-space: nowrap;
}
.filter-dialog-button {
position: absolute;
right: 0;
top: -20px;
}

@ -23,6 +23,7 @@ import {
FilterExpression,
SingleFilterExpression
} from "../../models/FilterExpression";
import {FilterDialogComponent} from "./filter-dialog/filter-dialog.component";
@Component({
@ -70,12 +71,9 @@ export class FileSearchComponent implements AfterViewChecked, OnInit {
}
public addSearchTag(tag: string) {
if (tag.startsWith("-")) {
this.filters.push(new SingleFilterExpression(TagQuery.fromString(tag)));
tag = tag.replace(/^-/g, '');
this.filters.push(new SingleFilterExpression(new TagQuery(tag, true)));
} else {
this.filters.push(new SingleFilterExpression(new TagQuery(tag, false)));
}
if (this.filters.filter(t => t.partiallyEq(tag)).length > 1) {
const index = this.filters.findIndex(t => t.partiallyEq(tag));
this.filters.splice(index, 1);
@ -143,4 +141,22 @@ export class FileSearchComponent implements AfterViewChecked, OnInit {
.map(t => negated ? "-" + t : t)
.slice(0, 20);
}
public openFilterDialog(): void {
const filterEntries = this.filters.map(f => f.clone());
const filterDialog = this.dialog.open(FilterDialogComponent, {
minWidth: "25vw",
data: {
filterEntries,
validTags: this.validTags,
},
disableClose: true,
});
filterDialog.afterClosed().subscribe(async (filterExpression) => {
if (filterExpression !== undefined || filterExpression?.length > 0) {
this.filters = filterExpression;
await this.searchForFiles();
}
});
}
}

@ -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 {
}
}

@ -9,6 +9,10 @@ export interface FilterExpression {
partiallyEq(value: any): boolean;
getDisplayName(): string;
clone(): FilterExpression;
queryList(): TagQuery[];
}
export class OrFilterExpression implements FilterExpression{
@ -30,6 +34,15 @@ export class OrFilterExpression implements FilterExpression{
public getDisplayName(): string {
return this.filter.map(t => t.getNormalizedTag()).join(" OR ");
}
public clone(): OrFilterExpression {
let tags = this.filter.map((t: TagQuery) => new TagQuery(t.tag, t.negate));
return new OrFilterExpression(tags)
}
public queryList(): TagQuery[] {
return this.filter;
}
}
export class SingleFilterExpression implements FilterExpression {
@ -51,4 +64,12 @@ export class SingleFilterExpression implements FilterExpression {
public getDisplayName(): string {
return this.filter.getNormalizedTag();
}
public clone(): FilterExpression {
return new SingleFilterExpression(new TagQuery(this.filter.tag, this.filter.negate))
}
public queryList(): TagQuery[] {
return [this.filter]
}
}

@ -1,7 +1,17 @@
import {SingleFilterExpression} from "./FilterExpression";
export class TagQuery {
constructor(public tag: string, public negate: boolean) {
}
public static fromString(tag: string): TagQuery {
if (tag.startsWith("-")) {
return new TagQuery(tag.replace(/^-/g, ''), true);
} else {
return new TagQuery(tag, false);
}
}
public getNormalizedTag(): string {
return this.negate ? "-" + this.tag : this.tag;
}

Loading…
Cancel
Save