Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: app updates are automatically applied if no unsaved changes are … #2181

Merged
merged 1 commit into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe("ChangelogComponent", () => {
{
provide: UpdateManagerService,
useValue: jasmine.createSpyObj([
"notifyUserWhenUpdateAvailable",
"listenToAppUpdates",
"regularlyCheckForUpdates",
"detectUnrecoverableState",
]),
Expand Down
2 changes: 1 addition & 1 deletion src/app/core/ui/latest-changes/latest-changes.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
70 changes: 43 additions & 27 deletions src/app/core/ui/latest-changes/update-manager.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,6 +24,7 @@ describe("UpdateManagerService", () => {
let stableSubject: Subject<boolean>;
let latestChangesDialog: jasmine.SpyObj<LatestChangesDialogService>;
let mockLogger: jasmine.SpyObj<LoggingService>;
let unsavedChanges: UnsavedChangesService;

beforeEach(() => {
mockLocation = jasmine.createSpyObj(["reload"]);
Expand All @@ -43,64 +45,80 @@ 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,
);

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();
Expand Down Expand Up @@ -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",
);
Expand All @@ -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();
Expand All @@ -176,6 +191,7 @@ describe("UpdateManagerService", () => {
snackBar,
mockLogger,
latestChangesDialog,
unsavedChanges,
mockLocation,
);
}
Expand Down
54 changes: 30 additions & 24 deletions src/app/core/ui/latest-changes/update-manager.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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, ""),
);
Expand All @@ -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());
}

/**
Expand All @@ -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();
}
}

/**
Expand Down