From 9d5ebc341a95db317e313958f42a08f2f204c2f8 Mon Sep 17 00:00:00 2001 From: Nathaniel Paulus Date: Thu, 6 Aug 2026 22:59:29 -0400 Subject: [PATCH] SF-3851 Fix source tab not being correctly removed on mobile --- .../translate/editor/editor.component.spec.ts | 46 +++++++++++++++++++ .../app/translate/editor/editor.component.ts | 4 +- .../lynx-insight-state.service.spec.ts | 5 +- 3 files changed, 52 insertions(+), 3 deletions(-) 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 81516ca547..00e30889b9 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 @@ -4051,6 +4051,45 @@ describe('EditorComponent', () => { expect(spyConsolidate).not.toHaveBeenCalled(); flush(); })); + + it('should remove the source tab when the source project is removed and tab groups are consolidated', fakeAsync(() => { + const env = new TestEnvironment(); + env.wait(); + env.breakpointObserver.emitObserveValue(true); + env.component['tabStateInitialized$'].next(true); + env.component['targetEditorLoaded$'].next(); + env.wait(); + + // The source tab has been consolidated into the target tab group + expect(env.component.tabState.getTabGroup('target')?.tabs.map(tab => tab.type)).toEqual([ + 'project-source', + 'project-target' + ]); + + env.removeSourceProject('project01'); + + expect(env.component.tabState.getTabGroup('target')?.tabs.map(tab => tab.type)).toEqual(['project-target']); + discardPeriodicTasks(); + env.dispose(); + })); + + it('should remove the source tab when the source project is removed and tab groups are not consolidated', fakeAsync(() => { + const env = new TestEnvironment(); + env.wait(); + env.breakpointObserver.emitObserveValue(false); + env.component['tabStateInitialized$'].next(true); + env.component['targetEditorLoaded$'].next(); + env.wait(); + + expect(env.component.tabState.getTabGroup('source')?.tabs.map(tab => tab.type)).toEqual(['project-source']); + + env.removeSourceProject('project01'); + + // The blank tab takes the place of the removed source tab + expect(env.component.tabState.getTabGroup('source')?.tabs.map(tab => tab.type)).toEqual(['blank-tab']); + discardPeriodicTasks(); + env.dispose(); + })); }); describe('initEditorTabs', () => { @@ -5354,6 +5393,13 @@ class TestEnvironment { this.fixture.detectChanges(); } + /** Simulates an administrator removing the source project on the project settings page. */ + removeSourceProject(projectId: string): void { + const projectDoc: SFProjectProfileDoc = this.getProjectDoc(projectId); + projectDoc.submitJson0Op(op => op.unset(p => p.translateConfig.source!), false); + this.wait(); + } + updateFontSize(projectId: string, size: number): void { const projectDoc: SFProjectProfileDoc = this.getProjectDoc(projectId); projectDoc.submitJson0Op(op => op.set(p => p.defaultFontSize, size), false); 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 c7779cda5e..b9f7f64c9a 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 @@ -1505,7 +1505,9 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy, } } } else if (existingSourceTab != null) { - this.tabState.removeTab('source', existingSourceTab.index); + // Remove from the group the tab is actually in, which is not the 'source' group when tab groups are + // consolidated for small screens + this.tabState.removeTab(existingSourceTab.groupId, existingSourceTab.index); } } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-state.service.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-state.service.spec.ts index a51cd08076..1a16230dfd 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-state.service.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-state.service.spec.ts @@ -1,10 +1,10 @@ import { TestBed } from '@angular/core/testing'; +import { Json0OpBuilder } from 'realtime-server/lib/esm/common/utils/json0-op-builder'; import { LynxInsightFilter, LynxInsightType } from 'realtime-server/lib/esm/scriptureforge/models/lynx-insight'; import { createTestProject } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-test-data'; +import { SFProjectUserConfig } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-user-config'; import { createTestProjectUserConfig } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-user-config-test-data'; import { TextInfo } from 'realtime-server/lib/esm/scriptureforge/models/text-info'; -import { Json0OpBuilder } from 'realtime-server/lib/esm/common/utils/json0-op-builder'; -import { SFProjectUserConfig } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-user-config'; import { BehaviorSubject, firstValueFrom, Subject } from 'rxjs'; import { anything, capture, instance, mock, resetCalls, verify, when } from 'ts-mockito'; import { ActivatedBookChapterService, RouteBookChapter } from 'xforge-common/activated-book-chapter.service'; @@ -380,6 +380,7 @@ describe('LynxInsightStateService', () => { describe('panel state persistence', () => { it('should not write to project user config on load when panel state is unchanged', () => { verify(mockProjectUserConfigDoc.submitJson0Op(anything())).never(); + expect().nothing(); }); it('should set only the panelData sub-path, preserving other lynxInsightState props', () => {