Operators refactor (added boolean), handling errors in frontend, improved export/import settings
This commit is contained in:
parent
7014dfdfb5
commit
da299c0535
@ -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)
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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,25 +266,22 @@ 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
|
|
||||||
console.error('An error occurred:', error);
|
|
||||||
return throwError(() => new Error('An error occurred')); // Rethrow or return a new observable
|
|
||||||
})
|
|
||||||
).subscribe({
|
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
this.response = response;
|
this.response = response;
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
// Handle subscription error
|
this.isError = true;
|
||||||
console.error('Subscription error:', error);
|
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
<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()">
|
<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>
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user