diff --git a/src/RealtimeServer/scriptureforge/models/sf-project-user-config.ts b/src/RealtimeServer/scriptureforge/models/sf-project-user-config.ts index f7f317439f7..6d89aefb887 100644 --- a/src/RealtimeServer/scriptureforge/models/sf-project-user-config.ts +++ b/src/RealtimeServer/scriptureforge/models/sf-project-user-config.ts @@ -31,4 +31,5 @@ export interface SFProjectUserConfig extends ProjectData { editorTabsOpen: EditorTabPersistData[]; lynxInsightState: LynxInsightUserData; selectedDraftTargetParatextId?: string; + showEditorTabsInSinglePane?: boolean; } diff --git a/src/RealtimeServer/scriptureforge/services/sf-project-user-config-service.ts b/src/RealtimeServer/scriptureforge/services/sf-project-user-config-service.ts index e79bcd540b4..7db045b435f 100644 --- a/src/RealtimeServer/scriptureforge/services/sf-project-user-config-service.ts +++ b/src/RealtimeServer/scriptureforge/services/sf-project-user-config-service.ts @@ -154,6 +154,9 @@ export class SFProjectUserConfigService extends SFProjectDataService> consolidatedTabs.push(...group.tabs); + // Order the tabs with the immovable tabs first + const orderedTabs = [...consolidatedTabs].sort((a, b) => Number(a.movable) - Number(b.movable)); + // Clear tabs from all groups except 'into' group - group.setTabs(group.groupId === into ? consolidatedTabs : []); + group.setTabs(group.groupId === into ? orderedTabs : []); }); // Remove tab from restore list if tab is removed from consolidated group diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.html index e499a4e20ef..7960511aac5 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/editor.component.html @@ -12,8 +12,8 @@ [(chapter)]="chapter" [chapters]="chapters" > - @if (showSourceTab || translatorSettingsEnabled) { -
 
