nise/nise-frontend/src/app/search/search.component.ts
2024-02-25 20:42:33 +01:00

280 lines
8.0 KiB
TypeScript

import {Component, OnInit} from '@angular/core';
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {DecimalPipe, JsonPipe, NgForOf, NgIf} from "@angular/common";
import {HttpClient} from "@angular/common/http";
import {environment} from "../../environments/environment";
import {countryCodeToFlag, formatDuration} from "../format";
import {OsuGradeComponent} from "../../corelib/components/osu-grade/osu-grade.component";
import {
FieldType,
Operator,
Query,
QueryBuilderComponent
} from "../../corelib/components/query-builder/query-builder.component";
import {RouterLink} from "@angular/router";
import {CalculatePageRangePipe} from "../../corelib/calculate-page-range.pipe";
import {DownloadFilesService} from "../../corelib/service/download-files.service";
import {CuteLoadingComponent} from "../../corelib/components/cute-loading/cute-loading.component";
import {Title} from "@angular/platform-browser";
export interface SchemaField {
name: string;
shortName: string;
category: 'user' | 'score' | 'beatmap' | 'metrics';
type: 'number' | 'string' | 'flag' | 'grade' | 'boolean' | 'datetime' | 'playtime';
validOperators: Operator[];
active: boolean;
description: string;
}
interface SchemaResponse {
fields: SchemaField[];
}
interface SearchResponse {
scores: any[];
pagination: SearchPagination;
}
interface SearchPagination {
currentPage: number;
pageSize: number;
totalResults: number;
totalPages: number;
}
interface Sorting {
field: string;
order: 'ASC' | 'DESC';
}
@Component({
selector: 'app-search',
standalone: true,
imports: [
ReactiveFormsModule,
NgForOf,
FormsModule,
JsonPipe,
NgIf,
DecimalPipe,
OsuGradeComponent,
QueryBuilderComponent,
RouterLink,
CalculatePageRangePipe,
CuteLoadingComponent
],
templateUrl: './search.component.html',
styleUrl: './search.component.css'
})
export class SearchComponent implements OnInit {
constructor(private httpClient: HttpClient,
private title: Title,
public downloadFilesService: DownloadFilesService) { }
currentSchemaVersion = 2
isError = false;
isLoading = false;
isLoadingSchema = true;
response: SearchResponse | null = null;
fields: SchemaField[] = [];
sortingOrder: Sorting | null = null;
queries: Query[] | null = null;
ngOnInit(): void {
this.title.setTitle("/k/ - advanced search");
this.isLoadingSchema = true;
this.httpClient.get<SchemaResponse>(`${environment.apiUrl}/search/schema`,).subscribe({
next: (response) => {
this.fields = response.fields;
this.fields.forEach(field => {
field.validOperators = this.getOperators(field.type);
})
this.loadPreviousFromLocalStorage();
this.isLoadingSchema = false;
},
error: () => {
alert('Error fetching schema');
}
});
}
getOperators(fieldType: FieldType | undefined): Operator[] {
switch (fieldType) {
case 'number':
return ['=', '>', '<', '>=', '<=', '!=']
.map((operatorType: String) => ({operatorType: operatorType, acceptsValues: 'any'}) as Operator);
case 'string':
return ['=', 'contains', 'like']
.map((operatorType: String) => ({operatorType: operatorType, acceptsValues: 'any'}) as Operator);
case 'boolean':
return ['=', '!=']
.map((operatorType: String) => ({operatorType: operatorType, acceptsValues: 'boolean'}) as Operator);
case 'flag':
return ['=', '!=']
.map((operatorType: String) => ({operatorType: operatorType, acceptsValues: 'flag'}) as Operator);
case 'grade':
return ['=', '!=']
.map((operatorType: String) => ({operatorType: operatorType, acceptsValues: 'grade'}) as Operator);
case 'datetime':
return ['before', 'after']
.map((operatorType: String) => ({operatorType: operatorType, acceptsValues: 'datetime'}) as Operator);
case 'playtime':
return ['>', '<', '>=', '<=']
.map((operatorType: String) => ({operatorType: operatorType, acceptsValues: 'any'}) as Operator);
default:
return [];
}
}
private loadPreviousFromLocalStorage(): void {
const storedQueries = localStorage.getItem('search_settings');
let parsedQueries = storedQueries ? JSON.parse(storedQueries) : null;
if (parsedQueries && this.verifySchema(parsedQueries)) {
this.queries = parsedQueries.queries;
this.sortingOrder = parsedQueries.sortingOrder;
this.fields.forEach(field => {
field.active = parsedQueries.columns[field.name] ?? field.active;
});
} else {
localStorage.removeItem('search_settings');
this.queries = [];
this.sortingOrder = {
field: 'user_id',
order: 'ASC'
};
}
}
deselectEntireFieldCategory(categoryName: string): void {
this.fields.forEach(field => {
if (field.category === categoryName) {
field.active = false;
}
});
this.saveSettingsToLocalStorage();
}
selectEntireFieldCategory(categoryName: string): void {
this.fields.forEach(field => {
if (field.category === categoryName) {
field.active = true;
}
});
this.saveSettingsToLocalStorage();
}
private serializeSettings() {
return {
queries: this.queries,
sortingOrder: this.sortingOrder,
columns: this.getColumnSettings(),
schemaVersion: this.currentSchemaVersion
};
}
private getColumnSettings() {
return this.fields.reduce<{ [key: string]: boolean }>((acc, field) => {
acc[field.name] = field.active;
return acc;
}, {});
}
saveSettingsToLocalStorage(): void {
const settings = this.serializeSettings();
localStorage.setItem('search_settings', JSON.stringify(settings));
}
exportSettings(): void {
const settings = this.serializeSettings();
this.downloadFilesService.downloadJSON(settings as any, 'nise-settings');
}
importSettings(json: any): void {
if (this.verifySchema(json)) {
this.queries = json.queries;
this.sortingOrder = json.sortingOrder;
this.fields.forEach(field => {
if (json.columns.hasOwnProperty(field.name)) {
field.active = json.columns[field.name];
}
});
} else {
alert('Invalid settings file.');
}
}
getColumns(): string[] {
return this.fields.map(field => field.name);
}
uploadSettingsFile(event: any): void {
const file = event.target.files[0];
if (file) {
const fileReader = new FileReader();
fileReader.onload = (e) => {
try {
const json = JSON.parse(fileReader.result as string);
this.importSettings(json);
} catch (error) {
console.error('Error parsing JSON', error);
}
};
fileReader.readAsText(file);
}
}
verifySchema(json: any): boolean {
if(!('schemaVersion' in json) || json.schemaVersion < this.currentSchemaVersion) {
return false;
}
return 'queries' in json && 'sortingOrder' in json && 'columns' in json;
}
search(pageNumber: number = 1): void {
this.isLoading = true;
this.isError = false;
this.response = null;
const body = {
queries: this.queries,
sorting: this.sortingOrder,
page: pageNumber
}
this.httpClient.post<SearchResponse>(`${environment.apiUrl}/search`, body)
.subscribe({
next: (response) => {
this.response = response;
this.isLoading = false;
},
error: () => {
this.isError = true;
this.isLoading = false;
}
});
this.saveSettingsToLocalStorage();
}
getValue(entry: any, columnName: string): any {
if (entry.hasOwnProperty(columnName)) {
return entry[columnName];
} else {
return null;
}
}
getId(entry: any): any {
return this.getValue(entry, 'replay_id');
}
protected readonly countryCodeToFlag = countryCodeToFlag;
protected readonly Math = Math;
protected readonly formatDuration = formatDuration;
}