From 96b3139e070d6cac1e7e669f702ce2164dde036e 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     |  61 +++++---
 .../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           | 142 ++++++++++++++----
 .../CoursewareTableOfContentsBlock.vue        |   8 +-
 .../courseware/CoursewareTabsContainer.vue    |   9 +-
 .../courseware/CoursewareTextBlock.vue        |  10 +-
 .../courseware/CoursewareTypewriterBlock.vue  |   8 +-
 .../courseware/CoursewareVideoBlock.vue       |   3 +
 .../vue/components/courseware/block-mixin.js  |  11 ++
 .../vue/store/courseware/courseware.module.js |  32 +++-
 35 files changed, 624 insertions(+), 198 deletions(-)

diff --git a/lib/classes/JsonApi/Routes/Courseware/BlocksShow.php b/lib/classes/JsonApi/Routes/Courseware/BlocksShow.php
index ba31717a7b8..4e5dd1724d8 100755
--- 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 1c527e45712..8a21cb9a8f0 100755
--- 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',
     ];
diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss
index 7b5343a14e5..effae59cf00 100755
--- a/resources/assets/stylesheets/scss/courseware.scss
+++ b/resources/assets/stylesheets/scss/courseware.scss
@@ -752,6 +752,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 {
@@ -892,6 +900,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 {
@@ -1908,6 +1925,9 @@ v i e w  w i d g e t
     .cw-action-widget-trash{
         @include background-icon(trash, 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 100755
--- 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 fcac6864b24..7279bdb4c2e 100644
--- a/resources/vue/components/courseware/CoursewareActionWidget.vue
+++ b/resources/vue/components/courseware/CoursewareActionWidget.vue
@@ -10,12 +10,17 @@
                 <translate>Vollbild einschalten</translate>
             </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>
             </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">
+                <translate>Sperre aufheben</translate>
+            </button>
+        </li>
+        <li v-if="canEdit && !blockedByAnotherUser" class="cw-action-widget-sort">
             <button @click="sortContainers">
                 <translate>Abschnitte sortieren</translate>
             </button>
@@ -35,7 +40,7 @@
                 <translate>Lesezeichen setzen</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>
             </button>
@@ -60,6 +65,11 @@ export default {
             userId: 'userId',
             consumeMode: 'consumeMode',
             showToolbar: 'showToolbar',
+
+            blocked: 'currentElementBlocked',
+            blockerId: 'currentElementBlockerId',
+            blockedByThisUser: 'currentElementBlockedByThisUser',
+            blockedByAnotherUser: 'currentElementBlockedByAnotherUser',
         }),
         isRoot() {
             if (!this.structuralElement) {
@@ -77,18 +87,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');
         },
@@ -102,6 +100,7 @@ export default {
             showElementAddDialog: 'showElementAddDialog',
             showElementDeleteDialog: 'showElementDeleteDialog',
             showElementInfoDialog: 'showElementInfoDialog',
+            showElementRemoveLockDialog: 'showElementRemoveLockDialog',
             setStructuralElementSortMode: 'setStructuralElementSortMode',
             companionInfo: 'companionInfo',
             addBookmark: 'addBookmark',
@@ -109,9 +108,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.') });
 
@@ -130,10 +131,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 100755
--- 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 100755
--- 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 bb2177edc0f..12d81d8e8e9 100755
--- a/resources/vue/components/courseware/CoursewareBlockActions.vue
+++ b/resources/vue/components/courseware/CoursewareBlockActions.vue
@@ -6,6 +6,7 @@
             @setVisibility="setVisibility"
             @showInfo="showInfo"
             @deleteBlock="deleteBlock"
+            @removeLock="removeLock"
         />
     </div>
 </template>
@@ -27,57 +28,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({
@@ -116,12 +129,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 100755
--- 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 100755
--- 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 100755
--- 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 100755
--- 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 e1da9884676..dc67fceb133 100755
--- a/resources/vue/components/courseware/CoursewareContainerActions.vue
+++ b/resources/vue/components/courseware/CoursewareContainerActions.vue
@@ -5,11 +5,13 @@
             @editContainer="editContainer"
             @deleteContainer="deleteContainer"
             @sortBlocks="sortBlocks"
+            @removeLock="removeLock"
         />
     </div>
 </template>
 
 <script>
+import { mapGetters } from 'vuex';
 export default {
     name: 'courseware-container-actions',
     props: {
@@ -17,20 +19,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) {
@@ -44,6 +73,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 100755
--- 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 afb014431d5..c4726ca3cea 100755
--- 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,15 +132,19 @@ 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: {
         ...mapGetters({
             blockTypes: 'blockTypes',
             userId: 'userId',
+            userById: 'users/byId',
             viewMode: 'viewMode',
             containerById: 'courseware-containers/byId',
         }),
@@ -141,10 +159,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;
@@ -152,6 +170,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'];
 
@@ -171,10 +199,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) {
@@ -188,26 +218,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;
                             }
@@ -223,25 +243,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});
@@ -273,13 +332,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 100755
--- 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 091b7176a33..64821d1fd4a 100755
--- 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"
         >
@@ -117,12 +118,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 100755
--- 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 100755
--- 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 100755
--- 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 100755
--- 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 100755
--- 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 100755
--- 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 100755
--- 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 100755
--- 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 100755
--- 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 100755
--- 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 075e382955a..13143f9a8c5 100755
--- a/resources/vue/components/courseware/CoursewareStructuralElement.vue
+++ b/resources/vue/components/courseware/CoursewareStructuralElement.vue
@@ -59,6 +59,7 @@
                                 @setBookmark="menuAction('setBookmark')"
                                 @sortContainers="menuAction('sortContainers')"
                                 @pdfExport="menuAction('pdfExport')"
+                                @removeLock="menuAction('removeLock')"
                             />
                         </template>
                     </courseware-ribbon>
@@ -72,6 +73,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"
@@ -492,6 +504,15 @@
                     @confirm="deleteCurrentElement"
                     @close="closeDeleteDialog"
                 ></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
@@ -585,6 +606,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,
@@ -623,6 +648,7 @@ export default {
             showInfoDialog: 'showStructuralElementInfoDialog',
             showDeleteDialog: 'showStructuralElementDeleteDialog',
             showOerDialog: 'showStructuralElementOerDialog',
+            showRemoveLockDialog: 'showStructuralElementRemoveLockDialog',
             oerEnabled: 'oerEnabled',
             oerTitle: 'oerTitle',
             licenses: 'licenses',
@@ -633,6 +659,11 @@ export default {
             viewMode: 'viewMode',
             taskById: 'courseware-tasks/byId',
             userById: 'users/byId',
+
+            blocked: 'currentElementBlocked',
+            blockerId: 'currentElementBlockerId',
+            blockedByThisUser: 'currentElementBlockedByThisUser',
+            blockedByAnotherUser: 'currentElementBlockedByAnotherUser',
         }),
 
         currentId() {
@@ -805,22 +836,31 @@ export default {
                 { id: 5, label: this.$gettext('Lesezeichen setzen'), icon: 'star', emit: 'setBookmark' },
             ];
             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.isRoot && this.canEdit && !this.isTask) {
+            if (!this.isRoot && this.canEdit && !this.isTask && !this.blocked) {
                 menu.push({
                     id: 9,
                     label: this.$gettext('Seite löschen'),
@@ -991,17 +1031,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';
@@ -1089,6 +1127,7 @@ export default {
             unlockObject: 'unlockObject',
             addBookmark: 'addBookmark',
             companionInfo: 'companionInfo',
+            companionWarning: 'companionWarning',
             companionError: 'companionError',
             uploadImageForStructuralElement: 'uploadImageForStructuralElement',
             deleteImageForStructuralElement: 'deleteImageForStructuralElement',
@@ -1100,10 +1139,12 @@ export default {
             showElementInfoDialog: 'showElementInfoDialog',
             showElementDeleteDialog: 'showElementDeleteDialog',
             showElementOerDialog: 'showElementOerDialog',
+            showElementRemoveLockDialog: 'showElementRemoveLockDialog',
             updateContainer: 'updateContainer',
             setStructuralElementSortMode: 'setStructuralElementSortMode',
             sortContainersInStructualElements: 'sortContainersInStructualElements',
             loadTask: 'loadTask',
+            setCurrentElementId: 'coursewareCurrentElement',
         }),
 
         initCurrent() {
@@ -1112,7 +1153,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.') });
 
@@ -1120,7 +1165,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 {
@@ -1129,6 +1174,7 @@ export default {
 
                         return false;
                     }
+                    this.initCurrent();
                     this.showElementEditDialog(true);
                     break;
                 case 'addElement':
@@ -1138,6 +1184,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;
@@ -1154,6 +1206,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.') });
 
@@ -1174,7 +1227,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();
         },
@@ -1196,6 +1253,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) {
@@ -1293,10 +1359,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.') });
@@ -1356,13 +1431,22 @@ export default {
         updateWriteApproval(approval) {
             this.currentElement.attributes['write-approval'] = approval;
         },
+        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() {
         this.pluginManager.registerComponentsLocally(this);
     },
 
     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 100755
--- 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 7d2cbbc1a8a..15b3c87b574 100755
--- 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 178f56bdc7a..059416dc9a2 100755
--- 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>
@@ -23,10 +24,12 @@
 <script>
 import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
 import StudipWysiwyg from '../StudipWysiwyg.vue';
+import { blockMixin } from './block-mixin.js';
 import { mapActions } from 'vuex';
 
 export default {
     name: 'courseware-text-block',
+    mixins: [blockMixin],
     components: {
         CoursewareDefaultBlock,
         StudipWysiwyg,
@@ -47,14 +50,13 @@ export default {
         },
     },
     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 100755
--- 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 100755
--- 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 2e084baf116..4e2dc77da59 100755
--- a/resources/vue/components/courseware/block-mixin.js
+++ b/resources/vue/components/courseware/block-mixin.js
@@ -20,5 +20,16 @@ export const blockMixin = {
         ...mapActions({
             updateUserProgress: 'courseware-user-progresses/update',
         }),
+        getReadableDate(date) {
+            let locale = navigator.language ? navigator.language : 'de-DE';
+            return new Date(date).toLocaleDateString(locale, {
+                year: "numeric",
+                month: "2-digit",
+                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 8b6845662ed..0a7f1e1e67c 100755
--- a/resources/vue/store/courseware/courseware.module.js
+++ b/resources/vue/store/courseware/courseware.module.js
@@ -36,6 +36,7 @@ const getDefaultState = () => {
         showStructuralElementInfoDialog: false,
         showStructuralElementDeleteDialog: false,
         showStructuralElementOerDialog: false,
+        showStructuralElementRemoveLockDialog: false,
 
         structuralElementSortMode: false,
 
@@ -70,6 +71,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;
     },
@@ -166,6 +183,9 @@ const getters = {
     showStructuralElementDeleteDialog(state) {
         return state.showStructuralElementDeleteDialog;
     },
+    showStructuralElementRemoveLockDialog(state) {
+        return state.showStructuralElementRemoveLockDialog;
+    },
     showOverviewElementAddDialog(state) {
         return state.showOverviewElementAddDialog;
     },
@@ -206,7 +226,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 });
@@ -215,7 +235,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',
         };
 
@@ -811,6 +831,10 @@ export const actions = {
         context.commit('setShowStructuralElementDeleteDialog', bool);
     },
 
+    showElementRemoveLockDialog(context, bool) {
+        context.commit('setShowStructuralElementRemoveLockDialog', bool);
+    },
+
     setShowOverviewElementAddDialog(context, bool) {
         context.commit('setShowOverviewElementAddDialog', bool);
     },
@@ -1312,6 +1336,10 @@ export const mutations = {
         state.showOverviewElementAddDialog = showAdd;
     },
 
+    setShowStructuralElementRemoveLockDialog(state, showRemoveLock) {
+        state.showStructuralElementRemoveLockDialog = showRemoveLock;
+    },
+
     setStructuralElementSortMode(state, mode) {
         state.structuralElementSortMode = mode;
     },
-- 
GitLab