nise/nise-frontend/src/app/search/search.component.ts

357 lines
10 KiB
TypeScript
Raw Normal View History

import {Component, OnInit} from '@angular/core';
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {DecimalPipe, JsonPipe, NgForOf, NgIf} from "@angular/common";
2024-06-08 12:32:35 +00:00
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";
2024-02-25 19:12:50 +00:00
import {
FieldType,
Operator,
Query,
QueryBuilderComponent
} from "../../corelib/components/query-builder/query-builder.component";
import {ActivatedRoute, RouterLink} from "@angular/router";
2024-02-24 13:59:17 +00:00
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";
2024-02-25 19:12:50 +00:00
import {Title} from "@angular/platform-browser";
export interface SchemaField {
name: string;
2024-02-24 17:38:37 +00:00
shortName: string;
category: 'user' | 'score' | 'beatmap' | 'metrics';
type: 'number' | 'string' | 'flag' | 'grade' | 'boolean' | 'datetime' | 'playtime';
2024-02-25 19:12:50 +00:00
validOperators: Operator[];
active: boolean;
description: string;
}
interface SchemaResponse {
fields: SchemaField[];
}
interface SearchResponse {
results: any[];
2024-02-24 13:59:17 +00:00
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,
2024-02-24 13:59:17 +00:00
QueryBuilderComponent,
RouterLink,
CalculatePageRangePipe,
CuteLoadingComponent
],
templateUrl: './search.component.html',
styleUrl: './search.component.css'
})
export class SearchComponent implements OnInit {
schemaUrl!: string;
searchUrl!: string;
pageTitle!: string;
localStorageKey!: string;
searchType!: string;
constructor(private httpClient: HttpClient,
2024-02-25 19:12:50 +00:00
private title: Title,
private route: ActivatedRoute,
public downloadFilesService: DownloadFilesService) { }
2024-02-25 19:42:33 +00:00
currentSchemaVersion = 2
isError = false;
2024-02-24 13:59:17 +00:00
isLoading = false;
isLoadingSchema = true;
response: SearchResponse | null = null;
fields: SchemaField[] = [];
sortingOrder: Sorting | null = null;
queries: Query[] | null = null;
ngOnInit(): void {
this.route.data.subscribe(data => {
const searchType = data['searchType'];
this.searchType = searchType;
if (searchType === 'user') {
this.schemaUrl = `${environment.apiUrl}/search-user/schema`;
this.searchUrl = `${environment.apiUrl}/search-user`;
this.pageTitle = '/m/ - user search';
this.localStorageKey = 'user_search_settings';
} else {
this.schemaUrl = `${environment.apiUrl}/search/schema`;
this.searchUrl = `${environment.apiUrl}/search`;
this.pageTitle = '/k/ - score search';
this.localStorageKey = 'search_settings';
}
this.title.setTitle(this.pageTitle);
this.isLoadingSchema = true;
this.httpClient.get<SchemaResponse>(this.schemaUrl).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');
}
});
});
}
hasFieldsInCategory(category: string): boolean {
return this.fields.some(field => field.category === category);
}
2024-02-25 19:12:50 +00:00
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 [];
}
}
2024-02-25 19:42:33 +00:00
private loadPreviousFromLocalStorage(): void {
const storedQueries = localStorage.getItem(this.localStorageKey);
2024-02-25 19:42:33 +00:00
let parsedQueries = storedQueries ? JSON.parse(storedQueries) : null;
if (parsedQueries && this.verifySchema(parsedQueries)) {
this.queries = parsedQueries.queries;
this.sortingOrder = parsedQueries.sortingOrder;
this.fields.forEach(field => {
2024-02-25 19:42:33 +00:00
field.active = parsedQueries.columns[field.name] ?? field.active;
});
2024-02-24 18:29:10 +00:00
} else {
localStorage.removeItem(this.localStorageKey);
2024-02-24 18:29:10 +00:00
this.queries = [];
this.sortingOrder = {
field: 'user_id',
order: 'ASC'
};
}
}
deselectEntireFieldCategory(categoryName: string): void {
this.fields.forEach(field => {
if (field.category === categoryName) {
field.active = false;
}
});
2024-02-24 18:29:10 +00:00
this.saveSettingsToLocalStorage();
}
selectEntireFieldCategory(categoryName: string): void {
this.fields.forEach(field => {
if (field.category === categoryName) {
field.active = true;
}
});
2024-02-24 18:29:10 +00:00
this.saveSettingsToLocalStorage();
}
2024-02-24 18:29:10 +00:00
private serializeSettings() {
return {
queries: this.queries,
sortingOrder: this.sortingOrder,
2024-02-25 19:42:33 +00:00
columns: this.getColumnSettings(),
schemaVersion: this.currentSchemaVersion
2024-02-24 18:29:10 +00:00
};
}
private getColumnSettings() {
return this.fields.reduce<{ [key: string]: boolean }>((acc, field) => {
acc[field.name] = field.active;
return acc;
}, {});
}
onSortingFieldChange(event: Event): void {
const selectElement = event.target as HTMLSelectElement;
if(!selectElement) {
return;
}
if(this.sortingOrder === null) {
this.sortingOrder = {
field: 'user_id',
order: 'ASC'
};
}
this.sortingOrder.field = selectElement.value;
}
2024-02-24 18:29:10 +00:00
saveSettingsToLocalStorage(): void {
const settings = this.serializeSettings();
localStorage.setItem(this.localStorageKey, JSON.stringify(settings));
}
exportForApi(): void {
if(!this.queries) {
return;
}
const body = {
queries: this.queries.map(query => ({
...query,
predicates: query.predicates.map(predicate => ({
field: {
name: predicate.field!!.name,
type: predicate.field!!.type
},
operator: predicate.operator,
value: predicate.value
}))
})),
sorting: this.sortingOrder,
page: 1
};
// Copy to cliboard
navigator.clipboard.writeText(JSON.stringify(body)).then(() => {
alert('Copied to clipboard');
}, () => {
alert('Error copying to clipboard');
});
2024-02-24 13:59:17 +00:00
}
exportSettings(): void {
2024-02-24 18:29:10 +00:00
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];
}
});
2024-02-25 19:42:33 +00:00
} else {
alert('Invalid settings file.');
2024-02-24 18:29:10 +00:00
}
2024-02-24 17:38:37 +00:00
}
getColumns(): string[] {
return this.fields.map(field => field.name);
2024-02-24 13:59:17 +00:00
}
2024-02-24 18:29:10 +00:00
uploadSettingsFile(event: any): void {
2024-02-24 13:59:17 +00:00
const file = event.target.files[0];
if (file) {
const fileReader = new FileReader();
fileReader.onload = (e) => {
try {
const json = JSON.parse(fileReader.result as string);
2024-02-24 18:29:10 +00:00
this.importSettings(json);
2024-02-24 13:59:17 +00:00
} catch (error) {
console.error('Error parsing JSON', error);
}
};
fileReader.readAsText(file);
}
}
verifySchema(json: any): boolean {
2024-02-25 19:42:33 +00:00
if(!('schemaVersion' in json) || json.schemaVersion < this.currentSchemaVersion) {
return false;
}
2024-02-24 18:29:10 +00:00
return 'queries' in json && 'sortingOrder' in json && 'columns' in json;
}
2024-02-24 13:59:17 +00:00
search(pageNumber: number = 1): void {
this.isLoading = true;
this.isError = false;
this.response = null;
const body = {
queries: this.queries,
2024-02-24 13:59:17 +00:00
sorting: this.sortingOrder,
page: pageNumber
}
this.httpClient.post<SearchResponse>(this.searchUrl, body)
.subscribe({
next: (response) => {
this.response = response;
this.isLoading = false;
},
2024-02-24 18:29:10 +00:00
error: () => {
this.isError = true;
this.isLoading = false;
}
});
2024-02-24 18:29:10 +00:00
this.saveSettingsToLocalStorage();
}
getValue(entry: any, columnName: string): any {
if (entry.hasOwnProperty(columnName)) {
return entry[columnName];
} else {
return null;
}
}
getLink(entry: any): any {
if(this.searchType === 'user') {
return "/u/" + this.getValue(entry, 'username');
} else {
return "/s/" + this.getValue(entry, 'replay_id');
}
}
protected readonly countryCodeToFlag = countryCodeToFlag;
2024-02-24 13:59:17 +00:00
protected readonly Math = Math;
protected readonly formatDuration = formatDuration;
2024-02-25 19:12:50 +00:00
}