From 6cb91bf26361b446a188798e38b93146b2678aac Mon Sep 17 00:00:00 2001 From: trivernis Date: Thu, 18 Nov 2021 21:12:54 +0100 Subject: [PATCH 1/6] Add basic filter dialog implementation Signed-off-by: trivernis --- mediarepo-ui/src/app/app.module.ts | 4 + .../file-search/file-search.component.html | 3 + .../file-search/file-search.component.scss | 6 + .../file-search/file-search.component.ts | 28 ++++- .../filter-dialog.component.html | 22 ++++ .../filter-dialog.component.scss | 22 ++++ .../filter-dialog.component.spec.ts | 25 ++++ .../filter-dialog/filter-dialog.component.ts | 108 ++++++++++++++++++ .../tag-filter-list-item.component.html | 16 +++ .../tag-filter-list-item.component.scss | 16 +++ .../tag-filter-list-item.component.spec.ts | 25 ++++ .../tag-filter-list-item.component.ts | 18 +++ .../src/app/models/FilterExpression.ts | 21 ++++ mediarepo-ui/src/app/models/TagQuery.ts | 10 ++ 14 files changed, 318 insertions(+), 6 deletions(-) create mode 100644 mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html create mode 100644 mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.scss create mode 100644 mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.spec.ts create mode 100644 mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.ts create mode 100644 mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.html create mode 100644 mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.scss create mode 100644 mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.spec.ts create mode 100644 mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.ts diff --git a/mediarepo-ui/src/app/app.module.ts b/mediarepo-ui/src/app/app.module.ts index ffaf063..ea345f8 100644 --- a/mediarepo-ui/src/app/app.module.ts +++ b/mediarepo-ui/src/app/app.module.ts @@ -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, diff --git a/mediarepo-ui/src/app/components/file-search/file-search.component.html b/mediarepo-ui/src/app/components/file-search/file-search.component.html index 63ce7e1..cfd3ae7 100644 --- a/mediarepo-ui/src/app/components/file-search/file-search.component.html +++ b/mediarepo-ui/src/app/components/file-search/file-search.component.html @@ -16,6 +16,9 @@ [formControl]="formControl" [matAutocomplete]="auto" matInput/> + {{tag}} diff --git a/mediarepo-ui/src/app/components/file-search/file-search.component.scss b/mediarepo-ui/src/app/components/file-search/file-search.component.scss index 15a087f..81993c7 100644 --- a/mediarepo-ui/src/app/components/file-search/file-search.component.scss +++ b/mediarepo-ui/src/app/components/file-search/file-search.component.scss @@ -54,3 +54,9 @@ cursor: pointer; white-space: nowrap; } + +.filter-dialog-button { + position: absolute; + right: 0; + top: -20px; +} diff --git a/mediarepo-ui/src/app/components/file-search/file-search.component.ts b/mediarepo-ui/src/app/components/file-search/file-search.component.ts index 2e9c991..7d77655 100644 --- a/mediarepo-ui/src/app/components/file-search/file-search.component.ts +++ b/mediarepo-ui/src/app/components/file-search/file-search.component.ts @@ -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("-")) { - tag = tag.replace(/^-/g, ''); - this.filters.push(new SingleFilterExpression(new TagQuery(tag, true))); - } else { - this.filters.push(new SingleFilterExpression(new TagQuery(tag, false))); - } + this.filters.push(new SingleFilterExpression(TagQuery.fromString(tag))); + 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); @@ -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(); + } + }); + } } diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html new file mode 100644 index 0000000..6b734e8 --- /dev/null +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html @@ -0,0 +1,22 @@ +

Filters

