If you would like to become a reviewer,
- apply now
- .
+ apply now.
When requested, provide this ID to a CodeGov admin:
diff --git a/src/frontend/src/app/pages/proposal-details/closed-proposal-summary/closed-proposal-summary.component.ts b/src/frontend/src/app/pages/proposal-details/closed-proposal-summary/closed-proposal-summary.component.ts
index c53c721d..5f387541 100644
--- a/src/frontend/src/app/pages/proposal-details/closed-proposal-summary/closed-proposal-summary.component.ts
+++ b/src/frontend/src/app/pages/proposal-details/closed-proposal-summary/closed-proposal-summary.component.ts
@@ -5,7 +5,6 @@ import {
OnInit,
computed,
input,
- signal,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { RouterLink } from '@angular/router';
@@ -15,11 +14,7 @@ import {
DashCircleIconComponent,
CheckCircleIconComponent,
} from '@cg/angular-ui';
-import {
- GetProposalResponse,
- ProposalCommitReviewSummary,
- ProposalReviewVote,
-} from '~core/api';
+import { GetProposalResponse, ProposalCommitReviewSummary } from '~core/api';
import { ReviewService } from '~core/state';
import {
KeyColComponent,
@@ -180,10 +175,8 @@ import {
{{ review.vote }}
@@ -282,7 +275,6 @@ import {
`,
})
export class ClosedProposalSummaryComponent implements OnInit {
- public readonly ProposalReviewVote = signal(ProposalReviewVote);
public readonly proposal = input.required();
public readonly reviewList = toSignal(this.reviewService.proposalReviewList$);
@@ -291,14 +283,8 @@ export class ClosedProposalSummaryComponent implements OnInit {
() =>
this.reviewList()?.reduce(
(accum, review) => ({
- adopt:
- review.vote === ProposalReviewVote.Adopt
- ? accum.adopt + 1
- : accum.adopt,
- reject:
- review.vote === ProposalReviewVote.Reject
- ? accum.reject + 1
- : accum.reject,
+ adopt: review.vote ? accum.adopt + 1 : accum.adopt,
+ reject: review.vote === false ? accum.reject + 1 : accum.reject,
buildReproduced: review.buildReproduced
? accum.buildReproduced + 1
: accum.buildReproduced,
@@ -313,10 +299,10 @@ export class ClosedProposalSummaryComponent implements OnInit {
this.reviewList()?.forEach(review => {
for (const commit of review.commits) {
const existingCommit =
- map.get(commit.id ?? commit.listId) ??
+ map.get(commit.id) ??
({
proposalId: this.proposal().id,
- commitId: commit.id ?? commit.listId,
+ commitId: commit.id,
commitSha: commit.commitSha,
totalReviewers: 0,
reviewedCount: 0,
@@ -343,7 +329,7 @@ export class ClosedProposalSummaryComponent implements OnInit {
}
}
- map.set(commit.id ?? commit.listId, existingCommit);
+ map.set(commit.id, existingCommit);
}
});
return Array.from(map.values());
diff --git a/src/frontend/src/app/pages/proposal-list/proposal-list.component.ts b/src/frontend/src/app/pages/proposal-list/proposal-list.component.ts
index 28de7657..5ba9b87f 100644
--- a/src/frontend/src/app/pages/proposal-list/proposal-list.component.ts
+++ b/src/frontend/src/app/pages/proposal-list/proposal-list.component.ts
@@ -20,7 +20,6 @@ import {
ValueColComponent,
FormFieldComponent,
InputDirective,
- LabelDirective,
} from '~core/ui';
enum ReviewPeriodStateFilter {
@@ -50,7 +49,6 @@ interface FilterForm {
ValueColComponent,
FormFieldComponent,
InputDirective,
- LabelDirective,
FormatDatePipe,
RouterLink,
CardComponent,
diff --git a/src/frontend/src/app/pages/proposal-review-edit/proposal-review-edit.component.spec.ts b/src/frontend/src/app/pages/proposal-review-edit/proposal-review-edit.component.spec.ts
index 8c01043a..a5b8a665 100644
--- a/src/frontend/src/app/pages/proposal-review-edit/proposal-review-edit.component.spec.ts
+++ b/src/frontend/src/app/pages/proposal-review-edit/proposal-review-edit.component.spec.ts
@@ -3,12 +3,10 @@ import { ActivatedRoute, convertToParamMap } from '@angular/router';
import { of } from 'rxjs';
import {
- ProposalLinkBaseUrl,
- ProposalVotingLinkType,
- ProposalTopic,
- ProposalState,
-} from '~core/api';
-import { ProposalService, ReviewService } from '~core/state';
+ ProposalService,
+ ReviewService,
+ ReviewSubmissionService,
+} from '~core/state';
import {
ProposalServiceMock,
proposalServiceMockFactory,
@@ -17,6 +15,10 @@ import {
ReviewServiceMock,
reviewServiceMockFactory,
} from '~core/state/review/review.service.mock';
+import {
+ ReviewSubmissionServiceMock,
+ reviewSubmissionServiceMockFactory,
+} from '~core/state/review-submission/review-submission.service.mock';
import {
ActivatedRouteMock,
activatedRouteMockFactory,
@@ -30,36 +32,12 @@ describe('ProposalReviewEditComponent', () => {
let proposalServiceMock: ProposalServiceMock;
let reviewServiceMock: ReviewServiceMock;
+ let reviewSubmissionServiceMock: ReviewSubmissionServiceMock;
let activatedRouteMock: ActivatedRouteMock;
beforeEach(async () => {
proposalServiceMock = proposalServiceMockFactory();
- defineProp(
- proposalServiceMock,
- 'currentProposal$',
- of({
- id: '1',
- nsProposalId: 1n,
- title: 'title',
- topic: ProposalTopic.RVM,
- type: 'unknown',
- state: ProposalState.InProgress,
- reviewPeriodEnd: new Date(2024, 1, 17, 1, 1, 25),
- votingPeriodEnd: new Date(2024, 1, 19, 1, 1, 25),
- proposedAt: new Date(2024, 1, 15, 1, 1, 25),
- proposedBy: 432432432423n,
- decidedAt: null,
- summary: 'Elect new replica binary revision',
- reviewCompletedAt: null,
- codeGovVote: null,
- proposalLinks: [
- {
- type: ProposalVotingLinkType.NNSDApp,
- link: ProposalLinkBaseUrl.NNSDApp + 1,
- },
- ],
- }),
- );
+ defineProp(proposalServiceMock, 'currentProposal$', of(null));
reviewServiceMock = reviewServiceMockFactory();
defineProp(reviewServiceMock, 'currentReview$', of(null));
@@ -71,6 +49,8 @@ describe('ProposalReviewEditComponent', () => {
of(convertToParamMap([{ id: 1 }])),
);
+ reviewSubmissionServiceMock = reviewSubmissionServiceMockFactory();
+
await TestBed.configureTestingModule({
imports: [ProposalReviewEditComponent],
providers: [
@@ -80,6 +60,10 @@ describe('ProposalReviewEditComponent', () => {
provide: ActivatedRoute,
useValue: activatedRouteMock,
},
+ {
+ provide: ReviewSubmissionService,
+ useValue: reviewSubmissionServiceMock,
+ },
],
}).compileComponents();
diff --git a/src/frontend/src/app/pages/proposal-review-edit/proposal-review-edit.component.ts b/src/frontend/src/app/pages/proposal-review-edit/proposal-review-edit.component.ts
index 16ef8da3..746308e0 100644
--- a/src/frontend/src/app/pages/proposal-review-edit/proposal-review-edit.component.ts
+++ b/src/frontend/src/app/pages/proposal-review-edit/proposal-review-edit.component.ts
@@ -12,7 +12,11 @@ import { first } from 'rxjs';
import { CardComponent } from '@cg/angular-ui';
import { GetProposalReviewCommitResponse, ProposalState } from '~core/api';
-import { ProposalService, ReviewService } from '~core/state';
+import {
+ ProposalService,
+ ReviewService,
+ ReviewSubmissionService,
+} from '~core/state';
import { filterNotNil, routeParam, toSyncSignal } from '~core/utils';
import { ReviewCommitsFormComponent } from './review-commits-form';
import { ReviewDetailsFormComponent } from './review-details-form';
@@ -70,6 +74,7 @@ export class ProposalReviewEditComponent implements OnInit {
private readonly router = inject(Router);
private readonly proposalService = inject(ProposalService);
private readonly reviewService = inject(ReviewService);
+ private readonly reviewSubmissionService = inject(ReviewSubmissionService);
public readonly currentProposal = toSyncSignal(
this.proposalService.currentProposal$,
@@ -82,6 +87,7 @@ export class ProposalReviewEditComponent implements OnInit {
constructor() {
routeParam('id').subscribe(proposalId => {
this.proposalService.setCurrentProposalId(proposalId);
+ this.reviewSubmissionService.loadOrCreateReview(proposalId);
});
this.proposalService.currentProposal$
diff --git a/src/frontend/src/app/pages/proposal-review-edit/review-commits-form/review-commits-form.component.spec.ts b/src/frontend/src/app/pages/proposal-review-edit/review-commits-form/review-commits-form.component.spec.ts
index 962e1beb..db552f0c 100644
--- a/src/frontend/src/app/pages/proposal-review-edit/review-commits-form/review-commits-form.component.spec.ts
+++ b/src/frontend/src/app/pages/proposal-review-edit/review-commits-form/review-commits-form.component.spec.ts
@@ -1,14 +1,31 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { of } from 'rxjs';
+import { defineProp } from '../../../testing';
+import { ReviewSubmissionService } from '~core/state';
+import {
+ ReviewSubmissionServiceMock,
+ reviewSubmissionServiceMockFactory,
+} from '~core/state/review-submission/review-submission.service.mock';
import { ReviewCommitsFormComponent } from './review-commits-form.component';
describe('ReviewCommitsFormComponent', () => {
let component: ReviewCommitsFormComponent;
let fixture: ComponentFixture;
+ let reviewSubmissionServiceMock: ReviewSubmissionServiceMock;
beforeEach(async () => {
+ reviewSubmissionServiceMock = reviewSubmissionServiceMockFactory();
+ defineProp(reviewSubmissionServiceMock, 'commits$', of(null));
+
await TestBed.configureTestingModule({
imports: [ReviewCommitsFormComponent],
+ providers: [
+ {
+ provide: ReviewSubmissionService,
+ useValue: reviewSubmissionServiceMock,
+ },
+ ],
}).compileComponents();
fixture = TestBed.createComponent(ReviewCommitsFormComponent);
diff --git a/src/frontend/src/app/pages/proposal-review-edit/review-commits-form/review-commits-form.component.ts b/src/frontend/src/app/pages/proposal-review-edit/review-commits-form/review-commits-form.component.ts
index 4689559d..0d48b395 100644
--- a/src/frontend/src/app/pages/proposal-review-edit/review-commits-form/review-commits-form.component.ts
+++ b/src/frontend/src/app/pages/proposal-review-edit/review-commits-form/review-commits-form.component.ts
@@ -3,18 +3,25 @@ import {
ChangeDetectionStrategy,
Component,
ElementRef,
- signal,
+ OnDestroy,
+ computed,
+ effect,
+ inject,
viewChildren,
} from '@angular/core';
import {
+ AbstractControl,
FormControl,
FormGroup,
ReactiveFormsModule,
+ ValidatorFn,
Validators,
} from '@angular/forms';
import { Subscription } from 'rxjs';
import { CardComponent, RadioInputComponent } from '@cg/angular-ui';
+import { ReviewCommitDetails } from '~core/api';
+import { ReviewSubmissionService } from '~core/state';
import {
FormFieldComponent,
InputDirective,
@@ -24,6 +31,7 @@ import {
KeyValueGridComponent,
ValueColComponent,
} from '~core/ui';
+import { boolToRadio, isNil, radioToBool, toSyncSignal } from '~core/utils';
@Component({
selector: 'app-review-commits-form',
@@ -41,6 +49,7 @@ import {
InputHintComponent,
RadioInputComponent,
],
+ changeDetection: ChangeDetectionStrategy.OnPush,
styles: [
`
@import '@cg/styles/common';
@@ -51,20 +60,20 @@ import {
`,
],
template: `
- @for (commitForm of commitForms(); track i; let i = $index) {
-
+ @for (commit of commits(); track commit.uiId; let i = $index) {
+
-
+
@@ -77,18 +86,22 @@ import {
Please enter a valid commit hash.
+
+ Another commit entry already has this hash.
+
+
- @if (commitForm.controls.id.value) {
+ @if (commitForms()[i].controls.commitSha.value) {
https://github.com/dfinity/ic/commit/{{
- commitForm.controls.id.value
+ commitForms()[i].controls.commitSha.value
}}
} @else {
@@ -131,7 +144,9 @@ import {
-
+
Matches description
@@ -164,14 +179,14 @@ import {
-
+
@@ -180,20 +195,6 @@ import {
-
-
-
-
-
-
-
-
-
@@ -218,15 +219,48 @@ import {
`,
- changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class ReviewCommitsFormComponent {
- public readonly commitForms = signal>>([]);
- public readonly commitShaInputs =
+export class ReviewCommitsFormComponent implements OnDestroy {
+ private readonly reviewSubmissionService = inject(ReviewSubmissionService);
+
+ public readonly commits = toSyncSignal(this.reviewSubmissionService.commits$);
+ public readonly commitForms = computed(() => {
+ const commits = this.commits() ?? [];
+
+ return commits.map((commit, index) => {
+ const commitForm = this.createCommitForm(index);
+ commitForm.setValue(
+ commitToFormValue(commit.commit.commitSha, commit.commit.details),
+ );
+ return commitForm;
+ });
+ });
+
+ private readonly commitShaInputs =
viewChildren>('commitShaInput');
- private readonly commitFormReviewedSubscriptions: Subscription[] = [];
- private readonly commitShaSubscriptions: Subscription[] = [];
+ private formSubscription = new Subscription();
+
+ constructor() {
+ effect(() => {
+ const commitForms = this.commitForms();
+
+ this.formSubscription.unsubscribe();
+ this.formSubscription = new Subscription();
+
+ commitForms.forEach((commitForm, index) => {
+ this.formSubscription.add(
+ commitForm.valueChanges.subscribe(value => {
+ this.onFormValueChange(value, index, commitForm);
+ }),
+ );
+ });
+ });
+ }
+
+ public ngOnDestroy(): void {
+ this.formSubscription.unsubscribe();
+ }
// [TODO] - convert to signal
public canAddCommitForm(): boolean {
@@ -234,64 +268,73 @@ export class ReviewCommitsFormComponent {
}
public onAddCommitForm(): void {
- if (this.canAddCommitForm()) {
- const commitForm = new FormGroup({
- id: new FormControl(null, {
- validators: [Validators.required, Validators.pattern(commitShaRegex)],
- }),
- reviewed: new FormControl(null, {
- validators: [Validators.required],
- }),
- matchesDescription: new FormControl(null),
- summary: new FormControl(null),
- highlights: new FormControl(null),
- });
+ if (!this.canAddCommitForm()) {
+ return;
+ }
- this.commitForms.set([...this.commitForms(), commitForm]);
+ this.reviewSubmissionService.addCommit();
- const reviewedSubscription =
- commitForm.controls.reviewed.valueChanges.subscribe(reviewed => {
- this.onCommitReviewedChange(reviewed === 1, commitForm);
- });
- this.commitFormReviewedSubscriptions.push(reviewedSubscription);
+ const commits = this.commits();
+ if (isNil(commits)) {
+ return;
+ }
- const commitShaSubscription =
- commitForm.controls.id.valueChanges.subscribe(commitSha => {
- this.onCommitShaChange(commitSha, commitForm);
- });
- this.commitShaSubscriptions.push(commitShaSubscription);
+ setTimeout(() => {
+ this.focusCommitForm(this.commitShaInputs().length - 1);
+ });
+ }
- this.focusCommitShaInput(this.commitForms().length - 1);
+ public onRemoveCommitForm(index: number): void {
+ const commits = this.commits();
+ if (isNil(commits)) {
+ return;
}
+
+ const commitSha = commits[index]?.commit.commitSha;
+
+ this.reviewSubmissionService.removeCommit(commitSha);
+
+ setTimeout(() => {
+ this.focusCommitForm(Math.min(index, this.commitShaInputs().length - 1));
+ });
}
- public onRemoveCommitForm(index: number): void {
- this.commitForms.set([
- ...this.commitForms().slice(0, index),
- ...this.commitForms().slice(index + 1),
- ]);
-
- const [reviewedSubscription] = this.commitFormReviewedSubscriptions.splice(
- index,
- 1,
- );
- reviewedSubscription.unsubscribe();
-
- const [commitShaSubscription] = this.commitShaSubscriptions.splice(
- index,
- 1,
- );
- commitShaSubscription.unsubscribe();
-
- this.focusCommitShaInput(Math.min(index, this.commitForms().length - 1));
+ private onFormValueChange(
+ value: Partial,
+ index: number,
+ commitForm: FormGroup,
+ ): void {
+ const commits = this.commits();
+ if (isNil(commits)) {
+ return;
+ }
+
+ const commit = commits[index];
+
+ const reviewed = radioToBool(value.reviewed);
+ if (reviewed !== commit.commit.details.reviewed) {
+ this.onCommitReviewedChange(reviewed, commitForm);
+ }
+
+ if (value.commitSha !== commit.commit.commitSha) {
+ this.onCommitShaChange(value.commitSha, commitForm);
+ }
+
+ if (commitForm.valid) {
+ this.reviewSubmissionService.updateCommit(
+ commit.commit.commitSha ? commit.commit.commitSha : null,
+ value.commitSha ? value.commitSha : null,
+ commitFromFormValue(value),
+ );
+ }
}
private onCommitReviewedChange(
- reviewed: boolean,
+ reviewed: boolean | null,
commitForm: FormGroup,
): void {
const matchesDescription = commitForm.controls.matchesDescription;
- const summary = commitForm.controls.summary;
+ const summary = commitForm.controls.comment;
if (reviewed) {
matchesDescription.addValidators(Validators.required);
@@ -300,44 +343,84 @@ export class ReviewCommitsFormComponent {
matchesDescription.removeValidators(Validators.required);
summary.removeValidators(Validators.required);
- matchesDescription.reset();
- summary.reset();
+ matchesDescription.reset(null, { emitEvent: false });
+ summary.reset(null, { emitEvent: false });
}
- matchesDescription.updateValueAndValidity();
- summary.updateValueAndValidity();
+ matchesDescription.updateValueAndValidity({ emitEvent: false });
+ summary.updateValueAndValidity({ emitEvent: false });
}
private onCommitShaChange(
commitSha: string | null | undefined,
commitForm: FormGroup,
): void {
- if (!commitSha) {
+ if (isNil(commitSha)) {
return;
}
const result = extractCommitSha(commitSha);
-
- if (!result) {
+ if (isNil(result)) {
return;
}
- commitForm.controls.id.setValue(result, { emitEvent: false });
+ commitForm.controls.commitSha.setValue(result, { emitEvent: false });
}
- private focusCommitShaInput(index: number): void {
- setTimeout(() => {
- this.commitShaInputs()[index]?.nativeElement.focus();
- }, 0);
+ private focusCommitForm(index: number): void {
+ this.commitShaInputs()[index]?.nativeElement.focus();
+ }
+
+ private createCommitForm(index: number): FormGroup {
+ return new FormGroup({
+ commitSha: new FormControl(null, {
+ validators: [
+ Validators.required,
+ Validators.pattern(commitShaRegex),
+ this.uniqueCommitShaValidator(index),
+ ],
+ }),
+ reviewed: new FormControl(null, {
+ validators: [Validators.required],
+ }),
+ matchesDescription: new FormControl(null),
+ comment: new FormControl(null),
+ });
+ }
+
+ private uniqueCommitShaValidator(index: number): ValidatorFn {
+ return (control: AbstractControl) => {
+ const commits = this.commits();
+ if (isNil(commits)) {
+ return null;
+ }
+
+ const commitSha = extractCommitSha(control.value);
+ if (isNil(commitSha)) {
+ return null;
+ }
+
+ const existingCommit = commits.find(
+ (commit, i) => i !== index && commit.commit.commitSha === commitSha,
+ );
+
+ return existingCommit ? { uniqueSha: true } : null;
+ };
}
}
+interface ReviewCommitFormValue {
+ commitSha: string | null;
+ reviewed: 0 | 1 | null;
+ matchesDescription: 0 | 1 | null;
+ comment: string | null;
+}
+
interface ReviewCommitForm {
- id: FormControl;
+ commitSha: FormControl;
reviewed: FormControl<0 | 1 | null>;
matchesDescription: FormControl<0 | 1 | null>;
- summary: FormControl;
- highlights: FormControl;
+ comment: FormControl;
}
const commitShaRegex = /[0-9a-f]{7,40}/;
@@ -345,3 +428,42 @@ const commitShaRegex = /[0-9a-f]{7,40}/;
function extractCommitSha(commitSha: string): string | null {
return commitShaRegex.exec(commitSha)?.[0] ?? null;
}
+
+function commitToFormValue(
+ commitSha: string | null,
+ commit: ReviewCommitDetails,
+): ReviewCommitFormValue {
+ const reviewed = boolToRadio(commit.reviewed);
+
+ let matchesDescription = null;
+ let comment = null;
+
+ if (commit.reviewed) {
+ matchesDescription = boolToRadio(commit.matchesDescription);
+ comment = commit.comment;
+ }
+
+ return {
+ commitSha: commitSha,
+ matchesDescription,
+ reviewed,
+ comment,
+ };
+}
+
+function commitFromFormValue(
+ formValue: Partial,
+): ReviewCommitDetails {
+ const reviewed = radioToBool(formValue.reviewed);
+
+ if (reviewed) {
+ return {
+ matchesDescription: radioToBool(formValue.matchesDescription),
+ reviewed: true,
+ comment: formValue.comment ?? null,
+ highlights: [],
+ };
+ }
+
+ return { reviewed };
+}
diff --git a/src/frontend/src/app/pages/proposal-review-edit/review-details-form/review-details-form.component.spec.ts b/src/frontend/src/app/pages/proposal-review-edit/review-details-form/review-details-form.component.spec.ts
index 4905f6eb..520223a6 100644
--- a/src/frontend/src/app/pages/proposal-review-edit/review-details-form/review-details-form.component.spec.ts
+++ b/src/frontend/src/app/pages/proposal-review-edit/review-details-form/review-details-form.component.spec.ts
@@ -1,14 +1,31 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { of } from 'rxjs';
+import { defineProp } from '../../../testing';
+import { ReviewSubmissionService } from '~core/state';
+import {
+ ReviewSubmissionServiceMock,
+ reviewSubmissionServiceMockFactory,
+} from '~core/state/review-submission/review-submission.service.mock';
import { ReviewDetailsFormComponent } from './review-details-form.component';
describe('ReviewDetailsFormComponent', () => {
let component: ReviewDetailsFormComponent;
let fixture: ComponentFixture;
+ let reviewSubmissionServiceMock: ReviewSubmissionServiceMock;
beforeEach(async () => {
+ reviewSubmissionServiceMock = reviewSubmissionServiceMockFactory();
+ defineProp(reviewSubmissionServiceMock, 'review$', of(null));
+
await TestBed.configureTestingModule({
imports: [ReviewDetailsFormComponent],
+ providers: [
+ {
+ provide: ReviewSubmissionService,
+ useValue: reviewSubmissionServiceMock,
+ },
+ ],
}).compileComponents();
fixture = TestBed.createComponent(ReviewDetailsFormComponent);
diff --git a/src/frontend/src/app/pages/proposal-review-edit/review-details-form/review-details-form.component.ts b/src/frontend/src/app/pages/proposal-review-edit/review-details-form/review-details-form.component.ts
index a25c3499..fd5203bc 100644
--- a/src/frontend/src/app/pages/proposal-review-edit/review-details-form/review-details-form.component.ts
+++ b/src/frontend/src/app/pages/proposal-review-edit/review-details-form/review-details-form.component.ts
@@ -1,12 +1,21 @@
import { CommonModule } from '@angular/common';
-import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ OnDestroy,
+ effect,
+ inject,
+ signal,
+} from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
+import { Subscription } from 'rxjs';
import {
ImageSet,
ImageUploaderBtnComponent,
RadioInputComponent,
} from '@cg/angular-ui';
+import { ReviewSubmissionService } from '~core/state';
import {
FormFieldComponent,
InputDirective,
@@ -14,6 +23,7 @@ import {
KeyValueGridComponent,
ValueColComponent,
} from '~core/ui';
+import { boolToRadio, isNil, radioToBool, toSyncSignal } from '~core/utils';
@Component({
selector: 'app-review-details-form',
@@ -34,14 +44,14 @@ import {
-
+
@@ -84,6 +94,33 @@ import {
+
+ Vote to adopt
+
+
+
+
+
+ Yes
+
+
+
+ No
+
+
+
+
+
Build verification images
@@ -102,24 +139,92 @@ import {
`,
})
-export class ReviewDetailsFormComponent {
+export class ReviewDetailsFormComponent implements OnDestroy {
+ private readonly reviewSubmissionService = inject(ReviewSubmissionService);
+
+ private readonly review = toSyncSignal(this.reviewSubmissionService.review$);
+
public readonly reviewForm = signal(
new FormGroup({
- timeSpent: new FormControl(null),
+ reviewDurationMins: new FormControl(null),
summary: new FormControl(null),
buildReproduced: new FormControl(null),
+ vote: new FormControl(null),
}),
);
-
public selectedImages = signal([]);
+ private formSubscription = new Subscription();
+
+ constructor() {
+ effect(() => {
+ const reviewForm = this.reviewForm();
+
+ this.formSubscription.unsubscribe();
+ this.formSubscription = new Subscription();
+
+ this.formSubscription.add(
+ reviewForm.valueChanges.subscribe(value => {
+ this.onFormValueChange(value, reviewForm);
+ }),
+ );
+ });
+
+ effect(() => {
+ const review = this.review();
+ if (!review) {
+ return;
+ }
+
+ this.reviewForm().setValue(
+ {
+ reviewDurationMins: review.reviewDurationMins,
+ summary: review.summary,
+ buildReproduced: boolToRadio(review.buildReproduced),
+ vote: boolToRadio(review.vote),
+ },
+ { emitEvent: false },
+ );
+ });
+ }
+
+ public ngOnDestroy(): void {
+ this.formSubscription.unsubscribe();
+ }
+
public onImagesSelected(images: ImageSet[]): void {
this.selectedImages.set(images);
}
+
+ private onFormValueChange(
+ value: Partial,
+ reviewForm: FormGroup,
+ ): void {
+ const review = this.review();
+ if (!reviewForm.valid || isNil(review)) {
+ return;
+ }
+
+ this.reviewSubmissionService.updateReview({
+ proposalId: review.proposalId,
+ summary: value.summary ?? null,
+ reviewDurationMins: value.reviewDurationMins ?? null,
+ buildReproduced: radioToBool(value.buildReproduced),
+ vote: radioToBool(value.vote),
+ });
+ }
+}
+
+interface ReviewFormValue {
+ reviewDurationMins: number | null;
+ summary: string | null;
+ buildReproduced: 0 | 1 | null;
+ vote: 0 | 1 | null;
}
interface ReviewForm {
- timeSpent: FormControl;
+ reviewDurationMins: FormControl;
summary: FormControl;
- buildReproduced: FormControl;
+ buildReproduced: FormControl<0 | 1 | null>;
+ vote: FormControl<0 | 1 | null>;
}
diff --git a/src/frontend/src/app/pages/proposal-review/proposal-review.component.ts b/src/frontend/src/app/pages/proposal-review/proposal-review.component.ts
index ba980942..4710899d 100644
--- a/src/frontend/src/app/pages/proposal-review/proposal-review.component.ts
+++ b/src/frontend/src/app/pages/proposal-review/proposal-review.component.ts
@@ -9,11 +9,7 @@ import { toSignal } from '@angular/core/rxjs-interop';
import { RouterLink } from '@angular/router';
import { CardComponent } from '@cg/angular-ui';
-import {
- ProposalReviewStatus,
- ProposalReviewVote,
- ProposalState,
-} from '~core/api';
+import { ProposalReviewStatus, ProposalState } from '~core/api';
import { ProfileService, ProposalService, ReviewService } from '~core/state';
import {
KeyColComponent,
@@ -84,10 +80,8 @@ import { isNotNil, routeParam } from '~core/utils';
{{ review.vote }}
@@ -200,7 +194,6 @@ import { isNotNil, routeParam } from '~core/utils';
})
export class ProposalReviewComponent {
public readonly ProposalReviewStatus = signal(ProposalReviewStatus);
- public readonly ProposalReviewVote = signal(ProposalReviewVote);
public readonly ProposalState = signal(ProposalState);
public readonly userProfile = toSignal(this.profileService.userProfile$);
diff --git a/src/frontend/src/app/testing/test-utils.ts b/src/frontend/src/app/testing/test-utils.ts
index 801b976a..70cc3d39 100644
--- a/src/frontend/src/app/testing/test-utils.ts
+++ b/src/frontend/src/app/testing/test-utils.ts
@@ -1,3 +1,5 @@
+import { TestScheduler } from 'rxjs/testing';
+
export function defineProp(
obj: T,
key: K,
@@ -8,3 +10,9 @@ export function defineProp(
writable: false,
});
}
+
+export function createTestScheduler(): TestScheduler {
+ return new TestScheduler((actual, expected) => {
+ expect(actual).toEqual(expected);
+ });
+}