+ @if (canShowSourceTab) { +
 
@if (showSourceTab) { } - @if (translatorSettingsEnabled) { - - } + } @if (canShare) {
 
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 14a13e5f5b1..81516ca5472 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 @@ -382,6 +382,44 @@ describe('EditorComponent', () => { expect(env.component.chapters.length).toEqual(50); })); + describe('Show editor tabs in single pane setting', () => { + it('shows the source tab if showEditorTabsInSinglePane is undefined', fakeAsync(() => { + const navigationParams: Params = { projectId: 'project01', bookId: 'MRK' }; + const env = new TestEnvironment(); + env.setCurrentUser('user01'); + env.setupProject(); + env.setProjectUserConfig({ showEditorTabsInSinglePane: undefined }); + env.routeWithParams(navigationParams); + env.wait(); + expect(env.component.showSourceTab).toBeTrue(); + env.dispose(); + })); + + it('shows the source tab if showEditorTabsInSinglePane is false', fakeAsync(() => { + const navigationParams: Params = { projectId: 'project01', bookId: 'MRK' }; + const env = new TestEnvironment(); + env.setCurrentUser('user01'); + env.setupProject(); + env.setProjectUserConfig({ showEditorTabsInSinglePane: false }); + env.routeWithParams(navigationParams); + env.wait(); + expect(env.component.showSourceTab).toBeTrue(); + env.dispose(); + })); + + it('does not show the source tab if showEditorTabsInSinglePane is true', fakeAsync(() => { + const navigationParams: Params = { projectId: 'project01', bookId: 'MRK' }; + const env = new TestEnvironment(); + env.setCurrentUser('user01'); + env.setupProject(); + env.setProjectUserConfig({ showEditorTabsInSinglePane: true }); + env.routeWithParams(navigationParams); + env.wait(); + expect(env.component.showSourceTab).toBeFalse(); + env.dispose(); + })); + }); + describe('Translation Suggestions enabled', () => { it('start with no previous selection', fakeAsync(() => { const env = new TestEnvironment(); @@ -3585,136 +3623,46 @@ describe('EditorComponent', () => { }); describe('Translator settings enabled/disabled', () => { - it('shows translator settings when translation suggestions are enabled but lynx features are disabled', fakeAsync(() => { - const projectConfig = { - translateConfig: { ...defaultTranslateConfig, translationSuggestionsEnabled: true }, - lynxConfig: { - autoCorrectionsEnabled: false, - assessmentsEnabled: false - } - }; - const navigationParams: Params = { projectId: 'project01', bookId: 'MRK' }; - - const env = new TestEnvironment(); - env.setupProject(projectConfig); - env.setProjectUserConfig(); - env.routeWithParams(navigationParams); - env.wait(); - expect(env.translatorSettingsButton).toBeTruthy(); - env.dispose(); - })); - - it('hides translator settings when suggestions are enabled for the project but user cant edit', fakeAsync(() => { - const projectConfig = { - translateConfig: { ...defaultTranslateConfig, translationSuggestionsEnabled: true } - }; - const navigationParams: Params = { projectId: 'project01', bookId: 'MRK' }; - - const env = new TestEnvironment(); - env.setCurrentUser('user06'); //has read but not edit - env.setupProject(projectConfig); - env.setProjectUserConfig(); - env.routeWithParams(navigationParams); - env.wait(); - expect(env.translatorSettingsButton).toBeFalsy(); - env.dispose(); - })); - - it('hides translator settings when both translation suggestions and lynx features are disabled', fakeAsync(() => { - const projectConfig = { - translateConfig: { ...defaultTranslateConfig, translationSuggestionsEnabled: false }, - lynxConfig: { - autoCorrectionsEnabled: false, - assessmentsEnabled: false - } - }; - const navigationParams: Params = { projectId: 'project01', bookId: 'MRK' }; - - const env = new TestEnvironment(); - env.setupProject(projectConfig); - env.setProjectUserConfig(); - env.routeWithParams(navigationParams); - env.wait(); - expect(env.translatorSettingsButton).toBeFalsy(); - env.dispose(); - })); - - it('hides translator settings when lynx features are enabled but user cant edit', fakeAsync(() => { - const projectConfig = { - translateConfig: { ...defaultTranslateConfig, translationSuggestionsEnabled: false }, - lynxConfig: { - autoCorrectionsEnabled: true, - assessmentsEnabled: true - } - }; + it('shows translator settings when the user has a paratext role', fakeAsync(() => { const navigationParams: Params = { projectId: 'project01', bookId: 'MRK' }; const env = new TestEnvironment(); - env.setCurrentUser('user06'); //has read but not edit - env.setupProject(projectConfig); - env.setProjectUserConfig(); - env.routeWithParams(navigationParams); - env.wait(); - expect(env.translatorSettingsButton).toBeFalsy(); - env.dispose(); - })); - - it('shows translator settings when suggestions are disabled but lynx features are enabled', fakeAsync(() => { - const projectConfig = { - translateConfig: { ...defaultTranslateConfig, translationSuggestionsEnabled: false }, - lynxConfig: { - autoCorrectionsEnabled: true, - assessmentsEnabled: false - } - }; - const navigationParams: Params = { projectId: 'project01', bookId: 'MRK' }; - - const env = new TestEnvironment(); - env.setupProject(projectConfig); + env.setCurrentUser('user06'); // Paratext Observer + env.setupProject(); env.setProjectUserConfig(); env.routeWithParams(navigationParams); env.wait(); + expect(env.component.showSourceTab).toBeTrue(); expect(env.translatorSettingsButton).toBeTruthy(); env.dispose(); })); - it('shows translator settings when both suggestions and lynx features are enabled', fakeAsync(() => { - const projectConfig = { - translateConfig: { ...defaultTranslateConfig, translationSuggestionsEnabled: true }, - lynxConfig: { - autoCorrectionsEnabled: true, - assessmentsEnabled: true - } - }; + it('shows translator settings when the user does not have a paratext role but the project has a source', fakeAsync(() => { const navigationParams: Params = { projectId: 'project01', bookId: 'MRK' }; const env = new TestEnvironment(); - env.setupProject(projectConfig); + env.setCurrentUser('user05'); // SF Commenter + env.setupProject(); env.setProjectUserConfig(); env.routeWithParams(navigationParams); env.wait(); + expect(env.component.showSourceTab).toBeTrue(); expect(env.translatorSettingsButton).toBeTruthy(); env.dispose(); })); - it('shows translator settings when lynx features are enabled but no source access', fakeAsync(() => { - const projectConfig = { - translateConfig: { translationSuggestionsEnabled: false }, - lynxConfig: { - autoCorrectionsEnabled: true, - assessmentsEnabled: false - } - }; + it('hides translator settings when the user does not have a paratext role and there is no source', fakeAsync(() => { const navigationParams: Params = { projectId: 'project01', bookId: 'MRK' }; const env = new TestEnvironment(); - // Remove source from project to simulate no source access delete env.testProjectProfile.translateConfig.source; - env.setupProject(projectConfig); + env.setCurrentUser('user05'); // SF Commenter + env.setupProject(); env.setProjectUserConfig(); env.routeWithParams(navigationParams); env.wait(); - expect(env.translatorSettingsButton).toBeTruthy(); + expect(env.component.showSourceTab).toBeFalse(); + expect(env.translatorSettingsButton).toBeFalsy(); env.dispose(); })); @@ -4205,6 +4153,41 @@ describe('EditorComponent', () => { expect(tabs.find(t => t.projectId === absentProjectId)).toBeUndefined(); env.dispose(); })); + + it('should not add a resource tab if it matches the source', fakeAsync(() => { + const env = new TestEnvironment(); + env.setProjectUserConfig({ + editorTabsOpen: [{ tabType: 'project-resource', groupId: 'source', projectId: 'project02' }] + }); + const spyCreateTab = spyOn(env.tabFactory, 'createTab').and.callThrough(); + env.wait(); + expect(spyCreateTab).not.toHaveBeenCalledWith('project-resource', jasmine.any(Object)); + discardPeriodicTasks(); + })); + + it('should add a resource tab if it does not match the source', fakeAsync(() => { + const env = new TestEnvironment(); + env.setupProject({ + translateConfig: { + source: { + paratextId: 'resource01', + name: 'Resource 1', + shortName: 'SRC', + projectRef: 'resource01', + writingSystem: { + tag: 'qaa' + } + } + } + }); + env.setProjectUserConfig({ + editorTabsOpen: [{ tabType: 'project-resource', groupId: 'source', projectId: 'project02' }] + }); + const spyCreateTab = spyOn(env.tabFactory, 'createTab').and.callThrough(); + env.wait(); + expect(spyCreateTab).toHaveBeenCalledWith('project-resource', jasmine.any(Object)); + discardPeriodicTasks(); + })); }); describe('updateDraftTabVisibility', () => { 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 65866df3be1..c7779cda5e6 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 @@ -77,6 +77,7 @@ import { } from 'rxjs'; import { debounceTime, + distinctUntilChanged, filter, first, map, @@ -349,6 +350,8 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy, private scrollSubscription?: Subscription; private tabStateInitialized$ = new BehaviorSubject(false); private visibleTabGroups: EditorTabGroupType[] = editorTabGroupTypes.slice(); + private showEditorTabsInSinglePaneSub?: Subscription; + private showEditorTabsInSinglePane$ = new BehaviorSubject(false); private readonly fabDiameter = 40; readonly fabVerticalCushion = 5; @@ -442,14 +445,6 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy, ); } - get translatorSettingsEnabled(): boolean { - return ( - ((this.hasSource && this.hasSourceViewRight && this.translationSuggestionsProjectEnabled) || - this.lynxProjectEnabled) && - this.userHasGeneralEditRight - ); - } - get numSuggestions(): number { return this.projectUserConfigDoc == null || this.projectUserConfigDoc.data == null ? 1 @@ -493,11 +488,14 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy, return SF_DEFAULT_TRANSLATE_SHARE_ROLE; } + get canShowSourceTab(): boolean { + return (this.hasSource && this.hasSourceViewRight) || this.isParatextUserRole; + } + get showSourceTab(): boolean { return ( - (this.hasSource && this.hasSourceViewRight) || - this.isParatextUserRole || - (this.tabState.getTabGroup('source')?.tabs.some(tab => tab.persist) ?? false) + (this.canShowSourceTab || (this.tabState.getTabGroup('source')?.tabs.some(tab => tab.persist) ?? false)) && + this.projectUserConfigDoc?.data?.showEditorTabsInSinglePane !== true ); } @@ -528,11 +526,11 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy, get hasSourceViewRight(): boolean { const sourceProject = this.sourceProjectDoc?.data; const sourceId = this.source?.id; - if (sourceProject == null || sourceId == null || this.isParatextUserRole) { - return this.isParatextUserRole; + if (sourceProject != null && sourceId != null) { + return this.permissionsService.canAccessText(sourceId, sourceProject); + } else { + return false; } - - return this.permissionsService.canAccessText(sourceId, sourceProject); } get canInsertNote(): boolean { @@ -813,6 +811,18 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy, } } }); + + // Show/hide the source tab based on the user project configuration + const showEditorTabsInSinglePane: boolean = + this.projectUserConfigDoc.data?.showEditorTabsInSinglePane ?? false; + this.showEditorTabsInSinglePane$.next(showEditorTabsInSinglePane); + this.visibleTabGroups = showEditorTabsInSinglePane ? ['target'] : editorTabGroupTypes.slice(); + this.showEditorTabsInSinglePaneSub?.unsubscribe(); + this.showEditorTabsInSinglePaneSub = this.projectUserConfigDoc.changes$.subscribe(async () => { + if (this.projectUserConfigDoc?.data != null) { + this.showEditorTabsInSinglePane$.next(this.projectUserConfigDoc.data.showEditorTabsInSinglePane ?? false); + } + }); } if (this.projectDoc?.data == null) { @@ -919,15 +929,16 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy, this.syncScroll(); }); - // Consolidate tab groups for small screen widths + // Consolidate tab groups for small screen widths, or if the setting is enabled combineLatest([ this.breakpointObserver.observe(this.mediaBreakpointService.width('<', Breakpoint.SM)), this.tabStateInitialized$.pipe(filter(initialized => initialized)), - this.targetEditorLoaded$.pipe(take(1)) + this.targetEditorLoaded$.pipe(take(1)), + this.showEditorTabsInSinglePane$.pipe(distinctUntilChanged()) ]) .pipe(quietTakeUntilDestroyed(this.destroyRef)) - .subscribe(([breakpointState]) => { - if (breakpointState.matches) { + .subscribe(([breakpointState, _initialized, _loaded, showEditorTabsInSinglePane]) => { + if (breakpointState.matches || showEditorTabsInSinglePane) { this.visibleTabGroups = ['target']; this.tabState.consolidateTabGroups('target'); } else { @@ -944,6 +955,7 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy, this.trainingSub?.unsubscribe(); this.projectDataChangesSub?.unsubscribe(); this.initializeLynxStateSub?.unsubscribe(); + this.showEditorTabsInSinglePaneSub?.unsubscribe(); this.metricsSession?.dispose(); this.onTargetDeleteSub?.unsubscribe(); this.bottomSheet?.dismiss(); @@ -1377,6 +1389,15 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy, continue; } + // Exclude resource tabs if they match the project source + if ( + tabData.tabType === 'project-resource' && + tabData.projectId != null && + tabData.projectId === projectDoc?.data?.translateConfig.source?.projectRef + ) { + continue; + } + // Create the tab const tab: EditorTabInfo = this.editorTabFactory.createTab(tabData.tabType, { projectId: tabData.projectId, diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.html index be659597835..3df6ae12986 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.html @@ -1,15 +1,30 @@

{{ t("translator_settings") }}

+ @if (!onlineStatusService.isOnline) { + + {{ t("settings_not_available_offline") }} + + } + + +
+ {{ t("editor_settings_title") }} +
+
+ + {{ t("editor_tabs_in_single_pane") }} + +
+
+
@if (showSuggestionsSettings) { - @if (!onlineStatusService.isOnline) { - - {{ t("settings_not_available_offline") }} - - } -
{ env.closeDialog(); })); + it('the show editor tabs in single pane should update when toggled', fakeAsync(async () => { + const env = new TestEnvironment(); + env.setupProject({ + userConfig: { + showEditorTabsInSinglePane: undefined + } + }); + env.openDialog(); + + const editorTabsSinglePaneToggle = await env.getEditorTabsSinglePaneToggle(); + expect(editorTabsSinglePaneToggle).not.toBeNull(); + expect(env.component!.showEditorTabsInSinglePaneSwitch.value).toBe(false); + expect(await env.isToggleChecked(editorTabsSinglePaneToggle!)).toBe(false); + + await env.toggleSlideToggle(editorTabsSinglePaneToggle!); + expect(env.component!.showEditorTabsInSinglePaneSwitch.value).toBe(true); + expect(await env.isToggleChecked(editorTabsSinglePaneToggle!)).toBe(true); + + const userConfigDoc = env.getProjectUserConfigDoc(); + expect(userConfigDoc.data!.showEditorTabsInSinglePane).toBe(true); + + await env.toggleSlideToggle(editorTabsSinglePaneToggle!); + expect(env.component!.showEditorTabsInSinglePaneSwitch.value).toBe(false); + expect(await env.isToggleChecked(editorTabsSinglePaneToggle!)).toBe(false); + + expect(userConfigDoc.data!.showEditorTabsInSinglePane).toBe(false); + env.closeDialog(); + })); + describe('Lynx Settings', () => { it('should show Lynx settings when both project features are enabled', fakeAsync(() => { const env = new TestEnvironment(); @@ -368,6 +397,10 @@ class TestEnvironment { return suggestionsCard as HTMLElement | null; } + get editorTabsSinglePaneSwitch(): HTMLElement | null { + return this.overlayContainerElement.querySelector('#editor-tabs-single-pane-switch') as HTMLElement | null; + } + get lynxSettingsSection(): HTMLElement | null { // Look for the card containing the lynx master switch const lynxCard = this.overlayContainerElement.querySelector('#lynx-master-switch')?.closest('mat-card'); @@ -518,6 +551,13 @@ class TestEnvironment { ); } + async getEditorTabsSinglePaneToggle(): Promise { + if (!this.loader) return null; + return await this.loader.getHarnessOrNull( + MatSlideToggleHarness.with({ selector: '#editor-tabs-single-pane-switch' }) + ); + } + async getLynxMasterToggle(): Promise { if (!this.loader) return null; return await this.loader.getHarnessOrNull(MatSlideToggleHarness.with({ selector: '#lynx-master-switch' })); diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.ts index 889a59624e8..60f9d21dbf8 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.ts @@ -59,6 +59,10 @@ export interface TranslatorSettingsDialogData { ] }) export class TranslatorSettingsDialogComponent implements OnInit { + readonly showEditorTabsInSinglePaneSwitch = new FormControl({ + value: false, + disabled: !this.onlineStatusService.isOnline + }); readonly suggestionsEnabledSwitch = new FormControl({ value: false, disabled: !this.onlineStatusService.isOnline @@ -117,6 +121,12 @@ export class TranslatorSettingsDialogComponent implements OnInit { this.confidenceThreshold$.next(value); } + setShowEditorTabsInSinglePaneEnabled(value: boolean): void { + void this.projectUserConfigDoc.submitJson0Op(op => + op.set(puc => puc.showEditorTabsInSinglePane, value) + ); + } + setNumSuggestions(value: string): void { this.numSuggestions = value; void this.projectUserConfigDoc.submitJson0Op(op => op.set(puc => puc.numSuggestions, parseInt(value, 10))); @@ -142,6 +152,11 @@ export class TranslatorSettingsDialogComponent implements OnInit { autoCorrectionsEnabled: value }); } + + private get showEditorTabsInSinglePaneUserEnabled(): boolean { + return this.projectUserConfigDoc.data?.showEditorTabsInSinglePane ?? false; + } + private get translationSuggestionsUserEnabled(): boolean { return this.projectUserConfigDoc.data?.translationSuggestionsEnabled ?? true; } @@ -163,9 +178,11 @@ export class TranslatorSettingsDialogComponent implements OnInit { private onOnlineStatusChange(): void { if (this.onlineStatusService.isOnline) { + this.showEditorTabsInSinglePaneSwitch.enable(); this.suggestionsEnabledSwitch.enable(); this.translationSuggestionsDisabled = !this.translationSuggestionsUserEnabled; } else { + this.showEditorTabsInSinglePaneSwitch.disable(); this.suggestionsEnabledSwitch.disable(); this.translationSuggestionsDisabled = true; } @@ -185,6 +202,7 @@ export class TranslatorSettingsDialogComponent implements OnInit { } // Update form control state + this.showEditorTabsInSinglePaneSwitch.setValue(this.showEditorTabsInSinglePaneUserEnabled, { emitEvent: false }); this.suggestionsEnabledSwitch.setValue(this.translationSuggestionsUserEnabled, { emitEvent: false }); this.lynxAssessmentsEnabled.setValue(this.lynxAssessmentsUserEnabled, { emitEvent: false }); this.lynxAutoCorrectEnabled.setValue(this.lynxAutoCorrectUserEnabled, { emitEvent: false }); 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 913cc52e45b..fc7db696dff 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 @@ -901,12 +901,14 @@ "translator_settings_dialog": { "better": "Better", "close": "Close", + "editor_settings_title": "Editor", + "editor_tabs_in_single_pane": "Show editor tabs in a single pane", "insights_enable_assessments": "Show assessments", "insights_enable_autocorrect": "Allow auto-corrections", "insights_settings_title": "Insights", "more": "More", "number_of_suggestions": "Number of suggestions", - "settings_not_available_offline": "Translation suggestion settings are not available while you are offline.", + "settings_not_available_offline": "Translator settings are not available while you are offline.", "suggestion_confidence": "Suggestion confidence", "translation_suggestions_settings_title": "Translation suggestions", "translator_settings": "Translator settings" diff --git a/src/SIL.XForge.Scripture/Models/SFProjectUserConfig.cs b/src/SIL.XForge.Scripture/Models/SFProjectUserConfig.cs index de735d09471..1a582de7918 100644 --- a/src/SIL.XForge.Scripture/Models/SFProjectUserConfig.cs +++ b/src/SIL.XForge.Scripture/Models/SFProjectUserConfig.cs @@ -38,4 +38,5 @@ public class SFProjectUserConfig : ProjectData public string? SelectedBiblicalTermsCategory { get; set; } public string? SelectedBiblicalTermsFilter { get; set; } public string? SelectedDraftTargetParatextId { get; set; } + public bool? ShowEditorTabsInSinglePane { get; set; } }