Repair filter dialog to accept rich expressions

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/4/head
trivernis 2 years ago
parent 2f5e6d8bcb
commit c2dbf6846c

@ -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",

@ -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;

@ -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));
}
}

@ -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,

@ -0,0 +1,3 @@
<div (click)="this.onClick()" [class.selected]="this.selected" class="selectable">
<ng-content></ng-content>
</div>

@ -0,0 +1,7 @@
.selectable.selected {
background-color: #5c5c5c;
}
body {
cursor: pointer;
}

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SelectableComponent } from './selectable.component';
describe('SelectableComponent', () => {
let component: SelectableComponent;
let fixture: ComponentFixture<SelectableComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SelectableComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SelectableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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<this>();
@Output() appUnselect = new EventEmitter<this>();
constructor() {
}
public onClick(): void {
this.selected = !this.selected;
if (this.selected) {
this.appSelect.emit(this);
} else {
this.appUnselect.emit(this);
}
}
}

@ -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,

@ -2,26 +2,30 @@
<div class="filter-dialog-content" mat-dialog-content>
<div class="filter-dialog-list">
<mat-list class="mat-filter-dialog-list">
<mat-list-item *ngFor="let expression of filters" [class.selected]="expression.selected"
class="filter-list-item">
<app-tag-filter-list-item (contextmenu)="contextMenu.onContextMenu($event)"
(querySelect)="this.addToSelection($event)"
(queryUnselect)="this.removeFromSelection($event)"
(removeClicked)="this.removeFilter($event)"
[expression]="expression"></app-tag-filter-list-item>
<mat-list-item *ngFor="let entry of this.renderedFilterEntries" class="filter-list-item">
<app-filter-expression-list-item (appSelect)="this.entrySelect(entry[0])"
(appUnselect)="this.entryUnselect(entry[0])"
(contextmenu)="contextMenu.onContextMenu($event)"
(entrySelect)="this.entrySelect(entry[0], $event[0])"
(entryUnselect)="this.entryUnselect(entry[0], $event[0])"
[filter]="entry[1]"
></app-filter-expression-list-item>
</mat-list-item>
</mat-list>
</div>
<mat-divider></mat-divider>
<app-tag-input [allowWildcards]="true" (tagAdded)="this.addFilter($event)" [allowNegation]="true" [availableTags]="this.availableTags"
class="tag-input"></app-tag-input>
<app-filter-input (filterAdded)="this.addFilter($event)"
[availableTags]="this.availableTags"
class="filter-input"></app-filter-input>
</div>
<div class="dialog-actions" mat-dialog-actions>
<button (click)="confirmFilter()" color="primary" mat-flat-button>Filter</button>
<button (click)="cancelFilter()" color="accent" mat-stroked-button>Cancel</button>
</div>
<app-context-menu #contextMenu>
<button (click)="this.convertSelectionToOrExpression()" mat-menu-item>Copy to OR-Expression</button>
<button (click)="this.convertSelectionToAndExpression()" mat-menu-item>Copy to AND-Expression</button>
<button (click)="this.invertSelection()" mat-menu-item>Invert</button>
<button (click)="this.removeSelectedFilters()" mat-menu-item>Remove</button>
<button (click)="this.createAndFromSelection(true)" mat-menu-item>Convert selection to AND</button>
<button (click)="this.createOrFromSelection(true)" mat-menu-item>Convert selection to OR</button>
<button (click)="this.createOrFromSelection(false)" mat-menu-item>Copy selection to OR</button>
<button (click)="this.createAndFromSelection(false)" mat-menu-item>Copy selection to AND</button>
</app-context-menu>

@ -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%;

@ -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<T> = {
[key: number]: T
};
@Component({
selector: "app-filter-dialog",
templateUrl: "./filter-dialog.component.html",
styleUrls: ["./filter-dialog.component.scss"]
})
export class FilterDialogComponent {
public filters: Selectable<GenericFilter>[];
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<number[]> = {};
constructor(public dialogRef: MatDialogRef<SortDialogComponent>, @Inject(
MAT_DIALOG_DATA) data: any) {
this.filters = data.filterEntries.map(
(f: GenericFilter) => new Selectable<GenericFilter>(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<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 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<GenericFilter>(
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<GenericFilter>(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<GenericFilter>[] = [];
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());
}
}

@ -0,0 +1,26 @@
<span *ngIf="this.orExpression" class="or-expression">
<mat-list>
<mat-list-item *ngFor="let entry of this.orExpression"
[class.selected]="componentSelectable.selected"
class="or-filter-list-item">
<app-selectable #componentSelectable
(appSelect)="this.entrySelect.emit(entry)"
(appUnselect)="this.entryUnselect.emit(entry)">
<app-property-query-item *ngIf="this.queryIs(entry[1], 'Property')"
[propertyQuery]="this.propertyQuery(entry[1]).Property"></app-property-query-item>
<app-tag-query-item *ngIf="this.queryIs(entry[1], 'Tag')"
[tagQuery]="this.tagQuery(entry[1]).Tag"></app-tag-query-item>
</app-selectable>
</mat-list-item>
</mat-list>
</span>
<span *ngIf="this.query" [class.selected]="singleSelectable.selected" class="query">
<app-selectable #singleSelectable
(appSelect)="this.appSelect.emit(this.query)"
(appUnselect)="this.appUnselect.emit(this.query)">
<app-property-query-item *ngIf="this.queryIs(this.query, 'Property')"
[propertyQuery]="this.propertyQuery(this.query).Property"></app-property-query-item>
<app-tag-query-item *ngIf="this.queryIs(this.query, 'Tag')"
[tagQuery]="this.tagQuery(this.query).Tag"></app-tag-query-item>
</app-selectable>
</span>

@ -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%;
}

@ -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<FilterExpressionListItemComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FilterExpressionListItemComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FilterExpressionListItemComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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<FilterQuery>();
@Output() appUnselect = new EventEmitter<FilterQuery>();
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;
}
}
}

