From f08cbbc79bedce4d397db55f1e484795b49d70f5 Mon Sep 17 00:00:00 2001
From: Ron Lucke <lucke@elan-ev.de>
Date: Mon, 4 Oct 2021 09:35:30 +0000
Subject: [PATCH] Fixes #153 and #169 and #255

---
 app/controllers/contents/courseware.php       |   4 +
 app/views/contents/courseware/courseware.php  |   1 +
 app/views/course/courseware/index.php         |   1 +
 .../Routes/Courseware/ContainersCopy.php      |   5 +
 .../Courseware/StructuralElementsCopy.php     |   7 +-
 lib/models/Courseware/BlockTypes/Text.php     |  94 +++++++++++
 .../assets/stylesheets/scss/courseware.scss   | 157 +++++++++++++++++-
 .../CoursewareAccordionContainer.vue          |  16 +-
 .../courseware/CoursewareActionWidget.vue     |   7 +-
 .../courseware/CoursewareAudioBlock.vue       |  12 +-
 .../courseware/CoursewareCourseManager.vue    |  85 ++++++----
 .../courseware/CoursewareImageMapBlock.vue    |  52 +++---
 .../courseware/CoursewareListContainer.vue    |   3 +-
 .../courseware/CoursewareManagerContainer.vue |  27 +--
 .../courseware/CoursewareManagerElement.vue   |   9 +-
 .../CoursewareStructuralElement.vue           |  35 ++--
 .../courseware/CoursewareTabsContainer.vue    |   5 +-
 resources/vue/courseware-index-app.js         |   6 +
 resources/vue/mixins/courseware/export.js     |  58 ++++++-
 resources/vue/mixins/courseware/import.js     | 108 +++++++++---
 .../vue/store/courseware/courseware.module.js |  92 +++++++++-
 21 files changed, 648 insertions(+), 136 deletions(-)

diff --git a/app/controllers/contents/courseware.php b/app/controllers/contents/courseware.php
index b27d510eccf..483f3c7ce1c 100755
--- a/app/controllers/contents/courseware.php
+++ b/app/controllers/contents/courseware.php
@@ -49,6 +49,8 @@ class Contents_CoursewareController extends AuthenticatedController
      */
     public function courseware_action($action = false, $widgetId = null)
     {
+        global $perm;
+
         Navigation::activateItem('/contents/courseware/courseware');
         $this->user_id = $GLOBALS['user']->id;
 
@@ -85,6 +87,8 @@ class Contents_CoursewareController extends AuthenticatedController
             array_push($this->licenses, $license->toArray());
         }
         $this->licenses = json_encode($this->licenses);
+
+        $this->oer_enabled = Config::get()->OERCAMPUS_ENABLED && $perm->have_perm(Config::get()->OER_PUBLIC_STATUS);
     }
 
     private function setCoursewareSidebar()
diff --git a/app/views/contents/courseware/courseware.php b/app/views/contents/courseware/courseware.php
index 2975b1b0a0e..b2f484744db 100755
--- a/app/views/contents/courseware/courseware.php
+++ b/app/views/contents/courseware/courseware.php
@@ -2,6 +2,7 @@
     id="courseware-index-app"
     entry-element-id="<?= $entry_element_id ?>"
     entry-type="users" entry-id="<?= $user_id ?>"
+    oer-enabled='<?= $oer_enabled ?>'
     oer-title="<?= Config::get()->OER_TITLE ?>"
     licenses='<?= $licenses ?>'
     >
diff --git a/app/views/course/courseware/index.php b/app/views/course/courseware/index.php
index 46a949cd586..8f372d26763 100755
--- a/app/views/course/courseware/index.php
+++ b/app/views/course/courseware/index.php
@@ -3,6 +3,7 @@
     entry-element-id="<?= $entry_element_id ?>"
     entry-type="courses"
     entry-id="<?= Context::getId() ?>"
+    oer-enabled="<?= Config::get()->OERCAMPUS_ENABLED?>"
     oer-title="<?= Config::get()->OER_TITLE ?>"
     licenses='<?= $licenses ?>'
     >
diff --git a/lib/classes/JsonApi/Routes/Courseware/ContainersCopy.php b/lib/classes/JsonApi/Routes/Courseware/ContainersCopy.php
index f476a038d35..9cfbf9dd73f 100755
--- a/lib/classes/JsonApi/Routes/Courseware/ContainersCopy.php
+++ b/lib/classes/JsonApi/Routes/Courseware/ContainersCopy.php
@@ -35,6 +35,11 @@ class ContainersCopy extends NonJsonApiController
         }
 
         $new_container = $this->copyContainer($user, $container, $element);
+
+        $response = $response->withHeader('Content-Type', 'application/json');
+        $response->getBody()->write((string) json_encode($new_container));
+
+        return $response;
     }
 
     private function copyContainer(\User $user, \Courseware\Container $remote_container, \Courseware\StructuralElement $element)
diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCopy.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCopy.php
index 96b0815db12..b63628e0114 100755
--- a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCopy.php
+++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCopy.php
@@ -32,7 +32,12 @@ class StructuralElementsCopy extends NonJsonApiController
             throw new AuthorizationFailedException();
         }
 
-        $new_container = $this->copyElement($user, $remote_element, $parent_element);
+        $new_element = $this->copyElement($user, $remote_element, $parent_element);
+
+        $response = $response->withHeader('Content-Type', 'application/json');
+        $response->getBody()->write((string) json_encode($new_element));
+
+        return $response;
     }
 
     private function copyElement(\User $user, \Courseware\StructuralElement $remote_element, \Courseware\StructuralElement $parent_element)
diff --git a/lib/models/Courseware/BlockTypes/Text.php b/lib/models/Courseware/BlockTypes/Text.php
index c9811c36974..ce2a774af6a 100755
--- a/lib/models/Courseware/BlockTypes/Text.php
+++ b/lib/models/Courseware/BlockTypes/Text.php
@@ -3,6 +3,7 @@
 namespace Courseware\BlockTypes;
 
 use Opis\JsonSchema\Schema;
