Skip to content

Commit

Permalink
SF-3077 Add UI for mixed training sources (#2875)
Browse files Browse the repository at this point in the history
  • Loading branch information
RaymondLuong3 authored Dec 19, 2024
1 parent 1897259 commit 1a603bd
Show file tree
Hide file tree
Showing 23 changed files with 700 additions and 200 deletions.
9 changes: 9 additions & 0 deletions src/RealtimeServer/scriptureforge/models/translate-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ export interface BaseProject {
shortName: string;
}

/**
* A per-project scripture range.
*/
export interface ProjectScriptureRange {
projectId: string;
scriptureRange: string;
}

export interface DraftConfig {
additionalTrainingData: boolean;
additionalTrainingSourceEnabled: boolean;
Expand All @@ -38,6 +46,7 @@ export interface DraftConfig {
lastSelectedTrainingBooks: number[];
lastSelectedTrainingDataFiles: string[];
lastSelectedTrainingScriptureRange?: string;
lastSelectedTrainingScriptureRanges?: ProjectScriptureRange[];
lastSelectedTranslationBooks: number[];
lastSelectedTranslationScriptureRange?: string;
servalConfig?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,28 @@ describe('SFProjectMigrations', () => {
expect(projectDoc.data.translateConfig.shareEnabled).not.toBeDefined();
});
});

describe('version 22', () => {
it('copies selected training and translation books to scripture ranges', async () => {
const env = new TestEnvironment(21);
const conn = env.server.connect();

await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', {
translateConfig: { draftConfig: { lastSelectedTrainingBooks: [1, 2, 3], lastSelectedTranslationBooks: [4, 5] } }
});
let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
expect(projectDoc.data.translateConfig.draftConfig.lastSelectedTrainingBooks).toEqual([1, 2, 3]);
expect(projectDoc.data.translateConfig.draftConfig.lastSelectedTranslationBooks).toEqual([4, 5]);

await env.server.migrateIfNecessary();

projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01');
expect(projectDoc.data.translateConfig.draftConfig.lastSelectedTrainingBooks).toEqual([1, 2, 3]);
expect(projectDoc.data.translateConfig.draftConfig.lastSelectedTranslationBooks).toEqual([4, 5]);
expect(projectDoc.data.translateConfig.draftConfig.lastSelectedTrainingScriptureRange).toEqual('GEN;EXO;LEV');
expect(projectDoc.data.translateConfig.draftConfig.lastSelectedTranslationScriptureRange).toEqual('NUM;DEU');
});
});
});

class TestEnvironment {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Canon } from '@sillsdev/scripture';
import { Doc, Op } from 'sharedb/lib/client';
import { DocMigration, MigrationConstructor, monotonicallyIncreasingMigrationList } from '../../common/migration';
import { Operation } from '../../common/models/project-rights';
Expand Down Expand Up @@ -387,6 +388,38 @@ class SFProjectMigration21 extends DocMigration {
}
}

class SFProjectMigration22 extends DocMigration {
static readonly VERSION = 22;

async migrateDoc(doc: Doc): Promise<void> {
const ops: Op[] = [];
if (doc.data.translateConfig.draftConfig.lastSelectedTrainingScriptureRange == null) {
const trainingRangeFromBooks: string[] = doc.data.translateConfig.draftConfig.lastSelectedTrainingBooks.map(
(b: number) => Canon.bookNumberToId(b)
);
if (trainingRangeFromBooks.length > 0) {
ops.push({
p: ['translateConfig', 'draftConfig', 'lastSelectedTrainingScriptureRange'],
oi: trainingRangeFromBooks.join(';')
});
}
}
if (doc.data.translateConfig.draftConfig.lastSelectedTranslationScriptureRange == null) {
const translationRangeFromBooks: string[] = doc.data.translateConfig.draftConfig.lastSelectedTranslationBooks.map(
(b: number) => Canon.bookNumberToId(b)
);
if (translationRangeFromBooks.length > 0) {
ops.push({
p: ['translateConfig', 'draftConfig', 'lastSelectedTranslationScriptureRange'],
oi: translationRangeFromBooks.join(';')
});
}
}

await submitMigrationOp(SFProjectMigration22.VERSION, doc, ops);
}
}