@ -1,22 +0,0 @@
<div (click)="onSelect()" *ngIf="expression.data.filter_type === 'Query'">
{{expression.data.getDisplayName()}}
<button (click)="this.removeClicked.emit(this)" class="remove-button" mat-button>
<ng-icon name="mat-remove"></ng-icon>
</button>
</div>
<div *ngIf="expression.data.filter_type === 'OrExpression'">
<mat-list>
<mat-list-item (mousedown)="$event.button === 0 && this.selectInnerIndex(entry[0])"
*ngFor="let entry of enumerate(this.expression.data.queryList())"
[class.selected]="this.selectedIndices.includes(entry[0])"
class="or-filter-list-item">
<span *ngIf="entry[0] > 0" class="or-span">OR</span>
{{entry[1].getNormalizedTag()}}
<button (mousedown)="$event.button === 0 && this.removeOrExpression(entry[0])"
class="remove-button-inner-list"
mat-button>
<ng-icon name="mat-remove"></ng-icon>
</button>
</mat-list-item>
</mat-list>
</div>

@ -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<TagFilterListItemComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TagFilterListItemComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TagFilterListItemComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -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<GenericFilter>;
@Output() removeClicked = new EventEmitter<TagFilterListItemComponent>();
@Output() querySelect = new EventEmitter<TagQuery>();
@Output() queryUnselect = new EventEmitter<TagQuery>();
public selectedIndices: number[] = [];
constructor() {
}
public ngOnChanges(changes: SimpleChanges): void {
if (changes["expression"]) {
this.selectedIndices = [];
}
}
public enumerate<T>(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);
}
}
}

@ -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,

@ -0,0 +1,15 @@
export function enumerate<T>(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<T>(list: T[], entry: T) {
const index = list.indexOf(entry);
if (index >= 0) {
list.splice(index, 1);
}
}

@ -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"

Loading…
Cancel
Save