|
|
@ -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 {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
|
|
|
import {SortDialogComponent} from "../sort-dialog/sort-dialog.component";
|
|
|
|
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 {Tag} from "../../../../../../api/models/Tag";
|
|
|
|
import {
|
|
|
|
import {SearchFilters} from "../../../../../../api/models/SearchFilters";
|
|
|
|
TagFilterListItemComponent
|
|
|
|
import {FilterExpression, FilterQuery} from "../../../../../../api/api-types/files";
|
|
|
|
} from "./tag-filter-list-item/tag-filter-list-item.component";
|
|
|
|
import {enumerate, removeByValue} from "../../../../../utils/list-utils";
|
|
|
|
import {Selectable} from "../../../../../models/Selectable";
|
|
|
|
|
|
|
|
|
|
|
|
type IndexableSelection<T> = {
|
|
|
|
|
|
|
|
[key: number]: T
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
@Component({
|
|
|
|
selector: "app-filter-dialog",
|
|
|
|
selector: "app-filter-dialog",
|
|
|
|
templateUrl: "./filter-dialog.component.html",
|
|
|
|
templateUrl: "./filter-dialog.component.html",
|
|
|
|
styleUrls: ["./filter-dialog.component.scss"]
|
|
|
|
styleUrls: ["./filter-dialog.component.scss"]
|
|
|
|
})
|
|
|
|
})
|
|
|
|
export class FilterDialogComponent {
|
|
|
|
export class FilterDialogComponent implements OnChanges {
|
|
|
|
|
|
|
|
|
|
|
|
public filters: Selectable<GenericFilter>[];
|
|
|
|
|
|
|
|
public availableTags: Tag[] = [];
|
|
|
|
public availableTags: Tag[] = [];
|
|
|
|
public mode: "AND" | "OR" = "AND";
|
|
|
|
public filters = new SearchFilters([]);
|
|
|
|
|
|
|
|
public renderedFilterEntries: [number, FilterExpression][] = [];
|
|
|
|
@ViewChildren(
|
|
|
|
private selectedIndices: IndexableSelection<number[]> = {};
|
|
|
|
TagFilterListItemComponent) filterListItems!: TagFilterListItemComponent[];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private selectedQueries: TagQuery[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(public dialogRef: MatDialogRef<SortDialogComponent>, @Inject(
|
|
|
|
constructor(public dialogRef: MatDialogRef<SortDialogComponent>, @Inject(
|
|
|
|
MAT_DIALOG_DATA) data: any) {
|
|
|
|
MAT_DIALOG_DATA) data: any) {
|
|
|
|
this.filters = data.filterEntries.map(
|
|
|
|
|
|
|
|
(f: GenericFilter) => new Selectable<GenericFilter>(f,
|
|
|
|
|
|
|
|
false)) ?? [];
|
|
|
|
|
|
|
|
this.availableTags = data.availableTags ?? [];
|
|
|
|
this.availableTags = data.availableTags ?? [];
|
|
|
|
|
|
|
|
this.filters = data.filters;
|
|
|
|
|
|
|
|
this.buildRenderedEntries();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static checkFiltersEqual(l: GenericFilter, r: GenericFilter): boolean {
|
|
|
|
public ngOnChanges(changes: SimpleChanges): void {
|
|
|
|
const lTags = l.queryList().map(q => q.getNormalizedTag()).sort();
|
|
|
|
if (changes["filters"]) {
|
|
|
|
const rTags = r.queryList().map(q => q.getNormalizedTag()).sort();
|
|
|
|
this.buildRenderedEntries();
|
|
|
|
let match = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (lTags.length == rTags.length) {
|
|
|
|
|
|
|
|
match = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const tag of lTags) {
|
|
|
|
|
|
|
|
match = rTags.includes(tag);
|
|
|
|
|
|
|
|
if (!match) {
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return match;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public cancelFilter(): void {
|
|
|
|
public cancelFilter(): void {
|
|
|
@ -60,118 +39,105 @@ export class FilterDialogComponent {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public confirmFilter(): void {
|
|
|
|
public confirmFilter(): void {
|
|
|
|
this.dialogRef.close(this.filters.map(f => f.data));
|
|
|
|
this.dialogRef.close(this.filters);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public removeFilter(event: TagFilterListItemComponent): void {
|
|
|
|
public entrySelect(index: number, subindex: number = -1): void {
|
|
|
|
const filter = event.expression;
|
|
|
|
this.selectedIndices[index] = this.selectedIndices[index] ?? [];
|
|
|
|
const index = this.filters.findIndex(f => f === filter);
|
|
|
|
this.selectedIndices[index].push(subindex);
|
|
|
|
if (index >= 0) {
|
|
|
|
|
|
|
|
this.filters.splice(index, 1);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
this.unselectAll();
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public addFilter(tag: string) {
|
|
|
|
public entryUnselect(index: number, subindex: number = -1): void {
|
|
|
|
const query = TagQuery.fromString(tag);
|
|
|
|
this.selectedIndices[index] = this.selectedIndices[index] ?? [];
|
|
|
|
|
|
|
|
removeByValue(this.selectedIndices[index], subindex);
|
|
|
|
if (this.mode === "AND" || this.filters.length === 0) {
|
|
|
|
|
|
|
|
this.filters.push(
|
|
|
|
|
|
|
|
new Selectable<GenericFilter>(
|
|
|
|
|
|
|
|
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<GenericFilter>(filterExpression,
|
|
|
|
|
|
|
|
false));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
this.unselectAll();
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public addToSelection(query: TagQuery): void {
|
|
|
|
public addFilter(expression: FilterExpression): void {
|
|
|
|
this.selectedQueries.push(query);
|
|
|
|
this.filters.addFilterExpression(expression);
|
|
|
|
|
|
|
|
this.buildRenderedEntries();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public removeFromSelection(query: TagQuery): void {
|
|
|
|
public removeSelectedFilters(): void {
|
|
|
|
const index = this.selectedQueries.indexOf(query);
|
|
|
|
const orderedIndices = Object.keys(this.selectedIndices).map(k => Number(k)).sort().reverse();
|
|
|
|
if (index > 0) {
|
|
|
|
|
|
|
|
this.selectedQueries.splice(index, 1);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public unselectAll() {
|
|
|
|
for (const indexStr of orderedIndices) {
|
|
|
|
this.filters.forEach(filter => filter.selected = false);
|
|
|
|
const index = indexStr;
|
|
|
|
this.selectedQueries = [];
|
|
|
|
const subIndices: number[] = this.selectedIndices[index];
|
|
|
|
this.filterListItems.forEach(i => i.selectedIndices = []);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public convertSelectionToAndExpression(): void {
|
|
|
|
if (subIndices.length === 1 && subIndices[0] === -1) {
|
|
|
|
for (const query of this.selectedQueries) {
|
|
|
|
this.filters.removeFilterAtIndex(index);
|
|
|
|
this.filters.push(
|
|
|
|
} else if (subIndices.length > 0) {
|
|
|
|
new Selectable<GenericFilter>(
|
|
|
|
for (const subIndex of subIndices.sort().reverse()) { // need to remove from the top down to avoid index shifting
|
|
|
|
new SingleFilterExpression(query),
|
|
|
|
this.filters.removeSubfilterAtIndex(index, subIndex);
|
|
|
|
false));
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.removeFilterDuplicates();
|
|
|
|
this.selectedIndices = {};
|
|
|
|
this.unselectAll();
|
|
|
|
this.buildRenderedEntries();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public convertSelectionToOrExpression(): void {
|
|
|
|
public createAndFromSelection(deleteOriginal: boolean): void {
|
|
|
|
const queries = this.selectedQueries;
|
|
|
|
const expressions: FilterExpression[] = [];
|
|
|
|
const expression = new OrFilterExpression(queries);
|
|
|
|
|
|
|
|
this.filters.push(new Selectable<GenericFilter>(expression, false));
|
|
|
|
for (const indexStr in this.selectedIndices) {
|
|
|
|
this.removeFilterDuplicates();
|
|
|
|
const index = Number(indexStr);
|
|
|
|
this.unselectAll();
|
|
|
|
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 {
|
|
|
|
public createOrFromSelection(deleteOriginal: boolean): void {
|
|
|
|
this.selectedQueries.forEach(query => query.negate = !query.negate);
|
|
|
|
const queries: FilterQuery[] = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private removeFilterDuplicates() {
|
|
|
|
for (const indexStr in this.selectedIndices) {
|
|
|
|
const filters = this.filters;
|
|
|
|
const index = Number(indexStr);
|
|
|
|
let newFilters: Selectable<GenericFilter>[] = [];
|
|
|
|
const subindices = this.selectedIndices[index];
|
|
|
|
|
|
|
|
|
|
|
|
for (const filterItem of filters) {
|
|
|
|
if (subindices.length === 1 && subindices[0] === -1) {
|
|
|
|
if (filterItem.data.filter_type == "OrExpression") {
|
|
|
|
const filterEntry = this.filters.getFilters()[index];
|
|
|
|
(filterItem.data as OrFilterExpression).removeDuplicates();
|
|
|
|
if ("Query" in filterEntry) {
|
|
|
|
}
|
|
|
|
queries.push(filterEntry.Query);
|
|
|
|
if (newFilters.findIndex(
|
|
|
|
}
|
|
|
|
f => FilterDialogComponent.checkFiltersEqual(f.data,
|
|
|
|
} else {
|
|
|
|
filterItem.data)) < 0) {
|
|
|
|
for (const subIndex of subindices) {
|
|
|
|
if (filterItem.data.filter_type == "OrExpression" && filterItem.data.queryList().length === 1) {
|
|
|
|
const query = this.filters.getSubfilterAtIndex(index, subIndex);
|
|
|
|
filterItem.data = new SingleFilterExpression(
|
|
|
|
if (query) {
|
|
|
|
filterItem.data.queryList()[0]);
|
|
|
|
queries.push(query);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
newFilters.push(filterItem);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.filters = newFilters;
|
|
|
|
if (deleteOriginal) {
|
|
|
|
}
|
|
|
|
this.removeSelectedFilters();
|
|
|
|
|
|
|
|
} else {
|
|
|
|
@HostListener("window:keydown", ["$event"])
|
|
|
|
this.selectedIndices = {};
|
|
|
|
private async handleKeydownEvent(event: KeyboardEvent) {
|
|
|
|
|
|
|
|
if (event.key === "Shift") {
|
|
|
|
|
|
|
|
this.mode = "OR";
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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 buildRenderedEntries() {
|
|
|
|
private async handleKeyupEvent(event: KeyboardEvent) {
|
|
|
|
this.renderedFilterEntries = enumerate(this.filters.getFilters());
|
|
|
|
if (event.key === "Shift") {
|
|
|
|
|
|
|
|
this.mode = "AND";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|