From bab9782203635788ed3176e490ed24939a6356c1 Mon Sep 17 00:00:00 2001 From: trivernis Date: Sat, 15 Jan 2022 12:36:51 +0100 Subject: [PATCH] Improve filter expression input and fix OR-expression display Signed-off-by: trivernis --- mediarepo-ui/angular.json | 242 +++++++++--------- mediarepo-ui/package.json | 2 +- .../src/api/models/FilterQueryBuilder.ts | 18 +- .../filter-input/filter-input.component.html | 9 +- .../filter-input/filter-input.component.ts | 85 ++++-- .../file-search/file-search.component.html | 2 +- .../filter-expression-item.component.html | 1 + .../filter-expression-item.component.scss | 9 + 8 files changed, 219 insertions(+), 149 deletions(-) diff --git a/mediarepo-ui/angular.json b/mediarepo-ui/angular.json index 8320a9e..7f6b32a 100644 --- a/mediarepo-ui/angular.json +++ b/mediarepo-ui/angular.json @@ -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" } diff --git a/mediarepo-ui/package.json b/mediarepo-ui/package.json index b099241..8d1cea7 100644 --- a/mediarepo-ui/package.json +++ b/mediarepo-ui/package.json @@ -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", diff --git a/mediarepo-ui/src/api/models/FilterQueryBuilder.ts b/mediarepo-ui/src/api/models/FilterQueryBuilder.ts index 3fae4fb..b75c879 100644 --- a/mediarepo-ui/src/api/models/FilterQueryBuilder.ts +++ b/mediarepo-ui/src/api/models/FilterQueryBuilder.ts @@ -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; diff --git a/mediarepo-ui/src/app/components/shared/input/filter-input/filter-input.component.html b/mediarepo-ui/src/app/components/shared/input/filter-input/filter-input.component.html index ba01846..072c1fb 100644 --- a/mediarepo-ui/src/app/components/shared/input/filter-input/filter-input.component.html +++ b/mediarepo-ui/src/app/components/shared/input/filter-input/filter-input.component.html @@ -3,14 +3,15 @@ Enter a filter expression - - - {{tag}} + + + {{filter.display}} diff --git a/mediarepo-ui/src/app/components/shared/input/filter-input/filter-input.component.ts b/mediarepo-ui/src/app/components/shared/input/filter-input/filter-input.component.ts index 80e42d0..a329515 100644 --- a/mediarepo-ui/src/app/components/shared/input/filter-input/filter-input.component.ts +++ b/mediarepo-ui/src/app/components/shared/input/filter-input/filter-input.component.ts @@ -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(); - public autosuggestFilters: Observable; + public autosuggestFilters: Observable; 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(); diff --git a/mediarepo-ui/src/app/components/shared/sidebar/file-search/file-search.component.html b/mediarepo-ui/src/app/components/shared/sidebar/file-search/file-search.component.html index c67f1b0..6ff8e10 100644 --- a/mediarepo-ui/src/app/components/shared/sidebar/file-search/file-search.component.html +++ b/mediarepo-ui/src/app/components/shared/sidebar/file-search/file-search.component.html @@ -38,7 +38,7 @@ -
diff --git a/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-expression-item/filter-expression-item.component.html b/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-expression-item/filter-expression-item.component.html index f1b2c8d..eb73324 100644 --- a/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-expression-item/filter-expression-item.component.html +++ b/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-expression-item/filter-expression-item.component.html @@ -4,6 +4,7 @@ [propertyQuery]="this.propertyQuery(query).Property"> + OR diff --git a/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-expression-item/filter-expression-item.component.scss b/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-expression-item/filter-expression-item.component.scss index e69de29..42f9cb3 100644 --- a/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-expression-item/filter-expression-item.component.scss +++ b/mediarepo-ui/src/app/components/shared/sidebar/file-search/filter-expression-item/filter-expression-item.component.scss @@ -0,0 +1,9 @@ +.or-expression { + .or-combinator { + color: #7dff70; + } + + .or-combinator:last-child { + display: none; + } +}