From c82a885b452fcb16a86acff921e3bd4cd064de61 Mon Sep 17 00:00:00 2001 From: Raymond Luong Date: Tue, 16 Sep 2025 12:24:57 -0600 Subject: [PATCH 1/2] SF-3566 Guide user to formatting options on draft tab --- .../editor-draft/editor-draft.component.html | 47 +++++++++++------- .../editor-draft/editor-draft.component.scss | 2 +- .../editor-draft.component.spec.ts | 49 +++++++++++++++++++ .../editor-draft/editor-draft.component.ts | 42 +++++++++------- .../src/assets/i18n/non_checking_en.json | 3 ++ 5 files changed, 107 insertions(+), 36 deletions(-) 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 286999f5949..6501d253318 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,15 +44,24 @@ }
@if (featureFlags.usfmFormat.enabled) { - - - + } @else { + + + + } } @if (userAppliedDraft) { @@ -61,16 +70,20 @@ } @if (canApplyDraft) { - - } - @if (!canApplyDraft) { + + + + } @else { {{ "editor_draft_tab.cannot_import" | transloco }} diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor-draft/editor-draft.component.scss b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor-draft/editor-draft.component.scss index 23c999a17e3..8e1a3b3a2e9 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor-draft/editor-draft.component.scss +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor-draft/editor-draft.component.scss @@ -31,7 +31,7 @@ app-notice { flex-grow: 1; display: flex; justify-content: flex-end; - column-gap: 4px; + gap: 4px; align-items: center; flex-wrap: wrap; } 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 780307deece..50f54957823 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 @@ -8,6 +8,7 @@ import { cloneDeep } from 'lodash-es'; import { TranslocoMarkupModule } from 'ngx-transloco-markup'; import { Delta } from 'quill'; import { createTestProjectProfile } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-test-data'; +import { ParagraphBreakFormat, QuoteFormat } from 'realtime-server/lib/esm/scriptureforge/models/translate-config'; import { of } from 'rxjs'; import { anything, mock, verify, when } from 'ts-mockito'; import { ActivatedProjectService } from 'xforge-common/activated-project.service'; @@ -351,6 +352,54 @@ describe('EditorDraftComponent', () => { })); describe('applyDraft', () => { + it('should allow user to apply draft when formatting selected', fakeAsync(() => { + const testProjectDoc: SFProjectProfileDoc = { + data: createTestProjectProfile({ + translateConfig: { + draftConfig: { + usfmConfig: { paragraphFormat: ParagraphBreakFormat.BestGuess, quoteFormat: QuoteFormat.Denormalized } + } + } + }) + } 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() + } 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(true); + flush(); + })); + it('should show a prompt when applying if the target has content', fakeAsync(() => { const testProjectDoc: SFProjectProfileDoc = { data: createTestProjectProfile() 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 3bf67bd7638..f83c27d27c0 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 @@ -67,6 +67,7 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges { isDraftReady = false; isDraftApplied = false; userAppliedDraft = false; + hasFormattingSelected = true; private selectedRevisionSubject = new BehaviorSubject(undefined); private selectedRevision$ = this.selectedRevisionSubject.asObservable(); @@ -99,6 +100,28 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges { private readonly router: Router ) {} + get bookId(): string { + return this.bookNum !== undefined ? Canon.bookNumberToId(this.bookNum) : ''; + } + + get canApplyDraft(): boolean { + if (this.targetProject == null || this.bookNum == null || this.chapter == null || this.draftDelta?.ops == null) { + return false; + } + return this.draftHandlingService.canApplyDraft(this.targetProject, this.bookNum, this.chapter, this.draftDelta.ops); + } + + 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; + } + ngOnChanges(): void { if (this.projectId == null || this.bookNum == null || this.chapter == null) { throw new Error('projectId, bookNum, or chapter is null'); @@ -118,10 +141,6 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges { this.selectedRevisionSubject.next(this.selectedRevision); } - get bookId(): string { - return this.bookNum !== undefined ? Canon.bookNumberToId(this.bookNum) : ''; - } - populateDraftTextInit(): void { combineLatest([ this.onlineStatusService.onlineStatus$, @@ -178,6 +197,7 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges { filterNullish(), tap(projectDoc => { this.targetProject = projectDoc.data; + this.hasFormattingSelected = projectDoc.data?.translateConfig.draftConfig.usfmConfig != null; }), distinctUntilChanged(), map(() => initialTimestamp) @@ -238,20 +258,6 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges { }); } - get canApplyDraft(): boolean { - if (this.targetProject == null || this.bookNum == null || this.chapter == null || this.draftDelta?.ops == null) { - return false; - } - return this.draftHandlingService.canApplyDraft(this.targetProject, this.bookNum, this.chapter, this.draftDelta.ops); - } - - get doesLatestHaveDraft(): boolean { - return ( - this.targetProject?.texts.find(t => t.bookNum === this.bookNum)?.chapters.find(c => c.number === this.chapter) - ?.hasDraft ?? false - ); - } - navigateToFormatting(): void { this.router.navigateByUrl(`/projects/${this.projectId}/draft-generation/format/${this.bookId}/${this.chapter}`); } 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 ca637ff53db..81df8f7ff91 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 @@ -402,13 +402,16 @@ "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.", From 6f7bff2b513a780dc978eff46e34f825bb0853e4 Mon Sep 17 00:00:00 2001 From: Raymond Luong Date: Wed, 24 Sep 2025 11:25:27 -0600 Subject: [PATCH 2/2] Do not require selecting formatting if not latest draft --- .../editor-draft/editor-draft.component.spec.ts | 16 +++++++++++++++- .../editor-draft/editor-draft.component.ts | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) 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 50f54957823..79dfa535fcd 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 @@ -7,6 +7,7 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { cloneDeep } from 'lodash-es'; import { TranslocoMarkupModule } from 'ngx-transloco-markup'; import { Delta } from 'quill'; +import { SFProjectRole } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-role'; import { createTestProjectProfile } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-test-data'; import { ParagraphBreakFormat, QuoteFormat } from 'realtime-server/lib/esm/scriptureforge/models/translate-config'; import { of } from 'rxjs'; @@ -355,6 +356,12 @@ describe('EditorDraftComponent', () => { it('should allow user to apply draft when formatting selected', fakeAsync(() => { const testProjectDoc: SFProjectProfileDoc = { data: createTestProjectProfile({ + texts: [ + { + bookNum: 1, + chapters: [{ number: 1, permissions: { user01: SFProjectRole.ParatextAdministrator }, hasDraft: true }] + } + ], translateConfig: { draftConfig: { usfmConfig: { paragraphFormat: ParagraphBreakFormat.BestGuess, quoteFormat: QuoteFormat.Denormalized } @@ -381,7 +388,14 @@ describe('EditorDraftComponent', () => { it('should guide user to select formatting options when formatting not selected', fakeAsync(() => { const testProjectDoc: SFProjectProfileDoc = { - data: createTestProjectProfile() + 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( 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 f83c27d27c0..79066f0c557 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 @@ -119,7 +119,7 @@ export class EditorDraftComponent implements AfterViewInit, OnChanges { } get mustChooseFormattingOptions(): boolean { - return this.featureFlags.usfmFormat.enabled && !this.hasFormattingSelected; + return this.featureFlags.usfmFormat.enabled && !this.hasFormattingSelected && this.doesLatestHaveDraft; } ngOnChanges(): void {