From c2dbf6846cb44bd08bd72806b288cf7640672a61 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sat, 15 Jan 2022 17:23:51 +0100 Subject: [PATCH] Repair filter dialog to accept rich expressions Signed-off-by: trivernis --- mediarepo-ui/package.json | 32 +-- .../src/api/models/FilterQueryBuilder.ts | 4 +- mediarepo-ui/src/api/models/SearchFilters.ts | 33 ++- .../shared/app-common/app-common.module.ts | 3 + .../selectable/selectable.component.html | 3 + .../selectable/selectable.component.scss | 7 + .../selectable/selectable.component.spec.ts | 25 ++ .../selectable/selectable.component.ts | 25 ++ .../file-search/file-search.component.ts | 5 +- .../filter-dialog.component.html | 28 ++- .../filter-dialog.component.scss | 8 +- .../filter-dialog/filter-dialog.component.ts | 222 ++++++++---------- ...filter-expression-list-item.component.html | 26 ++ ...ilter-expression-list-item.component.scss} | 20 +- ...ter-expression-list-item.component.spec.ts | 25 ++ .../filter-expression-list-item.component.ts | 57 +++++ .../tag-filter-list-item.component.html | 22 -- .../tag-filter-list-item.component.spec.ts | 25 -- .../tag-filter-list-item.component.ts | 77 ------ .../shared/sidebar/sidebar.module.ts | 6 +- mediarepo-ui/src/app/utils/list-utils.ts | 15 ++ mediarepo-ui/yarn.lock | 38 +-- 22 files changed, 377 insertions(+), 329 deletions(-) create mode 100644 mediarepo-ui/src/app/components/shared/app-common/selectable/selectable.component.html create mode 100644 mediarepo-ui/src/app/components/shared/app-common/selectable/selectable.component.scss create mode 100644 mediarepo-ui/src/app/components/shared/app-common/selectable/selectable.component.spec.ts create mode 100644 mediarepo-ui/src/app/components/shared/app-common/selectable/selectable.component.ts create mode 100644 mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-expression-list-item/filter-expression-list-item.component.html rename mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/{tag-filter-list-item/tag-filter-list-item.component.scss => filter-expression-list-item/filter-expression-list-item.component.scss} (69%) create mode 100644 mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-expression-list-item/filter-expression-list-item.component.spec.ts create mode 100644 mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-expression-list-item/filter-expression-list-item.component.ts delete mode 100644 mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.html delete mode 100644 mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.spec.ts delete mode 100644 mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.ts create mode 100644 mediarepo-ui/src/app/utils/list-utils.ts diff --git a/mediarepo-ui/package.json b/mediarepo-ui/package.json index 8d1cea7..2369bff 100644 --- a/mediarepo-ui/package.json +++ b/mediarepo-ui/package.json @@ -13,20 +13,20 @@ }, "private": true, "dependencies": { - "@angular/animations": "~13.1.1", - "@angular/cdk": "^13.1.1", - "@angular/common": "~13.1.1", - "@angular/compiler": "~13.1.1", - "@angular/core": "~13.1.1", + "@angular/animations": "~13.1.2", + "@angular/cdk": "^13.1.2", + "@angular/common": "~13.1.2", + "@angular/compiler": "~13.1.2", + "@angular/core": "~13.1.2", "@angular/flex-layout": "^13.0.0-beta.36", - "@angular/forms": "~13.1.1", - "@angular/material": "^13.1.1", - "@angular/platform-browser": "~13.1.1", - "@angular/platform-browser-dynamic": "~13.1.1", - "@angular/router": "~13.1.1", - "@ng-icons/core": "^13.2.0", - "@ng-icons/feather-icons": "^13.2.0", - "@ng-icons/material-icons": "13.1.0", + "@angular/forms": "~13.1.2", + "@angular/material": "^13.1.2", + "@angular/platform-browser": "~13.1.2", + "@angular/platform-browser-dynamic": "~13.1.2", + "@angular/router": "~13.1.2", + "@ng-icons/core": "^13.2.1", + "@ng-icons/feather-icons": "^13.2.1", + "@ng-icons/material-icons": "^13.2.1", "@tauri-apps/api": "^1.0.0-beta.8", "primeicons": "^5.0.0", "primeng": "^13.0.4", @@ -35,14 +35,14 @@ "zone.js": "~0.11.4" }, "devDependencies": { - "@angular-devkit/build-angular": "~13.1.2", + "@angular-devkit/build-angular": "~13.1.3", "@angular-eslint/builder": "^13.0.1", "@angular-eslint/eslint-plugin": "^13.0.1", "@angular-eslint/eslint-plugin-template": "^13.0.1", "@angular-eslint/schematics": "^13.0.1", "@angular-eslint/template-parser": "^13.0.1", - "@angular/cli": "~13.1.2", - "@angular/compiler-cli": "~13.1.1", + "@angular/cli": "~13.1.3", + "@angular/compiler-cli": "~13.1.2", "@tauri-apps/cli": "^1.0.0-beta.10", "@types/file-saver": "^2.0.4", "@types/jasmine": "~3.10.3", diff --git a/mediarepo-ui/src/api/models/FilterQueryBuilder.ts b/mediarepo-ui/src/api/models/FilterQueryBuilder.ts index 04a8a39..48c1a1f 100644 --- a/mediarepo-ui/src/api/models/FilterQueryBuilder.ts +++ b/mediarepo-ui/src/api/models/FilterQueryBuilder.ts @@ -64,9 +64,9 @@ export class FilterQueryBuilder { const parts = expressionStr.split(/\s+or\s+/gi); const queries = parts.map(part => this.buildFilterFromString(part)).filter(f => f != undefined) as FilterQuery[]; - if (queries.length > 0) { + if (queries.length > 1) { return { OrExpression: queries }; - } else if (queries.length == 1) { + } else if (queries.length === 1) { return { Query: queries[0] }; } else { return undefined; diff --git a/mediarepo-ui/src/api/models/SearchFilters.ts b/mediarepo-ui/src/api/models/SearchFilters.ts index de6fa63..dbe1fa7 100644 --- a/mediarepo-ui/src/api/models/SearchFilters.ts +++ b/mediarepo-ui/src/api/models/SearchFilters.ts @@ -1,4 +1,4 @@ -import {FilterExpression, FilterQuery} from "../api-types/files"; +import {FilterExpression, FilterExpressionQuery, FilterQuery} from "../api-types/files"; import * as deepEqual from "fast-deep-equal"; export class SearchFilters { @@ -13,12 +13,23 @@ export class SearchFilters { return this.filters; } + public getSubfilterAtIndex(index: number, subindex: number): FilterQuery | undefined { + if (index < this.filters.length) { + const filterEntry = this.filters[index]!; + if ("OrExpression" in filterEntry) { + return filterEntry.OrExpression[subindex]; + } + } + return undefined; + } + public hasFilter(expression: FilterExpression): boolean { return !!this.filters.find(f => deepEqual(f, expression)); } public addFilterExpression(filter: FilterExpression) { this.filters.push(filter); + this.processChangesToOrExpressions(); } public addFilter(filter: FilterQuery, index: number) { @@ -63,6 +74,7 @@ export class SearchFilters { } }); this.filters.splice(index); + this.processChangesToOrExpressions(); } public removeSubfilterAtIndex(index: number, subindex: number) { @@ -75,5 +87,24 @@ export class SearchFilters { this.removeFilterAtIndex(index); } } + this.processChangesToOrExpressions(); + } + + private processChangesToOrExpressions() { + const filters_to_remove: FilterExpression[] = []; + + for (const filter of this.filters) { + if ("OrExpression" in filter) { + if (filter.OrExpression.length === 1) { + const query = filter.OrExpression[0]; + let newFilter = filter as unknown as FilterExpressionQuery & { OrExpression: undefined }; + newFilter["OrExpression"] = undefined; + newFilter.Query = query; + } else if (filter.OrExpression.length === 0) { + filters_to_remove.push(filter); + } + } + } + filters_to_remove.forEach(f => this.removeFilter(f)); } } diff --git a/mediarepo-ui/src/app/components/shared/app-common/app-common.module.ts b/mediarepo-ui/src/app/components/shared/app-common/app-common.module.ts index 780847b..0f8a603 100644 --- a/mediarepo-ui/src/app/components/shared/app-common/app-common.module.ts +++ b/mediarepo-ui/src/app/components/shared/app-common/app-common.module.ts @@ -12,6 +12,7 @@ import {ContentAwareImageComponent} from "./content-aware-image/content-aware-im import {InputReceiverDirective} from "./input-receiver/input-receiver.directive"; import {MetadataEntryComponent} from "./metadata-entry/metadata-entry.component"; import {BusyDialogComponent} from "./busy-dialog/busy-dialog.component"; +import {SelectableComponent} from "./selectable/selectable.component"; @NgModule({ @@ -23,6 +24,7 @@ import {BusyDialogComponent} from "./busy-dialog/busy-dialog.component"; InputReceiverDirective, MetadataEntryComponent, BusyDialogComponent, + SelectableComponent, ], exports: [ ConfirmDialogComponent, @@ -31,6 +33,7 @@ import {BusyDialogComponent} from "./busy-dialog/busy-dialog.component"; ContentAwareImageComponent, InputReceiverDirective, MetadataEntryComponent, + SelectableComponent, ], imports: [ CommonModule, diff --git a/mediarepo-ui/src/app/components/shared/app-common/selectable/selectable.component.html b/mediarepo-ui/src/app/components/shared/app-common/selectable/selectable.component.html new file mode 100644 index 0000000..60b1a66 --- /dev/null +++ b/mediarepo-ui/src/app/components/shared/app-common/selectable/selectable.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/mediarepo-ui/src/app/components/shared/app-common/selectable/selectable.component.scss b/mediarepo-ui/src/app/components/shared/app-common/selectable/selectable.component.scss new file mode 100644 index 0000000..33b3125 --- /dev/null +++ b/mediarepo-ui/src/app/components/shared/app-common/selectable/selectable.component.scss @@ -0,0 +1,7 @@ +.selectable.selected { + background-color: #5c5c5c; +} + +body { + cursor: pointer; +} diff --git a/mediarepo-ui/src/app/components/shared/app-common/selectable/selectable.component.spec.ts b/mediarepo-ui/src/app/components/shared/app-common/selectable/selectable.component.spec.ts new file mode 100644 index 0000000..024302c --- /dev/null +++ b/mediarepo-ui/src/app/components/shared/app-common/selectable/selectable.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SelectableComponent } from './selectable.component'; + +describe('SelectableComponent', () => { + let component: SelectableComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SelectableComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SelectableComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/mediarepo-ui/src/app/components/shared/app-common/selectable/selectable.component.ts b/mediarepo-ui/src/app/components/shared/app-common/selectable/selectable.component.ts new file mode 100644 index 0000000..7213dff --- /dev/null +++ b/mediarepo-ui/src/app/components/shared/app-common/selectable/selectable.component.ts @@ -0,0 +1,25 @@ +import {Component, EventEmitter, Output} from "@angular/core"; + +@Component({ + selector: "app-selectable", + templateUrl: "./selectable.component.html", + styleUrls: ["./selectable.component.scss"] +}) +export class SelectableComponent { + public selected = false; + + @Output() appSelect = new EventEmitter(); + @Output() appUnselect = new EventEmitter(); + + constructor() { + } + + public onClick(): void { + this.selected = !this.selected; + if (this.selected) { + this.appSelect.emit(this); + } else { + this.appUnselect.emit(this); + } + } +} diff --git a/mediarepo-ui/src/app/components/shared/sidebar/file-search/file-search.component.ts b/mediarepo-ui/src/app/components/shared/sidebar/file-search/file-search.component.ts index e7d2f88..399025d 100644 --- a/mediarepo-ui/src/app/components/shared/sidebar/file-search/file-search.component.ts +++ b/mediarepo-ui/src/app/components/shared/sidebar/file-search/file-search.component.ts @@ -117,12 +117,13 @@ export class FileSearchComponent implements AfterViewChecked, OnInit { } public openFilterDialog(): void { - const filterEntries = this.filters; + const filterEntries = new SearchFilters(JSON.parse(JSON.stringify(this.filters.getFilters()))); + const filterDialog = this.dialog.open(FilterDialogComponent, { minWidth: "25vw", maxHeight: "80vh", data: { - filterEntries, + filters: filterEntries, availableTags: this.availableTags, }, disableClose: true, diff --git a/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-dialog.component.html b/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-dialog.component.html index 076a62f..1f05c7e 100644 --- a/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-dialog.component.html +++ b/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-dialog.component.html @@ -2,26 +2,30 @@
- - + +
- +
- - - + + + + + diff --git a/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-dialog.component.scss b/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-dialog.component.scss index d7eb4f0..dad4cf2 100644 --- a/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-dialog.component.scss +++ b/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-dialog.component.scss @@ -8,7 +8,7 @@ } } -.tag-input { +.filter-input { width: 100%; height: 5em; } @@ -20,14 +20,10 @@ mat-list-item.filter-list-item { cursor: pointer; } -app-tag-filter-list-item { +app-filter-expression-list-item { width: 100%; } -.selected { - background-color: #5c5c5c; -} - .filter-dialog-content { overflow: hidden; width: 100%; diff --git a/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-dialog.component.ts b/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-dialog.component.ts index f8019a0..f11843d 100644 --- a/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-dialog.component.ts +++ b/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-dialog.component.ts @@ -1,58 +1,37 @@ -import {Component, HostListener, Inject, ViewChildren} from "@angular/core"; +import {Component, Inject, OnChanges, SimpleChanges} from "@angular/core"; import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; import {SortDialogComponent} from "../sort-dialog/sort-dialog.component"; -import { - GenericFilter, - OrFilterExpression, - SingleFilterExpression -} from "../../../../../models/GenericFilter"; -import {TagQuery} from "../../../../../models/TagQuery"; import {Tag} from "../../../../../../api/models/Tag"; -import { - TagFilterListItemComponent -} from "./tag-filter-list-item/tag-filter-list-item.component"; -import {Selectable} from "../../../../../models/Selectable"; +import {SearchFilters} from "../../../../../../api/models/SearchFilters"; +import {FilterExpression, FilterQuery} from "../../../../../../api/api-types/files"; +import {enumerate, removeByValue} from "../../../../../utils/list-utils"; + +type IndexableSelection = { + [key: number]: T +}; @Component({ selector: "app-filter-dialog", templateUrl: "./filter-dialog.component.html", styleUrls: ["./filter-dialog.component.scss"] }) -export class FilterDialogComponent { - - public filters: Selectable[]; +export class FilterDialogComponent implements OnChanges { public availableTags: Tag[] = []; - public mode: "AND" | "OR" = "AND"; - - @ViewChildren( - TagFilterListItemComponent) filterListItems!: TagFilterListItemComponent[]; - - private selectedQueries: TagQuery[] = []; + public filters = new SearchFilters([]); + public renderedFilterEntries: [number, FilterExpression][] = []; + private selectedIndices: IndexableSelection = {}; constructor(public dialogRef: MatDialogRef, @Inject( MAT_DIALOG_DATA) data: any) { - this.filters = data.filterEntries.map( - (f: GenericFilter) => new Selectable(f, - false)) ?? []; this.availableTags = data.availableTags ?? []; + this.filters = data.filters; + this.buildRenderedEntries(); } - private static checkFiltersEqual(l: GenericFilter, r: GenericFilter): 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; - } - } + public ngOnChanges(changes: SimpleChanges): void { + if (changes["filters"]) { + this.buildRenderedEntries(); } - return match; } public cancelFilter(): void { @@ -60,118 +39,105 @@ export class FilterDialogComponent { } public confirmFilter(): void { - this.dialogRef.close(this.filters.map(f => f.data)); + this.dialogRef.close(this.filters); } - 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); - } - this.unselectAll(); + public entrySelect(index: number, subindex: number = -1): void { + this.selectedIndices[index] = this.selectedIndices[index] ?? []; + this.selectedIndices[index].push(subindex); } - 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)); - tag = tag.replace(/^-/g, ""); - - 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()?.data.queryList() ?? []; - - queryList.push(query); - const filterExpression = new OrFilterExpression(queryList); - filterExpression.removeDuplicates(); - this.filters.push( - new Selectable(filterExpression, - false)); - } - this.unselectAll(); + public entryUnselect(index: number, subindex: number = -1): void { + this.selectedIndices[index] = this.selectedIndices[index] ?? []; + removeByValue(this.selectedIndices[index], subindex); } - public addToSelection(query: TagQuery): void { - this.selectedQueries.push(query); + public addFilter(expression: FilterExpression): void { + this.filters.addFilterExpression(expression); + this.buildRenderedEntries(); } - public removeFromSelection(query: TagQuery): void { - const index = this.selectedQueries.indexOf(query); - if (index > 0) { - this.selectedQueries.splice(index, 1); - } - } + public removeSelectedFilters(): void { + const orderedIndices = Object.keys(this.selectedIndices).map(k => Number(k)).sort().reverse(); - public unselectAll() { - this.filters.forEach(filter => filter.selected = false); - this.selectedQueries = []; - this.filterListItems.forEach(i => i.selectedIndices = []); - } + for (const indexStr of orderedIndices) { + const index = indexStr; + const subIndices: number[] = this.selectedIndices[index]; - public convertSelectionToAndExpression(): void { - for (const query of this.selectedQueries) { - this.filters.push( - new Selectable( - new SingleFilterExpression(query), - false)); + if (subIndices.length === 1 && subIndices[0] === -1) { + this.filters.removeFilterAtIndex(index); + } else if (subIndices.length > 0) { + for (const subIndex of subIndices.sort().reverse()) { // need to remove from the top down to avoid index shifting + this.filters.removeSubfilterAtIndex(index, subIndex); + } + } } - this.removeFilterDuplicates(); - this.unselectAll(); + this.selectedIndices = {}; + this.buildRenderedEntries(); } - public convertSelectionToOrExpression(): void { - const queries = this.selectedQueries; - const expression = new OrFilterExpression(queries); - this.filters.push(new Selectable(expression, false)); - this.removeFilterDuplicates(); - this.unselectAll(); + public createAndFromSelection(deleteOriginal: boolean): void { + const expressions: FilterExpression[] = []; + + for (const indexStr in this.selectedIndices) { + const index = Number(indexStr); + const subindices = this.selectedIndices[index]; + + if (subindices.length === 1 && subindices[0] === -1) { + expressions.push(this.filters.getFilters()[index]); + } else { + for (const subIndex of subindices) { + const query = this.filters.getSubfilterAtIndex(index, subIndex); + if (query) { + expressions.push({ Query: query }); + } + } + } + } + if (deleteOriginal) { + this.removeSelectedFilters(); + } else { + this.selectedIndices = {}; + } + expressions.forEach(e => this.filters.addFilterExpression(e)); + this.buildRenderedEntries(); } - public invertSelection(): void { - this.selectedQueries.forEach(query => query.negate = !query.negate); - } + public createOrFromSelection(deleteOriginal: boolean): void { + const queries: FilterQuery[] = []; - private removeFilterDuplicates() { - const filters = this.filters; - let newFilters: Selectable[] = []; + for (const indexStr in this.selectedIndices) { + const index = Number(indexStr); + const subindices = this.selectedIndices[index]; - 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]); + if (subindices.length === 1 && subindices[0] === -1) { + const filterEntry = this.filters.getFilters()[index]; + if ("Query" in filterEntry) { + queries.push(filterEntry.Query); + } + } else { + for (const subIndex of subindices) { + const query = this.filters.getSubfilterAtIndex(index, subIndex); + if (query) { + queries.push(query); + } } - newFilters.push(filterItem); } } - this.filters = newFilters; - } - - @HostListener("window:keydown", ["$event"]) - private async handleKeydownEvent(event: KeyboardEvent) { - if (event.key === "Shift") { - this.mode = "OR"; + if (deleteOriginal) { + this.removeSelectedFilters(); + } else { + this.selectedIndices = {}; } + if (queries.length > 1) { + this.filters.addFilterExpression({ OrExpression: queries }); + } else if (queries.length === 1) { + this.filters.addFilterExpression({ Query: queries[0] }); + } + this.buildRenderedEntries(); } - @HostListener("window:keyup", ["$event"]) - private async handleKeyupEvent(event: KeyboardEvent) { - if (event.key === "Shift") { - this.mode = "AND"; - } + private buildRenderedEntries() { + this.renderedFilterEntries = enumerate(this.filters.getFilters()); } } diff --git a/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-expression-list-item/filter-expression-list-item.component.html b/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-expression-list-item/filter-expression-list-item.component.html new file mode 100644 index 0000000..db5796f --- /dev/null +++ b/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-expression-list-item/filter-expression-list-item.component.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.scss b/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-expression-list-item/filter-expression-list-item.component.scss similarity index 69% rename from mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.scss rename to mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-expression-list-item/filter-expression-list-item.component.scss index f2b3d66..cfec6b3 100644 --- a/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.scss +++ b/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-expression-list-item/filter-expression-list-item.component.scss @@ -1,14 +1,3 @@ -.remove-button, .remove-button-inner-list { - position: absolute; - right: 0; - z-index: 999; - top: calc(0.5em - 15px); -} - -.remove-button { - right: 16px; -} - mat-list { height: 100%; width: 100%; @@ -32,10 +21,11 @@ mat-list-item.or-filter-list-item { } } -.or-span { - margin-right: 0.5em; -} - .selected { background-color: #5c5c5c; } + +app-selectable { + width: 100%; + height: 100%; +} diff --git a/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-expression-list-item/filter-expression-list-item.component.spec.ts b/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-expression-list-item/filter-expression-list-item.component.spec.ts new file mode 100644 index 0000000..f50a419 --- /dev/null +++ b/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-expression-list-item/filter-expression-list-item.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FilterExpressionListItemComponent } from './filter-expression-list-item.component'; + +describe('FilterExpressionListItemComponent', () => { + let component: FilterExpressionListItemComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FilterExpressionListItemComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FilterExpressionListItemComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-expression-list-item/filter-expression-list-item.component.ts b/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-expression-list-item/filter-expression-list-item.component.ts new file mode 100644 index 0000000..e2ea659 --- /dev/null +++ b/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/filter-expression-list-item/filter-expression-list-item.component.ts @@ -0,0 +1,57 @@ +import {Component, EventEmitter, Input, OnChanges, Output, SimpleChanges} from "@angular/core"; +import { + FilterExpression, + FilterQuery, + FilterQueryProperty, + FilterQueryTag +} from "../../../../../../../api/api-types/files"; +import {enumerate} from "../../../../../../utils/list-utils"; + +@Component({ + selector: "app-filter-expression-list-item", + templateUrl: "./filter-expression-list-item.component.html", + styleUrls: ["./filter-expression-list-item.component.scss"] +}) +export class FilterExpressionListItemComponent implements OnChanges { + + + @Input() filter!: FilterExpression; + @Output() entrySelect = new EventEmitter<[number, FilterQuery]>(); + @Output() entryUnselect = new EventEmitter<[number, FilterQuery]>(); + + @Output() appSelect = new EventEmitter(); + @Output() appUnselect = new EventEmitter(); + + public orExpression: undefined | [number, FilterQuery][] = undefined; + public query: undefined | FilterQuery = undefined; + + constructor() { + this.parseFilter(); + } + + public ngOnChanges(changes: SimpleChanges): void { + if (changes["filter"]) { + this.parseFilter(); + } + } + + public queryIs(query: FilterQuery, key: "Property" | "Tag"): boolean { + return key in query; + } + + public propertyQuery(query: FilterQuery): FilterQueryProperty { + return query as FilterQueryProperty; + } + + public tagQuery(query: FilterQuery): FilterQueryTag { + return query as FilterQueryTag; + } + + private parseFilter() { + if (this.filter && "OrExpression" in this.filter) { + this.orExpression = enumerate(this.filter.OrExpression); + } else if (this.filter) { + this.query = this.filter.Query; + } + } +} diff --git a/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.html b/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.html deleted file mode 100644 index d87059f..0000000 --- a/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.html +++ /dev/null @@ -1,22 +0,0 @@ -
- {{expression.data.getDisplayName()}} - -
-
- - - OR - {{entry[1].getNormalizedTag()}} - - - -
diff --git a/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.spec.ts b/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.spec.ts deleted file mode 100644 index b6b65ef..0000000 --- a/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -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/shared/sidebar/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.ts b/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.ts deleted file mode 100644 index 444c887..0000000 --- a/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - Component, - EventEmitter, - Input, - OnChanges, - Output, - SimpleChanges -} from "@angular/core"; -import { - GenericFilter, - OrFilterExpression, - SingleFilterExpression -} from "../../../../../../models/GenericFilter"; -import {TagQuery} from "../../../../../../models/TagQuery"; -import {Selectable} from "../../../../../../models/Selectable"; - -@Component({ - selector: "app-tag-filter-list-item", - templateUrl: "./tag-filter-list-item.component.html", - styleUrls: ["./tag-filter-list-item.component.scss"] -}) -export class TagFilterListItemComponent implements OnChanges { - - @Input() expression!: Selectable; - @Output() removeClicked = new EventEmitter(); - @Output() querySelect = new EventEmitter(); - @Output() queryUnselect = new EventEmitter(); - - public selectedIndices: number[] = []; - - constructor() { - } - - public ngOnChanges(changes: SimpleChanges): void { - if (changes["expression"]) { - this.selectedIndices = []; - } - } - - 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]); - } - } - - 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/components/shared/sidebar/sidebar.module.ts b/mediarepo-ui/src/app/components/shared/sidebar/sidebar.module.ts index e30ae71..b711d80 100644 --- a/mediarepo-ui/src/app/components/shared/sidebar/sidebar.module.ts +++ b/mediarepo-ui/src/app/components/shared/sidebar/sidebar.module.ts @@ -20,9 +20,6 @@ import {MatDividerModule} from "@angular/material/divider"; import {FlexModule} from "@angular/flex-layout"; import {MatSelectModule} from "@angular/material/select"; import {MatInputModule} from "@angular/material/input"; -import { - TagFilterListItemComponent -} from "./file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component"; import {SortDialogComponent} from "./file-search/sort-dialog/sort-dialog.component"; import {FilterDialogComponent} from "./file-search/filter-dialog/filter-dialog.component"; import {MatListModule} from "@angular/material/list"; @@ -46,13 +43,13 @@ import {TagQueryItemComponent} from "./file-search/filter-expression-item/tag-qu import { PropertyQueryItemComponent } from "./file-search/filter-expression-item/property-query-item/property-query-item.component"; +import { FilterExpressionListItemComponent } from './file-search/filter-dialog/filter-expression-list-item/filter-expression-list-item.component'; @NgModule({ declarations: [ TagEditComponent, FileSearchComponent, - TagFilterListItemComponent, SortDialogComponent, FilterDialogComponent, FileImportComponent, @@ -62,6 +59,7 @@ import { FilterExpressionItemComponent, TagQueryItemComponent, PropertyQueryItemComponent, + FilterExpressionListItemComponent, ], exports: [ TagEditComponent, diff --git a/mediarepo-ui/src/app/utils/list-utils.ts b/mediarepo-ui/src/app/utils/list-utils.ts new file mode 100644 index 0000000..f3efb9e --- /dev/null +++ b/mediarepo-ui/src/app/utils/list-utils.ts @@ -0,0 +1,15 @@ +export function enumerate(list: T[]): [number, T][] { + const enumeratedEntries = []; + + for (let i = 0; i < list.length; i++) { + enumeratedEntries.push([i, list[i]] as [number, T]); + } + return enumeratedEntries; +} + +export function removeByValue(list: T[], entry: T) { + const index = list.indexOf(entry); + if (index >= 0) { + list.splice(index, 1); + } +} diff --git a/mediarepo-ui/yarn.lock b/mediarepo-ui/yarn.lock index c67318b..9ae0983 100644 --- a/mediarepo-ui/yarn.lock +++ b/mediarepo-ui/yarn.lock @@ -18,7 +18,7 @@ "@angular-devkit/core" "13.1.3" rxjs "6.6.7" -"@angular-devkit/build-angular@~13.1.2": +"@angular-devkit/build-angular@~13.1.3": version "13.1.3" resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-13.1.3.tgz#c04cef8a2d405cb66332b674d204a2717b6807f6" integrity sha512-C5Qv8aGmpGbETG4Mawly/5LnkRwfJAzANL5BtYJn8ZaDlZKCkhvAaRXHpm4Mdqg5idACAT8hgYqPQvqyEBaVDA== @@ -178,14 +178,14 @@ "@angular-eslint/bundled-angular-compiler" "13.0.1" "@typescript-eslint/experimental-utils" "5.3.0" -"@angular/animations@~13.1.1": +"@angular/animations@~13.1.2": version "13.1.2" resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-13.1.2.tgz#fdf0776eaf053b14a4118c682a62f24e4192609a" integrity sha512-k1eQ8YZq3eelLhJDQjkRCt/4MXxwK2TFeGdtcYJF0G7vFOppE8hlI4PT7Bvmk08lTqvgiqtTI3ZaYmIINLfUMg== dependencies: tslib "^2.3.0" -"@angular/cdk@^13.1.1": +"@angular/cdk@^13.1.2": version "13.1.2" resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-13.1.2.tgz#aaa1b577d1b8101d3d59f4da9a1ea51b7f7a5191" integrity sha512-xORyqvfM0MueJpxHxVi3CR/X/f1RPKr45vt7NV6/x91OTnh2ukwxg++dAGuA6M5gUAHcVAcaBrfju4GQlU9hmg== @@ -194,7 +194,7 @@ optionalDependencies: parse5 "^5.0.0" -"@angular/cli@~13.1.2": +"@angular/cli@~13.1.3": version "13.1.3" resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-13.1.3.tgz#d143f30ee67481cc315e0d18fecb076101dfa280" integrity sha512-Ju/A8LFnfcv1PC665a5FiIQx9SXqB+3yWYFXPIiVkkRcye95gpfsbV48WW7QV35gzIwbR1m3H907Zg6ptiNv0A== @@ -219,14 +219,14 @@ symbol-observable "4.0.0" uuid "8.3.2" -"@angular/common@~13.1.1": +"@angular/common@~13.1.2": version "13.1.2" resolved "https://registry.yarnpkg.com/@angular/common/-/common-13.1.2.tgz#6a4abe30b1cc42702452bfd2214e482675f5d889" integrity sha512-/8RWYQkZ1KPNvu2FANJM44wXlOMjMyxZVOEIn3llMRgxV2iiYtmluAOJNafTAbKedAuD6wiSpbi++QbioqCyyA== dependencies: tslib "^2.3.0" -"@angular/compiler-cli@~13.1.1": +"@angular/compiler-cli@~13.1.2": version "13.1.2" resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-13.1.2.tgz#f9adde80bd9d0c3d90d8758c9803537373259053" integrity sha512-yqM6RLcYtfwIuqBQ7eS7WdksBYY7Dh9sP4rElgLiEhDGIPQf6YE5zeuRThGq5pQ2fvHbNflw8QmTHu/18Y1u/g== @@ -243,14 +243,14 @@ tslib "^2.3.0" yargs "^17.2.1" -"@angular/compiler@~13.1.1": +"@angular/compiler@~13.1.2": version "13.1.2" resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-13.1.2.tgz#86afbe282d0ff407fd8aeb66a79a804f40e7efa4" integrity sha512-xbM3eClhUIHEFR0Et1bVC18Q7+kJx+hNNWWQl63RNYYBxTZnZpXA3mYi6IcEasy7BHkobVW+5teqlibFQY4gfQ== dependencies: tslib "^2.3.0" -"@angular/core@~13.1.1": +"@angular/core@~13.1.2": version "13.1.2" resolved "https://registry.yarnpkg.com/@angular/core/-/core-13.1.2.tgz#793b97d0b7339d5b405f39dd5d021b4b78fcf256" integrity sha512-dsb90lUf8BELzdg7MgSMfPc36xzZKsDggOimfXhIvmctgc+H71Zo07KYTy5JVqsscLdT+A/KBvtU1bKk4P+Rfg== @@ -264,35 +264,35 @@ dependencies: tslib "^2.3.0" -"@angular/forms@~13.1.1": +"@angular/forms@~13.1.2": version "13.1.2" resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-13.1.2.tgz#f72d7f84b78844a1606cd4226c2a3a1eb1de56b5" integrity sha512-r5I5cPngk2Erxe/OEL9Hl1j1VcNSAAyVzh7KmtOP8z7RZYCd0MeRISKrmA5CGn5Dh7A5POFLoOpBatmvnc4Z/A== dependencies: tslib "^2.3.0" -"@angular/material@^13.1.1": +"@angular/material@^13.1.2": version "13.1.2" resolved "https://registry.yarnpkg.com/@angular/material/-/material-13.1.2.tgz#497e9b34f4672ce207bb1198a823cda1f1d416ef" integrity sha512-M7eDgTMCZ/naoiS6Z5nj3N/sNUFc+CGPHX4yb563RuknqN7huDCvdyxA6KnhYLZsVlNCPh5ZrEr6H8ZiYJWcpg== dependencies: tslib "^2.3.0" -"@angular/platform-browser-dynamic@~13.1.1": +"@angular/platform-browser-dynamic@~13.1.2": version "13.1.2" resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-13.1.2.tgz#2d381503862be7a9d5fd74a27c1f8cf10d9b086e" integrity sha512-gABOn8DxGai56WmIt5o+eXtduabiq4Mlprg+6+dv+2PvWV871pLvswV9EGUSgwKXvbhBlDZDuNFU5LgvNDuGFg== dependencies: tslib "^2.3.0" -"@angular/platform-browser@~13.1.1": +"@angular/platform-browser@~13.1.2": version "13.1.2" resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-13.1.2.tgz#6b24c26cc01733f933a3c15288989259f83e8f46" integrity sha512-yBUWtYJHr/1LuK3/YRRav2O82i6RHVPtRoAlZHoeTlh2CYA4u1m3JHq9XBrxIxSXexBX69pMrZENW1xskwKRTQ== dependencies: tslib "^2.3.0" -"@angular/router@~13.1.1": +"@angular/router@~13.1.2": version "13.1.2" resolved "https://registry.yarnpkg.com/@angular/router/-/router-13.1.2.tgz#69146055473b9f5b8f9ba9b4de3a0740778ea174" integrity sha512-5S0De6SdlbERoX9FwOBiTWxINchW7nTPUIH/tdanOqq12cqp6/7NigOr3BZDSvUNIh/6Is+pSQTKGAbhxejN2w== @@ -1346,24 +1346,24 @@ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-1.0.0.tgz#3fdf5798f0b49e90155896f6291df186eac06c83" integrity sha512-9oLAnygRMi8Q5QkYEU4XWK04B+nuoXoxjRvRxgjuChkLZFBja0YPSgdZ7dZtwhncLBcQe/I/E+fLuk5qxcYVJA== -"@ng-icons/core@^13.2.0": +"@ng-icons/core@^13.2.1": version "13.2.1" resolved "https://registry.yarnpkg.com/@ng-icons/core/-/core-13.2.1.tgz#46d1d97d3d479da2fd2ca3b58978715f6b261338" integrity sha512-4wI60pGxD9ul9kEipY8Ph0cza6d71xAJj6eE1fqFDtTQ4DDUfSSGFbB8grxs9qUec5B9QCLk8QoLaAWEzyDn8Q== dependencies: tslib "^2.2.0" -"@ng-icons/feather-icons@^13.2.0": +"@ng-icons/feather-icons@^13.2.1": version "13.2.1" resolved "https://registry.yarnpkg.com/@ng-icons/feather-icons/-/feather-icons-13.2.1.tgz#1a937711f0b11aa505914a84abec685dfebf1bf6" integrity sha512-Uvassb3YS1bkQyrkHrUAimf5G7sAC9EzjZUSEbb23zsTYqp8R2Z4rUcJJkhzC/xZecKLhRqldQlKFsRVoo+Iug== dependencies: tslib "^2.2.0" -"@ng-icons/material-icons@13.1.0": - version "13.1.0" - resolved "https://registry.yarnpkg.com/@ng-icons/material-icons/-/material-icons-13.1.0.tgz#b42df8ebe55e7f3671e61056a535ff9651e0be76" - integrity sha512-9G3zCRueBTU1VRKjxw5ZGh77eH7i/qp+urI7rKYlyCSky+1arLhpsMFIdz/EoMrPTda6x3n61b0BZ+OUVMJJiA== +"@ng-icons/material-icons@^13.2.1": + version "13.2.1" + resolved "https://registry.yarnpkg.com/@ng-icons/material-icons/-/material-icons-13.2.1.tgz#4dcfd26f60f6c0c23fff28abc7f6c66931fb264e" + integrity sha512-YvIrLD6n/BuJTPRyNh3K9WBCid5MfstbFgkhGUeVOySlNriVMClusCL5KEwxq4xtFk7Q7pYkT8sIKd3x8E3SDA== dependencies: tslib "^2.2.0"