Improve filter dialog style

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

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

@ -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<string[]>;
@Input() availableTags: Tag[] = [];
@Input() validTags: string[] = [];
@Output() searchStartEvent = new EventEmitter<void>();
@Output() searchEndEvent = new EventEmitter<void>();
@ -148,7 +150,7 @@ export class FileSearchComponent implements AfterViewChecked, OnInit {
minWidth: "25vw",
data: {
filterEntries,
validTags: this.validTags,
availableTags: this.availableTags,
},
disableClose: true,
});

@ -2,19 +2,12 @@
<div mat-dialog-content>
<mat-list>
<mat-list-item class="filter-list-item" *ngFor="let expression of filters">
<app-tag-filter-list-item [expression]="expression"></app-tag-filter-list-item>
<app-tag-filter-list-item (removeClicked)="this.removeFilter($event)" [expression]="expression"></app-tag-filter-list-item>
</mat-list-item>
</mat-list>
<mat-divider></mat-divider>
<mat-form-field class="tag-input">
<mat-label>Enter tags to filter for</mat-label>
<input #tagInput matInput [formControl]="formControl" [matAutocomplete]="auto" (keydown.enter)="this.addFilterByInput()">
<mat-autocomplete #auto (optionSelected)="addFilterByAutocomplete($event)">
<mat-option *ngFor="let tag of suggestionTags | async" [value]="tag">
{{tag}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<app-tag-input class="tag-input" [allowNegation]="true" [availableTags]="this.availableTags"
(tagAdded)="this.addFilter($event)"></app-tag-input>
</div>
<div class="dialog-actions" mat-dialog-actions>
<button mat-flat-button color="primary" (click)="confirmFilter()">Filter</button>

@ -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<string[]>;
public validTags: string[] = [];
public formControl = new FormControl();
public filters: Selectable<FilterExpression>[];
public availableTags: Tag[] = [];
public mode: "AND" | "OR" = "AND";
@ViewChild("tagInput") tagInput!: ElementRef<HTMLInputElement>;
constructor(public dialogRef: MatDialogRef<SortDialogComponent>, @Inject(
MAT_DIALOG_DATA) data: any) {
this.filters = data.filterEntries;
this.validTags = data.validTags;
this.suggestionTags = this.formControl.valueChanges.pipe(startWith(null),
map(
(tag: string | null) => tag ? this.filterSuggestionTag(
tag) : this.validTags.slice(0, 20)));
this.filters = data.filterEntries.map((f: FilterExpression) => new Selectable<FilterExpression>(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<FilterExpression>(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<FilterExpression>(new OrFilterExpression(queryList), false));
}
}

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

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

@ -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<FilterExpression>;
@Output() removeClicked = new EventEmitter<TagFilterListItemComponent>();
constructor() { }
ngOnInit(): void {
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]);
}
}
}

@ -0,0 +1,15 @@
<mat-form-field>
<mat-label>
Enter a tag
</mat-label>
<input #tagInput
[formControl]="formControl"
matInput
(keydown.enter)="addTagByInput($event)"
[matAutocomplete]="auto">
<mat-autocomplete #auto (optionSelected)="addTagByAutocomplete($event)">
<mat-option *ngFor="let tag of autosuggestTags | async" [value]="tag">
{{tag}}
</mat-option>
</mat-autocomplete>
</mat-form-field>

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

@ -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<string>();
@ViewChild("tagInput") tagInput!: ElementRef<HTMLInputElement>;
public formControl = new FormControl();
public autosuggestTags: Observable<string[]>;
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)
}
}
}

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

@ -5,7 +5,8 @@
<div id="file-search-input">
<app-file-search #filesearch (searchEndEvent)="this.searchEndEvent.emit()"
(searchStartEvent)="this.searchStartEvent.emit()"
[validTags]="this.getValidTagsForSearch()"></app-file-search>
[validTags]="this.getValidTagsForSearch()"
[availableTags]="this.tagsOfFiles"></app-file-search>
</div>
<mat-divider fxFlex="1em"></mat-divider>
<div class="tag-list-header" fxFlex="40px">

Loading…
Cancel
Save