From df80b6e3a7839d9d5242997d42b1bee78a7841ad Mon Sep 17 00:00:00 2001 From: Jonas Date: Thu, 28 Nov 2024 14:49:32 +0100 Subject: [PATCH 1/2] refactor(SyncService): Move `hasUnsavedChanges` getter to SyncService Signed-off-by: Jonas --- src/components/Editor/Status.vue | 7 ++----- src/services/SessionApi.js | 4 ++++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/Editor/Status.vue b/src/components/Editor/Status.vue index 72113249ef9..f6ec67a9c25 100644 --- a/src/components/Editor/Status.vue +++ b/src/components/Editor/Status.vue @@ -89,22 +89,19 @@ export default { return this.dirtyStateIndicator ? t('text', 'Saving …') : t('text', 'Saved') }, dirtyStateIndicator() { - return this.dirty || this.hasUnsavedChanges + return this.dirty || this.document.hasUnsavedChanges }, lastSavedStatusTooltip() { let message = t('text', 'Last saved {lastSave}', { lastSave: this.lastSavedString }) if (this.hasSyncCollission) { message = t('text', 'The document has been changed outside of the editor. The changes cannot be applied.') } - if (this.dirty || this.hasUnsavedChanges) { + if (this.dirty || this.document.hasUnsavedChanges) { message += ' - ' + t('text', 'Unsaved changes') } return message }, - hasUnsavedChanges() { - return this.document && this.document.lastSavedVersion < this.document.currentVersion - }, hasSyncCollission() { return this.syncError && this.syncError.type === ERROR_TYPE.SAVE_COLLISSION }, diff --git a/src/services/SessionApi.js b/src/services/SessionApi.js index 45ff771b69b..73b7ed1a789 100644 --- a/src/services/SessionApi.js +++ b/src/services/SessionApi.js @@ -88,6 +88,10 @@ export class Connection { return this.closed } + get hasUnsavedChanges() { + return this.#document && this.#document.lastSavedVersion < this.#document.currentVersion + } + get #defaultParams() { return { documentId: this.#document.id, From 2fa0402c76ab9f6405836b32c288a0b35b45f940 Mon Sep 17 00:00:00 2001 From: Jonas Date: Thu, 28 Nov 2024 14:52:00 +0100 Subject: [PATCH 2/2] feat(SessionApi): Send save request via `sendBeacon` at `beforeunload` This will send a final save request on unsaved changes via the browsers native `navigator.sendBeacon()` function when navigating away from the website or the tab/browser is closed. Fixes: #6606 Signed-off-by: Jonas --- src/components/Editor.vue | 10 ++++++++++ src/services/SessionApi.js | 15 ++++++++++++--- src/services/SyncService.js | 12 ++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/components/Editor.vue b/src/components/Editor.vue index 76893fca873..1ebda122df2 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -678,7 +678,10 @@ export default { ) { this.dirty = state.dirty if (this.dirty) { + window.addEventListener('beforeunload', this.saveBeforeUnload) this.$syncService.autosave() + } else { + window.removeEventListener('beforeunload', this.saveBeforeUnload) } } } @@ -887,6 +890,13 @@ export default { updateEditorWidth(newWidth) { document.documentElement.style.setProperty('--text-editor-max-width', newWidth) }, + + async saveBeforeUnload(event) { + if (!this.dirty && !this.document.hasUnsavedChanges) { + return + } + await this.$syncService.saveNoWait() + }, }, } diff --git a/src/services/SessionApi.js b/src/services/SessionApi.js index 73b7ed1a789..b25422ac2c0 100644 --- a/src/services/SessionApi.js +++ b/src/services/SessionApi.js @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import axios from '@nextcloud/axios' +import { getRequestToken } from '@nextcloud/auth' import { generateUrl } from '@nextcloud/router' export class ConnectionClosedError extends Error { @@ -110,8 +111,9 @@ export class Connection { }) } - save({ version, autosaveContent, documentState, force, manualSave }) { - return this.#post(this.#url(`session/${this.#document.id}/save`), { + save({ version, autosaveContent, documentState, force, manualSave, useSendBeacon = false }) { + const url = this.#url(`session/${this.#document.id}/save`) + const data = { ...this.#defaultParams, filePath: this.#options.filePath, baseVersionEtag: this.#document.baseVersionEtag, @@ -120,7 +122,14 @@ export class Connection { documentState, force, manualSave, - }) + } + + if (useSendBeacon) { + data.requesttoken = getRequestToken() ?? '' + const blob = new Blob([JSON.stringify(data)], { type: 'application/json' }) + return navigator.sendBeacon(url, blob) + } + return this.#post(url, data) } push({ steps, version, awareness }) { diff --git a/src/services/SyncService.js b/src/services/SyncService.js index 61860b18fd9..a85c376ad38 100644 --- a/src/services/SyncService.js +++ b/src/services/SyncService.js @@ -299,6 +299,18 @@ class SyncService { } } + async saveNoWait() { + await this.#connection.save({ + version: this.version, + autosaveContent: this._getContent(), + documentState: this.getDocumentState(), + force: false, + manualSave: true, + useSendBeacon: true, + }) + logger.debug('[SyncService] saved using sendBeacon (nowait)') + } + forceSave() { return this.save({ force: true }) }