+require_once 'lib/classes/Markup.class.php';
 
 /**
  * This class represents the content of a Courseware text block.
@@ -41,6 +42,57 @@ class Text extends BlockType
         return Schema::fromJsonString(file_get_contents($schemaFile));
     }
 
+        /**
+     * get all files related to this block.
+     *
+     * @return \FileRef[] list of file references realted to this block
+     */
+    public function getFiles(): array
+    {
+        $payload = $this->getPayload();
+        $document = new \DOMDocument();
+        $files = [];
+
+        if ($payload['text']) {
+            $document->loadHTML($payload['text']);
+            $imageElements = $document->getElementsByTagName('img');
+            foreach ($imageElements as $element) {
+                if (!$element instanceof \DOMElement || !$element->hasAttribute('src')) {
+                    continue;
+                }
+                $file = $this->extractFile($element->getAttribute('src'));
+                if ($file !== null) {
+                    $files[] = $file;
+                }
+            }
+        }
+        return $files;
+    }
+
+    public function copyPayload(string $rangeId = ''): array
+    {
+        $payload = $this->getPayload();
+        $document = new \DOMDocument();
+
+        if ($payload['text']) {
+            $document->loadHTML(mb_convert_encoding($payload['text'], 'HTML-ENTITIES', 'UTF-8'));
+            $imageElements = $document->getElementsByTagName('img');
+            foreach ($imageElements as $element) {
+                if (!$element instanceof \DOMElement || !$element->hasAttribute('src')) {
+                    continue;
+                }
+                $file = $this->extractFile($element->getAttribute('src'));
+                if ($file !== null) {
+                    $file_copy_id = $this->copyFileById($file->id, $rangeId);
+                    $element->setAttribute('src', \FileRef::find($file_copy_id)->getDownloadURL());
+                }
+            }
+            $payload['text'] = $document->saveHTML();
+        }
+
+        return $payload;
+    }
+
     public static function getCategories(): array
     {
         return ['basis', 'text'];
@@ -55,4 +107,46 @@ class Text extends BlockType
     {
         return [];
     }
+
+        /**
+     * Calls a callback if a given URL is an internal URL.
+     *
+     * @param string   $url      The url to check
+     * @param callable $callback A callable to execute
+     *
+     * @return mixed The return value of the callback or null if the callback
+     *               is not executed
+     */
+    private function applyCallbackOnInternalUrl($url, $callback)
+    {
+        if (! \Studip\MarkupPrivate\MediaProxy\isInternalLink($url)) {
+            return null;
+        }
+        $components = parse_url($url);
+        if (
+            isset($components['path'])
+            && substr($components['path'], -13) == '/sendfile.php'
+            && isset($components['query'])
+            && $components['query'] != ''
+        ) {
+            parse_str($components['query'], $queryParams);
+
+            return $callback($components, $queryParams);
+        }
+
+        return null;
+    }
+
+    private function extractFile($url)
+    {
+        return $this->applyCallbackOnInternalUrl($url, function ($components, $queryParams) {
+            if (isset($queryParams['file_id'])) {
+                $file_ref = new \FileRef($queryParams['file_id']);
+                return $file_ref;
+
+            }
+
+            return array();
+        });
+    }
 }
diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss
index 8e5e50ede8e..b986a700d7a 100755
--- a/resources/assets/stylesheets/scss/courseware.scss
+++ b/resources/assets/stylesheets/scss/courseware.scss
@@ -1657,7 +1657,7 @@ v i e w  w i d g e t
         @include background-icon(export, clickable);
     }
     .cw-action-widget-oer{
-        @include background-icon(service, clickable);
+        @include background-icon(oer-campus, clickable);
     }
 }
 
@@ -2388,6 +2388,19 @@ m a n a g e r
     }
 }
 
+.cw-import-zip {
+    margin-bottom: 1em;
+
+    header {
+        font-size: 1.15;
+        font-weight: 700;
+    }
+    .progress-bar-wrapper {
+        width: 100%;
+        border: solid thin $content-color-40;
+    }
+}
+
 /* * * * * * * * * * *
 m a n a g e r  e n d
 * * * * * * * * * * */
