Operators refactor (added boolean), handling errors in frontend, improved export/import settings

This commit is contained in:
nise.moe 2024-02-24 17:58:10 +01:00
parent 7014dfdfb5
commit da299c0535
8 changed files with 118 additions and 44 deletions

View File

@ -129,10 +129,15 @@ class SearchController(
data class SearchPredicate( data class SearchPredicate(
val field: SearchField, val field: SearchField,
val operator: String, val operator: SearchPredicateOperator,
val value: String val value: String
) )
data class SearchPredicateOperator(
val operatorType: String,
val acceptsValues: String
)
data class SearchField( data class SearchField(
val name: String, val name: String,
val type: String val type: String
@ -352,8 +357,9 @@ class SearchController(
private fun buildPredicateCondition(predicate: SearchPredicate): Condition { private fun buildPredicateCondition(predicate: SearchPredicate): Condition {
val field = mapPredicateFieldToDatabaseField(predicate.field.name) val field = mapPredicateFieldToDatabaseField(predicate.field.name)
return when (predicate.field.type.lowercase()) { return when (predicate.field.type.lowercase()) {
"number" -> buildNumberCondition(field as Field<Double>, predicate.operator, predicate.value.toDouble()) "number" -> buildNumberCondition(field as Field<Double>, predicate.operator.operatorType, predicate.value.toDouble())
"string" -> buildStringCondition(field as Field<String>, predicate.operator, predicate.value) "string" -> buildStringCondition(field as Field<String>, predicate.operator.operatorType, predicate.value)
"boolean" -> buildBooleanCondition(field as Field<Boolean>, predicate.operator.operatorType, predicate.value.toBoolean())
else -> throw IllegalArgumentException("Invalid field type") else -> throw IllegalArgumentException("Invalid field type")
} }
} }
@ -423,6 +429,14 @@ class SearchController(
} }
} }
private fun buildBooleanCondition(field: Field<Boolean>, operator: String, value: Boolean): Condition {
return when (operator) {
"=" -> field.eq(value)
"!=" -> field.ne(value)
else -> throw IllegalArgumentException("Invalid operator")
}
}
private fun buildNumberCondition(field: Field<Double>, operator: String, value: Double): Condition { private fun buildNumberCondition(field: Field<Double>, operator: String, value: Double): Condition {
return when (operator) { return when (operator) {
"=" -> field.eq(value) "=" -> field.eq(value)

View File

@ -26,5 +26,5 @@
</div> </div>
<router-outlet></router-outlet> <router-outlet></router-outlet>
<div class="text-center version"> <div class="text-center version">
v20240222 v20240224
</div> </div>

View File

@ -4,8 +4,8 @@
<fieldset> <fieldset>
<legend>Table columns</legend> <legend>Table columns</legend>
<ng-container *ngFor="let category of ['user', 'beatmap', 'score']"> <ng-container *ngFor="let category of ['user', 'beatmap', 'score']">
<fieldset> <fieldset class="mb-2">
<legend>{{ category }}</legend> <legend>{{ category }} <button (click)="this.selectEntireFieldCategory(category)">Select all</button> <button (click)="this.deselectEntireFieldCategory(category)">Deselect all</button></legend>
<ng-container *ngFor="let field of fields"> <ng-container *ngFor="let field of fields">
<div *ngIf="field.category === category"> <div *ngIf="field.category === category">
<label> <label>
@ -43,14 +43,12 @@
</label> </label>
</fieldset> </fieldset>
<div class="text-center"> <div class="text-center mt-2">
<div class="text-center mb-2 mt-2"> <button (click)="exportSettings()">Export settings</button>
<button (click)="exportSettings()">Export Query</button> <button (click)="fileInput.click()" style="margin-left: 5px">Import settings</button>
<button (click)="fileInput.click()" style="margin-left: 5px">Import Query</button> <input type="file" #fileInput style="display: none" (change)="importSettings($event)" accept=".json">
<input type="file" #fileInput style="display: none" (change)="importSettings($event)" accept=".json"> </div>
</div> <div class="text-center mt-1">
<button (click)="search()" class="mb-2" style="font-size: 18px">Search</button> <button (click)="search()" class="mb-2" style="font-size: 18px">Search</button>
</div> </div>
@ -60,13 +58,18 @@
</div> </div>
</ng-container> </ng-container>
<div class="text-center alert-error" *ngIf="this.isError">
<p>Looks like something went wrong... :(</p>
<p>I'll look into what caused the error - but feel free to get in touch.</p>
</div>
<ng-container *ngIf="response"> <ng-container *ngIf="response">
<fieldset class="mb-2"> <fieldset class="mb-2">
<legend>tools</legend> <legend>tools</legend>
<div class="text-center"> <div class="text-center">
<button (click)="this.downloadFilesService.downloadCSV(response.scores)">Download .csv</button> <button (click)="this.downloadFilesService.downloadCSV(response.scores)">Download .csv</button>
<button (click)="this.downloadFilesService.downloadJSON(response.scores)">Download .json</button> <button (click)="this.downloadFilesService.downloadJSON(response.scores)">Download .json</button>
<button (click)="this.downloadFilesService.downloadXLSX(response.scores)">Download .xslx</button> <button (click)="this.downloadFilesService.downloadXLSX(response.scores)">Download .xlsx</button>
</div> </div>
</fieldset> </fieldset>
<div class="scrollable-table"> <div class="scrollable-table">

View File

@ -9,7 +9,6 @@ import {Field, Query, QueryBuilderComponent} from "../../corelib/components/quer
import {RouterLink} from "@angular/router"; import {RouterLink} from "@angular/router";
import {CalculatePageRangePipe} from "../../corelib/calculate-page-range.pipe"; import {CalculatePageRangePipe} from "../../corelib/calculate-page-range.pipe";
import {DownloadFilesService} from "../../corelib/service/download-files.service"; import {DownloadFilesService} from "../../corelib/service/download-files.service";
import {catchError, throwError} from "rxjs";
interface SchemaField { interface SchemaField {
name: string; name: string;
@ -96,6 +95,7 @@ export class SearchComponent implements OnInit {
constructor(private httpClient: HttpClient, public downloadFilesService: DownloadFilesService) { } constructor(private httpClient: HttpClient, public downloadFilesService: DownloadFilesService) { }
isError = false;
isLoading = false; isLoading = false;
response: SearchResponse | null = null; response: SearchResponse | null = null;
@ -189,6 +189,24 @@ export class SearchComponent implements OnInit {
}; };
} }
deselectEntireFieldCategory(categoryName: string): void {
this.fields.forEach(field => {
if (field.category === categoryName) {
field.active = false;
}
});
this.saveColumnsStatusToLocalStorage();
}
selectEntireFieldCategory(categoryName: string): void {
this.fields.forEach(field => {
if (field.category === categoryName) {
field.active = true;
}
});
this.saveColumnsStatusToLocalStorage();
}
mapSchemaFieldsToFields(): Field[] { mapSchemaFieldsToFields(): Field[] {
return this.fields.map(field => { return this.fields.map(field => {
return { return {
@ -248,28 +266,25 @@ export class SearchComponent implements OnInit {
search(pageNumber: number = 1): void { search(pageNumber: number = 1): void {
this.isLoading = true; this.isLoading = true;
this.isError = false;
this.response = null;
const body = { const body = {
queries: this.queries, queries: this.queries,
sorting: this.sortingOrder, sorting: this.sortingOrder,
page: pageNumber page: pageNumber
} }
this.httpClient.post<SearchResponse>(`${environment.apiUrl}/search`, body).pipe( this.httpClient.post<SearchResponse>(`${environment.apiUrl}/search`, body)
catchError(error => { .subscribe({
// Handle the error or rethrow next: (response) => {
console.error('An error occurred:', error); this.response = response;
return throwError(() => new Error('An error occurred')); // Rethrow or return a new observable this.isLoading = false;
}) },
).subscribe({ error: (error) => {
next: (response) => { this.isError = true;
this.response = response; this.isLoading = false;
this.isLoading = false; }
}, });
error: (error) => {
// Handle subscription error
console.error('Subscription error:', error);
this.isLoading = false;
}
});
this.updateLocalStorage(); this.updateLocalStorage();
} }

View File

@ -202,6 +202,10 @@ a.btn-success:hover {
margin-bottom: 40px; margin-bottom: 40px;
} }
.mt-1 {
margin-top: 10px;
}
.mt-2 { .mt-2 {
margin-top: 20px; margin-top: 20px;
} }

View File

@ -5,6 +5,7 @@ import {QueryComponent} from "../query/query.component";
export type FieldType = 'number' | 'string' | 'flag' | 'grade' | 'boolean'; export type FieldType = 'number' | 'string' | 'flag' | 'grade' | 'boolean';
export type OperatorType = '=' | '>' | '<' | 'contains' | 'like' | '>=' | '<=' | '!='; export type OperatorType = '=' | '>' | '<' | 'contains' | 'like' | '>=' | '<=' | '!=';
export type ValueType = 'any' | 'boolean';
export interface Field { export interface Field {
name: string; name: string;
@ -13,10 +14,15 @@ export interface Field {
export interface Predicate { export interface Predicate {
field: Field | null; field: Field | null;
operator: OperatorType | null; operator: Operator | null;
value: string | number | null; value: string | number | null;
} }
export interface Operator {
operatorType: OperatorType;
acceptsValues: ValueType;
}
export interface Query { export interface Query {
predicates: Predicate[]; predicates: Predicate[];
logicalOperator: 'AND' | 'OR'; logicalOperator: 'AND' | 'OR';

View File

@ -18,13 +18,31 @@
<option *ngFor="let field of fields" (click)="onFieldChange(predicate, field)" [selected]="field.name === predicate.field?.name">{{ field.name }}</option> <option *ngFor="let field of fields" (click)="onFieldChange(predicate, field)" [selected]="field.name === predicate.field?.name">{{ field.name }}</option>
</select> </select>
<select [(ngModel)]="predicate.operator" [disabled]="!predicate.field"> <select [disabled]="!predicate.field">
<option *ngFor="let operator of getOperators(predicate.field?.type)" [value]="operator" (change)="this.queryChanged.emit()"> <option *ngFor="let operator of getOperators(predicate.field?.type)"
{{ operator }} [selected]="compare(operator.operatorType, predicate.operator?.operatorType)"
(click)="predicate.operator = operator; this.queryChanged.emit()">
{{ operator.operatorType }}
</option> </option>
</select> </select>
<input [(ngModel)]="predicate.value" type="text" placeholder="Value" [disabled]="!predicate.field" (change)="this.queryChanged.emit()"> <ng-container *ngIf="predicate.operator">
<ng-container *ngIf="predicate.operator.acceptsValues == 'any'">
<input [(ngModel)]="predicate.value" type="text" placeholder="Value" [disabled]="!predicate.field" (change)="this.queryChanged.emit()">
</ng-container>
<ng-container *ngIf="predicate.operator.acceptsValues == 'boolean'">
<select [(ngModel)]="predicate.value" [disabled]="!predicate.field" (change)="this.queryChanged.emit()">
<option value="True">True</option>
<option value="False">False</option>
</select>
</ng-container>
</ng-container>
<button (click)="removePredicate(i)">X</button> <button (click)="removePredicate(i)">X</button>
</div> </div>

View File

@ -1,6 +1,6 @@
import {Component, EventEmitter, Input, Output} from '@angular/core'; import {Component, EventEmitter, Input, Output} from '@angular/core';
import {Field, FieldType, OperatorType, Predicate, Query} from "../query-builder/query-builder.component"; import {Field, FieldType, Operator, OperatorType, Predicate, Query} from "../query-builder/query-builder.component";
import {NgForOf} from "@angular/common"; import {JsonPipe, NgForOf, NgIf} from "@angular/common";
import {FormsModule, ReactiveFormsModule} from "@angular/forms"; import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@ -10,7 +10,9 @@ import { v4 as uuidv4 } from 'uuid';
imports: [ imports: [
NgForOf, NgForOf,
ReactiveFormsModule, ReactiveFormsModule,
FormsModule FormsModule,
NgIf,
JsonPipe
], ],
templateUrl: './query.component.html', templateUrl: './query.component.html',
styleUrl: './query.component.css' styleUrl: './query.component.css'
@ -32,17 +34,29 @@ export class QueryComponent {
predicate.operator = this.getOperators(selectedField.type)[0]; predicate.operator = this.getOperators(selectedField.type)[0];
} }
getOperators(fieldType: FieldType | undefined): OperatorType[] { getOperators(fieldType: FieldType | undefined): Operator[] {
switch (fieldType) { switch (fieldType) {
case 'number': case 'number':
return ['=', '>', '<', '>=', '<=', '!=']; return ['=', '>', '<', '>=', '<=', '!=']
.map((operatorType: String) => ({ operatorType: operatorType, acceptsValues: 'any'}) as Operator);
case 'string': case 'string':
return ['=', 'contains', 'like']; return ['=', 'contains', 'like']
.map((operatorType: String) => ({ operatorType: operatorType, acceptsValues: 'any'}) as Operator);
case 'boolean':
return ['=', '!=']
.map((operatorType: String) => ({ operatorType: operatorType, acceptsValues: 'boolean'}) as Operator);
default: default:
return []; return [];
} }
} }
compare(a: any, b: any): boolean {
console.warn('compare', a, b);
console.warn('compare', a === b)
console.error();
return a === b;
}
addPredicate(): void { addPredicate(): void {
this.query.predicates.push({ field: null, operator: null, value: null }); this.query.predicates.push({ field: null, operator: null, value: null });
this.queryChanged.emit(); this.queryChanged.emit();