Add extended filtering implementation
Signed-off-by: trivernis <trivernis@protonmail.com>pull/4/head
parent
8007bf64a7
commit
501a3c9df0
@ -0,0 +1,69 @@
|
||||
type CacheEntry<T> = {
|
||||
ttl: number,
|
||||
value: T,
|
||||
}
|
||||
|
||||
const cacheMap: {
|
||||
[key: string]: CacheEntry<any>
|
||||
} = {};
|
||||
|
||||
export class ShortCache {
|
||||
|
||||
public static async cached<T>(
|
||||
key: any,
|
||||
producer: () => Promise<T>,
|
||||
ttl: number = 1000,
|
||||
prefix: string = ""
|
||||
): Promise<T> {
|
||||
const cacheKey = prefix + JSON.stringify(key);
|
||||
const entry = this.getCacheEntry<T>(cacheKey, ttl);
|
||||
|
||||
if (entry) {
|
||||
console.debug("cache hit for key", cacheKey);
|
||||
return entry;
|
||||
} else {
|
||||
console.debug("cache miss key", cacheKey);
|
||||
const value = await producer();
|
||||
this.addCacheEntry(cacheKey, value, ttl);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public static startTicking() {
|
||||
(async () => {
|
||||
while (true) {
|
||||
ShortCache.tick();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
private static addCacheEntry(key: string, value: any, ttl: number) {
|
||||
cacheMap[key] = {
|
||||
ttl,
|
||||
value
|
||||
};
|
||||
console.debug("added cache entry with key", key);
|
||||
}
|
||||
|
||||
private static getCacheEntry<T>(key: string, ttl: number): T | undefined {
|
||||
const entry = cacheMap[key];
|
||||
if (entry) {
|
||||
entry.ttl = ttl;
|
||||
}
|
||||
return entry?.value;
|
||||
}
|
||||
|
||||
private static tick() {
|
||||
for (let key in cacheMap) {
|
||||
cacheMap[key].ttl -= 100;
|
||||
|
||||
if (cacheMap[key].ttl < 0) {
|
||||
console.debug("purged cache entry with key", key);
|
||||
delete cacheMap[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ShortCache.startTicking();
|
@ -0,0 +1,292 @@
|
||||
import {FileStatus, FilterQuery, PropertyQuery, ValueComparator} from "../api-types/files";
|
||||
|
||||
export type Comparator = "Less" | "Equal" | "Greater" | "Between";
|
||||
export type PropertyType =
|
||||
"Status"
|
||||
| "FileSize"
|
||||
| "ImportedTime"
|
||||
| "ChangedTime"
|
||||
| "CreatedTime"
|
||||
| "TagCount"
|
||||
| "Cd"
|
||||
| "Id";
|
||||
|
||||
export class FilterQueryBuilder {
|
||||
|
||||
public static tag(tag: string, negate: boolean): FilterQuery {
|
||||
return { Tag: { tag, negate } };
|
||||
}
|
||||
|
||||
public static status(status: FileStatus): FilterQuery {
|
||||
return filterQuery({ Status: status });
|
||||
}
|
||||
|
||||
public static fileSize(size: number, comparator: Comparator, max_size?: number): FilterQuery {
|
||||
return filterQuery(
|
||||
{ FileSize: valuesToCompareEnum(size, comparator, max_size) });
|
||||
}
|
||||
|
||||
public static importedTime(date: Date, comparator: Comparator, max_date: Date): FilterQuery {
|
||||
return filterQuery({
|
||||
ImportedTime: valuesToCompareEnum(date, comparator,
|
||||
max_date
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
public static changedTime(date: Date, comparator: Comparator, max_date: Date): FilterQuery {
|
||||
return filterQuery({
|
||||
ChangedTime: valuesToCompareEnum(date, comparator, max_date)
|
||||
});
|
||||
}
|
||||
|
||||
public static createdTime(date: Date, comparator: Comparator, max_date: Date): FilterQuery {
|
||||
return filterQuery({
|
||||
CreatedTime: valuesToCompareEnum(date, comparator, max_date)
|
||||
});
|
||||
}
|
||||
|
||||
public static tagCount(count: number, comparator: Comparator, max_count: number): FilterQuery {
|
||||
return filterQuery({
|
||||
TagCount: valuesToCompareEnum(count, comparator, max_count)
|
||||
});
|
||||
}
|
||||
|
||||
public static contentDescriptor(descriptor: string): FilterQuery {
|
||||
return filterQuery({ Cd: descriptor });
|
||||
}
|
||||
|
||||
public static fileId(id: number): FilterQuery {
|
||||
return filterQuery({ Id: id });
|
||||
}
|
||||
|
||||
public static buildFilterFromString(filterStr: string): FilterQuery {
|
||||
filterStr = filterStr.trim();
|
||||
|
||||
if (filterStr.startsWith(".")) {
|
||||
const cleanFilter = filterStr.replace(/^\./, "");
|
||||
const parsedPropertyFilter = this.parsePropertyFilterQuery(cleanFilter);
|
||||
if (parsedPropertyFilter) {
|
||||
return parsedPropertyFilter;
|
||||
}
|
||||
} else if (filterStr.startsWith("-")) {
|
||||
const tag = filterStr.replace(/^-/, "").trim();
|
||||
return this.tag(tag, true);
|
||||
}
|
||||
|
||||
return this.tag(filterStr, false);
|
||||
}
|
||||
|
||||
private static parsePropertyFilterQuery(expression: string): FilterQuery | undefined {
|
||||
let propertyName = "";
|
||||
let compareValue = "";
|
||||
let rawComparator = "";
|
||||
let comparatorStarted = false;
|
||||
let valueStarted = false;
|
||||
|
||||
for (const char of expression) {
|
||||
console.log(char);
|
||||
if (!valueStarted) {
|
||||
switch (char) {
|
||||
case " ":
|
||||
break;
|
||||
case "=":
|
||||
case "!":
|
||||
case ">":
|
||||
case "<":
|
||||
rawComparator += char;
|
||||
comparatorStarted = true;
|
||||
break;
|
||||
default:
|
||||
valueStarted = comparatorStarted;
|
||||
if (valueStarted) {
|
||||
compareValue += char;
|
||||
} else {
|
||||
propertyName += char;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
compareValue += char;
|
||||
}
|
||||
}
|
||||
|
||||
return this.parseQueryFromParts(propertyName, rawComparator, compareValue);
|
||||
}
|
||||
|
||||
private static parseQueryFromParts(
|
||||
propertyName: string,
|
||||
rawComparator: string,
|
||||
compareValue: string
|
||||
): FilterQuery | undefined {
|
||||
const property = this.parsePropertyName(propertyName);
|
||||
const comparator = this.parseComparator(rawComparator);
|
||||
console.log("Parts: ", propertyName, rawComparator, compareValue);
|
||||
|
||||
if (property && comparator) {
|
||||
let value;
|
||||
switch (property) {
|
||||
case "Status":
|
||||
value = parseStatus(compareValue);
|
||||
if (comparator === "Equal" && value != undefined) {
|
||||
return this.status(value);
|
||||
}
|
||||
break;
|
||||
case "FileSize":
|
||||
value = this.parsePropertyValue(compareValue, parseNumber);
|
||||
if (value != undefined) {
|
||||
return this.fileSize(value[0], comparator, value[1]);
|
||||
}
|
||||
break;
|
||||
case "ImportedTime":
|
||||
value = this.parsePropertyValue(compareValue, parseDate);
|
||||
if (value != undefined) {
|
||||
return this.importedTime(value[0], comparator, value[1]);
|
||||
}
|
||||
|
||||
break;
|
||||
case "ChangedTime":
|
||||
value = this.parsePropertyValue(compareValue, parseDate);
|
||||
if (value != undefined) {
|
||||
return this.changedTime(value[0], comparator, value[1]);
|
||||
}
|
||||
break;
|
||||
case "CreatedTime":
|
||||
value = this.parsePropertyValue(compareValue, parseDate);
|
||||
if (value != undefined) {
|
||||
return this.createdTime(value[0], comparator, value[1]);
|
||||
}
|
||||
break;
|
||||
case "TagCount":
|
||||
value = this.parsePropertyValue(compareValue, parseNumber);
|
||||
if (value != undefined) {
|
||||
return this.tagCount(value[0], comparator, value[1]);
|
||||
}
|
||||
break;
|
||||
case "Cd":
|
||||
if (compareValue) {
|
||||
return this.contentDescriptor(compareValue);
|
||||
}
|
||||
break;
|
||||
case "Id":
|
||||
value = parseNumber(compareValue);
|
||||
|
||||
if (value != undefined) {
|
||||
return this.fileId(value);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private static parseComparator(comparatorStr: string): Comparator | undefined {
|
||||
switch (comparatorStr) {
|
||||
case "=":
|
||||
case "==":
|
||||
return "Equal";
|
||||
case "<":
|
||||
return "Less";
|
||||
case ">":
|
||||
return "Greater";
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static parsePropertyName(nameStr: string): PropertyType | undefined {
|
||||
switch (nameStr.toLowerCase().replace(/-_/g, "")) {
|
||||
case "status":
|
||||
return "Status";
|
||||
case "filesize":
|
||||
return "FileSize";
|
||||
case "importedat":
|
||||
case "importeddate":
|
||||
case "importedtime":
|
||||
return "ImportedTime";
|
||||
case "changedat":
|
||||
case "changeddate":
|
||||
case "changedtime":
|
||||
return "ChangedTime";
|
||||
case "createdat":
|
||||
case "createddate":
|
||||
case "createdtime":
|
||||
return "CreatedTime";
|
||||
case "tagcount":
|
||||
return "TagCount";
|
||||
case "cd":
|
||||
case "contentdescriptor":
|
||||
return "Cd";
|
||||
case "id":
|
||||
case "fileid":
|
||||
return "Id";
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static parsePropertyValue<T>(
|
||||
valueStr: string,
|
||||
parseFn: (valueStr: string) => T | undefined
|
||||
): T[] | undefined {
|
||||
const [firstValue, secondValue] = valueStr.split(" ");
|
||||
if (secondValue != undefined) {
|
||||
const firstValueParsed = parseFn(firstValue);
|
||||
const secondValueParsed = parseFn(secondValue);
|
||||
|
||||
if (firstValueParsed && secondValueParsed) {
|
||||
return [firstValueParsed, secondValueParsed];
|
||||
}
|
||||
} else {
|
||||
const value = parseFn(firstValue);
|
||||
return value != undefined ? [value] : undefined;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function filterQuery(propertyQuery: PropertyQuery): FilterQuery {
|
||||
return { Property: propertyQuery };
|
||||
}
|
||||
|
||||
function valuesToCompareEnum<T>(min_value: T, comparator: Comparator, max_value?: T): ValueComparator<T> {
|
||||
switch (comparator) {
|
||||
case "Less":
|
||||
return { Less: min_value };
|
||||
case "Equal":
|
||||
return { Equal: min_value };
|
||||
case "Greater":
|
||||
return { Greater: min_value };
|
||||
case "Between":
|
||||
return { Between: [min_value, max_value!] };
|
||||
}
|
||||
}
|
||||
|
||||
function parseNumber(value: string): number | undefined {
|
||||
const num = Number(value);
|
||||
return isNaN(num) ? undefined : num;
|
||||
}
|
||||
|
||||
function parseDate(value: string): Date | undefined {
|
||||
const date = Date.parse(value);
|
||||
|
||||
if (isNaN(date)) {
|
||||
return undefined;
|
||||
}
|
||||
return new Date(date);
|
||||
}
|
||||
|
||||
function parseStatus(value: string): FileStatus | undefined {
|
||||
switch (value.toLowerCase()) {
|
||||
case "imported":
|
||||
return "Imported";
|
||||
case "archived":
|
||||
return "Archived";
|
||||
case "deleted":
|
||||
return "Deleted";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
import {FilterExpression, FilterQuery} from "../api-types/files";
|
||||
import * as deepEqual from "fast-deep-equal";
|
||||
|
||||
export class SearchFilters {
|
||||
constructor(private filters: FilterExpression[]) {
|
||||
}
|
||||
|
||||
public get length() {
|
||||
return this.filters.length;
|
||||
}
|
||||
|
||||
public getFilters(): FilterExpression[] {
|
||||
return this.filters;
|
||||
}
|
||||
|
||||
public hasFilter(expression: FilterExpression): boolean {
|
||||
return !!this.filters.find(f => deepEqual(f, expression));
|
||||
}
|
||||
|
||||
public addFilter(filter: FilterQuery, index: number) {
|
||||
this.filters = [...this.filters.slice(
|
||||
0,
|
||||
index
|
||||
), { Query: filter }, ...this.filters.slice(index)];
|
||||
}
|
||||
|
||||
public appendFilter(filter: FilterQuery) {
|
||||
this.filters.push({ Query: filter });
|
||||
}
|
||||
|
||||
public removeFilter(filterToRemove: FilterExpression) {
|
||||
this.filters = this.filters.filter(f => !deepEqual(f, filterToRemove));
|
||||
}
|
||||
|
||||
public removeFilterAtIndex(index: number) {
|
||||
this.filters.splice(index, 1);
|
||||
}
|
||||
|
||||
public appendSubfilter(filter: FilterQuery, index: number) {
|
||||
const expressionEntry = this.filters[index];
|
||||
|
||||
if (expressionEntry && "OrExpression" in expressionEntry) {
|
||||
expressionEntry["OrExpression"]!.push(filter);
|
||||
} else {
|
||||
const otherQuery = expressionEntry["Query"]!;
|
||||
let entry = expressionEntry as unknown as { OrExpression: FilterQuery[], Query: undefined };
|
||||
entry["Query"] = undefined;
|
||||
entry["OrExpression"] = [otherQuery, filter];
|
||||
}
|
||||
}
|
||||
|
||||
public removeSubfilter(queryToRemove: FilterQuery) {
|
||||
let index = this.filters.findIndex(f => {
|
||||
if ("Query" in f) {
|
||||
return false;
|
||||
} else {
|
||||
f["OrExpression"] = f["OrExpression"]!.filter(q => !deepEqual(q, queryToRemove));
|
||||
return (f["OrExpression"]!.length === 0);
|
||||
}
|
||||
});
|
||||
this.filters.splice(index);
|
||||
}
|
||||
|
||||
public removeSubfilterAtIndex(index: number, subindex: number) {
|
||||
const filterEntry = this.filters[index];
|
||||
|
||||
if (filterEntry && "OrExpression" in filterEntry) {
|
||||
filterEntry["OrExpression"]!.splice(subindex, 1);
|
||||
|
||||
if (filterEntry["OrExpression"]!.length === 0) {
|
||||
this.removeFilterAtIndex(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
<span *ngIf="is('OrExpression')" class="or-expression">
|
||||
<ng-container *ngFor="let query of this.orExpression().OrExpression">
|
||||
<app-property-query-item *ngIf="this.queryIs(query, 'Property')"
|
||||
[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>
|
||||
</ng-container>
|
||||
</span>
|
||||
<span *ngIf="is('Query')" class="query">
|
||||
<app-property-query-item *ngIf="this.queryIs(this.query().Query, 'Property')"
|
||||
[propertyQuery]="this.propertyQuery(this.query().Query).Property"></app-property-query-item>
|
||||
<app-tag-query-item *ngIf="this.queryIs(this.query().Query, 'Tag')"
|
||||
[tagQuery]="this.tagQuery(this.query().Query).Tag"></app-tag-query-item>
|
||||
</span>
|
@ -0,0 +1,25 @@
|
||||
import {ComponentFixture, TestBed} from "@angular/core/testing";
|
||||
|
||||
import {FilterExpressionItemComponent} from "./filter-expression-item.component";
|
||||
|
||||
describe("FilterItemComponent", () => {
|
||||
let component: FilterExpressionItemComponent;
|
||||
let fixture: ComponentFixture<FilterExpressionItemComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [FilterExpressionItemComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FilterExpressionItemComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,47 @@
|
||||
import {Component, Input} from "@angular/core";
|
||||
import {
|
||||
FilterExpression,
|
||||
FilterExpressionOrExpression,
|
||||
FilterExpressionQuery,
|
||||
FilterQuery,
|
||||
FilterQueryProperty,
|
||||
FilterQueryTag
|
||||
} from "../../../../../../api/api-types/files";
|
||||
|
||||
@Component({
|
||||
selector: "app-filter-expression-item",
|
||||
templateUrl: "./filter-expression-item.component.html",
|
||||
styleUrls: ["./filter-expression-item.component.scss"]
|
||||
})
|
||||
export class FilterExpressionItemComponent {
|
||||
|
||||
|
||||
@Input() filter!: FilterExpression;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
public is(key: "OrExpression" | "Query"): boolean {
|
||||
return key in this.filter;
|
||||
}
|
||||
|
||||
public orExpression(): FilterExpressionOrExpression {
|
||||
return this.filter as FilterExpressionOrExpression;
|
||||
}
|
||||
|
||||
public query(): FilterExpressionQuery {
|
||||
return this.filter as FilterExpressionQuery;
|
||||
}
|
||||
|
||||
public queryIs(query: FilterQuery, key: "Property" | "Tag"): boolean {
|
||||
return key in query;
|
||||
}
|
||||
|
||||
public propertyQuery(query: FilterQuery): FilterQueryProperty {
|
||||
return query as FilterQueryProperty;
|
||||
}
|
||||
|
||||
public tagQuery(query: FilterQuery): FilterQueryTag {
|
||||
return query as FilterQueryTag;
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
<span>{{this.stringExpression}}</span>
|
@ -0,0 +1,25 @@
|
||||
import {ComponentFixture, TestBed} from "@angular/core/testing";
|
||||
|
||||
import {PropertyQueryItemComponent} from "./property-query-item.component";
|
||||
|
||||
describe("PropertyQueryItemComponent", () => {
|
||||
let component: PropertyQueryItemComponent;
|
||||
let fixture: ComponentFixture<PropertyQueryItemComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [PropertyQueryItemComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PropertyQueryItemComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,101 @@
|
||||
import {Component, Input, OnChanges, OnInit, SimpleChanges} from "@angular/core";
|
||||
import {PropertyQuery, ValueComparator} from "../../../../../../../api/api-types/files";
|
||||
|
||||
@Component({
|
||||
selector: "app-property-query-item",
|
||||
templateUrl: "./property-query-item.component.html",
|
||||
styleUrls: ["./property-query-item.component.scss"]
|
||||
})
|
||||
export class PropertyQueryItemComponent implements OnInit, OnChanges {
|
||||
|
||||
@Input() propertyQuery!: PropertyQuery;
|
||||
|
||||
public stringExpression: string = "No Expression";
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
private static buildExpression(property: string, comparator: string, value: string): string {
|
||||
return `.${property} ${comparator} ${value}`;
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.stringExpression = this.getStringExpression();
|
||||
}
|
||||
|
||||
public ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes["propertyQuery"]) {
|
||||
this.stringExpression = this.getStringExpression();
|
||||
}
|
||||
}
|
||||
|
||||
public getStringExpression(): string {
|
||||
if ("Status" in this.propertyQuery) {
|
||||
return PropertyQueryItemComponent.buildExpression("Status", "is", this.propertyQuery.Status);
|
||||
} else if ("FileSize" in this.propertyQuery) {
|
||||
return PropertyQueryItemComponent.buildExpression(
|
||||
"FileSize",
|
||||
this.getComparator(this.propertyQuery.FileSize),
|
||||
this.getValue(this.propertyQuery.FileSize).toString()
|
||||
);
|
||||
} else if ("ImportedTime" in this.propertyQuery) {
|
||||
return PropertyQueryItemComponent.buildExpression(
|
||||
"ImportedTime",
|
||||
this.getComparator(this.propertyQuery.ImportedTime),
|
||||
this.getValue(this.propertyQuery.ImportedTime).toISOString()
|
||||
);
|
||||
} else if ("ChangedTime" in this.propertyQuery) {
|
||||
return PropertyQueryItemComponent.buildExpression(
|
||||
"ChangedTime",
|
||||
this.getComparator(this.propertyQuery.ChangedTime),
|
||||
this.getValue(this.propertyQuery.ChangedTime).toISOString()
|
||||
);
|
||||
} else if ("CreatedTime" in this.propertyQuery) {
|
||||
return PropertyQueryItemComponent.buildExpression(
|
||||
"CreatedTime",
|
||||
this.getComparator(this.propertyQuery.CreatedTime),
|
||||
this.getValue(this.propertyQuery.CreatedTime).toISOString()
|
||||
);
|
||||
} else if ("TagCount" in this.propertyQuery) {
|
||||
return PropertyQueryItemComponent.buildExpression(
|
||||
"TagCount",
|
||||
this.getComparator(this.propertyQuery.TagCount),
|
||||
this.getValue(this.propertyQuery.TagCount).toString()
|
||||
);
|
||||
} else if ("Cd" in this.propertyQuery) {
|
||||
return PropertyQueryItemComponent.buildExpression("ContentDescriptor", "is", this.propertyQuery.Cd);
|
||||
} else if ("Id" in this.propertyQuery) {
|
||||
return PropertyQueryItemComponent.buildExpression("FileId", "is", this.propertyQuery.Id.toString());
|
||||
} else {
|
||||
return "Invalid Expression";
|
||||
}
|
||||
}
|
||||
|
||||
public getComparator(value: ValueComparator<any>): "=" | "<" | ">" | "between" {
|
||||
if ("Greater" in value) {
|
||||
return ">";
|
||||
} else if ("Equal" in value) {
|
||||
return "=";
|
||||
} else if ("Less" in value) {
|
||||
return "<";
|
||||
} else {
|
||||
return "between";
|
||||
}
|
||||
}
|
||||
|
||||
public getValue<T>(value: ValueComparator<T>): T {
|
||||
const singleValueKeys: ("Greater" | "Equal" | "Less")[] = ["Greater", "Equal", "Less"];
|
||||
|
||||
for (const key of singleValueKeys) {
|
||||
if (key in value) {
|
||||
//@ts-ignore
|
||||
return value[key];
|
||||
}
|
||||
}
|
||||
if ("Between" in value) {
|
||||
return value.Between[0];
|
||||
} else {
|
||||
return "" as unknown as T; // unreachable
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
<span><span *ngIf="tagQuery.negate" class="tag-negation">-</span>{{tagQuery.tag}}</span>
|
@ -0,0 +1,25 @@
|
||||
import {ComponentFixture, TestBed} from "@angular/core/testing";
|
||||
|
||||
import {TagQueryItemComponent} from "./tag-query-item.component";
|
||||
|
||||
describe("TagQueryItemComponent", () => {
|
||||
let component: TagQueryItemComponent;
|
||||
let fixture: ComponentFixture<TagQueryItemComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [TagQueryItemComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TagQueryItemComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,15 @@
|
||||
import {Component, Input} from "@angular/core";
|
||||
import {TagQuery} from "../../../../../../../api/api-types/files";
|
||||
|
||||
@Component({
|
||||
selector: "app-tag-query-item",
|
||||
templateUrl: "./tag-query-item.component.html",
|
||||
styleUrls: ["./tag-query-item.component.scss"]
|
||||
})
|
||||
export class TagQueryItemComponent {
|
||||
|
||||
@Input() tagQuery!: TagQuery;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue