From 4564c60d9327d43883dfeca1ed47220271b83894 Mon Sep 17 00:00:00 2001 From: Raymond Luong Date: Thu, 19 Feb 2026 16:50:13 -0700 Subject: [PATCH 1/2] SF-3705 Show offline message in the import from Paratext section --- .../import-questions-dialog.component.html | 7 +- .../import-questions-dialog.component.spec.ts | 231 ++++++++++-------- .../src/assets/i18n/non_checking_en.json | 2 +- 3 files changed, 134 insertions(+), 106 deletions(-) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/import-questions-dialog/import-questions-dialog.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/import-questions-dialog/import-questions-dialog.component.html index 456aa8487d6..ba2436b2524 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/import-questions-dialog/import-questions-dialog.component.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/import-questions-dialog/import-questions-dialog.component.html @@ -50,7 +50,7 @@

{{ t("learn_more") }} @if (transceleratorRequest.status === "offline") { - {{ t("no_transcelerator_offline") }} + {{ t("no_import_offline", { method: "Transcelerator" }) }} } @else if (transceleratorRequest.status === "trying" && transceleratorRequest.failedAttempts > 0) { @@ -112,6 +112,11 @@

{{ t("import_from_paratext") }} {{ t("learn_more") }} + @if (!isOnline) { + + {{ t("no_import_offline", { method: "Paratext" }) }} + + } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/import-questions-dialog/import-questions-dialog.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/import-questions-dialog/import-questions-dialog.component.spec.ts index d4ad67541e8..90e87a893bf 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/import-questions-dialog/import-questions-dialog.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/import-questions-dialog/import-questions-dialog.component.spec.ts @@ -487,7 +487,7 @@ describe('ImportQuestionsDialogComponent', () => { it('does not try to load transcelerator questions when the user is online', fakeAsync(() => { const env = new TestEnvironment({ offline: true }); expect(env.importFromTransceleratorButton.disabled).toBe(true); - expect(env.errorMessages).toEqual(['Importing from Transcelerator is not available offline.']); + expect(env.errorMessages[0]).toEqual('Importing from Transcelerator is not available offline.'); env.setOnline(true); expect(env.importFromTransceleratorButton.disabled).toBe(false); expect(env.errorMessages).toEqual([]); @@ -502,115 +502,135 @@ describe('ImportQuestionsDialogComponent', () => { expect(env.overlayContainerElement.hasChildNodes()).withContext('close button closes dialog').toBeFalse(); })); - it('collects unique Paratext tags in alphabetical order', fakeAsync(() => { - const env = new TestEnvironment(); - const tagAlpha: ParatextNoteTag = { id: 1, name: 'Alpha' }; - const tagBeta: ParatextNoteTag = { id: 3, name: 'Beta' }; - const tagGamma: ParatextNoteTag = { id: 2, name: 'Gamma' }; - const notes: ParatextNote[] = [ - { - id: 'note-1', - verseRef: 'MAT 1:1', - comments: [ - { verseRef: 'MAT 1:1', content: '

Alpha

', tag: tagGamma }, - { verseRef: 'MAT 1:1', content: '

Alpha again

', tag: tagAlpha } - ] - }, - { - id: 'note-2', - verseRef: 'MAT 1:2', - comments: [ - { verseRef: 'MAT 1:2', content: '

Beta

', tag: tagBeta }, - { verseRef: 'MAT 1:2', content: '

Beta duplicate

', tag: tagBeta } - ] - } - ]; + describe('Import from Paratext', () => { + it('disables import button when user is offline', fakeAsync(() => { + const env = new TestEnvironment({ offline: true }); + tick(); + env.fixture.detectChanges(); - const tags = env.collectParatextTagOptions(env.component, notes); + expect(env.importFromParatextButton.disabled).toBe(true); + expect(env.errorMessages[1]).toEqual('Importing from Paratext is not available offline.'); + env.testOnlineStatusService.setIsOnline(true); + tick(); + env.fixture.detectChanges(); - expect(tags.length).toBe(3); - expect(tags.map(tag => tag.name)).toEqual(['Alpha', 'Beta', 'Gamma']); - expect(tags.map(tag => tag.id)).toEqual([1, 3, 2]); - })); + expect(env.importFromParatextButton.disabled).toBe(false); + })); - it('shows a message when no notes have tagged comments', fakeAsync(() => { - const env = new TestEnvironment(); - const notes: ParatextNote[] = [ - { - id: 'note-1', - verseRef: 'MAT 1:1', - comments: [{ verseRef: 'MAT 1:1', content: '

Note without tag

' }] - } - ]; - env.setParatextNotes(env.component, notes); - env.setParatextTagOptions(env.component, env.collectParatextTagOptions(env.component, notes)); - env.component.questionSource = 'paratext'; - env.component.showParatextTagSelector = true; - env.component.selectedParatextTagId = null; - env.component.errorState = undefined; - - env.fixture.detectChanges(); - tick(); + it('collects unique Paratext tags in alphabetical order', fakeAsync(() => { + const env = new TestEnvironment(); + const tagAlpha: ParatextNoteTag = { id: 1, name: 'Alpha' }; + const tagBeta: ParatextNoteTag = { id: 3, name: 'Beta' }; + const tagGamma: ParatextNoteTag = { id: 2, name: 'Gamma' }; + const notes: ParatextNote[] = [ + { + id: 'note-1', + verseRef: 'MAT 1:1', + comments: [ + { verseRef: 'MAT 1:1', content: '

Alpha

', tag: tagGamma }, + { verseRef: 'MAT 1:1', content: '

Alpha again

', tag: tagAlpha } + ] + }, + { + id: 'note-2', + verseRef: 'MAT 1:2', + comments: [ + { verseRef: 'MAT 1:2', content: '

Beta

', tag: tagBeta }, + { verseRef: 'MAT 1:2', content: '

Beta duplicate

', tag: tagBeta } + ] + } + ]; + + const tags = env.collectParatextTagOptions(env.component, notes); + + expect(tags.length).toBe(3); + expect(tags.map(tag => tag.name)).toEqual(['Alpha', 'Beta', 'Gamma']); + expect(tags.map(tag => tag.id)).toEqual([1, 3, 2]); + })); - expect(env.component.status).toBe('paratext_tag_selection'); - expect(env.getParatextTagMessage()).toBe('There are no tagged notes available to import.'); - })); + it('shows a message when no notes have tagged comments', fakeAsync(() => { + const env = new TestEnvironment(); + const notes: ParatextNote[] = [ + { + id: 'note-1', + verseRef: 'MAT 1:1', + comments: [{ verseRef: 'MAT 1:1', content: '

Note without tag

' }] + } + ]; + env.setParatextNotes(env.component, notes); + env.setParatextTagOptions(env.component, env.collectParatextTagOptions(env.component, notes)); + env.component.questionSource = 'paratext'; + env.component.showParatextTagSelector = true; + env.component.selectedParatextTagId = null; + env.component.errorState = undefined; + + env.fixture.detectChanges(); + tick(); + + expect(env.component.status).toBe('paratext_tag_selection'); + expect(env.getParatextTagMessage()).toBe('There are no tagged notes available to import.'); + })); - it('converts Paratext notes for the selected tag into questions', fakeAsync(() => { - const tagQuestions: ParatextNoteTag = { id: 7, name: 'Questions' }; - const notes: ParatextNote[] = [ - { - id: 'note-1', - verseRef: 'MAT 1:1', - comments: [ - { verseRef: 'MAT 1:1', content: '

Ignore

', tag: { id: 6, name: 'Other' } }, - { verseRef: 'MAT 1:1', content: '

Question text

', tag: tagQuestions } - ] - }, - { - id: 'note-2', - verseRef: 'MAT 1:2', - comments: [{ verseRef: 'MAT 1:2', content: '

Question 2

', tag: tagQuestions }] - }, - { - id: 'note-3', - verseRef: 'GEN 1:1', - comments: [{ verseRef: 'GEN 1:1', content: '

Different book

', tag: tagQuestions }] - } - ]; - const preexistingQuestion = TestEnvironment.createQuestionDocWithSource('note-1', new VerseRef('MAT 1:1'), 'text'); - const env = new TestEnvironment({ existingQuestions: [preexistingQuestion], paratextNotes: notes }); - env.setParatextNotes(env.component, notes); - env.setParatextTagOptions(env.component, env.collectParatextTagOptions(env.component, notes)); - env.component.selectedParatextTagId = tagQuestions.id; - env.component.questionSource = 'paratext'; - env.component.showParatextTagSelector = true; - - void env.component.confirmParatextTagSelection(); - tick(); - env.fixture.detectChanges(); - - expect(env.component.status).toBe('filter_notes'); - expect(env.component.filteredList.length).toBe(2); - const questionAlreadyImported = env.component.filteredList[0]; - expect(questionAlreadyImported.question.id).toBe('note-1'); - expect(questionAlreadyImported.question.text).toBe('Question text'); - expect(questionAlreadyImported.sfVersionOfQuestion).not.toBeUndefined(); - - expect(env.component.showDuplicateImportNote).toBeFalse(); - questionAlreadyImported.checked = true; - expect(env.component.showDuplicateImportNote).toBeTrue(); - })); + it('converts Paratext notes for the selected tag into questions', fakeAsync(() => { + const tagQuestions: ParatextNoteTag = { id: 7, name: 'Questions' }; + const notes: ParatextNote[] = [ + { + id: 'note-1', + verseRef: 'MAT 1:1', + comments: [ + { verseRef: 'MAT 1:1', content: '

Ignore

', tag: { id: 6, name: 'Other' } }, + { verseRef: 'MAT 1:1', content: '

Question text

', tag: tagQuestions } + ] + }, + { + id: 'note-2', + verseRef: 'MAT 1:2', + comments: [{ verseRef: 'MAT 1:2', content: '

Question 2

', tag: tagQuestions }] + }, + { + id: 'note-3', + verseRef: 'GEN 1:1', + comments: [{ verseRef: 'GEN 1:1', content: '

Different book

', tag: tagQuestions }] + } + ]; + const preexistingQuestion = TestEnvironment.createQuestionDocWithSource( + 'note-1', + new VerseRef('MAT 1:1'), + 'text' + ); + const env = new TestEnvironment({ existingQuestions: [preexistingQuestion], paratextNotes: notes }); + env.setParatextNotes(env.component, notes); + env.setParatextTagOptions(env.component, env.collectParatextTagOptions(env.component, notes)); + env.component.selectedParatextTagId = tagQuestions.id; + env.component.questionSource = 'paratext'; + env.component.showParatextTagSelector = true; + + void env.component.confirmParatextTagSelection(); + tick(); + env.fixture.detectChanges(); + + expect(env.component.status).toBe('filter_notes'); + expect(env.component.filteredList.length).toBe(2); + const questionAlreadyImported = env.component.filteredList[0]; + expect(questionAlreadyImported.question.id).toBe('note-1'); + expect(questionAlreadyImported.question.text).toBe('Question text'); + expect(questionAlreadyImported.sfVersionOfQuestion).not.toBeUndefined(); + + expect(env.component.showDuplicateImportNote).toBeFalse(); + questionAlreadyImported.checked = true; + expect(env.component.showDuplicateImportNote).toBeTrue(); + })); - it('shows an error when no Paratext project can be found', fakeAsync(() => { - const env = new TestEnvironment({ paratextProjects: [], paratextNotes: [] }); + it('shows an error when no Paratext project can be found', fakeAsync(() => { + const env = new TestEnvironment({ paratextProjects: [], paratextNotes: [] }); - env.click(env.importFromParatextButton); + env.click(env.importFromParatextButton); - expect(env.component.errorState).toBe('paratext_tag_load_error'); - expect(env.component.status).toBe('paratext_tag_load_error'); - expect(env.getParatextTagOptions(env.component).length).toBe(0); - })); + expect(env.component.errorState).toBe('paratext_tag_load_error'); + expect(env.component.status).toBe('paratext_tag_load_error'); + expect(env.getParatextTagOptions(env.component).length).toBe(0); + })); + }); }); class TestEnvironment { @@ -678,11 +698,13 @@ class TestEnvironment { ) { this.questions = options.transceleratorQuestions || this.questions; this.errorOnFetchQuestions = !!options.errorOnFetchQuestions; + + this.fixture = TestBed.createComponent(ChildViewContainerComponent); + if (options.offline === true) { - this.online$.next(false); + this.setOnline(false); } - this.fixture = TestBed.createComponent(ChildViewContainerComponent); if (options.editedQuestionIds) { this.simulateTransceleratorQuestionsAlreadyExisting(options.editedQuestionIds || []); } @@ -834,6 +856,7 @@ class TestEnvironment { setOnline(value: boolean): void { this.online$.next(value); + this.testOnlineStatusService.setIsOnline(value); tick(); this.fixture.detectChanges(); } 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 7c8a43091dc..b32c4697669 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 @@ -534,7 +534,7 @@ "network_error_transcelerator": "Network error fetching Transcelerator questions. Retrying ({{ count }}).", "next": "Next", "no_questions_available": "There are no questions for the books in this project.", - "no_transcelerator_offline": "Importing from Transcelerator is not available offline.", + "no_import_offline": "Importing from {{ method }} is not available offline.", "question_for_verse": "Question for verse {{ number }}", "question": "Question", "reference_from": "Reference from", From 4119081572a7e775b14211ef9ba42e8d259b4e40 Mon Sep 17 00:00:00 2001 From: Raymond Luong Date: Fri, 20 Feb 2026 12:27:23 -0700 Subject: [PATCH 2/2] code review fixes --- .../import-questions-dialog.component.spec.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/import-questions-dialog/import-questions-dialog.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/import-questions-dialog/import-questions-dialog.component.spec.ts index 90e87a893bf..4036a0bb82d 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/checking/import-questions-dialog/import-questions-dialog.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/checking/import-questions-dialog/import-questions-dialog.component.spec.ts @@ -505,15 +505,11 @@ describe('ImportQuestionsDialogComponent', () => { describe('Import from Paratext', () => { it('disables import button when user is offline', fakeAsync(() => { const env = new TestEnvironment({ offline: true }); - tick(); - env.fixture.detectChanges(); - expect(env.importFromParatextButton.disabled).toBe(true); expect(env.errorMessages[1]).toEqual('Importing from Paratext is not available offline.'); - env.testOnlineStatusService.setIsOnline(true); - tick(); - env.fixture.detectChanges(); + // simulate going back online + env.setOnline(true); expect(env.importFromParatextButton.disabled).toBe(false); }));