export const SF_PROJECT_MIGRATIONS: MigrationConstructor[] = monotonicallyIncreasingMigrationList([
SFProjectMigration1,
SFProjectMigration2,
Expand All @@ -408,5 +441,6 @@ export const SF_PROJECT_MIGRATIONS: MigrationConstructor[] = monotonicallyIncrea
SFProjectMigration18,
SFProjectMigration19,
SFProjectMigration20,
SFProjectMigration21
SFProjectMigration21,
SFProjectMigration22
]);
15 changes: 15 additions & 0 deletions src/RealtimeServer/scriptureforge/services/sf-project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,21 @@ export class SFProjectService extends ProjectService<SFProject> {
lastSelectedTrainingScriptureRange: {
bsonType: 'string'
},
lastSelectedTrainingScriptureRanges: {
bsonType: 'array',
items: {
bsonType: 'object',
properties: {
projectId: {
bsonType: 'string'
},
scriptureRange: {
bsonType: 'string'
}
},
additionalProperties: false
}
},
lastSelectedTranslationBooks: {
bsonType: 'array',
items: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,9 @@ describe('ServalProjectComponent', () => {
shortName: 'P4'
},
lastSelectedTrainingBooks: preTranslate ? [1, 2] : [],
lastSelectedTranslationBooks: preTranslate ? [3, 4] : []
lastSelectedTranslationBooks: preTranslate ? [3, 4] : [],
lastSelectedTrainingScriptureRange: preTranslate ? 'GEN;EXO' : undefined,
lastSelectedTranslationScriptureRange: preTranslate ? 'LEV;NUM' : undefined
},
preTranslate,
source: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { BuildDto } from '../machine-api/build-dto';
import { MobileNotSupportedComponent } from '../shared/mobile-not-supported/mobile-not-supported.component';
import { NoticeComponent } from '../shared/notice/notice.component';
import { SharedModule } from '../shared/shared.module';
import { projectLabel } from '../shared/utils';
import { booksFromScriptureRange, projectLabel } from '../shared/utils';
import { DraftZipProgress } from '../translate/draft-generation/draft-generation';
import { DraftGenerationService } from '../translate/draft-generation/draft-generation.service';
import { DraftInformationComponent } from '../translate/draft-generation/draft-information/draft-information.component';
Expand Down Expand Up @@ -150,13 +150,13 @@ export class ServalProjectComponent extends DataLoadingComponent implements OnIn
this.rows = rows;

// Setup the books
this.trainingBooks = project.translateConfig.draftConfig.lastSelectedTrainingBooks.map(bookNum =>
Canon.bookNumberToEnglishName(bookNum)
);
this.trainingBooks = booksFromScriptureRange(
project.translateConfig.draftConfig.lastSelectedTrainingScriptureRange ?? ''
).map(bookNum => Canon.bookNumberToEnglishName(bookNum));
this.trainingFiles = project.translateConfig.draftConfig.lastSelectedTrainingDataFiles;
this.translationBooks = project.translateConfig.draftConfig.lastSelectedTranslationBooks.map(bookNum =>
Canon.bookNumberToEnglishName(bookNum)
);
this.translationBooks = booksFromScriptureRange(
project.translateConfig.draftConfig.lastSelectedTranslationScriptureRange ?? ''
).map(bookNum => Canon.bookNumberToEnglishName(bookNum));

this.draftConfig = project.translateConfig.draftConfig;
this.draftJob$ = SFProjectService.hasDraft(project) ? this.getDraftJob(projectDoc.id) : of(undefined);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<ng-container *transloco="let t; read: 'book_select'">
@if (availableBooks.length > 0 && !readonly) {
<div>
@if (availableBooks.length > 0 && !readonly && !basicMode) {
<div class="scope-selection">
@if (projectName != null) {
<span class="project-name">{{ projectName }}</span>
}
<mat-checkbox
class="ot-checkbox"
value="OT"
Expand Down Expand Up @@ -44,14 +47,20 @@
[disabled]="readonly"
(change)="onChipListChange(book)"
>
<mat-chip-option
[value]="book"
[selected]="book.selected"
[matTooltip]="t('book_progress', { percent: getPercentage(book) | l10nPercent })"
>
{{ "canon.book_names." + book.bookId | transloco }}
<div class="border-fill" [style.width]="book.progressPercentage + '%'"></div>
</mat-chip-option>
@if (!basicMode) {
<mat-chip-option
[value]="book"
[selected]="book.selected"
[matTooltip]="t('book_progress', { percent: getPercentage(book) | l10nPercent })"
>
{{ "canon.book_names." + book.bookId | transloco }}
<div class="border-fill" [style.width]="book.progressPercentage + '%'"></div>
</mat-chip-option>
} @else {
<mat-chip-option [value]="book" [selected]="book.selected">
{{ "canon.book_names." + book.bookId | transloco }}
</mat-chip-option>
}
</mat-chip-listbox>
}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
@use 'src/variables';

.scope-selection {
display: flex;
align-items: center;
column-gap: 8px;

.project-name {
font-weight: 500;
}
}

.book-multi-select {
.mat-mdc-standard-chip {
position: relative;
Expand All @@ -24,29 +34,6 @@
}
}

.bulk-select {
width: fit-content;
margin-top: 2px;
margin-block-end: 12px;
font-weight: 300;
div {
display: flex;
align-items: center;
column-gap: 12px;
button {
padding-inline: 12px;
font-weight: 300;
font-size: 16px;
}
mat-button-toggle {
width: 50px;
}
.mat-button-toggle-checked {
background-color: unset;
}
}
}

.loading-message {
display: flex;
gap: 0.5em;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,16 @@ describe('BookMultiSelectComponent', () => {
fixture.detectChanges();
});

it('should initialize book options on ngOnChanges', async () => {
it('supports providing project name', async () => {
await component.ngOnChanges();
expect(fixture.nativeElement.querySelector('.project-name')).toBeNull();
component.projectName = 'Test Project';
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.project-name')).not.toBeNull();
});

it('should initialize book options on ngOnChanges', async () => {
await component.ngOnChanges();
expect(component.bookOptions).toEqual([
{ bookNum: 1, bookId: 'GEN', selected: true, progressPercentage: 0 },
{ bookNum: 2, bookId: 'EXO', selected: false, progressPercentage: 15 },
Expand Down Expand Up @@ -137,4 +144,15 @@ describe('BookMultiSelectComponent', () => {
expect(component.partialOT).toBe(false);
expect(component.partialDC).toBe(true);
});

it('can hide checkboxes and progress in basic mode', async () => {
await component.ngOnChanges();
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.book-multi-select .border-fill')).not.toBeNull();
expect(fixture.nativeElement.querySelector('.scope-selection')).not.toBeNull();
component.basicMode = true;
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.book-multi-select .border-fill')).toBeNull();
expect(fixture.nativeElement.querySelector('.scope-selection')).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export class BookMultiSelectComponent extends SubscriptionDisposable implements
@Input() availableBooks: number[] = [];
@Input() selectedBooks: number[] = [];
@Input() readonly: boolean = false;
@Input() projectName?: string;
@Input() basicMode: boolean = false;
@Output() bookSelect = new EventEmitter<number[]>();

protected loaded = false;
Expand Down
8 changes: 7 additions & 1 deletion src/SIL.XForge.Scripture/ClientApp/src/app/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SFProjectProfileDoc } from '../core/models/sf-project-profile-doc';
import { roleCanAccessCommunityChecking, roleCanAccessTranslate } from '../core/models/sf-project-role-info';
import { SFProjectUserConfigDoc } from '../core/models/sf-project-user-config-doc';
import { SelectableProject } from '../core/paratext.service';
import { DraftSource } from '../translate/draft-generation/draft-sources.service';

// Regular expression for getting the verse from a segment ref
// Some projects will have the right to left marker in the segment attribute which we need to account for
Expand Down Expand Up @@ -190,7 +191,7 @@ export function checkAppAccess(
}
}

export function projectLabel(project: SelectableProject | undefined): string {
export function projectLabel(project: SelectableProject | DraftSource | undefined): string {
if (project == null || (!project.shortName && !project.name)) {
return '';
}
Expand Down Expand Up @@ -265,6 +266,11 @@ export function getUnsupportedTags(deltaOp: DeltaOperation): string[] {
return [...invalidTags];
}

export function booksFromScriptureRange(scriptureRange: string): number[] {
if (scriptureRange === '') return [];
return scriptureRange.split(';').map(book => Canon.bookIdToNumber(book));
}

export class XmlUtils {
/** Encode text to be valid xml text node. Escape reserved xml characters such as & and < >. */
static encodeForXml(text: string): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,16 @@ <h4 class="explanation">
</button>
</div>
</mat-step>
<mat-step [completed]="isTrainingOptional || userSelectedTrainingBooks.length > 0">
<mat-step [completed]="isTrainingOptional || trainingSourceBooksSelected">
<ng-template matStepLabel>
{{ t("choose_books_for_training_label") }}
</ng-template>
<h1 class="mat-headline-4">{{ t("choose_books_for_training") }}</h1>
<h2>{{ t("translated_books") }}</h2>
<app-book-multi-select
[availableBooks]="availableTrainingBooks"
[selectedBooks]="initialSelectedTrainingBooks"
[projectName]="targetProjectName"
(bookSelect)="onTrainingBookSelect($event)"
data-test-id="draft-stepper-training-books"
></app-book-multi-select>
Expand Down Expand Up @@ -115,10 +117,34 @@ <h4 class="explanation">
</div>
</app-notice>
}
@if (unusableTranslateTargetBooks.length) {
<app-notice>
<transloco key="draft_generation_steps.unusable_target_books"></transloco>
</app-notice>
<h2>{{ t("reference_books") }}</h2>
<p class="reference-project-label">{{ trainingSourceProjectName }}</p>
@if (selectableSourceTrainingBooks.length === 0) {
<app-notice mode="basic" type="light" class="books-appear-notice">{{
t("training_books_will_appear")
}}</app-notice>
} @else {
<app-book-multi-select
[availableBooks]="selectableSourceTrainingBooks"
[selectedBooks]="userSelectedSourceTrainingBooks"
[basicMode]="true"
(bookSelect)="onSourceTrainingBookSelect($event)"
></app-book-multi-select>
}
@if (trainingAdditionalSourceProjectName?.length > 0) {
<p class="reference-project-label">{{ trainingAdditionalSourceProjectName }}</p>
@if (selectableAdditionalSourceTrainingBooks.length === 0) {
<app-notice mode="basic" type="light" class="books-appear-notice">{{
t("training_books_will_appear")
}}</app-notice>
} @else {
<app-book-multi-select
[availableBooks]="selectableAdditionalSourceTrainingBooks"
[selectedBooks]="userSelectedAdditionalSourceTrainingBooks"
[basicMode]="true"
(bookSelect)="onAdditionalSourceTrainingBookSelect($event)"
></app-book-multi-select>
}
}
@if (showBookSelectionError) {
<app-notice type="error">
Expand Down
Loading

0 comments on commit 1a603bd

Please sign in to comment.