Skip to content

Commit e3112e7

Browse files
SF-3349 Prompt user when leaving draft sources with unsaved changes (#3184)
1 parent ee149d8 commit e3112e7

File tree

4 files changed

+90
-4
lines changed

4 files changed

+90
-4
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import { mock } from 'ts-mockito';
3+
import { AuthGuard } from 'xforge-common/auth.guard';
4+
import { configureTestingModule } from 'xforge-common/test-utils';
5+
import { SFProjectService } from '../core/sf-project.service';
6+
import { DraftNavigationAuthGuard } from './project-router.guard';
7+
8+
const mockedAuthGuard = mock(AuthGuard);
9+
const mockedProjectService = mock(SFProjectService);
10+
11+
describe('DraftNavigationAuthGuard', () => {
12+
configureTestingModule(() => ({
13+
providers: [
14+
{ provide: AuthGuard, useMock: mockedAuthGuard },
15+
{ provide: SFProjectService, useMock: mockedProjectService }
16+
]
17+
}));
18+
19+
it('can navigate away when no changes', () => {
20+
// navigate away
21+
const env = new DraftNavigationTestEnvironment();
22+
spyOn(window, 'confirm');
23+
expect(
24+
env.service.canDeactivate({ deactivationPrompt: 'unsaved changed', promptUserToDeactivate: () => false })
25+
).toBe(true);
26+
expect(window.confirm).not.toHaveBeenCalled();
27+
});
28+
29+
it('can shows prompt and navigate away', () => {
30+
// navigate away
31+
const env = new DraftNavigationTestEnvironment();
32+
spyOn(window, 'confirm').and.returnValue(true);
33+
expect(
34+
env.service.canDeactivate({ deactivationPrompt: 'unsaved changed', promptUserToDeactivate: () => true })
35+
).toBe(true);
36+
expect(window.confirm).toHaveBeenCalled();
37+
});
38+
39+
it('can shows prompt and stay on page', () => {
40+
// navigate away
41+
const env = new DraftNavigationTestEnvironment();
42+
spyOn(window, 'confirm').and.returnValue(false);
43+
expect(
44+
env.service.canDeactivate({ deactivationPrompt: 'unsaved changed', promptUserToDeactivate: () => true })
45+
).toBe(false);
46+
expect(window.confirm).toHaveBeenCalled();
47+
});
48+
});
49+
50+
class DraftNavigationTestEnvironment {
51+
service: DraftNavigationAuthGuard;
52+
constructor() {
53+
this.service = TestBed.inject(DraftNavigationAuthGuard);
54+
}
55+
}

src/SIL.XForge.Scripture/ClientApp/src/app/shared/project-router.guard.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Injectable } from '@angular/core';
2-
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
2+
import { ActivatedRouteSnapshot, CanDeactivate, Router, RouterStateSnapshot } from '@angular/router';
33
import { Operation } from 'realtime-server/lib/esm/common/models/project-rights';
44
import { SF_PROJECT_RIGHTS, SFProjectDomain } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-rights';
55
import { SFProjectRole } from 'realtime-server/lib/esm/scriptureforge/models/sf-project-role';
@@ -168,3 +168,27 @@ export class TranslateAuthGuard extends RouterGuard {
168168
return false;
169169
}
170170
}
171+
172+
export interface DeactivateAllowed {
173+
deactivationPrompt: string;
174+
175+
promptUserToDeactivate(): boolean;
176+
}
177+
178+
@Injectable({
179+
providedIn: 'root'
180+
})
181+
export class DraftNavigationAuthGuard extends RouterGuard implements CanDeactivate<DeactivateAllowed> {
182+
constructor(authGuard: AuthGuard, projectService: SFProjectService) {
183+
super(authGuard, projectService);
184+
}
185+
186+
canDeactivate(component: DeactivateAllowed): boolean {
187+
if (!component.promptUserToDeactivate()) return true;
188+
return confirm(component.deactivationPrompt);
189+
}
190+
191+
check(_: SFProjectProfileDoc): boolean {
192+
return true;
193+
}
194+
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { hasData, notNull } from '../../../../type-utils';
2424
import { SFProjectProfileDoc } from '../../../core/models/sf-project-profile-doc';
2525
import { ParatextService, SelectableProject, SelectableProjectWithLanguageCode } from '../../../core/paratext.service';
2626
import { SFProjectService } from '../../../core/sf-project.service';
27+
import { DeactivateAllowed } from '../../../shared/project-router.guard';
2728
import { projectLabel } from '../../../shared/utils';
2829
import { isSFProjectSyncing } from '../../../sync/sync.component';
2930
import {
@@ -60,7 +61,7 @@ export interface ProjectStatus {
6061
templateUrl: './draft-sources.component.html',
6162
styleUrl: './draft-sources.component.scss'
6263
})
63-
export class DraftSourcesComponent extends DataLoadingComponent {
64+
export class DraftSourcesComponent extends DataLoadingComponent implements DeactivateAllowed {
6465
/** Indicator that a project setting change is for clearing a value. */
6566
static readonly projectSettingValueUnset = 'unset';
6667

@@ -82,6 +83,7 @@ export class DraftSourcesComponent extends DataLoadingComponent {
8283
languageCodeConfirmationMessageIfUserTriesToContinue: I18nKeyForComponent<'draft_sources'> | null = null;
8384
clearLanguageCodeConfirmationCheckbox = new EventEmitter<void>();
8485
changesMade = false;
86+
deactivationPrompt: string = this.i18n.translateStatic('draft_sources.discard_changes_confirmation');
8587

8688
/** Whether some projects are syncing currently. */
8789
syncStatus: Map<string, ProjectStatus> = new Map<string, ProjectStatus>();
@@ -288,6 +290,10 @@ export class DraftSourcesComponent extends DataLoadingComponent {
288290
}
289291
}
290292

293+
promptUserToDeactivate(): boolean {
294+
return this.changesMade;
295+
}
296+
291297
navigateToDrafting(): void {
292298
this.router.navigate(['/projects', this.activatedProjectService.projectId, 'draft-generation']);
293299
}

src/SIL.XForge.Scripture/ClientApp/src/app/translate/translate-routing.module.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { NgModule } from '@angular/core';
22
import { RouterModule, Routes } from '@angular/router';
3-
import { NmtDraftAuthGuard, TranslateAuthGuard } from '../shared/project-router.guard';
3+
import { DraftNavigationAuthGuard, NmtDraftAuthGuard, TranslateAuthGuard } from '../shared/project-router.guard';
44
import { DraftGenerationComponent } from './draft-generation/draft-generation.component';
55
import { DraftSourcesComponent } from './draft-generation/draft-sources/draft-sources.component';
66
import { EditorComponent } from './editor/editor.component';
@@ -22,7 +22,8 @@ const routes: Routes = [
2222
{
2323
path: 'projects/:projectId/draft-generation/sources',
2424
component: DraftSourcesComponent,
25-
canActivate: [NmtDraftAuthGuard]
25+
canActivate: [NmtDraftAuthGuard],
26+
canDeactivate: [DraftNavigationAuthGuard]
2627
}
2728
];
2829

0 commit comments

Comments
 (0)