diff --git a/src/app/core/ui/latest-changes/changelog/changelog.component.spec.ts b/src/app/core/ui/latest-changes/changelog/changelog.component.spec.ts index 1e6e887121..960a4e0c2a 100644 --- a/src/app/core/ui/latest-changes/changelog/changelog.component.spec.ts +++ b/src/app/core/ui/latest-changes/changelog/changelog.component.spec.ts @@ -57,7 +57,7 @@ describe("ChangelogComponent", () => { { provide: UpdateManagerService, useValue: jasmine.createSpyObj([ - "notifyUserWhenUpdateAvailable", + "listenToAppUpdates", "regularlyCheckForUpdates", "detectUnrecoverableState", ]), diff --git a/src/app/core/ui/latest-changes/latest-changes.module.ts b/src/app/core/ui/latest-changes/latest-changes.module.ts index e377ffb797..7056e58f3c 100644 --- a/src/app/core/ui/latest-changes/latest-changes.module.ts +++ b/src/app/core/ui/latest-changes/latest-changes.module.ts @@ -58,7 +58,7 @@ import { MatButtonModule } from "@angular/material/button"; }) export class LatestChangesModule { constructor(private updateManagerService: UpdateManagerService) { - this.updateManagerService.notifyUserWhenUpdateAvailable(); + this.updateManagerService.listenToAppUpdates(); this.updateManagerService.regularlyCheckForUpdates(); this.updateManagerService.detectUnrecoverableState(); } diff --git a/src/app/core/ui/latest-changes/update-manager.service.spec.ts b/src/app/core/ui/latest-changes/update-manager.service.spec.ts index 04b10f74f3..31344b4e7d 100644 --- a/src/app/core/ui/latest-changes/update-manager.service.spec.ts +++ b/src/app/core/ui/latest-changes/update-manager.service.spec.ts @@ -10,6 +10,7 @@ import { MatSnackBar } from "@angular/material/snack-bar"; import { LatestChangesDialogService } from "./latest-changes-dialog.service"; import { Subject } from "rxjs"; import { LoggingService } from "../../logging/logging.service"; +import { UnsavedChangesService } from "../../entity-details/form/unsaved-changes.service"; describe("UpdateManagerService", () => { let service: UpdateManagerService; @@ -23,6 +24,7 @@ describe("UpdateManagerService", () => { let stableSubject: Subject; let latestChangesDialog: jasmine.SpyObj; let mockLogger: jasmine.SpyObj; + let unsavedChanges: UnsavedChangesService; beforeEach(() => { mockLocation = jasmine.createSpyObj(["reload"]); @@ -43,32 +45,51 @@ describe("UpdateManagerService", () => { appRef = jasmine.createSpyObj([], { isStable: stableSubject }); latestChangesDialog = jasmine.createSpyObj(["showLatestChangesIfUpdated"]); mockLogger = jasmine.createSpyObj(["error"]); + unsavedChanges = new UnsavedChangesService(undefined); + unsavedChanges.pending = true; service = createService(); }); + afterEach(() => localStorage.clear()); + it("should create", () => { expect(service).toBeTruthy(); }); - it("should show a snackBar that allows to reload the page when an update is available", fakeAsync(() => { - service.notifyUserWhenUpdateAvailable(); + it("should show a snackBar that allows to reload the page when an update is available", () => { + service.listenToAppUpdates(); // notify about new update updateSubject.next({ type: "VERSION_READY" }); - tick(); expect(snackBar.open).toHaveBeenCalled(); // user activates update snackBarAction.next(undefined); - tick(); expect(mockLocation.reload).toHaveBeenCalled(); - })); + }); + + it("should reload app if no unsaved changes are detected", () => { + service.listenToAppUpdates(); + unsavedChanges.pending = true; + + updateSubject.next({ type: "VERSION_READY" }); + + expect(mockLocation.reload).not.toHaveBeenCalled(); + expect(snackBar.open).toHaveBeenCalled(); + + createService(); + unsavedChanges.pending = false; + + updateSubject.next({ type: "VERSION_READY" }); + + expect(mockLocation.reload).toHaveBeenCalled(); + }); it("should reload the page during construction if noted in the local storage", () => { const version = "1.1.1"; - window.localStorage.setItem( + localStorage.setItem( LatestChangesDialogService.VERSION_KEY, "update-" + version, ); @@ -76,31 +97,28 @@ describe("UpdateManagerService", () => { createService(); expect(mockLocation.reload).toHaveBeenCalled(); - expect( - window.localStorage.getItem(LatestChangesDialogService.VERSION_KEY), - ).toBe(version); + expect(localStorage.getItem(LatestChangesDialogService.VERSION_KEY)).toBe( + version, + ); }); - it("should set the note for reloading the app on next startup and remove it if user triggers reload manually", fakeAsync(() => { + it("should set the note for reloading the app on next startup and remove it if user triggers reload manually", () => { const version = "1.1.1"; - window.localStorage.setItem( - LatestChangesDialogService.VERSION_KEY, - version, - ); - service.notifyUserWhenUpdateAvailable(); + localStorage.setItem(LatestChangesDialogService.VERSION_KEY, version); + service.listenToAppUpdates(); updateSubject.next({ type: "VERSION_READY" }); - expect( - window.localStorage.getItem(LatestChangesDialogService.VERSION_KEY), - ).toBe("update-" + version); + expect(localStorage.getItem(LatestChangesDialogService.VERSION_KEY)).toBe( + "update-" + version, + ); // reload is triggered by clicking button on the snackbar snackBarAction.next(); - expect( - window.localStorage.getItem(LatestChangesDialogService.VERSION_KEY), - ).toBe(version); - })); + expect(localStorage.getItem(LatestChangesDialogService.VERSION_KEY)).toBe( + version, + ); + }); it("should check for updates once on startup and then every hour", fakeAsync(() => { service.regularlyCheckForUpdates(); @@ -138,7 +156,7 @@ describe("UpdateManagerService", () => { it("should trigger the latest changes dialog on startup only if update note is set", () => { latestChangesDialog.showLatestChangesIfUpdated.calls.reset(); - window.localStorage.setItem( + localStorage.setItem( LatestChangesDialogService.VERSION_KEY, "update-1.0.0", ); @@ -148,10 +166,7 @@ describe("UpdateManagerService", () => { latestChangesDialog.showLatestChangesIfUpdated, ).not.toHaveBeenCalled(); - window.localStorage.setItem( - LatestChangesDialogService.VERSION_KEY, - "1.0.0", - ); + localStorage.setItem(LatestChangesDialogService.VERSION_KEY, "1.0.0"); createService(); expect(latestChangesDialog.showLatestChangesIfUpdated).toHaveBeenCalled(); @@ -176,6 +191,7 @@ describe("UpdateManagerService", () => { snackBar, mockLogger, latestChangesDialog, + unsavedChanges, mockLocation, ); } diff --git a/src/app/core/ui/latest-changes/update-manager.service.ts b/src/app/core/ui/latest-changes/update-manager.service.ts index 883561d318..e3e7df9262 100644 --- a/src/app/core/ui/latest-changes/update-manager.service.ts +++ b/src/app/core/ui/latest-changes/update-manager.service.ts @@ -23,6 +23,7 @@ import { MatSnackBar } from "@angular/material/snack-bar"; import { LoggingService } from "../../logging/logging.service"; import { LatestChangesDialogService } from "./latest-changes-dialog.service"; import { LOCATION_TOKEN } from "../../../utils/di-tokens"; +import { UnsavedChangesService } from "../../entity-details/form/unsaved-changes.service"; /** * Check with the server whether a new version of the app is available in order to notify the user. @@ -41,17 +42,18 @@ export class UpdateManagerService { private snackBar: MatSnackBar, private logger: LoggingService, private latestChangesDialogService: LatestChangesDialogService, + private unsavedChanges: UnsavedChangesService, @Inject(LOCATION_TOKEN) private location: Location, ) { this.updates.unrecoverable.subscribe((err) => { this.logger.error("App is in unrecoverable state: " + err.reason); this.location.reload(); }); - const currentVersion = window.localStorage.getItem( + const currentVersion = localStorage.getItem( LatestChangesDialogService.VERSION_KEY, ); if (currentVersion && currentVersion.startsWith(this.UPDATE_PREFIX)) { - window.localStorage.setItem( + localStorage.setItem( LatestChangesDialogService.VERSION_KEY, currentVersion.replace(this.UPDATE_PREFIX, ""), ); @@ -64,13 +66,13 @@ export class UpdateManagerService { /** * Display a notification to the user in case a new app version is detected by the ServiceWorker. */ - public notifyUserWhenUpdateAvailable() { + public listenToAppUpdates() { if (!this.updates.isEnabled) { return; } this.updates.versionUpdates .pipe(filter((e) => e.type === "VERSION_READY")) - .subscribe(() => this.showUpdateNotification()); + .subscribe(() => this.updateIfPossible()); } /** @@ -93,33 +95,37 @@ export class UpdateManagerService { ); } - private showUpdateNotification() { + private updateIfPossible() { const currentVersion = - window.localStorage.getItem(LatestChangesDialogService.VERSION_KEY) || ""; + localStorage.getItem(LatestChangesDialogService.VERSION_KEY) || ""; if (currentVersion.startsWith(this.UPDATE_PREFIX)) { // Sometimes this is triggered multiple times for one update return; } - window.localStorage.setItem( - LatestChangesDialogService.VERSION_KEY, - this.UPDATE_PREFIX + currentVersion, - ); - - this.snackBar - .open( - $localize`A new version of the app is available!`, - $localize`:Action that a user can update the app with:Update`, - ) - .onAction() - .subscribe(() => { - window.localStorage.setItem( - LatestChangesDialogService.VERSION_KEY, - currentVersion, - ); + if (this.unsavedChanges.pending) { + // app cannot be safely reloaded + localStorage.setItem( + LatestChangesDialogService.VERSION_KEY, + this.UPDATE_PREFIX + currentVersion, + ); + this.snackBar + .open( + $localize`A new version of the app is available!`, + $localize`:Action that a user can update the app with:Update`, + ) + .onAction() + .subscribe(() => { + localStorage.setItem( + LatestChangesDialogService.VERSION_KEY, + currentVersion, + ); - this.location.reload(); - }); + this.location.reload(); + }); + } else { + this.location.reload(); + } } /**