@@ -3654,6 +3667,7 @@ headline block
 .cw-block-headline {
     .cw-block-headline-content {
         min-height: 600px;
+        overflow: hidden;
         background-position: center;
         background-size: 1095px;
 
@@ -3670,11 +3684,11 @@ headline block
             }
             h1 {
                 font-size: 10em;
-                line-height: 1.6em;
+                line-height: 1.2em;
             }
             h2 {
                 font-size: 2em;
-                line-height: 1.6em;
+                line-height: 1em;
             }
         }
         &.bigicon_top {
@@ -3719,7 +3733,7 @@ headline block
                 width: 100%;
                 .cw-block-headline-title {
                     h1 {
-                        margin-top: 1.5em;
+                        margin-top: 2em;
                         border: none;
                         font-size: 5em;
                         text-align: center;
@@ -3824,6 +3838,141 @@ headline block
     }
 }
 
+.cw-container-colspan-half {
+    .cw-block-headline {
+        .cw-block-headline-content {
+            min-height: 300px;
+    
+            &.half {
+                min-height: 150px;
+            }
+            &.heavy {
+                h1 {
+                    font-size: 4.5em;
+                }
+                h2 {
+                    font-size: 1.25em;
+                }
+            }
+            &.bigicon_top {
+                .icon-layer {
+                    background-position: center calc(50% - 4em);
+                    min-height: 300px;
+    
+                    &.half {
+                        min-height: 150px;
+                    }
+
+                    @each $icon in $icons {
+                        &.icon-black-#{$icon} {
+                            @include background-icon($icon, info, 98);
+                        }
+                        &.icon-white-#{$icon} {
+                            @include background-icon($icon, info-alt, 98);
+                        }
+                        &.icon-studip-blue-#{$icon} {
+                            @include background-icon($icon, clickable, 98);
+                        }
+                        &.icon-studip-red-#{$icon} {
+                            @include background-icon($icon, status-red, 98);
+                        }
+                        &.icon-studip-yellow-#{$icon} {
+                            @include background-icon($icon, status-yellow, 98);
+                        }
+                        &.icon-studip-green-#{$icon} {
+                            @include background-icon($icon, status-green, 98);
+                        }
+                    };
+    
+                    &.half {
+                        background-size: 72px;
+                        background-position: center calc(50% - 2em);
+                    }
+                }
+    
+    
+                .cw-block-headline-textbox {
+                    .cw-block-headline-title {
+                        h1 {
+                            margin-top: 2.5em;
+                            font-size: 2em;
+                        }
+                    }
+    
+                    .cw-block-headline-subtitle {
+                        h2 {
+                            font-size: 12px;
+                        }
+                    }
+                }
+            }
+            &.bigicon_before {
+                .icon-layer {
+                    min-height: 300px;
+
+                    &.half {
+                        min-height: 150px;
+                    }
+                    background-position: 2em center;
+                    @each $icon in $icons {
+                        &.icon-black-#{$icon} {
+                            @include background-icon($icon, info, 92);
+                        }
+                        &.icon-white-#{$icon} {
+                            @include background-icon($icon, info-alt, 92);
+                        }
+                        &.icon-studip-blue-#{$icon} {
+                            @include background-icon($icon, clickable, 92);
+                        }
+                        &.icon-studip-red-#{$icon} {
+                            @include background-icon($icon, status-red, 92);
+                        }
+                        &.icon-studip-yellow-#{$icon} {
+                            @include background-icon($icon, status-yellow, 92);
+                        }
+                        &.icon-studip-green-#{$icon} {
+                            @include background-icon($icon, status-green, 92);
+                        }
+                    };
+                }
+    
+                .cw-block-headline-textbox {
+                    .cw-block-headline-title {
+                        h1 {
+                            font-size: 2.5em;
+                        }
+                    }
+
+                }
+            }
+    
+            &.ribbon {
+                .icon-layer {
+                    min-height: 300px;
+
+                    &.half {
+                        min-height: 150px;
+                    }
+    
+                    .cw-block-headline-textbox {
+                        .cw-block-headline-title {
+                                h1 {
+                                font-size: 2.5em;
+                            }
+                        }
+    
+                        .cw-block-headline-subtitle {
+                            h2 {
+                                font-size: 12px;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
 /*
 headline block end
 */
diff --git a/resources/vue/components/courseware/CoursewareAccordionContainer.vue b/resources/vue/components/courseware/CoursewareAccordionContainer.vue
index d69425af229..3f99e9b5a23 100755
--- a/resources/vue/components/courseware/CoursewareAccordionContainer.vue
+++ b/resources/vue/components/courseware/CoursewareAccordionContainer.vue
@@ -9,16 +9,15 @@
     >
         <template v-slot:containerContent>
             <courseware-collapsible-box
-                v-for="(section, index) in container.attributes.payload.sections"
+                v-for="(section, index) in currentSections"
                 :key="index"
                 :title="section.name"
                 :icon="section.icon"
                 :open="index === 0"
             >
                 <ul class="cw-container-accordion-block-list">
-                    <li v-for="block in blocks" :key="block.id" class="cw-block-item">
+                    <li v-for="block in section.blocks" :key="block.id" class="cw-block-item">
                         <component
-                            v-if="section.blocks.includes(block.id)"
                             :is="component(block)"
                             :block="block"
                             :canEdit="canEdit"
@@ -92,6 +91,7 @@ export default {
     data() {
         return {
             currentContainer: {},
+            currentSections: [],
         };
     },
     computed: {
@@ -103,7 +103,7 @@ export default {
                 return [];
             }
 
-            return this.container.relationships.blocks.data.map(({ id }) => this.blockById({ id }));
+            return this.container.relationships.blocks.data.map(({ id }) => this.blockById({ id })).filter((a) => a);
         },
         showEditMode() {
             return this.$store.getters.viewMode === 'edit';
@@ -123,6 +123,14 @@ export default {
         initCurrentData() {
             // clone container to make edit reversible
             this.currentContainer = JSON.parse(JSON.stringify(this.container));
+
+            let view = this;
+            let sections = this.currentContainer.attributes.payload.sections;
+            sections.forEach(section => {
+                section.blocks = section.blocks.map((id) =>  view.blockById({id})).filter((a) => a);
+            });
+
+            this.currentSections = sections;
         },
         addSection() {
             this.currentContainer.attributes.payload.sections.push({ name: '', icon: '', blocks: [] });
diff --git a/resources/vue/components/courseware/CoursewareActionWidget.vue b/resources/vue/components/courseware/CoursewareActionWidget.vue
index 07238dd46cc..6238690bac4 100644
--- a/resources/vue/components/courseware/CoursewareActionWidget.vue
+++ b/resources/vue/components/courseware/CoursewareActionWidget.vue
@@ -5,7 +5,7 @@
         <li class="cw-action-widget-info" @click="showElementInfo"><translate>Informationen anzeigen</translate></li>
         <li class="cw-action-widget-star" @click="createBookmark"><translate>Lesezeichen setzen</translate></li>
         <li v-show="canEdit" @click="exportElement" class="cw-action-widget-export"><translate>Seite exportieren</translate></li>
-        <li v-show="canEdit" @click="oerElement" class="cw-action-widget-oer"><translate>Seite auf %{oerTitle} veröffentlichen</translate></li>
+        <li v-show="canEdit && oerEnabled" @click="oerElement" class="cw-action-widget-oer"><translate>Seite auf %{oerTitle} veröffentlichen</translate></li>
         <li v-show="!isRoot && canEdit" class="cw-action-widget-trash" @click="deleteElement"><translate>Seite löschen</translate></li>
     </ul>
 </template>
@@ -30,6 +30,7 @@ export default {
     computed: {
          ...mapGetters({
             structuralElementById: 'courseware-structural-elements/byId',
+            oerEnabled: 'oerEnabled',
             oerTitle: 'oerTitle',
         }),
         structuralElement() {
@@ -112,7 +113,5 @@ export default {
             this.setCurrentId(to.params.id);
         },
     },
-
-
 }
-</script>2
\ No newline at end of file
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareAudioBlock.vue b/resources/vue/components/courseware/CoursewareAudioBlock.vue
index 33d84069693..e004f4aef13 100755
--- a/resources/vue/components/courseware/CoursewareAudioBlock.vue
+++ b/resources/vue/components/courseware/CoursewareAudioBlock.vue
@@ -41,8 +41,8 @@
                         <button class="cw-audio-button cw-audio-stopbutton" @click="stopAudio" />
                     </div>
                 </div>
-                <div v-if="hasPlaylist" class="cw-audio-playlist-wrapper">
-                    <ul class="cw-audio-playlist">
+                <div class="cw-audio-playlist-wrapper">
+                    <ul v-show="hasPlaylist" class="cw-audio-playlist">
                         <li
                             v-for="(file, index) in files"
                             :key="file.id"
@@ -56,6 +56,9 @@
                             {{ file.name }}
                         </li>
                     </ul>
+                    <div v-if="emptyAudio" class="cw-audio-empty">
+                        <p><translate>Es ist keine Audio-Datei verfügbar</translate></p>
+                    </div>
                     <div v-if="showRecorder && canGetMediaDevices" class="cw-audio-playlist-recorder">
                         <button 
                             v-show="!userRecorderEnabled"
@@ -104,9 +107,6 @@
                         </span>
                     </div>
                 </div>
-                <div v-if="emptyAudio" class="cw-audio-empty">
-                    <p><translate>Es ist keine Audio-Datei verfügbar</translate></p>
-                </div>
             </template>
             <template v-if="canEdit" #edit>
                 <form class="default" @submit.prevent="">
@@ -302,7 +302,7 @@ export default {
             return '';
         },
         emptyAudio() {
-            if (this.currentSource === 'studip_folder' && this.currentFolderId !== '') {
+            if (this.currentSource === 'studip_folder' && this.currentFolderId !== '' && this.files.length > 0) {
                 return false;
             }
             if (this.currentSource === 'studip_file' && this.currentFileId !== '') {
diff --git a/resources/vue/components/courseware/CoursewareCourseManager.vue b/resources/vue/components/courseware/CoursewareCourseManager.vue
index 6005174984b..4f3f4575c9d 100755
--- a/resources/vue/components/courseware/CoursewareCourseManager.vue
+++ b/resources/vue/components/courseware/CoursewareCourseManager.vue
@@ -18,10 +18,13 @@
                 >
                     <translate>Alles exportieren</translate>
                 </button>
-                <br>
-                <translate v-if="exportRunning">
-                    Export läuft, bitte haben sie einen Moment Geduld...
-                </translate>
+                <courseware-companion-box v-show="exportRunning" :msgCompanion="$gettext('Export läuft, bitte haben sie einen Moment Geduld...')" mood="pointing"/>
+                <div v-if="exportRunning" class="cw-import-zip">
+                    <header>{{exportState}}:</header>
+                    <div class="progress-bar-wrapper">
+                        <div class="progress-bar" role="progressbar" :style="{width: exportProgress + '%'}" :aria-valuenow="exportProgress" aria-valuemin="0" aria-valuemax="100">{{ exportProgress }}%</div>
+                    </div>
+                </div>
             </courseware-tab>
         </courseware-tabs>
 
@@ -88,34 +91,41 @@
             </courseware-tab>
 
             <courseware-tab :name="$gettext('Importieren')">
+                <courseware-companion-box v-show="!importRunning && importDone" :msgCompanion="$gettext('Import erfolgreich!')" mood="special"/>
+                <courseware-companion-box v-show="importRunning" :msgCompanion="$gettext('Import läuft. Bitte verlassen Sie die Seite nicht bis der Import abgeschlossen wurde.')" mood="pointing"/>
                 <button
+                    v-show="!importRunning"
                     class="button"
                     @click.prevent="chooseFile"
-                    :class="{
-                        disabled: importRunning,
-                    }"
                 >
-                    Importdatei auswählen
+                    <translate>Importdatei auswählen</translate>
                 </button>
 
-                <div v-if="importZip">
-                    <b>{{ importZip.name }}</b
-                    ><br />
-                    <translate>Größe</translate>: <span>{{ getFileSizeText(importZip.size) }}</span>
+                <div v-if="importZip" class="cw-import-zip">
+                    <header>{{ importZip.name }}</header>
+                    <p><translate>Größe</translate>: {{ getFileSizeText(importZip.size) }}</p>
                 </div>
 
-                <br v-else />
+                <div v-if="importRunning" class="cw-import-zip">
+                    <header><translate>Importiere Dateien</translate>:</header>
+                    <div class="progress-bar-wrapper">
+                        <div class="progress-bar" role="progressbar" :style="{width: importFilesProgress + '%'}" :aria-valuenow="importFilesProgress" aria-valuemin="0" aria-valuemax="100">{{ importFilesProgress }}%</div>
+                    </div>
+                    {{ importFilesState }}
+                </div>
 
-                <div v-if="importState">
-                    {{ importState }}
+                <div v-if="fileImportDone && importRunning" class="cw-import-zip">
+                    <header><translate>Importiere Elemente</translate>:</header>
+                    <div class="progress-bar-wrapper">
+                        <div class="progress-bar" role="progressbar" :style="{width: importStructuresProgress + '%'}" :aria-valuenow="importStructuresProgress" aria-valuemin="0" aria-valuemax="100">{{ importStructuresProgress }}%</div>
+                    </div>
+                    {{ importStructuresState }}
                 </div>
 
                 <button
+                    v-show="importZip && !importRunning"
                     class="button"
                     @click.prevent="doImportCourseware"
-                    :class="{
-                        disabled: importRunning || !importZip,
-                    }"
                 >
                     <translate>Alles importieren</translate>
                 </button>
@@ -131,6 +141,7 @@ import CoursewareTab from './CoursewareTab.vue';
 import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue';
 import CoursewareManagerElement from './CoursewareManagerElement.vue';
 import CoursewareManagerCopySelector from './CoursewareManagerCopySelector.vue';
+import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
 import CoursewareImport from '@/vue/mixins/courseware/import.js';
 import CoursewareExport from '@/vue/mixins/courseware/export.js';
 import { mapActions, mapGetters } from 'vuex';
@@ -146,6 +157,7 @@ export default {
         CoursewareCollapsibleBox,
         CoursewareManagerElement,
         CoursewareManagerCopySelector,
+        CoursewareCompanionBox,
     },
 
     mixins: [CoursewareImport, CoursewareExport],
@@ -155,8 +167,6 @@ export default {
             exportRunning: false,
             importRunning: false,
             importZip: null,
-            importState: '',
-            importPos: 0,
             currentElement: {},
             currentId: null,
             selfElement: {},
@@ -169,6 +179,12 @@ export default {
         ...mapGetters({
             courseware: 'courseware',
             structuralElementById: 'courseware-structural-elements/byId',
+            importFilesState: 'importFilesState',
+            importFilesProgress: 'importFilesProgress',
+            importStructuresState: 'importStructuresState',
+            importStructuresProgress: 'importStructuresProgress',
+            exportState: 'exportState',
+            exportProgress: 'exportProgress'
         }),
         moveSelfPossible() {
             if (this.selfElement.relationships === undefined) {
@@ -186,6 +202,12 @@ export default {
         moveSelfChildPossible() {
             return this.currentId !== this.selfId;
         },
+        fileImportDone() {
+            return this.importFilesProgress === 100;
+        },
+        importDone() {
+            return this.importFilesProgress === 100 && this.importStructuresProgress === 100;
+        }
     },
 
     methods: {
@@ -199,6 +221,8 @@ export default {
             unlockObject: 'unlockObject',
             addBookmark: 'addBookmark',
             companionInfo: 'companionInfo',
+            setImportFilesProgress: 'setImportFilesProgress',
+            setImportStructuresProgress: 'setImportStructuresProgress',
         }),
         async reloadElements() {
             await this.setCurrentId(this.currentId);
@@ -220,16 +244,6 @@ export default {
         initSelf() {
             this.selfElement = this.structuralElementById({ id: this.selfId });
         },
-        animateImport() {
-            // get number of dots
-            this.importPos++;
-
-            if (this.importPos > 3) {
-                this.importPos = 0;
-            }
-
-            this.importState = this.$gettext('Import läuft') + '.'.repeat(this.importPos);
-        },
 
         async doExportCourseware() {
             if (this.exportRunning) {
@@ -239,12 +253,15 @@ export default {
             this.exportRunning = true;
 
             await this.loadCoursewareStructure();
-            await this.sendExportZip();
+            await this.sendExportZip(
+                this.courseware.relationships.root.data.id,
+                {withChildren: true}
+            );
 
             this.exportRunning = false;
         },
 
-        setImport() {
+        setImport(event) {
             this.importZip = event.target.files[0];
         },
 
@@ -254,7 +271,6 @@ export default {
             }
 
             this.importRunning = true;
-            this.animateImport();
 
             let view = this;
 
@@ -273,13 +289,14 @@ export default {
                 await view.importCourseware(courseware, parent_id, files);
             });
 
-            this.importState = this.$gettext('Import erfolgreich!');
             this.importZip = null;
             this.importRunning = false;
         },
 
         chooseFile() {
             this.$refs.importFile.click();
+            this.setImportFilesProgress(0);
+            this.setImportStructuresProgress(0);
         },
         getFileSizeText(size) {
             if (size / 1024 < 1000) {
diff --git a/resources/vue/components/courseware/CoursewareImageMapBlock.vue b/resources/vue/components/courseware/CoursewareImageMapBlock.vue
index f82bd410d13..082e2ac9ef3 100755
--- a/resources/vue/components/courseware/CoursewareImageMapBlock.vue
+++ b/resources/vue/components/courseware/CoursewareImageMapBlock.vue
@@ -372,31 +372,37 @@ export default {
                 context.closePath();
             });
         },
-        fitTextToShape(context, text, shape_width) {
-            let text_width = context.measureText(text).width;
-            if (text_width > shape_width) {
-                text = text.split(' ');
-                let line = '';
-                let word = ' ';
-                let new_text = [];
-                do {
-                    word = text.shift();
-                    if (context.measureText(word).width >= shape_width) {
-                        return [''];
-                    }
-                    line = line + word + ' ';
-                    if (context.measureText(line).width > shape_width) {
-                        text.unshift(word);
-                        line = line.substring(0, line.lastIndexOf(word));
-                        new_text.push(line.trim());
-                        line = '';
-                    }
-                } while (text.length > 0);
-                new_text.push(line.trim());
-                return new_text;
-            } else {
+        fitTextToShape( context , text, shapeWidth) {
+            shapeWidth = shapeWidth || 0;
+
+            let newText = [];
+            
+            if (shapeWidth <= 0) {
                 return [text];
             }
+            let words = text.split(' ');
+            let i = 1;
+            while (words.length > 0 && i <= words.length) {
+                let word = words.slice(0, i).join(' ');
+                let wordWidth = context.measureText(word).width + 2;
+                if ( wordWidth > shapeWidth ) {
+                    if (i === 1) {
+                        i = 2;
+                    }
+                    newText.push(words.slice(0, i - 1).join(' '));
+                    words = words.splice(i - 1);
+                    i = 1;
+                }
+                else {
+                    i++;
+                }
+            }
+            if (i > 0) {
+                newText.push(words.join(' '));
+            }
+
+            return newText;
+
         },
         mapImage() {
             let view = this;
diff --git a/resources/vue/components/courseware/CoursewareListContainer.vue b/resources/vue/components/courseware/CoursewareListContainer.vue
index 794231c7b8c..096befe2b8e 100755
--- a/resources/vue/components/courseware/CoursewareListContainer.vue
+++ b/resources/vue/components/courseware/CoursewareListContainer.vue
@@ -43,7 +43,7 @@ export default {
                 return [];
             }
 
-            return this.container.relationships.blocks.data.map(({ id }) => this.blockById({ id }));
+            return this.container.attributes.payload.sections[0].blocks.map((id) => this.blockById({ id })).filter((a) => a);
         },
         showEditMode() {
             return this.$store.getters.viewMode === 'edit';
@@ -51,7 +51,6 @@ export default {
     },
     methods: {
         storeContainer(data) {
-            console.log(data);
         },
         component(block) {
             if (block.attributes["block-type"] !== undefined) {
diff --git a/resources/vue/components/courseware/CoursewareManagerContainer.vue b/resources/vue/components/courseware/CoursewareManagerContainer.vue
index 62a1015687f..9a8a6ddea04 100755
--- a/resources/vue/components/courseware/CoursewareManagerContainer.vue
+++ b/resources/vue/components/courseware/CoursewareManagerContainer.vue
@@ -127,7 +127,7 @@ export default {
             return this.container.attributes['container-type'];
         },
         hasSections() {
-            return this.containerType() === 'tabs' || this.containerType() === 'accordion';
+            return this.containerType === 'tabs' || this.containerType === 'accordion';
         },
         getBlocksCount() {
             if (this.sectionsWithBlocksCurrentState === null) {
@@ -146,7 +146,7 @@ export default {
          }
     },
     mounted() {
-        this.sectionsWithBlocksCurrentState = this.getSectionsWithBlocks();
+        this.initSections();
     },
     methods: {
         ...mapActions({
@@ -181,36 +181,33 @@ export default {
                             return this.blockById({ id }) ?? [] //remove blocks which could not be loaded
                         }
                     );
-
-                    section.blocks.sort((a, b) => {
-                        return a.attributes.position > b.attributes.position;
-                    });
                 }
             });
 
             return blockSections;
         },
+        initSections() {
+            this.sectionsWithBlocksCurrentState = this.getSectionsWithBlocks();
+        },
         insertBlock(data) {
             this.$emit('insertBlock', data);
+            this.initSections();
         },
         sortBlocks() {
             this.sortBlocksActive = true;
         },
         async storeBlocksSort() {
-            const container = this.container;
+            const container = JSON.parse(JSON.stringify(this.container));
 
             this.sectionsWithBlocksCurrentState.forEach((section, index)=> {
                 if (section.blocks !== undefined) {
                     container.attributes.payload.sections[index].blocks = section.blocks.map(({ id }) => ( id ));
                 }
             });
-
             await this.lockObject({id: container.id, type: 'courseware-containers'});
             await this.updateContainer({ container: container, structuralElementId: this.container.relationships['structural-element'].data.id });
             await this.unlockObject({id: container.id, type: 'courseware-containers'});
 
-            await this.sortBlocksInContainer({ container: this.container, sections: this.sectionsWithBlocksCurrentState });
-
             this.sortBlocksActive = false;
         },
         resetBlocksSort() {
@@ -262,5 +259,15 @@ export default {
             });
         },
     },
+    watch: {
+        container: {
+            handler(state, prevState) {
+                if (state.attributes.payload.sections[0].blocks !== prevState.attributes.payload.sections[0].blocks) {
+                    this.initSections();
+                }
+            },
+            deep: true
+        }
+    },
 };
 </script>
diff --git a/resources/vue/components/courseware/CoursewareManagerElement.vue b/resources/vue/components/courseware/CoursewareManagerElement.vue
index 3794d701fe3..b703e8da78f 100755
--- a/resources/vue/components/courseware/CoursewareManagerElement.vue
+++ b/resources/vue/components/courseware/CoursewareManagerElement.vue
@@ -247,16 +247,11 @@ export default {
             }
         },
         containers() {
-            if (!this.currentElement) {
+            if (!this.currentElement || !this.currentElement.relationships) {
                 return [];
             }
 
-            const containers = this.$store.getters['courseware-containers/related']({
-                parent: this.currentElement,
-                relationship: 'containers',
-            });
-
-            return containers;
+            return this.currentElement.relationships.containers.data.map(({id}) => this.containerById({ id }));
         },
         children() {
             if (!this.currentElement) {
diff --git a/resources/vue/components/courseware/CoursewareStructuralElement.vue b/resources/vue/components/courseware/CoursewareStructuralElement.vue
index 00a24fb3158..78af7a4992d 100755
--- a/resources/vue/components/courseware/CoursewareStructuralElement.vue
+++ b/resources/vue/components/courseware/CoursewareStructuralElement.vue
@@ -297,22 +297,29 @@
             :confirmClass="'accept'"
             :closeText="textExport.close"
             :closeClass="'cancel'"
+            height="350"
             @close="showElementExportDialog(false)"
             @confirm="exportCurrentElement"
         >
             <template v-slot:dialogContent>
-                <translate>Hiermit exportieren Sie die Seite "{{ currentElement.attributes.title }}" als ZIP-Datei.</translate>
+                <div v-show="!exportRunning">
+                    <translate>Hiermit exportieren Sie die Seite "{{ currentElement.attributes.title }}" als ZIP-Datei.</translate>
 
-                <div class="cw-element-export">
-                    <label>
-                        <input type="checkbox" v-model="exportChildren">
-                        <translate>Unterseiten exportieren</translate>
-                    </label>
+                    <div class="cw-element-export">
+                        <label>
+                            <input type="checkbox" v-model="exportChildren">
+                            <translate>Unterseiten exportieren</translate>
+                        </label>
+                    </div>
                 </div>
 
-                <translate v-if="exportRunning">
-                    Export läuft...
-                </translate>
+                <courseware-companion-box v-show="exportRunning" :msgCompanion="$gettext('Export läuft, bitte haben sie einen Moment Geduld...')" mood="pointing"/>
+                <div v-show="exportRunning" class="cw-import-zip">
+                    <header>{{exportState}}:</header>
+                    <div class="progress-bar-wrapper">
+                        <div class="progress-bar" role="progressbar" :style="{width: exportProgress + '%'}" :aria-valuenow="exportProgress" aria-valuemin="0" aria-valuemax="100">{{ exportProgress }}%</div>
+                    </div>
+                </div>
             </template>
 
         </studip-dialog>
@@ -483,8 +490,11 @@ export default {
             showInfoDialog: 'showStructuralElementInfoDialog',
             showDeleteDialog : 'showStructuralElementDeleteDialog',
             showOerDialog : 'showStructuralElementOerDialog',
+            oerEnabled: 'oerEnabled',
             oerTitle: 'oerTitle',
-            licenses: 'licenses'
+            licenses: 'licenses',
+            exportState: 'exportState',
+            exportProgress: 'exportProgress'
         }),
 
         textOer() {
@@ -694,7 +704,10 @@ export default {
                 menu.push({ id: 1, label: this.$gettext('Seite bearbeiten'), icon: 'edit', emit: 'editCurrentElement' });
                 menu.push({ id: 2, label: this.$gettext('Seite hinzufügen'), icon: 'add', emit: 'addElement' });
                 menu.push({ id: 5, label: this.$gettext('Seite exportieren'), icon: 'export', emit: 'showExportOptions' });
-                menu.push({ id: 6, label: this.textOer.title, icon: 'service', emit: 'oerCurrentElement' });
+                
+            }
+            if (this.canEdit && this.oerEnabled) {
+                menu.push({ id: 6, label: this.textOer.title, icon: 'oer-campus', emit: 'oerCurrentElement' });
             }
             if(!this.isRoot && this.canEdit) {
                 menu.push({ id: 7, label: this.$gettext('Seite löschen'), icon: 'trash', emit: 'deleteCurrentElement' });
diff --git a/resources/vue/components/courseware/CoursewareTabsContainer.vue b/resources/vue/components/courseware/CoursewareTabsContainer.vue
index 397c7e0cd28..d14503e3d95 100755
--- a/resources/vue/components/courseware/CoursewareTabsContainer.vue
+++ b/resources/vue/components/courseware/CoursewareTabsContainer.vue
@@ -20,7 +20,6 @@
                     <ul class="cw-container-tabs-block-list">
                         <li v-for="block in section.blocks" :key="block.id" class="cw-block-item">
                             <component
-                                v-if="section.blocks.includes(block.id)"
                                 :is="component(block)"
                                 :block="block"
                                 :canEdit="canEdit"
@@ -111,7 +110,7 @@ export default {
                 return [];
             }
 
-            return this.container.relationships.blocks.data.map(({ id }) => this.blockById({ id }));
+            return this.container.relationships.blocks.data.map(({ id }) => this.blockById({ id })).filter((a) => a);
         },
         showEditMode() {
             return this.$store.getters.viewMode === 'edit';
@@ -135,7 +134,7 @@ export default {
             let view = this;
             let sections = this.currentContainer.attributes.payload.sections;
             sections.forEach(section => {
-                section.blocks = section.blocks.map((id) => view.blockById({id}));
+                section.blocks = section.blocks.map((id) =>  view.blockById({id})).filter((a) => a);
             });
 
             this.currentSections = sections;
diff --git a/resources/vue/courseware-index-app.js b/resources/vue/courseware-index-app.js
index 1294de2cbb9..2594e66ec23 100755
--- a/resources/vue/courseware-index-app.js
+++ b/resources/vue/courseware-index-app.js
@@ -25,6 +25,7 @@ const mountApp = (STUDIP, createApp, element) => {
     let elem_id = null;
     let entry_id = null;
     let entry_type = null;
+    let oer_enabled = null;
     let oer_title = null;
     let licenses = null;
     let elem;
@@ -43,6 +44,10 @@ const mountApp = (STUDIP, createApp, element) => {
                 entry_id = elem.attributes['entry-id'].value;
             }
 
+            if (elem.attributes['oer-enabled'] !== undefined) {
+                oer_enabled = elem.attributes['oer-enabled'].value;
+            }
+
             if (elem.attributes['oer-title'] !== undefined) {
                 oer_title = elem.attributes['oer-title'].value;
             }
@@ -116,6 +121,7 @@ const mountApp = (STUDIP, createApp, element) => {
 
     store.dispatch('coursewareCurrentElement', elem_id);
 
+    store.dispatch('oerEnabled', oer_enabled);
     store.dispatch('oerTitle', oer_title);
     store.dispatch('licenses', licenses);
 
diff --git a/resources/vue/mixins/courseware/export.js b/resources/vue/mixins/courseware/export.js
index 7a1a8e2efb0..65e8ece5b3d 100755
--- a/resources/vue/mixins/courseware/export.js
+++ b/resources/vue/mixins/courseware/export.js
@@ -20,13 +20,22 @@ export default {
                 json: [],
                 download: [],
             },
+            elementCounter: 0,
+            exportElementCounter: 0,
         };
     },
 
     methods: {
         async sendExportZip(root_id = null, options) {
+            let view = this;
             let zip = await this.createExportFile(root_id, options);
-            await zip.generateAsync({ type: 'blob' }).then(function (content) {
+            this.setExportState(this.$gettext('Erstelle Zip-Archiv'));
+            this.setExportProgress(0);
+            await zip.generateAsync({ type: 'blob' }, function updateCallback(metadata) {
+                view.setExportProgress(metadata.percent.toFixed(0));
+            }).then(function (content) {
+                view.setExportState('');
+                view.setExportProgress(0);
                 FileSaver.saveAs(content, 'courseware-export-' + new Date().toISOString().slice(0, 10) + '.zip');
             });
         },
@@ -38,7 +47,8 @@ export default {
                 root_id = this.courseware.relationships.root.data.id;
                 completeExport = true;
             }
-
+            this.setExportState(this.$gettext('Exportiere Elemente'));
+            this.setExportProgress(0);
             let exportData = await this.exportCourseware(root_id, options);
 
             let zip = new JSZip();
@@ -50,6 +60,10 @@ export default {
             }
 
             // add all additional files from blocks
+            let i = 1;
+            let filesCounter = Object.keys(exportData.files.download).length;
+            this.setExportState(this.$gettext('Lade Dateien'));
+            this.setExportProgress(0);
             for (let id in exportData.files.download) {
                 zip.file(
                     id,
@@ -59,6 +73,8 @@ export default {
                             return textString;
                         })
                 );
+                this.setExportProgress(parseInt(i / filesCounter * 100));
+                i++;
             }
 
             return zip;
@@ -78,6 +94,8 @@ export default {
 
             // load whole courseware nonetheless, only export relevant elements
             let elements = await this.$store.getters['courseware-structural-elements/all'];
+            this.exportElementCounter = 0;
+            this.elementCounter = await this.countElements([root_element]);
 
             root_element.containers = [];
             if (root_element.relationships.containers?.data?.length) {
@@ -89,6 +107,7 @@ export default {
                             })
                         )
                     );
+                    this.exportElementCounter++;
                 }
             }
 
@@ -115,6 +134,28 @@ export default {
             };
         },
 
+        countElements(element) {
+            let counter = 0;
+            if (element.length) {
+                for (var i = 0; i < element.length; i++) {
+                    counter++;
+                    if (element[i].relationships.children?.data?.length > 0) {
+                        let children = [];
+                        element[i].relationships.children?.data.forEach(child => {
+                            children.push(this.structuralElementById({id: child.id}));
+                        });
+                        counter += this.countElements(children);
+                    }
+
+                    if (element[i].relationships.containers?.data?.length > 0) {
+                        counter += element[i].relationships.containers.data.length
+                    }
+                }
+            }
+
+            return counter;
+        },
+
         async exportToOER(element, options) {
             let formData = new FormData();
 
@@ -155,6 +196,7 @@ export default {
             for (var i = 0; i < data.length; i++) {
                 if (data[i].relationships.parent.data?.id === parentId) {
                     let new_childs = await this.exportStructuralElement(data[i].id, data);
+                    this.exportElementCounter++;
                     let content = { ...data[i] };
                     content.containers = [];
 
@@ -172,6 +214,7 @@ export default {
                                     })
                                 )
                             );
+                            this.exportElementCounter++;
                         }
                     }
 
@@ -255,7 +298,16 @@ export default {
             'loadStructuralElement',
             'loadFileRefs',
             'loadFolder',
-            'companionInfo'
+            'companionInfo',
+            'setExportState',
+            'setExportProgress'
         ]),
     },
+    watch: {
+        exportElementCounter(counter) {
+            if (this.elementCounter !== 0) {
+                this.setExportProgress(parseInt(counter / this.elementCounter * 100));
+            }
+        }
+    },
 };
diff --git a/resources/vue/mixins/courseware/import.js b/resources/vue/mixins/courseware/import.js
index c2b83c7d3ac..703f0100191 100755
--- a/resources/vue/mixins/courseware/import.js
+++ b/resources/vue/mixins/courseware/import.js
@@ -4,7 +4,9 @@ export default {
     data() {
         return {
             importFolder: null,
-            file_mapping: {}
+            file_mapping: {},
+            elementCounter: 0,
+            importElementCounter: 0,
         };
     },
 
@@ -16,30 +18,57 @@ export default {
     },
 
     methods: {
-        animateImport() {},
 
         async importCourseware(element, parent_id, files)
         {
             // import all files
             await this.uploadAllFiles(files);
 
-            this.animateImport();
+            this.elementCounter = await this.countImportElements([element]);
+            this.setImportStructuresState('');
+            this.importElementCounter = 0;
 
             await this.importStructuralElement([element], parent_id, files);
 
         },
 
+        countImportElements(element) {
+            let counter = 0;
+            if (element.length) {
+                for (var i = 0; i < element.length; i++) {
+                    counter++;
+                    if (element[i].children?.length > 0) {
+                        counter += this.countImportElements(element[i].children);
+                    }
+
+                    if (element[i].containers?.length > 0) {
+                        for (var j = 0; j < element[i].containers.length; j++) {
+                            counter++;
+                            let container = element[i].containers[j];
+                            if (container.blocks?.length) {
+                                for (var k = 0; k < container.blocks.length; k++) {
+                                    counter++;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            return counter;
+        },
+
         async importStructuralElement(element, parent_id, files) {
             if (element.length) {
                 for (var i = 0; i < element.length; i++) {
                     // TODO: create element on server and fetch new id
+                    this.setImportStructuresState('Lege Seite an: ' + element[i].attributes.title);
                     await this.createStructuralElement({
                         attributes: element[i].attributes,
                         parentId: parent_id,
                         currentId: parent_id,
                     });
-
-                    this.animateImport();
+                    this.importElementCounter++;
 
                     let new_element = this.$store.getters['courseware-structural-elements/lastCreated'];
                     if (element[i].children?.length > 0) {
@@ -50,19 +79,24 @@ export default {
                         for (var j = 0; j < element[i].containers.length; j++) {
                             let container = element[i].containers[j];
                             // TODO: create element on server and fetch new id
+                            this.setImportStructuresState('Lege Abschnitt an: ' + container.attributes.title);
                             await this.createContainer({
                                 attributes: container.attributes,
                                 structuralElementId: new_element.id,
                             });
-
-                            this.animateImport();
+                            this.importElementCounter++;
 
                             let new_container = this.$store.getters['courseware-containers/lastCreated'];
+                            await this.unlockObject({ id: new_container.id, type: 'courseware-containers' });
 
                             if (container.blocks?.length) {
+                                let new_block = null;
                                 for (var k = 0; k < container.blocks.length; k++) {
-                                    await this.importBlock(container.blocks[k], new_container, files);
+                                    new_block = await this.importBlock(container.blocks[k], new_container, files);
+                                    this.importElementCounter++;
+                                    await this.updateContainerPayload(new_container, new_element.id, container.blocks[k].id, new_block.id);
                                 }
+
                             }
                         }
                     }
@@ -72,13 +106,12 @@ export default {
 
         async importBlock(block, block_container, files) {
             // TODO: create element
+            this.setImportStructuresState('Lege neuen Block an: ' + block.attributes.title);
             await this.createBlockInContainer({
                 container: {type: block_container.type, id: block_container.id},
                 blockType: block.attributes['block-type'],
             });
 
-            this.animateImport();
-
             let new_block = this.$store.getters['courseware-blocks/lastCreated'];
 
             // update old id ids in payload part
@@ -94,20 +127,41 @@ export default {
                     block.attributes.payload = JSON.parse(payload);
                 }
             }
-
+            this.setImportStructuresState('Aktualisiere neuen Block: ' + block.attributes.title);
             await this.updateBlockInContainer({
                 attributes: block.attributes,
                 blockId: new_block.id,
                 containerId: block_container.id,
             });
 
-            this.animateImport();
+            return new_block;
+        },
+
+        async updateContainerPayload(container, structuralElementId, oldBlockId, newBlockId) {
+
+            container.attributes.payload.sections.forEach((section, index) => {
+                let blockIndex = section.blocks.findIndex(blockID => blockID === oldBlockId);
+                
+                if(blockIndex > -1) {
+                    container.attributes.payload.sections[index].blocks[blockIndex] = newBlockId; 
+                }
+            });
+
+            await this.lockObject({ id: container.id, type: 'courseware-containers' });
+            await this.updateContainer({
+                container: container,
+                structuralElementId: structuralElementId
+            });
+            await this.unlockObject({ id: container.id, type: 'courseware-containers' });
         },
 
 
         async uploadAllFiles(files) {
             // create folder for importing the files into
+            this.setImportFilesProgress(0);
+            this.setImportFilesState('');
             let now = new Date();
+            this.setImportFilesState('Lege Import Ordner an...');
             let main_folder = await this.createRootFolder({
                 context: this.context,
                 folder: {
@@ -118,16 +172,14 @@ export default {
                 }
             });
 
-            this.animateImport();
-
             let folders = {};
 
             // upload all files to the newly created folder
             if (main_folder) {
                 for (var i = 0; i < files.length; i++) {
-
                     // if the subfolder with the referenced id does not exist yet, create it
                     if (!folders[files[i].folder.id]) {
+                        this.setImportFilesState(this.$gettext('Lege Ordner an') + ': ' + files[i].folder.name);
                         folders[files[i].folder.id] = await this.createFolder({
                             context: this.context,
                             parent: {
@@ -137,7 +189,7 @@ export default {
                                 }
                             },
                             folder: {
-                                type: files[i].folder.type,
+                                type: 'StandardFolder',
                                 name: files[i].folder.name
                             }
                         });
@@ -155,8 +207,8 @@ export default {
                             filedata: filedata,
                             folder: folders[files[i].folder.id]
                         });
-
-                        this.animateImport();
+                        this.setImportFilesState(this.$gettext('Erzeuge Datei') + ': ' + files[i].attributes.name);
+                        this.setImportFilesProgress(parseInt(i / files.length * 100));
 
                         //file mapping
                         this.file_mapping[files[i].id] = {
@@ -168,6 +220,8 @@ export default {
             } else {
                 return false;
             }
+            this.setImportFilesProgress(100);
+            this.setImportFilesState('');
 
             return true;
         },
@@ -176,10 +230,26 @@ export default {
             'createBlockInContainer',
             'createContainer',
             'createStructuralElement',
+            'updateContainer',
             'updateBlockInContainer',
             'createFolder',
             'createRootFolder',
-            'createFile'
+            'createFile',
+            'lockObject',
+            'unlockObject',
+            'setImportFilesState',
+            'setImportFilesProgress',
+            'setImportStructuresState',
+            'setImportStructuresProgress',
         ]),
     },
+    watch: {
+        importElementCounter(counter) {
+            if (this.elementCounter !== 0) {
+                this.setImportStructuresProgress(parseInt(counter / this.elementCounter * 100));
+            } else {
+                this.setImportStructuresProgress(100);
+            }
+        }
+    },
 };
diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js
index 90d6f91039f..3323ed421d5 100755
--- a/resources/vue/store/courseware/courseware.module.js
+++ b/resources/vue/store/courseware/courseware.module.js
@@ -8,6 +8,7 @@ const getDefaultState = () => {
         context: {},
         courseware: {},
         currentElement: {},
+        oerEnabled: null,
         oerTitle: null,
         licenses: null, // we need a route for License SORM
         httpClient: null,
@@ -31,6 +32,14 @@ const getDefaultState = () => {
         showStructuralElementInfoDialog: false,
         showStructuralElementDeleteDialog: false,
         showStructuralElementOerDialog: false,
+
+        importFilesState: '',
+        importFilesProgress: 0,
+        importStructuresState: '',
+        importStructuresProgress: 0,
+
+        exportState: '',
+        exportProgress: 0,
     };
 };
 
@@ -49,6 +58,9 @@ const getters = {
     currentElement(state) {
         return state.currentElement;
     },
+    oerEnabled(state) {
+        return state.oerEnabled;
+    },
     oerTitle(state) {
         return state.oerTitle;
     },
@@ -129,7 +141,25 @@ const getters = {
     },
     showStructuralElementDeleteDialog(state) {
         return state.showStructuralElementDeleteDialog;
-    }
+    },
+    importFilesState(state) {
+        return state.importFilesState;
+    },
+    importFilesProgress(state) {
+        return state.importFilesProgress;
+    },
+    importStructuresState(state) {
+        return state.importStructuresState;
+    },
+    importStructuresProgress(state) {
+        return state.importStructuresProgress;
+    },
+    exportState(state) {
+        return state.exportState;
+    },
+    exportProgress(state) {
+        return state.exportProgress;
+    },
 };
 
 export const state = { ...initialState };
@@ -205,7 +235,7 @@ export const actions = {
     async createRootFolder({ dispatch, rootGetters }, { context, folder }) {
         // get root folder for this context
         await dispatch(
-            'courses/loadRelated',
+            `${context.type}/loadRelated`,
             {
                 parent: context,
                 relationship: 'folders',
@@ -213,7 +243,7 @@ export const actions = {
             { root: true }
         );
 
-        let folders = await rootGetters['courses/related']({
+        let folders = await rootGetters[`${context.type}/related`]({
             parent: context,
             relationship: 'folders',
         });
@@ -244,7 +274,7 @@ export const actions = {
             },
         };
 
-        return state.httpClient.post(`courses/${context.id}/folders`, newFolder).then((response) => {
+        return state.httpClient.post(`${context.type}/${context.id}/folders`, newFolder).then((response) => {
             return response.data.data;
         });
     },
@@ -263,7 +293,7 @@ export const actions = {
             },
         };
 
-        return state.httpClient.post(`courses/${context.id}/folders`, newFolder).then((response) => {
+        return state.httpClient.post(`${context.type}/${context.id}/folders`, newFolder).then((response) => {
             return response.data.data;
         });
     },
@@ -597,6 +627,10 @@ export const actions = {
         context.commit('coursewareContextSet', id);
     },
 
+    oerEnabled(context, enabled) {
+        context.commit('oerEnabledSet', enabled);
+    },
+
     oerTitle(context, title) {
         context.commit('oerTitleSet', title);
     },
@@ -673,6 +707,26 @@ export const actions = {
         context.commit('setShowStructuralElementDeleteDialog', bool)
     },
 
+    setImportFilesState({commit}, state ) {
+        commit('setImportFilesState', state)
+    },
+    setImportFilesProgress({commit}, percent ) {
+        commit('setImportFilesProgress', percent)
+    },
+    setImportStructuresState({commit}, state ) {
+        commit('setImportStructuresState', state)
+    },
+    setImportStructuresProgress({commit}, percent ) {
+        commit('setImportStructuresProgress', percent)
+    },
+
+    setExportState({commit}, state) {
+        commit('setExportState', state)
+    },
+    setExportProgress({commit}, percent) {
+        commit('setExportProgress', percent)
+    },
+
     addBookmark({ dispatch, rootGetters }, structuralElement) {
         const cw = rootGetters['courseware'];
 
@@ -893,6 +947,10 @@ export const mutations = {
         state.context = data;
     },
 
+    oerEnabledSet(state, data) {
+        state.oerEnabled = data;
+    },
+
     oerTitleSet(state, data) {
         state.oerTitle = data;
     },
@@ -979,7 +1037,31 @@ export const mutations = {
 
     setShowStructuralElementDeleteDialog(state, showDelete) {
         state.showStructuralElementDeleteDialog = showDelete;
+    },
+
+    setImportFilesState(state, importFilesState) {
+        state.importFilesState = importFilesState;
+    },
+
+    setImportFilesProgress(state, importFilesProgress) {
+        state.importFilesProgress = importFilesProgress;
+    },
+
+    setImportStructuresState(state, importStructuresState) {
+        state.importStructuresState = importStructuresState;
+    },
+
+    setImportStructuresProgress(state, importStructuresProgress) {
+        state.importStructuresProgress = importStructuresProgress;
+    },
+
+    setExportState(state, exportState) {
+        state.exportState = exportState;
+    },
+    setExportProgress(state, exportProgress) {
+        state.exportProgress = exportProgress;
     }
+
 };
 
 export default {
-- 
GitLab