From 2813b37d1212ffc86c849fa2576b036857ab7422 Mon Sep 17 00:00:00 2001 From: Farbod Zamani <zamani@elan-ev.de> Date: Mon, 5 Dec 2022 08:51:56 +0000 Subject: [PATCH] CW: Improved Autosave and UnlockObject Closes #1310 Merge request studip/studip!1196 --- .../bootstrap/beforeunload-event-listener.js | 6 ++ resources/assets/javascripts/entry-base.js | 1 + .../CoursewareAccordionContainer.vue | 4 ++ .../courseware/CoursewareActionWidget.vue | 70 +++++++++++++++++++ .../courseware/CoursewareBlockEdit.vue | 23 +++++- .../courseware/CoursewareDefaultBlock.vue | 26 ++++++- .../courseware/CoursewareDefaultContainer.vue | 36 +++++++++- .../CoursewareStructuralElement.vue | 54 ++++++++++++-- 8 files changed, 210 insertions(+), 10 deletions(-) create mode 100644 resources/assets/javascripts/bootstrap/beforeunload-event-listener.js diff --git a/resources/assets/javascripts/bootstrap/beforeunload-event-listener.js b/resources/assets/javascripts/bootstrap/beforeunload-event-listener.js new file mode 100644 index 00000000000..4e966308cd0 --- /dev/null +++ b/resources/assets/javascripts/bootstrap/beforeunload-event-listener.js @@ -0,0 +1,6 @@ +STUDIP.domReady(() => { + // Before-unload event listener. + window.addEventListener('beforeunload', (e) => { + STUDIP.eventBus.emit('studip:beforeunload', e); + }, {capture: true}); +}); diff --git a/resources/assets/javascripts/entry-base.js b/resources/assets/javascripts/entry-base.js index fdff1c7a938..09779ddb164 100644 --- a/resources/assets/javascripts/entry-base.js +++ b/resources/assets/javascripts/entry-base.js @@ -86,6 +86,7 @@ import "./bootstrap/admin-courses.js" import "./bootstrap/cache-admin.js" import "./bootstrap/oer.js" import "./bootstrap/courseware.js" +import "./bootstrap/beforeunload-event-listener.js" import "./mvv_course_wizard.js" import "./mvv.js" diff --git a/resources/vue/components/courseware/CoursewareAccordionContainer.vue b/resources/vue/components/courseware/CoursewareAccordionContainer.vue index 8e4d0984d1a..92029c69b89 100644 --- a/resources/vue/components/courseware/CoursewareAccordionContainer.vue +++ b/resources/vue/components/courseware/CoursewareAccordionContainer.vue @@ -8,6 +8,7 @@ @storeContainer="storeContainer" @closeEdit="initCurrentData" @sortBlocks="enableSort" + @setSortMode="setSortMode" > <template v-slot:containerContent> <courseware-collapsible-box @@ -244,6 +245,9 @@ export default { if (blockAdder.container !== undefined && blockAdder.container.id === this.container.id) { this.initCurrentData(); } + }, + setSortMode(mode) { + this.sortMode = mode; } }, watch: { diff --git a/resources/vue/components/courseware/CoursewareActionWidget.vue b/resources/vue/components/courseware/CoursewareActionWidget.vue index f4d690683aa..89f27ea549e 100644 --- a/resources/vue/components/courseware/CoursewareActionWidget.vue +++ b/resources/vue/components/courseware/CoursewareActionWidget.vue @@ -74,6 +74,12 @@ export default { SidebarWidget, }, mixins: [CoursewareExport], + data() { + return { + objectIsBlocked: false, + blockedFrom: '' + } + }, computed: { ...mapGetters({ userId: 'userId', @@ -86,6 +92,9 @@ export default { blockerId: 'currentElementBlockerId', blockedByThisUser: 'currentElementBlockedByThisUser', blockedByAnotherUser: 'currentElementBlockedByAnotherUser', + editDialogState: 'showStructuralElementEditDialog', + deleteDialogState: 'showStructuralElementDeleteDialog', + structuralElementSortMode: 'structuralElementSortMode' }), isRoot() { if (!this.structuralElement) { @@ -126,6 +135,7 @@ export default { companionInfo: 'companionInfo', addBookmark: 'addBookmark', lockObject: 'lockObject', + unlockObject: 'unlockObject', setConsumeMode: 'coursewareConsumeMode', setViewMode: 'coursewareViewMode', setShowToolbar: 'coursewareShowToolbar', @@ -141,6 +151,8 @@ export default { } try { await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = true; + this.blockedFrom = 'editDialogState'; } catch(error) { if (error.status === 409) { this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') }); @@ -164,6 +176,8 @@ export default { } try { await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = true; + this.blockedFrom = 'structuralElementSortMode'; } catch (error) { if (error.status === 409) { this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') }); @@ -188,6 +202,8 @@ export default { return false; } await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = true; + this.blockedFrom = 'deleteDialogState'; this.showElementDeleteDialog(true); }, addElement() { @@ -213,6 +229,60 @@ export default { }, linkElement() { this.showElementLinkDialog(true); + }, + async beforeUnloadActions() { + this.beforeUnloadCleanup(); + if (this.objectIsBlocked) { + await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + } + }, + beforeUnloadCleanup() { + // The following dialogs and elements must be set to be closed, in order to avoid lockObject conflicts. + this.showElementEditDialog(false); + this.setStructuralElementSortMode(false); + this.showElementDeleteDialog(false); + }, + async beforeUnloadHandler(event) { + if (this.objectIsBlocked) { + event.preventDefault(); + event.returnValue = 'There are unsaved changes, do you want to leave?'; + await this.beforeUnloadActions(); + return event.returnValue; + } + return null; + } + }, + mounted () { + STUDIP.eventBus.on('studip:beforeunload', this.beforeUnloadHandler); + }, + beforeDestroy () { + STUDIP.eventBus.off('studip:beforeunload', this.beforeUnloadHandler); + }, + watch: { + blockedByThisUser(newValue) { + if (!newValue) { + this.objectIsBlocked = false + } + }, + editDialogState(newValue) { + if (!newValue && this.blockedFrom == 'editDialogState') { + this.objectIsBlocked = false + } + }, + structuralElementSortMode(newValue) { + if (!newValue && this.blockedFrom == 'structuralElementSortMode') { + this.objectIsBlocked = false + } + }, + deleteDialogState(newValue) { + if (!newValue && this.blockedFrom == 'deleteDialogState') { + this.objectIsBlocked = false + } + }, + objectIsBlocked(newValue) { + if (!newValue) { + this.blockedFrom = ''; + } } }, }; diff --git a/resources/vue/components/courseware/CoursewareBlockEdit.vue b/resources/vue/components/courseware/CoursewareBlockEdit.vue index 55b8b3a1b9b..f48a6b06280 100644 --- a/resources/vue/components/courseware/CoursewareBlockEdit.vue +++ b/resources/vue/components/courseware/CoursewareBlockEdit.vue @@ -18,6 +18,7 @@ export default { name: 'courseware-block-edit', props: { block: Object, + objectIsBlocked: Boolean, }, data() { return { @@ -33,12 +34,28 @@ export default { this.$store.dispatch('coursewareBlockAdder', {}); this.$store.dispatch('coursewareShowToolbar', false); }, + beforeUnloadHandler(event) { + if (this.exitHandler || this.objectIsBlocked) { + event.preventDefault(); + this.autoSave(); + event.returnValue = 'There are unsaved changes, do you want to leave?'; + return event.returnValue; + } + return null; + }, + async autoSave() { + this.$emit('store'); + this.exitHandler = false; + } }, beforeDestroy() { if (this.exitHandler) { - console.log('autosave'); - this.$emit('store'); + this.autoSave(); } - } + STUDIP.eventBus.off('studip:beforeunload', this.beforeUnloadHandler); + }, + mounted () { + STUDIP.eventBus.on('studip:beforeunload', this.beforeUnloadHandler); + }, }; </script> diff --git a/resources/vue/components/courseware/CoursewareDefaultBlock.vue b/resources/vue/components/courseware/CoursewareDefaultBlock.vue index 14ab304ec61..6a9ad39f9ab 100644 --- a/resources/vue/components/courseware/CoursewareDefaultBlock.vue +++ b/resources/vue/components/courseware/CoursewareDefaultBlock.vue @@ -36,6 +36,7 @@ <courseware-block-edit v-if="canEdit && showEdit" :block="block" + :objectIsBlocked="objectIsBlocked" @store="prepareStoreEdit" @close="closeEdit" > @@ -130,6 +131,7 @@ export default { textDeleteAlert: this.$gettext('Möchten Sie diesen Block wirklich löschen?'), textRemoveLockTitle: this.$gettext('Sperre aufheben'), textRemoveLockAlert: this.$gettext('Möchten Sie die Sperre dieses Blocks wirklich aufheben?'), + objectIsBlocked: false, }; }, computed: { @@ -217,6 +219,7 @@ export default { await this.loadBlock({ id: this.block.id, options: { include: 'edit-blocker' } }); if (!this.blocked) { await this.lockObject({ id: this.block.id, type: 'courseware-blocks' }); + this.objectIsBlocked = true; if (!this.preview) { this.showContent = false; } @@ -264,15 +267,23 @@ export default { } if (this.blockerId === null) { await this.lockObject({ id: this.block.id, type: 'courseware-blocks' }); + this.objectIsBlocked = true; this.$emit('storeEdit'); } + + // To make sure the object is not blocked. + if (this.objectIsBlocked || this.blockedByThisUser) { + await this.unlockObject({ id: this.block.id, type: 'courseware-blocks' }); + this.objectIsBlocked = false; + } }, async closeEdit() { await this.loadBlock({ id: this.block.id , options: { include: 'edit-blocker' } }); // has block editor lock changed? this.displayFeature(false); this.$emit('closeEdit'); - if (this.blockedByThisUser) { + if (this.blockedByThisUser || this.blockedByThisUser) { await this.unlockObject({ id: this.block.id, type: 'courseware-blocks' }); + this.objectIsBlocked = false; } this.loadBlock({ id: this.block.id , options: { include: 'edit-blocker' } }); // to update block editor lock }, @@ -280,6 +291,7 @@ export default { await this.loadBlock({ id: this.block.id, options: { include: 'edit-blocker' } }); if (!this.blocked) { await this.lockObject({ id: this.block.id, type: 'courseware-blocks' }); + this.objectIsBlocked = true; this.showDeleteDialog = true; } else { if (this.blockedByThisUser) { @@ -298,6 +310,7 @@ export default { await this.loadBlock({ id: this.block.id, options: { include: 'edit-blocker' } }); if (this.blockedByThisUser) { await this.unlockObject({ id: this.block.id, type: 'courseware-blocks' }); + this.objectIsBlocked = false; } this.showDeleteDialog = false; }, @@ -323,6 +336,7 @@ export default { // lock parent container await this.lockObject({ id: containerId, type: 'courseware-containers' }); + this.objectIsBlocked = true; // update container information for (let i = 0; i < sections.length; i++) { for (let j = 0; j < sections[i].blocks.length; j++) { @@ -337,6 +351,7 @@ export default { await this.updateContainer({ container, structuralElementId }); // unlock container await this.unlockObject({ id: containerId, type: 'courseware-containers' }); + this.objectIsBlocked = false; await this.loadContainer(containerId); this.deleteBlock({ blockId: this.block.id, @@ -348,14 +363,21 @@ export default { }, async executeRemoveLock() { await this.unlockObject({ id: this.block.id , type: 'courseware-blocks' }); + this.objectIsBlocked = false; await this.loadBlock({ id: this.block.id }); this.showRemoveLockDialog = false; + }, + async performUnloadObject() { + await this.unlockObject({ id: this.block.id , type: 'courseware-blocks' }); + this.objectIsBlocked = false; } }, watch: { showEdit(state) { - this.$emit('showEdit', state); + if (state) { + this.objectIsBlocked = true; + } } } }; diff --git a/resources/vue/components/courseware/CoursewareDefaultContainer.vue b/resources/vue/components/courseware/CoursewareDefaultContainer.vue index 70a6da708f0..38f6b09863c 100644 --- a/resources/vue/components/courseware/CoursewareDefaultContainer.vue +++ b/resources/vue/components/courseware/CoursewareDefaultContainer.vue @@ -93,6 +93,7 @@ export default { textDeleteAlert: this.$gettext('Möchten Sie diesen Abschnitt wirklich löschen?'), textRemoveLockTitle: this.$gettext('Sperre aufheben'), textRemoveLockAlert: this.$gettext('Möchten Sie die Sperre dieses Abschnitts wirklich aufheben?'), + objectIsBlocked: false, }; }, computed: { @@ -147,6 +148,7 @@ export default { } await this.lockObject({ id: this.container.id, type: 'courseware-containers' }); + this.objectIsBlocked = true; this.showEditDialog = true; }, async closeEdit() { @@ -155,6 +157,7 @@ export default { this.showEditDialog = false; if (this.blockedByThisUser) { await this.unlockObject({ id: this.container.id, type: 'courseware-containers' }); + this.objectIsBlocked = false; } await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } }); }, @@ -174,6 +177,7 @@ export default { } if (this.blockerId === null) { await this.lockObject({ id: this.container.id, type: 'courseware-containers' }); + this.objectIsBlocked = true; this.$emit('storeContainer'); } this.showEditDialog = false; @@ -182,6 +186,7 @@ export default { await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } }); if (!this.blocked) { await this.lockObject({ id: this.container.id, type: 'courseware-containers' }); + this.objectIsBlocked = true; this.showDeleteDialog = true; } else { if (this.blockedByThisUser) { @@ -200,6 +205,7 @@ export default { await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } }); if (this.blockedByThisUser) { await this.unlockObject({ id: this.container.id, type: 'courseware-containers' }); + this.objectIsBlocked = false; } this.showDeleteDialog = false; }, @@ -231,6 +237,7 @@ export default { return false; } await this.lockObject({ id: this.container.id, type: 'courseware-containers' }); + this.objectIsBlocked = true; this.$emit('sortBlocks'); }, displayRemoveLockDialog() { @@ -238,10 +245,37 @@ export default { }, async executeRemoveLock() { await this.unlockObject({ id: this.container.id , type: 'courseware-containers' }); + this.objectIsBlocked = false; await this.loadContainer({ id: this.container.id }); this.showRemoveLockDialog = false; }, - + async beforeUnloadActions() { + this.beforeUnloadCleanup(); + if (this.blockedByThisUser || this.objectIsBlocked) { + await this.unlockObject({ id: this.container.id , type: 'courseware-containers' }); + } + }, + beforeUnloadCleanup() { + // The following dialogs and elements must be set to be closed, in order to avoid lockObject conflicts. + this.showEditDialog = false; + this.showDeleteDialog = false; + this.$emit('setSortMode', false); + }, + async beforeUnloadHandler(event) { + if (this.blockedByThisUser || this.objectIsBlocked) { + event.preventDefault(); + event.returnValue = 'There are unsaved changes, do you want to leave?'; + await this.beforeUnloadActions(); + return event.returnValue; + } + return null; + } + }, + mounted () { + STUDIP.eventBus.on('studip:beforeunload', this.beforeUnloadHandler); + }, + beforeDestroy () { + STUDIP.eventBus.off('studip:beforeunload', this.beforeUnloadHandler); }, watch: { diff --git a/resources/vue/components/courseware/CoursewareStructuralElement.vue b/resources/vue/components/courseware/CoursewareStructuralElement.vue index ae0f9a1993f..8c12b889762 100644 --- a/resources/vue/components/courseware/CoursewareStructuralElement.vue +++ b/resources/vue/components/courseware/CoursewareStructuralElement.vue @@ -732,6 +732,7 @@ export default { 'expire-date': '' }, deletingPreviewImage: false, + objectIsBlocked: false, }; }, @@ -1348,7 +1349,8 @@ export default { this.uploadFileError = ''; this.deletingPreviewImage = false; }, - async menuAction(action) { + async menuAction(action, type = 'open') { + this.lastAction = action; switch (action) { case 'removeLock': this.displayRemoveLockDialog(); @@ -1362,6 +1364,7 @@ export default { } try { await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = true; } catch(error) { if (error.status === 409) { this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') }); @@ -1393,6 +1396,7 @@ export default { return false; } await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = true; this.showElementDeleteDialog(true); break; case 'showInfo': @@ -1419,6 +1423,7 @@ export default { } try { await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = true; } catch (error) { if (error.status === 409) { this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') }); @@ -1439,6 +1444,7 @@ export default { await this.loadStructuralElement(this.currentElement.id); if (this.blockedByThisUser) { await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = false; await this.loadStructuralElement(this.currentElement.id); } this.showElementEditDialog(false); @@ -1476,6 +1482,7 @@ export default { } if (!this.blocked) { await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = true; } const file = this.$refs?.upload_image?.files[0]; if (file) { @@ -1512,6 +1519,7 @@ export default { id: this.currentId, }); await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = false; this.$emit('select', this.currentId); this.initCurrent(); }, @@ -1520,7 +1528,7 @@ export default { this.setStructuralElementSortMode(true); }, - storeSort() { + async storeSort() { this.setStructuralElementSortMode(false); this.sortContainersInStructualElements({ @@ -1528,11 +1536,19 @@ export default { containers: this.containerList, }); this.$emit('select', this.currentId); + if (this.blockedByThisUser) { + await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = false; + } }, - resetSort() { + async resetSort() { this.setStructuralElementSortMode(false); this.containerList = this.containers; + if (this.blockedByThisUser) { + await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = false; + } }, async exportCurrentElement(data) { @@ -1580,6 +1596,7 @@ export default { await this.loadStructuralElement(this.currentElement.id); if (this.blockedByThisUser) { await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = false; } this.showElementDeleteDialog(false); }, @@ -1641,7 +1658,7 @@ export default { this.companionSuccess({ info: this.$gettextInterpolate( - this.$gettext('Die Seite %{ pageTitle } wurde erfolgreich angelegt.'), + this.$gettext('Die Seite %{ pageTitle } wurde erfolgreich angelegt.'), { pageTitle: newElement.attributes.title } ) }); @@ -1709,13 +1726,42 @@ export default { }, async executeRemoveLock() { await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + this.objectIsBlocked = false; await this.loadStructuralElement(this.currentElement.id); this.showElementRemoveLockDialog(false); + }, + async beforeUnloadActions() { + this.beforeUnloadCleanup(); + if (this.blockedByThisUser && this.blocked || this.objectIsBlocked) { + await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' }); + } + }, + beforeUnloadCleanup() { + // The following dialogs and elements must be set to be closed, in order to avoid lockObject conflicts. + this.showElementEditDialog(false); + this.showElementDeleteDialog(false); + this.setStructuralElementSortMode(false); + this.showElementAddDialog(false); + }, + async beforeUnloadHandler(event) { + if ((this.blockedByThisUser && this.blocked) || this.objectIsBlocked) { + event.preventDefault(); + event.returnValue = 'There are unsaved changes, do you want to leave?'; + await this.beforeUnloadActions(); + return event.returnValue; + } + return null; } }, created() { this.pluginManager.registerComponentsLocally(this); }, + mounted () { + STUDIP.eventBus.on('studip:beforeunload', this.beforeUnloadHandler); + }, + beforeDestroy () { + STUDIP.eventBus.off('studip:beforeunload', this.beforeUnloadHandler); + }, watch: { async structuralElement() { -- GitLab