|
|
|
@ -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();
|
|
|
|
|
if (expressions && "OrExpression" in expressions) {
|
|
|
|
|
valid = this.validateFilters(expressions.OrExpression);
|
|
|
|
|
} else if (expressions) {
|
|
|
|
|
valid = this.validateFilters([expressions.Query]);
|
|
|
|
|
} else {
|
|
|
|
|
this.formControl.setErrors(["invalid tag"]);
|
|
|
|
|
valid = false;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
this.filterAdded.emit({ Query: filter });
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|