Skip to content

Commit dae85dc

Browse files
committed
SF-3759 Add frontend interface
1 parent cae91b0 commit dae85dc

6 files changed

Lines changed: 255 additions & 20 deletions

File tree

src/SIL.XForge.Scripture/ClientApp/src/app/core/sf-project.service.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,19 @@ describe('SFProjectService', () => {
180180
}));
181181
});
182182

183+
describe('onlineSetQualityEstimationConfig', () => {
184+
it('should invoke the command service', fakeAsync(async () => {
185+
const env = new TestEnvironment();
186+
await env.service.onlineSetQualityEstimationConfig('project01', {
187+
version: '0.1',
188+
slope: 109.6145,
189+
intercept: -14.0633
190+
});
191+
verify(mockedCommandService.onlineInvoke(anything(), 'setQualityEstimationConfig', anything())).once();
192+
expect().nothing();
193+
}));
194+
});
195+
183196
class TestEnvironment {
184197
readonly httpTestingController: HttpTestingController;
185198
readonly service: SFProjectService;

src/SIL.XForge.Scripture/ClientApp/src/app/core/sf-project.service.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import { SF_PROJECT_RIGHTS, SFProjectDomain } from 'realtime-server/lib/esm/scri
1414
import { SFProjectRole } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-role';
1515
import { getSFProjectUserConfigDocId } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-user-config';
1616
import { TextAudio } from 'realtime-server/lib/esm/scriptureforge/models/text-audio';
17-
import { DraftUsfmConfig } from 'realtime-server/lib/esm/scriptureforge/models/translate-config';
17+
import {
18+
DraftUsfmConfig,
19+
QualityEstimationConfig
20+
} from 'realtime-server/lib/esm/scriptureforge/models/translate-config';
1821
import { Subject } from 'rxjs';
1922
import { CommandService } from 'xforge-common/command.service';
2023
import { LocationService } from 'xforge-common/location.service';
@@ -362,6 +365,16 @@ export class SFProjectService extends ProjectService<SFProject, SFProjectDoc> {
362365
});
363366
}
364367

