Improve filter expression input and fix OR-expression display

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/4/head
trivernis 3 years ago
parent 605e6e7a50
commit bab9782203

@ -64,7 +64,11 @@
}, },
"development": { "development": {
"buildOptimizer": false, "buildOptimizer": false,
"optimization": false, "optimization": {
"fonts": false,
"styles": false,
"scripts": true
},
"vendorChunk": true, "vendorChunk": true,
"extractLicenses": false, "extractLicenses": false,
"sourceMap": true, "sourceMap": true,

@ -3,7 +3,7 @@
"version": "0.12.0", "version": "0.12.0",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve --configuration production", "start": "ng serve",
"build": "ng build", "build": "ng build",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"watch-prod": "ng build --watch --configuration production", "watch-prod": "ng build --watch --configuration production",

@ -1,4 +1,4 @@
import {FileStatus, FilterQuery, PropertyQuery, ValueComparator} from "../api-types/files"; import {FileStatus, FilterExpression, FilterQuery, PropertyQuery, ValueComparator} from "../api-types/files";
export type Comparator = "Less" | "Equal" | "Greater" | "Between"; export type Comparator = "Less" | "Equal" | "Greater" | "Between";
export type PropertyType = export type PropertyType =
@ -60,7 +60,20 @@ export class FilterQueryBuilder {
return filterQuery({ Id: id }); return filterQuery({ Id: id });
} }
public static buildFilterFromString(filterStr: string): FilterQuery { public static buildFilterExpressionsFromString(expressionStr: string): FilterExpression | undefined {
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) {
return { OrExpression: queries };
} else if (queries.length == 1) {
return { Query: queries[0] };
} else {
return undefined;
}
}
public static buildFilterFromString(filterStr: string): FilterQuery | undefined {
filterStr = filterStr.trim(); filterStr = filterStr.trim();
if (filterStr.startsWith(".")) { if (filterStr.startsWith(".")) {
@ -119,7 +132,6 @@ export class FilterQueryBuilder {
): FilterQuery | undefined { ): FilterQuery | undefined {
const property = this.parsePropertyName(propertyName); const property = this.parsePropertyName(propertyName);
const comparator = this.parseComparator(rawComparator); const comparator = this.parseComparator(rawComparator);
console.log("Parts: ", propertyName, rawComparator, compareValue);
if (property && comparator) { if (property && comparator) {
let value; let value;

@ -3,14 +3,15 @@
Enter a filter expression Enter a filter expression
</mat-label> </mat-label>
<input <input
(keydown.enter)="addFilterByInput()" (keydown.enter)="addExpressionByInput()"
[formControl]="formControl" [formControl]="formControl"
[matAutocomplete]="auto" [matAutocomplete]="auto"
matInput> matInput>
<ng-content></ng-content> <ng-content></ng-content>
<mat-autocomplete #auto (optionSelected)="addFilterByAutocomplete($event)"> <mat-autocomplete #auto
<mat-option *ngFor="let tag of autosuggestFilters | async" [value]="tag"> (optionSelected)="this.skipEnterOnce = true">
{{tag}} <mat-option *ngFor="let filter of autosuggestFilters | async" [value]="filter.value">
{{filter.display}}
</mat-option> </mat-option>
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>

@ -2,12 +2,16 @@ import {Component, EventEmitter, Input, OnChanges, Output, SimpleChanges} from "
import {Observable} from "rxjs"; import {Observable} from "rxjs";
import {FormControl} from "@angular/forms"; import {FormControl} from "@angular/forms";
import {Tag} from "../../../../../api/models/Tag"; import {Tag} from "../../../../../api/models/Tag";
import {FilterExpression} from "../../../../../api/api-types/files"; import {FilterExpression, FilterQuery} from "../../../../../api/api-types/files";
import {debounceTime, map, startWith} from "rxjs/operators"; import {debounceTime, map, startWith} from "rxjs/operators";
import {compareSearchResults} from "../../../../utils/compare-utils"; import {compareSearchResults} from "../../../../utils/compare-utils";
import {MatAutocompleteSelectedEvent} from "@angular/material/autocomplete";
import {FilterQueryBuilder} from "../../../../../api/models/FilterQueryBuilder"; import {FilterQueryBuilder} from "../../../../../api/models/FilterQueryBuilder";
type AutocompleteEntry = {
value: string,
display: string,
};
@Component({ @Component({
selector: "app-filter-input", selector: "app-filter-input",
templateUrl: "./filter-input.component.html", templateUrl: "./filter-input.component.html",
@ -18,9 +22,11 @@ export class FilterInputComponent implements OnChanges {
@Input() availableTags: Tag[] = []; @Input() availableTags: Tag[] = [];
@Output() filterAdded = new EventEmitter<FilterExpression>(); @Output() filterAdded = new EventEmitter<FilterExpression>();
public autosuggestFilters: Observable<string[]>; public autosuggestFilters: Observable<AutocompleteEntry[]>;
public formControl = new FormControl(); public formControl = new FormControl();
public skipEnterOnce = false;
private propertyQueriesWithValues: { [key: string]: (string | undefined)[] } = { private propertyQueriesWithValues: { [key: string]: (string | undefined)[] } = {
".status": ["imported", "archived", "deleted"], ".status": ["imported", "archived", "deleted"],
".fileSize": [undefined], ".fileSize": [undefined],
@ -42,7 +48,12 @@ export class FilterInputComponent implements OnChanges {
this.autosuggestFilters = this.formControl.valueChanges.pipe( this.autosuggestFilters = this.formControl.valueChanges.pipe(
startWith(null), startWith(null),
debounceTime(250), debounceTime(250),
map((value) => value ? this.filterAutosuggestFilters(value) : this.tagsForAutocomplete.slice(0, 20)) map((value) => value ? this.filterAutosuggestFilters(value) : this.tagsForAutocomplete.slice(
0,
20
).map(t => {
return { value: t, display: this.buildAutocompleteValue(t) };
}))
); );
this.tagsForAutocomplete = this.availableTags.map( this.tagsForAutocomplete = this.availableTags.map(
t => t.getNormalizedOutput()); t => t.getNormalizedOutput());
@ -55,34 +66,62 @@ export class FilterInputComponent implements OnChanges {
} }
} }
public addFilterByInput(): void { public addExpressionByInput(): void {
const filter = FilterQueryBuilder.buildFilterFromString(this.formControl.value); if (this.skipEnterOnce) {
this.skipEnterOnce = false; // workaround to be able to listen to enter (because change is unrelieable) while still allowing enter in autocomplete
return;
}
const expressions = FilterQueryBuilder.buildFilterExpressionsFromString(this.formControl.value);
console.log(this.formControl.value, expressions);
if ("Tag" in filter) { let valid: boolean;
const tagFilter = filter["Tag"];
if (this.tagsForAutocomplete.includes(tagFilter.tag)) { if (expressions && "OrExpression" in expressions) {
this.filterAdded.emit({ Query: filter }); valid = this.validateFilters(expressions.OrExpression);
this.clearFilterInput(); } else if (expressions) {
valid = this.validateFilters([expressions.Query]);
} else { } else {
this.formControl.setErrors(["invalid tag"]); valid = false;
} }
} else {
this.filterAdded.emit({ Query: filter }); if (valid) {
this.filterAdded.emit(expressions);
this.clearFilterInput(); this.clearFilterInput();
} else {
this.formControl.setErrors(["invalid filters"]);
} }
} }
public addFilterByAutocomplete(_event: MatAutocompleteSelectedEvent): void { public buildAutocompleteValue(value: string): string {
if (this.formControl.value) {
const queryParts = this.formControl.value.split(/\s+or\s+/gi);
if (queryParts.length > 1) {
value = queryParts.slice(0, queryParts.length - 1).join(" OR ") + " OR " + value;
}
}
return value;
}
private validateFilters(filters: FilterQuery[]): boolean {
for (const filter of filters) {
if ("Tag" in filter && !this.tagsForAutocomplete.includes(filter["Tag"].tag)) {
console.debug("tags don't include", filter);
return false;
}
}
return true;
} }
private filterAutosuggestFilters(filterValue: string): string[] { private filterAutosuggestFilters(filterValue: string): AutocompleteEntry[] {
const trimmedValue = filterValue.trim(); const queryParts = filterValue.split(/\s+or\s+/gi);
const latestQuery = queryParts[queryParts.length - 1];
const trimmedValue = latestQuery.trim();
let isNegation = trimmedValue.startsWith("-"); let isNegation = trimmedValue.startsWith("-");
const cleanValue = trimmedValue.replace(/^-/, ""); const cleanValue = trimmedValue.replace(/^-/, "");
const autosuggestTags = this.tagsForAutocomplete.filter(t => t.includes(cleanValue)).map(t => isNegation ? "-" + t : t); const autosuggestTags = this.tagsForAutocomplete.filter(t => t.includes(cleanValue)).map(t => isNegation ? "-" + t : t);
let propertyQuerySuggestions: string[] = []; let propertyQuerySuggestions: string[] = [];
console.error("NEW STATE");
if (trimmedValue.startsWith(".")) { if (trimmedValue.startsWith(".")) {
propertyQuerySuggestions = this.buildPropertyQuerySuggestions(trimmedValue); propertyQuerySuggestions = this.buildPropertyQuerySuggestions(trimmedValue);
@ -92,7 +131,12 @@ export class FilterInputComponent implements OnChanges {
cleanValue, cleanValue,
r, r,
l l
)).slice(0, 50); )).slice(0, 50).map(e => {
return {
display: e,
value: this.buildAutocompleteValue(e)
};
});
} }
private clearFilterInput() { private clearFilterInput() {
@ -115,7 +159,6 @@ export class FilterInputComponent implements OnChanges {
} else if (parts.length > 2) { } else if (parts.length > 2) {
value = parts[2].trim(); value = parts[2].trim();
} }
console.log("properties", validProperties, "comparators", validComparators, "value", value);
if (validComparators.length == 1) { if (validComparators.length == 1) {
return validProperties.map(p => validComparators.map(c => this.propertyQueriesWithValues[p].map(v => `${p} ${c} ${v ?? value}`.trim())).flat()).flat(); return validProperties.map(p => validComparators.map(c => this.propertyQueriesWithValues[p].map(v => `${p} ${c} ${v ?? value}`.trim())).flat()).flat();

@ -38,7 +38,7 @@
<app-busy-indicator [blurBackground]="true" [busy]="this.tagsLoading" [darkenBackground]="false"> <app-busy-indicator [blurBackground]="true" [busy]="this.tagsLoading" [darkenBackground]="false">
<cdk-virtual-scroll-viewport itemSize="50" maxBufferPx="4000" minBufferPx="500"> <cdk-virtual-scroll-viewport itemSize="50" maxBufferPx="4000" minBufferPx="500">
<div (click)="addFilterExpression(tag.getNormalizedOutput())" <div (click)="addTagFilter(tag.getNormalizedOutput())"
(contextmenu)="contextMenuTag = tag; contextMenu.onContextMenu($event)" (contextmenu)="contextMenuTag = tag; contextMenu.onContextMenu($event)"
*cdkVirtualFor="let tag of contextTags" class="selectable-tag"> *cdkVirtualFor="let tag of contextTags" class="selectable-tag">
<app-tag-item [tag]="tag"></app-tag-item> <app-tag-item [tag]="tag"></app-tag-item>

@ -4,6 +4,7 @@
[propertyQuery]="this.propertyQuery(query).Property"></app-property-query-item> [propertyQuery]="this.propertyQuery(query).Property"></app-property-query-item>
<app-tag-query-item *ngIf="this.queryIs(query, 'Tag')" <app-tag-query-item *ngIf="this.queryIs(query, 'Tag')"
[tagQuery]="this.tagQuery(query).Tag"></app-tag-query-item> [tagQuery]="this.tagQuery(query).Tag"></app-tag-query-item>
<span class="or-combinator"> OR </span>
</ng-container> </ng-container>
</span> </span>
<span *ngIf="is('Query')" class="query"> <span *ngIf="is('Query')" class="query">

@ -0,0 +1,9 @@
.or-expression {
.or-combinator {
color: #7dff70;
}
.or-combinator:last-child {
display: none;
}
}
Loading…
Cancel
Save