diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-entry/draft-history-entry.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-entry/draft-history-entry.component.spec.ts index dd035d16c99..d4048443f43 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-entry/draft-history-entry.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-entry/draft-history-entry.component.spec.ts @@ -22,7 +22,7 @@ import { SFProjectService } from '../../../../core/sf-project.service'; import { BuildDto } from '../../../../machine-api/build-dto'; import { BuildStates } from '../../../../machine-api/build-states'; import { DraftGenerationService } from '../../draft-generation.service'; -import { FORMATTING_OPTIONS_SUPPORTED_DATE } from '../../draft-utils'; +import { DraftOptionsService, FORMATTING_OPTIONS_SUPPORTED_DATE } from '../../draft-options.service'; import { TrainingDataService } from '../../training-data/training-data.service'; import { DraftHistoryEntryComponent } from './draft-history-entry.component'; @@ -33,6 +33,7 @@ const mockedUserService = mock(UserService); const mockedTrainingDataService = mock(TrainingDataService); const mockedActivatedProjectService = mock(ActivatedProjectService); const mockedFeatureFlagsService = mock(FeatureFlagService); +const mockedDraftOptionsService = mock(DraftOptionsService); const oneDay = 1000 * 60 * 60 * 24; const dateBeforeFormattingSupported = new Date(FORMATTING_OPTIONS_SUPPORTED_DATE.getTime() - oneDay).toISOString(); @@ -56,7 +57,8 @@ describe('DraftHistoryEntryComponent', () => { { provide: UserService, useMock: mockedUserService }, { provide: TrainingDataService, useMock: mockedTrainingDataService }, { provide: ActivatedProjectService, useMock: mockedActivatedProjectService }, - { provide: FeatureFlagService, useMock: mockedFeatureFlagsService } + { provide: FeatureFlagService, useMock: mockedFeatureFlagsService }, + { provide: DraftOptionsService, useMock: mockedDraftOptionsService } ] })); @@ -81,6 +83,7 @@ describe('DraftHistoryEntryComponent', () => { when(mockedTrainingDataService.queryTrainingDataAsync(anything(), anything())).thenResolve( instance(trainingDataQuery) ); + when(mockedDraftOptionsService.areFormattingOptionsAvailableButUnselected()).thenReturn(true); fixture = TestBed.createComponent(DraftHistoryEntryComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -165,6 +168,8 @@ describe('DraftHistoryEntryComponent', () => { })); it('should show the USFM format option when the project is the latest draft', fakeAsync(() => { + when(mockedDraftOptionsService.areFormattingOptionsAvailableButUnselected()).thenReturn(false); + when(mockedDraftOptionsService.areFormattingOptionsSupportedForBuild(anything())).thenReturn(true); const user = 'user-display-name'; const date = dateAfterFormattingSupported; const trainingBooks = ['EXO']; @@ -236,6 +241,8 @@ describe('DraftHistoryEntryComponent', () => { })); it('should handle builds with additional info referencing a deleted user', fakeAsync(() => { + when(mockedDraftOptionsService.areFormattingOptionsAvailableButUnselected()).thenReturn(false); + when(mockedDraftOptionsService.areFormattingOptionsSupportedForBuild(anything())).thenReturn(true); when(mockedI18nService.formatDate(anything())).thenReturn('formatted-date'); when(mockedI18nService.formatAndLocalizeScriptureRange('GEN')).thenReturn('Genesis'); when(mockedI18nService.formatAndLocalizeScriptureRange('EXO')).thenReturn('Exodus'); @@ -281,7 +288,6 @@ describe('DraftHistoryEntryComponent', () => { expect(component.buildRequestedByUserName).toBeUndefined(); expect(component.buildRequestedAtDate).toBe(''); expect(component.draftIsAvailable).toBe(false); - expect(fixture.nativeElement.querySelector('.format-usfm')).toBeNull(); expect(component.hasDetails).toBe(false); expect(component.entry).toBe(entry); }); @@ -347,6 +353,8 @@ describe('DraftHistoryEntryComponent', () => { }); it('should show set draft format UI', fakeAsync(() => { + when(mockedDraftOptionsService.areFormattingOptionsAvailableButUnselected()).thenReturn(false); + when(mockedDraftOptionsService.areFormattingOptionsSupportedForBuild(anything())).thenReturn(true); const date = dateAfterFormattingSupported; component.entry = { id: 'build01', @@ -413,6 +421,7 @@ describe('DraftHistoryEntryComponent', () => { })); it('should not show the USFM format option for drafts created before the supported date', fakeAsync(() => { + when(mockedDraftOptionsService.areFormattingOptionsSupportedForBuild(anything())).thenReturn(false); const user = 'user-display-name'; const date = dateBeforeFormattingSupported; const trainingBooks = ['EXO']; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-entry/draft-history-entry.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-entry/draft-history-entry.component.ts index 33d83ce8f8b..37ac7bd207b 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-entry/draft-history-entry.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-history-list/draft-history-entry/draft-history-entry.component.ts @@ -18,8 +18,8 @@ import { BuildDto } from '../../../../machine-api/build-dto'; import { BuildStates } from '../../../../machine-api/build-states'; import { RIGHT_TO_LEFT_MARK } from '../../../../shared/utils'; import { DraftDownloadButtonComponent } from '../../draft-download-button/draft-download-button.component'; +import { DraftOptionsService } from '../../draft-options.service'; import { DraftPreviewBooksComponent } from '../../draft-preview-books/draft-preview-books.component'; -import { FORMATTING_OPTIONS_SUPPORTED_DATE } from '../../draft-utils'; import { TrainingDataService } from '../../training-data/training-data.service'; const STATUS_INFO: Record = { @@ -269,13 +269,11 @@ export class DraftHistoryEntryComponent { } get formattingOptionsSelected(): boolean { - return this.activatedProjectService.projectDoc?.data?.translateConfig.draftConfig.usfmConfig != null; + return this.draftOptionsService.areFormattingOptionsSelected(); } get formattingOptionsSupported(): boolean { - return this.featureFlags.usfmFormat.enabled && this.entry?.additionalInfo?.dateFinished != null - ? new Date(this.entry.additionalInfo.dateFinished) > FORMATTING_OPTIONS_SUPPORTED_DATE - : false; + return this.draftOptionsService.areFormattingOptionsSupportedForBuild(this.entry); } @Input() isLatestBuild: boolean = false; @@ -295,6 +293,7 @@ export class DraftHistoryEntryComponent { private readonly trainingDataService: TrainingDataService, private readonly activatedProjectService: ActivatedProjectService, readonly featureFlags: FeatureFlagService, + private readonly draftOptionsService: DraftOptionsService, private readonly destroyRef: DestroyRef ) {} diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-options.service.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-options.service.spec.ts new file mode 100644 index 00000000000..c90ce057a7f --- /dev/null +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-options.service.spec.ts @@ -0,0 +1,130 @@ +import { TestBed } from '@angular/core/testing'; +import { + DraftUsfmConfig, + ParagraphBreakFormat, + QuoteFormat +} from 'realtime-server/lib/esm/scriptureforge/models/translate-config'; +import { instance, mock, when } from 'ts-mockito'; +import { ActivatedProjectService } from 'xforge-common/activated-project.service'; +import { createTestFeatureFlag, FeatureFlagService } from 'xforge-common/feature-flags/feature-flag.service'; +import { SFProjectProfileDoc } from '../../core/models/sf-project-profile-doc'; +import { BuildDto } from '../../machine-api/build-dto'; +import { DraftOptionsService, FORMATTING_OPTIONS_SUPPORTED_DATE } from './draft-options.service'; + +const mockedActivatedProject = mock(ActivatedProjectService); +const mockedFeatureFlagService = mock(FeatureFlagService); + +describe('DraftOptionsService', () => { + let service: DraftOptionsService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + DraftOptionsService, + { provide: ActivatedProjectService, useValue: instance(mockedActivatedProject) }, + { provide: FeatureFlagService, useValue: instance(mockedFeatureFlagService) } + ] + }); + when(mockedFeatureFlagService.usfmFormat).thenReturn(createTestFeatureFlag(true)); + service = TestBed.inject(DraftOptionsService); + }); + + function buildProjectDoc(usfmConfig: Partial | 'absent'): SFProjectProfileDoc { + const draftConfig: any = {}; + if (usfmConfig === 'absent') { + } else { + draftConfig.usfmConfig = { ...usfmConfig }; + } + const doc = { + data: { + translateConfig: { + draftConfig + } + } + } as unknown as SFProjectProfileDoc; + return doc; + } + + const PROJECT_DOC_BOTH_FORMATS: SFProjectProfileDoc = buildProjectDoc({ + paragraphFormat: ParagraphBreakFormat.BestGuess, + quoteFormat: QuoteFormat.Normalized + }); + const PROJECT_DOC_PARAGRAPH_ONLY: SFProjectProfileDoc = buildProjectDoc({ + paragraphFormat: ParagraphBreakFormat.BestGuess + }); + const PROJECT_DOC_QUOTE_ONLY: SFProjectProfileDoc = buildProjectDoc({ + quoteFormat: QuoteFormat.Normalized + }); + const PROJECT_DOC_EMPTY_USFM: SFProjectProfileDoc = buildProjectDoc({}); + + describe('areFormattingOptionsSelected', () => { + it('returns true when flag enabled and both options set', () => { + when(mockedActivatedProject.projectDoc).thenReturn(PROJECT_DOC_BOTH_FORMATS); + expect(service.areFormattingOptionsSelected()).toBe(true); + }); + + it('returns false when flag enabled and one option missing', () => { + when(mockedActivatedProject.projectDoc).thenReturn(PROJECT_DOC_PARAGRAPH_ONLY); + expect(service.areFormattingOptionsSelected()).toBe(false); + }); + + it('returns false when flag enabled and both options missing', () => { + when(mockedActivatedProject.projectDoc).thenReturn(PROJECT_DOC_EMPTY_USFM); + expect(service.areFormattingOptionsSelected()).toBe(false); + }); + + it('returns false when flag disabled even if both options set', () => { + when(mockedFeatureFlagService.usfmFormat).thenReturn(createTestFeatureFlag(false)); + when(mockedActivatedProject.projectDoc).thenReturn(PROJECT_DOC_BOTH_FORMATS); + expect(service.areFormattingOptionsSelected()).toBe(false); + }); + }); + + describe('areFormattingOptionsAvailableButUnselected', () => { + it('returns true when flag enabled and both options missing', () => { + when(mockedActivatedProject.projectDoc).thenReturn(PROJECT_DOC_EMPTY_USFM); + expect(service.areFormattingOptionsAvailableButUnselected()).toBe(true); + }); + + it('returns true when flag enabled and one option missing', () => { + when(mockedActivatedProject.projectDoc).thenReturn(PROJECT_DOC_QUOTE_ONLY); + expect(service.areFormattingOptionsAvailableButUnselected()).toBe(true); + }); + + it('returns false when flag enabled and both options set', () => { + when(mockedActivatedProject.projectDoc).thenReturn(PROJECT_DOC_BOTH_FORMATS); + expect(service.areFormattingOptionsAvailableButUnselected()).toBe(false); + }); + + it('returns false when flag disabled', () => { + when(mockedFeatureFlagService.usfmFormat).thenReturn(createTestFeatureFlag(false)); + when(mockedActivatedProject.projectDoc).thenReturn(PROJECT_DOC_EMPTY_USFM); + expect(service.areFormattingOptionsAvailableButUnselected()).toBe(false); + }); + }); + + describe('areFormattingOptionsSupportedForBuild', () => { + function buildWith(date: Date | undefined, flagEnabled: boolean = true): BuildDto | undefined { + when(mockedFeatureFlagService.usfmFormat).thenReturn(createTestFeatureFlag(flagEnabled)); + if (date == null) { + return { additionalInfo: {} } as BuildDto; + } + return { additionalInfo: { dateFinished: date.toJSON() } } as BuildDto; + } + + it('returns true when flag enabled and date after supported date', () => { + const entry = buildWith(new Date(FORMATTING_OPTIONS_SUPPORTED_DATE.getTime() + 1)); + expect(service.areFormattingOptionsSupportedForBuild(entry)).toBe(true); + }); + + it('returns false when flag disabled even if date after supported date', () => { + const entry = buildWith(new Date(FORMATTING_OPTIONS_SUPPORTED_DATE.getTime() + 1), false); + expect(service.areFormattingOptionsSupportedForBuild(entry)).toBe(false); + }); + + it('returns false when date before supported date', () => { + const entry = buildWith(new Date(FORMATTING_OPTIONS_SUPPORTED_DATE.getTime() - 1)); + expect(service.areFormattingOptionsSupportedForBuild(entry)).toBe(false); + }); + }); +}); diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-options.service.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-options.service.ts new file mode 100644 index 00000000000..b20ea62b256 --- /dev/null +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-options.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@angular/core'; +import { ActivatedProjectService } from 'xforge-common/activated-project.service'; +import { FeatureFlagService } from 'xforge-common/feature-flags/feature-flag.service'; +import { BuildDto } from '../../machine-api/build-dto'; + +// Corresponds to Serval 1.11.0 release +export const FORMATTING_OPTIONS_SUPPORTED_DATE: Date = new Date('2025-09-25T00:00:00Z'); + +@Injectable({ + providedIn: 'root' +}) +export class DraftOptionsService { + constructor( + private readonly activatedProjectService: ActivatedProjectService, + private readonly featureFlags: FeatureFlagService + ) {} + + areFormattingOptionsSelected(): boolean { + return ( + this.featureFlags.usfmFormat.enabled && + this.activatedProjectService.projectDoc?.data?.translateConfig.draftConfig.usfmConfig?.paragraphFormat != null && + this.activatedProjectService.projectDoc?.data?.translateConfig.draftConfig.usfmConfig?.quoteFormat != null + ); + } + + areFormattingOptionsAvailableButUnselected(): boolean { + return ( + this.featureFlags.usfmFormat.enabled && + (this.activatedProjectService.projectDoc?.data?.translateConfig.draftConfig.usfmConfig?.paragraphFormat == null || + this.activatedProjectService.projectDoc?.data?.translateConfig.draftConfig.usfmConfig?.quoteFormat == null) + ); + } + + areFormattingOptionsSupportedForBuild(entry: BuildDto | undefined): boolean { + return this.featureFlags.usfmFormat.enabled && entry?.additionalInfo?.dateFinished != null + ? new Date(entry.additionalInfo.dateFinished) > FORMATTING_OPTIONS_SUPPORTED_DATE + : false; + } +} diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-utils.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-utils.ts index 7881d5df669..fe87365ee43 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-utils.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-utils.ts @@ -3,9 +3,6 @@ import { TranslateSource } from 'realtime-server/lib/esm/scriptureforge/models/t import language_code_mapping from '../../../../../language_code_mapping.json'; import { SelectableProjectWithLanguageCode } from '../../core/paratext.service'; -// Corresponds to Serval 1.11.0 release -export const FORMATTING_OPTIONS_SUPPORTED_DATE: Date = new Date('2025-09-25T00:00:00Z'); - /** Represents draft sources as a set of two {@link TranslateSource} arrays, and one {@link SFProjectProfile} array. */ export interface DraftSourcesAsTranslateSourceArrays { trainingSources: TranslateSource[]; diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor-draft/editor-draft.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor-draft/editor-draft.component.html index 9a904618ce5..fd071184495 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor-draft/editor-draft.component.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor-draft/editor-draft.component.html @@ -44,24 +44,15 @@ }
@if (featureFlags.usfmFormat.enabled) { - @if (mustChooseFormattingOptions) { - - } @else if (formattingOptionsSupported) { - - - - } + } @if (userAppliedDraft) { @@ -70,19 +61,14 @@ } @if (canApplyDraft) { - - - + }
@if (!canApplyDraft) { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor-draft/editor-draft.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor-draft/editor-draft.component.spec.ts index c1d65bd662a..2ff5050534e 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor-draft/editor-draft.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor-draft/editor-draft.component.spec.ts @@ -415,6 +415,7 @@ describe('EditorDraftComponent', () => { ); when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc)); when(mockDialogService.confirm(anything(), anything())).thenResolve(true); + when(mockDraftHandlingService.canApplyDraft(anything(), anything(), anything(), anything())).thenReturn(true); spyOn(component, 'getTargetOps').and.returnValue(of(targetDelta.ops)); when(mockDraftHandlingService.getDraft(anything(), anything())).thenReturn(of(draftDelta.ops!)); when(mockDraftHandlingService.draftDataToOps(anything(), anything())).thenReturn(draftDelta.ops!); @@ -422,64 +423,7 @@ describe('EditorDraftComponent', () => { fixture.detectChanges(); tick(EDITOR_READY_TIMEOUT); - expect(component.mustChooseFormattingOptions).toBe(false); - flush(); - })); - - it('should hide formatting options for drafts created before supported date', fakeAsync(() => { - const testProjectDoc: SFProjectProfileDoc = { - data: createTestProjectProfile({ - texts: [ - { - bookNum: 1, - chapters: [{ number: 1, permissions: { user01: SFProjectRole.ParatextAdministrator }, hasDraft: true }] - } - ] - }) - } as SFProjectProfileDoc; - when(mockDraftGenerationService.draftExists(anything(), anything(), anything())).thenReturn(of(true)); - when(mockDraftGenerationService.getGeneratedDraftHistory(anything(), anything(), anything())).thenReturn( - of(draftHistory) - ); - when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc)); - when(mockDialogService.confirm(anything(), anything())).thenResolve(true); - spyOn(component, 'getTargetOps').and.returnValue(of(targetDelta.ops)); - when(mockDraftHandlingService.getDraft(anything(), anything())).thenReturn(of(draftDelta.ops!)); - when(mockDraftHandlingService.draftDataToOps(anything(), anything())).thenReturn(draftDelta.ops!); - - fixture.detectChanges(); - tick(EDITOR_READY_TIMEOUT); - - expect(component.mustChooseFormattingOptions).toBe(false); - flush(); - })); - - it('should guide user to select formatting options when formatting not selected', fakeAsync(() => { - const testProjectDoc: SFProjectProfileDoc = { - data: createTestProjectProfile({ - texts: [ - { - bookNum: 1, - chapters: [{ number: 1, permissions: { user01: SFProjectRole.ParatextAdministrator }, hasDraft: true }] - } - ] - }) - } as SFProjectProfileDoc; - when(mockDraftGenerationService.draftExists(anything(), anything(), anything())).thenReturn(of(true)); - const historyAfterFormattingOptions: Revision[] = [{ timestamp: '2025-10-01T12:00:00.000Z' }]; - when(mockDraftGenerationService.getGeneratedDraftHistory(anything(), anything(), anything())).thenReturn( - of(historyAfterFormattingOptions) - ); - when(mockActivatedProjectService.changes$).thenReturn(of(testProjectDoc)); - when(mockDialogService.confirm(anything(), anything())).thenResolve(true); - spyOn(component, 'getTargetOps').and.returnValue(of(targetDelta.ops)); - when(mockDraftHandlingService.getDraft(anything(), anything())).thenReturn(of(draftDelta.ops!)); - when(mockDraftHandlingService.draftDataToOps(anything(), anything())).thenReturn(draftDelta.ops!); - - fixture.detectChanges(); - tick(EDITOR_READY_TIMEOUT); - - expect(component.mustChooseFormattingOptions).toBe(true); + expect(component.canApplyDraft).toBe(true); flush(); })); diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor-draft/editor-draft.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor-draft/editor-draft.component.ts index 89498f83c01..25fb5ece52e 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor-draft/editor-draft.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor-draft/editor-draft.component.ts @@ -42,7 +42,6 @@ import { BuildStates } from '../../../machine-api/build-states'; import { TextComponent } from '../../../shared/text/text.component'; import { DraftGenerationService } from '../../draft-generation/draft-generation.service'; import { DraftHandlingService } from '../../draft-generation/draft-handling.service'; -import { FORMATTING_OPTIONS_SUPPORTED_DATE } from '../../draft-generation/draft-utils'; @Component({ selector: 'app-editor-draft', templateUrl: './editor-draft.component.html', @@ -68,8 +67,6 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges { isDraftReady = false; isDraftApplied = false; userAppliedDraft = false; - hasFormattingSelected = true; - formattingOptionsSupported = true; private selectedRevisionSubject = new BehaviorSubject(undefined); private selectedRevision$ = this.selectedRevisionSubject.asObservable(); @@ -115,25 +112,15 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges { return this.draftHandlingService.canApplyDraft(this.targetProject, this.bookNum, this.chapter, this.draftDelta.ops); } - get doesLatestBuildHaveDraft(): boolean { + get doesLatestHaveDraft(): boolean { return ( this.targetProject?.texts.find(t => t.bookNum === this.bookNum)?.chapters.find(c => c.number === this.chapter) ?.hasDraft ?? false ); } - get mustChooseFormattingOptions(): boolean { - return ( - this.featureFlags.usfmFormat.enabled && - !this.hasFormattingSelected && - this.formattingOptionsSupported && - this.doesLatestBuildHaveDraft - ); - } - set draftRevisions(value: Revision[]) { this._draftRevisions = value; - this.formattingOptionsSupported = value.some(rev => new Date(rev.timestamp) > FORMATTING_OPTIONS_SUPPORTED_DATE); } get draftRevisions(): Revision[] { @@ -216,7 +203,6 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges { filterNullish(), tap(projectDoc => { this.targetProject = projectDoc.data; - this.hasFormattingSelected = projectDoc.data?.translateConfig.draftConfig.usfmConfig != null; }), distinctUntilChanged(), map(() => initialTimestamp) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.spec.ts index 896d80a3c32..3d095c4a794 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.spec.ts @@ -107,6 +107,7 @@ import { PRESENCE_EDITOR_ACTIVE_TIMEOUT } from '../../shared/text/text.component import { XmlUtils } from '../../shared/utils'; import { BiblicalTermsComponent } from '../biblical-terms/biblical-terms.component'; import { DraftGenerationService } from '../draft-generation/draft-generation.service'; +import { DraftOptionsService } from '../draft-generation/draft-options.service'; import { DraftPreviewBooksComponent } from '../draft-generation/draft-preview-books/draft-preview-books.component'; import { TrainingProgressComponent } from '../training-progress/training-progress.component'; import { EditorDraftComponent } from './editor-draft/editor-draft.component'; @@ -132,6 +133,7 @@ const mockedTranslationEngineService = mock(TranslationEngineService); const mockedMatDialog = mock(MatDialog); const mockedHttpClient = mock(HttpClient); const mockedDraftGenerationService = mock(DraftGenerationService); +const mockedDraftOptionsService = mock(DraftOptionsService); const mockedParatextService = mock(ParatextService); const mockedPermissionsService = mock(PermissionsService); const mockedLynxWorkspaceService = mock(LynxWorkspaceService); @@ -198,6 +200,7 @@ describe('EditorComponent', () => { { provide: BreakpointObserver, useClass: TestBreakpointObserver }, { provide: HttpClient, useMock: mockedHttpClient }, { provide: DraftGenerationService, useMock: mockedDraftGenerationService }, + { provide: DraftOptionsService, useMock: mockedDraftOptionsService }, { provide: ParatextService, useMock: mockedParatextService }, { provide: TabFactoryService, useValue: EditorTabFactoryService }, { provide: TabMenuService, useValue: EditorTabMenuService }, @@ -4103,6 +4106,26 @@ describe('EditorComponent', () => { env.dispose(); })); + it('should not add draft preview tab when draft formatting (usfmConfig) is not set', fakeAsync(() => { + const env = new TestEnvironment(env => { + Object.defineProperty(env.component, 'showSource', { get: () => true }); + }); + env.setupProject({ translateConfig: { draftConfig: {} } }); + // Formatting options not selected, so draft tab should not be shown + when(mockedDraftOptionsService.areFormattingOptionsAvailableButUnselected()).thenReturn(true); + when(mockedPermissionsService.canAccessDrafts(anything(), anything())).thenReturn(true); + env.wait(); + env.routeWithParams({ projectId: 'project01', bookId: 'LUK', chapter: '1' }); + env.wait(); + + const sourceTabGroup = env.component.tabState.getTabGroup('source'); + expect(sourceTabGroup?.tabs.find(t => t.type === 'draft')).toBeUndefined(); + + const targetTabGroup = env.component.tabState.getTabGroup('target'); + expect(targetTabGroup?.tabs.find(t => t.type === 'draft')).toBeUndefined(); + env.dispose(); + })); + it('should hide source draft preview tab when switching to chapter with no draft', fakeAsync(() => { const env = new TestEnvironment(env => { Object.defineProperty(env.component, 'showSource', { get: () => true }); diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.ts index 2929079bc70..ae560759482 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.ts @@ -136,6 +136,7 @@ import { XmlUtils } from '../../shared/utils'; import { DraftGenerationService } from '../draft-generation/draft-generation.service'; +import { DraftOptionsService } from '../draft-generation/draft-options.service'; import { EditorHistoryService } from './editor-history/editor-history.service'; import { LynxInsightStateService } from './lynx/insights/lynx-insight-state.service'; import { MultiCursorViewer } from './multi-viewer/multi-viewer.component'; @@ -313,6 +314,7 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy, private readonly editorTabPersistenceService: EditorTabPersistenceService, private readonly textDocService: TextDocService, private readonly draftGenerationService: DraftGenerationService, + private readonly draftOptionsService: DraftOptionsService, private readonly destroyRef: DestroyRef, private readonly breakpointObserver: BreakpointObserver, private readonly mediaBreakpointService: MediaBreakpointService, @@ -1488,7 +1490,11 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy, this.projectDoc, this.userService.currentUserId ); - if (((hasDraft && !draftApplied) || urlDraftActive) && canViewDrafts) { + if ( + ((hasDraft && !draftApplied) || urlDraftActive) && + canViewDrafts && + !this.draftOptionsService.areFormattingOptionsAvailableButUnselected() + ) { // URL may indicate to select the 'draft' tab (such as when coming from generate draft page) const groupIdToAddTo: EditorTabGroupType = this.showSource ? 'source' : 'target'; 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 c2e965a31dc..c8ad3abe99b 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 @@ -16,6 +16,7 @@ import { SFProjectProfileDoc } from '../../../core/models/sf-project-profile-doc import { SF_TYPE_REGISTRY } from '../../../core/models/sf-type-registry'; import { PermissionsService } from '../../../core/permissions.service'; import { TabStateService } from '../../../shared/sf-tab-group'; +import { DraftOptionsService } from '../../draft-generation/draft-options.service'; import { EditorTabMenuService } from './editor-tab-menu.service'; import { EditorTabInfo } from './editor-tabs.types'; @@ -24,6 +25,7 @@ const activatedProjectMock = mock(ActivatedProjectService); const tabStateMock: TabStateService = mock(TabStateService); const mockUserService = mock(UserService); const mockPermissionsService = mock(PermissionsService); +const mockDraftOptionsService = mock(DraftOptionsService); describe('EditorTabMenuService', () => { configureTestingModule(() => ({ @@ -34,7 +36,8 @@ describe('EditorTabMenuService', () => { { provide: TabStateService, useMock: tabStateMock }, { provide: UserService, useMock: mockUserService }, { provide: PermissionsService, useMock: mockPermissionsService }, - { provide: OnlineStatusService, useClass: TestOnlineStatusService } + { provide: OnlineStatusService, useClass: TestOnlineStatusService }, + { provide: DraftOptionsService, useMock: mockDraftOptionsService } ] })); @@ -246,6 +249,22 @@ describe('EditorTabMenuService', () => { expect(spy).toHaveBeenCalledTimes(1); }); }); + + it('should not show draft menu item when draft formatting (usfmConfig) is not set', done => { + const env = new TestEnvironment(); + // Simulate formatting options available but still unselected, so draft tab should be hidden + when(mockDraftOptionsService.areFormattingOptionsAvailableButUnselected()).thenReturn(true); + env.setExistingTabs([]); + service['canShowHistory'] = () => true; + service['canShowResource'] = () => true; + service['canShowBiblicalTerms'] = () => false; + service.getMenuItems().subscribe(items => { + expect(items.find(i => i.type === 'draft')).toBeUndefined(); + expect(items.find(i => i.type === 'history')).toBeDefined(); + expect(items.find(i => i.type === 'project-resource')).toBeDefined(); + done(); + }); + }); }); class TestEnvironment { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/tabs/editor-tab-menu.service.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/tabs/editor-tab-menu.service.ts index 07cd4697d56..98a1e6d45d1 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/tabs/editor-tab-menu.service.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/tabs/editor-tab-menu.service.ts @@ -17,6 +17,7 @@ import { ParatextService } from '../../../core/paratext.service'; import { PermissionsService } from '../../../core/permissions.service'; import { SFProjectService } from '../../../core/sf-project.service'; import { TabMenuItem, TabMenuService, TabStateService } from '../../../shared/sf-tab-group'; +import { DraftOptionsService } from '../../draft-generation/draft-options.service'; import { EditorTabInfo } from './editor-tabs.types'; @Injectable() export class EditorTabMenuService implements TabMenuService { @@ -29,7 +30,8 @@ export class EditorTabMenuService implements TabMenuService private readonly onlineStatus: OnlineStatusService, private readonly tabState: TabStateService, private readonly permissionsService: PermissionsService, - private readonly i18n: I18nService + private readonly i18n: I18nService, + private readonly draftOptionsService: DraftOptionsService ) {} getMenuItems(): Observable { @@ -51,7 +53,8 @@ export class EditorTabMenuService implements TabMenuService isOnline && projectDoc.data != null && SFProjectService.hasDraft(projectDoc.data) && - this.permissionsService.canAccessDrafts(projectDoc, this.userService.currentUserId); + this.permissionsService.canAccessDrafts(projectDoc, this.userService.currentUserId) && + !this.draftOptionsService.areFormattingOptionsAvailableButUnselected(); const items: Observable[] = []; for (const tabType of editorTabTypes) { 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 2b9ed40d67d..de8b9b8b36a 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 @@ -403,16 +403,13 @@ "your_comment": "Your comment" }, "editor_draft_tab": { - "add_chapter_to_project": "Add this chapter to the project", "apply_to_project": "Add to project", "cannot_import": "You cannot import this draft because you do not have permission to edit this chapter. Permissions can be updated in Paratext.", "click_book_to_preview": "Click a book below to preview the draft and add it to your project.", "draft_indicator_applied": "Added", "draft_legacy_warning": "We have updated our drafting functionality. You can take advantage of this by [link:generateDraftUrl]generating a new draft[/link].", "error_applying_draft": "Failed to add the draft to the project. Try again later.", - "formatting": "Formatting", "format_draft": "Formatting options", - "format_draft_before": "Select formatting options before adding it to your project.", "format_draft_can": "Customize formatting options for the draft", "format_draft_cannot": "You can only change formatting for books from the latest draft", "no_draft_notice": "{{ bookChapterName }} has no draft.",