From 50acea3bfd0345d82e66a7c48b4318b5daa6d6b2 Mon Sep 17 00:00:00 2001
From: Ron Lucke <lucke@elan-ev.de>
Date: Mon, 26 Sep 2022 10:34:12 +0000
Subject: [PATCH] fix #887

Closes #887 and #1257

Merge request studip/studip!871
---
 .../JsonApi/Routes/Courseware/BlocksShow.php  |   2 +-
 .../Courseware/StructuralElementsShow.php     |   2 +
 .../assets/stylesheets/scss/courseware.scss   |  20 +++
 .../CoursewareAccordionContainer.vue          |   9 +-
 .../courseware/CoursewareActionWidget.vue     |  77 +++++++---
 .../courseware/CoursewareAudioBlock.vue       |   4 +-
 .../courseware/CoursewareBeforeAfterBlock.vue |  23 ++-
 .../courseware/CoursewareBlockActions.vue     |  97 +++++++-----
 .../courseware/CoursewareCanvasBlock.vue      |  13 +-
 .../courseware/CoursewareChartBlock.vue       |   3 +
 .../courseware/CoursewareCodeBlock.vue        |   3 +
 .../courseware/CoursewareConfirmBlock.vue     |   1 +
 .../courseware/CoursewareContainerActions.vue |  54 +++++--
 .../courseware/CoursewareDateBlock.vue        |   6 +-
 .../courseware/CoursewareDefaultBlock.vue     | 126 +++++++++++----
 .../courseware/CoursewareDefaultContainer.vue | 124 +++++++++++----
 .../courseware/CoursewareDialogCardsBlock.vue |   4 +-
 .../courseware/CoursewareDocumentBlock.vue    |   4 +-
 .../courseware/CoursewareDownloadBlock.vue    |   4 +-
 .../courseware/CoursewareEmbedBlock.vue       |   5 +-
 .../courseware/CoursewareFolderBlock.vue      |   3 +
 .../courseware/CoursewareGalleryBlock.vue     |   4 +-
 .../courseware/CoursewareHeadlineBlock.vue    |   8 +-
 .../courseware/CoursewareIframeBlock.vue      |   4 +-
 .../courseware/CoursewareImageMapBlock.vue    |   4 +-
 .../courseware/CoursewareKeyPointBlock.vue    |   8 +-
 .../courseware/CoursewareLinkBlock.vue        |   3 +
 .../CoursewareStructuralElement.vue           | 144 ++++++++++++++----
 .../CoursewareTableOfContentsBlock.vue        |   8 +-
 .../courseware/CoursewareTabsContainer.vue    |   9 +-
 .../courseware/CoursewareTextBlock.vue        |  12 +-
 .../courseware/CoursewareTypewriterBlock.vue  |   8 +-
 .../courseware/CoursewareVideoBlock.vue       |   3 +
 .../vue/components/courseware/block-mixin.js  |   3 +
 .../vue/store/courseware/courseware.module.js |  32 +++-
 35 files changed, 626 insertions(+), 208 deletions(-)

diff --git a/lib/classes/JsonApi/Routes/Courseware/BlocksShow.php b/lib/classes/JsonApi/Routes/Courseware/BlocksShow.php
index ba31717a7b8..4e5dd1724d8 100644
--- a/lib/classes/JsonApi/Routes/Courseware/BlocksShow.php
+++ b/lib/classes/JsonApi/Routes/Courseware/BlocksShow.php
@@ -18,7 +18,7 @@ class BlocksShow extends JsonApiController
         'container',
         'owner',
         'editor',
-        'edit_blocker',
+        'edit-blocker',
         'user-data-field',
         'user-progress',
     ];
diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsShow.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsShow.php
index f4c0766ecbe..4aa07166220 100644
--- a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsShow.php
+++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsShow.php
@@ -19,6 +19,7 @@ class StructuralElementsShow extends JsonApiController
         'ancestors',
         'children',
         'containers',
+        'containers.edit-blocker',
         'containers.blocks',
         'containers.blocks.edit-blocker',
         'containers.blocks.editor',
@@ -27,6 +28,7 @@ class StructuralElementsShow extends JsonApiController
         'containers.blocks.user-progress',
         'course',
         'editor',
+        'edit-blocker',
         'owner',
         'parent',
         'target'
diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss
index ed57ee883aa..902072803a7 100644
--- a/resources/assets/stylesheets/scss/courseware.scss
+++ b/resources/assets/stylesheets/scss/courseware.scss
@@ -837,6 +837,14 @@ ribbon end
             font-weight: 700;
             line-height: 2em;
             font-size: 1.1em;