368+
async onlineSetQualityEstimationConfig(
369+
projectId: string,
370+
qualityEstimationConfig: QualityEstimationConfig | null
371+
): Promise<void> {
372+
return await this.onlineInvoke<void>('setQualityEstimationConfig', {
373+
projectId,
374+
qualityEstimationConfig
375+
});
376+
}
377+
365378
async onlineSetServalConfig(projectId: string, servalConfig: string | null | undefined): Promise<void> {
366379
return await this.onlineInvoke<void>('setServalConfig', {
367380
projectId,

src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/serval-project.component.html

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,52 @@ <h2>Pre-Translation Configuration</h2>
6666
<mat-form-field [formGroup]="form" appearance="outline" class="serval-config-field">
6767
<mat-label>NMT Build Options Override (JSON)</mat-label>
6868
<textarea matInput cdkTextareaAutosize id="serval-config" formControlName="servalConfig"></textarea>
69-
<app-write-status [state]="updateState" [formGroup]="form" id="serval-config-status"></app-write-status>
69+
<app-write-status
70+
[state]="servalConfigUpdateState"
71+
[formGroup]="form"
72+
id="serval-config-status"
73+
></app-write-status>
7074
</mat-form-field>
7175
</mat-card-content>
7276
<mat-card-actions>
7377
<button mat-flat-button id="save-serval-config" color="primary" (click)="updateServalConfig()">Save</button>
7478
</mat-card-actions>
7579
</mat-card>
80+
<mat-card>
81+
<mat-card-header>
82+
<mat-card-title>Quality Estimation Configuration</mat-card-title>
83+
</mat-card-header>
84+
<mat-card-content>
85+
<p>
86+
This value must be a valid JSON string, and will be in the file linregress.json created by
87+
quality_estimation.py.
88+
</p>
89+
<mat-form-field [formGroup]="form" appearance="outline" class="quality-estimation-config-field">
90+
<mat-label>Quality Estimation Configuration (JSON)</mat-label>
91+
<textarea
92+
matInput
93+
cdkTextareaAutosize
94+
id="quality-estimation-config"
95+
formControlName="qualityEstimationConfig"
96+
></textarea>
97+
<app-write-status
98+
[state]="qualityEstimationConfigUpdateState"
99+
[formGroup]="form"
100+
id="quality-estimation-config-status"
101+
></app-write-status>
102+
</mat-form-field>
103+
</mat-card-content>
104+
<mat-card-actions>
105+
<button
106+
mat-flat-button
107+
id="save-quality-estimation-config"
108+
color="primary"
109+
(click)="updateQualityEstimationConfig()"
110+
>
111+
Save
112+
</button>
113+
</mat-card-actions>
114+
</mat-card>
76115
</div>
77116
</div>
78117

src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/serval-project.component.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
flex-direction: row;
99
column-gap: 10px;
1010

11-
.serval-config-field {
11+
.serval-config-field,
12+
.quality-estimation-config-field {
1213
width: 100%;
1314
}
1415
}

src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/serval-project.component.spec.ts

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { BehaviorSubject, of, throwError } from 'rxjs';
1212
import { anything, instance, mock, verify, when } from 'ts-mockito';
1313
import { ActivatedProjectService } from 'xforge-common/activated-project.service';
1414
import { AuthService } from 'xforge-common/auth.service';
15+
import { CommandError, CommandErrorCode } from 'xforge-common/command.service';
1516
import { FileService } from 'xforge-common/file.service';
1617
import { I18nService } from 'xforge-common/i18n.service';
1718
import { FileType } from 'xforge-common/models/file-offline-data';
@@ -308,6 +309,103 @@ describe('ServalProjectComponent', () => {
308309
verify(mockSFProjectService.onlineSetServalConfig(env.mockProjectId, anything())).never();
309310
expect(env.statusDone(env.servalConfigStatus)).toBeNull();
310311
}));
312+
313+
it('should notify of a backend error', fakeAsync(() => {
314+
const env = new TestEnvironment();
315+
when(mockSFProjectService.onlineSetServalConfig(env.mockProjectId, anything())).thenReject(
316+
new CommandError(CommandErrorCode.InternalError, 'error')
317+
);
318+
expect(env.servalConfigTextArea.value).toBe('');
319+
expect(env.statusError(env.servalConfigStatus)).toBeNull();
320+
321+
env.setServalConfigValue('{}');
322+
env.clickElement(env.saveServalConfigButton);
323+
324+
verify(mockSFProjectService.onlineSetServalConfig(env.mockProjectId, anything())).once();
325+
expect(env.statusError(env.servalConfigStatus)).not.toBeNull();
326+
}));
327+
});
328+
329+
describe('quality estimation configuration', () => {
330+
it('should change quality estimation config value', fakeAsync(() => {
331+
const env = new TestEnvironment();
332+
expect(env.qualityEstimationConfigTextArea.value).toBe('');
333+
expect(env.statusDone(env.qualityEstimationConfigStatus)).toBeNull();
334+
335+
env.setQualityEstimationConfigValue('{ "version": "0.1", "slope": 109.6145, "intercept": -14.0633 }');
336+
env.clickElement(env.saveQualityEstimationConfigButton);
337+
338+
verify(mockSFProjectService.onlineSetQualityEstimationConfig(env.mockProjectId, anything())).once();
339+
expect(env.statusDone(env.qualityEstimationConfigStatus)).not.toBeNull();
340+
}));
341+
342+
it('should clear the quality estimation config value', fakeAsync(() => {
343+
const env = new TestEnvironment({
344+
preTranslate: true,
345+
draftConfig: { qualityEstimationConfig: { version: '0.1', slope: 109.6145, intercept: -14.0633 } }
346+
});
347+
expect(env.qualityEstimationConfigTextArea.value).toBe(
348+
'{"version":"0.1","slope":109.6145,"intercept":-14.0633}'
349+
);
350+
expect(env.statusDone(env.qualityEstimationConfigStatus)).toBeNull();
351+
352+
env.setQualityEstimationConfigValue('');
353+
env.clickElement(env.saveQualityEstimationConfigButton);
354+
355+
verify(mockSFProjectService.onlineSetQualityEstimationConfig(env.mockProjectId, anything())).once();
356+
expect(env.statusDone(env.qualityEstimationConfigStatus)).not.toBeNull();
357+
}));
358+
359+
it('should not update an unchanged quality estimation config value', fakeAsync(() => {
360+
const env = new TestEnvironment();
361+
expect(env.qualityEstimationConfigTextArea.value).toBe('');
362+
expect(env.statusDone(env.qualityEstimationConfigStatus)).toBeNull();
363+
364+
env.setQualityEstimationConfigValue('');
365+
env.clickElement(env.saveQualityEstimationConfigButton);
366+
367+
verify(mockSFProjectService.onlineSetQualityEstimationConfig(env.mockProjectId, anything())).never();
368+
expect(env.statusDone(env.qualityEstimationConfigStatus)).toBeNull();
369+
}));
370+
371+
it('should not update a non-JSON value', fakeAsync(() => {
372+
const env = new TestEnvironment();
373+
expect(env.qualityEstimationConfigTextArea.value).toBe('');
374+
expect(env.statusError(env.qualityEstimationConfigStatus)).toBeNull();
375+
376+
env.setQualityEstimationConfigValue('test');
377+
env.clickElement(env.saveQualityEstimationConfigButton);
378+
379+
verify(mockSFProjectService.onlineSetQualityEstimationConfig(env.mockProjectId, anything())).never();
380+
expect(env.statusError(env.qualityEstimationConfigStatus)).not.toBeNull();
381+
}));
382+
383+
it('should not update an invalid value', fakeAsync(() => {
384+
const env = new TestEnvironment();
385+
expect(env.qualityEstimationConfigTextArea.value).toBe('');
386+
expect(env.statusError(env.qualityEstimationConfigStatus)).toBeNull();
387+
388+
env.setQualityEstimationConfigValue('{"prop": "value"}');
389+
env.clickElement(env.saveQualityEstimationConfigButton);
390+
391+
verify(mockSFProjectService.onlineSetQualityEstimationConfig(env.mockProjectId, anything())).never();
392+
expect(env.statusError(env.qualityEstimationConfigStatus)).not.toBeNull();
393+
}));
394+
395+
it('should notify of a backend error', fakeAsync(() => {
396+
const env = new TestEnvironment();
397+
when(mockSFProjectService.onlineSetQualityEstimationConfig(env.mockProjectId, anything())).thenReject(
398+
new CommandError(CommandErrorCode.InternalError, 'error')
399+
);
400+
expect(env.qualityEstimationConfigTextArea.value).toBe('');
401+
expect(env.statusError(env.qualityEstimationConfigStatus)).toBeNull();
402+
403+
env.setQualityEstimationConfigValue('{ "version": "0.1", "slope": 109.6145, "intercept": -14.0633 }');
404+
env.clickElement(env.saveQualityEstimationConfigButton);
405+
406+
verify(mockSFProjectService.onlineSetQualityEstimationConfig(env.mockProjectId, anything())).once();
407+
expect(env.statusError(env.qualityEstimationConfigStatus)).not.toBeNull();
408+
}));
311409
});
312410
});
313411

