diff --git a/src/RealtimeServer/scriptureforge/models/translate-config.ts b/src/RealtimeServer/scriptureforge/models/translate-config.ts index b26c1435b7..45247a0095 100644 --- a/src/RealtimeServer/scriptureforge/models/translate-config.ts +++ b/src/RealtimeServer/scriptureforge/models/translate-config.ts @@ -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; @@ -38,6 +46,7 @@ export interface DraftConfig { lastSelectedTrainingBooks: number[]; lastSelectedTrainingDataFiles: string[]; lastSelectedTrainingScriptureRange?: string; + lastSelectedTrainingScriptureRanges?: ProjectScriptureRange[]; lastSelectedTranslationBooks: number[]; lastSelectedTranslationScriptureRange?: string; servalConfig?: string; diff --git a/src/RealtimeServer/scriptureforge/services/sf-project-migrations.spec.ts b/src/RealtimeServer/scriptureforge/services/sf-project-migrations.spec.ts index 0a5e326834..58f030d50a 100644 --- a/src/RealtimeServer/scriptureforge/services/sf-project-migrations.spec.ts +++ b/src/RealtimeServer/scriptureforge/services/sf-project-migrations.spec.ts @@ -220,36 +220,36 @@ describe('SFProjectMigrations', () => { expect(projectDoc.data.translateConfig.preTranslate).toBe(false); }); }); -}); -describe('version 11', () => { - it('adds biblical terms properties', async () => { - const env = new TestEnvironment(10); - const conn = env.server.connect(); - await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', {}); - let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01'); - expect(projectDoc.data.biblicalTermsConfig).not.toBeDefined(); + describe('version 11', () => { + it('adds biblical terms properties', async () => { + const env = new TestEnvironment(10); + const conn = env.server.connect(); + await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', {}); + let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01'); + expect(projectDoc.data.biblicalTermsConfig).not.toBeDefined(); - await env.server.migrateIfNecessary(); + await env.server.migrateIfNecessary(); - projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01'); - expect(projectDoc.data.biblicalTermsConfig.biblicalTermsEnabled).toBe(false); - expect(projectDoc.data.biblicalTermsConfig.hasRenderings).toBe(false); + projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01'); + expect(projectDoc.data.biblicalTermsConfig.biblicalTermsEnabled).toBe(false); + expect(projectDoc.data.biblicalTermsConfig.hasRenderings).toBe(false); + }); }); -}); -describe('version 12', () => { - it('adds draftConfig to translateConfig', async () => { - const env = new TestEnvironment(11); - const conn = env.server.connect(); - await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', { translateConfig: {} }); - let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01'); - expect(projectDoc.data.translateConfig.draftConfig).not.toBeDefined(); + describe('version 12', () => { + it('adds draftConfig to translateConfig', async () => { + const env = new TestEnvironment(11); + const conn = env.server.connect(); + await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', { translateConfig: {} }); + let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01'); + expect(projectDoc.data.translateConfig.draftConfig).not.toBeDefined(); - await env.server.migrateIfNecessary(); + await env.server.migrateIfNecessary(); - projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01'); - expect(projectDoc.data.translateConfig.draftConfig).toBeDefined(); + projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01'); + expect(projectDoc.data.translateConfig.draftConfig).toBeDefined(); + }); }); describe('version 13', () => { @@ -455,6 +455,28 @@ describe('version 12', () => { expect(projectDoc.data.translateConfig.draftConfig.additionalTrainingSourceEnabled).toBe(false); }); }); + + describe('version 21', () => { + it('copies selected training and translation books to scripture ranges', async () => { + const env = new TestEnvironment(20); + 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 { diff --git a/src/RealtimeServer/scriptureforge/services/sf-project-migrations.ts b/src/RealtimeServer/scriptureforge/services/sf-project-migrations.ts index 00f0b502a2..63971c11d5 100644 --- a/src/RealtimeServer/scriptureforge/services/sf-project-migrations.ts +++ b/src/RealtimeServer/scriptureforge/services/sf-project-migrations.ts @@ -1,3 +1,4 @@ +import { Canon } from '@sillsdev/scripture'; import { Doc, Op } from 'sharedb/lib/client'; import { DocMigration, MigrationConstructor } from '../../common/migration'; import { submitMigrationOp } from '../../common/realtime-server'; @@ -343,6 +344,38 @@ class SFProjectMigration20 extends DocMigration { } } +class SFProjectMigration21 extends DocMigration { + static readonly VERSION = 21; + + async migrateDoc(doc: Doc): Promise { + 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(SFProjectMigration20.VERSION, doc, ops); + } +} + export const SF_PROJECT_MIGRATIONS: MigrationConstructor[] = [ SFProjectMigration1, SFProjectMigration2, @@ -363,5 +396,6 @@ export const SF_PROJECT_MIGRATIONS: MigrationConstructor[] = [ SFProjectMigration17, SFProjectMigration18, SFProjectMigration19, - SFProjectMigration20 + SFProjectMigration20, + SFProjectMigration21 ]; diff --git a/src/RealtimeServer/scriptureforge/services/sf-project-service.ts b/src/RealtimeServer/scriptureforge/services/sf-project-service.ts index 766c721dc2..a11164bff0 100644 --- a/src/RealtimeServer/scriptureforge/services/sf-project-service.ts +++ b/src/RealtimeServer/scriptureforge/services/sf-project-service.ts @@ -268,6 +268,21 @@ export class SFProjectService extends ProjectService { lastSelectedTrainingScriptureRange: { bsonType: 'string' }, + lastSelectedTrainingScriptureRanges: { + bsonType: 'array', + items: { + bsonType: 'object', + properties: { + projectId: { + bsonType: 'string' + }, + scriptureRange: { + bsonType: 'string' + } + }, + additionalProperties: false + } + }, lastSelectedTranslationBooks: { bsonType: 'array', items: { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/serval-project.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/serval-project.component.spec.ts index 37d026bacf..2756fd2f28 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/serval-project.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/serval-project.component.spec.ts @@ -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: { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/serval-project.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/serval-project.component.ts index 2d08d58c1a..3030cdab7b 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/serval-project.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/serval-project.component.ts @@ -16,7 +16,7 @@ import { SFProjectService } from '../core/sf-project.service'; import { BuildDto } from '../machine-api/build-dto'; 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'; @@ -142,13 +142,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); diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.html index 170a0b905f..56c42c2bb7 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.html @@ -1,6 +1,9 @@ - @if (availableBooks.length > 0 && !readonly) { -
+ @if (availableBooks.length > 0 && !readonly && !basicMode) { +
+ @if (projectName != null) { + {{ projectName }} + } - - {{ "canon.book_names." + book.bookId | transloco }} -
-
+ @if (!basicMode) { + + {{ "canon.book_names." + book.bookId | transloco }} +
+
+ } @else { + + {{ "canon.book_names." + book.bookId | transloco }} + + } }
diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.scss b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.scss index 278c40ce8d..e5f3d3782b 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.scss +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.scss @@ -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; @@ -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; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.spec.ts index 04f65b4fb6..44afbf21fa 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.spec.ts @@ -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 }, @@ -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(); + }); }); diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.ts index 3b16bdd403..b15021de9f 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.ts @@ -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(); protected loaded = false; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/utils.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/utils.ts index e497431fb8..b35bb01433 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/shared/utils.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/shared/utils.ts @@ -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 @@ -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 ''; } @@ -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 { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation-steps/draft-generation-steps.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation-steps/draft-generation-steps.component.html index 8ee595074e..679fc37f38 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation-steps/draft-generation-steps.component.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation-steps/draft-generation-steps.component.html @@ -75,14 +75,16 @@

- + {{ t("choose_books_for_training_label") }}

{{ t("choose_books_for_training") }}

+

{{ t("translated_books") }}

@@ -115,10 +117,34 @@

} - @if (unusableTranslateTargetBooks.length) { - - - +

{{ t("reference_books") }}

+

{{ trainingSourceProjectName }}

+ @if (selectableSourceTrainingBooks.length === 0) { + {{ + t("training_books_will_appear") + }} + } @else { + + } + @if (trainingAdditionalSourceProjectName?.length > 0) { +

{{ trainingAdditionalSourceProjectName }}

+ @if (selectableAdditionalSourceTrainingBooks.length === 0) { + {{ + t("training_books_will_appear") + }} + } @else { + + } } @if (showBookSelectionError) { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation-steps/draft-generation-steps.component.scss b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation-steps/draft-generation-steps.component.scss index ba7bf302d8..c24213a312 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation-steps/draft-generation-steps.component.scss +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation-steps/draft-generation-steps.component.scss @@ -18,6 +18,10 @@ h1 { margin: 12px 0; } +h2 { + font-weight: 500; +} + // Prevent font increase when selecting a step .mat-stepper-horizontal { --mat-stepper-header-selected-state-label-text-size: var(--mat-stepper-header-label-text-size); @@ -97,6 +101,10 @@ app-notice { } } +.reference-project-label { + font-weight: 500; +} + .loading { display: flex; margin: 1em; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation-steps/draft-generation-steps.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation-steps/draft-generation-steps.component.spec.ts index 4c81e5eb13..025ab30af1 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation-steps/draft-generation-steps.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation-steps/draft-generation-steps.component.spec.ts @@ -71,9 +71,17 @@ describe('DraftGenerationStepsComponent', () => { }) } as SFProjectProfileDoc; + const mockAdditionalTrainingSourceProjectDoc = { + data: createTestProjectProfile({ + texts: [{ bookNum: 2 }, { bookNum: 3 }, { bookNum: 4 }, { bookNum: 5 }, { bookNum: 8 }, { bookNum: 100 }] + }) + } as SFProjectProfileDoc; + const mockUserDoc = { data: createTestUser({ - sites: { [environment.siteId]: { projects: ['alternateTrainingProject', 'sourceProject', 'test'] } } + sites: { + [environment.siteId]: { projects: ['alternateTrainingProject', 'sourceProject', 'test', 'sourceProject2'] } + } }) } as UserDoc; @@ -134,6 +142,7 @@ describe('DraftGenerationStepsComponent', () => { when(mockProjectService.getProfile('alternateTrainingProject')).thenResolve( mockAlternateTrainingSourceProjectDoc ); + when(mockFeatureFlagService.allowFastTraining).thenReturn(createTestFeatureFlag(false)); when(mockTrainingDataService.queryTrainingDataAsync(anything())).thenResolve(instance(mockTrainingDataQuery)); when(mockTrainingDataQuery.docs).thenReturn([]); @@ -160,6 +169,55 @@ describe('DraftGenerationStepsComponent', () => { expect(component.unusableTranslateTargetBooks).toEqual([4, 5]); expect(component.unusableTrainingTargetBooks).toEqual([4, 5, 8]); })); + + it('should allow selecting books from the alternate training source project', () => { + const trainingBooks = [3]; + const trainingDataFiles: string[] = []; + const translationBooks = [2]; + + component.userSelectedTrainingBooks = trainingBooks; + component.userSelectedTranslateBooks = translationBooks; + component.selectedTrainingDataIds = trainingDataFiles; + component['draftSourceProjectIds'] = { + draftingSourceId: 'sourceProject', + trainingSourceId: 'sourceProject', + trainingAlternateSourceId: 'alternateTrainingProject' + }; + component.onStepChange(); + expect(component.availableTrainingBooks).toEqual(trainingBooks); + expect(component.selectableSourceTrainingBooks).toEqual(trainingBooks); + expect(component.userSelectedSourceTrainingBooks).toEqual(trainingBooks); + expect(fixture.nativeElement.querySelector('.books-appear-notice')).toBeNull(); + + component.onSourceTrainingBookSelect([]); + fixture.detectChanges(); + expect(component.selectableSourceTrainingBooks).toEqual(trainingBooks); + expect(component.userSelectedSourceTrainingBooks).toEqual([]); + expect(fixture.nativeElement.querySelector('.books-appear-notice')).toBeNull(); + }); + + it('does not allow selecting not selectable source training books', () => { + const trainingBooks = [3]; + const trainingDataFiles: string[] = []; + const translationBooks = [2]; + + component.userSelectedTrainingBooks = trainingBooks; + component.userSelectedTranslateBooks = translationBooks; + component.selectedTrainingDataIds = trainingDataFiles; + component['draftSourceProjectIds'] = { + draftingSourceId: 'sourceProject', + trainingSourceId: 'sourceProject', + trainingAlternateSourceId: 'alternateTrainingProject' + }; + component.onStepChange(); + expect(component.availableTrainingBooks).toEqual(trainingBooks); + expect(component.selectableSourceTrainingBooks).toEqual(trainingBooks); + expect(component.userSelectedSourceTrainingBooks).toEqual(trainingBooks); + + component.onSourceTrainingBookSelect([2, 3]); + fixture.detectChanges(); + expect(component.userSelectedSourceTrainingBooks).toEqual(trainingBooks); + }); }); describe('NO alternate training source project', () => { @@ -218,6 +276,8 @@ describe('DraftGenerationStepsComponent', () => { it('should select no books initially', () => { expect(component.initialSelectedTrainingBooks).toEqual([]); expect(component.userSelectedTrainingBooks).toEqual([]); + expect(component.userSelectedSourceTrainingBooks).toEqual([]); + expect(component.userSelectedAdditionalSourceTrainingBooks).toEqual([]); expect(component.initialSelectedTranslateBooks).toEqual([]); expect(component.userSelectedTranslateBooks).toEqual([]); }); @@ -230,6 +290,8 @@ describe('DraftGenerationStepsComponent', () => { component.userSelectedTrainingBooks = trainingBooks; component.userSelectedTranslateBooks = translationBooks; component.selectedTrainingDataIds = trainingDataFiles; + component.userSelectedSourceTrainingBooks = trainingBooks; + component['draftSourceProjectIds'] = { draftingSourceId: 'sourceProject', trainingSourceId: 'sourceProject' }; spyOn(component.done, 'emit'); expect(component.isStepsCompleted).toBe(false); @@ -246,11 +308,9 @@ describe('DraftGenerationStepsComponent', () => { fixture.detectChanges(); expect(component.done.emit).toHaveBeenCalledWith({ - trainingBooks: trainingBooks.filter(book => !translationBooks.includes(book)), trainingDataFiles, - trainingScriptureRanges: [], - translationBooks, - translationScriptureRanges: [], + trainingScriptureRanges: [{ projectId: 'sourceProject', scriptureRange: 'LEV' }], + translationScriptureRange: 'GEN;EXO', fastTraining: false } as DraftGenerationStepsResult); expect(component.isStepsCompleted).toBe(true); @@ -317,6 +377,143 @@ describe('DraftGenerationStepsComponent', () => { })); }); + describe('additional training source project', () => { + beforeEach(fakeAsync(() => { + const mockTargetProjectDoc = { + data: createTestProjectProfile({ + texts: [{ bookNum: 1 }, { bookNum: 2 }, { bookNum: 3 }, { bookNum: 6 }, { bookNum: 7 }], + translateConfig: { + source: { projectRef: 'sourceProject' }, + draftConfig: { + additionalTrainingSourceEnabled: true, + additionalTrainingSource: { projectRef: 'sourceProject2' } + } + } + }) + } as SFProjectProfileDoc; + when(mockActivatedProjectService.projectDoc).thenReturn(mockTargetProjectDoc); + const targetProjectDoc$ = new BehaviorSubject(mockTargetProjectDoc); + when(mockActivatedProjectService.projectDoc$).thenReturn(targetProjectDoc$); + when(mockUserService.getCurrentUser()).thenResolve(mockUserDoc); + when(mockFeatureFlagService.allowFastTraining).thenReturn(createTestFeatureFlag(false)); + when(mockProjectService.getProfile('sourceProject')).thenResolve(mockSourceNonNllbProjectDoc); + when(mockProjectService.getProfile('sourceProject2')).thenResolve(mockAdditionalTrainingSourceProjectDoc); + when(mockNllbLanguageService.isNllbLanguageAsync(anything())).thenResolve(true); + when(mockNllbLanguageService.isNllbLanguageAsync('xyz')).thenResolve(false); + when(mockTrainingDataService.queryTrainingDataAsync(anything())).thenResolve(instance(mockTrainingDataQuery)); + when(mockTrainingDataQuery.docs).thenReturn([]); + + fixture = TestBed.createComponent(DraftGenerationStepsComponent); + component = fixture.componentInstance; + tick(); + fixture.detectChanges(); + })); + + it('should show and hide selectable training source books when training books selected', () => { + const trainingBooks = [3]; + const trainingDataFiles: string[] = []; + const translationBooks = [1, 2]; + + component.userSelectedTrainingBooks = []; + component.userSelectedTranslateBooks = translationBooks; + component.selectedTrainingDataIds = trainingDataFiles; + component.userSelectedSourceTrainingBooks = []; + component.userSelectedAdditionalSourceTrainingBooks = []; + component['availableAdditionalTrainingBooks'] = trainingBooks; + component['draftSourceProjectIds'] = { + draftingSourceId: 'sourceProject', + trainingSourceId: 'sourceProject', + trainingAdditionalSourceId: 'sourceProject2' + }; + component.onStepChange(); + fixture.detectChanges(); + expect(component.availableTrainingBooks).toEqual(trainingBooks); + expect(component.selectableSourceTrainingBooks).toEqual([]); + expect(component.selectableAdditionalSourceTrainingBooks).toEqual([]); + expect(fixture.nativeElement.querySelector('.books-appear-notice')).not.toBeNull(); + + // select a training book + component.onTrainingBookSelect(trainingBooks); + fixture.detectChanges(); + expect(component.selectableSourceTrainingBooks).toEqual(trainingBooks); + expect(component.selectableAdditionalSourceTrainingBooks).toEqual(trainingBooks); + expect(component.userSelectedSourceTrainingBooks).toEqual(trainingBooks); + expect(component.userSelectedAdditionalSourceTrainingBooks).toEqual(trainingBooks); + expect(fixture.nativeElement.querySelector('.books-appear-notice')).toBeNull(); + + // deselect all training books + component.onTrainingBookSelect([]); + fixture.detectChanges(); + expect(component.selectableSourceTrainingBooks).toEqual([]); + expect(component.selectableAdditionalSourceTrainingBooks).toEqual([]); + expect(component.userSelectedSourceTrainingBooks).toEqual([]); + expect(component.userSelectedAdditionalSourceTrainingBooks).toEqual([]); + expect(fixture.nativeElement.querySelector('.books-appear-notice')).not.toBeNull(); + }); + + it('should correctly emit the selected books when done', () => { + const trainingBooks = [3]; + const trainingDataFiles: string[] = []; + const translationBooks = [1, 2]; + + component.userSelectedTrainingBooks = trainingBooks; + component.userSelectedTranslateBooks = translationBooks; + component.selectedTrainingDataIds = trainingDataFiles; + component.userSelectedSourceTrainingBooks = trainingBooks; + component.userSelectedAdditionalSourceTrainingBooks = trainingBooks; + component['draftSourceProjectIds'] = { + draftingSourceId: 'sourceProject', + trainingSourceId: 'sourceProject', + trainingAdditionalSourceId: 'sourceProject2' + }; + + spyOn(component.done, 'emit'); + expect(component.isStepsCompleted).toBe(false); + // Advance to the next step when at last step should emit books result + fixture.detectChanges(); + component.tryAdvanceStep(); + fixture.detectChanges(); + component.tryAdvanceStep(); + fixture.detectChanges(); + + expect(component.done.emit).toHaveBeenCalledWith({ + trainingDataFiles, + trainingScriptureRanges: [ + { projectId: 'sourceProject', scriptureRange: 'LEV' }, + { projectId: 'sourceProject2', scriptureRange: 'LEV' } + ], + translationScriptureRange: 'GEN;EXO', + fastTraining: false + } as DraftGenerationStepsResult); + expect(component.isStepsCompleted).toBe(true); + }); + + it('does not allow selecting not selectable additional source training books', () => { + const trainingBooks = [3]; + const trainingDataFiles: string[] = []; + const translationBooks = [1, 2]; + + component.userSelectedTrainingBooks = trainingBooks; + component.userSelectedTranslateBooks = translationBooks; + component.selectedTrainingDataIds = trainingDataFiles; + component['draftSourceProjectIds'] = { + draftingSourceId: 'sourceProject', + trainingSourceId: 'sourceProject', + trainingAdditionalSourceId: 'sourceProject2' + }; + component.onStepChange(); + expect(component.availableTrainingBooks).toEqual(trainingBooks); + expect(component.selectableSourceTrainingBooks).toEqual(trainingBooks); + expect(component.userSelectedSourceTrainingBooks).toEqual(trainingBooks); + expect(component.selectableAdditionalSourceTrainingBooks).toEqual(trainingBooks); + expect(component.userSelectedAdditionalSourceTrainingBooks).toEqual(trainingBooks); + + component.onAdditionalSourceTrainingBookSelect([2, 3]); + fixture.detectChanges(); + expect(component.userSelectedAdditionalSourceTrainingBooks).toEqual(trainingBooks); + }); + }); + describe('allow fast training feature flag is enabled', () => { beforeEach(fakeAsync(() => { when(mockActivatedProjectService.projectDoc).thenReturn(mockTargetProjectDoc); @@ -340,6 +537,8 @@ describe('DraftGenerationStepsComponent', () => { component.userSelectedTrainingBooks = trainingBooks; component.userSelectedTranslateBooks = translationBooks; component.selectedTrainingDataIds = trainingDataFiles; + component.userSelectedSourceTrainingBooks = trainingBooks; + component['draftSourceProjectIds'] = { draftingSourceId: 'sourceProject', trainingSourceId: 'sourceProject' }; spyOn(component.done, 'emit'); @@ -361,11 +560,9 @@ describe('DraftGenerationStepsComponent', () => { fixture.detectChanges(); expect(component.done.emit).toHaveBeenCalledWith({ - trainingBooks, trainingDataFiles, - trainingScriptureRanges: [], - translationBooks, - translationScriptureRanges: [], + trainingScriptureRanges: [{ projectId: 'sourceProject', scriptureRange: 'GEN;EXO' }], + translationScriptureRange: 'LEV;NUM', fastTraining: true } as DraftGenerationStepsResult); expect(generateDraftButton['disabled']).toBe(true); @@ -379,9 +576,9 @@ describe('DraftGenerationStepsComponent', () => { translateConfig: { source: { projectRef: 'test' }, draftConfig: { - lastSelectedTrainingBooks: [2, 3, 4], lastSelectedTrainingDataFiles: [], - lastSelectedTranslationBooks: [2, 3, 4] + lastSelectedTranslationScriptureRange: 'GEN;EXO', + lastSelectedTrainingScriptureRanges: [{ projectId: 'test', scriptureRange: 'LEV' }] } } }) @@ -400,9 +597,9 @@ describe('DraftGenerationStepsComponent', () => { tick(); })); - it('should restore previously selected books', () => { - expect(component.initialSelectedTrainingBooks).toEqual([2, 3]); - expect(component.initialSelectedTranslateBooks).toEqual([2, 3]); + it('should restore previously selected ranges', () => { + expect(component.initialSelectedTrainingBooks).toEqual([3]); + expect(component.initialSelectedTranslateBooks).toEqual([1, 2]); }); }); diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation-steps/draft-generation-steps.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation-steps/draft-generation-steps.component.ts index 1a268fd14a..5d4204dc45 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation-steps/draft-generation-steps.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation-steps/draft-generation-steps.component.ts @@ -4,6 +4,7 @@ import { TranslocoModule } from '@ngneat/transloco'; import { Canon } from '@sillsdev/scripture'; import { TranslocoMarkupModule } from 'ngx-transloco-markup'; import { TrainingData } from 'realtime-server/lib/esm/scriptureforge/models/training-data'; +import { ProjectScriptureRange } from 'realtime-server/lib/esm/scriptureforge/models/translate-config'; import { Subscription, merge } from 'rxjs'; import { filter, tap } from 'rxjs/operators'; import { ActivatedProjectService } from 'xforge-common/activated-project.service'; @@ -16,22 +17,20 @@ import { filterNullish } from 'xforge-common/util/rxjs-util'; import { TrainingDataDoc } from '../../../core/models/training-data-doc'; import { BookMultiSelectComponent } from '../../../shared/book-multi-select/book-multi-select.component'; import { SharedModule } from '../../../shared/shared.module'; +import { booksFromScriptureRange, projectLabel } from '../../../shared/utils'; import { NllbLanguageService } from '../../nllb-language.service'; import { ConfirmSourcesComponent } from '../confirm-sources/confirm-sources.component'; -import { ProjectScriptureRange } from '../draft-generation'; -import { DraftSource, DraftSourcesService } from '../draft-sources.service'; +import { DraftSource, DraftSourceIds, DraftSourcesService } from '../draft-sources.service'; import { TrainingDataMultiSelectComponent } from '../training-data/training-data-multi-select.component'; import { TrainingDataUploadDialogComponent } from '../training-data/training-data-upload-dialog.component'; import { TrainingDataService } from '../training-data/training-data.service'; export interface DraftGenerationStepsResult { - trainingBooks: number[]; trainingDataFiles: string[]; trainingScriptureRange?: string; trainingScriptureRanges: ProjectScriptureRange[]; - translationBooks: number[]; translationScriptureRange?: string; - translationScriptureRanges: ProjectScriptureRange[]; + translationScriptureRanges?: ProjectScriptureRange[]; fastTraining: boolean; } @@ -58,6 +57,8 @@ export class DraftGenerationStepsComponent extends SubscriptionDisposable implem availableTranslateBooks?: number[] = undefined; availableTrainingBooks: number[] = []; + selectableSourceTrainingBooks: number[] = []; + selectableAdditionalSourceTrainingBooks: number[] = []; availableTrainingData: Readonly[] = []; // Unusable books do not exist in the target or corresponding drafting/training source project @@ -70,14 +71,14 @@ export class DraftGenerationStepsComponent extends SubscriptionDisposable implem initialSelectedTranslateBooks: number[] = []; userSelectedTrainingBooks: number[] = []; userSelectedTranslateBooks: number[] = []; + userSelectedSourceTrainingBooks: number[] = []; + userSelectedAdditionalSourceTrainingBooks: number[] = []; selectedTrainingDataIds: string[] = []; - // When translate books are selected, they will be filtered out from this list - initialAvailableTrainingBooks: number[] = []; - draftingSourceProjectName?: string; trainingSourceProjectName?: string; + trainingAdditionalSourceProjectName?: string; targetProjectName?: string; showBookSelectionError = false; @@ -93,6 +94,10 @@ export class DraftGenerationStepsComponent extends SubscriptionDisposable implem protected languagesVerified = false; protected nextClickedOnLanguageVerification = false; + // When translate books are selected, they will be filtered out from this list + private initialAvailableTrainingBooks: number[] = []; + private availableAdditionalTrainingBooks: number[] = []; + private draftSourceProjectIds?: DraftSourceIds; private trainingDataQuery?: RealtimeQuery; private trainingDataSub?: Subscription; @@ -110,13 +115,25 @@ export class DraftGenerationStepsComponent extends SubscriptionDisposable implem ngOnInit(): void { this.subscribe( this.draftSourcesService.getDraftProjectSources().pipe( - filter(({ target, source, alternateSource, alternateTrainingSource }) => { - this.setProjectDisplayNames(target, alternateSource ?? source, alternateTrainingSource); + filter(({ target, source, alternateSource, alternateTrainingSource, additionalTrainingSource }) => { + this.setProjectDisplayNames( + target, + alternateSource ?? source, + alternateTrainingSource, + additionalTrainingSource + ); return target != null && source != null; }) ), // Build book lists - async ({ target, source, alternateSource, alternateTrainingSource }) => { + async ({ + target, + source, + alternateSource, + alternateTrainingSource, + additionalTrainingSource, + draftSourceIds + }) => { // The null values will have been filtered above target = target!; // Use the alternate source if specified, otherwise use the source @@ -128,21 +145,20 @@ export class DraftGenerationStepsComponent extends SubscriptionDisposable implem (await this.nllbLanguageService.isNllbLanguageAsync(draftingSource.writingSystem.tag)); const draftingSourceBooks = new Set(); - let trainingSourceBooks = new Set(); - for (const text of draftingSource.texts) { draftingSourceBooks.add(text.bookNum); } - if (alternateTrainingSource != null) { - for (const text of alternateTrainingSource.texts) { - trainingSourceBooks.add(text.bookNum); - } - } else { - // If no training source project, use drafting source project books - trainingSourceBooks = draftingSourceBooks; - } + let trainingSourceBooks: Set = + alternateTrainingSource != null + ? new Set(alternateTrainingSource.texts.map(t => t.bookNum)) + : draftingSourceBooks; + let additionalTrainingSourceBooks: Set | undefined = + additionalTrainingSource != null + ? new Set(additionalTrainingSource?.texts.map(t => t.bookNum)) + : undefined; + this.draftSourceProjectIds = draftSourceIds; this.availableTranslateBooks = []; // If book exists in both target and source, add to available books. @@ -171,6 +187,9 @@ export class DraftGenerationStepsComponent extends SubscriptionDisposable implem } else { this.unusableTrainingSourceBooks.push(bookNum); } + if (additionalTrainingSourceBooks != null && additionalTrainingSourceBooks.has(bookNum)) { + this.availableAdditionalTrainingBooks.push(bookNum); + } } // Store initially available training books that will be filtered to remove user selected translate books @@ -197,7 +216,6 @@ export class DraftGenerationStepsComponent extends SubscriptionDisposable implem // Query for all training data files in the project this.trainingDataQuery?.dispose(); this.trainingDataQuery = await this.trainingDataService.queryTrainingDataAsync(projectDoc.id); - let projectChanged: boolean = true; // Subscribe to this query, and show these @@ -228,7 +246,27 @@ export class DraftGenerationStepsComponent extends SubscriptionDisposable implem } onTrainingBookSelect(selectedBooks: number[]): void { - this.userSelectedTrainingBooks = selectedBooks; + const newBookSelections: number[] = selectedBooks.filter(b => !this.userSelectedTrainingBooks.includes(b)); + this.userSelectedTrainingBooks = [...selectedBooks]; + this.selectableSourceTrainingBooks = [...selectedBooks]; + this.selectableAdditionalSourceTrainingBooks = this.availableAdditionalTrainingBooks.filter(b => + selectedBooks.includes(b) + ); + + // remove selected books that are no longer selectable + this.userSelectedSourceTrainingBooks = this.userSelectedSourceTrainingBooks.filter(b => selectedBooks.includes(b)); + this.userSelectedAdditionalSourceTrainingBooks = this.userSelectedAdditionalSourceTrainingBooks.filter(b => + selectedBooks.includes(b) + ); + + // automatically select books that are newly selected as training books + for (const bookNum of newBookSelections) { + this.userSelectedSourceTrainingBooks.push(bookNum); + if (this.selectableAdditionalSourceTrainingBooks.includes(bookNum)) { + this.userSelectedAdditionalSourceTrainingBooks.push(bookNum); + } + } + this.clearErrorMessage(); } @@ -237,6 +275,18 @@ export class DraftGenerationStepsComponent extends SubscriptionDisposable implem this.clearErrorMessage(); } + onSourceTrainingBookSelect(selectedBooks: number[]): void { + this.userSelectedSourceTrainingBooks = this.selectableSourceTrainingBooks.filter(b => selectedBooks.includes(b)); + this.clearErrorMessage(); + } + + onAdditionalSourceTrainingBookSelect(selectedBooks: number[]): void { + this.userSelectedAdditionalSourceTrainingBooks = this.selectableAdditionalSourceTrainingBooks.filter(b => + selectedBooks.includes(b) + ); + this.clearErrorMessage(); + } + onTranslateBookSelect(selectedBooks: number[]): void { this.userSelectedTranslateBooks = selectedBooks; this.clearErrorMessage(); @@ -256,12 +306,26 @@ export class DraftGenerationStepsComponent extends SubscriptionDisposable implem this.stepper.next(); } else { this.isStepsCompleted = true; + const trainingScriptureRange: ProjectScriptureRange = this.convertToScriptureRange( + this.draftSourceProjectIds!.trainingAlternateSourceId ?? this.draftSourceProjectIds!.trainingSourceId, + this.userSelectedSourceTrainingBooks + ); + + const trainingScriptureRanges: ProjectScriptureRange[] = [trainingScriptureRange]; + // Use the additional training range if selected + const useAdditionalTranslateRange: boolean = this.userSelectedAdditionalSourceTrainingBooks.length > 0; + if (useAdditionalTranslateRange) { + trainingScriptureRanges.push( + this.convertToScriptureRange( + this.draftSourceProjectIds!.trainingAdditionalSourceId, + this.userSelectedAdditionalSourceTrainingBooks + ) + ); + } this.done.emit({ - trainingBooks: this.userSelectedTrainingBooks, - trainingScriptureRanges: [], + trainingScriptureRanges, trainingDataFiles: this.selectedTrainingDataIds, - translationBooks: this.userSelectedTranslateBooks, - translationScriptureRanges: [], + translationScriptureRange: this.userSelectedTranslateBooks.map(b => Canon.bookNumberToId(b)).join(';'), fastTraining: this.fastTraining }); } @@ -269,7 +333,7 @@ export class DraftGenerationStepsComponent extends SubscriptionDisposable implem /** * Filter selected translate books from available/selected training books. - * Currently, training books cannot in the set of translate books, + * Currently, training books cannot be in the set of translate books, * but this requirement may be removed in the future. */ updateTrainingBooks(): void { @@ -284,13 +348,25 @@ export class DraftGenerationStepsComponent extends SubscriptionDisposable implem ); this.initialSelectedTrainingBooks = newSelectedTrainingBooks; - this.userSelectedTrainingBooks = newSelectedTrainingBooks; + this.userSelectedTrainingBooks = [...newSelectedTrainingBooks]; + this.selectableSourceTrainingBooks = [...newSelectedTrainingBooks]; + this.userSelectedSourceTrainingBooks = [...newSelectedTrainingBooks]; + this.selectableAdditionalSourceTrainingBooks = this.availableAdditionalTrainingBooks.filter(b => + newSelectedTrainingBooks.includes(b) + ); + this.userSelectedAdditionalSourceTrainingBooks = this.selectableAdditionalSourceTrainingBooks.filter(b => + newSelectedTrainingBooks.includes(b) + ); } bookNames(books: number[]): string { return books.map(bookNum => this.i18n.localizeBook(bookNum)).join(', '); } + private convertToScriptureRange(projectId: string, books: number[]): ProjectScriptureRange { + return { projectId: projectId, scriptureRange: books.map(b => Canon.bookNumberToId(b)).join(';') }; + } + private validateCurrentStep(): boolean { const isValid = this.stepper.selected?.completed!; this.showBookSelectionError = !isValid; @@ -303,30 +379,44 @@ export class DraftGenerationStepsComponent extends SubscriptionDisposable implem private setInitialTranslateBooks(availableBooks: number[]): void { // Get the previously selected translation books from the target project - const previousBooks: Set = new Set( - this.activatedProject.projectDoc?.data?.translateConfig.draftConfig.lastSelectedTranslationBooks ?? [] - ); + const previousTranslationRange: string = + this.activatedProject.projectDoc?.data?.translateConfig.draftConfig.lastSelectedTranslationScriptureRange ?? ''; + const previousBooks: Set = new Set(booksFromScriptureRange(previousTranslationRange)); // The intersection is all of the available books in the source project that match the target's previous books - const intersection = availableBooks.filter(bookNum => previousBooks.has(bookNum)); + const intersection: number[] = availableBooks.filter(bookNum => previousBooks.has(bookNum)); // Set the selected books to the intersection, or if the intersection is empty, do not select any this.initialSelectedTranslateBooks = intersection.length > 0 ? intersection : []; - this.userSelectedTranslateBooks = this.initialSelectedTranslateBooks; + this.userSelectedTranslateBooks = [...this.initialSelectedTranslateBooks]; } private setInitialTrainingBooks(availableBooks: number[]): void { // Get the previously selected training books from the target project - const previousBooks: Set = new Set( - this.activatedProject.projectDoc?.data?.translateConfig.draftConfig.lastSelectedTrainingBooks ?? [] - ); + const trainingSourceId = + this.draftSourceProjectIds?.trainingAlternateSourceId ?? this.draftSourceProjectIds?.trainingSourceId; + let previousTrainingRange: string = + this.activatedProject.projectDoc?.data?.translateConfig.draftConfig.lastSelectedTrainingScriptureRanges?.find( + r => r.projectId === trainingSourceId + )?.scriptureRange ?? ''; + const trainingScriptureRange = + this.activatedProject.projectDoc?.data?.translateConfig.draftConfig.lastSelectedTrainingScriptureRange; + if (previousTrainingRange === '' && trainingScriptureRange != null) { + previousTrainingRange = + this.activatedProject.projectDoc?.data?.translateConfig.draftConfig.lastSelectedTrainingScriptureRange ?? ''; + } + const previousBooks: Set = new Set(booksFromScriptureRange(previousTrainingRange)); // The intersection is all of the available books in the source project that match the target's previous books - const intersection = availableBooks.filter(bookNum => previousBooks.has(bookNum)); + const intersection: number[] = availableBooks.filter(bookNum => previousBooks.has(bookNum)); // Set the selected books to the intersection, or if the intersection is empty, do not select any this.initialSelectedTrainingBooks = intersection.length > 0 ? intersection : []; - this.userSelectedTrainingBooks = this.initialSelectedTrainingBooks; + this.userSelectedTrainingBooks = [...this.initialSelectedTrainingBooks]; + this.userSelectedSourceTrainingBooks = [...this.initialSelectedTrainingBooks]; + this.userSelectedAdditionalSourceTrainingBooks = this.availableAdditionalTrainingBooks.filter(b => + this.initialSelectedTrainingBooks.includes(b) + ); } private setInitialTrainingDataFiles(availableDataFiles: string[]): void { @@ -347,12 +437,14 @@ export class DraftGenerationStepsComponent extends SubscriptionDisposable implem private setProjectDisplayNames( target: DraftSource | undefined, draftingSource: DraftSource | undefined, - trainingSource: DraftSource | undefined + trainingSource: DraftSource | undefined, + additionalTrainingSource: DraftSource | undefined ): void { - this.targetProjectName = target != null ? `${target.shortName} - ${target.name}` : ''; - this.draftingSourceProjectName = - draftingSource != null ? `${draftingSource.shortName} - ${draftingSource.name}` : ''; + this.targetProjectName = target != null ? projectLabel(target) : ''; + this.draftingSourceProjectName = draftingSource != null ? projectLabel(draftingSource) : ''; this.trainingSourceProjectName = - trainingSource != null ? `${trainingSource.shortName} - ${trainingSource.name}` : this.draftingSourceProjectName; + trainingSource != null ? projectLabel(trainingSource) : this.draftingSourceProjectName; + this.trainingAdditionalSourceProjectName = + additionalTrainingSource != null ? projectLabel(additionalTrainingSource) : ''; } } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.component.spec.ts index 9fc12a10f1..3ebaf48c78 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.component.spec.ts @@ -185,7 +185,9 @@ describe('DraftGenerationComponent', () => { }, draftConfig: { lastSelectedTrainingBooks: preTranslate ? [1] : [], - lastSelectedTranslationBooks: preTranslate ? [2] : [] + lastSelectedTranslationBooks: preTranslate ? [2] : [], + lastSelectedTrainingScriptureRange: preTranslate ? 'GEN' : undefined, + lastSelectedTranslationScriptureRange: preTranslate ? 'EXO' : undefined } }, texts: [ @@ -1966,10 +1968,8 @@ describe('DraftGenerationComponent', () => { env.component.currentPage = 'steps'; env.component.startBuild({ - trainingBooks: [], trainingDataFiles: [], trainingScriptureRanges: [], - translationBooks: [], translationScriptureRanges: [], fastTraining: false, projectId: projectId @@ -1978,10 +1978,8 @@ describe('DraftGenerationComponent', () => { expect(env.component.currentPage).toBe('steps'); expect(mockDraftGenerationService.startBuildOrGetActiveBuild).toHaveBeenCalledWith({ projectId: projectId, - trainingBooks: [], trainingDataFiles: [], trainingScriptureRanges: [], - translationBooks: [], translationScriptureRanges: [], fastTraining: false }); @@ -1997,10 +1995,8 @@ describe('DraftGenerationComponent', () => { env.component.cancelDialogRef = instance(mockDialogRef); env.component.startBuild({ - trainingBooks: [], trainingDataFiles: [], trainingScriptureRanges: [], - translationBooks: [], translationScriptureRanges: [], fastTraining: false, projectId: projectId @@ -2008,10 +2004,8 @@ describe('DraftGenerationComponent', () => { env.startedOrActiveBuild$.next({ ...buildDto, state: BuildStates.Queued }); expect(mockDraftGenerationService.startBuildOrGetActiveBuild).toHaveBeenCalledWith({ projectId: projectId, - trainingBooks: [], trainingDataFiles: [], trainingScriptureRanges: [], - translationBooks: [], translationScriptureRanges: [], fastTraining: false }); @@ -2026,10 +2020,8 @@ describe('DraftGenerationComponent', () => { env.component.cancelDialogRef = instance(mockDialogRef); env.component.startBuild({ - trainingBooks: [], trainingDataFiles: [], trainingScriptureRanges: [], - translationBooks: [], translationScriptureRanges: [], fastTraining: false, projectId: projectId @@ -2037,10 +2029,8 @@ describe('DraftGenerationComponent', () => { env.startedOrActiveBuild$.next({ ...buildDto, state: BuildStates.Pending }); expect(mockDraftGenerationService.startBuildOrGetActiveBuild).toHaveBeenCalledWith({ projectId: projectId, - trainingBooks: [], trainingDataFiles: [], trainingScriptureRanges: [], - translationBooks: [], translationScriptureRanges: [], fastTraining: false }); @@ -2055,10 +2045,8 @@ describe('DraftGenerationComponent', () => { env.component.cancelDialogRef = instance(mockDialogRef); env.component.startBuild({ - trainingBooks: [], trainingDataFiles: [], trainingScriptureRanges: [], - translationBooks: [], translationScriptureRanges: [], fastTraining: false, projectId: projectId @@ -2066,10 +2054,8 @@ describe('DraftGenerationComponent', () => { env.startedOrActiveBuild$.next({ ...buildDto, state: BuildStates.Active }); expect(mockDraftGenerationService.startBuildOrGetActiveBuild).toHaveBeenCalledWith({ projectId: projectId, - trainingBooks: [], trainingDataFiles: [], trainingScriptureRanges: [], - translationBooks: [], translationScriptureRanges: [], fastTraining: false }); @@ -2085,10 +2071,8 @@ describe('DraftGenerationComponent', () => { env.component.cancelDialogRef = instance(mockDialogRef); env.component.startBuild({ - trainingBooks: [], trainingDataFiles: [], trainingScriptureRanges: [], - translationBooks: [], translationScriptureRanges: [], fastTraining: false, projectId: projectId @@ -2096,10 +2080,8 @@ describe('DraftGenerationComponent', () => { env.startedOrActiveBuild$.next({ ...buildDto, state: BuildStates.Canceled }); expect(mockDraftGenerationService.startBuildOrGetActiveBuild).toHaveBeenCalledWith({ projectId: projectId, - trainingBooks: [], trainingDataFiles: [], trainingScriptureRanges: [], - translationBooks: [], translationScriptureRanges: [], fastTraining: false }); @@ -2114,10 +2096,8 @@ describe('DraftGenerationComponent', () => { }); env.component.startBuild({ - trainingBooks: [], trainingDataFiles: [], trainingScriptureRanges: [], - translationBooks: [], translationScriptureRanges: [], fastTraining: false, projectId: projectId @@ -2126,10 +2106,8 @@ describe('DraftGenerationComponent', () => { expect(mockDraftGenerationService.startBuildOrGetActiveBuild).toHaveBeenCalledWith({ projectId: projectId, - trainingBooks: [], trainingDataFiles: [], trainingScriptureRanges: [], - translationBooks: [], translationScriptureRanges: [], fastTraining: false }); @@ -2383,7 +2361,7 @@ describe('DraftGenerationComponent', () => { // Update the has draft flag for the project projectDoc.data!.texts[0].chapters[0].hasDraft = true; - projectDoc.data!.translateConfig.draftConfig.lastSelectedTranslationBooks = [1]; + projectDoc.data!.translateConfig.draftConfig.lastSelectedTranslationScriptureRange = 'GEN'; projectSubject.next(projectDoc); buildSubject.next({ ...buildDto, state: BuildStates.Completed }); diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.component.ts index a4d3699e16..e99fbe959f 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.component.ts @@ -442,13 +442,11 @@ export class DraftGenerationComponent extends DataLoadingComponent implements On onPreGenerationStepsComplete(result: DraftGenerationStepsResult): void { this.startBuild({ projectId: this.activatedProject.projectId!, - trainingBooks: result.trainingBooks, trainingDataFiles: result.trainingDataFiles, trainingScriptureRange: result.trainingScriptureRange, trainingScriptureRanges: result.trainingScriptureRanges, - translationBooks: result.translationBooks, translationScriptureRange: result.translationScriptureRange, - translationScriptureRanges: result.trainingScriptureRanges, + translationScriptureRanges: result.translationScriptureRanges, fastTraining: result.fastTraining }); } @@ -570,7 +568,7 @@ export class DraftGenerationComponent extends DataLoadingComponent implements On private hasStartedBuild(projectDoc: SFProjectProfileDoc): boolean { return ( projectDoc.data?.translateConfig.preTranslate === true && - projectDoc.data?.translateConfig.draftConfig.lastSelectedTranslationBooks.length > 0 + projectDoc.data?.translateConfig.draftConfig.lastSelectedTranslationScriptureRange != null ); } } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.service.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.service.spec.ts index 1599ff8542..40ecd6e6f7 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.service.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.service.spec.ts @@ -41,10 +41,8 @@ describe('DraftGenerationService', () => { const projectId = 'testProjectId'; const buildConfig: BuildConfig = { projectId, - trainingBooks: [], trainingDataFiles: [], translationScriptureRanges: [], - translationBooks: [], trainingScriptureRanges: [], fastTraining: false }; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.ts index 8e08feafdb..2d501c20b9 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.ts @@ -1,4 +1,5 @@ import { InjectionToken } from '@angular/core'; +import { ProjectScriptureRange } from 'realtime-server/lib/esm/scriptureforge/models/translate-config'; import { BuildStates } from '../../machine-api/build-states'; /** @@ -6,24 +7,14 @@ import { BuildStates } from '../../machine-api/build-states'; */ export interface BuildConfig { projectId: string; - trainingBooks: number[]; trainingDataFiles: string[]; trainingScriptureRange?: string; trainingScriptureRanges: ProjectScriptureRange[]; - translationBooks: number[]; translationScriptureRange?: string; translationScriptureRanges: ProjectScriptureRange[]; fastTraining: boolean; } -/** - * A per-project scripture range. - */ -export interface ProjectScriptureRange { - projectId: string; - scriptureRange: string; -} - /** * Dictionary of 'segmentRef -> segment text'. */ diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-sources.service.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-sources.service.spec.ts index ae0b6ee85e..154c595739 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-sources.service.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-sources.service.spec.ts @@ -11,7 +11,7 @@ import { UserService } from 'xforge-common/user.service'; import { environment } from '../../../environments/environment'; import { SFProjectProfileDoc } from '../../core/models/sf-project-profile-doc'; import { SFProjectService } from '../../core/sf-project.service'; -import { DraftSources, DraftSourcesService } from './draft-sources.service'; +import { DraftSource, DraftSources, DraftSourcesService } from './draft-sources.service'; describe('DraftSourcesService', () => { let service: DraftSourcesService; @@ -45,7 +45,14 @@ describe('DraftSourcesService', () => { source: undefined, alternateSource: undefined, alternateTrainingSource: undefined, - additionalTrainingSource: undefined + additionalTrainingSource: undefined, + draftSourceIds: { + draftingSourceId: undefined, + draftingAlternateSourceId: undefined, + trainingSourceId: undefined, + trainingAlternateSourceId: undefined, + trainingAdditionalSourceId: undefined + } } as DraftSources); done(); }); @@ -141,6 +148,13 @@ describe('DraftSourcesService', () => { tag: 'en_UK' }, noAccess: true + }, + draftSourceIds: { + draftingSourceId: 'source_project', + draftingAlternateSourceId: 'alternate_source_project', + trainingSourceId: 'source_project', + trainingAlternateSourceId: 'alternate_training_source_project', + trainingAdditionalSourceId: 'additional_training_source_project' } } as DraftSources); done(); @@ -242,7 +256,14 @@ describe('DraftSourcesService', () => { source: sourceProject, alternateSource: alternateSourceProject, alternateTrainingSource: alternateTrainingSourceProject, - additionalTrainingSource: additionalTrainingSourceProject + additionalTrainingSource: additionalTrainingSourceProject, + draftSourceIds: { + draftingSourceId: 'source_project', + draftingAlternateSourceId: 'alternate_source_project', + trainingSourceId: 'source_project', + trainingAlternateSourceId: 'alternate_training_source_project', + trainingAdditionalSourceId: 'additional_training_source_project' + } } as DraftSources); done(); }); @@ -271,13 +292,7 @@ describe('DraftSourcesService', () => { ); service.getDraftProjectSources().subscribe(result => { - expect(result).toEqual({ - target: targetProject, - source: undefined, - alternateSource: undefined, - alternateTrainingSource: undefined, - additionalTrainingSource: undefined - } as DraftSources); + expectTargetOnly(targetProject, result); done(); }); }); @@ -286,7 +301,7 @@ describe('DraftSourcesService', () => { const targetProject = createTestProjectProfile({ translateConfig: { draftConfig: { - alternateSourceEnabled: false + alternateSourceEnabled: true } } }); @@ -297,13 +312,7 @@ describe('DraftSourcesService', () => { ); service.getDraftProjectSources().subscribe(result => { - expect(result).toEqual({ - target: targetProject, - source: undefined, - alternateSource: undefined, - alternateTrainingSource: undefined, - additionalTrainingSource: undefined - } as DraftSources); + expectTargetOnly(targetProject, result); done(); }); }); @@ -331,13 +340,7 @@ describe('DraftSourcesService', () => { ); service.getDraftProjectSources().subscribe(result => { - expect(result).toEqual({ - target: targetProject, - source: undefined, - alternateSource: undefined, - alternateTrainingSource: undefined, - additionalTrainingSource: undefined - } as DraftSources); + expectTargetOnly(targetProject, result); done(); }); }); @@ -346,7 +349,7 @@ describe('DraftSourcesService', () => { const targetProject = createTestProjectProfile({ translateConfig: { draftConfig: { - alternateTrainingSourceEnabled: false + alternateTrainingSourceEnabled: true } } }); @@ -357,13 +360,7 @@ describe('DraftSourcesService', () => { ); service.getDraftProjectSources().subscribe(result => { - expect(result).toEqual({ - target: targetProject, - source: undefined, - alternateSource: undefined, - alternateTrainingSource: undefined, - additionalTrainingSource: undefined - } as DraftSources); + expectTargetOnly(targetProject, result); done(); }); }); @@ -391,13 +388,7 @@ describe('DraftSourcesService', () => { ); service.getDraftProjectSources().subscribe(result => { - expect(result).toEqual({ - target: targetProject, - source: undefined, - alternateSource: undefined, - alternateTrainingSource: undefined, - additionalTrainingSource: undefined - } as DraftSources); + expectTargetOnly(targetProject, result); done(); }); }); @@ -406,7 +397,7 @@ describe('DraftSourcesService', () => { const targetProject = createTestProjectProfile({ translateConfig: { draftConfig: { - additionalTrainingSourceEnabled: false + additionalTrainingSourceEnabled: true } } }); @@ -417,15 +408,26 @@ describe('DraftSourcesService', () => { ); service.getDraftProjectSources().subscribe(result => { - expect(result).toEqual({ - target: targetProject, - source: undefined, - alternateSource: undefined, - alternateTrainingSource: undefined, - additionalTrainingSource: undefined - } as DraftSources); + expectTargetOnly(targetProject, result); done(); }); }); }); + + function expectTargetOnly(targetProject: DraftSource, result: DraftSources): void { + expect(result).toEqual({ + target: targetProject, + source: undefined, + alternateSource: undefined, + alternateTrainingSource: undefined, + additionalTrainingSource: undefined, + draftSourceIds: { + draftingSourceId: undefined, + draftingAlternateSourceId: undefined, + trainingSourceId: undefined, + trainingAlternateSourceId: undefined, + trainingAdditionalSourceId: undefined + } + } as DraftSources); + } }); diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-sources.service.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-sources.service.ts index a208295acf..f99ecf2762 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-sources.service.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-sources.service.ts @@ -23,12 +23,22 @@ export interface DraftSource { interface DraftSourceDoc { data: DraftSource; } + +export interface DraftSourceIds { + trainingSourceId?: string; + trainingAlternateSourceId?: string; + trainingAdditionalSourceId?: string; + draftingSourceId?: string; + draftingAlternateSourceId?: string; +} + export interface DraftSources { target?: Readonly; source?: Readonly; alternateSource?: Readonly; alternateTrainingSource?: Readonly; additionalTrainingSource?: Readonly; + draftSourceIds?: DraftSourceIds; } @Injectable({ @@ -117,7 +127,14 @@ export class DraftSourcesService { source: sourceDoc?.data, alternateSource: alternateSourceDoc?.data, alternateTrainingSource: alternateTrainingSourceDoc?.data, - additionalTrainingSource: additionalTrainingSourceProjectDoc?.data + additionalTrainingSource: additionalTrainingSourceProjectDoc?.data, + draftSourceIds: { + trainingSourceId: sourceProjectId, + trainingAlternateSourceId: alternateTrainingSourceProjectId, + trainingAdditionalSourceId: additionalTrainingSourceProjectId, + draftingSourceId: sourceProjectId, + draftingAlternateSourceId: alternateSourceProjectId + } }; }) ); diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/tabs/editor-tab-menu.service.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/tabs/editor-tab-menu.service.spec.ts index 63b2347977..49afc448e9 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/tabs/editor-tab-menu.service.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/tabs/editor-tab-menu.service.spec.ts @@ -266,7 +266,7 @@ class TestEnvironment { ], translateConfig: { preTranslate: true, - draftConfig: { lastSelectedTranslationBooks: [40], lastSelectedTrainingBooks: [41] } + draftConfig: { lastSelectedTranslationScriptureRange: 'MAT', lastSelectedTrainingScriptureRange: 'MRK' } }, userRoles: TestEnvironment.rolesByUser, biblicalTermsConfig: { biblicalTermsEnabled: true } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/non_checking_en.json b/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/non_checking_en.json index db9e513dfe..1d901296ae 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/non_checking_en.json +++ b/src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/non_checking_en.json @@ -251,8 +251,11 @@ "next": "Next", "no_available_books": "You have no books available for drafting.", "overview": "Overview", + "reference_books": "Reference books", "these_source_books_cannot_be_used_for_training": "The following books cannot be used for training as they are not in the training source text ({{ trainingSourceProjectName }}).", "these_source_books_cannot_be_used_for_translating": "The following books cannot be translated as they are not in the drafting source text ({{ draftingSourceProjectName }}).", + "training_books_will_appear": "Training books will appear as you select books under translated books", + "translated_books": "Translated books", "unusable_target_books": "Can't find the book you're looking for? Be sure the book is created in Paratext, then sync your project." }, "draft_preview_books": {