Skip to content

Commit 65c2e2c

Browse files
authored
SF-3271 Prevent Save & Sync of draft sources when offline (#3148)
1 parent a0fad2f commit 65c2e2c

File tree

6 files changed

+96
-4
lines changed

6 files changed

+96
-4
lines changed

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-sources/draft-sources.component.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,14 @@ <h3>{{ t("overview_source") }} {{ parentheses(sourceLanguageDisplayName) }}</h3>
179179
></app-language-codes-confirmation>
180180

181181
<div class="component-footer">
182+
@if (!appOnline) {
183+
<mat-error id="offline-message">
184+
{{ t("offline_message") }}
185+
</mat-error>
186+
}
182187
<div class="page-actions">
183188
<button mat-button (click)="cancel()"><mat-icon>close</mat-icon>{{ t("cancel") }}</button>
184-
<button mat-flat-button color="primary" (click)="save()">
189+
<button id="save_button" mat-flat-button color="primary" (click)="save()" [disabled]="!appOnline">
185190
<mat-icon>check</mat-icon>{{ t("save_and_sync") }}
186191
</button>
187192
</div>

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-sources/draft-sources.component.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,13 @@ strong {
172172
background: var(--sf-draft-source-active-blank-background);
173173
}
174174

175+
#offline-message {
176+
display: flex;
177+
justify-self: flex-end;
178+
gap: 0.5em;
179+
margin-bottom: 0.5em;
180+
}
181+
175182
.page-actions {
176183
display: flex;
177184
gap: 0.5em;

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-sources/draft-sources.component.spec.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { AuthService } from 'xforge-common/auth.service';
1313
import { createTestFeatureFlag, FeatureFlagService } from 'xforge-common/feature-flags/feature-flag.service';
1414
import { I18nService } from 'xforge-common/i18n.service';
1515
import { NoticeService } from 'xforge-common/notice.service';
16+
import { OnlineStatusService } from 'xforge-common/online-status.service';
17+
import { TestOnlineStatusModule } from 'xforge-common/test-online-status.module';
18+
import { TestOnlineStatusService } from 'xforge-common/test-online-status.service';
1619
import { TestRealtimeModule } from 'xforge-common/test-realtime.module';
1720
import { TestRealtimeService } from 'xforge-common/test-realtime.service';
1821
import { configureTestingModule, TestTranslocoModule } from 'xforge-common/test-utils';
@@ -52,7 +55,12 @@ const mockedAuthService = mock(AuthService);
5255

5356
describe('DraftSourcesComponent', () => {
5457
configureTestingModule(() => ({
55-
imports: [TestRealtimeModule.forRoot(SF_TYPE_REGISTRY), NoopAnimationsModule, TestTranslocoModule],
58+
imports: [
59+
TestOnlineStatusModule.forRoot(),
60+
TestRealtimeModule.forRoot(SF_TYPE_REGISTRY),
61+
NoopAnimationsModule,
62+
TestTranslocoModule
63+
],
5664
declarations: [],
5765
providers: [
5866
{ provide: ParatextService, useMock: mockedParatextService },
@@ -63,7 +71,8 @@ describe('DraftSourcesComponent', () => {
6371
{ provide: DraftSourcesService, useMock: mockedDraftSourcesService },
6472
{ provide: SFUserProjectsService, useMock: mockedSFUserProjectsService },
6573
{ provide: FeatureFlagService, useMock: mockedFeatureFlagService },
66-
{ provide: AuthService, useMock: mockedAuthService }
74+
{ provide: AuthService, useMock: mockedAuthService },
75+
{ provide: OnlineStatusService, useClass: TestOnlineStatusService }
6776
]
6877
}));
6978

@@ -382,6 +391,32 @@ describe('DraftSourcesComponent', () => {
382391
alternateTrainingSourceParatextId: 'unset'
383392
});
384393
});
394+
395+
it('should disable save and sync button and display offline message when offline', fakeAsync(() => {
396+
const env = new TestEnvironment();
397+
env.testOnlineStatusService.setIsOnline(false);
398+
env.fixture.detectChanges();
399+
tick();
400+
401+
const offlineMessage: DebugElement = env.fixture.debugElement.query(By.css('#offline-message'));
402+
expect(offlineMessage).not.toBeNull();
403+
404+
const saveButton: DebugElement = env.fixture.debugElement.query(By.css('#save_button'));
405+
expect(saveButton.attributes.disabled).toBe('true');
406+
}));
407+
408+
it('should enable save & sync button and not display offline message when online', fakeAsync(() => {
409+
const env = new TestEnvironment();
410+
env.testOnlineStatusService.setIsOnline(true);
411+
env.fixture.detectChanges();
412+
tick();
413+
414+
const offlineMessage: DebugElement = env.fixture.debugElement.query(By.css('#offline-message'));
415+
expect(offlineMessage).toBeNull();
416+
417+
const saveButton: DebugElement = env.fixture.debugElement.query(By.css('#save_button'));
418+
expect(saveButton.attributes.disabled).toBeUndefined();
419+
}));
385420
});
386421
});
387422