@@ -363,7 +461,8 @@ describe('ServalProjectComponent', () => {
363461
lastSelectedTrainingScriptureRanges: args.draftConfig?.lastSelectedTrainingScriptureRanges ?? undefined,
364462
lastSelectedTranslationScriptureRanges:
365463
args.draftConfig?.lastSelectedTranslationScriptureRanges ?? undefined,
366-
servalConfig: args.draftConfig?.servalConfig ?? undefined
464+
servalConfig: args.draftConfig?.servalConfig ?? undefined,
465+
qualityEstimationConfig: args.draftConfig?.qualityEstimationConfig ?? undefined
367466
},
368467
preTranslate: args.preTranslate,
369468
source: {
@@ -395,6 +494,7 @@ describe('ServalProjectComponent', () => {
395494
when(mockDraftGenerationService.getBuildProgress(anything())).thenReturn(of({ additionalInfo: {} } as BuildDto));
396495
when(mockSFProjectService.hasDraft(anything())).thenReturn(args.preTranslate);
397496
when(mockSFProjectService.onlineSetServalConfig(this.mockProjectId, anything())).thenResolve();
497+
when(mockSFProjectService.onlineSetQualityEstimationConfig(this.mockProjectId, anything())).thenResolve();
398498
const trainingData: TrainingDataDoc[] = [
399499
{
400500
id: 'training01',
@@ -444,6 +544,18 @@ describe('ServalProjectComponent', () => {
444544
return this.fixture.nativeElement.querySelector('.training-data-table td button');
445545
}
446546

547+
get qualityEstimationConfigStatus(): DebugElement {
548+
return this.fixture.debugElement.query(By.css('#quality-estimation-config-status'));
549+
}
550+
551+
get qualityEstimationConfigTextArea(): HTMLTextAreaElement {
552+
return this.fixture.nativeElement.querySelector('#quality-estimation-config') as HTMLTextAreaElement;
553+
}
554+
555+
get saveQualityEstimationConfigButton(): HTMLInputElement {
556+
return this.fixture.nativeElement.querySelector('#save-quality-estimation-config');
557+
}
558+
447559
get saveServalConfigButton(): HTMLInputElement {
448560
return this.fixture.nativeElement.querySelector('#save-serval-config');
449561
}
@@ -485,6 +597,14 @@ describe('ServalProjectComponent', () => {
485597
return node.querySelector('.translation-range')?.textContent ?? '';
486598
}
487599

600+
setQualityEstimationConfigValue(value: string): void {
601+
this.qualityEstimationConfigTextArea.value = value;
602+
this.qualityEstimationConfigTextArea.dispatchEvent(new Event('input'));
603+
this.fixture.detectChanges();
604+
tick();
605+
this.fixture.detectChanges();
606+
}
607+
488608
setServalConfigValue(value: string): void {
489609
this.servalConfigTextArea.value = value;
490610
this.servalConfigTextArea.dispatchEvent(new Event('input'));
@@ -496,5 +616,9 @@ describe('ServalProjectComponent', () => {
496616
statusDone(element: DebugElement): HTMLElement {
497617
return element.nativeElement.querySelector('.check-icon') as HTMLElement;
498618
}
619+
620+
statusError(element: DebugElement): HTMLElement {
621+
return element.nativeElement.querySelector('.error-icon') as HTMLElement;
622+
}
499623
}
500624
});

0 commit comments

Comments
 (0)