Added link to advanced search in header, refactor, provide schema from backend

This commit is contained in:
nise.moe 2024-02-24 18:21:27 +01:00
parent da299c0535
commit b1635fd79c
6 changed files with 190 additions and 136 deletions

View File

@ -13,6 +13,7 @@ import org.jooq.Record
import org.jooq.Result import org.jooq.Result
import org.jooq.impl.DSL import org.jooq.impl.DSL
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestHeader
@ -143,11 +144,98 @@ class SearchController(
val type: String val type: String
) )
data class SchemaField(
val name: String,
val shortName: String,
val category: Category,
val type: Type,
val active: Boolean,
val description: String
)
// Define the Category and Type enums
enum class Category {
user, score, beatmap
}
enum class Type {
number, string, flag, grade, boolean
}
data class SearchSchema(
val fields: List<SchemaField>
)
@GetMapping("search")
fun getSearchSchema(): ResponseEntity<SearchSchema> {
val fields = listOf(
// User fields
SchemaField("user_id", "ID", Category.user, Type.number, true, "unique identifier for a user"),
SchemaField("user_username", "Username", Category.user, Type.string, true, "user's name"),
SchemaField("user_join_date", "Join Date", Category.user, Type.string, true, "when the user joined"),
SchemaField("user_country", "Country", Category.user, Type.flag, true, "user's country flag"),
SchemaField("user_country_rank", "Country Rank", Category.user, Type.number, true, "ranking within user's country"),
SchemaField("user_rank", "Rank", Category.user, Type.number, true, "global ranking"),
SchemaField("user_pp_raw", "User PP", Category.user, Type.number, true, "performance points"),
SchemaField("user_accuracy", "User Accuracy", Category.user, Type.number, true, "hit accuracy percentage"),
SchemaField("user_playcount", "Playcount", Category.user, Type.number, true, "total plays"),
SchemaField("user_total_score", "Total Score", Category.user, Type.number, true, "cumulative score"),
SchemaField("user_ranked_score", "Ranked Score", Category.user, Type.number, true, "score from ranked maps"),
SchemaField("user_seconds_played", "Play Time", Category.user, Type.number, true, "total play time in seconds"),
SchemaField("user_count_300", "300s", Category.user, Type.number, true, "number of 300 hits"),
SchemaField("user_count_100", "100s", Category.user, Type.number, true, "number of 100 hits"),
SchemaField("user_count_50", "50s", Category.user, Type.number, true, "number of 50 hits"),
SchemaField("user_count_miss", "Misses", Category.user, Type.number, true, "missed hits"),
// Score fields
SchemaField("beatmap_id", "Beatmap ID", Category.score, Type.number, true, "identifies the beatmap"),
SchemaField("count_300", "300s", Category.score, Type.number, true, "number of 300 hits in score"),
SchemaField("count_100", "100s", Category.score, Type.number, true, "number of 100 hits in score"),
SchemaField("count_50", "50s", Category.score, Type.number, true, "number of 50 hits in score"),
SchemaField("count_miss", "Misses", Category.score, Type.number, true, "missed hits in score"),
SchemaField("date", "Date", Category.score, Type.number, true, "when score was achieved"),
SchemaField("max_combo", "Max Combo", Category.score, Type.number, true, "highest combo in score"),
SchemaField("mods", "Mods", Category.score, Type.number, true, "game modifiers used"),
SchemaField("perfect", "Perfect", Category.score, Type.boolean, true, "if score is a full combo"),
SchemaField("pp", "Score PP", Category.score, Type.number, true, "performance points for score"),
SchemaField("rank", "Rank", Category.score, Type.grade, true, "score grade"),
SchemaField("replay_id", "Replay ID", Category.score, Type.number, true, "identifier for replay"),
SchemaField("score", "Score", Category.score, Type.number, true, "score value"),
SchemaField("ur", "UR", Category.score, Type.number, true, "unstable rate"),
SchemaField("frametime", "Frame Time", Category.score, Type.number, true, "average frame time during play"),
SchemaField("edge_hits", "Edge Hits", Category.score, Type.number, true, "hits at the edge of hit window"),
SchemaField("snaps", "Snaps", Category.score, Type.number, true, "rapid cursor movements"),
SchemaField("adjusted_ur", "Adj. UR", Category.score, Type.number, true, "adjusted unstable rate"),
SchemaField("mean_error", "Mean Error", Category.score, Type.number, true, "average timing error"),
SchemaField("error_variance", "Error Var.", Category.score, Type.number, true, "variability of error in scores"),
SchemaField("error_standard_deviation", "Error SD", Category.score, Type.number, true, "standard deviation of error"),
SchemaField("minimum_error", "Min Error", Category.score, Type.number, true, "smallest error recorded"),
SchemaField("maximum_error", "Max Error", Category.score, Type.number, true, "largest error recorded"),
SchemaField("error_range", "Error Range", Category.score, Type.number, true, "range between min and max error"),
SchemaField("error_coefficient_of_variation", "Error CV", Category.score, Type.number, true, "relative variability of error"),
SchemaField("error_kurtosis", "Kurtosis", Category.score, Type.number, true, "peakedness of error distribution"),
SchemaField("error_skewness", "Skewness", Category.score, Type.number, true, "asymmetry of error distribution"),
SchemaField("keypresses_median_adjusted", "KP Median Adj.", Category.score, Type.number, true, "median of adjusted keypresses"),
SchemaField("keypresses_standard_deviation_adjusted", "KP std. Adj.", Category.score, Type.number, true, "std. dev of adjusted keypresses"),
SchemaField("sliderend_release_median_adjusted", "Sliderend Median Adj.", Category.score, Type.number, true, "median of adjusted sliderend releases"),
SchemaField("sliderend_release_standard_deviation_adjusted", "Sliderend std. Adj.", Category.score, Type.number, true, "std. dev of adjusted sliderend releases"),
// Beatmap fields
SchemaField("beatmap_artist", "Artist", Category.beatmap, Type.string, true, "artist of the beatmap"),
SchemaField("beatmap_beatmapset_id", "Set ID", Category.beatmap, Type.number, true, "id of the beatmap set"),
SchemaField("beatmap_creator", "Creator", Category.beatmap, Type.string, true, "creator of the beatmap"),
SchemaField("beatmap_source", "Source", Category.beatmap, Type.string, true, "source of the beatmap music"),
SchemaField("beatmap_star_rating", "Stars", Category.beatmap, Type.number, true, "(★) difficulty rating of the beatmap"),
SchemaField("beatmap_title", "Title", Category.beatmap, Type.string, true, "title of the beatmap"),
SchemaField("beatmap_version", "Version", Category.beatmap, Type.string, true, "version or difficulty name of the beatmap")
)
val schema = SearchSchema(fields)
return ResponseEntity.ok(schema)
}
@PostMapping("search") @PostMapping("search")
fun doSearch(@RequestBody request: SearchRequest, @RequestHeader("X-NISE-API") apiVersion: String): ResponseEntity<SearchResponse> { fun doSearch(@RequestBody request: SearchRequest, @RequestHeader("X-NISE-API") apiVersion: String): ResponseEntity<SearchResponse> {
if(!authService.isAdmin())
return ResponseEntity.status(401).build()
if (apiVersion.isBlank()) if (apiVersion.isBlank())
return ResponseEntity.badRequest().build() return ResponseEntity.badRequest().build()

View File

@ -6,15 +6,16 @@
</div> </div>
<div> <div>
<h2>/nise.moe/</h2> <h2>/nise.moe/</h2>
<ul style="font-size: 15px"> <ul style="font-size: 15px; line-height: 19px;">
<li><a [routerLink]="['/']">./home</a></li> <li><a [routerLink]="['/']">./home</a></li>
<li><a [routerLink]="['/sus']">./suspicious-scores</a></li> <li><a [routerLink]="['/sus']">./suspicious-scores</a></li>
<li><a [routerLink]="['/stolen']">./stolen-replays</a></li> <li><a [routerLink]="['/stolen']">./stolen-replays</a></li>
<li><a [routerLink]="['/search']">./advanced-search</a></li>
</ul> </ul>
<form (ngSubmit)="onSubmit()"> <form (ngSubmit)="onSubmit()">
<input type="text" [(ngModel)]="term" [ngModelOptions]="{standalone: true}" id="nise-osu-username" required minlength="2" maxlength="50" placeholder="Search for users..."> <input type="text" [(ngModel)]="term" [ngModelOptions]="{standalone: true}" id="nise-osu-username" required minlength="2" maxlength="50" placeholder="Search for users...">
</form> </form>
<div style="margin-top: 12px"> <div style="margin-top: 8px">
<ng-container *ngIf="this.userService.isUserLoggedIn()"> <ng-container *ngIf="this.userService.isUserLoggedIn()">
hi, <span class="user-details" [title]="this.userService.currentUser?.username">{{this.userService.currentUser?.username}}</span> <a [href]="this.userService.getLogoutUrl()">Logout</a> hi, <span class="user-details" [title]="this.userService.currentUser?.username">{{this.userService.currentUser?.username}}</span> <a [href]="this.userService.getLogoutUrl()">Logout</a>
</ng-container> </ng-container>

View File

@ -64,70 +64,78 @@
</div> </div>
<ng-container *ngIf="response"> <ng-container *ngIf="response">
<fieldset class="mb-2"> <ng-container *ngIf="response.scores.length <= 0">
<legend>tools</legend> <div class="text-center alert-error">
<div class="text-center"> <p>No results for your query - try different parameters.</p>
<button (click)="this.downloadFilesService.downloadCSV(response.scores)">Download .csv</button>
<button (click)="this.downloadFilesService.downloadJSON(response.scores)">Download .json</button>
<button (click)="this.downloadFilesService.downloadXLSX(response.scores)">Download .xlsx</button>
</div> </div>
</fieldset> </ng-container>
<div class="scrollable-table">
<table class="table-border">
<thead>
<tr>
<th *ngFor="let column of fields" [hidden]="!column.active" class="text-center">
{{ column.short_name }}
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let entry of response.scores" class="score-entry" [routerLink]="['/s/' + entry.replay_id]">
<td *ngFor="let column of fields" [hidden]="!column.active" class="text-center" style="line-height: 32px">
<ng-container *ngIf="column.type == 'number'">
{{ getValue(entry, column.name) | number }}
</ng-container>
<ng-container *ngIf="column.type == 'flag'">
<span class="flag">{{ countryCodeToFlag(getValue(entry, column.name)) }}</span>
</ng-container>
<ng-container *ngIf="column.type == 'grade'">
<app-osu-grade [grade]="getValue(entry, column.name)"></app-osu-grade>
</ng-container>
<ng-container *ngIf="column.type == 'boolean'">
<ng-container *ngIf="getValue(entry, column.name) == true">
</ng-container>
<ng-container *ngIf="getValue(entry, column.name) == false">
</ng-container>
</ng-container>
<ng-container *ngIf="column.type == 'string'">
{{ getValue(entry, column.name) }}
</ng-container>
</td>
</tr>
</tbody>
</table>
</div>
<div class="text-center mt-2"> <ng-container *ngIf="response.scores.length > 0">
<p>Total results: {{ response.pagination.totalResults | number }}</p> <fieldset class="mb-2">
<p>Page: {{ response.pagination.currentPage | number }} / {{ response.pagination.totalPages | number }}</p> <legend>tools</legend>
<div class="mb-2"> <div class="text-center">
<button *ngIf="response.pagination.currentPage > 5" (click)="this.search(1)" style="margin-right: 5px">1</button> <button (click)="this.downloadFilesService.downloadCSV(response.scores)">Download .csv</button>
<span *ngIf="response.pagination.currentPage > 6">... </span> <button (click)="this.downloadFilesService.downloadJSON(response.scores)">Download .json</button>
<button *ngFor="let page of [].constructor(Math.min(response.pagination.totalPages, 10)) | calculatePageRange:response.pagination.currentPage:response.pagination.totalPages; let i = index" <button (click)="this.downloadFilesService.downloadXLSX(response.scores)">Download .xlsx</button>
(click)="this.search(page)" </div>
[disabled]="page == response.pagination.currentPage" </fieldset>
style="margin-right: 5px"> <div class="scrollable-table">
{{ page }} <table class="table-border">
</button> <thead>
<span *ngIf="response.pagination.currentPage < response.pagination.totalPages - 5">... </span> <tr>
<button *ngIf="response.pagination.currentPage < response.pagination.totalPages - 4" (click)="this.search(response.pagination.totalPages)" style="margin-right: 5px">{{ response.pagination.totalPages }}</button> <th *ngFor="let column of fields" [hidden]="!column.active" class="text-center">
{{ column.short_name }}
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let entry of response.scores" class="score-entry" [routerLink]="['/s/' + entry.replay_id]">
<td *ngFor="let column of fields" [hidden]="!column.active" class="text-center" style="line-height: 32px">
<ng-container *ngIf="column.type == 'number'">
{{ getValue(entry, column.name) | number }}
</ng-container>
<ng-container *ngIf="column.type == 'flag'">
<span class="flag">{{ countryCodeToFlag(getValue(entry, column.name)) }}</span>
</ng-container>
<ng-container *ngIf="column.type == 'grade'">
<app-osu-grade [grade]="getValue(entry, column.name)"></app-osu-grade>
</ng-container>
<ng-container *ngIf="column.type == 'boolean'">
<ng-container *ngIf="getValue(entry, column.name) == true">
</ng-container>
<ng-container *ngIf="getValue(entry, column.name) == false">
</ng-container>
</ng-container>
<ng-container *ngIf="column.type == 'string'">
{{ getValue(entry, column.name) }}
</ng-container>
</td>
</tr>
</tbody>
</table>
</div> </div>
<button (click)="this.search(response.pagination.currentPage - 1)" [disabled]="response.pagination.currentPage == 1">← Previous</button>
<button (click)="this.search(response.pagination.currentPage + 1)" [disabled]="response.pagination.currentPage == response.pagination.totalPages" style="margin-left: 5px">Next →</button> <div class="text-center mt-2">
</div> <p>Total results: {{ response.pagination.totalResults | number }}</p>
<p>Page: {{ response.pagination.currentPage | number }} / {{ response.pagination.totalPages | number }}</p>
<div class="mb-2">
<button *ngIf="response.pagination.currentPage > 5" (click)="this.search(1)" style="margin-right: 5px">1</button>
<span *ngIf="response.pagination.currentPage > 6">... </span>
<button *ngFor="let page of [].constructor(Math.min(response.pagination.totalPages, 10)) | calculatePageRange:response.pagination.currentPage:response.pagination.totalPages; let i = index"
(click)="this.search(page)"
[disabled]="page == response.pagination.currentPage"
style="margin-right: 5px">
{{ page }}
</button>
<span *ngIf="response.pagination.currentPage < response.pagination.totalPages - 5">... </span>
<button *ngIf="response.pagination.currentPage < response.pagination.totalPages - 4" (click)="this.search(response.pagination.totalPages)" style="margin-right: 5px">{{ response.pagination.totalPages }}</button>
</div>
<button (click)="this.search(response.pagination.currentPage - 1)" [disabled]="response.pagination.currentPage == 1">← Previous</button>
<button (click)="this.search(response.pagination.currentPage + 1)" [disabled]="response.pagination.currentPage == response.pagination.totalPages" style="margin-left: 5px">Next →</button>
</div>
</ng-container>
</ng-container> </ng-container>

View File

@ -9,6 +9,7 @@ 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 {LocalCacheService} from "../../corelib/service/local-cache.service";
interface SchemaField { interface SchemaField {
name: string; name: string;
@ -19,6 +20,10 @@ interface SchemaField {
description: string; description: string;
} }
interface SchemaResponse {
fields: SchemaField[];
}
interface SearchResponse { interface SearchResponse {
scores: SearchResponseEntry[]; scores: SearchResponseEntry[];
pagination: SearchPagination; pagination: SearchPagination;
@ -93,78 +98,37 @@ interface SearchResponseEntry {
}) })
export class SearchComponent implements OnInit { export class SearchComponent implements OnInit {
constructor(private httpClient: HttpClient, public downloadFilesService: DownloadFilesService) { } constructor(private httpClient: HttpClient,
private localCacheService: LocalCacheService,
public downloadFilesService: DownloadFilesService) { }
isError = false; isError = false;
isLoading = false; isLoading = false;
response: SearchResponse | null = null; response: SearchResponse | null = null;
fields: SchemaField[] = [ fields: SchemaField[] = [];
// User fields
{ name: "user_id", short_name: "ID", category: "user", type: "number", active: true, description: "unique identifier for a user" },
{ name: "user_username", short_name: "Username", category: "user", type: "string", active: true, description: "user's name" },
{ name: "user_join_date", short_name: "Join Date", category: "user", type: "string", active: true, description: "when the user joined" },
{ name: "user_country", short_name: "Country", category: "user", type: "flag", active: true, description: "user's country flag" },
{ name: "user_country_rank", short_name: "Country Rank", category: "user", type: "number", active: true, description: "ranking within user's country" },
{ name: "user_rank", short_name: "Rank", category: "user", type: "number", active: true, description: "global ranking" },
{ name: "user_pp_raw", short_name: "User PP", category: "user", type: "number", active: true, description: "performance points" },
{ name: "user_accuracy", short_name: "User Accuracy", category: "user", type: "number", active: true, description: "hit accuracy percentage" },
{ name: "user_playcount", short_name: "Playcount", category: "user", type: "number", active: true, description: "total plays" },
{ name: "user_total_score", short_name: "Total Score", category: "user", type: "number", active: true, description: "cumulative score" },
{ name: "user_ranked_score", short_name: "Ranked Score", category: "user", type: "number", active: true, description: "score from ranked maps" },
{ name: "user_seconds_played", short_name: "Play Time", category: "user", type: "number", active: true, description: "total play time in seconds" },
{ name: "user_count_300", short_name: "300s", category: "user", type: "number", active: true, description: "number of 300 hits" },
{ name: "user_count_100", short_name: "100s", category: "user", type: "number", active: true, description: "number of 100 hits" },
{ name: "user_count_50", short_name: "50s", category: "user", type: "number", active: true, description: "number of 50 hits" },
{ name: "user_count_miss", short_name: "Misses", category: "user", type: "number", active: true, description: "missed hits" },
// Score fields
{ name: "beatmap_id", short_name: "Beatmap ID", category: "score", type: "number", active: true, description: "identifies the beatmap" },
{ name: "count_300", short_name: "300s", category: "score", type: "number", active: true, description: "number of 300 hits in score" },
{ name: "count_100", short_name: "100s", category: "score", type: "number", active: true, description: "number of 100 hits in score" },
{ name: "count_50", short_name: "50s", category: "score", type: "number", active: true, description: "number of 50 hits in score" },
{ name: "count_miss", short_name: "Misses", category: "score", type: "number", active: true, description: "missed hits in score" },
{ name: "date", short_name: "Date", category: "score", type: "string", active: true, description: "when score was achieved" },
{ name: "max_combo", short_name: "Max Combo", category: "score", type: "number", active: true, description: "highest combo in score" },
{ name: "mods", short_name: "Mods", category: "score", type: "number", active: true, description: "game modifiers used" },
{ name: "perfect", short_name: "Perfect", category: "score", type: "boolean", active: true, description: "if score is a full combo" },
{ name: "pp", short_name: "Score PP", category: "score", type: "number", active: true, description: "performance points for score" },
{ name: "rank", short_name: "Rank", category: "score", type: "grade", active: true, description: "score grade" },
{ name: "replay_id", short_name: "Replay ID", category: "score", type: "number", active: true, description: "identifier for replay" },
{ name: "score", short_name: "Score", category: "score", type: "number", active: true, description: "score value" },
{ name: "ur", short_name: "UR", category: "score", type: "number", active: true, description: "unstable rate" },
{ name: "frametime", short_name: "Frame Time", category: "score", type: "number", active: true, description: "average frame time during play" },
{ name: "edge_hits", short_name: "Edge Hits", category: "score", type: "number", active: true, description: "hits at the edge of hit window" },
{ name: "snaps", short_name: "Snaps", category: "score", type: "number", active: true, description: "rapid cursor movements" },
{ name: "adjusted_ur", short_name: "Adj. UR", category: "score", type: "number", active: true, description: "adjusted unstable rate" },
{ name: "mean_error", short_name: "Mean Error", category: "score", type: "number", active: true, description: "average timing error" },
{ name: 'error_variance', short_name: 'Error Var.', category: 'score', type: 'number', active: true, description: 'variability of error in scores' },
{ name: 'error_standard_deviation', short_name: 'Error SD', category: 'score', type: 'number', active: true, description: 'standard deviation of error' },
{ name: 'minimum_error', short_name: 'Min Error', category: 'score', type: 'number', active: true, description: 'smallest error recorded' },
{ name: 'maximum_error', short_name: 'Max Error', category: 'score', type: 'number', active: true, description: 'largest error recorded' },
{ name: 'error_range', short_name: 'Error Range', category: 'score', type: 'number', active: true, description: 'range between min and max error' },
{ name: 'error_coefficient_of_variation', short_name: 'Error CV', category: 'score', type: 'number', active: true, description: 'relative variability of error' },
{ name: 'error_kurtosis', short_name: 'Kurtosis', category: 'score', type: 'number', active: true, description: 'peakedness of error distribution' },
{ name: 'error_skewness', short_name: 'Skewness', category: 'score', type: 'number', active: true, description: 'asymmetry of error distribution' },
{ name: 'keypresses_median_adjusted', short_name: 'KP Median Adj.', category: 'score', type: 'number', active: true, description: 'median of adjusted keypresses' },
{ name: 'keypresses_standard_deviation_adjusted', short_name: 'KP std. Adj.', category: 'score', type: 'number', active: true, description: 'std. dev of adjusted keypresses' },
{ name: 'sliderend_release_median_adjusted', short_name: 'Sliderend Median Adj.', category: 'score', type: 'number', active: true, description: 'median of adjusted sliderend releases' },
{ name: 'sliderend_release_standard_deviation_adjusted', short_name: 'Sliderend std. Adj.', category: 'score', type: 'number', active: true, description: 'std. dev of adjusted sliderend releases' },
// Beatmap fields
{ name: 'beatmap_artist', short_name: 'Artist', category: 'beatmap', type: 'string', active: true, description: 'artist of the beatmap' },
{ name: 'beatmap_beatmapset_id', short_name: 'Set ID', category: 'beatmap', type: 'number', active: true, description: 'id of the beatmap set' },
{ name: 'beatmap_creator', short_name: 'Creator', category: 'beatmap', type: 'string', active: true, description: 'creator of the beatmap' },
{ name: 'beatmap_source', short_name: 'Source', category: 'beatmap', type: 'string', active: true, description: 'source of the beatmap music' },
{ name: 'beatmap_star_rating', short_name: 'Stars', category: 'beatmap', type: 'number', active: true, description: '(★) difficulty rating of the beatmap' },
{ name: 'beatmap_title', short_name: 'Title', category: 'beatmap', type: 'string', active: true, description: 'title of the beatmap' },
{ name: 'beatmap_version', short_name: 'Version', category: 'beatmap', type: 'string', active: true, description: 'version or difficulty name of the beatmap' }
];
sortingOrder: Sorting | null = null; sortingOrder: Sorting | null = null;
queries: Query[] | null = null; queries: Query[] | null = null;
ngOnInit(): void { ngOnInit(): void {
this.localCacheService.fetchData<SchemaResponse>(
"search-schema",
`${environment.apiUrl}/search`,
60
).subscribe({
next: (response) => {
this.fields = response.fields;
this.loadPreviousFromLocalStorage();
},
error: () => {
alert('Error fetching schema');
}
});
}
private loadPreviousFromLocalStorage() {
const storedQueries = localStorage.getItem('search_queries'); const storedQueries = localStorage.getItem('search_queries');
if (storedQueries) { if (storedQueries) {
this.queries = JSON.parse(storedQueries); this.queries = JSON.parse(storedQueries);

View File

@ -20,7 +20,7 @@
<select [disabled]="!predicate.field"> <select [disabled]="!predicate.field">
<option *ngFor="let operator of getOperators(predicate.field?.type)" <option *ngFor="let operator of getOperators(predicate.field?.type)"
[selected]="compare(operator.operatorType, predicate.operator?.operatorType)" [selected]="operator.operatorType === predicate.operator?.operatorType"
(click)="predicate.operator = operator; this.queryChanged.emit()"> (click)="predicate.operator = operator; this.queryChanged.emit()">
{{ operator.operatorType }} {{ operator.operatorType }}
</option> </option>

View File

@ -50,13 +50,6 @@ export class QueryComponent {
} }
} }
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();