@@ -390,6 +425,9 @@ class TestEnvironment {
390425
readonly fixture: ComponentFixture<DraftSourcesComponent>;
391426
readonly realtimeService: TestRealtimeService;
392427
readonly activatedProjectDoc: WithData<SFProjectDoc>;
428+
readonly testOnlineStatusService: TestOnlineStatusService = TestBed.inject(
429+
OnlineStatusService
430+
) as TestOnlineStatusService;
393431

394432
constructor() {
395433
const userSFProjectsAndResourcesCount: number = 6;

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-sources/draft-sources.component.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
44
import { MatCardModule } from '@angular/material/card';
55
import { MatCheckboxModule } from '@angular/material/checkbox';
66
import { MatRippleModule } from '@angular/material/core';
7+
import { MatFormFieldModule } from '@angular/material/form-field';
78
import { MatIconModule } from '@angular/material/icon';
89
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
910
import { Router } from '@angular/router';
@@ -16,6 +17,7 @@ import { FeatureFlagService } from 'xforge-common/feature-flags/feature-flag.ser
1617
import { I18nKeyForComponent, I18nService } from 'xforge-common/i18n.service';
1718
import { ElementState } from 'xforge-common/models/element-state';
1819
import { NoticeService } from 'xforge-common/notice.service';
20+
import { OnlineStatusService } from 'xforge-common/online-status.service';
1921
import { SFUserProjectsService } from 'xforge-common/user-projects.service';
2022
import { quietTakeUntilDestroyed } from 'xforge-common/util/rxjs-util';
2123
import { XForgeCommonModule } from 'xforge-common/xforge-common.module';
@@ -45,6 +47,7 @@ export interface ProjectStatus {
4547
standalone: true,
4648
imports: [
4749
MatButtonModule,
50+
MatFormFieldModule,
4851
MatIconModule,
4952
XForgeCommonModule,
5053
MatRippleModule,
@@ -98,6 +101,7 @@ export class DraftSourcesComponent extends DataLoadingComponent {
98101
private readonly userProjectsService: SFUserProjectsService,
99102
private readonly router: Router,
100103
private readonly featureFlags: FeatureFlagService,
104+
private readonly onlineStatus: OnlineStatusService,
101105
readonly i18n: I18nService,
102106
noticeService: NoticeService
103107
) {
@@ -132,6 +136,10 @@ export class DraftSourcesComponent extends DataLoadingComponent {
132136
this.loadProjects();
133137
}
134138

139+
get appOnline(): boolean {
140+
return this.onlineStatus.isOnline;
141+
}
142+
135143
get loading(): boolean {
136144
return !this.isLoaded;
137145
}

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-sources/draft-sources.stories.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ActivatedProjectService } from 'xforge-common/activated-project.service
1111
import { AuthService } from 'xforge-common/auth.service';
1212
import { DialogService } from 'xforge-common/dialog.service';
1313
import { createTestFeatureFlag, FeatureFlagService } from 'xforge-common/feature-flags/feature-flag.service';
14+
import { OnlineStatusService } from 'xforge-common/online-status.service';
1415
import { SFUserProjectsService } from 'xforge-common/user-projects.service';
1516
import { ParatextProject } from '../../../core/models/paratext-project';
1617
import { SFProjectProfileDoc } from '../../../core/models/sf-project-profile-doc';
@@ -26,6 +27,7 @@ const mockedUserProjectsService = mock(SFUserProjectsService);
2627
const mockedRouter = mock(Router);
2728
const mockedFeatureFlags = mock(FeatureFlagService);
2829
const mockedAuthService = mock(AuthService);
30+
const mockedOnlineStatusService = mock(OnlineStatusService);
2931

3032
const blankProjectDoc = { id: 'project1', data: createTestProjectProfile() } as SFProjectProfileDoc;
3133

@@ -78,6 +80,15 @@ function setUpMocks(args: DraftSourcesComponentStoryState): void {
7880
when(mockedFeatureFlags.allowAdditionalTrainingSource).thenReturn(createTestFeatureFlag(args.mixedSource));
7981
when(mockedAuthService.currentUserId).thenReturn('user1');
8082

83+
when(mockedOnlineStatusService.onlineStatus$).thenReturn(of(args.online));
84+
when(mockedOnlineStatusService.isOnline).thenReturn(args.online);
85+
when(mockedOnlineStatusService.online).thenReturn(
86+
new Promise(resolve => {
87+
if (args.online) resolve();
88+
// Else, never resolve.
89+
})
90+
);
91+
8192
const languageCodes = ['en', 'fr', 'es', 'pt', 'de', 'ru', 'zh', 'ar', 'hi', 'bn'];
8293

8394
function languageName(code: string): string {
@@ -120,11 +131,13 @@ function setUpMocks(args: DraftSourcesComponentStoryState): void {
120131
interface DraftSourcesComponentStoryState {
121132
project: SFProjectProfileDoc;
122133
mixedSource: boolean;
134+
online: boolean;
123135
}
124136

125137
const defaultArgs: DraftSourcesComponentStoryState = {
126138
project: blankProjectDoc,
127-
mixedSource: true
139+
mixedSource: true,
140+
online: true
128141
};
129142

130143
export default {
@@ -143,6 +156,7 @@ export default {
143156
{ provide: Router, useValue: instance(mockedRouter) },
144157
{ provide: FeatureFlagService, useValue: instance(mockedFeatureFlags) },
145158
{ provide: AuthService, useValue: instance(mockedAuthService) },
159+
{ provide: OnlineStatusService, useValue: instance(mockedOnlineStatusService) },
146160
defaultTranslocoMarkupTranspilers()
147161
]
148162
})
@@ -435,3 +449,22 @@ export const CanHandleBackTranslationProjectsWithUnknownLanguage: Story = {
435449
await userEvent.click(await canvas.findByRole('checkbox'));
436450
}
437451
};
452+
453+
// See SF-3271
454+
export const CannotSaveAndSyncWhenOffline: Story = {
455+
args: {
456+
project: blankProjectDoc,
457+
mixedSource: true,
458+
online: false
459+
},
460+
play: async ({ canvasElement }) => {
461+
const canvas = within(canvasElement);
462+
const offlineMessage = canvasElement.querySelector('mat-error')?.textContent ?? '';
463+
// Offline message is displayed
464+
expect(offlineMessage).toContain(
465+
'You are offline. Please connect to the internet to save and sync your draft sources.'
466+
);
467+
// Save button is disabled
468+
expect(canvas.getByRole('button', { name: /Save & sync/ })).toBeDisabled();
469+
}
470+
};

src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/non_checking_en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@
284284
"language_code": "Language code: ",
285285
"leave_and_discard": "Leave & discard changes",
286286
"next": "Next",
287+
"offline_message": "You are offline. Please connect to the internet to save and sync your draft sources.",
287288
"overview_reference": "Reference",
288289
"overview_source": "Source",
289290
"overview_translated_project": "Translated project",

0 commit comments

Comments
 (0)