Skip to content

Commit 00bf33f

Browse files
Implement status filter in frontend
1 parent dad8129 commit 00bf33f

11 files changed

Lines changed: 139 additions & 34 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
@let gold = problem().gold;
2+
<button
3+
class="btn btn-outline-warning px-2"
4+
(click)="onToggleGold()"
5+
[attr.aria-label]="gold ? 'Remove gold status' : 'Mark as gold'"
6+
i18n-aria-label
7+
>
8+
<fa-icon [icon]="faMedal" aria-hidden="true"></fa-icon>
9+
<span class="ms-2">
10+
@if (gold) {
11+
<ng-container i18n>Ungold</ng-container>
12+
} @else {
13+
<ng-container i18n>Gold</ng-container>
14+
}
15+
</span>
16+
</button>

frontend/src/app/annotate/annotation-input/problem-details/gold-toggle.component.scss

Whitespace-only changes.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { ProblemService } from '@/services/problem.service';
2+
import { Problem } from '@/types';
3+
import { Component, inject, input } from '@angular/core';
4+
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
5+
import { faMedal } from '@fortawesome/free-solid-svg-icons';
6+
7+
@Component({
8+
selector: 'la-gold-toggle',
9+
imports: [FontAwesomeModule],
10+
templateUrl: './gold-toggle.component.html',
11+
styleUrl: './gold-toggle.component.scss'
12+
})
13+
export class GoldToggleComponent {
14+
public readonly problem = input.required<Problem>();
15+
private problemService = inject(ProblemService);
16+
17+
public faMedal = faMedal;
18+
19+
public onToggleGold(): void {
20+
const problem = this.problem();
21+
if (!problem?.id) {
22+
return;
23+
}
24+
this.problemService.toggleGold$.next({ id: problem.id, gold: !problem.gold });
25+
}
26+
}