+
+ + + + + + + + Enter tags to filter for + + + + {{tag}} + + + +
+
+ + +
diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.scss b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.scss new file mode 100644 index 0000000..49c51ef --- /dev/null +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.scss @@ -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%; +} diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.spec.ts b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.spec.ts new file mode 100644 index 0000000..7c3bedb --- /dev/null +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FilterDialogComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FilterDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.ts b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.ts new file mode 100644 index 0000000..8a10146 --- /dev/null +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.ts @@ -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; + public validTags: string[] = []; + public formControl = new FormControl(); + public mode: "AND" | "OR" = "AND"; + @ViewChild("tagInput") tagInput!: ElementRef; + + constructor(public dialogRef: MatDialogRef, @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"; + } + } +} diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.html b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.html new file mode 100644 index 0000000..2682840 --- /dev/null +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.html @@ -0,0 +1,16 @@ + + {{expression.getDisplayName()}} + + +
+ + + {{query.getNormalizedTag()}} + + + +
diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.scss b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.scss new file mode 100644 index 0000000..4e0e0a9 --- /dev/null +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.scss @@ -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%; +} diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.spec.ts b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.spec.ts new file mode 100644 index 0000000..c6a054a --- /dev/null +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ TagFilterListItemComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TagFilterListItemComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.ts b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.ts new file mode 100644 index 0000000..7997d47 --- /dev/null +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.ts @@ -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 { + } + +} diff --git a/mediarepo-ui/src/app/models/FilterExpression.ts b/mediarepo-ui/src/app/models/FilterExpression.ts index 33ac090..391877d 100644 --- a/mediarepo-ui/src/app/models/FilterExpression.ts +++ b/mediarepo-ui/src/app/models/FilterExpression.ts @@ -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] + } } diff --git a/mediarepo-ui/src/app/models/TagQuery.ts b/mediarepo-ui/src/app/models/TagQuery.ts index f10e907..493f5c7 100644 --- a/mediarepo-ui/src/app/models/TagQuery.ts +++ b/mediarepo-ui/src/app/models/TagQuery.ts @@ -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; } From 05c2aa3507794ddae440277af8ac392a4f620005 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sat, 20 Nov 2021 12:26:56 +0100 Subject: [PATCH 2/6] Improve filter dialog style Signed-off-by: trivernis --- mediarepo-ui/src/app/app.module.ts | 2 + .../file-search/file-search.component.ts | 4 +- .../filter-dialog.component.html | 13 +- .../filter-dialog/filter-dialog.component.ts | 64 +++------- .../tag-filter-list-item.component.html | 15 +-- .../tag-filter-list-item.component.scss | 21 +++- .../tag-filter-list-item.component.ts | 33 ++++- .../inputs/tag-input/tag-input.component.html | 15 +++ .../inputs/tag-input/tag-input.component.scss | 3 + .../tag-input/tag-input.component.spec.ts | 25 ++++ .../inputs/tag-input/tag-input.component.ts | 114 ++++++++++++++++++ .../src/app/models/FilterExpression.ts | 4 + .../files-tab-sidebar.component.html | 3 +- 13 files changed, 247 insertions(+), 69 deletions(-) create mode 100644 mediarepo-ui/src/app/components/inputs/tag-input/tag-input.component.html create mode 100644 mediarepo-ui/src/app/components/inputs/tag-input/tag-input.component.scss create mode 100644 mediarepo-ui/src/app/components/inputs/tag-input/tag-input.component.spec.ts create mode 100644 mediarepo-ui/src/app/components/inputs/tag-input/tag-input.component.ts diff --git a/mediarepo-ui/src/app/app.module.ts b/mediarepo-ui/src/app/app.module.ts index ea345f8..f8a82d4 100644 --- a/mediarepo-ui/src/app/app.module.ts +++ b/mediarepo-ui/src/app/app.module.ts @@ -56,6 +56,7 @@ import { FilesystemImportComponent } from './pages/home/import-tab/import-tab-si 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'; +import { TagInputComponent } from './components/inputs/tag-input/tag-input.component'; @NgModule({ declarations: [ @@ -82,6 +83,7 @@ import { TagFilterListItemComponent } from './components/file-search/filter-dial FilesystemImportComponent, FilterDialogComponent, TagFilterListItemComponent, + TagInputComponent, ], imports: [ BrowserModule, diff --git a/mediarepo-ui/src/app/components/file-search/file-search.component.ts b/mediarepo-ui/src/app/components/file-search/file-search.component.ts index 7d77655..2f30d31 100644 --- a/mediarepo-ui/src/app/components/file-search/file-search.component.ts +++ b/mediarepo-ui/src/app/components/file-search/file-search.component.ts @@ -24,6 +24,7 @@ import { SingleFilterExpression } from "../../models/FilterExpression"; import {FilterDialogComponent} from "./filter-dialog/filter-dialog.component"; +import {Tag} from "../../models/Tag"; @Component({ @@ -38,6 +39,7 @@ export class FileSearchComponent implements AfterViewChecked, OnInit { public filters: FilterExpression[] = []; public suggestionTags: Observable; + @Input() availableTags: Tag[] = []; @Input() validTags: string[] = []; @Output() searchStartEvent = new EventEmitter(); @Output() searchEndEvent = new EventEmitter(); @@ -148,7 +150,7 @@ export class FileSearchComponent implements AfterViewChecked, OnInit { minWidth: "25vw", data: { filterEntries, - validTags: this.validTags, + availableTags: this.availableTags, }, disableClose: true, }); diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html index 6b734e8..ce83f9a 100644 --- a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html @@ -2,19 +2,12 @@
- + - - Enter tags to filter for - - - - {{tag}} - - - +
diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.ts b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.ts index 8a10146..fdea7c4 100644 --- a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.ts +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.ts @@ -11,11 +11,10 @@ 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"; +import {Tag} from "../../../models/Tag"; +import {TagFilterListItemComponent} from "./tag-filter-list-item/tag-filter-list-item.component"; +import {Selectable} from "../../../models/Selectable"; @Component({ selector: 'app-filter-dialog', @@ -24,22 +23,14 @@ import {TagQuery} from "../../../models/TagQuery"; }) export class FilterDialogComponent { - public filters: FilterExpression[]; - public suggestionTags: Observable; - public validTags: string[] = []; - public formControl = new FormControl(); + public filters: Selectable[]; + public availableTags: Tag[] = []; public mode: "AND" | "OR" = "AND"; - @ViewChild("tagInput") tagInput!: ElementRef; constructor(public dialogRef: MatDialogRef, @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))); + this.filters = data.filterEntries.map((f: FilterExpression) => new Selectable(f, false)) ?? []; + this.availableTags = data.availableTags ?? []; } public cancelFilter(): void { @@ -47,48 +38,33 @@ export class FilterDialogComponent { } 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); + this.dialogRef.close(this.filters.map(f => f.data)); } - 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 removeFilter(event: TagFilterListItemComponent): void { + const filter = event.expression; + const index = this.filters.findIndex(f => f === filter); + if (index >= 0) { + this.filters.splice(index, 1); + } } public addFilter(tag: string) { const query = TagQuery.fromString(tag); - if (this.mode === "AND") { - this.filters.push(new SingleFilterExpression(query)); + if (this.mode === "AND" || this.filters.length === 0) { + this.filters.push(new Selectable(new SingleFilterExpression(query), false)); tag = tag.replace(/^-/g, ''); - if (this.filters.filter(t => t.partiallyEq(tag)).length > 1) { - const index = this.filters.findIndex(t => t.partiallyEq(tag)); + if (this.filters.filter(t => t.data.partiallyEq(tag)).length > 1) { + const index = this.filters.findIndex(t => t.data.partiallyEq(tag)); this.filters.splice(index, 1); } } else { - let queryList = this.filters.pop()?.queryList() ?? []; + let queryList = this.filters.pop()?.data.queryList() ?? []; queryList.push(query); - this.filters.push(new OrFilterExpression(queryList)); + this.filters.push(new Selectable(new OrFilterExpression(queryList), false)); } } diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.html b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.html index 2682840..8b573d9 100644 --- a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.html +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.html @@ -1,14 +1,15 @@ - - {{expression.getDisplayName()}} - -
+
- - {{query.getNormalizedTag()}} - diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.scss b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.scss index 4e0e0a9..e64d496 100644 --- a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.scss +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.scss @@ -1,16 +1,33 @@ -.remove-button { +.remove-button, .remove-button-inner-list { position: absolute; - top: calc(0.5em - 15px); right: 0; + z-index: 999; + top: calc(0.5em - 15px); +} + +.remove-button { + right: 16px; } mat-list { height: 100%; width: 100%; + display: block; + background-color: #353535; + border-radius: 0.25em; } mat-list-item.or-filter-list-item { padding: 0.5em 0; height: 100%; width: 100%; + + ::ng-deep .mat-list-item-content { + padding-right: 0; + margin-right: 0; + } +} + +.or-span { + margin-right: 0.5em; } diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.ts b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.ts index 7997d47..abe1391 100644 --- a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.ts +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.ts @@ -1,5 +1,18 @@ -import {Component, Input, OnInit} from '@angular/core'; -import {FilterExpression} from "../../../../models/FilterExpression"; +import { + ChangeDetectorRef, + Component, + EventEmitter, + Inject, + Input, + OnInit, + Output +} from '@angular/core'; +import { + FilterExpression, + OrFilterExpression, SingleFilterExpression +} from "../../../../models/FilterExpression"; +import {TagQuery} from "../../../../models/TagQuery"; +import {Selectable} from "../../../../models/Selectable"; @Component({ selector: 'app-tag-filter-list-item', @@ -8,11 +21,23 @@ import {FilterExpression} from "../../../../models/FilterExpression"; }) export class TagFilterListItemComponent { - @Input() expression!: FilterExpression; + @Input() expression!: Selectable; + @Output() removeClicked = new EventEmitter(); constructor() { } - ngOnInit(): void { + public enumerate(items: T[]): [number, T][] { + return items.map((value, index) => [index, value]); } + public removeOrExpression(index: number) { + const expression = this.expression.data as OrFilterExpression; + expression.removeQueryEntry(index); + + if (expression.filter.length == 0) { + this.removeClicked.emit(this); + } else if (expression.filter.length == 1) { + this.expression.data = new SingleFilterExpression(expression.filter[0]); + } + } } diff --git a/mediarepo-ui/src/app/components/inputs/tag-input/tag-input.component.html b/mediarepo-ui/src/app/components/inputs/tag-input/tag-input.component.html new file mode 100644 index 0000000..b1b8997 --- /dev/null +++ b/mediarepo-ui/src/app/components/inputs/tag-input/tag-input.component.html @@ -0,0 +1,15 @@ + + + Enter a tag + + + + + {{tag}} + + + diff --git a/mediarepo-ui/src/app/components/inputs/tag-input/tag-input.component.scss b/mediarepo-ui/src/app/components/inputs/tag-input/tag-input.component.scss new file mode 100644 index 0000000..c7acb4b --- /dev/null +++ b/mediarepo-ui/src/app/components/inputs/tag-input/tag-input.component.scss @@ -0,0 +1,3 @@ +mat-form-field { + width: 100%; +} diff --git a/mediarepo-ui/src/app/components/inputs/tag-input/tag-input.component.spec.ts b/mediarepo-ui/src/app/components/inputs/tag-input/tag-input.component.spec.ts new file mode 100644 index 0000000..b94bb54 --- /dev/null +++ b/mediarepo-ui/src/app/components/inputs/tag-input/tag-input.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TagInputComponent } from './tag-input.component'; + +describe('TagInputComponent', () => { + let component: TagInputComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ TagInputComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TagInputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/mediarepo-ui/src/app/components/inputs/tag-input/tag-input.component.ts b/mediarepo-ui/src/app/components/inputs/tag-input/tag-input.component.ts new file mode 100644 index 0000000..178f138 --- /dev/null +++ b/mediarepo-ui/src/app/components/inputs/tag-input/tag-input.component.ts @@ -0,0 +1,114 @@ +import { + Component, ElementRef, + EventEmitter, + Input, OnChanges, + OnInit, + Output, SimpleChanges, + ViewChild +} from '@angular/core'; +import {Tag} from "../../../models/Tag"; +import {FormControl} from "@angular/forms"; +import {MatAutocompleteSelectedEvent} from "@angular/material/autocomplete"; +import {Observable} from "rxjs"; +import {debounceTime, delay, map, startWith} from "rxjs/operators"; + +@Component({ + selector: 'app-tag-input', + templateUrl: './tag-input.component.html', + styleUrls: ['./tag-input.component.scss'] +}) +export class TagInputComponent implements OnChanges{ + + @Input() availableTags: Tag[] = []; + @Input() allowNegation: boolean = false; + @Input() allowInvalid: boolean = false; + @Output() tagAdded = new EventEmitter(); + + @ViewChild("tagInput") tagInput!: ElementRef; + public formControl = new FormControl(); + public autosuggestTags: Observable; + private tagsForAutocomplete: string[] = []; + + constructor() { + this.tagsForAutocomplete = this.availableTags.map(t => t.getNormalizedOutput()); + this.autosuggestTags = this.formControl.valueChanges.pipe( + startWith(null), + debounceTime(250), + map((tag: string | null) => tag ? this.filterSuggestionTag(tag) : this.tagsForAutocomplete.slice(0, 20))); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes["availableTags"]) { + this.tagsForAutocomplete = this.availableTags.map(t => t.getNormalizedOutput()); + } + } + + public addTagByInput(event: any): void { + this.addTag(this.formControl.value); + } + + public addTagByAutocomplete(event: MatAutocompleteSelectedEvent): void { + this.addTag(event.option.value); + } + + private addTag(value: string) { + const tag = this.normalizeTag(value); + if (tag.length > 0 && (this.allowInvalid || this.checkTagValid(tag))) { + this.tagAdded.emit(tag); + this.formControl.setValue(""); + this.tagInput.nativeElement.value = ""; + } + } + + private filterSuggestionTag(tag: string) { + let normalizedTag = this.normalizeTag(tag); + const negated = normalizedTag.startsWith("-") && this.allowNegation; + normalizedTag = this.allowNegation? normalizedTag.replace(/^-/, "") : normalizedTag; + + return this.tagsForAutocomplete.filter( + t => t.includes(normalizedTag)) + .map(t => negated ? "-" + t: t) + .sort((l, r) => this.compareSuggestionTags(normalizedTag, l, r)) + .slice(0, 20); + } + + private checkTagValid(tag: string): boolean { + if (this.allowNegation) { + tag = tag.replace(/^-/, ""); + } + return this.tagsForAutocomplete.includes(tag); + } + + /** + * Normalizes the tag by removing whitespaces + * @param {string} tag + * @returns {string} + * @private + */ + private normalizeTag(tag: string): string { + let normalizedTag = tag.trim(); + let parts = normalizedTag.split(":"); + + if (parts.length > 1) { + const namespace = parts.shift()!.trim(); + const name = parts.join(":").trim(); + return namespace + ":" + name; + } else { + return normalizedTag; + } + } + + private compareSuggestionTags(query: string, l: string, r: string): number { + if (l.startsWith(query) && !r.startsWith(query)) { + return -1; + } else if (!l.startsWith(query) && r.startsWith(query)) { + return 1; + } else if (l.length < r.length) { + return -1; + } else if (l.length > r.length) { + return 1; + } else { + return l.localeCompare(r) + } + } +} diff --git a/mediarepo-ui/src/app/models/FilterExpression.ts b/mediarepo-ui/src/app/models/FilterExpression.ts index 391877d..7929872 100644 --- a/mediarepo-ui/src/app/models/FilterExpression.ts +++ b/mediarepo-ui/src/app/models/FilterExpression.ts @@ -43,6 +43,10 @@ export class OrFilterExpression implements FilterExpression{ public queryList(): TagQuery[] { return this.filter; } + + public removeQueryEntry(index: number) { + this.filter.splice(index, 1); + } } export class SingleFilterExpression implements FilterExpression { diff --git a/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.html b/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.html index e837b8f..00a98ef 100644 --- a/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.html +++ b/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.html @@ -5,7 +5,8 @@
+ [validTags]="this.getValidTagsForSearch()" + [availableTags]="this.tagsOfFiles">
From 349d1dfc31359bf39e09684bf4691b4892ab406c Mon Sep 17 00:00:00 2001 From: trivernis Date: Sat, 20 Nov 2021 12:32:33 +0100 Subject: [PATCH 3/6] Fix filter tag suggestions not using all tags Signed-off-by: trivernis --- .../src/app/components/file-search/file-search.component.scss | 2 +- .../files-tab-sidebar/files-tab-sidebar.component.html | 2 +- .../files-tab/files-tab-sidebar/files-tab-sidebar.component.ts | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/mediarepo-ui/src/app/components/file-search/file-search.component.scss b/mediarepo-ui/src/app/components/file-search/file-search.component.scss index 81993c7..1dfe215 100644 --- a/mediarepo-ui/src/app/components/file-search/file-search.component.scss +++ b/mediarepo-ui/src/app/components/file-search/file-search.component.scss @@ -57,6 +57,6 @@ .filter-dialog-button { position: absolute; - right: 0; + right: -13px; top: -20px; } diff --git a/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.html b/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.html index 00a98ef..83d9da1 100644 --- a/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.html +++ b/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.html @@ -6,7 +6,7 @@ + [availableTags]="this.allTags">
diff --git a/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.ts b/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.ts index 062c8c2..6767c0c 100644 --- a/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.ts +++ b/mediarepo-ui/src/app/pages/home/files-tab/files-tab-sidebar/files-tab-sidebar.component.ts @@ -32,6 +32,7 @@ export class FilesTabSidebarComponent implements OnInit, OnChanges { public tagsOfFiles: Tag[] = []; public tags: Tag[] = []; + public allTags: Tag[] = []; public files: File[] = []; public tagsOfSelection: Tag[] = []; @@ -43,6 +44,7 @@ export class FilesTabSidebarComponent implements OnInit, OnChanges { }); this.repoService.selectedRepository.subscribe( async (repo) => repo && this.fileSearch && await this.fileSearch.searchForFiles()); + this.tagService.tags.subscribe(t => this.allTags = t); } async ngOnInit() { From 69c188d28833f21ed9593dd26045fd24242953dd Mon Sep 17 00:00:00 2001 From: trivernis Date: Sat, 20 Nov 2021 15:10:02 +0100 Subject: [PATCH 4/6] Add context menu to switch filters from or to and Signed-off-by: trivernis --- mediarepo-ui/src/app/app.module.ts | 2 + .../context-menu/context-menu.component.html | 5 + .../context-menu/context-menu.component.scss | 4 + .../context-menu.component.spec.ts | 25 +++++ .../context-menu/context-menu.component.ts | 32 ++++++ .../filter-dialog.component.html | 9 +- .../filter-dialog.component.scss | 6 ++ .../filter-dialog/filter-dialog.component.ts | 101 ++++++++++++++++-- .../tag-filter-list-item.component.html | 9 +- .../tag-filter-list-item.component.scss | 8 ++ .../tag-filter-list-item.component.ts | 40 ++++++- .../src/app/models/FilterExpression.ts | 12 +++ 12 files changed, 232 insertions(+), 21 deletions(-) create mode 100644 mediarepo-ui/src/app/components/context-menu/context-menu.component.html create mode 100644 mediarepo-ui/src/app/components/context-menu/context-menu.component.scss create mode 100644 mediarepo-ui/src/app/components/context-menu/context-menu.component.spec.ts create mode 100644 mediarepo-ui/src/app/components/context-menu/context-menu.component.ts diff --git a/mediarepo-ui/src/app/app.module.ts b/mediarepo-ui/src/app/app.module.ts index f8a82d4..dceb63e 100644 --- a/mediarepo-ui/src/app/app.module.ts +++ b/mediarepo-ui/src/app/app.module.ts @@ -57,6 +57,7 @@ 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'; import { TagInputComponent } from './components/inputs/tag-input/tag-input.component'; +import { ContextMenuComponent } from './components/context-menu/context-menu.component'; @NgModule({ declarations: [ @@ -84,6 +85,7 @@ import { TagInputComponent } from './components/inputs/tag-input/tag-input.compo FilterDialogComponent, TagFilterListItemComponent, TagInputComponent, + ContextMenuComponent, ], imports: [ BrowserModule, diff --git a/mediarepo-ui/src/app/components/context-menu/context-menu.component.html b/mediarepo-ui/src/app/components/context-menu/context-menu.component.html new file mode 100644 index 0000000..d182a4b --- /dev/null +++ b/mediarepo-ui/src/app/components/context-menu/context-menu.component.html @@ -0,0 +1,5 @@ + + + + + diff --git a/mediarepo-ui/src/app/components/context-menu/context-menu.component.scss b/mediarepo-ui/src/app/components/context-menu/context-menu.component.scss new file mode 100644 index 0000000..960a1df --- /dev/null +++ b/mediarepo-ui/src/app/components/context-menu/context-menu.component.scss @@ -0,0 +1,4 @@ +.menu-anchor { + visibility: hidden; + position: fixed; +} diff --git a/mediarepo-ui/src/app/components/context-menu/context-menu.component.spec.ts b/mediarepo-ui/src/app/components/context-menu/context-menu.component.spec.ts new file mode 100644 index 0000000..55a3414 --- /dev/null +++ b/mediarepo-ui/src/app/components/context-menu/context-menu.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ContextMenuComponent } from './context-menu.component'; + +describe('ContextMenuComponent', () => { + let component: ContextMenuComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ContextMenuComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ContextMenuComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/mediarepo-ui/src/app/components/context-menu/context-menu.component.ts b/mediarepo-ui/src/app/components/context-menu/context-menu.component.ts new file mode 100644 index 0000000..38e0105 --- /dev/null +++ b/mediarepo-ui/src/app/components/context-menu/context-menu.component.ts @@ -0,0 +1,32 @@ +import { + Component, + ComponentFactoryResolver, + OnInit, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import {MatMenuTrigger} from "@angular/material/menu"; + +@Component({ + selector: 'app-context-menu', + templateUrl: './context-menu.component.html', + styleUrls: ['./context-menu.component.scss'] +}) +export class ContextMenuComponent { + + + public x: string = "0"; + public y: string = "0"; + + @ViewChild(MatMenuTrigger) menuTrigger!: MatMenuTrigger + + constructor() { + } + + public onContextMenu(event: MouseEvent) { + event.preventDefault(); + this.x = event.clientX + "px"; + this.y = event.clientY + "px"; + this.menuTrigger.openMenu(); + } +} diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html index ce83f9a..9e8801e 100644 --- a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html @@ -1,8 +1,9 @@

Filters

- - + + @@ -13,3 +14,7 @@
+ + + + diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.scss b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.scss index 49c51ef..bea8807 100644 --- a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.scss +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.scss @@ -15,8 +15,14 @@ mat-list-item.filter-list-item { height: 100%; padding: 0.5em 0; + user-select: none; + cursor: pointer; } app-tag-filter-list-item { width: 100%; } + +.selected { + background-color: #5c5c5c; +} diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.ts b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.ts index fdea7c4..e5e9f45 100644 --- a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.ts +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.ts @@ -1,14 +1,9 @@ -import { - Component, - ElementRef, - HostListener, - Inject, - ViewChild -} from '@angular/core'; +import {Component, HostListener, Inject, ViewChildren} from '@angular/core'; import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; import {SortDialogComponent} from "../sort-dialog/sort-dialog.component"; import { - FilterExpression, OrFilterExpression, + FilterExpression, + OrFilterExpression, SingleFilterExpression } from "../../../models/FilterExpression"; import {TagQuery} from "../../../models/TagQuery"; @@ -27,9 +22,16 @@ export class FilterDialogComponent { public availableTags: Tag[] = []; public mode: "AND" | "OR" = "AND"; + @ViewChildren( + TagFilterListItemComponent) filterListItems!: TagFilterListItemComponent[]; + + private selectedQueries: TagQuery[] = []; + constructor(public dialogRef: MatDialogRef, @Inject( MAT_DIALOG_DATA) data: any) { - this.filters = data.filterEntries.map((f: FilterExpression) => new Selectable(f, false)) ?? []; + this.filters = data.filterEntries.map( + (f: FilterExpression) => new Selectable(f, + false)) ?? []; this.availableTags = data.availableTags ?? []; } @@ -47,13 +49,16 @@ export class FilterDialogComponent { if (index >= 0) { this.filters.splice(index, 1); } + this.unselectAll(); } public addFilter(tag: string) { const query = TagQuery.fromString(tag); if (this.mode === "AND" || this.filters.length === 0) { - this.filters.push(new Selectable(new SingleFilterExpression(query), false)); + this.filters.push( + new Selectable(new SingleFilterExpression(query), + false)); tag = tag.replace(/^-/g, ''); if (this.filters.filter(t => t.data.partiallyEq(tag)).length > 1) { @@ -64,8 +69,82 @@ export class FilterDialogComponent { let queryList = this.filters.pop()?.data.queryList() ?? []; queryList.push(query); - this.filters.push(new Selectable(new OrFilterExpression(queryList), false)); + const filterExpression = new OrFilterExpression(queryList); + filterExpression.removeDuplicates(); + this.filters.push( + new Selectable(filterExpression, + false)); + } + this.unselectAll(); + } + + public addToSelection(query: TagQuery): void { + this.selectedQueries.push(query); + } + + public removeFromSelection(query: TagQuery): void { + const index = this.selectedQueries.indexOf(query); + if (index > 0) { + this.selectedQueries.splice(index, 1); + } + } + + public unselectAll() { + this.filters.forEach(filter => filter.selected = false); + this.selectedQueries = []; + this.filterListItems.forEach(i => i.selectedIndices = []); + } + + public convertSelectionToAndExpression(): void { + for (const query of this.selectedQueries) { + this.filters.push(new Selectable(new SingleFilterExpression(query), false)); + } + this.removeFilterDuplicates(); + this.unselectAll(); + } + + public convertSelectionToOrExpression(): void { + const queries = this.selectedQueries; + const expression = new OrFilterExpression(queries); + this.filters.push(new Selectable(expression, false)); + this.removeFilterDuplicates(); + this.unselectAll(); + } + + private removeFilterDuplicates() { + const filters = this.filters; + let newFilters: Selectable[] = []; + + for (const filterItem of filters) { + if (filterItem.data.filter_type == "OrExpression") { + (filterItem.data as OrFilterExpression).removeDuplicates(); + } + if (newFilters.findIndex(f => FilterDialogComponent.checkFiltersEqual(f.data, filterItem.data)) < 0) { + if (filterItem.data.filter_type == "OrExpression" && filterItem.data.queryList().length === 1) { + filterItem.data = new SingleFilterExpression(filterItem.data.queryList()[0]); + } + newFilters.push(filterItem); + } + } + this.filters = newFilters; + } + + private static checkFiltersEqual(l: FilterExpression, r: FilterExpression): boolean { + const lTags = l.queryList().map(q => q.getNormalizedTag()).sort(); + const rTags = r.queryList().map(q => q.getNormalizedTag()).sort(); + let match = false; + + if (lTags.length == rTags.length) { + match = true; + + for (const tag of lTags) { + match = rTags.includes(tag); + if (!match) { + break; + } + } } + return match; } @HostListener("window:keydown", ["$event"]) diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.html b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.html index 8b573d9..b1116e3 100644 --- a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.html +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.html @@ -1,15 +1,16 @@ - +
{{expression.data.getDisplayName()}} - +
- + OR {{entry[1].getNormalizedTag()}} - diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.scss b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.scss index e64d496..37a6050 100644 --- a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.scss +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.scss @@ -14,6 +14,7 @@ mat-list { width: 100%; display: block; background-color: #353535; + padding: 0; border-radius: 0.25em; } @@ -21,6 +22,9 @@ mat-list-item.or-filter-list-item { padding: 0.5em 0; height: 100%; width: 100%; + border-collapse: collapse; + cursor: pointer; + user-select: none; ::ng-deep .mat-list-item-content { padding-right: 0; @@ -31,3 +35,7 @@ mat-list-item.or-filter-list-item { .or-span { margin-right: 0.5em; } + +.selected { + background-color: #5c5c5c; +} diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.ts b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.ts index abe1391..f2d0b03 100644 --- a/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.ts +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.ts @@ -3,9 +3,9 @@ import { Component, EventEmitter, Inject, - Input, + Input, OnChanges, OnInit, - Output + Output, SimpleChanges } from '@angular/core'; import { FilterExpression, @@ -19,12 +19,22 @@ import {Selectable} from "../../../../models/Selectable"; templateUrl: './tag-filter-list-item.component.html', styleUrls: ['./tag-filter-list-item.component.scss'] }) -export class TagFilterListItemComponent { +export class TagFilterListItemComponent implements OnChanges { @Input() expression!: Selectable; @Output() removeClicked = new EventEmitter(); + @Output() querySelect = new EventEmitter(); + @Output() queryUnselect = new EventEmitter(); - constructor() { } + public selectedIndices: number[] = []; + + constructor(private changeDetector: ChangeDetectorRef) { } + + public ngOnChanges(changes: SimpleChanges): void { + if (changes["expression"]) { + this.selectedIndices = []; + } + } public enumerate(items: T[]): [number, T][] { return items.map((value, index) => [index, value]); @@ -40,4 +50,26 @@ export class TagFilterListItemComponent { this.expression.data = new SingleFilterExpression(expression.filter[0]); } } + + public selectInnerIndex(index: number): void { + const expression = this.expression.data as OrFilterExpression; + + if (this.selectedIndices.includes(index)) { + const elementIndex = this.selectedIndices.indexOf(index); + this.selectedIndices.splice(elementIndex, 1); + this.queryUnselect.emit(expression.filter[index]); + } else { + this.selectedIndices.push(index); + this.querySelect.emit(expression.filter[index]); + } + } + + public onSelect(): void { + this.expression.selected = !this.expression.selected; + if (this.expression.selected) { + this.querySelect.emit(this.expression.data.filter as TagQuery); + } else { + this.queryUnselect.emit(this.expression.data.filter as TagQuery); + } + } } diff --git a/mediarepo-ui/src/app/models/FilterExpression.ts b/mediarepo-ui/src/app/models/FilterExpression.ts index 7929872..e2e8d62 100644 --- a/mediarepo-ui/src/app/models/FilterExpression.ts +++ b/mediarepo-ui/src/app/models/FilterExpression.ts @@ -47,6 +47,18 @@ export class OrFilterExpression implements FilterExpression{ public removeQueryEntry(index: number) { this.filter.splice(index, 1); } + + public removeDuplicates() { + const filters = this.filter.reverse(); + let newEntries: TagQuery[] = []; + + for (const entry of filters) { + if (newEntries.findIndex(f => f.tag === entry.tag) < 0) { + newEntries.push(entry); + } + } + this.filter = newEntries.reverse(); + } } export class SingleFilterExpression implements FilterExpression { From 3a461080ea2ca58570af718189c8e0d1ea6d85fd Mon Sep 17 00:00:00 2001 From: trivernis Date: Sat, 20 Nov 2021 15:21:53 +0100 Subject: [PATCH 5/6] Fix scrolling behaviour of filter dialog Signed-off-by: trivernis --- .../file-search/file-search.component.ts | 1 + .../filter-dialog.component.html | 20 ++++++++++--------- .../filter-dialog.component.scss | 13 ++++++++++++ 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/mediarepo-ui/src/app/components/file-search/file-search.component.ts b/mediarepo-ui/src/app/components/file-search/file-search.component.ts index 2f30d31..da3f060 100644 --- a/mediarepo-ui/src/app/components/file-search/file-search.component.ts +++ b/mediarepo-ui/src/app/components/file-search/file-search.component.ts @@ -148,6 +148,7 @@ export class FileSearchComponent implements AfterViewChecked, OnInit { const filterEntries = this.filters.map(f => f.clone()); const filterDialog = this.dialog.open(FilterDialogComponent, { minWidth: "25vw", + height: "80vh", data: { filterEntries, availableTags: this.availableTags, diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html index 9e8801e..c5762b8 100644 --- a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html @@ -1,13 +1,15 @@

Filters

-
- - - - - - - +
+ + + + + +
+ +
diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.scss b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.scss index bea8807..ca664f3 100644 --- a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.scss +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.scss @@ -26,3 +26,16 @@ app-tag-filter-list-item { .selected { background-color: #5c5c5c; } + +.filter-dialog-content { + overflow: hidden; + height: 100%; + width: 100%; + margin: 0; +} + +.filter-dialog-list { + overflow-y: auto; + height: 100%; + width: 100%; +} From dff586bc1011067ad9e87d5f3da7c1ffc74e3387 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sat, 20 Nov 2021 15:24:47 +0100 Subject: [PATCH 6/6] Add invert action to filter context menu Signed-off-by: trivernis --- .../file-search/filter-dialog/filter-dialog.component.html | 1 + .../file-search/filter-dialog/filter-dialog.component.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html index c5762b8..3d76050 100644 --- a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.html @@ -19,4 +19,5 @@ + diff --git a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.ts b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.ts index e5e9f45..ce97c95 100644 --- a/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.ts +++ b/mediarepo-ui/src/app/components/file-search/filter-dialog/filter-dialog.component.ts @@ -111,6 +111,10 @@ export class FilterDialogComponent { this.unselectAll(); } + public invertSelection(): void { + this.selectedQueries.forEach(query => query.negate = !query.negate); + } + private removeFilterDuplicates() { const filters = this.filters; let newFilters: Selectable[] = [];