From d2d149d3680713dfc3121350710bcd59b9254ae0 Mon Sep 17 00:00:00 2001 From: siltomato Date: Tue, 2 Sep 2025 13:39:55 -0400 Subject: [PATCH 1/8] rename 'suggestions settings' to 'translator settings' --- .../app/translate/editor/editor.component.html | 2 +- .../translate/editor/editor.component.spec.ts | 8 ++++---- .../src/app/translate/editor/editor.component.ts | 14 +++++++------- ...=> translator-settings-dialog.component.html} | 2 +- ...=> translator-settings-dialog.component.scss} | 0 ...translator-settings-dialog.component.spec.ts} | 16 ++++++++-------- ...s => translator-settings-dialog.component.ts} | 10 +++++----- .../src/app/translate/translate.module.ts | 4 ++-- .../src/assets/i18n/non_checking_en.json | 2 +- 9 files changed, 29 insertions(+), 29 deletions(-) rename src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/{suggestions-settings-dialog.component.html => translator-settings-dialog.component.html} (95%) rename src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/{suggestions-settings-dialog.component.scss => translator-settings-dialog.component.scss} (100%) rename src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/{suggestions-settings-dialog.component.spec.ts => translator-settings-dialog.component.spec.ts} (95%) rename src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/{suggestions-settings-dialog.component.ts => translator-settings-dialog.component.ts} (92%) 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 dae5b4f27f..7e9147402a 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 @@ -33,7 +33,7 @@ appBlurOnClick type="button" id="settings-btn" - (click)="openSuggestionsSettings()" + (click)="openTranslatorSettings()" [title]="t('configure_translation_suggestions')" > settings 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 37676e746a..9a299039df 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 @@ -3706,7 +3706,7 @@ describe('EditorComponent', () => { env.setProjectUserConfig(); env.routeWithParams(navigationParams); env.wait(); - expect(env.suggestionsSettingsButton).toBeTruthy(); + expect(env.translatorSettingsButton).toBeTruthy(); env.dispose(); })); @@ -3722,7 +3722,7 @@ describe('EditorComponent', () => { env.setProjectUserConfig(); env.routeWithParams(navigationParams); env.wait(); - expect(env.suggestionsSettingsButton).toBeFalsy(); + expect(env.translatorSettingsButton).toBeFalsy(); env.dispose(); })); @@ -3737,7 +3737,7 @@ describe('EditorComponent', () => { env.setProjectUserConfig(); env.routeWithParams(navigationParams); env.wait(); - expect(env.suggestionsSettingsButton).toBeFalsy(); + expect(env.translatorSettingsButton).toBeFalsy(); env.dispose(); })); @@ -4736,7 +4736,7 @@ class TestEnvironment { return Canon.bookNumberToEnglishName(this.component.bookNum!); } - get suggestionsSettingsButton(): DebugElement { + get translatorSettingsButton(): DebugElement { return this.fixture.debugElement.query(By.css('#settings-btn')); } 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 eef1172a81..db1dd3e3ea 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 @@ -128,10 +128,6 @@ 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'; import { NoteDialogComponent, NoteDialogData, NoteDialogResult } from './note-dialog/note-dialog.component'; -import { - SuggestionsSettingsDialogComponent, - SuggestionsSettingsDialogData -} from './suggestions-settings-dialog.component'; import { Suggestion } from './suggestions.component'; import { EditorTabAddRequestService } from './tabs/editor-tab-add-request.service'; import { EditorTabFactoryService } from './tabs/editor-tab-factory.service'; @@ -139,6 +135,10 @@ import { EditorTabMenuService } from './tabs/editor-tab-menu.service'; import { EditorTabPersistenceService } from './tabs/editor-tab-persistence.service'; import { EditorTabInfo } from './tabs/editor-tabs.types'; import { TranslateMetricsSession } from './translate-metrics-session'; +import { + TranslatorSettingsDialogComponent, + TranslatorSettingsDialogData +} from './translator-settings-dialog.component'; export const UPDATE_SUGGESTIONS_TIMEOUT = 100; @@ -1142,13 +1142,13 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy, } } - openSuggestionsSettings(): void { + openTranslatorSettings(): void { if (this.projectDoc == null || this.projectUserConfigDoc == null) { return; } - const dialogRef = this.openMatDialog( - SuggestionsSettingsDialogComponent, + const dialogRef = this.openMatDialog( + TranslatorSettingsDialogComponent, { data: { projectDoc: this.projectDoc, projectUserConfigDoc: this.projectUserConfigDoc } } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/suggestions-settings-dialog.component.html b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.html similarity index 95% rename from src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/suggestions-settings-dialog.component.html rename to src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.html index f4a2c3a8bf..67911cb9c3 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/suggestions-settings-dialog.component.html +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.html @@ -1,4 +1,4 @@ - +

{{ t("translator_settings") }}

@if (!onlineStatusService.isOnline) { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/suggestions-settings-dialog.component.scss b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.scss similarity index 100% rename from src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/suggestions-settings-dialog.component.scss rename to src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.scss diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/suggestions-settings-dialog.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.spec.ts similarity index 95% rename from src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/suggestions-settings-dialog.component.spec.ts rename to src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.spec.ts index 6c59566259..a020860f73 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/suggestions-settings-dialog.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.spec.ts @@ -31,11 +31,11 @@ import { SFProjectUserConfigDoc } from '../../core/models/sf-project-user-config import { SF_TYPE_REGISTRY } from '../../core/models/sf-type-registry'; import { CONFIDENCE_THRESHOLD_TIMEOUT, - SuggestionsSettingsDialogComponent, - SuggestionsSettingsDialogData -} from './suggestions-settings-dialog.component'; + TranslatorSettingsDialogComponent, + TranslatorSettingsDialogData +} from './translator-settings-dialog.component'; -describe('SuggestionsSettingsDialogComponent', () => { +describe('TranslatorSettingsDialogComponent', () => { configureTestingModule(() => ({ imports: [ DialogTestModule, @@ -120,7 +120,7 @@ describe('SuggestionsSettingsDialogComponent', () => { @NgModule({ imports: [UICommonModule, TestTranslocoModule], - declarations: [SuggestionsSettingsDialogComponent] + declarations: [TranslatorSettingsDialogComponent] }) class DialogTestModule {} @@ -130,7 +130,7 @@ interface TestEnvironmentConstructorArgs { class TestEnvironment { readonly fixture: ComponentFixture; - component?: SuggestionsSettingsDialogComponent; + component?: TranslatorSettingsDialogComponent; readonly testOnlineStatusService: TestOnlineStatusService = TestBed.inject( OnlineStatusService ) as TestOnlineStatusService; @@ -194,11 +194,11 @@ class TestEnvironment { .then(projectUserConfigDoc => { const viewContainerRef = this.fixture.componentInstance.childViewContainer; const projectDoc = this.getProjectProfileDoc(); - const config: MatDialogConfig = { + const config: MatDialogConfig = { data: { projectDoc, projectUserConfigDoc }, viewContainerRef }; - const dialogRef = TestBed.inject(MatDialog).open(SuggestionsSettingsDialogComponent, config); + const dialogRef = TestBed.inject(MatDialog).open(TranslatorSettingsDialogComponent, config); this.component = dialogRef.componentInstance; }); this.wait(); diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/suggestions-settings-dialog.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.ts similarity index 92% rename from src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/suggestions-settings-dialog.component.ts rename to src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.ts index cbe9d6408e..bb4d69c793 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/suggestions-settings-dialog.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.ts @@ -9,16 +9,16 @@ import { SFProjectProfileDoc } from '../../core/models/sf-project-profile-doc'; import { SFProjectUserConfigDoc } from '../../core/models/sf-project-user-config-doc'; export const CONFIDENCE_THRESHOLD_TIMEOUT = 500; -export interface SuggestionsSettingsDialogData { +export interface TranslatorSettingsDialogData { projectDoc: SFProjectProfileDoc; projectUserConfigDoc: SFProjectUserConfigDoc; } @Component({ - templateUrl: './suggestions-settings-dialog.component.html', - styleUrls: ['./suggestions-settings-dialog.component.scss'] + templateUrl: './translator-settings-dialog.component.html', + styleUrls: ['./translator-settings-dialog.component.scss'] }) -export class SuggestionsSettingsDialogComponent { +export class TranslatorSettingsDialogComponent { suggestionsEnabledSwitch = new UntypedFormControl({ disabled: !this.onlineStatusService.isOnline }); private readonly projectDoc: SFProjectProfileDoc; @@ -26,7 +26,7 @@ export class SuggestionsSettingsDialogComponent { private confidenceThreshold$ = new BehaviorSubject(20); constructor( - @Inject(MAT_DIALOG_DATA) data: SuggestionsSettingsDialogData, + @Inject(MAT_DIALOG_DATA) data: TranslatorSettingsDialogData, readonly onlineStatusService: OnlineStatusService, private destroyRef: DestroyRef ) { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/translate.module.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/translate.module.ts index f2f13f89fd..bbbe112f27 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/translate.module.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/translate.module.ts @@ -22,9 +22,9 @@ import { EditorComponent } from './editor/editor.component'; import { LynxInsightsModule } from './editor/lynx/insights/lynx-insights.module'; import { MultiViewerComponent } from './editor/multi-viewer/multi-viewer.component'; import { NoteDialogComponent } from './editor/note-dialog/note-dialog.component'; -import { SuggestionsSettingsDialogComponent } from './editor/suggestions-settings-dialog.component'; import { SuggestionsComponent } from './editor/suggestions.component'; import { EditorTabAddResourceDialogComponent } from './editor/tabs/editor-tab-add-resource-dialog/editor-tab-add-resource-dialog.component'; +import { TranslatorSettingsDialogComponent } from './editor/translator-settings-dialog.component'; import { FontUnsupportedMessageComponent } from './font-unsupported-message/font-unsupported-message.component'; import { TrainingProgressComponent } from './training-progress/training-progress.component'; import { TranslateOverviewComponent } from './translate-overview/translate-overview.component'; @@ -37,7 +37,7 @@ import { TranslateRoutingModule } from './translate-routing.module'; MultiViewerComponent, NoteDialogComponent, SuggestionsComponent, - SuggestionsSettingsDialogComponent, + TranslatorSettingsDialogComponent, TrainingProgressComponent, TranslateOverviewComponent, HistoryChooserComponent, 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 aaca42e280..53cd1f772b 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 @@ -744,7 +744,7 @@ "cancel": "Cancel", "select": "Select" }, - "suggestions_settings_dialog": { + "translator_settings_dialog": { "better": "Better", "close": "Close", "more": "More", From 006e52a84df6c767235fcf60220c19ee98b1b336 Mon Sep 17 00:00:00 2001 From: siltomato Date: Fri, 5 Sep 2025 13:36:11 -0400 Subject: [PATCH 2/8] add lynx user settings to translator settings --- .../scriptureforge/models/lynx-config.ts | 5 + .../sf-project-user-config-test-data.ts | 3 +- .../models/sf-project-user-config.ts | 2 + .../sf-project-user-config-migrations.ts | 17 +- .../sf-project-user-config-service.ts | 12 + .../translate/editor/editor.component.html | 6 +- .../translate/editor/editor.component.spec.ts | 324 ++++++++++++---- .../app/translate/editor/editor.component.ts | 65 +++- .../translator-settings-dialog.component.html | 123 ++++-- .../translator-settings-dialog.component.scss | 49 ++- ...anslator-settings-dialog.component.spec.ts | 351 ++++++++++++++++-- .../translator-settings-dialog.component.ts | 139 +++++-- .../src/assets/i18n/non_checking_en.json | 7 +- .../Models/LynxUserConfig.cs | 17 + .../Models/SFProjectUserConfig.cs | 1 + 15 files changed, 936 insertions(+), 185 deletions(-) create mode 100644 src/SIL.XForge.Scripture/Models/LynxUserConfig.cs diff --git a/src/RealtimeServer/scriptureforge/models/lynx-config.ts b/src/RealtimeServer/scriptureforge/models/lynx-config.ts index 0880ea8ecb..5dc6b1ea16 100644 --- a/src/RealtimeServer/scriptureforge/models/lynx-config.ts +++ b/src/RealtimeServer/scriptureforge/models/lynx-config.ts @@ -4,3 +4,8 @@ export interface LynxConfig { punctuationCheckerEnabled: boolean; allowedCharacterCheckerEnabled: boolean; } + +export interface LynxUserConfig { + autoCorrectionsEnabled?: boolean; + assessmentsEnabled?: boolean; +} diff --git a/src/RealtimeServer/scriptureforge/models/sf-project-user-config-test-data.ts b/src/RealtimeServer/scriptureforge/models/sf-project-user-config-test-data.ts index 3ed892f182..79b7cb206b 100644 --- a/src/RealtimeServer/scriptureforge/models/sf-project-user-config-test-data.ts +++ b/src/RealtimeServer/scriptureforge/models/sf-project-user-config-test-data.ts @@ -18,7 +18,8 @@ export function createTestProjectUserConfig(overrides?: RecursivePartial { + if (doc.data.lynxUserConfig === undefined) { + const op: ObjectInsertOp = { + p: ['lynxUserConfig'], + oi: { assessmentsEnabled: true, autoCorrectionsEnabled: true } + }; + await submitMigrationOp(SFProjectUserConfigMigration9.VERSION, doc, [op]); + } + } +} + export const SF_PROJECT_USER_CONFIG_MIGRATIONS: MigrationConstructor[] = monotonicallyIncreasingMigrationList([ SFProjectUserConfigMigration1, SFProjectUserConfigMigration2, @@ -104,5 +118,6 @@ export const SF_PROJECT_USER_CONFIG_MIGRATIONS: MigrationConstructor[] = monoton SFProjectUserConfigMigration5, SFProjectUserConfigMigration6, SFProjectUserConfigMigration7, - SFProjectUserConfigMigration8 + SFProjectUserConfigMigration8, + SFProjectUserConfigMigration9 ]); 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 f97f158fe1..64905522f9 100644 --- a/src/RealtimeServer/scriptureforge/services/sf-project-user-config-service.ts +++ b/src/RealtimeServer/scriptureforge/services/sf-project-user-config-service.ts @@ -145,6 +145,18 @@ export class SFProjectUserConfigService extends SFProjectDataService - @if (showSource || suggestionsSettingsEnabled) { -
 
+ @if (showSource || translatorSettingsEnabled) { +
 
@if (showSource) { } - @if (suggestionsSettingsEnabled) { + @if (translatorSettingsEnabled) { diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.scss b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.scss index 9bccaecf17..e41e05b5f5 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.scss +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.scss @@ -1,3 +1,5 @@ +$disabledTextOpacity: 0.6; + mat-dialog-content { display: flex; flex-direction: column; @@ -6,6 +8,44 @@ mat-dialog-content { max-width: 100%; } +mat-card:last-of-type { + margin-bottom: 0.1em; // Prevent card box shadow cutoff +} + +.toggle-label { + margin-inline-start: 0.2rem; +} + +.card-header-with-toggle { + margin-bottom: 1.5em; + + .card-title { + font-size: 1.3em; + font-weight: 500; + line-height: normal; + } +} + +.offline { + .card-title { + opacity: $disabledTextOpacity; + } +} + +.settings-content { + padding-inline-start: 1em; +} + +.lynx-sub-settings { + mat-slide-toggle { + margin-bottom: 0.8em; + + &:last-child { + margin-bottom: 0; + } + } +} + .slider-labels { margin-top: 15px; display: flex; @@ -16,13 +56,18 @@ mat-dialog-content { .suggestions-confidence-field { display: flex; flex-direction: column; + margin-top: 1em; } .minor-header { font-size: 0.875rem; font-weight: 500; + + &.disabled { + opacity: $disabledTextOpacity; + } } -#suggestions-enabled-switch { - margin-block: 1em; +app-notice { + margin-bottom: 1em; } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.spec.ts index a020860f73..6dbbf1566d 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.spec.ts @@ -1,11 +1,13 @@ +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { DebugElement, NgModule } from '@angular/core'; import { ComponentFixture, fakeAsync, flush, TestBed, tick } from '@angular/core/testing'; import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { MatSelect } from '@angular/material/select'; +import { MatSlideToggleHarness } from '@angular/material/slide-toggle/testing'; import { MatSlider } from '@angular/material/slider'; import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { cloneDeep } from 'lodash-es'; import { SFProjectProfile } from 'realtime-server/lib/esm/scriptureforge/models/sf-project'; 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'; @@ -14,6 +16,7 @@ import { SF_PROJECT_USER_CONFIGS_COLLECTION, 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 { OnlineStatusService } from 'xforge-common/online-status.service'; import { TestOnlineStatusModule } from 'xforge-common/test-online-status.module'; import { TestOnlineStatusService } from 'xforge-common/test-online-status.service'; @@ -29,6 +32,7 @@ import { UICommonModule } from 'xforge-common/ui-common.module'; import { SFProjectProfileDoc } from '../../core/models/sf-project-profile-doc'; import { SFProjectUserConfigDoc } from '../../core/models/sf-project-user-config-doc'; import { SF_TYPE_REGISTRY } from '../../core/models/sf-type-registry'; +import { NoticeComponent } from '../../shared/notice/notice.component'; import { CONFIDENCE_THRESHOLD_TIMEOUT, TranslatorSettingsDialogComponent, @@ -58,13 +62,19 @@ describe('TranslatorSettingsDialogComponent', () => { env.closeDialog(); })); - it('update suggestions enabled', fakeAsync(() => { + it('update suggestions enabled', fakeAsync(async () => { const env = new TestEnvironment(); env.openDialog(); expect(env.component!.translationSuggestionsUserEnabled).toBe(true); - env.clickSwitch(env.suggestionsEnabledSwitch); + const suggestionsToggle = await env.getSuggestionsEnabledToggle(); + expect(suggestionsToggle).not.toBeNull(); + expect(await env.isToggleChecked(suggestionsToggle!)).toBe(true); + + await env.toggleSlideToggle(suggestionsToggle!); expect(env.component!.translationSuggestionsUserEnabled).toBe(false); + expect(await env.isToggleChecked(suggestionsToggle!)).toBe(false); + const userConfigDoc = env.getProjectUserConfigDoc(); expect(userConfigDoc.data!.translationSuggestionsEnabled).toBe(false); env.closeDialog(); @@ -93,33 +103,237 @@ describe('TranslatorSettingsDialogComponent', () => { const env = new TestEnvironment(); env.openDialog(); - expect(env.offlineText).toBeNull(); + expect(env.offlineAppNotice == null).toBeTrue(); expect(env.suggestionsEnabledCheckbox.disabled).toBe(false); expect(env.confidenceThresholdSlider.disabled).toBe(false); expect(env.numSuggestionsSelect.disabled).toBe(false); env.isOnline = false; - expect(env.offlineText).not.toBeNull(); + expect(env.offlineAppNotice == null).toBeFalse(); expect(env.suggestionsEnabledCheckbox.disabled).toBe(true); expect(env.confidenceThresholdSlider.disabled).toBe(true); expect(env.numSuggestionsSelect.disabled).toBe(true); env.closeDialog(); })); - it('the suggestions toggle is switched on when the dialog opens while offline', fakeAsync(() => { + it('should hide translation suggestions section when project has translation suggestions disabled', fakeAsync(async () => { + const env = new TestEnvironment(); + const projectDoc = env.getProjectProfileDoc(); + + env.setupProject({ + userConfig: { + translationSuggestionsEnabled: true + } + }); + projectDoc.submitJson0Op(op => { + op.set(p => p.translateConfig!.translationSuggestionsEnabled, false); + }); + env.fixture.detectChanges(); + + env.openDialog(); + + expect(env.component!.showSuggestionsSettings).toBe(false); + expect(env.suggestionsSection == null).toBeTrue(); + env.closeDialog(); + })); + + it('should show translation suggestions section when project has translation suggestions enabled', fakeAsync(async () => { + const env = new TestEnvironment(); + env.openDialog(); + + expect(env.component!.showSuggestionsSettings).toBe(true); + expect(env.suggestionsSection == null).toBeFalse(); + env.closeDialog(); + })); + + it('the suggestions toggle is switched on when the dialog opens while offline', fakeAsync(async () => { const env = new TestEnvironment(); env.isOnline = false; env.openDialog(); - expect(env.suggestionsEnabledCheckbox.disabled).toBe(true); - expect(env.isSwitchChecked(env.suggestionsEnabledCheckbox)).toBe(true); + const suggestionsToggle = await env.getSuggestionsEnabledToggle(); + expect(await env.isToggleDisabled(suggestionsToggle!)).toBe(true); + expect(await env.isToggleChecked(suggestionsToggle!)).toBe(true); env.closeDialog(); })); + + describe('Lynx Settings', () => { + it('should show Lynx settings when both project features are enabled', fakeAsync(() => { + const env = new TestEnvironment(); + env.setupProject({ + projectConfig: { + lynxConfig: { + autoCorrectionsEnabled: true, + assessmentsEnabled: true, + punctuationCheckerEnabled: false, + allowedCharacterCheckerEnabled: false + } + } + }); + env.openDialog(); + + expect(env.lynxSettingsSection == null).toBeFalse(); + expect(env.lynxMasterSwitch == null).toBeFalse(); + expect(env.lynxAssessmentsSwitch == null).toBeFalse(); + expect(env.lynxAutoCorrectSwitch == null).toBeFalse(); + env.closeDialog(); + })); + + it('should hide Lynx settings when project features are disabled', fakeAsync(() => { + const env = new TestEnvironment(); + env.setupProject({ + projectConfig: { + lynxConfig: { + autoCorrectionsEnabled: false, + assessmentsEnabled: false, + punctuationCheckerEnabled: false, + allowedCharacterCheckerEnabled: false + } + } + }); + env.openDialog(); + + expect(env.lynxSettingsSection == null).toBeTrue(); + env.closeDialog(); + })); + + it('should show only assessments switch when only assessments is enabled in project', fakeAsync(() => { + const env = new TestEnvironment(); + env.setupProject({ + projectConfig: { + lynxConfig: { + autoCorrectionsEnabled: false, + assessmentsEnabled: true, + punctuationCheckerEnabled: false, + allowedCharacterCheckerEnabled: false + } + } + }); + env.openDialog(); + + expect(env.lynxSettingsSection == null).toBeFalse(); + expect(env.lynxMasterSwitch == null).toBeFalse(); + expect(env.lynxAssessmentsSwitch == null).toBeFalse(); + expect(env.lynxAutoCorrectSwitch == null).toBeFalse; + env.closeDialog(); + })); + + it('should show only auto-correct switch when only auto-correct is enabled in project', fakeAsync(() => { + const env = new TestEnvironment(); + env.setupProject({ + projectConfig: { + lynxConfig: { + autoCorrectionsEnabled: true, + assessmentsEnabled: false, + punctuationCheckerEnabled: false, + allowedCharacterCheckerEnabled: false + } + } + }); + env.openDialog(); + + expect(env.lynxSettingsSection == null).toBeFalse(); + expect(env.lynxMasterSwitch == null).toBeFalse(); + expect(env.lynxAssessmentsSwitch == null).toBeFalse; + expect(env.lynxAutoCorrectSwitch == null).toBeFalse(); + env.closeDialog(); + })); + + it('should update user lynx master setting when toggled', fakeAsync(async () => { + const env = new TestEnvironment(); + env.setupProject({ + projectConfig: { + lynxConfig: { + autoCorrectionsEnabled: true, + assessmentsEnabled: true, + punctuationCheckerEnabled: false, + allowedCharacterCheckerEnabled: false + } + } + }); + env.openDialog(); + + const lynxMasterToggle = await env.getLynxMasterToggle(); + expect(lynxMasterToggle).not.toBeNull(); + expect(env.component!.lynxMasterSwitch.value).toBe(false); + expect(await env.isToggleChecked(lynxMasterToggle!)).toBe(false); + + await env.toggleSlideToggle(lynxMasterToggle!); + expect(env.component!.lynxMasterSwitch.value).toBe(true); + expect(await env.isToggleChecked(lynxMasterToggle!)).toBe(true); + + const userConfigDoc = env.getProjectUserConfigDoc(); + expect(userConfigDoc.data!.lynxUserConfig?.autoCorrectionsEnabled).toBe(true); + expect(userConfigDoc.data!.lynxUserConfig?.assessmentsEnabled).toBe(true); + env.closeDialog(); + })); + + it('should update user lynx assessments setting when toggled', fakeAsync(async () => { + const env = new TestEnvironment(); + env.setupProject({ + projectConfig: { + lynxConfig: { + autoCorrectionsEnabled: true, + assessmentsEnabled: true, + punctuationCheckerEnabled: false, + allowedCharacterCheckerEnabled: false + } + } + }); + env.openDialog(); + + const lynxAssessmentsToggle = await env.getLynxAssessmentsToggle(); + expect(env.component!.lynxAssessmentsEnabled.value).toBe(false); + expect(await env.isToggleChecked(lynxAssessmentsToggle!)).toBe(false); + + await env.toggleSlideToggle(lynxAssessmentsToggle!); + expect(env.component!.lynxAssessmentsEnabled.value).toBe(true); + expect(await env.isToggleChecked(lynxAssessmentsToggle!)).toBe(true); + + const userConfigDoc = env.getProjectUserConfigDoc(); + expect(userConfigDoc.data!.lynxUserConfig?.assessmentsEnabled).toBe(true); + env.closeDialog(); + })); + + it('should enable Lynx settings even when offline', fakeAsync(async () => { + const env = new TestEnvironment(); + env.setupProject({ + projectConfig: { + lynxConfig: { + autoCorrectionsEnabled: true, + assessmentsEnabled: true, + punctuationCheckerEnabled: false, + allowedCharacterCheckerEnabled: false + } + } + }); + env.isOnline = false; + env.openDialog(); + + const lynxMasterToggle = await env.getLynxMasterToggle(); + const lynxAssessmentsToggle = await env.getLynxAssessmentsToggle(); + const lynxAutoCorrectToggle = await env.getLynxAutoCorrectToggle(); + const suggestionsToggle = await env.getSuggestionsEnabledToggle(); + + expect(lynxMasterToggle).not.toBeNull(); + expect(lynxAssessmentsToggle).not.toBeNull(); + expect(lynxAutoCorrectToggle).not.toBeNull(); + expect(suggestionsToggle).not.toBeNull(); + + expect(await env.isToggleDisabled(lynxMasterToggle!)).toBe(false); + expect(await env.isToggleDisabled(lynxAssessmentsToggle!)).toBe(false); + expect(await env.isToggleDisabled(lynxAutoCorrectToggle!)).toBe(false); + + // But translation suggestions should be disabled + expect(await env.isToggleDisabled(suggestionsToggle!)).toBe(true); + env.closeDialog(); + })); + }); }); @NgModule({ - imports: [UICommonModule, TestTranslocoModule], + imports: [UICommonModule, TestTranslocoModule, NoticeComponent], declarations: [TranslatorSettingsDialogComponent] }) class DialogTestModule {} @@ -131,6 +345,7 @@ interface TestEnvironmentConstructorArgs { class TestEnvironment { readonly fixture: ComponentFixture; component?: TranslatorSettingsDialogComponent; + loader?: HarnessLoader; readonly testOnlineStatusService: TestOnlineStatusService = TestBed.inject( OnlineStatusService ) as TestOnlineStatusService; @@ -156,7 +371,7 @@ class TestEnvironment { } get suggestionsEnabledSwitch(): HTMLElement { - return this.overlayContainerElement.querySelector('#suggestions-enabled-switch') as HTMLElement; + return this.overlayContainerElement.querySelector('#translation-suggestions-master-switch') as HTMLElement; } get suggestionsEnabledCheckbox(): HTMLInputElement { @@ -167,14 +382,40 @@ class TestEnvironment { return this.fixture.debugElement.query(By.css('#num-suggestions-select')).componentInstance; } - get offlineText(): DebugElement { - return this.fixture.debugElement.query(By.css('.offline-text')); + get offlineAppNotice(): DebugElement { + return this.fixture.debugElement.query(By.css('app-notice[icon="cloud_off"]')); } get closeButton(): HTMLElement { return this.overlayContainerElement.querySelector('button[mat-dialog-close]') as HTMLElement; } + get suggestionsSection(): HTMLElement | null { + // Look for the card containing the translation suggestions master switch + const suggestionsCard = this.overlayContainerElement + .querySelector('#translation-suggestions-master-switch') + ?.closest('mat-card'); + return suggestionsCard 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'); + return lynxCard as HTMLElement | null; + } + + get lynxMasterSwitch(): HTMLElement | null { + return this.overlayContainerElement.querySelector('#lynx-master-switch') as HTMLElement | null; + } + + get lynxAssessmentsSwitch(): HTMLElement | null { + return this.overlayContainerElement.querySelector('#lynx-assessments-enabled') as HTMLElement | null; + } + + get lynxAutoCorrectSwitch(): HTMLElement | null { + return this.overlayContainerElement.querySelector('#lynx-autocorrect-enabled') as HTMLElement | null; + } + set isOnline(value: boolean) { this.testOnlineStatusService.setIsOnline(value); this.wait(); @@ -200,6 +441,7 @@ class TestEnvironment { }; const dialogRef = TestBed.inject(MatDialog).open(TranslatorSettingsDialogComponent, config); this.component = dialogRef.componentInstance; + this.loader = TestbedHarnessEnvironment.documentRootLoader(this.fixture); }); this.wait(); } @@ -211,8 +453,10 @@ class TestEnvironment { } setProjectUserConfig(userConfig: Partial = {}): void { - const user1Config = cloneDeep(userConfig) as SFProjectUserConfig; - user1Config.ownerRef = 'user01'; + const user1Config = createTestProjectUserConfig({ + ownerRef: 'user01', + ...userConfig + }); this.realtimeService.addSnapshot(SFProjectUserConfigDoc.COLLECTION, { id: getSFProjectUserConfigDocId('project01', user1Config.ownerRef), data: user1Config @@ -228,6 +472,40 @@ class TestEnvironment { }); } + setupProject({ + userConfig = {}, + projectConfig = {} + }: { + userConfig?: Partial; + projectConfig?: Partial; + } = {}): void { + const user1Config: SFProjectUserConfig = createTestProjectUserConfig({ + ownerRef: 'user01', + confidenceThreshold: 0.5, + ...userConfig + }); + + this.realtimeService.addSnapshot(SFProjectUserConfigDoc.COLLECTION, { + id: getSFProjectUserConfigDocId('project01', user1Config.ownerRef), + data: user1Config + }); + + const projectProfile = { + ...createTestProjectProfile({ + translateConfig: { + translationSuggestionsEnabled: user1Config.translationSuggestionsEnabled + }, + userRoles: { user01: SFProjectRole.ParatextTranslator } + }), + ...projectConfig + }; + + this.realtimeService.addSnapshot(SFProjectProfileDoc.COLLECTION, { + id: 'project01', + data: projectProfile + }); + } + getProjectProfileDoc(): SFProjectProfileDoc { return this.realtimeService.get(SFProjectProfileDoc.COLLECTION, 'project01'); } @@ -239,15 +517,6 @@ class TestEnvironment { ); } - isSwitchChecked(switchButton: HTMLElement): boolean { - return switchButton.classList.contains('mdc-switch--checked'); - } - - clickSwitch(element: HTMLElement): void { - const button = element.querySelector('button[role=switch]') as HTMLInputElement; - this.click(button); - } - click(element: HTMLElement): void { element.click(); flush(); @@ -271,4 +540,40 @@ class TestEnvironment { tick(); this.fixture.detectChanges(); } + + async getSuggestionsEnabledToggle(): Promise { + if (!this.loader) return null; + return await this.loader.getHarnessOrNull( + MatSlideToggleHarness.with({ selector: '#translation-suggestions-master-switch' }) + ); + } + + async getLynxMasterToggle(): Promise { + if (!this.loader) return null; + return await this.loader.getHarnessOrNull(MatSlideToggleHarness.with({ selector: '#lynx-master-switch' })); + } + + async getLynxAssessmentsToggle(): Promise { + if (!this.loader) return null; + return await this.loader.getHarnessOrNull(MatSlideToggleHarness.with({ selector: '#lynx-assessments-enabled' })); + } + + async getLynxAutoCorrectToggle(): Promise { + if (!this.loader) return null; + return await this.loader.getHarnessOrNull(MatSlideToggleHarness.with({ selector: '#lynx-autocorrect-enabled' })); + } + + async toggleSlideToggle(toggle: MatSlideToggleHarness): Promise { + await toggle.toggle(); + this.fixture.detectChanges(); + tick(); + } + + async isToggleChecked(toggle: MatSlideToggleHarness): Promise { + return await toggle.isChecked(); + } + + async isToggleDisabled(toggle: MatSlideToggleHarness): Promise { + return await toggle.isDisabled(); + } } 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 bb4d69c793..1bad962462 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 @@ -1,12 +1,14 @@ -import { Component, DestroyRef, Inject } from '@angular/core'; -import { UntypedFormControl } from '@angular/forms'; +import { Component, DestroyRef, Inject, OnInit } from '@angular/core'; +import { FormControl } from '@angular/forms'; import { MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { BehaviorSubject } from 'rxjs'; -import { debounceTime, map, skip } from 'rxjs/operators'; +import { LynxUserConfig } from 'realtime-server/lib/esm/scriptureforge/models/lynx-config'; +import { BehaviorSubject, combineLatest } from 'rxjs'; +import { debounceTime, map, skip, startWith } from 'rxjs/operators'; import { OnlineStatusService } from 'xforge-common/online-status.service'; import { quietTakeUntilDestroyed } from 'xforge-common/util/rxjs-util'; import { SFProjectProfileDoc } from '../../core/models/sf-project-profile-doc'; import { SFProjectUserConfigDoc } from '../../core/models/sf-project-user-config-doc'; + export const CONFIDENCE_THRESHOLD_TIMEOUT = 500; export interface TranslatorSettingsDialogData { @@ -18,37 +20,45 @@ export interface TranslatorSettingsDialogData { templateUrl: './translator-settings-dialog.component.html', styleUrls: ['./translator-settings-dialog.component.scss'] }) -export class TranslatorSettingsDialogComponent { - suggestionsEnabledSwitch = new UntypedFormControl({ disabled: !this.onlineStatusService.isOnline }); +export class TranslatorSettingsDialogComponent implements OnInit { + suggestionsEnabledSwitch = new FormControl({ value: false, disabled: !this.onlineStatusService.isOnline }); + lynxMasterSwitch = new FormControl(false); + lynxAssessmentsEnabled = new FormControl(false); + lynxAutoCorrectEnabled = new FormControl(false); - private readonly projectDoc: SFProjectProfileDoc; - private readonly projectUserConfigDoc: SFProjectUserConfigDoc; + private readonly projectDoc: SFProjectProfileDoc = this.data.projectDoc; + private readonly projectUserConfigDoc: SFProjectUserConfigDoc = this.data.projectUserConfigDoc; private confidenceThreshold$ = new BehaviorSubject(20); constructor( - @Inject(MAT_DIALOG_DATA) data: TranslatorSettingsDialogData, + @Inject(MAT_DIALOG_DATA) private readonly data: TranslatorSettingsDialogData, readonly onlineStatusService: OnlineStatusService, private destroyRef: DestroyRef - ) { - this.projectDoc = data.projectDoc; - this.projectUserConfigDoc = data.projectUserConfigDoc; + ) {} + ngOnInit(): void { if (this.projectUserConfigDoc.data != null) { const percent = Math.round(this.projectUserConfigDoc.data.confidenceThreshold * 100); this.confidenceThreshold$.next(percent); } this.suggestionsEnabledSwitch.setValue(this.translationSuggestionsUserEnabled); - onlineStatusService.onlineStatus$.subscribe(() => { - this.updateSwitches(); - }); - this.projectDoc.changes$.subscribe(() => { - this.updateSwitches(); - }); - this.projectUserConfigDoc.changes$.subscribe(() => { - this.updateSwitches(); + this.lynxAssessmentsEnabled.setValue(this.lynxAssessmentsUserEnabled); + this.lynxAutoCorrectEnabled.setValue(this.lynxAutoCorrectUserEnabled); + this.lynxMasterSwitch.setValue(this.lynxMasterEnabled); + + combineLatest([this.onlineStatusService.onlineStatus$, this.projectDoc.changes$.pipe(startWith(null))]) + .pipe(quietTakeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.updateTranslationSuggestionsSwitch(); + }); + + this.projectUserConfigDoc.changes$.pipe(quietTakeUntilDestroyed(this.destroyRef)).subscribe(() => { + this.updateTranslationSuggestionsSwitch(); + this.lynxAssessmentsEnabled.setValue(this.lynxAssessmentsUserEnabled, { emitEvent: false }); + this.lynxAutoCorrectEnabled.setValue(this.lynxAutoCorrectUserEnabled, { emitEvent: false }); + this.lynxMasterSwitch.setValue(this.lynxMasterEnabled, { emitEvent: false }); }); - this.updateSwitches(); this.confidenceThreshold$ .pipe( @@ -62,18 +72,6 @@ export class TranslatorSettingsDialogComponent { ); } - setTranslationSettingsEnabled(value: boolean): void { - this.projectUserConfigDoc.submitJson0Op(op => op.set(puc => puc.translationSuggestionsEnabled, value)); - } - - updateSwitches(): void { - if (this.onlineStatusService.isOnline) { - this.suggestionsEnabledSwitch.enable(); - } else { - this.suggestionsEnabledSwitch.disable(); - } - } - get translationSuggestionsDisabled(): boolean { return !this.translationSuggestionsUserEnabled || !this.onlineStatusService.isOnline; } @@ -97,4 +95,79 @@ export class TranslatorSettingsDialogComponent { set confidenceThreshold(value: number) { this.confidenceThreshold$.next(value); } + + get lynxAssessmentsUserEnabled(): boolean { + return this.projectUserConfigDoc.data?.lynxUserConfig?.assessmentsEnabled ?? false; + } + + get lynxAutoCorrectUserEnabled(): boolean { + return this.projectUserConfigDoc.data?.lynxUserConfig?.autoCorrectionsEnabled ?? false; + } + + get lynxAssessmentsProjectEnabled(): boolean { + return !!this.projectDoc.data?.lynxConfig?.assessmentsEnabled; + } + + get lynxAutoCorrectProjectEnabled(): boolean { + return !!this.projectDoc.data?.lynxConfig?.autoCorrectionsEnabled; + } + + get lynxMasterEnabled(): boolean { + return ( + (this.lynxAssessmentsProjectEnabled && this.lynxAssessmentsUserEnabled) || + (this.lynxAutoCorrectProjectEnabled && this.lynxAutoCorrectUserEnabled) + ); + } + + get showSuggestionsSettings(): boolean { + return !!this.projectDoc.data?.translateConfig.translationSuggestionsEnabled; + } + + get showLynxSettings(): boolean { + return this.lynxAssessmentsProjectEnabled || this.lynxAutoCorrectProjectEnabled; + } + + setTranslationSettingsEnabled(value: boolean): void { + this.projectUserConfigDoc.submitJson0Op(op => op.set(puc => puc.translationSuggestionsEnabled, value)); + } + + updateTranslationSuggestionsSwitch(): void { + if (this.onlineStatusService.isOnline) { + this.suggestionsEnabledSwitch.enable(); + } else { + this.suggestionsEnabledSwitch.disable(); + } + } + + setLynxAssessmentsEnabled(value: boolean): void { + this.updateLynxUserConfig({ assessmentsEnabled: value }); + } + + setLynxAutoCorrectEnabled(value: boolean): void { + this.updateLynxUserConfig({ autoCorrectionsEnabled: value }); + } + + setLynxMasterEnabled(value: boolean): void { + this.updateLynxUserConfig({ + assessmentsEnabled: value, + autoCorrectionsEnabled: value + }); + } + + private updateLynxUserConfig(updates: Partial): void { + this.projectUserConfigDoc.submitJson0Op(op => { + const currentData = this.projectUserConfigDoc.data as any; + if (currentData?.lynxUserConfig == null) { + op.set(puc => puc.lynxUserConfig, { + assessmentsEnabled: true, + autoCorrectionsEnabled: true, + ...updates + }); + } else { + for (const [key, value] of Object.entries(updates)) { + op.set(puc => puc.lynxUserConfig![key], value); + } + } + }); + } } 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 53cd1f772b..7b59f8d9ba 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 @@ -747,11 +747,14 @@ "translator_settings_dialog": { "better": "Better", "close": "Close", + "lynx_enable_assessments": "Show assessments", + "lynx_enable_autocorrect": "Allow auto-corrections", + "lynx_settings_title": "Lynx", "more": "More", "number_of_suggestions": "Number of suggestions", - "settings_not_available_offline": "These settings are not available while you are offline.", + "settings_not_available_offline": "Translation suggestion settings are not available while you are offline.", "suggestion_confidence": "Suggestion confidence", - "show_translation_suggestions": "Show translation suggestions", + "translation_suggestions_settings_title": "Translation suggestions", "translator_settings": "Translator settings" }, "supported_back_translation_languages_dialog": { diff --git a/src/SIL.XForge.Scripture/Models/LynxUserConfig.cs b/src/SIL.XForge.Scripture/Models/LynxUserConfig.cs new file mode 100644 index 0000000000..8fdaa4a63d --- /dev/null +++ b/src/SIL.XForge.Scripture/Models/LynxUserConfig.cs @@ -0,0 +1,17 @@ +namespace SIL.XForge.Scripture.Models; + +/// +/// User-specific configuration settings for Lynx writing assistance features. +/// +public class LynxUserConfig +{ + /// + /// Gets or sets whether Lynx auto-corrections (on-type edits) are enabled for this user. + /// + public bool? AutoCorrectionsEnabled { get; set; } + + /// + /// Gets or sets whether Lynx assessments (insights) are enabled for this user. + /// + public bool? AssessmentsEnabled { get; set; } +} diff --git a/src/SIL.XForge.Scripture/Models/SFProjectUserConfig.cs b/src/SIL.XForge.Scripture/Models/SFProjectUserConfig.cs index 46c546bb66..0bf3916475 100644 --- a/src/SIL.XForge.Scripture/Models/SFProjectUserConfig.cs +++ b/src/SIL.XForge.Scripture/Models/SFProjectUserConfig.cs @@ -29,6 +29,7 @@ public class SFProjectUserConfig : ProjectData public List CommentRefsRead { get; set; } = []; public List EditorTabsOpen { get; set; } = []; public LynxInsightUserData? LynxInsightState { get; set; } + public LynxUserConfig? LynxUserConfig { get; set; } public string? SelectedQuestionRef { get; set; } [Obsolete("For backwards compatibility with older frontend clients. Deprecated September 2024.")] From 07d9a11de86edda51eef6983351dc5029e4b080b Mon Sep 17 00:00:00 2001 From: siltomato Date: Mon, 8 Sep 2025 15:11:56 -0400 Subject: [PATCH 3/8] refactor and add migration test --- .../scriptureforge/models/lynx-config.ts | 5 --- .../models/lynx-insight-user-data.ts | 2 + .../sf-project-user-config-test-data.ts | 3 +- .../models/sf-project-user-config.ts | 2 - .../services/sf-project-migrations.spec.ts | 20 +++++++++ .../sf-project-user-config-migrations.spec.ts | 15 +++++++ .../sf-project-user-config-migrations.ts | 17 +------ .../sf-project-user-config-service.ts | 8 +--- .../translate/editor/editor.component.spec.ts | 10 ++--- .../app/translate/editor/editor.component.ts | 8 ++-- ...anslator-settings-dialog.component.spec.ts | 45 +++++-------------- .../translator-settings-dialog.component.ts | 26 ++++------- src/SIL.XForge.Scripture/Models/LynxConfig.cs | 4 +- .../Models/LynxInsightUserData.cs | 10 +++++ .../Models/LynxUserConfig.cs | 17 ------- .../Models/SFProjectUserConfig.cs | 1 - 16 files changed, 81 insertions(+), 112 deletions(-) delete mode 100644 src/SIL.XForge.Scripture/Models/LynxUserConfig.cs diff --git a/src/RealtimeServer/scriptureforge/models/lynx-config.ts b/src/RealtimeServer/scriptureforge/models/lynx-config.ts index 5dc6b1ea16..0880ea8ecb 100644 --- a/src/RealtimeServer/scriptureforge/models/lynx-config.ts +++ b/src/RealtimeServer/scriptureforge/models/lynx-config.ts @@ -4,8 +4,3 @@ export interface LynxConfig { punctuationCheckerEnabled: boolean; allowedCharacterCheckerEnabled: boolean; } - -export interface LynxUserConfig { - autoCorrectionsEnabled?: boolean; - assessmentsEnabled?: boolean; -} diff --git a/src/RealtimeServer/scriptureforge/models/lynx-insight-user-data.ts b/src/RealtimeServer/scriptureforge/models/lynx-insight-user-data.ts index fed2872c11..d3cff3d02a 100644 --- a/src/RealtimeServer/scriptureforge/models/lynx-insight-user-data.ts +++ b/src/RealtimeServer/scriptureforge/models/lynx-insight-user-data.ts @@ -1,6 +1,8 @@ import { LynxInsightFilter, LynxInsightSortOrder } from './lynx-insight'; export interface LynxInsightUserData { + autoCorrectionsEnabled?: boolean; + assessmentsEnabled?: boolean; panelData?: LynxInsightPanelUserData; } diff --git a/src/RealtimeServer/scriptureforge/models/sf-project-user-config-test-data.ts b/src/RealtimeServer/scriptureforge/models/sf-project-user-config-test-data.ts index 79b7cb206b..3ed892f182 100644 --- a/src/RealtimeServer/scriptureforge/models/sf-project-user-config-test-data.ts +++ b/src/RealtimeServer/scriptureforge/models/sf-project-user-config-test-data.ts @@ -18,8 +18,7 @@ export function createTestProjectUserConfig(overrides?: RecursivePartial { }); }); + describe('version 25', () => { + it('adds lynxConfig with default values', async () => { + const env = new TestEnvironment(24); + const conn = env.server.connect(); + await createDoc(conn, SF_PROJECTS_COLLECTION, 'project01', {}); + let projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01'); + expect(projectDoc.data.lynxConfig).toBeUndefined(); + + await env.server.migrateIfNecessary(); + + projectDoc = await fetchDoc(conn, SF_PROJECTS_COLLECTION, 'project01'); + expect(projectDoc.data.lynxConfig).toEqual({ + autoCorrectionsEnabled: false, + assessmentsEnabled: false, + punctuationCheckerEnabled: false, + allowedCharacterCheckerEnabled: false + }); + }); + }); + describe('version 26', () => { it('removes lastSelectedTrainingBooks from draftConfig', async () => { const env = new TestEnvironment(25); diff --git a/src/RealtimeServer/scriptureforge/services/sf-project-user-config-migrations.spec.ts b/src/RealtimeServer/scriptureforge/services/sf-project-user-config-migrations.spec.ts index aefbf21d31..a4119aa9b8 100644 --- a/src/RealtimeServer/scriptureforge/services/sf-project-user-config-migrations.spec.ts +++ b/src/RealtimeServer/scriptureforge/services/sf-project-user-config-migrations.spec.ts @@ -118,6 +118,21 @@ describe('SFProjectUserConfigMigrations', () => { expect(userConfigDoc.version).toBe(1); }); }); + + describe('version 8', () => { + it('adds lynxInsightState property', async () => { + const env = new TestEnvironment(7); + const conn = env.server.connect(); + await createDoc(conn, SF_PROJECT_USER_CONFIGS_COLLECTION, 'project01:user01', {}); + let userConfigDoc = await fetchDoc(conn, SF_PROJECT_USER_CONFIGS_COLLECTION, 'project01:user01'); + expect(userConfigDoc.data.lynxInsightState).not.toBeDefined(); + + await env.server.migrateIfNecessary(); + + userConfigDoc = await fetchDoc(conn, SF_PROJECT_USER_CONFIGS_COLLECTION, 'project01:user01'); + expect(userConfigDoc.data.lynxInsightState).toEqual({}); + }); + }); }); class TestEnvironment { diff --git a/src/RealtimeServer/scriptureforge/services/sf-project-user-config-migrations.ts b/src/RealtimeServer/scriptureforge/services/sf-project-user-config-migrations.ts index 5c7678fc39..523230b603 100644 --- a/src/RealtimeServer/scriptureforge/services/sf-project-user-config-migrations.ts +++ b/src/RealtimeServer/scriptureforge/services/sf-project-user-config-migrations.ts @@ -96,20 +96,6 @@ class SFProjectUserConfigMigration8 extends DocMigration { } } -class SFProjectUserConfigMigration9 extends DocMigration { - static readonly VERSION = 9; - - async migrateDoc(doc: Doc): Promise { - if (doc.data.lynxUserConfig === undefined) { - const op: ObjectInsertOp = { - p: ['lynxUserConfig'], - oi: { assessmentsEnabled: true, autoCorrectionsEnabled: true } - }; - await submitMigrationOp(SFProjectUserConfigMigration9.VERSION, doc, [op]); - } - } -} - export const SF_PROJECT_USER_CONFIG_MIGRATIONS: MigrationConstructor[] = monotonicallyIncreasingMigrationList([ SFProjectUserConfigMigration1, SFProjectUserConfigMigration2, @@ -118,6 +104,5 @@ export const SF_PROJECT_USER_CONFIG_MIGRATIONS: MigrationConstructor[] = monoton SFProjectUserConfigMigration5, SFProjectUserConfigMigration6, SFProjectUserConfigMigration7, - SFProjectUserConfigMigration8, - SFProjectUserConfigMigration9 + SFProjectUserConfigMigration8 ]); 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 64905522f9..f319602f60 100644 --- a/src/RealtimeServer/scriptureforge/services/sf-project-user-config-service.ts +++ b/src/RealtimeServer/scriptureforge/services/sf-project-user-config-service.ts @@ -142,13 +142,7 @@ export class SFProjectUserConfigService extends SFProjectDataService { env.setProjectUserConfig({ selectedBookNum: 42, selectedChapterNum: 2, - lynxUserConfig: { + lynxInsightState: { autoCorrectionsEnabled: true, assessmentsEnabled: true } @@ -4444,7 +4444,7 @@ describe('EditorComponent', () => { env.setProjectUserConfig({ selectedBookNum: 42, selectedChapterNum: 2, - lynxUserConfig: { + lynxInsightState: { autoCorrectionsEnabled: false, assessmentsEnabled: false } @@ -4475,7 +4475,7 @@ describe('EditorComponent', () => { env.setProjectUserConfig({ selectedBookNum: 42, selectedChapterNum: 2, - lynxUserConfig: { + lynxInsightState: { autoCorrectionsEnabled: true, assessmentsEnabled: true } @@ -4506,7 +4506,7 @@ describe('EditorComponent', () => { env.setProjectUserConfig({ selectedBookNum: 42, selectedChapterNum: 2, - lynxUserConfig: { + lynxInsightState: { autoCorrectionsEnabled: false, assessmentsEnabled: true } @@ -4537,7 +4537,7 @@ describe('EditorComponent', () => { env.setProjectUserConfig({ selectedBookNum: 42, selectedChapterNum: 2, - lynxUserConfig: { + lynxInsightState: { autoCorrectionsEnabled: true, assessmentsEnabled: 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 e0a5c5714c..d9eae5236b 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 @@ -2608,20 +2608,20 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy, this.activatedProject.changes$.pipe(map(projectDoc => projectDoc?.data?.lynxConfig)), projectUserConfigDoc?.changes$.pipe( startWith(projectUserConfigDoc), - map(() => projectUserConfigDoc?.data?.lynxUserConfig) + map(() => projectUserConfigDoc?.data?.lynxInsightState) ) ?? of(undefined) ]) .pipe(quietTakeUntilDestroyed(this.destroyRef)) - .subscribe(([hasEditPermission, lynxProjectConfig, lynxUserConfig]) => { + .subscribe(([hasEditPermission, lynxProjectConfig, lynxInsightState]) => { const canEdit: boolean = hasEditPermission && this.isUsfmValid; // Enable lynx features only if user can edit chapter and lynx is enabled in both project AND user settings this.lynxInsightsEnabled = - canEdit && (lynxProjectConfig?.assessmentsEnabled ?? false) && (lynxUserConfig?.assessmentsEnabled ?? true); + canEdit && (lynxProjectConfig?.assessmentsEnabled ?? false) && (lynxInsightState?.assessmentsEnabled ?? true); this.lynxAutoCorrectionsEnabled = canEdit && (lynxProjectConfig?.autoCorrectionsEnabled ?? false) && - (lynxUserConfig?.autoCorrectionsEnabled ?? true); + (lynxInsightState?.autoCorrectionsEnabled ?? true); }); } } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.spec.ts index 6dbbf1566d..a1cdf819f4 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/translator-settings-dialog.component.spec.ts @@ -219,7 +219,7 @@ describe('TranslatorSettingsDialogComponent', () => { env.closeDialog(); })); - it('should show only auto-correct switch when only auto-correct is enabled in project', fakeAsync(() => { + it('should show only auto-corrections switch when only auto-corrections is enabled in project', fakeAsync(() => { const env = new TestEnvironment(); env.setupProject({ projectConfig: { @@ -243,6 +243,12 @@ describe('TranslatorSettingsDialogComponent', () => { it('should update user lynx master setting when toggled', fakeAsync(async () => { const env = new TestEnvironment(); env.setupProject({ + userConfig: { + lynxInsightState: { + assessmentsEnabled: true, + autoCorrectionsEnabled: true + } + }, projectConfig: { lynxConfig: { autoCorrectionsEnabled: true, @@ -256,43 +262,16 @@ describe('TranslatorSettingsDialogComponent', () => { const lynxMasterToggle = await env.getLynxMasterToggle(); expect(lynxMasterToggle).not.toBeNull(); - expect(env.component!.lynxMasterSwitch.value).toBe(false); - expect(await env.isToggleChecked(lynxMasterToggle!)).toBe(false); - - await env.toggleSlideToggle(lynxMasterToggle!); expect(env.component!.lynxMasterSwitch.value).toBe(true); expect(await env.isToggleChecked(lynxMasterToggle!)).toBe(true); - const userConfigDoc = env.getProjectUserConfigDoc(); - expect(userConfigDoc.data!.lynxUserConfig?.autoCorrectionsEnabled).toBe(true); - expect(userConfigDoc.data!.lynxUserConfig?.assessmentsEnabled).toBe(true); - env.closeDialog(); - })); - - it('should update user lynx assessments setting when toggled', fakeAsync(async () => { - const env = new TestEnvironment(); - env.setupProject({ - projectConfig: { - lynxConfig: { - autoCorrectionsEnabled: true, - assessmentsEnabled: true, - punctuationCheckerEnabled: false, - allowedCharacterCheckerEnabled: false - } - } - }); - env.openDialog(); - - const lynxAssessmentsToggle = await env.getLynxAssessmentsToggle(); - expect(env.component!.lynxAssessmentsEnabled.value).toBe(false); - expect(await env.isToggleChecked(lynxAssessmentsToggle!)).toBe(false); - - await env.toggleSlideToggle(lynxAssessmentsToggle!); - expect(env.component!.lynxAssessmentsEnabled.value).toBe(true); - expect(await env.isToggleChecked(lynxAssessmentsToggle!)).toBe(true); + await env.toggleSlideToggle(lynxMasterToggle!); + expect(env.component!.lynxMasterSwitch.value).toBe(false); + expect(await env.isToggleChecked(lynxMasterToggle!)).toBe(false); const userConfigDoc = env.getProjectUserConfigDoc(); - expect(userConfigDoc.data!.lynxUserConfig?.assessmentsEnabled).toBe(true); + expect(userConfigDoc.data!.lynxInsightState?.autoCorrectionsEnabled).toBe(false); + expect(userConfigDoc.data!.lynxInsightState?.assessmentsEnabled).toBe(false); env.closeDialog(); })); 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 1bad962462..be242935a2 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 @@ -1,7 +1,6 @@ import { Component, DestroyRef, Inject, OnInit } from '@angular/core'; import { FormControl } from '@angular/forms'; import { MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { LynxUserConfig } from 'realtime-server/lib/esm/scriptureforge/models/lynx-config'; import { BehaviorSubject, combineLatest } from 'rxjs'; import { debounceTime, map, skip, startWith } from 'rxjs/operators'; import { OnlineStatusService } from 'xforge-common/online-status.service'; @@ -97,11 +96,11 @@ export class TranslatorSettingsDialogComponent implements OnInit { } get lynxAssessmentsUserEnabled(): boolean { - return this.projectUserConfigDoc.data?.lynxUserConfig?.assessmentsEnabled ?? false; + return this.projectUserConfigDoc.data?.lynxInsightState?.assessmentsEnabled ?? true; } get lynxAutoCorrectUserEnabled(): boolean { - return this.projectUserConfigDoc.data?.lynxUserConfig?.autoCorrectionsEnabled ?? false; + return this.projectUserConfigDoc.data?.lynxInsightState?.autoCorrectionsEnabled ?? true; } get lynxAssessmentsProjectEnabled(): boolean { @@ -140,33 +139,24 @@ export class TranslatorSettingsDialogComponent implements OnInit { } setLynxAssessmentsEnabled(value: boolean): void { - this.updateLynxUserConfig({ assessmentsEnabled: value }); + this.updateLynxInsightState({ assessmentsEnabled: value }); } setLynxAutoCorrectEnabled(value: boolean): void { - this.updateLynxUserConfig({ autoCorrectionsEnabled: value }); + this.updateLynxInsightState({ autoCorrectionsEnabled: value }); } setLynxMasterEnabled(value: boolean): void { - this.updateLynxUserConfig({ + this.updateLynxInsightState({ assessmentsEnabled: value, autoCorrectionsEnabled: value }); } - private updateLynxUserConfig(updates: Partial): void { + private updateLynxInsightState(updates: { assessmentsEnabled?: boolean; autoCorrectionsEnabled?: boolean }): void { this.projectUserConfigDoc.submitJson0Op(op => { - const currentData = this.projectUserConfigDoc.data as any; - if (currentData?.lynxUserConfig == null) { - op.set(puc => puc.lynxUserConfig, { - assessmentsEnabled: true, - autoCorrectionsEnabled: true, - ...updates - }); - } else { - for (const [key, value] of Object.entries(updates)) { - op.set(puc => puc.lynxUserConfig![key], value); - } + for (const [key, value] of Object.entries(updates)) { + op.set(puc => puc.lynxInsightState![key], value); } }); } diff --git a/src/SIL.XForge.Scripture/Models/LynxConfig.cs b/src/SIL.XForge.Scripture/Models/LynxConfig.cs index 6bb028be17..39f02e4d84 100644 --- a/src/SIL.XForge.Scripture/Models/LynxConfig.cs +++ b/src/SIL.XForge.Scripture/Models/LynxConfig.cs @@ -18,10 +18,10 @@ public class LynxConfig /// /// Gets or sets whether Lynx punctuation checking is enabled (subset of assessments). /// - public bool PunctuationCheckerEnabled { get; set; } = true; + public bool PunctuationCheckerEnabled { get; set; } /// /// Gets or sets whether Lynx allowed character checking is enabled (subset of assessments). /// - public bool AllowedCharacterCheckerEnabled { get; set; } = false; + public bool AllowedCharacterCheckerEnabled { get; set; } } diff --git a/src/SIL.XForge.Scripture/Models/LynxInsightUserData.cs b/src/SIL.XForge.Scripture/Models/LynxInsightUserData.cs index a1d75a70a0..14c6745523 100644 --- a/src/SIL.XForge.Scripture/Models/LynxInsightUserData.cs +++ b/src/SIL.XForge.Scripture/Models/LynxInsightUserData.cs @@ -8,6 +8,16 @@ namespace SIL.XForge.Scripture.Models; public class LynxInsightUserData { public LynxInsightPanelUserData? PanelData { get; set; } + + /// + /// Gets or sets whether Lynx auto-corrections (on-type edits) are enabled for this user. + /// + public bool? AutoCorrectionsEnabled { get; set; } + + /// + /// Gets or sets whether Lynx assessments (insights) are enabled for this user. + /// + public bool? AssessmentsEnabled { get; set; } } public class LynxInsightPanelUserData diff --git a/src/SIL.XForge.Scripture/Models/LynxUserConfig.cs b/src/SIL.XForge.Scripture/Models/LynxUserConfig.cs deleted file mode 100644 index 8fdaa4a63d..0000000000 --- a/src/SIL.XForge.Scripture/Models/LynxUserConfig.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace SIL.XForge.Scripture.Models; - -/// -/// User-specific configuration settings for Lynx writing assistance features. -/// -public class LynxUserConfig -{ - /// - /// Gets or sets whether Lynx auto-corrections (on-type edits) are enabled for this user. - /// - public bool? AutoCorrectionsEnabled { get; set; } - - /// - /// Gets or sets whether Lynx assessments (insights) are enabled for this user. - /// - public bool? AssessmentsEnabled { get; set; } -} diff --git a/src/SIL.XForge.Scripture/Models/SFProjectUserConfig.cs b/src/SIL.XForge.Scripture/Models/SFProjectUserConfig.cs index 0bf3916475..46c546bb66 100644 --- a/src/SIL.XForge.Scripture/Models/SFProjectUserConfig.cs +++ b/src/SIL.XForge.Scripture/Models/SFProjectUserConfig.cs @@ -29,7 +29,6 @@ public class SFProjectUserConfig : ProjectData public List CommentRefsRead { get; set; } = []; public List EditorTabsOpen { get; set; } = []; public LynxInsightUserData? LynxInsightState { get; set; } - public LynxUserConfig? LynxUserConfig { get; set; } public string? SelectedQuestionRef { get; set; } [Obsolete("For backwards compatibility with older frontend clients. Deprecated September 2024.")] From 0576663fc7f3cce54948ab3378f844bd8762d1e1 Mon Sep 17 00:00:00 2001 From: siltomato Date: Mon, 8 Sep 2025 15:28:16 -0400 Subject: [PATCH 4/8] ensure translation settings button shows when no source if lynx enabled --- .../translate/editor/editor.component.spec.ts | 21 +++++++++++++++++++ .../app/translate/editor/editor.component.ts | 7 +------ 2 files changed, 22 insertions(+), 6 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 be94dfeb6e..6ce9b82ba5 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 @@ -3630,6 +3630,27 @@ describe('EditorComponent', () => { 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 + } + }; + 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.setProjectUserConfig(); + env.routeWithParams(navigationParams); + env.wait(); + expect(env.translatorSettingsButton).toBeTruthy(); + env.dispose(); + })); }); describe('Translation Suggestions disabled', () => { 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 d9eae5236b..9548e4c197 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 @@ -385,12 +385,7 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy, } get translatorSettingsEnabled(): boolean { - return ( - this.hasSource && - this.hasSourceViewRight && - (this.translationSuggestionsProjectEnabled || this.lynxProjectEnabled) && - this.userHasGeneralEditRight - ); + return (this.translationSuggestionsProjectEnabled || this.lynxProjectEnabled) && this.userHasGeneralEditRight; } get numSuggestions(): number { From 909fca4aae790e2e02514297e176a0da4b6dfad1 Mon Sep 17 00:00:00 2001 From: siltomato Date: Tue, 9 Sep 2025 13:22:12 -0400 Subject: [PATCH 5/8] added explicit 'void' and adjusted translatorSettingsEnabled condition --- .../src/app/translate/editor/editor.component.ts | 6 +++++- .../editor/translator-settings-dialog.component.ts | 13 ++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) 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 9548e4c197..8ae52a7f70 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 @@ -385,7 +385,11 @@ export class EditorComponent extends DataLoadingComponent implements OnDestroy, } get translatorSettingsEnabled(): boolean { - return (this.translationSuggestionsProjectEnabled || this.lynxProjectEnabled) && this.userHasGeneralEditRight; + return ( + ((this.hasSource && this.hasSourceViewRight && this.translationSuggestionsProjectEnabled) || + this.lynxProjectEnabled) && + this.userHasGeneralEditRight + ); } get numSuggestions(): number { 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 be242935a2..23da4a7697 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 @@ -66,8 +66,9 @@ export class TranslatorSettingsDialogComponent implements OnInit { map(value => value / 100), quietTakeUntilDestroyed(this.destroyRef) ) - .subscribe(threshold => - this.projectUserConfigDoc.submitJson0Op(op => op.set(puc => puc.confidenceThreshold, threshold)) + .subscribe( + threshold => + void this.projectUserConfigDoc.submitJson0Op(op => op.set(puc => puc.confidenceThreshold, threshold)) ); } @@ -84,7 +85,7 @@ export class TranslatorSettingsDialogComponent implements OnInit { } set numSuggestions(value: string) { - this.projectUserConfigDoc.submitJson0Op(op => op.set(puc => puc.numSuggestions, parseInt(value, 10))); + void this.projectUserConfigDoc.submitJson0Op(op => op.set(puc => puc.numSuggestions, parseInt(value, 10))); } get confidenceThreshold(): number { @@ -127,7 +128,9 @@ export class TranslatorSettingsDialogComponent implements OnInit { } setTranslationSettingsEnabled(value: boolean): void { - this.projectUserConfigDoc.submitJson0Op(op => op.set(puc => puc.translationSuggestionsEnabled, value)); + void this.projectUserConfigDoc.submitJson0Op(op => + op.set(puc => puc.translationSuggestionsEnabled, value) + ); } updateTranslationSuggestionsSwitch(): void { @@ -154,7 +157,7 @@ export class TranslatorSettingsDialogComponent implements OnInit { } private updateLynxInsightState(updates: { assessmentsEnabled?: boolean; autoCorrectionsEnabled?: boolean }): void { - this.projectUserConfigDoc.submitJson0Op(op => { + void this.projectUserConfigDoc.submitJson0Op(op => { for (const [key, value] of Object.entries(updates)) { op.set(puc => puc.lynxInsightState![key], value); } From 3c201354f2a37e9a3b533de09f2d80c3f40194e8 Mon Sep 17 00:00:00 2001 From: siltomato Date: Wed, 10 Sep 2025 12:59:44 -0400 Subject: [PATCH 6/8] ensure blots are cleared and overlays closed when lynx is disabled --- ...x-insight-editor-objects.component.spec.ts | 66 +++++++++++++++++++ .../lynx-insight-editor-objects.component.ts | 4 ++ .../insights/lynx-insight-state.service.ts | 5 +- 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-editor-objects/lynx-insight-editor-objects.component.spec.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-editor-objects/lynx-insight-editor-objects.component.spec.ts index 6412984e72..a27d124462 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-editor-objects/lynx-insight-editor-objects.component.spec.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-editor-objects/lynx-insight-editor-objects.component.spec.ts @@ -62,6 +62,63 @@ describe('LynxInsightEditorObjectsComponent', () => { verify(mockInsightRenderService.render(anything(), anything())).once(); })); + it('should clear formatting and overlays when insights are disabled', fakeAsync(() => { + const env = new TestEnvironment(); + const testInsight = env.createTestInsight(); + + // Setup initial state with insights enabled + env.setEditorReady(true); + env.setFilteredInsights([testInsight]); + tick(); + flush(); + + // Verify initial render occurred + verify(mockInsightRenderService.render(anything(), anything())).atLeast(1); + + // Disable insights + env.setInsightsEnabled(false); + tick(); + flush(); + + // Verify cleanup methods were called + verify(mockInsightRenderService.removeAllInsightFormatting(anything())).atLeast(1); + verify(mockOverlayService.close()).atLeast(1); + verify(mockInsightStateService.clearDisplayState()).atLeast(1); + })); + + it('should not render insights when initially disabled', fakeAsync(() => { + const env = new TestEnvironment({ insightsEnabled: false }); + + env.setEditorReady(true); + env.setFilteredInsights([env.createTestInsight()]); + tick(); + flush(); + + // Verify render was not called when insights are disabled + verify(mockInsightRenderService.render(anything(), anything())).never(); + })); + + it('should resume rendering when insights are re-enabled', fakeAsync(() => { + const env = new TestEnvironment({ insightsEnabled: false }); + const testInsight = env.createTestInsight(); + + env.setEditorReady(true); + env.setFilteredInsights([testInsight]); + tick(); + flush(); + + // Verify no rendering when disabled + verify(mockInsightRenderService.render(anything(), anything())).never(); + + // Re-enable insights + env.setInsightsEnabled(true); + tick(); + flush(); + + // Verify rendering resumes + verify(mockInsightRenderService.render(anything(), anything())).once(); + })); + it('should close overlays when editor becomes ready', fakeAsync(() => { const env = new TestEnvironment({ initialEditorReady: false }); @@ -250,6 +307,7 @@ class HostComponent { interface TestEnvArgs { initialEditorReady?: boolean; + insightsEnabled?: boolean; } class TestEnvironment { @@ -265,6 +323,7 @@ class TestEnvironment { constructor(args: TestEnvArgs = {}) { const textModelConverter = instance(mockTextModelConverter); const initialEditorReady = args.initialEditorReady ?? true; + const insightsEnabled = args.insightsEnabled ?? true; this.editorReadySubject = new BehaviorSubject(initialEditorReady); this.filteredInsightsSubject = new BehaviorSubject([]); @@ -303,6 +362,7 @@ class TestEnvironment { when(mockInsightStateService.filteredChapterInsights$).thenReturn(this.filteredInsightsSubject); when(mockInsightStateService.displayState$).thenReturn(this.displayStateSubject); when(mockInsightStateService.updateDisplayState(anything())).thenReturn(); + when(mockInsightStateService.clearDisplayState()).thenReturn(); when(mockInsightRenderService.render(anything(), anything())).thenResolve(); when(mockInsightRenderService.renderActionOverlay(anything(), anything(), anything(), anything())).thenResolve(); when(mockInsightRenderService.renderCursorActiveState(anything(), anything())).thenResolve(); @@ -321,6 +381,7 @@ class TestEnvironment { // Set the inputs before calling detectChanges to ensure they're available during ngOnInit this.hostComponent.editor = actualEditor; this.hostComponent.textModelConverter = textModelConverter; + this.hostComponent.insightsEnabled = insightsEnabled; this.fixture.detectChanges(); this.component = this.hostComponent.component; @@ -330,6 +391,11 @@ class TestEnvironment { this.editorReadySubject.next(ready); } + setInsightsEnabled(enabled: boolean): void { + this.hostComponent.insightsEnabled = enabled; + this.fixture.detectChanges(); + } + setFilteredInsights(insights: LynxInsight[]): void { this.filteredInsightsSubject.next(insights); } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-editor-objects/lynx-insight-editor-objects.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-editor-objects/lynx-insight-editor-objects.component.ts index 3544000004..5fea59b2a0 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-editor-objects/lynx-insight-editor-objects.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-editor-objects/lynx-insight-editor-objects.component.ts @@ -127,6 +127,10 @@ export class LynxInsightEditorObjectsComponent implements OnChanges, OnInit, OnD distinctUntilChanged(), switchMap(show => { if (!show) { + // Clear any existing blots and overlays when insights are disabled + this.insightRenderService.removeAllInsightFormatting(this.editor!); + this.overlayService.close(); + this.insightState.clearDisplayState(); return EMPTY; } diff --git a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-state.service.ts b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-state.service.ts index 93e102b7c6..ec02686f65 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-state.service.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/app/translate/editor/lynx/insights/lynx-insight-state.service.ts @@ -207,7 +207,10 @@ export class LynxInsightStateService { } clearDisplayState(): void { - this.displayStateSource$.next({ activeInsightIds: [], cursorActiveInsightIds: [] }); + this.displayStateSource$.next({ + activeInsightIds: [], + cursorActiveInsightIds: [] + }); } togglePanelVisibility(): void { From 9fab53e36e38409d02382e9b6454b6d11c6e05f5 Mon Sep 17 00:00:00 2001 From: siltomato Date: Wed, 10 Sep 2025 13:07:22 -0400 Subject: [PATCH 7/8] 'lynx' to 'insights' in translation file --- .../editor/translator-settings-dialog.component.html | 6 +++--- .../ClientApp/src/assets/i18n/non_checking_en.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) 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 e2c86a87fb..bb60352c2b 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 @@ -68,7 +68,7 @@

{{ t("translator_settings") }}

[formControl]="lynxMasterSwitch" (change)="setLynxMasterEnabled($event.checked)" > - {{ t("lynx_settings_title") }} + {{ t("insights_settings_title") }} @@ -79,7 +79,7 @@

{{ t("translator_settings") }}

[formControl]="lynxAssessmentsEnabled" (change)="setLynxAssessmentsEnabled($event.checked)" > - {{ t("lynx_enable_assessments") }} + {{ t("insights_enable_assessments") }} } @if (lynxAutoCorrectProjectEnabled) { @@ -88,7 +88,7 @@

{{ t("translator_settings") }}

[formControl]="lynxAutoCorrectEnabled" (change)="setLynxAutoCorrectEnabled($event.checked)" > - {{ t("lynx_enable_autocorrect") }} + {{ t("insights_enable_autocorrect") }} } 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 7b59f8d9ba..8e199a4ca6 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 @@ -747,9 +747,9 @@ "translator_settings_dialog": { "better": "Better", "close": "Close", - "lynx_enable_assessments": "Show assessments", - "lynx_enable_autocorrect": "Allow auto-corrections", - "lynx_settings_title": "Lynx", + "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.", From 8bdebb1501446a2b60d9c128d2d337175dff3b9d Mon Sep 17 00:00:00 2001 From: siltomato Date: Fri, 12 Sep 2025 18:11:25 -0400 Subject: [PATCH 8/8] fix reference error for newly connected projects --- .../editor/translator-settings-dialog.component.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 23da4a7697..2353556ec4 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 @@ -158,8 +158,12 @@ export class TranslatorSettingsDialogComponent implements OnInit { private updateLynxInsightState(updates: { assessmentsEnabled?: boolean; autoCorrectionsEnabled?: boolean }): void { void this.projectUserConfigDoc.submitJson0Op(op => { + if (this.projectUserConfigDoc.data?.lynxInsightState == null) { + op.set(puc => puc.lynxInsightState, {}); + } + for (const [key, value] of Object.entries(updates)) { - op.set(puc => puc.lynxInsightState![key], value); + op.set(puc => puc.lynxInsightState[key], value); } }); }