Improve filter expression input and fix OR-expression display

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

@ -1,126 +1,130 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "yarn",
"defaultCollection": "@angular-eslint/schematics"
},
"newProjectRoot": "projects",
"projects": {
"mediarepo-ui": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
},
"@schematics/angular:application": {
"strict": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/mediarepo-ui",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"./node_modules/@angular/material/prebuilt-themes/purple-green.css",
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "1mb",
"maximumError": "10mb"
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "yarn",
"defaultCollection": "@angular-eslint/schematics"
},
"newProjectRoot": "projects",
"projects": {
"mediarepo-ui": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "100kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
"@schematics/angular:application": {
"strict": true
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "mediarepo-ui:build:production"
},
"development": {
"browserTarget": "mediarepo-ui:build:development"
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/mediarepo-ui",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"./node_modules/@angular/material/prebuilt-themes/purple-green.css",
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "1mb",
"maximumError": "10mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "100kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": {
"fonts": false,
"styles": false,
"scripts": true
},
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "mediarepo-ui:build:production"
},
"development": {
"browserTarget": "mediarepo-ui:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "mediarepo-ui:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"./node_modules/@angular/material/prebuilt-themes/purple-green.css",
"src/styles.scss"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "mediarepo-ui:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"./node_modules/@angular/material/prebuilt-themes/purple-green.css",
"src/styles.scss"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
}
},
"defaultProject": "mediarepo-ui"
},
"defaultProject": "mediarepo-ui"
}

@ -3,7 +3,7 @@
"version": "0.12.0",
"scripts": {
"ng": "ng",
"start": "ng serve --configuration production",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"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 PropertyType =
@ -60,7 +60,20 @@ export class FilterQueryBuilder {
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();
if (filterStr.startsWith(".")) {
@ -119,7 +132,6 @@ export class FilterQueryBuilder {
): FilterQuery | undefined {
const property = this.parsePropertyName(propertyName);
const comparator = this.parseComparator(rawComparator);
console.log("Parts: ", propertyName, rawComparator, compareValue);
if (property && comparator) {
let value;

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

@ -2,12 +2,16 @@ import {Component, EventEmitter, Input, OnChanges, Output, SimpleChanges} from "
import {Observable} from "rxjs";
import {FormControl} from "@angular/forms";
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 {compareSearchResults} from "../../../../utils/compare-utils";
import {MatAutocompleteSelectedEvent} from "@angular/material/autocomplete";
import {FilterQueryBuilder} from "../../../../../api/models/FilterQueryBuilder";
type AutocompleteEntry = {
value: string,
display: string,
};
@Component({
selector: "app-filter-input",
templateUrl: "./filter-input.component.html",
@ -18,9 +22,11 @@ export class FilterInputComponent implements OnChanges {
@Input() availableTags: Tag[] = [];
@Output() filterAdded = new EventEmitter<FilterExpression>();
public autosuggestFilters: Observable<string[]>;
public autosuggestFilters: Observable<AutocompleteEntry[]>;
public formControl = new FormControl();
public skipEnterOnce = false;
private propertyQueriesWithValues: { [key: string]: (string | undefined)[] } = {
".status": ["imported", "archived", "deleted"],
".fileSize": [undefined],
@ -42,7 +48,12 @@ export class FilterInputComponent implements OnChanges {
this.autosuggestFilters = this.formControl.valueChanges.pipe(
startWith(null),
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(
t => t.getNormalizedOutput());
@ -55,34 +66,62 @@ export class FilterInputComponent implements OnChanges {
}
}
public addFilterByInput(): void {
const filter = FilterQueryBuilder.buildFilterFromString(this.formControl.value);
public addExpressionByInput(): void {
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) {
const tagFilter = filter["Tag"];
let valid: boolean;
if (this.tagsForAutocomplete.includes(tagFilter.tag)) {
this.filterAdded.emit({ Query: filter });
this.clearFilterInput();
} else {
this.formControl.setErrors(["invalid tag"]);
}
if (expressions && "OrExpression" in expressions) {
valid = this.validateFilters(expressions.OrExpression);
} else if (expressions) {
valid = this.validateFilters([expressions.Query]);
} else {
this.filterAdded.emit({ Query: filter });
valid = false;
}
if (valid) {
this.filterAdded.emit(expressions);
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[] {
const trimmedValue = filterValue.trim();
private filterAutosuggestFilters(filterValue: string): AutocompleteEntry[] {
const queryParts = filterValue.split(/\s+or\s+/gi);
const latestQuery = queryParts[queryParts.length - 1];
const trimmedValue = latestQuery.trim();
let isNegation = trimmedValue.startsWith("-");
const cleanValue = trimmedValue.replace(/^-/, "");
const autosuggestTags = this.tagsForAutocomplete.filter(t => t.includes(cleanValue)).map(t => isNegation ? "-" + t : t);
let propertyQuerySuggestions: string[] = [];
console.error("NEW STATE");
if (trimmedValue.startsWith(".")) {
propertyQuerySuggestions = this.buildPropertyQuerySuggestions(trimmedValue);
@ -92,7 +131,12 @@ export class FilterInputComponent implements OnChanges {
cleanValue,
r,
l
)).slice(0, 50);
)).slice(0, 50).map(e => {
return {
display: e,
value: this.buildAutocompleteValue(e)
};
});
}
private clearFilterInput() {
@ -115,7 +159,6 @@ export class FilterInputComponent implements OnChanges {
} else if (parts.length > 2) {
value = parts[2].trim();
}
console.log("properties", validProperties, "comparators", validComparators, "value", value);
if (validComparators.length == 1) {
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">
<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)"
*cdkVirtualFor="let tag of contextTags" class="selectable-tag">
<app-tag-item [tag]="tag"></app-tag-item>

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