+
+            &.cw-default-container-blocker-warning {
+                font-weight: 400;
+            }
+        }
+
+        img {
+            vertical-align: text-bottom;
         }
 
         .cw-container-actions {
@@ -977,6 +985,15 @@ form.cw-container-dialog-edit-form {
         font-weight: 700;
         line-height: 2em;
         font-size: 1.1em;
+
+        &.cw-default-block-invisible-info,
+        &.cw-default-block-blocker-warning {
+            font-weight: 400;
+        }
+    }
+
+    img {
+        vertical-align: text-bottom;
     }
 
     .cw-block-actions {
@@ -2003,6 +2020,9 @@ v i e w  w i d g e t
     .cw-action-widget-link {
         @include background-icon(group, clickable);
     }
+    .cw-action-widget-remove-lock{
+        @include background-icon(lock-unlocked, clickable);
+    }
 }
 .cw-export-widget {
     .cw-export-widget-export{
diff --git a/resources/vue/components/courseware/CoursewareAccordionContainer.vue b/resources/vue/components/courseware/CoursewareAccordionContainer.vue
index de50ae1db8b..7d9f7626232 100644
--- a/resources/vue/components/courseware/CoursewareAccordionContainer.vue
+++ b/resources/vue/components/courseware/CoursewareAccordionContainer.vue
@@ -4,6 +4,7 @@
         containerClass="cw-container-accordion"
         :canEdit="canEdit"
         :isTeacher="isTeacher"
+        @showEdit="setShowEdit"
         @storeContainer="storeContainer"
         @closeEdit="initCurrentData"
         @sortBlocks="enableSort"
@@ -114,6 +115,7 @@ export default {
     },
     data() {
         return {
+            showEdit: false,
             currentContainer: {},
             currentSections: [],
             unallocatedBlocks: [],
@@ -180,6 +182,9 @@ export default {
 
             this.currentSections = sections;
         },
+        setShowEdit(state) {
+            this.showEdit = state;
+        },
         addSection() {
             this.currentContainer.attributes.payload.sections.push({ name: '', icon: '', blocks: [] });
         },
@@ -243,7 +248,9 @@ export default {
     },
     watch: {
         blocks() {
-            this.initCurrentData();
+            if (!this.showEdit) {
+                this.initCurrentData();
+            }
         }
     }
 };
diff --git a/resources/vue/components/courseware/CoursewareActionWidget.vue b/resources/vue/components/courseware/CoursewareActionWidget.vue
index a32c7491d17..2db5c2f964d 100644
--- a/resources/vue/components/courseware/CoursewareActionWidget.vue
+++ b/resources/vue/components/courseware/CoursewareActionWidget.vue
@@ -9,37 +9,42 @@
                 </li>
                 <li class="cw-action-widget-show-consume-mode">
                     <button @click="showConsumeMode">
-                        <translate>Vollbild einschalten</translate>
+                        {{ $gettext('Vollbild einschalten') }}
                     </button>
                 </li>
-                <li v-if="canEdit" class="cw-action-widget-edit">
+                <li v-if="canEdit && !blockedByAnotherUser" class="cw-action-widget-edit">
                     <button @click="editElement">
-                        <translate>Seite bearbeiten</translate>
+                        {{ $gettext('Seite bearbeiten') }}
                     </button>
                 </li>
-                <li v-if="canEdit" class="cw-action-widget-sort">
+                <li v-if="canEdit && blockedByAnotherUser" class="cw-action-widget-remove-lock">
+                    <button @click="removeElementLock">
+                        {{ $gettext('Sperre aufheben') }}
+                    </button>
+                </li>
+                <li v-if="canEdit && !blockedByAnotherUser" class="cw-action-widget-sort">
                     <button @click="sortContainers">
-                        <translate>Abschnitte sortieren</translate>
+                        {{ $gettext('Abschnitte sortieren') }}
                     </button>
                 </li>
                 <li v-if="canEdit" class="cw-action-widget-add">
                     <button @click="addElement">
-                        <translate>Seite hinzufügen</translate>
+                        {{ $gettext('Seite hinzufügen') }}
                     </button>
                 </li>
                 <li class="cw-action-widget-info">
                     <button @click="showElementInfo">
-                        <translate>Informationen anzeigen</translate>
+                        {{ $gettext('Informationen anzeigen') }}
                     </button>
                 </li>
                 <li class="cw-action-widget-star">
                     <button @click="createBookmark">
-                        <translate>Lesezeichen setzen</translate>
+                        {{ $gettext('Lesezeichen setzen') }}
                     </button>
                 </li>
                 <li v-if="context.type === 'users'" class="cw-action-widget-link">
                     <button @click="linkElement">
-                        <translate>Öffentlichen Link erzeugen</translate>
+                        {{ $gettext('Öffentlichen Link erzeugen') }}
                     </button>
                 </li>
                 <li v-if="!isOwner" class="cw-action-widget-oer">
@@ -47,9 +52,9 @@
                         <translate>Material für den OER Campus vorschlagen</translate>
                     </button>
                 </li>
-                <li v-if="!isRoot && canEdit" class="cw-action-widget-trash">
+                <li v-if="!isRoot && canEdit && !blockedByAnotherUser" class="cw-action-widget-trash">
                     <button @click="deleteElement">
-                        <translate>Seite löschen</translate>
+                        {{ $gettext('Seite löschen') }}
                     </button>
                 </li>
             </ul>
@@ -77,6 +82,11 @@ export default {
             consumeMode: 'consumeMode',
             showToolbar: 'showToolbar',
             context: 'context',
+
+            blocked: 'currentElementBlocked',
+            blockerId: 'currentElementBlockerId',
+            blockedByThisUser: 'currentElementBlockedByThisUser',
+            blockedByAnotherUser: 'currentElementBlockedByAnotherUser',
         }),
         isRoot() {
             if (!this.structuralElement) {
@@ -94,18 +104,6 @@ export default {
         currentId() {
             return this.structuralElement?.id;
         },
-        blocked() {
-            return this.structuralElement?.relationships['edit-blocker'].data !== null;
-        },
-        blockerId() {
-            return this.blocked ? this.structuralElement?.relationships['edit-blocker'].data?.id : null;
-        },
-        blockedByThisUser() {
-            return this.blocked && this.userId === this.blockerId;
-        },
-        blockedByAnotherUser() {
-            return this.blocked && this.userId !== this.blockerId;
-        },
         tocText() {
             return this.showToolbar ? this.$gettext('Inhaltsverzeichnis ausblenden') : this.$gettext('Inhaltsverzeichnis anzeigen');
         },
@@ -123,6 +121,7 @@ export default {
             showElementDeleteDialog: 'showElementDeleteDialog',
             showElementInfoDialog: 'showElementInfoDialog',
             showElementLinkDialog: 'showElementLinkDialog',
+            showElementRemoveLockDialog: 'showElementRemoveLockDialog',
             updateShowSuggestOerDialog: 'updateShowSuggestOerDialog',
             setStructuralElementSortMode: 'setStructuralElementSortMode',
             companionInfo: 'companionInfo',
@@ -131,9 +130,11 @@ export default {
             setConsumeMode: 'coursewareConsumeMode',
             setViewMode: 'coursewareViewMode',
             setShowToolbar: 'coursewareShowToolbar',
-            setSelectedToolbarItem: 'coursewareSelectedToolbarItem'
+            setSelectedToolbarItem: 'coursewareSelectedToolbarItem',
+            loadStructuralElement: 'loadStructuralElement',
         }),
         async editElement() {
+            await this.loadStructuralElement(this.currentId);
             if (this.blockedByAnotherUser) {
                 this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
 
@@ -152,10 +153,36 @@ export default {
             }
             this.showElementEditDialog(true);
         },
-        sortContainers() {
+        async removeElementLock() {
+            this.showElementRemoveLockDialog(true);
+        },
+        async sortContainers() {
+            await this.loadStructuralElement(this.currentId);
+            if (this.blockedByAnotherUser) {
+                this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
+
+                return false;
+            }
+            try {
+                await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' });
+            } catch (error) {
+                if (error.status === 409) {
+                    this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
+                } else {
+                    console.log(error);
+                }
+
+                return false;
+            }
             this.setStructuralElementSortMode(true);
         },
         async deleteElement() {
+            await this.loadStructuralElement(this.currentId);
+            if (this.blockedByAnotherUser) {
+                this.companionInfo({ info: this.$gettextInterpolate('Löschen nicht möglich, da %{blockingUserName} die Seite bearbeitet.', {blockingUserName: this.blockingUserName}) });
+
+                return false;
+            }
             await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' });
             this.showElementDeleteDialog(true);
         },
diff --git a/resources/vue/components/courseware/CoursewareAudioBlock.vue b/resources/vue/components/courseware/CoursewareAudioBlock.vue
index 218f11dc666..77e3166a590 100644
--- a/resources/vue/components/courseware/CoursewareAudioBlock.vue
+++ b/resources/vue/components/courseware/CoursewareAudioBlock.vue
@@ -5,6 +5,7 @@
             :canEdit="canEdit"
             :isTeacher="isTeacher"
             :preview="true"
+            @showEdit="initCurrentData"
             @storeEdit="storeBlock"
             @closeEdit="initCurrentData"
         >
@@ -167,11 +168,12 @@
 import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
 import CoursewareFileChooser from './CoursewareFileChooser.vue';
 import CoursewareFolderChooser from './CoursewareFolderChooser.vue';
-
+import { blockMixin } from './block-mixin.js';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-audio-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
         CoursewareFileChooser,
diff --git a/resources/vue/components/courseware/CoursewareBeforeAfterBlock.vue b/resources/vue/components/courseware/CoursewareBeforeAfterBlock.vue
index 2c68473b9cd..f714f25bc4a 100644
--- a/resources/vue/components/courseware/CoursewareBeforeAfterBlock.vue
+++ b/resources/vue/components/courseware/CoursewareBeforeAfterBlock.vue
@@ -5,11 +5,12 @@
             :canEdit="canEdit"
             :isTeacher="isTeacher"
             :preview="true"
+            @showEdit="initCurrentData"
             @storeEdit="storeBlock"
             @closeEdit="initCurrentData"
         >
             <template #content>
-                <TwentyTwenty :before="currentBeforeUrl" :after="currentAfterUrl" />
+                <TwentyTwenty v-if="!isEmpty" :before="currentBeforeUrl" :after="currentAfterUrl" />
             </template>
             <template v-if="canEdit" #edit>
                 <form class="default" @submit.prevent="">
@@ -61,12 +62,14 @@
 <script>
 import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
 import CoursewareFileChooser from './CoursewareFileChooser.vue';
+import { blockMixin } from './block-mixin.js';
 import TwentyTwenty from 'vue-twentytwenty';
 import 'vue-twentytwenty/dist/vue-twentytwenty.css';
 import { mapActions } from 'vuex';
 
 export default {
     name: 'courseware-before-after-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
         CoursewareFileChooser,
@@ -145,6 +148,7 @@ export default {
             this.currentAfterFile  = this.afterFile;
         });
 
+        this.loadImages();
         this.initCurrentData();
     },
     methods: {
@@ -153,6 +157,23 @@ export default {
             loadFileRefs: 'loadFileRefs',
             companionWarning: 'companionWarning',
         }),
+        loadImages() {
+            this.loadFileRefs(this.block.id).then((response) => {
+                for (let i = 0; i < response.length; i++) {
+                    if (response[i].id === this.beforeFileId) {
+                        this.beforeFile = response[i];
+                    }
+
+                    if (response[i].id === this.afterFileId) {
+                        this.afterFile = response[i];
+                    }
+                }
+
+                this.currentBeforeFile = this.beforeFile;
+                this.currentAfterFile  = this.afterFile;
+            });
+        },
+
         initCurrentData() {
             this.currentBeforeSource = this.beforeSource;
             this.currentBeforeFileId = this.beforeFileId;
diff --git a/resources/vue/components/courseware/CoursewareBlockActions.vue b/resources/vue/components/courseware/CoursewareBlockActions.vue
index 2948369385e..031e3aa4d99 100644
--- a/resources/vue/components/courseware/CoursewareBlockActions.vue
+++ b/resources/vue/components/courseware/CoursewareBlockActions.vue
@@ -7,6 +7,7 @@
             @setVisibility="setVisibility"
             @showInfo="showInfo"
             @deleteBlock="deleteBlock"
+            @removeLock="removeLock"
         />
     </div>
 </template>
@@ -28,57 +29,69 @@ export default {
         },
         block: Object,
     },
-    data() {
-        return {
-            menuItems: [],
-        };
-    },
     computed: {
         ...mapGetters({
             userId: 'userId',
+            userIsTeacher: 'userIsTeacher',
         }),
         blocked() {
-            return this.block?.relationships['edit-blocker'].data !== null;
+            return this.block?.relationships?.['edit-blocker']?.data !== null;
         },
         blockerId() {
-            return this.blocked ? this.block?.relationships['edit-blocker'].data?.id : null;
+            return this.blocked ? this.block?.relationships?.['edit-blocker']?.data?.id : null;
         },
-    },
-    mounted() {
-        if (this.canEdit) {
-            if (!this.deleteOnly) {
-                this.menuItems.push({
-                    id: 1,
-                    label: this.$gettext('Block bearbeiten'),
-                    icon: 'edit',
-                    emit: 'editBlock',
-                });
-                this.menuItems.push({
-                    id: 2,
-                    label: this.block.attributes.visible
-                        ? this.$gettext('unsichtbar setzen')
-                        : this.$gettext('sichtbar setzen'),
-                    icon: this.block.attributes.visible ? 'visibility-visible' : 'visibility-invisible', // do we change the icons ?
-                    emit: 'setVisibility',
-                });
-                this.menuItems.push({
-                    id: 7,
-                    label: this.$gettext('Informationen zum Block'),
-                    icon: 'info',
-                    emit: 'showInfo',
-                });
+        blockedByThisUser() {
+            return this.blocked && this.userId === this.blockerId;
+        },
+        blockedByAnotherUser() {
+            return this.blocked && this.userId !== this.blockerId;
+        },
+        menuItems() {
+            let menuItems = [];
+            if (this.canEdit) {
+                if (!this.deleteOnly) {
+                    if (!this.blocked) {
+                        menuItems.push({ id: 1, label: this.$gettext('Block bearbeiten'), icon: 'edit', emit: 'editBlock' });
+                        menuItems.push({
+                            id: 2,
+                            label: this.block.attributes.visible
+                                ? this.$gettext('unsichtbar setzen')
+                                : this.$gettext('sichtbar setzen'),
+                            icon: this.block.attributes.visible ? 'visibility-visible' : 'visibility-invisible', // do we change the icons ?
+                            emit: 'setVisibility',
+                        });
+                    }
+                    if (this.blocked && this.blockedByAnotherUser && this.userIsTeacher) {
+                        menuItems.push({
+                            id: 8,
+                            label: this.$gettext('Sperre aufheben'),
+                            icon: 'lock-unlocked',
+                            emit: 'removeLock',
+                        });
+                    }
+                    if (!this.blocked || this.blockedByThisUser) {
+                        menuItems.push({
+                            id: 9,
+                            label: this.$gettext('Block löschen'), 
+                            icon: 'trash',
+                            emit: 'deleteBlock' 
+                        });
+                    }
+                    menuItems.push({
+                        id: 7,
+                        label: this.$gettext('Informationen zum Block'),
+                        icon: 'info',
+                        emit: 'showInfo',
+                    });
+                }
             }
-            this.menuItems.push({
-                id: 9,
-                label: this.$gettext('Block löschen'), 
-                icon: 'trash',
-                emit: 'deleteBlock',
+            menuItems.sort((a, b) => {
+                return a.id > b.id ? 1 : b.id > a.id ? -1 : 0;
             });
+
+            return menuItems;
         }
 
-        this.menuItems.sort((a, b) => {
-            return a.id > b.id ? 1 : b.id > a.id ? -1 : 0;
-        });
     },
     methods: {
         ...mapActions({
@@ -117,12 +130,12 @@ export default {
 
             await this.unlockObject({ id: this.block.id, type: 'courseware-blocks' });
         },
-        copyToClipboard() {
-            // use JSONAPI to copy to clipboard
-        },
         deleteBlock() {
             this.$emit('deleteBlock');
         },
+        removeLock() {
+            this.$emit('removeLock');
+        }
     },
 };
 </script>
diff --git a/resources/vue/components/courseware/CoursewareCanvasBlock.vue b/resources/vue/components/courseware/CoursewareCanvasBlock.vue
index de474cee2f9..31c9258f50c 100644
--- a/resources/vue/components/courseware/CoursewareCanvasBlock.vue
+++ b/resources/vue/components/courseware/CoursewareCanvasBlock.vue
@@ -5,6 +5,7 @@
             :canEdit="canEdit"
             :isTeacher="isTeacher"
             :preview="true"
+            @showEdit="initCurrentData"
             @storeEdit="storeBlock"
             @closeEdit="initCurrentData"
         >
@@ -147,11 +148,12 @@
 import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
 import CoursewareFileChooser from './CoursewareFileChooser.vue';
 import CoursewareFolderChooser from './CoursewareFolderChooser.vue';
-
+import { blockMixin } from './block-mixin.js';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-canvas-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
         CoursewareFileChooser,
@@ -261,6 +263,7 @@ export default {
             this.initCurrentData();
             this.buildCanvas();
         });
+        this.loadImageFile();
     },
     methods: {
         ...mapActions({
@@ -286,6 +289,14 @@ export default {
                 this.Text = JSON.parse(this.canvasDraw.Text);
             }
         },
+        loadImageFile() {
+            this.loadFileRefs(this.block.id).then((response) => {
+                this.file = response[0];
+                this.currentFile = this.file;
+                this.initCurrentData();
+                this.buildCanvas();
+            });
+        },
         updateCurrentFile(file) {
             this.currentFile = file;
             this.currentFileId = file.id;
diff --git a/resources/vue/components/courseware/CoursewareChartBlock.vue b/resources/vue/components/courseware/CoursewareChartBlock.vue
index 8dd58ec2433..4329659fe90 100644
--- a/resources/vue/components/courseware/CoursewareChartBlock.vue
+++ b/resources/vue/components/courseware/CoursewareChartBlock.vue
@@ -5,6 +5,7 @@
             :canEdit="canEdit"
             :isTeacher="isTeacher"
             :preview="true"
+            @showEdit="initCurrentData"
             @storeEdit="storeBlock"
             @closeEdit="initCurrentData"
         >
@@ -84,12 +85,14 @@
 
 <script>
 import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
+import { blockMixin } from './block-mixin.js';
 import Chart from 'chart.js';
 import { mapActions } from 'vuex';
 import StudipIcon from '../StudipIcon.vue';
 
 export default {
     name: 'courseware-chart-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
         StudipIcon,
diff --git a/resources/vue/components/courseware/CoursewareCodeBlock.vue b/resources/vue/components/courseware/CoursewareCodeBlock.vue
index 275a77f5e87..db383735a42 100644
--- a/resources/vue/components/courseware/CoursewareCodeBlock.vue
+++ b/resources/vue/components/courseware/CoursewareCodeBlock.vue
@@ -5,6 +5,7 @@
             :canEdit="canEdit"
             :isTeacher="isTeacher"
             :preview="true"
+            @showEdit="initCurrentData"
             @storeEdit="storeBlock"
             @closeEdit="initCurrentData"
         >
@@ -35,12 +36,14 @@
 
 <script>
 import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
+import { blockMixin } from './block-mixin.js';
 import hljs from 'highlight.js';
 
 import { mapActions } from 'vuex';
 
 export default {
     name: 'courseware-code-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
     },
diff --git a/resources/vue/components/courseware/CoursewareConfirmBlock.vue b/resources/vue/components/courseware/CoursewareConfirmBlock.vue
index 4e602f82e1f..6d37a576d4b 100644
--- a/resources/vue/components/courseware/CoursewareConfirmBlock.vue
+++ b/resources/vue/components/courseware/CoursewareConfirmBlock.vue
@@ -6,6 +6,7 @@
             :isTeacher="isTeacher"
             :preview="true"
             :defaultGrade="false"
+            @showEdit="initCurrentData"
             @storeEdit="storeBlock"
             @closeEdit="initCurrentData"
         >
diff --git a/resources/vue/components/courseware/CoursewareContainerActions.vue b/resources/vue/components/courseware/CoursewareContainerActions.vue
index 40e618e24e2..fdea003c010 100644
--- a/resources/vue/components/courseware/CoursewareContainerActions.vue
+++ b/resources/vue/components/courseware/CoursewareContainerActions.vue
@@ -6,11 +6,13 @@
             @editContainer="editContainer"
             @deleteContainer="deleteContainer"
             @sortBlocks="sortBlocks"
+            @removeLock="removeLock"
         />
     </div>
 </template>
 
 <script>
+import { mapGetters } from 'vuex';
 export default {
     name: 'courseware-container-actions',
     props: {
@@ -18,20 +20,47 @@ export default {
         container: Object,
     },
     computed: {
+        ...mapGetters({
+            userId: 'userId',
+            userIsTeacher: 'userIsTeacher',
+        }),
+        blocked() {
+            return this.container?.relationships?.['edit-blocker']?.data !== null;
+        },
+        blockerId() {
+            return this.blocked ? this.container?.relationships?.['edit-blocker']?.data?.id : null;
+        },
+        blockedByThisUser() {
+            return this.blocked && this.userId === this.blockerId;
+        },
+        blockedByAnotherUser() {
+            return this.blocked && this.userId !== this.blockerId;
+        },
         menuItems() {
-            if (this.container.attributes["container-type"] === 'list') {
-                return [
-                    { id: 1, label: this.$gettext('Blöcke sortieren'), icon: 'arr_1sort', emit: 'sortBlocks' },
-                    { id: 2, label: this.$gettext('Abschnitt löschen'), icon: 'trash', emit: 'deleteContainer' }
-                ];
-            } else {
-                return [
-                    { id: 1, label: this.$gettext('Abschnitt bearbeiten'), icon: 'edit', emit: 'editContainer' },
-                    { id: 2, label: this.$gettext('Blöcke sortieren'), icon: 'arr_1sort', emit: 'sortBlocks' },
-                    { id: 3, label: this.$gettext('Abschnitt löschen'), icon: 'trash', emit: 'deleteContainer' },
-                ];
+            let menuItems = [];
+            if (!this.blockedByAnotherUser) {
+                if (this.container.attributes["container-type"] !== 'list') {
+                    menuItems.push({ id: 1, label: this.$gettext('Abschnitt bearbeiten'), icon: 'edit', emit: 'editContainer' });
+                }
+                menuItems.push({ id: 2, label: this.$gettext('Blöcke sortieren'), icon: 'arr_1sort', emit: 'sortBlocks' });
+                menuItems.push({ id: 3, label: this.$gettext('Abschnitt löschen'), icon: 'trash', emit: 'deleteContainer' });
             }
+
+            if (this.blocked && this.blockedByAnotherUser && this.userIsTeacher) {
+                menuItems.push({
+                    id: 4,
+                    label: this.$gettext('Sperre aufheben'),
+                    icon: 'lock-unlocked',
+                    emit: 'removeLock',
+                });
+            }
+
+            menuItems.sort((a, b) => {
+                return a.id > b.id ? 1 : b.id > a.id ? -1 : 0;
+            });
+            return menuItems;
         },
+
     },
     methods: {
         menuAction(action) {
@@ -45,6 +74,9 @@ export default {
         },
         sortBlocks() {
             this.$emit('sortBlocks');
+        },
+        removeLock() {
+            this.$emit('removeLock');
         }
     },
 };
diff --git a/resources/vue/components/courseware/CoursewareDateBlock.vue b/resources/vue/components/courseware/CoursewareDateBlock.vue
index da119891f81..21623f75579 100644
--- a/resources/vue/components/courseware/CoursewareDateBlock.vue
+++ b/resources/vue/components/courseware/CoursewareDateBlock.vue
@@ -5,6 +5,7 @@
             :canEdit="canEdit"
             :isTeacher="isTeacher"
             :preview="true"
+            @showEdit="initCurrentData"
             @storeEdit="storeBlock"
             @closeEdit="initCurrentData"
         >
@@ -84,9 +85,10 @@
 <script>
 import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
 import { mapActions } from 'vuex';
-
+import { blockMixin } from './block-mixin.js';
 export default {
     name: 'courseware-date-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
     },
@@ -196,8 +198,6 @@ export default {
                     containerId: this.block.relationships.container.data.id,
                 });
             }
-
-
         },
     },
 };
diff --git a/resources/vue/components/courseware/CoursewareDefaultBlock.vue b/resources/vue/components/courseware/CoursewareDefaultBlock.vue
index dd56fbe5191..5df64c99a5a 100644
--- a/resources/vue/components/courseware/CoursewareDefaultBlock.vue
+++ b/resources/vue/components/courseware/CoursewareDefaultBlock.vue
@@ -3,12 +3,15 @@
         <div class="cw-content-wrapper" :class="[showEditMode ? 'cw-content-wrapper-active' : '']">
             <header v-if="showEditMode" class="cw-block-header">
                 <span class="cw-sortable-handle"></span>
-                <span v-if="!block.attributes.visible" class="cw-default-block-invisible-info">
-                    <studip-icon shape="visibility-invisible" />
-                </span>
+                <studip-icon v-if="!block.attributes.visible" shape="visibility-invisible" />
+                <studip-icon v-if="blockedByAnotherUser" shape="lock-locked" />
                 <span>{{ blockTitle }}</span>
+                <span v-if="blockedByAnotherUser" class="cw-default-block-blocker-warning">
+                    | {{ $gettextInterpolate('wird im Moment von %{ userName } bearbeitet', { userName: this.blockingUserName }) }}
+                </span>
+
                 <span v-if="!block.attributes.visible" class="cw-default-block-invisible-info">
-                    (<translate>unsichtbar für Nutzende ohne Schreibrecht</translate>)
+                    | {{ $gettext('unsichtbar für Nutzende ohne Schreibrecht') }}
                 </span>
                 <courseware-block-actions
                     :block="block"
@@ -18,6 +21,7 @@
                     @showInfo="displayFeature('Info')"
                     @showExportOptions="displayFeature('ExportOptions')"
                     @deleteBlock="displayDeleteDialog()"
+                    @removeLock="displayRemoveLockDialog()"
                 />
             </header>
             <div v-if="showContent" class="cw-block-content">
@@ -59,8 +63,18 @@
             height="180"
             width="360"
             @confirm="executeDelete"
-            @close="showDeleteDialog = false"
+            @close="closeDeleteDialog"
+        ></studip-dialog>
+        <studip-dialog
+            v-if="showRemoveLockDialog"
+            :title="textRemoveLockTitle"
+            :question="textRemoveLockAlert"
+            height="200"
+            width="450"
+            @confirm="executeRemoveLock"
+            @close="showRemoveLockDialog = false"
         ></studip-dialog>
+
     </div>
 </template>
 
@@ -118,9 +132,12 @@ export default {
             showContent: true,
             showEditModeShortcut: false,
             showDeleteDialog: false,
+            showRemoveLockDialog: false,
             currentComments: [],
             textDeleteTitle: this.$gettext('Block unwiderruflich löschen'),
             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?'),
         };
     },
     computed: {
@@ -129,6 +146,7 @@ export default {
             containerById: 'courseware-containers/byId',
             context: 'context',
             userId: 'userId',
+            userById: 'users/byId',
             viewMode: 'viewMode',
         }),
         showEditMode() {
@@ -142,10 +160,10 @@ export default {
             return this.viewMode === 'discuss';
         },
         blocked() {
-            return this.block?.relationships['edit-blocker'].data !== null;
+            return this.block?.relationships?.['edit-blocker']?.data !== null;
         },
         blockerId() {
-            return this.blocked ? this.block?.relationships['edit-blocker'].data?.id : null;
+            return this.blocked ? this.block?.relationships?.['edit-blocker']?.data?.id : null;
         },
         blockedByThisUser() {
             return this.blocked && this.userId === this.blockerId;
@@ -153,6 +171,16 @@ export default {
         blockedByAnotherUser() {
             return this.blocked && this.userId !== this.blockerId;
         },
+        blockingUser() {
+            if (this.blockedByAnotherUser) {
+                return this.userById({id: this.blockerId});
+            }
+
+            return null;
+        },
+        blockingUserName() {
+            return this.blockingUser ? this.blockingUser.attributes['formatted-name'] : '';
+        },
         blockTitle() {
             const type = this.block.attributes['block-type'];
 
@@ -175,10 +203,12 @@ export default {
     methods: {
         ...mapActions({
             companionInfo: 'companionInfo',
+            companionWarning: 'companionWarning',
             deleteBlock: 'deleteBlockInContainer',
             lockObject: 'lockObject',
             unlockObject: 'unlockObject',
             loadContainer: 'loadContainer',
+            loadBlock: 'courseware-blocks/loadById',
             updateContainer: 'updateContainer',
         }),
         async displayFeature(element) {
@@ -192,26 +222,16 @@ export default {
             this.showContent = true;
             if (element) {
                 if (element === 'Edit') {
-                    await this.loadContainer(this.block.relationships.container.data.id);
+                    await this.loadBlock({ id: this.block.id, options: { include: 'edit-blocker' } });
                     if (!this.blocked) {
-                        try {
-                            await this.lockObject({ id: this.block.id, type: 'courseware-blocks' });
-                        } catch(error) {
-                            if (error.status === 403) {
-                                this.companionInfo({ info: this.$gettext('Dieser Block wird bereits bearbeitet.') });
-                            } else {
-                                console.log(error);
-                            }
-
-                            return false;
-                        }
+                        await this.lockObject({ id: this.block.id, type: 'courseware-blocks' });
                         if (!this.preview) {
                             this.showContent = false;
                         }
                         this['show' + element] = true;
                         this.showFeatures = true;
                     } else {
-                        if (this.userId === this.blockerId) {
+                        if (this.blockedByThisUser) {
                             if (!this.preview) {
                                 this.showContent = false;
                             }
@@ -227,25 +247,64 @@ export default {
                 }
             }
         },
+        prepareStoreEdit() {
+            // storeEdit is only emitted when the block is not in deleting process.
+            if (!this.showDeleteDialog) {
+                this.storeBlock();
+            }
+        },
+        async storeBlock() {
+            await this.loadBlock({ id: this.block.id, options: { include: 'edit-blocker' } });
+
+            if (this.blockedByThisUser) {
+                this.$emit('storeEdit');
+            }
+
+            if (this.blockedByAnotherUser) {
+                this.companionWarning({ info: this.$gettextInterpolate('Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.', {blockingUserName: this.blockingUserName}) });
+                this.displayFeature(false);
+                this.$emit('closeEdit');
+            }
+            if (this.blockerId === null) {
+                await this.lockObject({ id: this.block.id, type: 'courseware-blocks' });
+                this.$emit('storeEdit');
+            }
+        },
         async closeEdit() {
+            await this.loadBlock({ id: this.block.id , options: { include: 'edit-blocker' } }); // has block editor lock changed?
             this.displayFeature(false);
             this.$emit('closeEdit');
-            await this.unlockObject({ id: this.block.id, type: 'courseware-blocks' });
-            this.loadContainer(this.block.relationships.container.data.id); // to update block editor lock
+            if (this.blockedByThisUser) {
+                await this.unlockObject({ id: this.block.id, type: 'courseware-blocks' });
+            }
+            this.loadBlock({ id: this.block.id , options: { include: 'edit-blocker' } }); // to update block editor lock
         },
         async displayDeleteDialog() {
+            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.showDeleteDialog = true;
             } else {
-                if (this.userId === this.blockerId) {
+                if (this.blockedByThisUser) {
                     this.showDeleteDialog = true;
                 } else {
-                    this.companionInfo({ info: 'Dieser Block wird bereits bearbeitet.' });
+                    this.companionInfo({ info: this.$gettextInterpolate('Löschen nicht möglich, da %{blockingUserName} den Block bearbeitet.', {blockingUserName: this.blockingUserName}) });
                 }
             }
         },
+        async closeDeleteDialog() {
+            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.showDeleteDialog = false;
+        },
         async executeDelete() {
+            await this.loadBlock({ id: this.block.id, options: { include: 'edit-blocker' } });
+            if (this.blockedByAnotherUser) {
+                this.companionInfo({ info: this.$gettextInterpolate('Löschen nicht möglich, da %{blockingUserName} die Bearbeitung übernommen hat.', {blockingUserName: this.blockingUserName}) });
+                return false;
+            }
             const containerId = this.block.relationships.container.data.id;
             await this.loadContainer(containerId);
             let container = this.containerById({id: containerId});
@@ -277,13 +336,20 @@ export default {
                 containerId: containerId,
             });
         },
-
-        prepareStoreEdit() {
-            // storeEdit is only emitted when the block is not in deleting process.
-            if (!this.showDeleteDialog) {
-                this.$emit('storeEdit');
-            }
+        displayRemoveLockDialog() {
+            this.showRemoveLockDialog = true;
+        },
+        async executeRemoveLock() {
+            await this.unlockObject({ id: this.block.id , type: 'courseware-blocks' });
+            await this.loadBlock({ id: this.block.id });
+            this.showRemoveLockDialog = false;
         }
+
     },
+    watch: {
+        showEdit(state) {
+            this.$emit('showEdit', state);
+        }
+    }
 };
 </script>
diff --git a/resources/vue/components/courseware/CoursewareDefaultContainer.vue b/resources/vue/components/courseware/CoursewareDefaultContainer.vue
index a24e9a8bd51..962febba6f3 100644
--- a/resources/vue/components/courseware/CoursewareDefaultContainer.vue
+++ b/resources/vue/components/courseware/CoursewareDefaultContainer.vue
@@ -5,13 +5,18 @@
     >
         <div class="cw-container-content">
             <header v-if="showEditMode && canEdit" class="cw-container-header">
+                <studip-icon v-if="blockedByAnotherUser" shape="lock-locked" />
                 <span>{{ container.attributes.title }} ({{container.attributes.width}})</span>
+                <span v-if="blockedByAnotherUser" class="cw-default-container-blocker-warning">
+                    | {{ $gettextInterpolate('wird im Moment von %{ userName } bearbeitet', { userName: this.blockingUserName }) }}
+                </span>
                 <courseware-container-actions
                     :canEdit="canEdit"
                     :container="container"
                     @editContainer="displayEditDialog"
                     @deleteContainer="displayDeleteDialog"
                     @sortBlocks="sortBlocks"
+                    @removeLock="displayRemoveLockDialog"
                 />
             </header>
             <div class="cw-block-wrapper" :class="{ 'cw-block-wrapper-active': showEditMode }">
@@ -44,6 +49,17 @@
                 @confirm="executeDelete"
                 @close="closeDeleteDialog"
             ></studip-dialog>
+
+            <studip-dialog
+                v-if="showRemoveLockDialog"
+                :title="textRemoveLockTitle"
+                :question="textRemoveLockAlert"
+                height="200"
+                width="450"
+                @confirm="executeRemoveLock"
+                @close="showRemoveLockDialog = false"
+            ></studip-dialog>
+
         </div>
     </div>
 </template>
@@ -69,16 +85,20 @@ export default {
         return {
             showDeleteDialog: false,
             showEditDialog: false,
+            showRemoveLockDialog: false,
             textEditConfirm: this.$gettext('Speichern'),
             textEditClose: this.$gettext('Schließen'),
             textEditTitle: this.$gettext('Abschnitt bearbeiten'),
             textDeleteTitle: this.$gettext('Abschnitt unwiderruflich löschen'),
             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?'),
         };
     },
     computed: {
         ...mapGetters({
             userId: 'userId',
+            userById: 'users/byId',
         }),
         showEditMode() {
             return this.$store.getters.viewMode === 'edit';
@@ -87,10 +107,10 @@ export default {
             return this.container.attributes.payload.colspan ? this.container.attributes.payload.colspan : 'full';
         },
         blocked() {
-            return this.container?.relationships['edit-blocker'].data !== null;
+            return this.container?.relationships?.['edit-blocker']?.data !== null;
         },
         blockerId() {
-            return this.blocked ? this.container?.relationships['edit-blocker'].data?.id : null;
+            return this.blocked ? this.container?.relationships?.['edit-blocker']?.data?.id : null;
         },
         blockedByThisUser() {
             return this.blocked && this.userId === this.blockerId;
@@ -98,53 +118,88 @@ export default {
         blockedByAnotherUser() {
             return this.blocked && this.userId !== this.blockerId;
         },
+        blockingUser() {
+            if (this.blockedByAnotherUser) {
+                return this.userById({id: this.blockerId});
+            }
+
+            return null;
+        },
+        blockingUserName() {
+            return this.blockingUser ? this.blockingUser.attributes['formatted-name'] : '';
+        },
     },
     methods: {
         ...mapActions({
+            companionInfo: 'companionInfo',
+            companionWarning: 'companionWarning',
+            loadContainer: 'courseware-containers/loadById',
             deleteContainer: 'deleteContainer',
             lockObject: 'lockObject',
             unlockObject: 'unlockObject',
             companionInfo: 'companionInfo',
         }),
         async displayEditDialog() {
+            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
             if (this.blockedByAnotherUser) {
                 this.companionInfo({ info: this.$gettext('Dieser Abschnitt wird bereits bearbeitet.') });
 
                 return false;
             }
-            try {
-                await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
-            } catch(error) {
-                if (error.status === 409) {
-                    this.companionInfo({ info: this.$gettext('Dieser Abschnitt wird bereits bearbeitet.') });
-                } else {
-                    console.log(error);
-                }
-
-                return false;
-            }
 
+            await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
             this.showEditDialog = true;
         },
         async closeEdit() {
+            await this.loadContainer({ id: this.container.id });
             this.$emit('closeEdit');
             this.showEditDialog = false;
-            await this.unlockObject({ id: this.container.id, type: 'courseware-containers' });
+            if (this.blockedByThisUser) {
+                await this.unlockObject({ id: this.container.id, type: 'courseware-containers' });
+            }
+            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
         },
         async storeContainer() {
-            this.$emit('storeContainer');
+            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
+            if (this.blockedByThisUser) {
+                this.$emit('storeContainer');
+            }
+            if (this.blockedByAnotherUser) {
+                this.companionWarning({ info: this.$gettextInterpolate('Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.', {blockingUserName: this.blockingUserName}) });
+                this.$emit('closeEdit');
+            }
+            if (this.blockerId === null) {
+                await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
+                this.$emit('storeContainer');
+            }
             this.showEditDialog = false;
-            // await this.unlockObject({ id: this.container.id, type: 'courseware-containers' });
         },
         async displayDeleteDialog() {
-            await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
-            this.showDeleteDialog = true;
+            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.showDeleteDialog = true;
+            } else {
+                if (this.blockedByThisUser) {
+                    this.showDeleteDialog = true;
+                } else {
+                    this.companionInfo({ info: this.$gettextInterpolate('Löschen nicht möglich, da %{blockingUserName} den Abschnitt bearbeitet.', {blockingUserName: this.blockingUserName}) });
+                }
+            }
         },
         async closeDeleteDialog() {
-            await this.unlockObject({ id: this.container.id, type: 'courseware-containers' });
+            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.showDeleteDialog = false;
         },
         async executeDelete() {
+            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
+            if (this.blockedByAnotherUser) {
+                this.companionInfo({ info: this.$gettextInterpolate('Löschen nicht möglich, da %{blockingUserName} die Bearbeitung übernommen hat.', {blockingUserName: this.blockingUserName}) });
+                return false;
+            }
             await this.deleteContainer({
                 containerId: this.container.id,
                 structuralElementId: this.container.relationships['structural-element'].data.id,
@@ -155,24 +210,31 @@ export default {
             this.showDeleteDialog = false;
         },
         async sortBlocks() {
+            await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } });
             if (this.blockedByAnotherUser) {
                 this.companionInfo({ info: this.$gettext('Dieser Abschnitt wird bereits bearbeitet.') });
 
                 return false;
             }
-            try {
-                await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
-            } catch(error) {
-                if (error.status === 409) {
-                    this.companionInfo({ info: this.$gettext('Dieser Abschnitt wird bereits bearbeitet.') });
-                } else {
-                    console.log(error);
-                }
-
-                return false;
-            }
+            await this.lockObject({ id: this.container.id, type: 'courseware-containers' });
             this.$emit('sortBlocks');
-        }
+        },
+        displayRemoveLockDialog() {
+            this.showRemoveLockDialog = true;
+        },
+        async executeRemoveLock() {
+            await this.unlockObject({ id: this.container.id , type: 'courseware-containers' });
+            await this.loadContainer({ id: this.container.id });
+            this.showRemoveLockDialog = false;
+        },
+
     },
+
+    watch: {
+        showEditDialog(state) {
+            this.$emit('showEdit', state);
+        }
+    }
+
 };
 </script>
diff --git a/resources/vue/components/courseware/CoursewareDialogCardsBlock.vue b/resources/vue/components/courseware/CoursewareDialogCardsBlock.vue
index a8173c7d193..022590bb005 100644
--- a/resources/vue/components/courseware/CoursewareDialogCardsBlock.vue
+++ b/resources/vue/components/courseware/CoursewareDialogCardsBlock.vue
@@ -5,6 +5,7 @@
             :canEdit="canEdit"
             :isTeacher="isTeacher"
             :preview="true"
+            @showEdit="initCurrentData"
             @storeEdit="storeBlock"
             @closeEdit="initCurrentData"
         >
@@ -115,12 +116,13 @@ import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
 import CoursewareFileChooser from './CoursewareFileChooser.vue';
 import CoursewareTabs from './CoursewareTabs.vue';
 import CoursewareTab from './CoursewareTab.vue';
-
+import { blockMixin } from './block-mixin.js';
 import { mapActions } from 'vuex';
 import StudipIcon from '../StudipIcon.vue';
 
 export default {
     name: 'courseware-dialog-cards-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
         CoursewareFileChooser,
diff --git a/resources/vue/components/courseware/CoursewareDocumentBlock.vue b/resources/vue/components/courseware/CoursewareDocumentBlock.vue
index 134e167ca45..f24f5f82155 100644
--- a/resources/vue/components/courseware/CoursewareDocumentBlock.vue
+++ b/resources/vue/components/courseware/CoursewareDocumentBlock.vue
@@ -5,6 +5,7 @@
             :canEdit="canEdit"
             :isTeacher="isTeacher"
             :preview="false"
+            @showEdit="initCurrentData"
             @storeEdit="storeBlock"
             @closeEdit="initCurrentData"
         >
@@ -69,6 +70,7 @@
 <script>
 import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
 import CoursewareFileChooser from './CoursewareFileChooser.vue';
+import { blockMixin } from './block-mixin.js';
 import * as pdfjsLib from 'pdfjs-dist';
 import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';
 
@@ -76,6 +78,7 @@ import { mapActions } from 'vuex';
 
 export default {
     name: 'courseware-document-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
         CoursewareFileChooser,
@@ -256,7 +259,6 @@ export default {
                     containerId: this.block.relationships.container.data.id,
                 });
             }
-
         },
     },
 };
diff --git a/resources/vue/components/courseware/CoursewareDownloadBlock.vue b/resources/vue/components/courseware/CoursewareDownloadBlock.vue
index 1f7a78beb90..d5c08d2eb35 100644
--- a/resources/vue/components/courseware/CoursewareDownloadBlock.vue
+++ b/resources/vue/components/courseware/CoursewareDownloadBlock.vue
@@ -6,6 +6,7 @@
             :isTeacher="isTeacher"
             :preview="true"
             :defaultGrade="false"
+            @showEdit="initCurrentData"
             @storeEdit="storeBlock"
             @closeEdit="initCurrentData"
         >
@@ -77,11 +78,12 @@
 <script>
 import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
 import CoursewareFileChooser from './CoursewareFileChooser.vue';
-
+import { blockMixin } from './block-mixin.js';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-download-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
         CoursewareFileChooser,
diff --git a/resources/vue/components/courseware/CoursewareEmbedBlock.vue b/resources/vue/components/courseware/CoursewareEmbedBlock.vue
index ec8a4259985..8ee8808fb92 100644
--- a/resources/vue/components/courseware/CoursewareEmbedBlock.vue
+++ b/resources/vue/components/courseware/CoursewareEmbedBlock.vue
@@ -5,6 +5,7 @@
             :canEdit="canEdit"
             :isTeacher="isTeacher"
             :preview="false"
+            @showEdit="initCurrentData"
             @storeEdit="storeBlock"
             @closeEdit="initCurrentData"
         >
@@ -87,11 +88,12 @@
 
 <script>
 import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-
+import { blockMixin } from './block-mixin.js';
 import { mapActions } from 'vuex';
 
 export default {
     name: 'courseware-embed-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
     },
@@ -245,5 +247,6 @@ export default {
             });
         },
     },
+
 };
 </script>
diff --git a/resources/vue/components/courseware/CoursewareFolderBlock.vue b/resources/vue/components/courseware/CoursewareFolderBlock.vue
index 2d1f355f5b5..2090c35358a 100644
--- a/resources/vue/components/courseware/CoursewareFolderBlock.vue
+++ b/resources/vue/components/courseware/CoursewareFolderBlock.vue
@@ -5,6 +5,7 @@
             :canEdit="canEdit"
             :isTeacher="isTeacher"
             :preview="true"
+            @showEdit="initCurrentData"
             @storeEdit="storeBlock"
             @closeEdit="initCurrentData"
         >
@@ -141,10 +142,12 @@ import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
 import CoursewareFolderChooser from './CoursewareFolderChooser.vue';
 import StudipDialog from '../StudipDialog.vue';
 
+import { blockMixin } from './block-mixin.js';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-folder-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
         CoursewareFolderChooser,
diff --git a/resources/vue/components/courseware/CoursewareGalleryBlock.vue b/resources/vue/components/courseware/CoursewareGalleryBlock.vue
index 9d0549b25c9..9e4fced7a0f 100644
--- a/resources/vue/components/courseware/CoursewareGalleryBlock.vue
+++ b/resources/vue/components/courseware/CoursewareGalleryBlock.vue
@@ -5,6 +5,7 @@
             :canEdit="canEdit"
             :isTeacher="isTeacher"
             :preview="true"
+            @showEdit="initCurrentData"
             @storeEdit="storeBlock"
             @closeEdit="initCurrentData"
         >
@@ -83,11 +84,12 @@
 <script>
 import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
 import CoursewareFolderChooser from './CoursewareFolderChooser.vue';
-
+import { blockMixin } from './block-mixin.js';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-gallery-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
         CoursewareFolderChooser,
diff --git a/resources/vue/components/courseware/CoursewareHeadlineBlock.vue b/resources/vue/components/courseware/CoursewareHeadlineBlock.vue
index 9cc3e5557fc..8a5bc6ce5d8 100644
--- a/resources/vue/components/courseware/CoursewareHeadlineBlock.vue
+++ b/resources/vue/components/courseware/CoursewareHeadlineBlock.vue
@@ -5,8 +5,9 @@
             :canEdit="canEdit"
             :isTeacher="isTeacher"
             :preview="true"
+            @showEdit="initCurrentData"
             @storeEdit="storeText"
-            @closeEdit="closeEdit"
+            @closeEdit="initCurrentData"
         >
             <template #content>
                 <div
@@ -166,11 +167,13 @@
 <script>
 import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
 import CoursewareFileChooser from './CoursewareFileChooser.vue';
+import { blockMixin } from './block-mixin.js';
 import { mapGetters, mapActions } from 'vuex';
 import contentIcons from './content-icons.js';
 
 export default {
     name: 'courseware-headline-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
         CoursewareFileChooser,
@@ -342,9 +345,6 @@ export default {
             this.currentBackgroundImageId = file.id;
             this.currentBackgroundURL = file.download_url;
         },
-        closeEdit() {
-            this.initCurrentData();
-        },
         storeText() {
             let attributes = {};
             attributes.payload = {};
diff --git a/resources/vue/components/courseware/CoursewareIframeBlock.vue b/resources/vue/components/courseware/CoursewareIframeBlock.vue
index 1f0f5bf9b3a..57c5c098b14 100644
--- a/resources/vue/components/courseware/CoursewareIframeBlock.vue
+++ b/resources/vue/components/courseware/CoursewareIframeBlock.vue
@@ -5,6 +5,7 @@
             :canEdit="canEdit"
             :isTeacher="isTeacher"
             :preview="true"
+            @showEdit="initCurrentData"
             @storeEdit="storeBlock"
             @closeEdit="initCurrentData"
         >
@@ -104,12 +105,13 @@
 
 <script>
 import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
-
+import { blockMixin } from './block-mixin.js';
 import { mapActions, mapGetters } from 'vuex';
 import md5 from 'md5';
 
 export default {
     name: 'courseware-iframe-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
     },
diff --git a/resources/vue/components/courseware/CoursewareImageMapBlock.vue b/resources/vue/components/courseware/CoursewareImageMapBlock.vue
index 9ba60d662c1..f097e782bc6 100644
--- a/resources/vue/components/courseware/CoursewareImageMapBlock.vue
+++ b/resources/vue/components/courseware/CoursewareImageMapBlock.vue
@@ -5,6 +5,7 @@
             :canEdit="canEdit"
             :isTeacher="isTeacher"
             :preview="true"
+            @showEdit="initCurrentData"
             @storeEdit="storeBlock"
             @closeEdit="initCurrentData"
         >
@@ -160,11 +161,12 @@ import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
 import CoursewareFileChooser from './CoursewareFileChooser.vue';
 import CoursewareTabs from './CoursewareTabs.vue';
 import CoursewareTab from './CoursewareTab.vue';
-
+import { blockMixin } from './block-mixin.js';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-image-map-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
         CoursewareFileChooser,
diff --git a/resources/vue/components/courseware/CoursewareKeyPointBlock.vue b/resources/vue/components/courseware/CoursewareKeyPointBlock.vue
index 39124e548b9..05d5d2fd701 100644
--- a/resources/vue/components/courseware/CoursewareKeyPointBlock.vue
+++ b/resources/vue/components/courseware/CoursewareKeyPointBlock.vue
@@ -5,8 +5,9 @@
             :canEdit="canEdit"
             :isTeacher="isTeacher"
             :preview="true"
+            @showEdit="initCurrentData"
             @storeEdit="storeBlock"
-            @closeEdit="closeEdit"
+            @closeEdit="initCurrentData"
         >
             <template #content>
                 <div class="cw-keypoint-content" :class="['cw-keypoint-' + currentColor]">
@@ -79,11 +80,13 @@
 
 <script>
 import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
+import { blockMixin } from './block-mixin.js';
 import { mapActions } from 'vuex';
 import contentIcons from './content-icons.js';
 
 export default {
     name: 'courseware-key-point-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
     },
@@ -194,9 +197,6 @@ export default {
                 containerId: this.block.relationships.container.data.id,
             });
         },
-        closeEdit() {
-            this.initCurrentData();
-        },
     },
     mounted() {
         this.initCurrentData();
diff --git a/resources/vue/components/courseware/CoursewareLinkBlock.vue b/resources/vue/components/courseware/CoursewareLinkBlock.vue
index a2fce7d0045..9d65163c55e 100644
--- a/resources/vue/components/courseware/CoursewareLinkBlock.vue
+++ b/resources/vue/components/courseware/CoursewareLinkBlock.vue
@@ -5,6 +5,7 @@
             :canEdit="canEdit"
             :isTeacher="isTeacher"
             :preview="true"
+            @showEdit="initCurrentData"
             @storeEdit="storeBlock"
             @closeEdit="initCurrentData"
         >
@@ -62,10 +63,12 @@
 
 <script>
 import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
+import { blockMixin } from './block-mixin.js';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-link-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
     },
diff --git a/resources/vue/components/courseware/CoursewareStructuralElement.vue b/resources/vue/components/courseware/CoursewareStructuralElement.vue
index bdd01965f3c..5d468d55da4 100644
--- a/resources/vue/components/courseware/CoursewareStructuralElement.vue
+++ b/resources/vue/components/courseware/CoursewareStructuralElement.vue
@@ -62,6 +62,7 @@
                                 @pdfExport="menuAction('pdfExport')"
                                 @showSuggest="menuAction('showSuggest')"
                                 @linkElement="menuAction('linkElement')"
+                                @removeLock="menuAction('removeLock')"
                             />
                         </template>
                     </courseware-ribbon>
@@ -75,6 +76,17 @@
                         }"
                     >
                         <div v-if="structuralElementLoaded" class="cw-companion-box-wrapper">
+                            <courseware-companion-box 
+                                v-if="blockedByAnotherUser"
+                                :msgCompanion="$gettextInterpolate('Die Einstellungen dieser Seite werden im Moment von %{blockingUserName} bearbeitet', {blockingUserName: blockingUserName})"
+                                mood="pointing"
+                            >
+                                <template #companionActions>
+                                    <button class="button" @click="menuAction('removeLock')">
+                                        {{ textRemoveLock.title }}
+                                    </button>
+                                </template>
+                            </courseware-companion-box>
                             <courseware-empty-element-box
                                 v-if="showEmptyElementBox"
                                 :canEdit="canEdit"
@@ -589,6 +601,15 @@
                         </form>
                     </template>
                 </studip-dialog>
+                <studip-dialog
+                    v-if="showRemoveLockDialog"
+                    :title="textRemoveLock.title"
+                    :question="textRemoveLock.alert"
+                    height="200"
+                    width="450"
+                    @confirm="executeRemoveLock"
+                    @close="showElementRemoveLockDialog(false)"
+                ></studip-dialog>
             </div>
             <div v-else>
                 <courseware-companion-box
@@ -685,6 +706,10 @@ export default {
                 perv: this.$gettext('zurück'),
                 next: this.$gettext('weiter'),
             },
+            textRemoveLock: {
+                title: this.$gettext('Sperre aufheben'),
+                alert: this.$gettext('Möchten Sie die Sperre der Seite wirklich aufheben?'),
+            },
             exportRunning: false,
             exportChildren: false,
             oerExportRunning: false,
@@ -705,7 +730,7 @@ export default {
             publicLink: {
                 passsword: '',
                 'expire-date': ''
-            }
+            },
         };
     },
 
@@ -731,6 +756,7 @@ export default {
             showOerDialog: 'showStructuralElementOerDialog',
             showSuggestOerDialog: 'showSuggestOerDialog',
             showLinkDialog: 'showStructuralElementLinkDialog',
+            showRemoveLockDialog: 'showStructuralElementRemoveLockDialog',
             oerEnabled: 'oerEnabled',
             licenses: 'licenses',
             exportState: 'exportState',
@@ -740,6 +766,11 @@ export default {
             viewMode: 'viewMode',
             taskById: 'courseware-tasks/byId',
             userById: 'users/byId',
+
+            blocked: 'currentElementBlocked',
+            blockerId: 'currentElementBlockerId',
+            blockedByThisUser: 'currentElementBlockedByThisUser',
+            blockedByAnotherUser: 'currentElementBlockedByAnotherUser',
         }),
 
         currentId() {
@@ -927,25 +958,34 @@ export default {
 
             ];
             if (this.canEdit) {
-                menu.push({
-                    id: 1,
-                    label: this.$gettext('Seite bearbeiten'),
-                    icon: 'edit',
-                    emit: 'editCurrentElement',
-                });
-                menu.push({
-                    id: 2,
-                    label: this.$gettext('Abschnitte sortieren'),
-                    icon: 'arr_1sort',
-                    emit: 'sortContainers',
-                });
-
+                if (!this.blocked) {
+                    menu.push({
+                        id: 1,
+                        label: this.$gettext('Seite bearbeiten'),
+                        icon: 'edit',
+                        emit: 'editCurrentElement',
+                    });
+                    menu.push({
+                        id: 2,
+                        label: this.$gettext('Abschnitte sortieren'),
+                        icon: 'arr_1sort',
+                        emit: 'sortContainers',
+                    });
+                } 
+                if (this.blocked && this.blockedByAnotherUser && this.userIsTeacher) {
+                    menu.push({
+                        id: 1,
+                        label: this.textRemoveLock.title,
+                        icon: 'lock-unlocked',
+                        emit: 'removeLock',
+                    });
+                }
                 menu.push({ id: 3, label: this.$gettext('Seite hinzufügen'), icon: 'add', emit: 'addElement' });
             }
             if (this.context.type === 'users') {
                 menu.push({ id: 7, label: this.$gettext('Öffentlichen Link erzeugen'), icon: 'group', emit: 'linkElement' });
             }
-            if (!this.isRoot && this.canEdit && !this.isTask) {
+            if (!this.isRoot && this.canEdit && !this.isTask && !this.blocked) {
                 menu.push({
                     id: 8,
                     label: this.$gettext('Seite löschen'),
@@ -1116,17 +1156,15 @@ export default {
 
             return '';
         },
-        blocked() {
-            return this.structuralElement?.relationships['edit-blocker'].data !== null;
-        },
-        blockerId() {
-            return this.blocked ? this.structuralElement?.relationships['edit-blocker'].data?.id : null;
-        },
-        blockedByThisUser() {
-            return this.blocked && this.userId === this.blockerId;
+        blockingUser() {
+            if (this.blockedByAnotherUser) {
+                return this.userById({id: this.blockerId});
+            }
+
+            return null;
         },
-        blockedByAnotherUser() {
-            return this.blocked && this.userId !== this.blockerId;
+        blockingUserName() {
+            return this.blockingUser ? this.blockingUser.attributes['formatted-name'] : '';
         },
         discussView() {
             return this.viewMode === 'discuss';
@@ -1256,6 +1294,7 @@ export default {
             unlockObject: 'unlockObject',
             addBookmark: 'addBookmark',
             companionInfo: 'companionInfo',
+            companionWarning: 'companionWarning',
             companionError: 'companionError',
             uploadImageForStructuralElement: 'uploadImageForStructuralElement',
             deleteImageForStructuralElement: 'deleteImageForStructuralElement',
@@ -1268,6 +1307,7 @@ export default {
             showElementDeleteDialog: 'showElementDeleteDialog',
             showElementOerDialog: 'showElementOerDialog',
             showElementLinkDialog: 'showElementLinkDialog',
+            showElementRemoveLockDialog: 'showElementRemoveLockDialog',
             updateShowSuggestOerDialog: 'updateShowSuggestOerDialog',
             updateContainer: 'updateContainer',
             setStructuralElementSortMode: 'setStructuralElementSortMode',
@@ -1275,6 +1315,7 @@ export default {
             loadTask: 'loadTask',
             loadStructuralElement: 'loadStructuralElement',
             createLink: 'createLink',
+            setCurrentElementId: 'coursewareCurrentElement',
         }),
 
         initCurrent() {
@@ -1283,7 +1324,11 @@ export default {
         },
         async menuAction(action) {
             switch (action) {
+                case 'removeLock':
+                    this.displayRemoveLockDialog();
+                    break;
                 case 'editCurrentElement':
+                    await this.loadStructuralElement(this.currentId);
                     if (this.blockedByAnotherUser) {
                         this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
 
@@ -1291,7 +1336,7 @@ export default {
                     }
                     try {
                         await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' });
-                    } catch (error) {
+                    } catch(error) {
                         if (error.status === 409) {
                             this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
                         } else {
@@ -1300,6 +1345,7 @@ export default {
 
                         return false;
                     }
+                    this.initCurrent();
                     this.showElementEditDialog(true);
                     break;
                 case 'addElement':
@@ -1309,6 +1355,12 @@ export default {
                     this.showElementAddDialog(true);
                     break;
                 case 'deleteCurrentElement':
+                    await this.loadStructuralElement(this.currentId);
+                    if (this.blockedByAnotherUser) {
+                        this.companionInfo({ info: this.$gettextInterpolate('Löschen nicht möglich, da %{blockingUserName} die Seite bearbeitet.', {blockingUserName: this.blockingUserName}) });
+
+                        return false;
+                    }
                     await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' });
                     this.showElementDeleteDialog(true);
                     break;
@@ -1328,6 +1380,7 @@ export default {
                     this.setBookmark();
                     break;
                 case 'sortContainers':
+                    await this.loadStructuralElement(this.currentId);
                     if (this.blockedByAnotherUser) {
                         this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') });
 
@@ -1352,7 +1405,11 @@ export default {
             }
         },
         async closeEditDialog() {
-            await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' });
+            await this.loadStructuralElement(this.currentElement.id);
+            if (this.blockedByThisUser) {
+                await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' });
+                await this.loadStructuralElement(this.currentElement.id);
+            }
             this.showElementEditDialog(false);
             this.initCurrent();
         },
@@ -1374,6 +1431,15 @@ export default {
             this.initCurrent();
         },
         async storeCurrentElement() {
+            await this.loadStructuralElement(this.currentElement.id);
+            if (this.blockedByAnotherUser) {
+                this.companionWarning({ info: this.$gettextInterpolate('Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.', {blockingUserName: this.blockingUserName}) });
+                this.showElementEditDialog(false);
+                return false;
+            }
+            if (!this.blocked) {
+                await this.lockObject({ id: this.currentId, type: 'courseware-structural-elements' });
+            }
             const file = this.$refs?.upload_image?.files[0];
             if (file) {
                 if (file.size > 2097152) {
@@ -1471,10 +1537,19 @@ export default {
         },
 
         async closeDeleteDialog() {
-            await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' });
+            await this.loadStructuralElement(this.currentElement.id);
+            if (this.blockedByThisUser) {
+                await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' });
+            }
             this.showElementDeleteDialog(false);
         },
-        deleteCurrentElement() {
+        async deleteCurrentElement() {
+            await this.loadStructuralElement(this.currentElement.id);
+            if (this.blockedByAnotherUser) {
+                this.companionWarning({ info: this.$gettextInterpolate('Löschen nicht möglich, da %{blockingUserName} die Bearbeitung übernommen hat.', {blockingUserName: this.blockingUserName}) });
+                this.showElementDeleteDialog(false);
+                return false;
+            }
             let parent_id = this.structuralElement.relationships.parent.data.id;
             this.showElementDeleteDialog(false);
             this.companionInfo({ info: this.$gettext('Lösche Seite und alle darunter liegenden Elemente.') });
@@ -1561,6 +1636,14 @@ export default {
                 'expire-date': ''
             };
             this.showElementLinkDialog(false);
+        },
+        displayRemoveLockDialog() {
+            this.showElementRemoveLockDialog(true);
+        },
+        async executeRemoveLock() {
+            await this.unlockObject({ id: this.currentId, type: 'courseware-structural-elements' });
+            await this.loadStructuralElement(this.currentElement.id);
+            this.showElementRemoveLockDialog(false);
         }
     },
     created() {
@@ -1568,7 +1651,8 @@ export default {
     },
 
     watch: {
-        structuralElement() {
+        async structuralElement() {
+            this.setCurrentElementId(this.structuralElement.id);
             this.initCurrent();
             if (this.isTask) {
                 this.loadTask({
diff --git a/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue b/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue
index c5d268ef988..1a582808109 100644
--- a/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue
+++ b/resources/vue/components/courseware/CoursewareTableOfContentsBlock.vue
@@ -5,8 +5,9 @@
             :canEdit="canEdit"
             :isTeacher="isTeacher"
             :preview="true"
+            @showEdit="initCurrentData"
             @storeEdit="storeText"
-            @closeEdit="closeEdit"
+            @closeEdit="initCurrentData"
         >
             <template #content>
                 <div v-if="currentStyle !== 'tiles' && currentTitle !== ''" class="cw-block-title">{{ currentTitle }}</div>
@@ -88,10 +89,12 @@
 
 <script>
 import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
+import { blockMixin } from './block-mixin.js';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-table-of-contents-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
     },
@@ -168,9 +171,6 @@ export default {
             this.currentTitle = this.title;
             this.currentStyle = this.style;
         },
-        closeEdit() {
-            this.initCurrentData();
-        },
         storeText() {
             let attributes = {};
             attributes.payload = {};
diff --git a/resources/vue/components/courseware/CoursewareTabsContainer.vue b/resources/vue/components/courseware/CoursewareTabsContainer.vue
index 66349d95d18..abe78f899e0 100644
--- a/resources/vue/components/courseware/CoursewareTabsContainer.vue
+++ b/resources/vue/components/courseware/CoursewareTabsContainer.vue
@@ -4,6 +4,7 @@
         containerClass="cw-container-tabs"
         :canEdit="canEdit"
         :isTeacher="isTeacher"
+        @showEdit="setShowEdit"
         @storeContainer="storeContainer"
         @closeEdit="initCurrentData"
         @sortBlocks="enableSort"
@@ -129,6 +130,7 @@ export default {
     },
     data() {
         return {
+            showEdit: false,
             currentContainer: null,
             currentSections: [],
             unallocatedBlocks: [],
@@ -197,6 +199,9 @@ export default {
 
             this.currentSections = sections;
         },
+        setShowEdit(state) {
+            this.showEdit = state;
+        },
         addSection() {
             this.currentContainer.attributes.payload.sections.push({ name: '', icon: '', blocks: [] });
         },
@@ -261,7 +266,9 @@ export default {
     },
     watch: {
         blocks() {
-            this.initCurrentData();
+            if (!this.showEdit) {
+                this.initCurrentData();
+            }
         }
     }
 };
diff --git a/resources/vue/components/courseware/CoursewareTextBlock.vue b/resources/vue/components/courseware/CoursewareTextBlock.vue
index d051cb643d0..fdae56d00bd 100644
--- a/resources/vue/components/courseware/CoursewareTextBlock.vue
+++ b/resources/vue/components/courseware/CoursewareTextBlock.vue
@@ -6,8 +6,9 @@
             :isTeacher="isTeacher"
             :preview="false"
             ref="defaultBlock"
+            @showEdit="initCurrent"
             @storeEdit="storeText"
-            @closeEdit="closeEdit"
+            @closeEdit="initCurrent"
         >
             <template #content>
                 <section class="cw-block-content formatted-content" v-html="currentText" ref="content"></section>
@@ -22,11 +23,13 @@
 
 <script>
 import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
+import { blockMixin } from './block-mixin.js';
 import ClassicEditor from '../../../assets/javascripts/chunks/wysiwyg.js'
 import { mapActions } from 'vuex';
 
 export default {
     name: 'courseware-text-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
     },
@@ -45,19 +48,18 @@ export default {
         };
     },
     computed: {
-        text() {
+        text() {    
             return this.block?.attributes?.payload?.text;
         },
     },
     mounted() {
-        this.currentText = this.text;
-        this.loadMathjax();
+        this.initCurrent();
     },
     methods: {
         ...mapActions({
             updateBlock: 'updateBlockInContainer',
         }),
-        closeEdit() {
+        initCurrent() {
             this.currentText = this.text;
             this.loadMathjax();
         },
diff --git a/resources/vue/components/courseware/CoursewareTypewriterBlock.vue b/resources/vue/components/courseware/CoursewareTypewriterBlock.vue
index abdc8ebe368..14c70720344 100644
--- a/resources/vue/components/courseware/CoursewareTypewriterBlock.vue
+++ b/resources/vue/components/courseware/CoursewareTypewriterBlock.vue
@@ -5,8 +5,9 @@
         :canEdit="canEdit"
         :isTeacher="isTeacher"
         :preview="true"
+        @showEdit="initCurrentData"
         @storeEdit="storeText"
-        @closeEdit="closeEdit"
+        @closeEdit="initCurrentData"
         >
             <template #content>
                 <div class="cw-typewriter-content">
@@ -67,11 +68,13 @@
 
 <script>
 import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
+import { blockMixin } from './block-mixin.js';
 import { VueTyper } from 'vue-typer';
 import { mapActions } from 'vuex';
 
 export default {
     name: 'courseware-typewriter-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
         VueTyper,
@@ -134,9 +137,6 @@ export default {
                 this.currentText = text;
             });
         },
-        closeEdit() {
-            this.initCurrentData();
-        },
         storeText() {
             let attributes = {};
             attributes.payload = {};
diff --git a/resources/vue/components/courseware/CoursewareVideoBlock.vue b/resources/vue/components/courseware/CoursewareVideoBlock.vue
index e5e31fcab43..40f7e9679dc 100644
--- a/resources/vue/components/courseware/CoursewareVideoBlock.vue
+++ b/resources/vue/components/courseware/CoursewareVideoBlock.vue
@@ -5,6 +5,7 @@
             :canEdit="canEdit"
             :isTeacher="isTeacher"
             :preview="true"
+            @showEdit="initCurrentData"
             @storeEdit="storeBlock"
             @closeEdit="initCurrentData"
         >
@@ -75,10 +76,12 @@
 <script>
 import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
 import CoursewareFileChooser from './CoursewareFileChooser.vue';
+import { blockMixin } from './block-mixin.js';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
     name: 'courseware-video-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
         CoursewareFileChooser,
diff --git a/resources/vue/components/courseware/block-mixin.js b/resources/vue/components/courseware/block-mixin.js
index 45eb6316b0a..4e2dc77da59 100644
--- a/resources/vue/components/courseware/block-mixin.js
+++ b/resources/vue/components/courseware/block-mixin.js
@@ -28,5 +28,8 @@ export const blockMixin = {
                 day: "2-digit",
             });
         },
+        setShowEdit(state) {
+            this.showEdit = state;
+        },
     },
 };
diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js
index 0c4811e28c3..93368305352 100644
--- a/resources/vue/store/courseware/courseware.module.js
+++ b/resources/vue/store/courseware/courseware.module.js
@@ -36,6 +36,7 @@ const getDefaultState = () => {
         showStructuralElementDeleteDialog: false,
         showStructuralElementOerDialog: false,
         showStructuralElementLinkDialog: false,
+        showStructuralElementRemoveLockDialog: false,
 
         showSuggestOerDialog: false,
 
@@ -77,6 +78,22 @@ const getters = {
     currentElement(state) {
         return state.currentElement;
     },
+    currentStructuralElement(state, getters, rootState, rootGetters) {
+        const id = getters.currentElement;
+        return rootGetters['courseware-structural-elements/byId']({ id });
+    },
+    currentElementBlocked(state, getters, rootState, rootGetters) {
+        return getters.currentStructuralElement?.relationships?.['edit-blocker']?.data !== null;
+    },
+    currentElementBlockerId(state, getters) {
+        return getters.currentElementBlocked ? getters.currentStructuralElement?.relationships?.['edit-blocker']?.data?.id : null;
+    },
+    currentElementBlockedByThisUser(state, getters) {
+        return getters.currentElementBlocked && getters.userId === getters.currentElementBlockerId;
+    },
+    currentElementBlockedByAnotherUser(state, getters) {
+        return getters.currentElementBlocked && getters.userId !== getters.currentElementBlockerId;
+    },
     oerEnabled(state) {
         return state.oerEnabled;
     },
@@ -173,6 +190,9 @@ const getters = {
     showStructuralElementLinkDialog(state) {
         return state.showStructuralElementLinkDialog;
     },
+    showStructuralElementRemoveLockDialog(state) {
+        return state.showStructuralElementRemoveLockDialog;
+    },
     showOverviewElementAddDialog(state) {
         return state.showOverviewElementAddDialog;
     },
@@ -228,7 +248,7 @@ export const state = { ...initialState };
 export const actions = {
     loadContainer({ dispatch }, containerId) {
         const options = {
-            include: 'blocks',
+            include: 'blocks,blocks.edit-blocker','fields[users]': 'formatted-name',
         };
 
         return dispatch('courseware-containers/loadById', { id: containerId, options }, { root: true });
@@ -237,7 +257,7 @@ export const actions = {
     loadStructuralElement({ dispatch }, structuralElementId) {
         const options = {
             include:
-                'containers,containers.blocks,containers.blocks.editor,containers.blocks.owner,containers.blocks.user-data-field,containers.blocks.user-progress,editor,owner',
+                'containers,containers.edit-blocker,containers.blocks,containers.blocks.editor,containers.blocks.owner,containers.blocks.user-data-field,containers.blocks.user-progress,containers.blocks.edit-blocker,editor,edit-blocker,owner',
             'fields[users]': 'formatted-name',
         };
 
@@ -840,6 +860,10 @@ export const actions = {
         context.commit('setShowStructuralElementLinkDialog', bool);
     },
 
+    showElementRemoveLockDialog(context, bool) {
+        context.commit('setShowStructuralElementRemoveLockDialog', bool);
+    },
+
     setShowOverviewElementAddDialog(context, bool) {
         context.commit('setShowOverviewElementAddDialog', bool);
     },
@@ -1398,6 +1422,10 @@ export const mutations = {
         state.showStructuralElementLinkDialog = showLink;
     },
 
+    setShowStructuralElementRemoveLockDialog(state, showRemoveLock) {
+        state.showStructuralElementRemoveLockDialog = showRemoveLock;
+    },
+
     setStructuralElementSortMode(state, mode) {
         state.structuralElementSortMode = mode;
     },
-- 
GitLab