frontend/src/app/annotate/annotation-input/problem-details/problem-details.component.html

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,28 @@
11
@let appMode = appMode$ | async;
22
@let currentUser = currentUser$ | async;
3-
43
@if (problemDetails(); as details) {
54
<div>
6-
@if (currentUser?.canChangeProblemVisibility) {
7-
<la-visibility-toggle [problem]="problem()" />
8-
}
5+
<div class="position-relative d-flex justify-content-end gap-2 mb-2">
6+
<span
7+
class="badge d-flex align-items-center gap-1"
8+
[class.text-bg-warning]="problem().status === ProblemStatus.GOLD"
9+
[class.text-bg-secondary]="
10+
problem().status === ProblemStatus.SILVER
11+
"
12+
[class.text-bg-light]="problem().status === ProblemStatus.BRONZE"
13+
[ngbTooltip]="statusLabels[problem().status]"
14+
aria-hidden="true"
15+
>
16+
<fa-icon [icon]="faMedal" />
17+
{{ statusLabels[problem().status] }}
18+
</span>
19+
@if (currentUser?.canChangeProblemStatus) {
20+
<la-gold-toggle [problem]="problem()" />
21+
}
22+
@if (currentUser?.canChangeProblemVisibility) {
23+
<la-visibility-toggle [problem]="problem()" />
24+
}
25+
</div>
926
<table class="table table-sm table-borderless">
1027
<tbody>
1128
<tr>

frontend/src/app/annotate/annotation-input/problem-details/problem-details.component.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
import { Dataset, EntailmentLabel, LabelAnnotation, Problem } from "../../../types";
1+
import { Dataset, EntailmentLabel, LabelAnnotation, Problem, ProblemStatus } from "../../../types";
22
import { Component, computed, inject, input } from "@angular/core";
33
import { EntailmentLabelBadgeComponent } from "./entailment-label-badge/entailment-label-badge.component";
4-
import { faArrowUpRightFromSquare, faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
4+
import { faArrowUpRightFromSquare, faMedal, faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
55
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
66
import { NgbTooltipModule } from "@ng-bootstrap/ng-bootstrap";
7-
import { datasetLabels } from "@/shared/displayTextMappings";
7+
import { datasetLabels, statusLabels } from "@/shared/displayTextMappings";
88
import { ProblemLabelsComponent } from "./problem-labels/problem-labels.component";
99
import { CommonModule } from "@angular/common";
1010
import { RouterModule } from "@angular/router";
1111
import { ProblemService } from "@/services/problem.service";
1212
import { AuthService } from "@/services/auth.service";
1313
import { VisibilityToggleComponent } from "./visibility-toggle/visibility-toggle.component";
14+
import { GoldToggleComponent } from "./gold-toggle.component";
1415

1516
export interface ProblemDetails {
1617
problemId: string;
@@ -33,7 +34,8 @@ export interface ProblemDetails {
3334
NgbTooltipModule,
3435
CommonModule,
3536
RouterModule,
36-
VisibilityToggleComponent
37+
VisibilityToggleComponent,
38+
GoldToggleComponent,
3739
],
3840
templateUrl: "./problem-details.component.html",
3941
styleUrl: "./problem-details.component.scss",
@@ -56,7 +58,10 @@ export class ProblemDetailsComponent {
5658

5759
public faQuestionCircle = faQuestionCircle;
5860
public faArrowUpRight = faArrowUpRightFromSquare;
61+
public faMedal = faMedal;
5962
public datasetLabels = datasetLabels;
63+
public statusLabels = statusLabels;
64+
public ProblemStatus = ProblemStatus;
6065

6166
public sectionString = computed<string | null>(() => {
6267
const problemDetails = this.problemDetails();

frontend/src/app/annotate/search/search.component.html

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,18 @@
4040
[options]="entailmentLabelOptions"
4141
/>
4242

43-
<label for="annotator" class="form-label" i18n>Gold</label>
43+
<label for="annotator" class="form-label" i18n>Status</label>
4444
<la-filter-select
45-
[formControl]="form.controls.gold"
46-
[options]="goldOptions"
45+
[formControl]="form.controls.status"
46+
[options]="statusOptions"
4747
/>
4848

4949
@if (canChangeVisibility) {
50-
<label for="hidden" class="form-label" i18n>Visibility</label>
51-
<la-filter-select
52-
[formControl]="form.controls.hidden"
53-
[options]="hiddenOptions"
54-
/>
50+
<label for="hidden" class="form-label" i18n>Visibility</label>
51+
<la-filter-select
52+
[formControl]="form.controls.hidden"
53+
[options]="hiddenOptions"
54+
/>
5555
}
5656
</div>
5757

frontend/src/app/annotate/search/search.component.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe("SearchComponent", () => {
1818
provide: ActivatedRoute,
1919
useValue: {
2020
params: of({ problemId: "1" }),
21-
queryParamMap: of({ dataset: null, entailmentLabel: null, gold: null, text: "" }),
21+
queryParamMap: of({ dataset: null, entailmentLabel: null, status: null, text: "" }),
2222
}
2323
}]
2424
}).compileComponents();

frontend/src/app/annotate/search/search.component.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Dataset, EntailmentLabel } from "@/types";
1+
import { Dataset, EntailmentLabel, ProblemStatus } from "@/types";
22
import { CommonModule } from "@angular/common";
33
import { Component, DestroyRef, inject } from "@angular/core";
44
import {
@@ -15,7 +15,7 @@ import {
1515
FilterSelectComponent,
1616
SelectOption,
1717
} from "./filter-select/filter-select.component";
18-
import { datasetLabels, entailmentLabels } from "@/shared/displayTextMappings";
18+
import { datasetLabels, entailmentLabels, statusLabels } from "@/shared/displayTextMappings";
1919
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
2020
import { ActivatedRoute, Router } from "@angular/router";
2121
import { IconButtonComponent } from "@/shared/icon-button/icon-button.component";
@@ -24,7 +24,7 @@ import { AuthService } from "@/services/auth.service";
2424
interface SearchParams {
2525
dataset: Dataset | null;
2626
entailmentLabel: EntailmentLabel | null;
27-
gold: boolean | null;
27+
status: ProblemStatus | null;
2828
text: string | null;
2929
hidden: boolean | null;
3030
}
@@ -56,7 +56,7 @@ export class SearchComponent {
5656
public form = new FormGroup<SearchParamsForm>({
5757
dataset: new FormControl<Dataset | null>(null),
5858
entailmentLabel: new FormControl<EntailmentLabel | null>(null),
59-
gold: new FormControl<boolean | null>(null),
59+
status: new FormControl<ProblemStatus | null>(null),
6060
text: new FormControl<string | null>(""),
6161
hidden: new FormControl<boolean | null>(null),
6262
});
@@ -85,10 +85,12 @@ export class SearchComponent {
8585
})
8686
);
8787

88-
public goldOptions: SelectOption<boolean>[] = [
89-
{ value: true, label: $localize`Gold Only` },
90-
{ value: false, label: $localize`Non-Gold Only` },
91-
];
88+
public statusOptions: SelectOption<ProblemStatus>[] = Object.values(ProblemStatus).map(
89+
(status) => ({
90+
value: status,
91+
label: statusLabels[status],
92+
})
93+
);
9294

9395
public hiddenOptions: SelectOption<boolean>[] = [
9496
{ value: true, label: $localize`Hidden Only` },
@@ -116,7 +118,7 @@ export class SearchComponent {
116118
this.form.patchValue({
117119
dataset: this.isDataset(dataset) ? dataset : null,
118120
entailmentLabel: this.isEntailmentLabel(entailmentLabel) ? entailmentLabel : null,
119-
gold: queryParams.get('gold') === null ? null : queryParams.get('gold') === 'true',
121+
status: this.isProblemStatus(queryParams.get('status')) ? queryParams.get('status') as ProblemStatus : null,
120122
text: queryParams.get('text') as string | null,
121123
hidden: queryParams.get('hidden') === null ? null : queryParams.get('hidden') === 'true',
122124
});
@@ -132,6 +134,10 @@ export class SearchComponent {
132134
return value !== null && Object.values(EntailmentLabel).includes(value as EntailmentLabel);
133135
}
134136

137+
private isProblemStatus(value: string | null): value is ProblemStatus {
138+
return value !== null && Object.values(ProblemStatus).includes(value as ProblemStatus);
139+
}
140+
135141
public clearFilters(): void {
136142
this.form.reset();
137143
}

frontend/src/app/services/problem.service.spec.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { TestBed } from "@angular/core/testing";
22
import { HttpTestingController, provideHttpClientTesting } from "@angular/common/http/testing";
33
import { ProblemService } from "./problem.service";
4-
import { Dataset, EntailmentLabel, Problem, ProblemResponse, SaveProblemResponse } from "@/types";
4+
import { Dataset, EntailmentLabel, Problem, ProblemResponse, ProblemStatus, SaveProblemResponse } from "@/types";
55
import { convertToParamMap } from "@angular/router";
66
import { provideHttpClient } from "@angular/common/http";
77
import { ParseInput } from "@/annotate/annotation-input/annotation-input.component";
@@ -61,6 +61,8 @@ describe("ProblemService", () => {
6161
kbAnnotations: [],
6262
labelAnnotations: [],
6363
hidden: false,
64+
gold: false,
65+
status: ProblemStatus.BRONZE,
6466
},
6567
index: 1,
6668
total: 1,
@@ -82,7 +84,7 @@ describe("ProblemService", () => {
8284
done();
8385
});
8486

85-
const req = httpMock.expectOne(`/api/problem/${mockProblemId}/?text=&dataset=&gold=&entailmentLabel=&hidden=`);
87+
const req = httpMock.expectOne(`/api/problem/${mockProblemId}/?text=&dataset=&status=&entailmentLabel=&hidden=`);
8688
expect(req.request.method).toBe("GET");
8789
req.flush(mockResponse);
8890
});
@@ -105,7 +107,7 @@ describe("ProblemService", () => {
105107
expect(req.request.method).toBe("GET");
106108
expect(req.request.params.get("text")).toBe("");
107109
expect(req.request.params.get("dataset")).toBe("");
108-
expect(req.request.params.get("gold")).toBe("");
110+
expect(req.request.params.get("status")).toBe("");
109111
expect(req.request.params.get("entailmentLabel")).toBe("");
110112
req.flush("Not Found", { status: 404, statusText: "Not Found" });
111113
});
@@ -123,7 +125,7 @@ describe("ProblemService", () => {
123125
const req = httpMock.expectOne(r => r.url.startsWith("/api/problem/abc"));
124126
expect(req.request.params.get("text")).toBe("search");
125127
expect(req.request.params.get("dataset")).toBe("FRACAS");
126-
expect(req.request.params.get("gold")).toBe("");
128+
expect(req.request.params.get("status")).toBe("");
127129
expect(req.request.params.get("entailmentLabel")).toBe("");
128130
req.flush({});
129131
});

frontend/src/app/services/problem.service.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ParseInput } from "@/annotate/annotation-input/annotation-input.component";
22
import extractBaseParam from "@/shared/extractBaseParam";
3-
import { ProblemResponse, SaveProblemResponse, Dataset, EntailmentLabel, Problem, Label, ToggleVisibilityInput } from "@/types";
3+
import { ProblemResponse, SaveProblemResponse, Dataset, EntailmentLabel, Problem, Label, ToggleVisibilityInput, ToggleGoldInput, ProblemStatus } from "@/types";
44
import { HttpClient, HttpParams } from "@angular/common/http";
55
import { Injectable, inject } from "@angular/core";
66
import { ParamMap } from "@angular/router";
@@ -31,7 +31,8 @@ export class ProblemService {
3131
// Submit a new problem to be saved to the database.
3232
public submit$ = new Subject<ParseInput>();
3333
public refetchProblem$ = new Subject<void>();
34-
public toggleVisibility$ = new Subject<ToggleVisibilityInput & { id: number }>();
34+
public toggleVisibility$ = new Subject<ToggleVisibilityInput & { id: number; }>();
35+
public toggleGold$ = new Subject<ToggleGoldInput & { id: number; }>();
3536

3637
private visibilityToggleSuccess$ = this.toggleVisibility$.pipe(
3738
exhaustMap(({ id, hidden }) =>
@@ -50,10 +51,28 @@ export class ProblemService {
5051
map(() => this.allParams$.value),
5152
);
5253

54+
private goldToggleSuccess$ = this.toggleGold$.pipe(
55+
exhaustMap(({ id, gold }) =>
56+
this.http.post<ToggleGoldInput>(`/api/problem/${id}/set-status/`, { gold }).pipe(
57+
catchError((error) => {
58+
this.toastService.show({
59+
header: $localize`Error updating status`,
60+
body: error.message || $localize`Could not update problem status.`,
61+
type: 'danger',
62+
});
63+
return of(null);
64+
})
65+
)
66+
),
67+
filter(result => result !== null),
68+
map(() => this.allParams$.value),
69+
);
70+
5371
public problemResponse$: Observable<ProblemResponse | null> = merge(
5472
this.allParams$,
5573
this.refetchProblem$.pipe(map(() => this.allParams$.value)),
5674
this.visibilityToggleSuccess$,
75+
this.goldToggleSuccess$,
5776
).pipe(
5877
filter(allParams => allParams !== null),
5978
switchMap(({ params, queryParams }) => {
@@ -150,6 +169,8 @@ export class ProblemService {
150169
premises: existingProblem?.premises ?? [],
151170
entailmentLabel: EntailmentLabel.UNKNOWN,
152171
hidden: false,
172+
gold: false,
173+
status: ProblemStatus.BRONZE,
153174
extraData: null,
154175
kbAnnotations: existingProblem?.kbAnnotations.map(annotation => ({
155176
...annotation, id: null,
@@ -186,14 +207,14 @@ export class ProblemService {
186207
private extractSearchParams(routeParams: ParamMap): HttpParams {
187208
const text = routeParams.get("text");
188209
const dataset = routeParams.get("dataset");
189-
const gold = routeParams.get("gold");
210+
const status = routeParams.get("status");
190211
const entailmentLabel = routeParams.get("entailmentLabel");
191212
const hidden = routeParams.get("hidden");
192213

193214
const paramRecord: Record<string, string> = {
194215
text: text ?? '',
195216
dataset: dataset ?? '',
196-
gold: gold ?? '',
217+
status: status ?? '',
197218
entailmentLabel: entailmentLabel ?? '',
198219
hidden: hidden ?? '',
199220
};

0 commit comments

Comments
 (0)