From 2f36c2ffa4d27511db78b89719aaf74a0143c096 Mon Sep 17 00:00:00 2001
From: Marcus Eibrink-Lunzenauer <lunzenauer@elan-ev.de>
Date: Tue, 18 Mar 2025 13:53:50 +0100
Subject: [PATCH] Import changes for vite.

---
 app/controllers/course/files.php              |    2 +-
 app/controllers/files.php                     |    2 +-
 app/controllers/institute/files.php           |    2 +-
 app/views/messages/write.php                  |    2 +-
 lib/classes/PageLayout.php                    |   19 +-
 package.json                                  |    3 +
 .../assets/javascripts/bootstrap/admission.js |    2 +-
 resources/assets/javascripts/bootstrap/vue.js |   18 +-
 resources/assets/javascripts/chunk-loader.js  |   35 +-
 .../assets/javascripts/chunks/tablesorter.js  |    4 -
 resources/assets/javascripts/chunks/vue.js    |    3 +
 .../{entry-base.js => entry-bootstrap.js}     |   13 +-
 .../assets/javascripts/entry-legacy-libs.js   |   71 +
 resources/assets/javascripts/entry-lib.js     |    3 +
 resources/assets/javascripts/entry-wysiwyg.js |    1 -
 resources/assets/javascripts/init.js          |  171 ---
 resources/assets/javascripts/jquery-bundle.js |  150 ---
 resources/assets/javascripts/lib/admission.js |   18 +-
 .../assets/javascripts/lib/fullcalendar.js    |   39 +-
 resources/assets/javascripts/lib/gettext.ts   |    3 +-
 .../assets/javascripts/lib/reststate-vuex.js  |    3 +-
 resources/assets/javascripts/lib/studip.js    |  169 +++
 resources/assets/javascripts/public-path.js   |    2 -
 resources/assets/stylesheets/mixins.scss      |    2 +-
 resources/assets/stylesheets/print.scss       |    4 +-
 .../stylesheets/scss/font-face-lato.scss      |   80 +-
 .../assets/stylesheets/scss/installer.scss    |   12 +-
 .../stylesheets/scss/jquery-ui/studip.scss    |    2 +-
 .../assets/stylesheets/scss/messagebox.scss   |    2 +-
 .../assets/stylesheets/scss/selects.scss      |    2 +-
 .../stylesheets/scss/table_of_contents.scss   |    2 +-
 resources/assets/stylesheets/scss/vips.scss   |    2 +-
 .../assets/stylesheets/studip-jquery-ui.scss  |    3 +-
 resources/vue/avatar-app.js                   |    2 +-
 resources/vue/base-components.js              |   47 +-
 .../vue/components/CacheAdministration.vue    |    2 +-
 .../vue/components/ConsultationCreator.vue    |    6 +-
 resources/vue/components/ContentBar.vue       |    9 +-
 .../vue/components/ContentBarTocItemList.vue  |    2 +-
 resources/vue/components/SearchWidget.vue     |    4 +-
 resources/vue/components/SearchWithFilter.vue |    2 +-
 resources/vue/components/StudipArticle.vue    |    2 +-
 resources/vue/components/StudipContentBox.vue |    4 +-
 .../vue/components/StudipWizardDialog.vue     |    4 +-
 resources/vue/components/WikiEditor.vue       |    2 +-
 .../vue/components/WikiEditorOnlineUsers.vue  |    4 +-
 .../admission/AdmissionRuleConfig.vue         |    2 +-
 .../admission/AdmissionRuleTypeSelector.vue   |    2 +-
 .../admission/ConfigureCourseSet.vue          |    2 +-
 .../components/admission/InstantCourseSet.vue |    2 +-
 .../vue/components/admission/ValidityTime.vue |    2 +-
 .../{ => rules}/ConditionalAdmission.vue      |    6 +-
 .../{ => rules}/CourseMemberAdmission.vue     |    6 +-
 .../{ => rules}/LimitedAdmission.vue          |    4 +-
 .../admission/{ => rules}/LockedAdmission.vue |    2 +-
 .../ParticipantRestrictedAdmission.vue        |    6 +-
 .../{ => rules}/PasswordAdmission.vue         |    2 +-
 .../{ => rules}/PreferentialAdmission.vue     |    4 +-
 .../admission/{ => rules}/TermsAdmission.vue  |    2 +-
 .../admission/{ => rules}/TimedAdmission.vue  |    4 +-
 resources/vue/components/avatar/AvatarApp.vue |    4 +-
 .../vue/components/{ => base}/Datepicker.vue  |    2 +-
 .../components/{ => base}/Datetimepicker.vue  |    0
 .../components/{ => base}/EditableList.vue    |    0
 .../components/{ => base}/I18nTextarea.vue    |    2 +-
 .../{ => base}/Multiquicksearch.vue           |    0
 .../vue/components/{ => base}/Multiselect.vue |    0
 .../vue/components/{ => base}/Quicksearch.vue |    0
 .../vue/components/{ => base}/RangeInput.vue  |    0
 .../components/{ => base}/SidebarWidget.vue   |    0
 .../{ => base}/StudipActionMenu.vue           |    2 +-
 .../components/{ => base}/StudipAssetImg.vue  |    0
 .../components/{ => base}/StudipDateTime.vue  |    0
 .../components/{ => base}/StudipDialog.vue    |    0
 .../components/{ => base}/StudipFileSize.vue  |    0
 .../{ => base}/StudipFolderSize.vue           |    0
 .../vue/components/{ => base}/StudipIcon.vue  |    0
 .../{ => base}/StudipMessageBox.vue           |    0
 .../{ => base}/StudipMultiPersonSearch.vue    |    0
 .../{ => base}/StudipProxiedCheckbox.vue      |    0
 .../{ => base}/StudipProxyCheckbox.vue        |    0
 .../components/{ => base}/StudipSelect.vue    |    0
 .../{ => base}/StudipTooltipIcon.vue          |    0
 .../components/{ => base}/StudipWysiwyg.vue   |   12 +-
 .../vue/components/blubber/SearchWidget.vue   |    2 +-
 .../vue/components/blubber/ThreadsWidget.vue  |    2 +-
 .../courseware/CoursewareActivityItem.vue     |    2 +-
 .../courseware/CoursewareAdminTemplates.vue   |    4 +-
 .../courseware/CoursewareContentBookmarks.vue |    2 +-
 .../courseware/CoursewareContentLinks.vue     |    4 +-
 .../courseware/CoursewareContentShared.vue    |    6 +-
 .../CoursewareBiographyAchievementsBlock.vue  |    2 +-
 .../blocks/CoursewareBiographyGoalsBlock.vue  |    2 +-
 .../blocks/CoursewareBlockActions.vue         |    2 +-
 .../blocks/CoursewareBlubberComment.vue       |    2 +-
 .../blocks/CoursewareDefaultBlock.vue         |    4 +-
 .../courseware/blocks/CoursewareTextBlock.vue |    4 +-
 .../containers/CoursewareDefaultContainer.vue |    2 +-
 .../containers/container-components.js        |    2 +-
 .../layouts/CoursewareCallToActionBox.vue     |    2 +-
 .../layouts/CoursewareCollapsibleBox.vue      |    2 +-
 .../structural-element/CoursewareRibbon.vue   |    9 +-
 .../CoursewareRibbonToolbar.vue               |    3 +-
 .../CoursewareSearchResults.vue               |    2 +-
 .../CoursewareStructuralElement.vue           |    7 +-
 .../CoursewareStructuralElementDialogAdd.vue  |    2 +-
 .../CoursewareStructuralElementDialogCopy.vue |    2 +-
 ...wareStructuralElementDialogPermissions.vue |    2 +-
 ...ewareStructuralElementDialogPublicLink.vue |    2 +-
 .../PublicCoursewareStructuralElement.vue     |    3 +-
 .../tasks/CoursewareDashboardStudents.vue     |    5 +-
 .../tasks/CoursewareDashboardTasksList.vue    |    6 +-
 .../tasks/peer-review/AssessmentDialog.vue    |    2 +-
 .../AssessmentTypeEditorDialog.vue            |    2 +-
 .../tasks/peer-review/PairingEditor.vue       |    3 +-
 .../tasks/peer-review/PairingEditorDialog.vue |    2 +-
 .../peer-review/ProcessConfiguration.vue      |    2 +-
 .../tasks/peer-review/ProcessCreateDialog.vue |    4 +-
 .../tasks/peer-review/ProcessCreateForm.vue   |    2 +-
 .../peer-review/ProcessDurationDialog.vue     |    2 +-
 .../tasks/peer-review/ProcessEditDialog.vue   |    4 +-
 .../tasks/peer-review/ProcessStatus.vue       |    2 +-
 .../tasks/peer-review/ProcessesList.vue       |    1 -
 .../tasks/peer-review/ResultDialog.vue        |    2 +-
 .../assessment-types/editors/EditorForm.vue   |    3 +-
 .../assessment-types/editors/EditorTable.vue  |    1 -
 .../toolbar/CoursewareToolbarClipboard.vue    |    2 +-
 .../unit/CoursewareShelfDialogAdd.vue         |    2 +-
 .../unit/CoursewareShelfDialogCopy.vue        |    2 +-
 .../unit/CoursewareShelfDialogTopics.vue      |    2 +-
 .../CoursewareUnitItemDialogPermissions.vue   | 1172 ++++++++---------
 .../unit/CoursewareUnitProgress.vue           |    2 +-
 .../widgets/CoursewareActionWidget.vue        |    2 +-
 .../CoursewareActivitiesWidgetFilterType.vue  |    2 +-
 .../CoursewareActivitiesWidgetFilterUnit.vue  |    2 +-
 ...areCommentsOverviewWidgetFilterCreated.vue |    2 +-
 ...sewareCommentsOverviewWidgetFilterType.vue |    2 +-
 .../widgets/CoursewareExportWidget.vue        |    2 +-
 .../widgets/CoursewareImportWidget.vue        |    2 +-
 .../widgets/CoursewareSearchWidget.vue        |    4 +-
 .../widgets/CoursewareShelfActionWidget.vue   |    2 +-
 .../widgets/CoursewareShelfImportWidget.vue   |    2 +-
 .../widgets/CoursewareTasksActionWidget.vue   |    8 +-
 .../components/feedback/StudipFiveStars.vue   |    2 +-
 .../feedback/StudipFiveStarsInput.vue         |    2 +-
 .../form_inputs/CalendarPermissionsTable.vue  |    2 +-
 .../components/form_inputs/DateListInput.vue  |    2 +-
 .../form_inputs/MyCoursesColouredTable.vue    |    2 +-
 .../form_inputs/QuicksearchListInput.vue      |    2 +-
 .../massmail/MassMailMessagesList.vue         |    4 +-
 .../massmail/MassMailPermissions.vue          |    2 +-
 .../questionnaires/QuestionnaireEditor.vue    |   10 +-
 .../AutomatedDataEdit.vue                     |    2 +-
 .../{ => question-types}/FreetextEdit.vue     |    4 +-
 .../{ => question-types}/LikertEdit.vue       |    8 +-
 .../QuestionnaireInfoEdit.vue                 |    4 +-
 .../{ => question-types}/RangescaleEdit.vue   |    6 +-
 .../{ => question-types}/VoteEdit.vue         |    6 +-
 .../components/responsive/NavigationItem.vue  |    2 +-
 .../responsive/ResponsiveContentBar.vue       |    2 +-
 .../responsive/ResponsiveNavigation.vue       |    2 +-
 .../components/stock-images/ActionsWidget.vue |    2 +-
 .../stock-images/ColorFilterWidget.vue        |    2 +-
 .../stock-images/OrientationFilterWidget.vue  |    2 +-
 .../vue/components/stock-images/Page.vue      |    2 +-
 .../components/stock-images/SearchWidget.vue  |    2 +-
 .../stock-images/SelectableImageCard.vue      |    2 +-
 .../vue/components/tree/AssignLinkWidget.vue  |    4 +-
 .../vue/components/tree/StudipTreeNode.vue    |    6 +-
 .../vue/components/tree/StudipTreeTable.vue   |    2 +-
 .../components/tree/StudipTreeTableRows.vue   |    2 +-
 .../components/tree/StudipTreeViewWidget.vue  |    2 +-
 .../vue/components/tree/TreeBreadcrumb.vue    |    2 +-
 .../vue/components/tree/TreeExportWidget.vue  |    4 +-
 .../components/tree/TreeNodeCoursePath.vue    |    2 +-
 .../vue/components/tree/TreeSearchResult.vue  |    4 +-
 resources/vue/store/StudipStore.js            |   12 +-
 templates/helpbar/helpbar.php                 |    2 +-
 templates/layouts/base.php                    |   43 +-
 vite.config.js                                |   87 ++
 180 files changed, 1289 insertions(+), 1296 deletions(-)
 rename resources/assets/javascripts/{entry-base.js => entry-bootstrap.js} (91%)
 create mode 100644 resources/assets/javascripts/entry-legacy-libs.js
 create mode 100644 resources/assets/javascripts/entry-lib.js
 delete mode 100644 resources/assets/javascripts/entry-wysiwyg.js
 delete mode 100644 resources/assets/javascripts/init.js
 delete mode 100644 resources/assets/javascripts/jquery-bundle.js
 create mode 100644 resources/assets/javascripts/lib/studip.js
 delete mode 100644 resources/assets/javascripts/public-path.js
 rename resources/vue/components/admission/{ => rules}/ConditionalAdmission.vue (97%)
 rename resources/vue/components/admission/{ => rules}/CourseMemberAdmission.vue (95%)
 rename resources/vue/components/admission/{ => rules}/LimitedAdmission.vue (93%)
 rename resources/vue/components/admission/{ => rules}/LockedAdmission.vue (92%)
 rename resources/vue/components/admission/{ => rules}/ParticipantRestrictedAdmission.vue (93%)
 rename resources/vue/components/admission/{ => rules}/PasswordAdmission.vue (97%)
 rename resources/vue/components/admission/{ => rules}/PreferentialAdmission.vue (96%)
 rename resources/vue/components/admission/{ => rules}/TermsAdmission.vue (95%)
 rename resources/vue/components/admission/{ => rules}/TimedAdmission.vue (94%)
 rename resources/vue/components/{ => base}/Datepicker.vue (98%)
 rename resources/vue/components/{ => base}/Datetimepicker.vue (100%)
 rename resources/vue/components/{ => base}/EditableList.vue (100%)
 rename resources/vue/components/{ => base}/I18nTextarea.vue (99%)
 rename resources/vue/components/{ => base}/Multiquicksearch.vue (100%)
 rename resources/vue/components/{ => base}/Multiselect.vue (100%)
 rename resources/vue/components/{ => base}/Quicksearch.vue (100%)
 rename resources/vue/components/{ => base}/RangeInput.vue (100%)
 rename resources/vue/components/{ => base}/SidebarWidget.vue (100%)
 rename resources/vue/components/{ => base}/StudipActionMenu.vue (98%)
 rename resources/vue/components/{ => base}/StudipAssetImg.vue (100%)
 rename resources/vue/components/{ => base}/StudipDateTime.vue (100%)
 rename resources/vue/components/{ => base}/StudipDialog.vue (100%)
 rename resources/vue/components/{ => base}/StudipFileSize.vue (100%)
 rename resources/vue/components/{ => base}/StudipFolderSize.vue (100%)
 rename resources/vue/components/{ => base}/StudipIcon.vue (100%)
 rename resources/vue/components/{ => base}/StudipMessageBox.vue (100%)
 rename resources/vue/components/{ => base}/StudipMultiPersonSearch.vue (100%)
 rename resources/vue/components/{ => base}/StudipProxiedCheckbox.vue (100%)
 rename resources/vue/components/{ => base}/StudipProxyCheckbox.vue (100%)
 rename resources/vue/components/{ => base}/StudipSelect.vue (100%)
 rename resources/vue/components/{ => base}/StudipTooltipIcon.vue (100%)
 rename resources/vue/components/{ => base}/StudipWysiwyg.vue (83%)
 rename resources/vue/components/questionnaires/{ => question-types}/AutomatedDataEdit.vue (96%)
 rename resources/vue/components/questionnaires/{ => question-types}/FreetextEdit.vue (81%)
 rename resources/vue/components/questionnaires/{ => question-types}/LikertEdit.vue (89%)
 rename resources/vue/components/questionnaires/{ => question-types}/QuestionnaireInfoEdit.vue (88%)
 rename resources/vue/components/questionnaires/{ => question-types}/RangescaleEdit.vue (93%)
 rename resources/vue/components/questionnaires/{ => question-types}/VoteEdit.vue (86%)
 create mode 100644 vite.config.js

diff --git a/app/controllers/course/files.php b/app/controllers/course/files.php
index e40ac45631a..56de8aeb177 100644
--- a/app/controllers/course/files.php
+++ b/app/controllers/course/files.php
@@ -37,7 +37,7 @@ class Course_FilesController extends AuthenticatedController
         if (is_object($GLOBALS['user']) && $GLOBALS['user']->id !== 'nobody') {
             $constraints = FileManager::getUploadTypeConfig($this->course->id);
 
-            PageLayout::addHeadElement('script', ['type' => 'text/javascript'], sprintf(
+            PageLayout::addHeadElement('script', ['type' => 'module'], sprintf(
                 'STUDIP.Files.setUploadConstraints(%s);',
                 json_encode($constraints)
             ));
diff --git a/app/controllers/files.php b/app/controllers/files.php
index b8ccf175909..ed052155822 100644
--- a/app/controllers/files.php
+++ b/app/controllers/files.php
@@ -48,7 +48,7 @@ class FilesController extends AuthenticatedController
 
         $constraints = FileManager::getUploadTypeConfig($this->user->id);
 
-        PageLayout::addHeadElement('script', ['type' => 'text/javascript'], sprintf(
+        PageLayout::addHeadElement('script', ['type' => 'module'], sprintf(
             'STUDIP.Files.setUploadConstraints(%s);',
             json_encode($constraints)
         ));
diff --git a/app/controllers/institute/files.php b/app/controllers/institute/files.php
index ca46dc89739..4ed8bc6f92d 100644
--- a/app/controllers/institute/files.php
+++ b/app/controllers/institute/files.php
@@ -39,7 +39,7 @@ class Institute_FilesController extends AuthenticatedController
         if (is_object($GLOBALS['user']) && $GLOBALS['user']->id !== 'nobody') {
             $constraints = FileManager::getUploadTypeConfig($this->institute->id);
 
-            PageLayout::addHeadElement('script', ['type' => 'text/javascript'], sprintf(
+            PageLayout::addHeadElement('script', ['type' => 'module'], sprintf(
                 'STUDIP.Files.setUploadConstraints(%s);',
                 json_encode($constraints)
             ));
diff --git a/app/views/messages/write.php b/app/views/messages/write.php
index 8bf05f61190..5492dbd69db 100644
--- a/app/views/messages/write.php
+++ b/app/views/messages/write.php
@@ -69,7 +69,7 @@
         ?>
         </div>
         <script>
-            STUDIP.MultiPersonSearch.init();
+            STUDIP.ready(() => STUDIP.MultiPersonSearch.init());
         </script>
     </div>
     <div>
diff --git a/lib/classes/PageLayout.php b/lib/classes/PageLayout.php
index ed5e1aefdff..c4f73ce61af 100644
--- a/lib/classes/PageLayout.php
+++ b/lib/classes/PageLayout.php
@@ -134,9 +134,22 @@ class PageLayout
             'title' => _('Hilfe zur Textformatierung')
         ]);
 
-        self::addStylesheet('studip-base.css?v=' . $v, ['media' => 'screen']);
-        self::addScript('studip-base.js?v=' . $v);
-        self::addScript('studip-wysiwyg.js?v=' . $v);
+        self::addStylesheet('studip-bootstrap.css?v=' . $v, ['media' => 'screen']);
+
+        self::addScript('jquery.min.js?v=' . $v);
+        self::addScript('jquery-ui.min.js?v=' . $v);
+        self::addScript('select2.full.min.js?v=' . $v);
+        self::addScript('jquery.tablesorter.combined.min.js?v=' . $v);
+        self::addScript('jquery-ui-timepicker-addon.min.js?v=' . $v);
+        self::addScript('jquery.scrollTo.min.js?v=' . $v);
+        self::addScript('jquery.qrcode.min.js?v=' . $v);
+        self::addScript('jquery.ui.touch-punch.min.js?v=' . $v);
+        self::addScript('lodash.min.js?v=' . $v);
+        self::addStylesheet('jquery-ui-timepicker-addon.min.css?v=' . $v);
+
+        self::addScript('studip-legacy-libs.js?v=' . $v, ['type' => 'module']);
+        self::addScript('studip-lib.js?v=' . $v, ['type' => 'module']);
+        self::addScript('studip-bootstrap.js?v=' . $v, ['type' => 'module']);
 
         self::addStylesheet('print.css?v=' . $v, ['media' => 'print']);
 
diff --git a/package.json b/package.json
index e52d0117411..5e400ad7a2f 100644
--- a/package.json
+++ b/package.json
@@ -72,6 +72,7 @@
         "@types/jquery": "^3.5.16",
         "@types/jqueryui": "^1.12.16",
         "@types/lodash": "^4.14.191",
+        "@vitejs/plugin-vue": "^5.2.1",
         "@vue/compiler-sfc": "^3.5.13",
         "@vue/eslint-config-typescript": "^13.0.0",
         "altcha": "^0.3.2",
@@ -140,6 +141,8 @@
         "ts-loader": "^9.5.1",
         "typescript": "^5.7.2",
         "typescript-eslint": "^8.17.0",
+        "vite": "^6.2.1",
+        "vite-plugin-static-copy": "^2.3.0",
         "vue": "^3.5.13",
         "vue-dragscroll": "^4.0.6",
         "vue-loader": "^17.4.2",
diff --git a/resources/assets/javascripts/bootstrap/admission.js b/resources/assets/javascripts/bootstrap/admission.js
index 2fff6cfd22e..ba017db792a 100644
--- a/resources/assets/javascripts/bootstrap/admission.js
+++ b/resources/assets/javascripts/bootstrap/admission.js
@@ -16,7 +16,7 @@ STUDIP.ready(function () {
 
         if (STUDIP.Admission.availableRules[ruleType] !== undefined) {
 
-            import('@/vue/components/admission/' + STUDIP.Admission.availableRules[ruleType])
+            import(`@/vue/components/admission/${STUDIP.Admission.availableRules[ruleType]}.vue`)
                 .then(result => {
                     const components = {};
                     components[ruleType] = result.default;
diff --git a/resources/assets/javascripts/bootstrap/vue.js b/resources/assets/javascripts/bootstrap/vue.js
index 74c59657807..b91570949d5 100644
--- a/resources/assets/javascripts/bootstrap/vue.js
+++ b/resources/assets/javascripts/bootstrap/vue.js
@@ -1,11 +1,10 @@
 import { defineAsyncComponent } from 'vue';
-import { isFunction } from "lodash";
 
 function attachComponents(app, configuredComponents) {
     configuredComponents.forEach(component => {
         const name = component.split('/').reverse()[0];
         app.component(name, defineAsyncComponent(() => {
-            const temp = import(`../../../vue/components/${component}.vue`);
+            const temp = importComponent(component);
             temp.then(({default: c}) => {
                 const mounted = c.mounted ?? null;
                 c.mounted = function (...args) {
@@ -29,6 +28,15 @@ function attachComponents(app, configuredComponents) {
     });
 }
 
+function importComponent(component) {
+    if (!component.includes("/")) {
+        return import(`../../../vue/components/${component}.vue`);
+    }
+
+    const [directory, file] = component.split("/");
+    return import(`../../../vue/components/${directory}/${file}.vue`);
+}
+
 STUDIP.ready(() => {
     document.querySelectorAll('[data-vue-app]:not([data-vue-app-created])').forEach(async (node) => {
         node.dataset.vueAppCreated = 'true';
@@ -50,7 +58,7 @@ STUDIP.ready(() => {
         // Set up vuex stores
         for (const [index, name] of Object.entries(config.vuexStores)) {
             promises.push(
-                import(`../../../vue/store/${name}`).then(storeConfig => {
+                import(`../../../vue/store/${name}.js`).then(storeConfig => {
                     if (!store.hasModule(index)) {
                         store.registerModule(index, storeConfig.default);
                     }
@@ -71,14 +79,14 @@ STUDIP.ready(() => {
         // Set up pinia stores
         for (const [name, command] of Object.entries(config.stores)) {
             promises.push(
-                import(`../../../vue/store/pinia/${name}`).then((storeConfig) => {
+                import(`../../../vue/store/pinia/${name}.js`).then((storeConfig) => {
                     const piniaStore = storeConfig[command]();
 
                     const dataElement = document.getElementById(`vue-store-data-${name}`);
                     if (dataElement) {
                         const data = JSON.parse(dataElement.innerText);
                         Object.keys(data).forEach(command => {
-                            if (isFunction(piniaStore[command])) {
+                            if (_.isFunction(piniaStore[command])) {
                                 piniaStore[command](data[command]);
                             } else {
                                 piniaStore[command] = data[command];
diff --git a/resources/assets/javascripts/chunk-loader.js b/resources/assets/javascripts/chunk-loader.js
index 8cb692cf87e..e5fc4d575ac 100644
--- a/resources/assets/javascripts/chunk-loader.js
+++ b/resources/assets/javascripts/chunk-loader.js
@@ -24,41 +24,26 @@ export const loadChunk = function (chunk, { silent = false } = {}) {
         case 'courseware':
             promise = Promise.all([
                 STUDIP.loadChunk('vue'),
-                import(
-                    /* webpackChunkName: "courseware" */
-                    './chunks/courseware'
-                ),
+                import('./chunks/courseware'),
             ]).then(([Vue]) => Vue);
             break;
 
         case 'code-highlight':
-            promise = import(
-                /* webpackChunkName: "code-highlight" */
-                './chunks/code-highlight'
-            ).then(({default: hljs}) => {
+            promise = import('./chunks/code-highlight').then(({default: hljs}) => {
                 return hljs;
             });
             break;
 
         case 'chartist':
-            promise = import(
-                /* webpackChunkName: "chartist" */
-                './chunks/chartist'
-            ).then(({ default: Chartist }) => Chartist);
+            promise = import('./chunks/chartist').then(({ default: Chartist }) => Chartist);
             break;
 
         case 'fullcalendar':
-            promise = import(
-                /* webpackChunkName: "fullcalendar" */
-                './chunks/fullcalendar'
-            );
+            promise = import('./chunks/fullcalendar');
             break;
 
         case 'tablesorter':
-            promise = import(
-                /* webpackChunkName: "tablesorter" */
-                './chunks/tablesorter'
-            );
+            promise = import('./chunks/tablesorter');
             break;
 
         case 'mathjax':
@@ -81,17 +66,11 @@ export const loadChunk = function (chunk, { silent = false } = {}) {
             break;
 
         case 'vue':
-            promise = import(
-                /* webpackChunkName: "vue.js" */
-                './chunks/vue'
-            );
+            promise = import('./chunks/vue');
             break;
 
         case 'wysiwyg':
-            promise = import(
-                /* webpackChunkName: "wysiwyg.js" */
-                './chunks/wysiwyg'
-            );
+            promise = import('./chunks/wysiwyg');
             break;
 
         default:
diff --git a/resources/assets/javascripts/chunks/tablesorter.js b/resources/assets/javascripts/chunks/tablesorter.js
index ed77c026b76..c58c7088928 100644
--- a/resources/assets/javascripts/chunks/tablesorter.js
+++ b/resources/assets/javascripts/chunks/tablesorter.js
@@ -1,9 +1,5 @@
 import { $gettext } from '../lib/gettext'
 
-import "tablesorter/dist/js/jquery.tablesorter"
-import "tablesorter/dist/js/extras/jquery.tablesorter.pager.min.js"
-import "tablesorter/dist/js/jquery.tablesorter.widgets.js"
-
 jQuery.tablesorter.addParser({
     id: 'htmldata',
     is(s, table, cell) {
diff --git a/resources/assets/javascripts/chunks/vue.js b/resources/assets/javascripts/chunks/vue.js
index eafade2ab7b..d7620c6da25 100644
--- a/resources/assets/javascripts/chunks/vue.js
+++ b/resources/assets/javascripts/chunks/vue.js
@@ -70,6 +70,9 @@ function createApp(options = {}, ...args) {
     registerGlobalComponents(app);
     registerGlobalDirectives(app);
 
+    // Make the current state of "focus mode" (fullscreen) available to Vue components.
+    eventBus.on('switch-focus-mode', (mode) => store.commit('studip/switchConsumeMode', mode));
+
     if (options.el) {
         app.mount(options.el);
     }
diff --git a/resources/assets/javascripts/entry-base.js b/resources/assets/javascripts/entry-bootstrap.js
similarity index 91%
rename from resources/assets/javascripts/entry-base.js
rename to resources/assets/javascripts/entry-bootstrap.js
index db3d1f621ae..714a9a9edb7 100644
--- a/resources/assets/javascripts/entry-base.js
+++ b/resources/assets/javascripts/entry-bootstrap.js
@@ -1,18 +1,6 @@
-import './public-path.js'
-
-// promise polyfill needed for IE11 to load tablesorter
-import 'es6-promise/auto'
-
 import "../stylesheets/studip-jquery-ui.scss"
-// Basic scss support
 import "../stylesheets/studip.scss"
 
-import lodash from "lodash"
-window._ = lodash
-
-import "./jquery-bundle.js"
-
-import "./init.js"
 import "./bootstrap/responsive.js"
 import "./bootstrap/vue.js"
 
@@ -79,6 +67,7 @@ import "./bootstrap/courseware.js"
 import "./bootstrap/external_pages.js"
 import "./bootstrap/vips.js"
 import "./bootstrap/admission.js"
+import "./bootstrap/wysiwyg.js"
 
 import "./mvv_course_wizard.js"
 import "./mvv.js"
diff --git a/resources/assets/javascripts/entry-legacy-libs.js b/resources/assets/javascripts/entry-legacy-libs.js
new file mode 100644
index 00000000000..3994fc20967
--- /dev/null
+++ b/resources/assets/javascripts/entry-legacy-libs.js
@@ -0,0 +1,71 @@
+import 'multiselect';
+
+import './studip-jquery-tweaks.js';
+import './studip-jquery.multi-select.tweaks.js';
+import './studip-jquery-selection-helper.js';
+
+import 'blueimp-file-upload';
+import 'blueimp-file-upload/js/jquery.iframe-transport.js';
+
+import './jquery/autoresize.jquery.min.js';
+
+// Create jQuery "plugin" that just reverses the elements' order. This is
+// neccessary since the navigation is built and afterwards, we need to
+// check the navigation's open status in reverse order (from bottom to top)
+jQuery.fn.reverse = [].reverse;
+
+$.fn.extend({
+    showAjaxNotification(position) {
+        position = position || 'left';
+        return this.each(function () {
+            if ($(this).data('ajax_notification')) {
+                return;
+            }
+
+            $(this).wrap('<span class="ajax_notification" />');
+            const thisHeight = $(this).height();
+            const thisPosition = $(this).position();
+            const notification = $('<span class="notification" />').hide().insertBefore(this);
+            const changes = {
+                marginLeft: 0,
+                marginRight: 0,
+            };
+
+            changes[position === 'right' ? 'marginRight' : 'marginLeft'] = notification.outerWidth(true);
+
+            $(this)
+                .data({
+                    ajax_notification: notification,
+                })
+                .parent()
+                .animate(changes, 'fast', function () {
+                    const offset = thisPosition;
+                    const styles = {
+                        left: offset.left - notification.outerWidth(true),
+                        top: offset.top + Math.max(0, Math.floor((thisHeight - notification.outerHeight(true)) / 2)),
+                    };
+                    if (position === 'right') {
+                        styles.left += $(this).outerWidth(true);
+                    }
+                    notification.css(styles).fadeIn('fast');
+                });
+        });
+    },
+    hideAjaxNotification() {
+        return this.each(function () {
+            var $this = $(this).stop(),
+                notification = $this.data('ajax_notification');
+            if (!notification) {
+                return;
+            }
+
+            notification.stop().fadeOut('fast', function () {
+                $this.animate({ marginLeft: 0, marginRight: 0 }, 'fast', function () {
+                    $this.unwrap();
+                });
+                $(this).remove();
+            });
+            $(this).removeData('ajax_notification');
+        });
+    },
+});
diff --git a/resources/assets/javascripts/entry-lib.js b/resources/assets/javascripts/entry-lib.js
new file mode 100644
index 00000000000..d9d9e760e68
--- /dev/null
+++ b/resources/assets/javascripts/entry-lib.js
@@ -0,0 +1,3 @@
+import { STUDIP } from './lib/studip.js';
+
+Object.assign(window.STUDIP, STUDIP);
diff --git a/resources/assets/javascripts/entry-wysiwyg.js b/resources/assets/javascripts/entry-wysiwyg.js
deleted file mode 100644
index 4255f4d6a62..00000000000
--- a/resources/assets/javascripts/entry-wysiwyg.js
+++ /dev/null
@@ -1 +0,0 @@
-import "./bootstrap/wysiwyg.js"
diff --git a/resources/assets/javascripts/init.js b/resources/assets/javascripts/init.js
deleted file mode 100644
index 4af6ed9659d..00000000000
--- a/resources/assets/javascripts/init.js
+++ /dev/null
@@ -1,171 +0,0 @@
-import { loadChunk, loadScript, } from './chunk-loader.js';
-import Vue from './lib/studip-vue.js';
-
-import ActionMenu from './lib/actionmenu.js';
-import ActivityFeed from './lib/activityfeed.js';
-import admin_sem_class from './lib/admin_sem_class.js';
-import AdminCourses from './lib/admin-courses.js';
-import Admission from './lib/admission.js';
-import Arbeitsgruppen from './lib/arbeitsgruppen.js';
-import Archive from './lib/archive.js';
-import Avatar from './lib/avatar.js';
-import BigImageHandler from './lib/big_image_handler.js';
-import Blubber from './lib/blubber.js';
-import Browse from './lib/browse.js';
-import Cache from './lib/cache.js';
-import Calendar from './lib/calendar.js';
-import Clipboard from './lib/clipboard.js';
-import Cookie from './lib/cookie.js';
-import CourseWizard from './lib/course_wizard.js';
-import { createURLHelper } from './lib/url_helper';
-import CSS from './lib/css.js';
-import Dates from './lib/dates.js';
-import DateTime from './lib/datetime.js';
-import Dialog from './lib/dialog.js';
-import DragAndDropUpload from './lib/drag_and_drop_upload.js';
-import enrollment from './lib/enrollment.js';
-import eventBus from './lib/event-bus.ts';
-import extractCallback from './lib/extract_callback.js';
-import Files from './lib/files.js';
-import FilesDashboard from './lib/files_dashboard.js';
-import Folders from './lib/folders.js';
-import Forms from './lib/forms.js';
-import Forum from './lib/forum.js';
-import Fullcalendar from './lib/fullcalendar.js';
-import Fullscreen from './lib/fullscreen.js';
-import GlobalSearch from './lib/global_search.js';
-import HeaderMagic from './lib/header_magic.js';
-import i18n from './lib/i18n.js';
-import InlineEditing from './lib/inline-editing.js';
-import JSONAPI, { jsonapi } from './lib/jsonapi.ts';
-import JSUpdater from './lib/jsupdater.js';
-import Lightbox from './lib/lightbox.js';
-import Markup from './lib/markup.js';
-import Members from './lib/members.js';
-import Messages from './lib/messages.js';
-import MultiPersonSearch from './lib/multi_person_search.js';
-import MultiSelect from './lib/multi_select.js';
-import NavigationShrinker from './lib/navigation_shrinker.js';
-import OER from './lib/oer.js';
-import OldUpload from './lib/old_upload.js';
-import Overlapping from './lib/overlapping.js';
-import Overlay from './lib/overlay.js';
-import PageLayout from './lib/page_layout.js';
-import parseOptions from './lib/parse_options.js';
-import PersonalNotifications from './lib/personal_notifications.js';
-import QRCode from './lib/qr_code.js';
-import Questionnaire from './lib/questionnaire.js';
-import QuickSearch from './lib/quick_search.js';
-import QuickSelection from './lib/quick_selection.js';
-import Raumzeit from './lib/raumzeit.js';
-import {ready, domReady, dialogReady} from './lib/ready.js';
-import Report from './lib/report.ts';
-import Resources from './lib/resources.js';
-import Responsive from './lib/responsive.js';
-import Screenreader from './lib/screenreader.js';
-import Scroll from './lib/scroll.js';
-import Search from './lib/search.js';
-import Sidebar from './lib/sidebar.js';
-import SkipLinks from './lib/skip_links.js';
-import startpage from './lib/startpage.js';
-import Statusgroups from './lib/statusgroups.js';
-import study_area_selection from './lib/study_area_selection.js';
-import Table from './lib/table.js';
-import TableOfContents from './lib/table-of-contents.js';
-import Tooltip from './lib/tooltip.js';
-import Tour from './lib/tour.js';
-import * as Gettext from './lib/gettext';
-import UserFilter from './lib/user_filter.js';
-import wysiwyg from './lib/wysiwyg.js';
-import ScrollToTop from './lib/scroll_to_top.js';
-import * as Vips from './lib/vips.js';
-
-const configURLHelper = _.get(window, 'STUDIP.URLHelper', {});
-const URLHelper = createURLHelper(configURLHelper);
-
-window.STUDIP = _.assign(window.STUDIP || {}, {
-    ActionMenu,
-    ActivityFeed,
-    admin_sem_class,
-    AdminCourses,
-    Admission,
-    Arbeitsgruppen,
-    Archive,
-    Avatar,
-    BigImageHandler,
-    Blubber,
-    Browse,
-    Cache,
-    Calendar,
-    Cookie,
-    CourseWizard,
-    CSS,
-    Dates,
-    DateTime,
-    Dialog,
-    DragAndDropUpload,
-    enrollment,
-    eventBus,
-    extractCallback,
-    Files,
-    FilesDashboard,
-    Folders,
-    Forms,
-    Forum,
-    Fullcalendar,
-    Fullscreen,
-    Gettext,
-    GlobalSearch,
-    HeaderMagic,
-    i18n,
-    InlineEditing,
-    jsonapi,
-    JSONAPI,
-    JSUpdater,
-    Lightbox,
-    loadChunk,
-    loadScript,
-    Markup,
-    Members,
-    Messages,
-    MultiPersonSearch,
-    MultiSelect,
-    NavigationShrinker,
-    OER,
-    OldUpload,
-    Overlapping,
-    Overlay,
-    PageLayout,
-    parseOptions,
-    PersonalNotifications,
-    QRCode,
-    Questionnaire,
-    QuickSearch,
-    QuickSelection,
-    Raumzeit,
-    Report,
-    Responsive,
-    Scroll,
-    Screenreader,
-    Search,
-    Sidebar,
-    SkipLinks,
-    startpage,
-    Statusgroups,
-    study_area_selection,
-    Table,
-    TableOfContents,
-    Tooltip,
-    Tour,
-    URLHelper,
-    UserFilter,
-    wysiwyg,
-    Resources,
-    Clipboard,
-    ready,
-    domReady,
-    dialogReady,
-    ScrollToTop,
-    Vips,
-    Vue,
-});
diff --git a/resources/assets/javascripts/jquery-bundle.js b/resources/assets/javascripts/jquery-bundle.js
deleted file mode 100644
index f96bd24e445..00000000000
--- a/resources/assets/javascripts/jquery-bundle.js
+++ /dev/null
@@ -1,150 +0,0 @@
-import $ from 'expose-loader?exposes=$,jQuery!jquery';
-
- import { setLocale } from './lib/gettext';
-
-import 'jquery-ui/ui/widget.js';
-import 'jquery-ui/ui/position.js';
-import 'jquery-ui/ui/data.js';
-import 'jquery-ui/ui/disable-selection.js';
-import 'jquery-ui/ui/focusable.js';
-import 'jquery-ui/ui/form.js';
-import 'jquery-ui/ui/form-reset-mixin.js';
-import 'jquery-ui/ui/ie.js';
-import 'jquery-ui/ui/keycode.js';
-import 'jquery-ui/ui/labels.js';
-import 'jquery-ui/ui/plugin.js';
-import 'jquery-ui/ui/safe-active-element.js';
-import 'jquery-ui/ui/safe-blur.js';
-import 'jquery-ui/ui/scroll-parent.js';
-import 'jquery-ui/ui/tabbable.js';
-import 'jquery-ui/ui/unique-id.js';
-import 'jquery-ui/ui/version.js';
-import 'jquery-ui/ui/widgets/draggable.js';
-import 'jquery-ui/ui/widgets/droppable.js';
-import 'jquery-ui/ui/widgets/resizable.js';
-import 'jquery-ui/ui/widgets/selectable.js';
-import 'jquery-ui/ui/widgets/sortable.js';
-import 'jquery-ui/ui/widgets/accordion.js';
-import 'jquery-ui/ui/widgets/autocomplete.js';
-import 'jquery-ui/ui/widgets/button.js';
-import 'jquery-ui/ui/widgets/checkboxradio.js';
-import 'jquery-ui/ui/widgets/controlgroup.js';
-import 'jquery-ui/ui/widgets/datepicker.js';
-import 'jquery-ui/ui/widgets/dialog.js';
-import 'jquery-ui/ui/widgets/menu.js';
-import 'jquery-ui/ui/widgets/mouse.js';
-import 'jquery-ui/ui/widgets/progressbar.js';
-import 'jquery-ui/ui/widgets/selectmenu.js';
-import 'jquery-ui/ui/widgets/slider.js';
-import 'jquery-ui/ui/widgets/spinner.js';
-import 'jquery-ui/ui/widgets/tabs.js';
-import 'jquery-ui/ui/widgets/tooltip.js';
-import 'jquery-ui/ui/effect.js';
-import 'jquery-ui/ui/effects/effect-blind.js';
-import 'jquery-ui/ui/effects/effect-bounce.js';
-import 'jquery-ui/ui/effects/effect-clip.js';
-import 'jquery-ui/ui/effects/effect-drop.js';
-import 'jquery-ui/ui/effects/effect-explode.js';
-import 'jquery-ui/ui/effects/effect-fade.js';
-import 'jquery-ui/ui/effects/effect-fold.js';
-import 'jquery-ui/ui/effects/effect-highlight.js';
-import 'jquery-ui/ui/effects/effect-puff.js';
-import 'jquery-ui/ui/effects/effect-pulsate.js';
-import 'jquery-ui/ui/effects/effect-scale.js';
-import 'jquery-ui/ui/effects/effect-shake.js';
-import 'jquery-ui/ui/effects/effect-size.js';
-import 'jquery-ui/ui/effects/effect-slide.js';
-import 'jquery-ui/ui/effects/effect-transfer.js';
-
-import 'jquery-ui-timepicker-addon';
-
-import 'multiselect';
-
-import 'jquery.scrollto';
-import 'jquery.qrcode';
-
-import 'jquery-ui-touch-punch';
-
-import './studip-jquery-tweaks.js';
-import './studip-jquery.multi-select.tweaks.js';
-import './studip-jquery-selection-helper.js';
-
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-import select2 from 'select2/dist/js/select2.full.js';
-
-import 'blueimp-file-upload';
-import 'blueimp-file-upload/js/jquery.iframe-transport.js';
-
-import './jquery/autoresize.jquery.min.js';
-
-// Create jQuery "plugin" that just reverses the elements' order. This is
-// neccessary since the navigation is built and afterwards, we need to
-// check the navigation's open status in reverse order (from bottom to top)
-jQuery.fn.reverse = [].reverse;
-
-$.fn.extend({
-    showAjaxNotification(position) {
-        position = position || 'left';
-        return this.each(function () {
-            if ($(this).data('ajax_notification')) {
-                return;
-            }
-
-            $(this).wrap('<span class="ajax_notification" />');
-            const thisHeight = $(this).height();
-            const thisPosition = $(this).position();
-            const notification = $('<span class="notification" />')
-                .hide()
-                .insertBefore(this);
-            const changes = {
-                marginLeft: 0,
-                marginRight: 0
-            };
-
-            changes[position === 'right' ? 'marginRight' : 'marginLeft'] = notification.outerWidth(true);
-
-            $(this)
-                .data({
-                    ajax_notification: notification
-                })
-                .parent()
-                .animate(changes, 'fast', function () {
-                    const offset = thisPosition;
-                    const styles = {
-                        left: offset.left - notification.outerWidth(true),
-                        top:
-                            offset.top +
-                            Math.max(0, Math.floor((thisHeight - notification.outerHeight(true)) / 2))
-                    };
-                    if (position === 'right') {
-                        styles.left += $(this).outerWidth(true);
-                    }
-                    notification.css(styles).fadeIn('fast');
-                });
-        });
-    },
-    hideAjaxNotification() {
-        return this.each(function () {
-            var $this = $(this).stop(),
-                notification = $this.data('ajax_notification');
-            if (!notification) {
-                return;
-            }
-
-            notification.stop().fadeOut('fast', function () {
-                $this.animate({marginLeft: 0, marginRight: 0}, 'fast', function () {
-                    $this.unwrap();
-                });
-                $(this).remove();
-            });
-            $(this).removeData('ajax_notification');
-        });
-    }
-});
-
-$(document).ready(async () => {
-    await setLocale();
-    STUDIP.ready.trigger('dom');
-}).on('dialog-update', (event, data) => {
-    STUDIP.ready.trigger('dialog', data.dialog);
-});
diff --git a/resources/assets/javascripts/lib/admission.js b/resources/assets/javascripts/lib/admission.js
index c735cb2b79a..01499c76d74 100644
--- a/resources/assets/javascripts/lib/admission.js
+++ b/resources/assets/javascripts/lib/admission.js
@@ -9,15 +9,15 @@ const Admission = {
      * All registered rule types with their corresponding Vue components
      */
     availableRules: {
-        ConditionalAdmission: 'ConditionalAdmission.vue',
-        CourseMemberAdmission: 'CourseMemberAdmission.vue',
-        LimitedAdmission: 'LimitedAdmission.vue',
-        LockedAdmission: 'LockedAdmission.vue',
-        ParticipantRestrictedAdmission: 'ParticipantRestrictedAdmission.vue',
-        PasswordAdmission: 'PasswordAdmission.vue',
-        PreferentialAdmission: 'PreferentialAdmission.vue',
-        TermsAdmission: 'TermsAdmission.vue',
-        TimedAdmission: 'TimedAdmission.vue'
+        ConditionalAdmission: 'ConditionalAdmission',
+        CourseMemberAdmission: 'CourseMemberAdmission',
+        LimitedAdmission: 'LimitedAdmission',
+        LockedAdmission: 'LockedAdmission',
+        ParticipantRestrictedAdmission: 'ParticipantRestrictedAdmission',
+        PasswordAdmission: 'PasswordAdmission',
+        PreferentialAdmission: 'PreferentialAdmission',
+        TermsAdmission: 'TermsAdmission',
+        TimedAdmission: 'TimedAdmission'
     },
 
     getCourses: function(targetUrl) {
diff --git a/resources/assets/javascripts/lib/fullcalendar.js b/resources/assets/javascripts/lib/fullcalendar.js
index f4e2826aed9..e470abe356a 100644
--- a/resources/assets/javascripts/lib/fullcalendar.js
+++ b/resources/assets/javascripts/lib/fullcalendar.js
@@ -13,8 +13,6 @@ import resourceCommonPlugin from '@fullcalendar/resource-common';
 import resourceTimeGridPlugin from '@fullcalendar/resource-timegrid';
 import resourceTimelinePlugin from '@fullcalendar/resource-timeline';
 
-import { jsPDF } from 'jspdf';
-import html2canvas from 'html2canvas';
 import Responsive from "./responsive";
 
 Date.prototype.getWeekNumber = function () {
@@ -178,30 +176,35 @@ class Fullcalendar
 
     static downloadPDF(format = 'landscape', withWeekend = false)
     {
-        $('*[data-fullcalendar="1"]').each(function () {
+        $('*[data-fullcalendar="1"]').each(async function () {
             if (this.calendar != undefined) {
                 $(this).addClass('print-view').toggleClass('without-weekend', !withWeekend);
 
-                var title = $(this).data('title');
-                let print_title = $('<h1>').text(title).prependTo(this);
+                const title = $(this).data('title');
+                const print_title = $('<h1>').text(title).prependTo(this);
 
                 window.scrollTo(0, 0);
 
-                html2canvas(this).then(canvas => {
-                    var imgData = canvas.toDataURL('image/jpeg');
-                    var pdf = new jsPDF({
-                        orientation: format === 'landscape' ? 'landscape' : 'portrait'
-                    });
-                    if (format === 'landscape') {
-                        pdf.addImage(imgData, 'JPEG', 20, 20, 250, 250, 'i1', 'NONE', 0);
-                    } else {
-                        pdf.addImage(imgData, 'JPEG', 25, 20, 160, 190, 'i1', 'NONE', 0);
-                    }
-                    pdf.save(title + '.pdf');
-                });
-
                 print_title.remove();
                 $(this).removeClass('print-view without-weekend');
+
+                const [html2canvas, { jsPDF }] = await Promise.all([
+                    import('html2canvas'),
+                    import('jspdf')
+                ]);
+
+                const canvas = await html2canvas(this);
+
+                var imgData = canvas.toDataURL('image/jpeg');
+                var pdf = new jsPDF({
+                    orientation: format === 'landscape' ? 'landscape' : 'portrait'
+                });
+                if (format === 'landscape') {
+                    pdf.addImage(imgData, 'JPEG', 20, 20, 250, 250, 'i1', 'NONE', 0);
+                } else {
+                    pdf.addImage(imgData, 'JPEG', 25, 20, 160, 190, 'i1', 'NONE', 0);
+                }
+                pdf.save(title + '.pdf');
             }
         });
     }
diff --git a/resources/assets/javascripts/lib/gettext.ts b/resources/assets/javascripts/lib/gettext.ts
index 3ed710ad864..12f037f2965 100644
--- a/resources/assets/javascripts/lib/gettext.ts
+++ b/resources/assets/javascripts/lib/gettext.ts
@@ -1,5 +1,4 @@
 import {createGettext, LanguageData} from 'vue3-gettext';
-import * as defaultTranslations from '../../../../locale/de/LC_MESSAGES/js-resources.json';
 import eventBus from './event-bus';
 
 interface StringDict {
@@ -92,7 +91,7 @@ function getAvailableLanguages() {
 
 function getInitialState() {
     const translations: Translations = Object.entries(getInstalledLanguages()).reduce((memo, [lang]) => {
-        memo[lang] = lang === DEFAULT_LANG ? defaultTranslations : '';
+        memo[lang] = {};
 
         return memo;
     }, {} as Translations);
diff --git a/resources/assets/javascripts/lib/reststate-vuex.js b/resources/assets/javascripts/lib/reststate-vuex.js
index 47aaaaaec45..aabdce18e43 100644
--- a/resources/assets/javascripts/lib/reststate-vuex.js
+++ b/resources/assets/javascripts/lib/reststate-vuex.js
@@ -1,5 +1,4 @@
 import ResourceClient from './reststate-client.js';
-import { isEqual } from 'lodash';
 
 const STATUS_INITIAL = 'INITIAL';
 const STATUS_LOADING = 'LOADING';
@@ -83,7 +82,7 @@ const storeIncluded = ({ dispatch }, result) => {
 };
 
 const matches = criteria => test =>
-    Object.keys(criteria).every(key => isEqual(criteria[key], test[key]));
+    Object.keys(criteria).every(key => _.isEqual(criteria[key], test[key]));
 
 const handleError = commit => errorResponse => {
     commit('SET_STATUS', STATUS_ERROR);
diff --git a/resources/assets/javascripts/lib/studip.js b/resources/assets/javascripts/lib/studip.js
new file mode 100644
index 00000000000..0a5d92423f2
--- /dev/null
+++ b/resources/assets/javascripts/lib/studip.js
@@ -0,0 +1,169 @@
+import { loadChunk, loadScript, } from '../chunk-loader.js';
+import Vue from './studip-vue.js';
+
+import ActionMenu from './actionmenu.js';
+import ActivityFeed from './activityfeed.js';
+import admin_sem_class from './admin_sem_class.js';
+import AdminCourses from './admin-courses.js';
+import Admission from './admission.js';
+import Arbeitsgruppen from './arbeitsgruppen.js';
+import Archive from './archive.js';
+import Avatar from './avatar.js';
+import BigImageHandler from './big_image_handler.js';
+import Blubber from './blubber.js';
+import Browse from './browse.js';
+import Cache from './cache.js';
+import Calendar from './calendar.js';
+import Clipboard from './clipboard.js';
+import Cookie from './cookie.js';
+import CourseWizard from './course_wizard.js';
+import { createURLHelper } from './url_helper';
+import CSS from './css.js';
+import Dates from './dates.js';
+import DateTime from './datetime.js';
+import Dialog from './dialog.js';
+import DragAndDropUpload from './drag_and_drop_upload.js';
+import enrollment from './enrollment.js';
+import eventBus from './event-bus.ts';
+import extractCallback from './extract_callback.js';
+import Files from './files.js';
+import FilesDashboard from './files_dashboard.js';
+import Folders from './folders.js';
+import Forms from './forms.js';
+import Forum from './forum.js';
+import Fullcalendar from './fullcalendar.js';
+import Fullscreen from './fullscreen.js';
+import GlobalSearch from './global_search.js';
+import HeaderMagic from './header_magic.js';
+import i18n from './i18n.js';
+import InlineEditing from './inline-editing.js';
+import JSONAPI, { jsonapi } from './jsonapi.ts';
+import JSUpdater from './jsupdater.js';
+import Lightbox from './lightbox.js';
+import Markup from './markup.js';
+import Members from './members.js';
+import Messages from './messages.js';
+import MultiPersonSearch from './multi_person_search.js';
+import MultiSelect from './multi_select.js';
+import NavigationShrinker from './navigation_shrinker.js';
+import OER from './oer.js';
+import OldUpload from './old_upload.js';
+import Overlapping from './overlapping.js';
+import Overlay from './overlay.js';
+import PageLayout from './page_layout.js';
+import parseOptions from './parse_options.js';
+import PersonalNotifications from './personal_notifications.js';
+import QRCode from './qr_code.js';
+import Questionnaire from './questionnaire.js';
+import QuickSearch from './quick_search.js';
+import QuickSelection from './quick_selection.js';
+import Raumzeit from './raumzeit.js';
+import Report from './report.ts';
+import Resources from './resources.js';
+import Responsive from './responsive.js';
+import Screenreader from './screenreader.js';
+import Scroll from './scroll.js';
+import Search from './search.js';
+import Sidebar from './sidebar.js';
+import SkipLinks from './skip_links.js';
+import startpage from './startpage.js';
+import Statusgroups from './statusgroups.js';
+import study_area_selection from './study_area_selection.js';
+import Table from './table.js';
+import TableOfContents from './table-of-contents.js';
+import Tooltip from './tooltip.js';
+import Tour from './tour.js';
+import * as Gettext from './gettext';
+import UserFilter from './user_filter.js';
+import wysiwyg from './wysiwyg.js';
+import ScrollToTop from './scroll_to_top.js';
+import * as Vips from './vips.js';
+
+const configURLHelper = window?.STUDIP?.URLHelper ?? {};
+const URLHelper = createURLHelper(configURLHelper);
+
+const STUDIP = {
+    ActionMenu,
+    ActivityFeed,
+    admin_sem_class,
+    AdminCourses,
+    Admission,
+    Arbeitsgruppen,
+    Archive,
+    Avatar,
+    BigImageHandler,
+    Blubber,
+    Browse,
+    Cache,
+    Calendar,
+    Cookie,
+    CourseWizard,
+    CSS,
+    Dates,
+    DateTime,
+    Dialog,
+    DragAndDropUpload,
+    enrollment,
+    eventBus,
+    extractCallback,
+    Files,
+    FilesDashboard,
+    Folders,
+    Forms,
+    Forum,
+    Fullcalendar,
+    Fullscreen,
+    Gettext,
+    GlobalSearch,
+    HeaderMagic,
+    i18n,
+    InlineEditing,
+    jsonapi,
+    JSONAPI,
+    JSUpdater,
+    Lightbox,
+    loadChunk,
+    loadScript,
+    Markup,
+    Members,
+    Messages,
+    MultiPersonSearch,
+    MultiSelect,
+    NavigationShrinker,
+    OER,
+    OldUpload,
+    Overlapping,
+    Overlay,
+    PageLayout,
+    parseOptions,
+    PersonalNotifications,
+    QRCode,
+    Questionnaire,
+    QuickSearch,
+    QuickSelection,
+    Raumzeit,
+    Report,
+    Responsive,
+    Scroll,
+    Screenreader,
+    Search,
+    Sidebar,
+    SkipLinks,
+    startpage,
+    Statusgroups,
+    study_area_selection,
+    Table,
+    TableOfContents,
+    Tooltip,
+    Tour,
+    URLHelper,
+    UserFilter,
+    wysiwyg,
+    Resources,
+    Clipboard,
+    ScrollToTop,
+    Vips,
+    Vue,
+};
+
+export { STUDIP };
diff --git a/resources/assets/javascripts/public-path.js b/resources/assets/javascripts/public-path.js
deleted file mode 100644
index a84ad1b63cf..00000000000
--- a/resources/assets/javascripts/public-path.js
+++ /dev/null
@@ -1,2 +0,0 @@
-/* global __webpack_public_path__ : writable */
-__webpack_public_path__ = __webpack_public_path__ || (window.STUDIP && window.STUDIP.ASSETS_URL)
diff --git a/resources/assets/stylesheets/mixins.scss b/resources/assets/stylesheets/mixins.scss
index a7bb714cafa..9c64f9e5ce2 100644
--- a/resources/assets/stylesheets/mixins.scss
+++ b/resources/assets/stylesheets/mixins.scss
@@ -1,4 +1,4 @@
-$image-path: "../images" !default;
+$image-path: "/assets/images" !default;
 $icon-path: "#{$image-path}/icons" !default;
 
 @import "scss/variables";
diff --git a/resources/assets/stylesheets/print.scss b/resources/assets/stylesheets/print.scss
index 070ed4f39ee..ce63d5e28e4 100644
--- a/resources/assets/stylesheets/print.scss
+++ b/resources/assets/stylesheets/print.scss
@@ -121,7 +121,7 @@ td.rahmen_table_row_odd {
     border-top: 1pt solid var(--black);
 
     &:after {
-        content: url('../images/logos/logo2b.png');
+        content: url('/assets/images/logos/logo2b.png');
     }
 }
 
@@ -181,7 +181,7 @@ td.quote {
 
 a.link-intern {
     padding-left: 18px;
-    @include background-icon(intern);
+    @include background-icon(link-intern);
     background-repeat: no-repeat;
 }
 
diff --git a/resources/assets/stylesheets/scss/font-face-lato.scss b/resources/assets/stylesheets/scss/font-face-lato.scss
index 2961b5a8ec4..fcba87345d2 100644
--- a/resources/assets/stylesheets/scss/font-face-lato.scss
+++ b/resources/assets/stylesheets/scss/font-face-lato.scss
@@ -1,10 +1,10 @@
 @font-face {
   font-family: 'Lato';
-  src: url('../fonts/LatoLatin/LatoLatin-Thin.eot'); /* IE9 Compat Modes */
-  src: url('../fonts/LatoLatin/LatoLatin-Thin.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
-       url('../fonts/LatoLatin/LatoLatin-Thin.woff2') format('woff2'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-Thin.woff') format('woff'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-Thin.ttf') format('truetype');
+  src: url('/assets/fonts/LatoLatin/LatoLatin-Thin.eot'); /* IE9 Compat Modes */
+  src: url('/assets/fonts/LatoLatin/LatoLatin-Thin.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/assets/fonts/LatoLatin/LatoLatin-Thin.woff2') format('woff2'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-Thin.woff') format('woff'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-Thin.ttf') format('truetype');
   font-display: auto;
   font-style: normal;
   font-weight: 100;
@@ -14,11 +14,11 @@
 
 @font-face {
   font-family: 'Lato';
-  src: url('../fonts/LatoLatin/LatoLatin-ThinItalic.eot'); /* IE9 Compat Modes */
-  src: url('../fonts/LatoLatin/LatoLatin-ThinItalic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
-       url('../fonts/LatoLatin/LatoLatin-ThinItalic.woff2') format('woff2'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-ThinItalic.woff') format('woff'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-ThinItalic.ttf') format('truetype');
+  src: url('/assets/fonts/LatoLatin/LatoLatin-ThinItalic.eot'); /* IE9 Compat Modes */
+  src: url('/assets/fonts/LatoLatin/LatoLatin-ThinItalic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/assets/fonts/LatoLatin/LatoLatin-ThinItalic.woff2') format('woff2'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-ThinItalic.woff') format('woff'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-ThinItalic.ttf') format('truetype');
   font-display: auto;
   font-style: italic;
   font-weight: 100;
@@ -28,11 +28,11 @@
 
 @font-face {
   font-family: 'Lato';
-  src: url('../fonts/LatoLatin/LatoLatin-Light.eot'); /* IE9 Compat Modes */
-  src: url('../fonts/LatoLatin/LatoLatin-Light.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
-       url('../fonts/LatoLatin/LatoLatin-Light.woff2') format('woff2'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-Light.woff') format('woff'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-Light.ttf') format('truetype');
+  src: url('/assets/fonts/LatoLatin/LatoLatin-Light.eot'); /* IE9 Compat Modes */
+  src: url('/assets/fonts/LatoLatin/LatoLatin-Light.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/assets/fonts/LatoLatin/LatoLatin-Light.woff2') format('woff2'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-Light.woff') format('woff'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-Light.ttf') format('truetype');
   font-display: auto;
   font-style: normal;
   font-weight: 300;
@@ -42,11 +42,11 @@
 
 @font-face {
   font-family: 'Lato';
-  src: url('../fonts/LatoLatin/LatoLatin-LightItalic.eot'); /* IE9 Compat Modes */
-  src: url('../fonts/LatoLatin/LatoLatin-LightItalic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
-       url('../fonts/LatoLatin/LatoLatin-LightItalic.woff2') format('woff2'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-LightItalic.woff') format('woff'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-LightItalic.ttf') format('truetype');
+  src: url('/assets/fonts/LatoLatin/LatoLatin-LightItalic.eot'); /* IE9 Compat Modes */
+  src: url('/assets/fonts/LatoLatin/LatoLatin-LightItalic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/assets/fonts/LatoLatin/LatoLatin-LightItalic.woff2') format('woff2'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-LightItalic.woff') format('woff'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-LightItalic.ttf') format('truetype');
   font-display: auto;
   font-style: italic;
   font-weight: 300;
@@ -56,11 +56,11 @@
 
 @font-face {
   font-family: 'Lato';
-  src: url('../fonts/LatoLatin/LatoLatin-Regular.eot'); /* IE9 Compat Modes */
-  src: url('../fonts/LatoLatin/LatoLatin-Regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
-       url('../fonts/LatoLatin/LatoLatin-Regular.woff2') format('woff2'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-Regular.woff') format('woff'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-Regular.ttf') format('truetype');
+  src: url('/assets/fonts/LatoLatin/LatoLatin-Regular.eot'); /* IE9 Compat Modes */
+  src: url('/assets/fonts/LatoLatin/LatoLatin-Regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/assets/fonts/LatoLatin/LatoLatin-Regular.woff2') format('woff2'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-Regular.woff') format('woff'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-Regular.ttf') format('truetype');
   font-display: auto;
   font-style: normal;
   font-weight: 400;
@@ -70,11 +70,11 @@
 
 @font-face {
   font-family: 'Lato';
-  src: url('../fonts/LatoLatin/LatoLatin-Italic.eot'); /* IE9 Compat Modes */
-  src: url('../fonts/LatoLatin/LatoLatin-Italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
-       url('../fonts/LatoLatin/LatoLatin-Italic.woff2') format('woff2'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-Italic.woff') format('woff'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-Italic.ttf') format('truetype');
+  src: url('/assets/fonts/LatoLatin/LatoLatin-Italic.eot'); /* IE9 Compat Modes */
+  src: url('/assets/fonts/LatoLatin/LatoLatin-Italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/assets/fonts/LatoLatin/LatoLatin-Italic.woff2') format('woff2'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-Italic.woff') format('woff'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-Italic.ttf') format('truetype');
   font-display: auto;
   font-style: italic;
   font-weight: 400;
@@ -84,11 +84,11 @@
 
 @font-face {
   font-family: 'Lato';
-  src: url('../fonts/LatoLatin/LatoLatin-Bold.eot'); /* IE9 Compat Modes */
-  src: url('../fonts/LatoLatin/LatoLatin-Bold.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
-       url('../fonts/LatoLatin/LatoLatin-Bold.woff2') format('woff2'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-Bold.woff') format('woff'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-Bold.ttf') format('truetype');
+  src: url('/assets/fonts/LatoLatin/LatoLatin-Bold.eot'); /* IE9 Compat Modes */
+  src: url('/assets/fonts/LatoLatin/LatoLatin-Bold.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/assets/fonts/LatoLatin/LatoLatin-Bold.woff2') format('woff2'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-Bold.woff') format('woff'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-Bold.ttf') format('truetype');
   font-display: auto;
   font-style: normal;
   font-weight: 700;
@@ -98,11 +98,11 @@
 
 @font-face {
   font-family: 'Lato';
-  src: url('../fonts/LatoLatin/LatoLatin-BoldItalic.eot'); /* IE9 Compat Modes */
-  src: url('../fonts/LatoLatin/LatoLatin-BoldItalic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
-       url('../fonts/LatoLatin/LatoLatin-BoldItalic.woff2') format('woff2'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-BoldItalic.woff') format('woff'), /* Modern Browsers */
-       url('../fonts/LatoLatin/LatoLatin-BoldItalic.ttf') format('truetype');
+  src: url('/assets/fonts/LatoLatin/LatoLatin-BoldItalic.eot'); /* IE9 Compat Modes */
+  src: url('/assets/fonts/LatoLatin/LatoLatin-BoldItalic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/assets/fonts/LatoLatin/LatoLatin-BoldItalic.woff2') format('woff2'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-BoldItalic.woff') format('woff'), /* Modern Browsers */
+       url('/assets/fonts/LatoLatin/LatoLatin-BoldItalic.ttf') format('truetype');
   font-display: auto;
   font-style: italic;
   font-weight: 700;
diff --git a/resources/assets/stylesheets/scss/installer.scss b/resources/assets/stylesheets/scss/installer.scss
index 865144f93c9..dd545dfa39c 100644
--- a/resources/assets/stylesheets/scss/installer.scss
+++ b/resources/assets/stylesheets/scss/installer.scss
@@ -6,8 +6,8 @@ body#install {
 }
 
 .stage {
-    $image-path: '../images/';
-    $icon-path: '#{$image-path}icons/';
+    $image-path: '/assets/images';
+    $icon-path: '#{$image-path}/icons';
 
     background: #fff;
     margin: 0 auto;
@@ -21,7 +21,7 @@ body#install {
         left: auto;
 
         &.ui-widget.ui-widget-content .ui-dialog-titlebar {
-            background-image: url('#{$image-path}logos/studip-logo.svg');
+            background-image: url('#{$image-path}/logos/studip-logo.svg');
             background-position: right 10px top 10px;
             background-repeat: no-repeat;
             background-size: 120px;
@@ -99,19 +99,19 @@ body#install {
 
             &.failed {
                 &::before {
-                    content: url('#{$icon-path}red/decline.svg') ' ';
+                    content: url('#{$icon-path}/red/decline.svg') ' ';
                 }
                 color: var(--red);
             }
             &.success {
                 &::before {
-                    content: url('#{$icon-path}green/accept.svg') ' ';
+                    content: url('#{$icon-path}/green/accept.svg') ' ';
                 }
                 color: var(--green);
             }
             &.notice {
                 &::before {
-                    content: url('#{$icon-path}blue/info-circle.svg') ' ';
+                    content: url('#{$icon-path}/blue/info-circle.svg') ' ';
                 }
                 color: var(--black);
             }
diff --git a/resources/assets/stylesheets/scss/jquery-ui/studip.scss b/resources/assets/stylesheets/scss/jquery-ui/studip.scss
index 002eb1716cf..14ee9e86936 100644
--- a/resources/assets/stylesheets/scss/jquery-ui/studip.scss
+++ b/resources/assets/stylesheets/scss/jquery-ui/studip.scss
@@ -157,7 +157,7 @@ textarea.ui-resizable-handle.ui-resizable-s {
 }
 
 .ui-datepicker-header .ui-icon {
-    background-image: url(../images/vendor/jquery-ui/ui-icons_ffffff_256x240.png);
+    background-image: url(/assets/images/vendor/jquery-ui/ui-icons_ffffff_256x240.png);
     height: $icon-size-inline;
     width: $icon-size-inline;
 }
diff --git a/resources/assets/stylesheets/scss/messagebox.scss b/resources/assets/stylesheets/scss/messagebox.scss
index 336eb0da56e..24567d51d66 100644
--- a/resources/assets/stylesheets/scss/messagebox.scss
+++ b/resources/assets/stylesheets/scss/messagebox.scss
@@ -111,7 +111,7 @@ section.contentbox {
         color: #000;
         border-color: var(--yellow);
         background-color: white;
-        background-image: url("@{image-path}/messagebox/question.png");
+        background-image: url("/assets/images/messagebox/question.png");
         background-size: 32px 32px;
         box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.5);
 
diff --git a/resources/assets/stylesheets/scss/selects.scss b/resources/assets/stylesheets/scss/selects.scss
index 3e46dd67228..2aa653a886c 100644
--- a/resources/assets/stylesheets/scss/selects.scss
+++ b/resources/assets/stylesheets/scss/selects.scss
@@ -40,7 +40,7 @@ select {
     }
 }
 
-@import "~select2/dist/css/select2";
+@import "select2/dist/css/select2";
 
 // The wrapper is neccessary for the validation error messages to appear
 // at the correct position
diff --git a/resources/assets/stylesheets/scss/table_of_contents.scss b/resources/assets/stylesheets/scss/table_of_contents.scss
index ef8077c43e5..9af4baea068 100644
--- a/resources/assets/stylesheets/scss/table_of_contents.scss
+++ b/resources/assets/stylesheets/scss/table_of_contents.scss
@@ -240,7 +240,7 @@ section > .toc {
 
 
 #toc-button {
-    background-image: url('#{$icon-path}blue/table-of-contents.svg');
+    background-image: url('#{$icon-path}/blue/table-of-contents.svg');
 
     height: 24px;
     width: 24px;
diff --git a/resources/assets/stylesheets/scss/vips.scss b/resources/assets/stylesheets/scss/vips.scss
index f11afda08e4..620972a9dd5 100644
--- a/resources/assets/stylesheets/scss/vips.scss
+++ b/resources/assets/stylesheets/scss/vips.scss
@@ -68,7 +68,7 @@ progress.assignment {
 
 .vips-teaser {
     background-color: var(--content-color-20);
-    background-image: url(../images/icons/blue/vips.svg);
+    background-image: url(/assets/images/icons/blue/vips.svg);
     background-position: 64px 50%;
     background-repeat: no-repeat;
     background-size: 120px;
diff --git a/resources/assets/stylesheets/studip-jquery-ui.scss b/resources/assets/stylesheets/studip-jquery-ui.scss
index d95af13b56d..c3e47f70dcc 100644
--- a/resources/assets/stylesheets/studip-jquery-ui.scss
+++ b/resources/assets/stylesheets/studip-jquery-ui.scss
@@ -5,8 +5,7 @@ $z-index: 1001;
 @import "jquery-ui.structure.css";
 @import "scss/jquery-ui/custom";
 @import "scss/jquery-ui/studip";
-@import "~jquery-ui-timepicker-addon/dist/jquery-ui-timepicker-addon.css";
-@import "~multiselect/css/multi-select.css";
+@import "multiselect/css/multi-select.css";
 
 // Tweaks/adjustments for multi-select
 .ms-container {
diff --git a/resources/vue/avatar-app.js b/resources/vue/avatar-app.js
index 63db01c5f99..49c4f69b74f 100644
--- a/resources/vue/avatar-app.js
+++ b/resources/vue/avatar-app.js
@@ -1,5 +1,5 @@
 import AvatarApp from './components/avatar/AvatarApp.vue';
-import AvatarModule from './store/avatar.module';
+import AvatarModule from './store/avatar.module.js';
 import axios from 'axios';
 import { h } from 'vue';
 
diff --git a/resources/vue/base-components.js b/resources/vue/base-components.js
index 25246429e5a..07d3a365471 100644
--- a/resources/vue/base-components.js
+++ b/resources/vue/base-components.js
@@ -1,39 +1,20 @@
 import { defineAsyncComponent } from 'vue';
 
+const formInputs = import.meta.glob('./components/form_inputs/*.vue');
+const baseComponents = import.meta.glob('./components/base/*.vue');
+
+function defineComponents(imports) {
+    return Object.fromEntries(
+        Object.entries(imports).map(([path, loader]) => {
+            const name = path.split('/').pop().replace('.vue', '');
+            return [name, defineAsyncComponent(loader)];
+        }),
+    );
+}
+
 const BaseComponents = {
-    CaptchaInput: defineAsyncComponent(() => import('./components/form_inputs/CaptchaInput.vue')),
-    CalendarPermissionsTable: defineAsyncComponent(() => import('./components/form_inputs/CalendarPermissionsTable.vue')),
-    DateListInput: defineAsyncComponent(() => import('./components/form_inputs/DateListInput.vue')),
-    Datepicker: defineAsyncComponent(() => import('./components/Datepicker.vue')),
-    Datetimepicker: defineAsyncComponent(() => import('./components/Datetimepicker.vue')),
-    DayOfWeekSelect: defineAsyncComponent(() => import('./components/form_inputs/DayOfWeekSelect.vue')),
-    EditableList: defineAsyncComponent(() => import('./components/EditableList.vue')),
-    FileUpload: defineAsyncComponent(() => import('./components/form_inputs/FileUpload.vue')),
-    I18nTextarea: defineAsyncComponent(() => import("./components/I18nTextarea.vue")),
-    Multiquicksearch: defineAsyncComponent(() => import('./components/Multiquicksearch.vue')),
-    Multiselect: defineAsyncComponent(() => import('./components/Multiselect.vue')),
-    MyCoursesColouredTable: defineAsyncComponent(() => import('./components/form_inputs/MyCoursesColouredTable.vue')),
-    Quicksearch: defineAsyncComponent(() => import('./components/Quicksearch.vue')),
-    QuicksearchListInput: defineAsyncComponent(() => import('./components/form_inputs/QuicksearchListInput.vue')),
-    RangeInput: defineAsyncComponent(() => import('./components/RangeInput.vue')),
-    RepetitionInput: defineAsyncComponent(() => import("./components/form_inputs/RepetitionInput.vue")),
-    SerialTextMarkers: defineAsyncComponent(() => import('./components/form_inputs/SerialTextMarkers.vue')),
-    SidebarWidget: defineAsyncComponent(() => import('./components/SidebarWidget.vue')),
-    StudipActionMenu: defineAsyncComponent(() => import('./components/StudipActionMenu.vue')),
-    StudipAssetImg: defineAsyncComponent(() => import('./components/StudipAssetImg.vue')),
-    StudipDateTime: defineAsyncComponent(() => import('./components/StudipDateTime.vue')),
-    StudipDialog: defineAsyncComponent(() => import('./components/StudipDialog.vue')),
-    StudipFileSize: defineAsyncComponent(() => import('./components/StudipFileSize.vue')),
-    StudipFolderSize: defineAsyncComponent(() => import('./components/StudipFolderSize.vue')),
-    StudipIcon: defineAsyncComponent(() => import('./components/StudipIcon.vue')),
-    StudipMessageBox: defineAsyncComponent(() => import('./components/StudipMessageBox.vue')),
-    StudipMultiPersonSearch: defineAsyncComponent(() => import('./components/StudipMultiPersonSearch.vue')),
-    StudipProxiedCheckbox: defineAsyncComponent(() => import('./components/StudipProxiedCheckbox.vue')),
-    StudipProxyCheckbox: defineAsyncComponent(() => import('./components/StudipProxyCheckbox.vue')),
-    StudipSelect: defineAsyncComponent(() => import('./components/StudipSelect.vue')),
-    StudipTooltipIcon: defineAsyncComponent(() => import('./components/StudipTooltipIcon.vue')),
-    StudipWysiwyg: defineAsyncComponent(() => import('./components/StudipWysiwyg.vue')),
-    UserFilterInput: defineAsyncComponent(() => import('./components/form_inputs/UserFilterInput.vue')),
+    ...defineComponents(formInputs),
+    ...defineComponents(baseComponents),
 };
 
 export default BaseComponents;
diff --git a/resources/vue/components/CacheAdministration.vue b/resources/vue/components/CacheAdministration.vue
index 5721d40105e..4c36e77636f 100644
--- a/resources/vue/components/CacheAdministration.vue
+++ b/resources/vue/components/CacheAdministration.vue
@@ -39,7 +39,7 @@
 import FileCacheConfig from './FileCacheConfig.vue'
 import MemcachedCacheConfig from './MemcachedCacheConfig.vue'
 import RedisCacheConfig from './RedisCacheConfig.vue'
-import StudipMessageBox from './StudipMessageBox.vue';
+import StudipMessageBox from '@/vue/components/base/StudipMessageBox.vue';
 
 export default {
     name: 'CacheAdministration',
diff --git a/resources/vue/components/ConsultationCreator.vue b/resources/vue/components/ConsultationCreator.vue
index 558d1bd61ed..6b59bcc006d 100644
--- a/resources/vue/components/ConsultationCreator.vue
+++ b/resources/vue/components/ConsultationCreator.vue
@@ -285,11 +285,11 @@
     </form>
 </template>
 <script>
-import StudipTooltipIcon from './StudipTooltipIcon.vue';
-import Datepicker from './Datepicker.vue';
+import StudipTooltipIcon from './base/StudipTooltipIcon.vue';
+import Datepicker from './base/Datepicker.vue';
 
 import moment from 'moment';
-import StudipSelect from './StudipSelect.vue';
+import StudipSelect from './base/StudipSelect.vue';
 import Timepicker from './Timepicker.vue';
 
 export default {
diff --git a/resources/vue/components/ContentBar.vue b/resources/vue/components/ContentBar.vue
index fc1c1e15c5d..5dc73e2f3b4 100644
--- a/resources/vue/components/ContentBar.vue
+++ b/resources/vue/components/ContentBar.vue
@@ -54,8 +54,7 @@
 import { defineComponent, PropType } from 'vue';
 
 import '../../assets/stylesheets/scss/courseware/layouts/ribbon.scss';
-import StudipIcon from './StudipIcon.vue';
-import { store } from '../../assets/javascripts/chunks/vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 import { TOCItem } from './table-of-contents';
 import ContentBarTableOfContents from './ContentBarTableOfContents.vue';
 
@@ -140,11 +139,7 @@ export default defineComponent({
     },
     computed: {
         consumeMode(): boolean {
-            // We have to access the global studipStore over an import rather than
-            // using $store/mapState/mapGetters/etc.,  because this component is
-            // compatible with Courseware, and in the various Courseware apps,
-            // $store does not include the global StudipStore.
-            return store.state.studip.consumeMode;
+            return this.$store.state.studip.consumeMode;
         },
         breadcrumbFallback(): boolean {
             return window.outerWidth < 1200;
diff --git a/resources/vue/components/ContentBarTocItemList.vue b/resources/vue/components/ContentBarTocItemList.vue
index 15101c480ca..99c42765fe8 100644
--- a/resources/vue/components/ContentBarTocItemList.vue
+++ b/resources/vue/components/ContentBarTocItemList.vue
@@ -18,7 +18,7 @@
 import { defineComponent, PropType } from 'vue';
 
 import { TOCItem } from './table-of-contents';
-import StudipIcon from './StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default defineComponent({
     name: 'ContentBarTocItemList',
diff --git a/resources/vue/components/SearchWidget.vue b/resources/vue/components/SearchWidget.vue
index 15772168ba2..6bec95f8f63 100644
--- a/resources/vue/components/SearchWidget.vue
+++ b/resources/vue/components/SearchWidget.vue
@@ -26,8 +26,8 @@
 </template>
 
 <script>
-import SidebarWidget from './SidebarWidget.vue';
-import StudipIcon from './StudipIcon.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     name: 'search-widget',
diff --git a/resources/vue/components/SearchWithFilter.vue b/resources/vue/components/SearchWithFilter.vue
index 3c45ea36924..526c9ae53af 100644
--- a/resources/vue/components/SearchWithFilter.vue
+++ b/resources/vue/components/SearchWithFilter.vue
@@ -50,7 +50,7 @@
 </template>
 
 <script>
-import StudipIcon from './StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 let searchIndex = 0;
 
diff --git a/resources/vue/components/StudipArticle.vue b/resources/vue/components/StudipArticle.vue
index 68dffd0aad0..9719182f9c8 100644
--- a/resources/vue/components/StudipArticle.vue
+++ b/resources/vue/components/StudipArticle.vue
@@ -20,7 +20,7 @@
 </template>
 
 <script>
-import StudipIcon from './StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     props: {
diff --git a/resources/vue/components/StudipContentBox.vue b/resources/vue/components/StudipContentBox.vue
index ab5bf9584c2..f90f2193828 100644
--- a/resources/vue/components/StudipContentBox.vue
+++ b/resources/vue/components/StudipContentBox.vue
@@ -23,8 +23,8 @@
 </template>
 
 <script>
-import StudipActionMenu from './StudipActionMenu.vue';
-import StudipIcon from './StudipIcon.vue';
+import StudipActionMenu from '@/vue/components/base/StudipActionMenu.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     components: { StudipActionMenu, StudipIcon },
diff --git a/resources/vue/components/StudipWizardDialog.vue b/resources/vue/components/StudipWizardDialog.vue
index 5c60a27fac7..6169ba16da6 100644
--- a/resources/vue/components/StudipWizardDialog.vue
+++ b/resources/vue/components/StudipWizardDialog.vue
@@ -98,8 +98,8 @@
 </template>
 
 <script>
-import StudipDialog from './StudipDialog.vue'
-import StudipIcon from './StudipIcon.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue'
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 export default {
     name: 'studip-wizard-dialog',
     emits: ['close', 'confirm'],
diff --git a/resources/vue/components/WikiEditor.vue b/resources/vue/components/WikiEditor.vue
index 8fc2e41292f..1e9fcc16317 100644
--- a/resources/vue/components/WikiEditor.vue
+++ b/resources/vue/components/WikiEditor.vue
@@ -79,7 +79,7 @@
 </template>
 <script>
 import WikiEditorOnlineUsers from "./WikiEditorOnlineUsers.vue";
-import StudipDateTime from "./StudipDateTime.vue";
+import StudipDateTime from "@/vue/components/base/StudipDateTime.vue";
 import JSUpdater from "@/assets/javascripts/lib/jsupdater";
 import ContentBar from "./ContentBar.vue";
 import ContentBarBreadcrumbs from "./ContentBarBreadcrumbs.vue";
diff --git a/resources/vue/components/WikiEditorOnlineUsers.vue b/resources/vue/components/WikiEditorOnlineUsers.vue
index 7bee689b83f..24492f1d5dc 100644
--- a/resources/vue/components/WikiEditorOnlineUsers.vue
+++ b/resources/vue/components/WikiEditorOnlineUsers.vue
@@ -20,8 +20,8 @@
     </Teleport>
 </template>
 <script>
-import SidebarWidget from "./SidebarWidget.vue";
-import StudipIcon from "./StudipIcon.vue";
+import SidebarWidget from "@/vue/components/base/SidebarWidget.vue";
+import StudipIcon from "@/vue/components/base/StudipIcon.vue";
 
 export default {
     name: 'WikiEditorOnlineUsers',
diff --git a/resources/vue/components/admission/AdmissionRuleConfig.vue b/resources/vue/components/admission/AdmissionRuleConfig.vue
index d6d6e653f40..3104f19bfef 100644
--- a/resources/vue/components/admission/AdmissionRuleConfig.vue
+++ b/resources/vue/components/admission/AdmissionRuleConfig.vue
@@ -94,7 +94,7 @@ export default {
     },
     created() {
         const file = STUDIP.Admission.availableRules[this.type];
-        import(`@/vue/components/admission/${file}`).then((module) => {
+        import(`@/vue/components/admission/rules/${file}.vue`).then((module) => {
             this.component = shallowRef(module.default);
             this.props = {
                 id: this.theRule?.id,
diff --git a/resources/vue/components/admission/AdmissionRuleTypeSelector.vue b/resources/vue/components/admission/AdmissionRuleTypeSelector.vue
index e8bbe8c9fe6..7e75fd95b09 100644
--- a/resources/vue/components/admission/AdmissionRuleTypeSelector.vue
+++ b/resources/vue/components/admission/AdmissionRuleTypeSelector.vue
@@ -66,7 +66,7 @@
 
 <script>
 import StudipProgressIndicator from '../StudipProgressIndicator.vue';
-import StudipDialog from '../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 
 export default {
     name: 'AdmissionRuleTypeSelector',
diff --git a/resources/vue/components/admission/ConfigureCourseSet.vue b/resources/vue/components/admission/ConfigureCourseSet.vue
index 595f5ef2e22..b4abc5ed561 100644
--- a/resources/vue/components/admission/ConfigureCourseSet.vue
+++ b/resources/vue/components/admission/ConfigureCourseSet.vue
@@ -350,7 +350,7 @@
 </template>
 
 <script>
-import quicksearch from '../Quicksearch.vue';
+import quicksearch from '@/vue/components/base/Quicksearch.vue';
 import AdmissionRuleTypeSelector from './AdmissionRuleTypeSelector.vue';
 import AdmissionRuleConfig from './AdmissionRuleConfig.vue';
 import StudipProgressIndicator from "../StudipProgressIndicator.vue";
diff --git a/resources/vue/components/admission/InstantCourseSet.vue b/resources/vue/components/admission/InstantCourseSet.vue
index ec555c685bf..39dc7217925 100644
--- a/resources/vue/components/admission/InstantCourseSet.vue
+++ b/resources/vue/components/admission/InstantCourseSet.vue
@@ -23,7 +23,7 @@
 </template>
 
 <script>
-import AdmissionRuleConfig from './AdmissionRuleConfig';
+import AdmissionRuleConfig from './AdmissionRuleConfig.vue';
 
 export default {
     name: 'InstantCourseSet',
diff --git a/resources/vue/components/admission/ValidityTime.vue b/resources/vue/components/admission/ValidityTime.vue
index 32efd04a239..80f154aaa08 100644
--- a/resources/vue/components/admission/ValidityTime.vue
+++ b/resources/vue/components/admission/ValidityTime.vue
@@ -29,7 +29,7 @@
 </template>
 
 <script>
-import datetimepicker from '../Datetimepicker.vue';
+import datetimepicker from '@/vue/components/base/Datetimepicker.vue';
 
 export default {
     name: 'ValidityTime',
diff --git a/resources/vue/components/admission/ConditionalAdmission.vue b/resources/vue/components/admission/rules/ConditionalAdmission.vue
similarity index 97%
rename from resources/vue/components/admission/ConditionalAdmission.vue
rename to resources/vue/components/admission/rules/ConditionalAdmission.vue
index a0eee478c4e..65a9e4d79a0 100644
--- a/resources/vue/components/admission/ConditionalAdmission.vue
+++ b/resources/vue/components/admission/rules/ConditionalAdmission.vue
@@ -88,9 +88,9 @@
 </template>
 
 <script>
-import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin';
-import ValidityTime from "./ValidityTime.vue";
-import StudipUserFilter from "../StudipUserFilter.vue";
+import { AdmissionRuleMixin } from '../../../mixins/AdmissionRuleMixin';
+import ValidityTime from "../ValidityTime.vue";
+import StudipUserFilter from "../../StudipUserFilter.vue";
 
 export default {
     name: 'ConditionalAdmission',
diff --git a/resources/vue/components/admission/CourseMemberAdmission.vue b/resources/vue/components/admission/rules/CourseMemberAdmission.vue
similarity index 95%
rename from resources/vue/components/admission/CourseMemberAdmission.vue
rename to resources/vue/components/admission/rules/CourseMemberAdmission.vue
index 3684eb35e27..6ea8b675728 100644
--- a/resources/vue/components/admission/CourseMemberAdmission.vue
+++ b/resources/vue/components/admission/rules/CourseMemberAdmission.vue
@@ -37,9 +37,9 @@
 </template>
 
 <script>
-import {AdmissionRuleMixin} from '../../mixins/AdmissionRuleMixin';
-import ValidityTime from './ValidityTime.vue';
-import quicksearch from '../Quicksearch.vue';
+import {AdmissionRuleMixin} from '../../../mixins/AdmissionRuleMixin';
+import ValidityTime from '../ValidityTime.vue';
+import quicksearch from '@/vue/components/base/Quicksearch.vue';
 
 export default {
     name: 'CourseMemberAdmission',
diff --git a/resources/vue/components/admission/LimitedAdmission.vue b/resources/vue/components/admission/rules/LimitedAdmission.vue
similarity index 93%
rename from resources/vue/components/admission/LimitedAdmission.vue
rename to resources/vue/components/admission/rules/LimitedAdmission.vue
index bfa57965330..942931e5c75 100644
--- a/resources/vue/components/admission/LimitedAdmission.vue
+++ b/resources/vue/components/admission/rules/LimitedAdmission.vue
@@ -19,8 +19,8 @@
 </template>
 
 <script>
-import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin';
-import ValidityTime from './ValidityTime.vue';
+import { AdmissionRuleMixin } from '../../../mixins/AdmissionRuleMixin';
+import ValidityTime from '../ValidityTime.vue';
 
 export default {
     name: 'LimitedAdmission',
diff --git a/resources/vue/components/admission/LockedAdmission.vue b/resources/vue/components/admission/rules/LockedAdmission.vue
similarity index 92%
rename from resources/vue/components/admission/LockedAdmission.vue
rename to resources/vue/components/admission/rules/LockedAdmission.vue
index 3e050bed6b7..553467e5302 100644
--- a/resources/vue/components/admission/LockedAdmission.vue
+++ b/resources/vue/components/admission/rules/LockedAdmission.vue
@@ -10,7 +10,7 @@
 </template>
 
 <script>
-import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin';
+import { AdmissionRuleMixin } from '../../../mixins/AdmissionRuleMixin';
 
 export default {
     name: 'LockedAdmission',
diff --git a/resources/vue/components/admission/ParticipantRestrictedAdmission.vue b/resources/vue/components/admission/rules/ParticipantRestrictedAdmission.vue
similarity index 93%
rename from resources/vue/components/admission/ParticipantRestrictedAdmission.vue
rename to resources/vue/components/admission/rules/ParticipantRestrictedAdmission.vue
index a7468f17a3e..971a05bb8d2 100644
--- a/resources/vue/components/admission/ParticipantRestrictedAdmission.vue
+++ b/resources/vue/components/admission/rules/ParticipantRestrictedAdmission.vue
@@ -23,9 +23,9 @@
 </template>
 
 <script>
-import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin';
-import datetimepicker from '../Datetimepicker.vue';
-import StudipTooltipIcon from '../StudipTooltipIcon.vue';
+import { AdmissionRuleMixin } from '../../../mixins/AdmissionRuleMixin';
+import datetimepicker from '@/vue/components/base/Datetimepicker.vue';
+import StudipTooltipIcon from '@/vue/components/base/StudipTooltipIcon.vue';
 
 export default {
     name: 'ParticipantRestrictedAdmission',
diff --git a/resources/vue/components/admission/PasswordAdmission.vue b/resources/vue/components/admission/rules/PasswordAdmission.vue
similarity index 97%
rename from resources/vue/components/admission/PasswordAdmission.vue
rename to resources/vue/components/admission/rules/PasswordAdmission.vue
index 5d87e4d454d..30c3e463c72 100644
--- a/resources/vue/components/admission/PasswordAdmission.vue
+++ b/resources/vue/components/admission/rules/PasswordAdmission.vue
@@ -29,7 +29,7 @@
 </template>
 
 <script>
-import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin';
+import { AdmissionRuleMixin } from '../../../mixins/AdmissionRuleMixin';
 
 export default {
     name: 'PasswordAdmission',
diff --git a/resources/vue/components/admission/PreferentialAdmission.vue b/resources/vue/components/admission/rules/PreferentialAdmission.vue
similarity index 96%
rename from resources/vue/components/admission/PreferentialAdmission.vue
rename to resources/vue/components/admission/rules/PreferentialAdmission.vue
index 76841ec5741..38f4e75ff5b 100644
--- a/resources/vue/components/admission/PreferentialAdmission.vue
+++ b/resources/vue/components/admission/rules/PreferentialAdmission.vue
@@ -46,8 +46,8 @@
 </template>
 
 <script>
-import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin';
-import StudipUserFilter from "../StudipUserFilter.vue";
+import { AdmissionRuleMixin } from '../../../mixins/AdmissionRuleMixin';
+import StudipUserFilter from "../../StudipUserFilter.vue";
 
 export default {
     name: 'PreferentialAdmission',
diff --git a/resources/vue/components/admission/TermsAdmission.vue b/resources/vue/components/admission/rules/TermsAdmission.vue
similarity index 95%
rename from resources/vue/components/admission/TermsAdmission.vue
rename to resources/vue/components/admission/rules/TermsAdmission.vue
index 98abcf8fa0b..2cf6733feab 100644
--- a/resources/vue/components/admission/TermsAdmission.vue
+++ b/resources/vue/components/admission/rules/TermsAdmission.vue
@@ -13,7 +13,7 @@
 </template>
 
 <script>
-import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin';
+import { AdmissionRuleMixin } from '../../../mixins/AdmissionRuleMixin';
 
 export default {
     name: 'TermsAdmission',
diff --git a/resources/vue/components/admission/TimedAdmission.vue b/resources/vue/components/admission/rules/TimedAdmission.vue
similarity index 94%
rename from resources/vue/components/admission/TimedAdmission.vue
rename to resources/vue/components/admission/rules/TimedAdmission.vue
index 2437d7ebd94..1569e7e802a 100644
--- a/resources/vue/components/admission/TimedAdmission.vue
+++ b/resources/vue/components/admission/rules/TimedAdmission.vue
@@ -22,8 +22,8 @@
 </template>
 
 <script>
-import { AdmissionRuleMixin } from '../../mixins/AdmissionRuleMixin';
-import datetimepicker from "../Datetimepicker.vue";
+import { AdmissionRuleMixin } from '../../../mixins/AdmissionRuleMixin';
+import datetimepicker from "@/vue/components/base/Datetimepicker.vue";
 export default {
     name: 'TimedAdmission',
     components: { datetimepicker },
diff --git a/resources/vue/components/avatar/AvatarApp.vue b/resources/vue/components/avatar/AvatarApp.vue
index 8a33e380c0a..cdae34538a4 100644
--- a/resources/vue/components/avatar/AvatarApp.vue
+++ b/resources/vue/components/avatar/AvatarApp.vue
@@ -142,8 +142,8 @@
 
 <script>
 import axios from 'axios';
-import StudipDialog from '../StudipDialog.vue';
-import StudipMessageBox from '../StudipMessageBox.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
+import StudipMessageBox from '@/vue/components/base/StudipMessageBox.vue';
 import StudipSquareButton from '../StudipSquareButton.vue';
 import StockImageSelector from '../stock-images/SelectorDialog.vue';
 import { mapGetters } from 'vuex';
diff --git a/resources/vue/components/Datepicker.vue b/resources/vue/components/base/Datepicker.vue
similarity index 98%
rename from resources/vue/components/Datepicker.vue
rename to resources/vue/components/base/Datepicker.vue
index 9aeadf12db2..8ee1ca90cb7 100644
--- a/resources/vue/components/Datepicker.vue
+++ b/resources/vue/components/base/Datepicker.vue
@@ -10,7 +10,7 @@
 </template>
 
 <script>
-import RestrictedDatesHelper from '../../assets/javascripts/lib/RestrictedDatesHelper';
+import RestrictedDatesHelper from '@/assets/javascripts/lib/RestrictedDatesHelper';
 
 export default {
     name: 'Datepicker',
diff --git a/resources/vue/components/Datetimepicker.vue b/resources/vue/components/base/Datetimepicker.vue
similarity index 100%
rename from resources/vue/components/Datetimepicker.vue
rename to resources/vue/components/base/Datetimepicker.vue
diff --git a/resources/vue/components/EditableList.vue b/resources/vue/components/base/EditableList.vue
similarity index 100%
rename from resources/vue/components/EditableList.vue
rename to resources/vue/components/base/EditableList.vue
diff --git a/resources/vue/components/I18nTextarea.vue b/resources/vue/components/base/I18nTextarea.vue
similarity index 99%
rename from resources/vue/components/I18nTextarea.vue
rename to resources/vue/components/base/I18nTextarea.vue
index c6d102b913e..ecfb5e1e18d 100644
--- a/resources/vue/components/I18nTextarea.vue
+++ b/resources/vue/components/base/I18nTextarea.vue
@@ -65,7 +65,7 @@
 </template>
 
 <script>
-import StudipWysiwyg from './StudipWysiwyg.vue';
+import StudipWysiwyg from '@/vue/components/base/StudipWysiwyg.vue';
 export default {
     name: 'i18n-textarea',
     components: {
diff --git a/resources/vue/components/Multiquicksearch.vue b/resources/vue/components/base/Multiquicksearch.vue
similarity index 100%
rename from resources/vue/components/Multiquicksearch.vue
rename to resources/vue/components/base/Multiquicksearch.vue
diff --git a/resources/vue/components/Multiselect.vue b/resources/vue/components/base/Multiselect.vue
similarity index 100%
rename from resources/vue/components/Multiselect.vue
rename to resources/vue/components/base/Multiselect.vue
diff --git a/resources/vue/components/Quicksearch.vue b/resources/vue/components/base/Quicksearch.vue
similarity index 100%
rename from resources/vue/components/Quicksearch.vue
rename to resources/vue/components/base/Quicksearch.vue
diff --git a/resources/vue/components/RangeInput.vue b/resources/vue/components/base/RangeInput.vue
similarity index 100%
rename from resources/vue/components/RangeInput.vue
rename to resources/vue/components/base/RangeInput.vue
diff --git a/resources/vue/components/SidebarWidget.vue b/resources/vue/components/base/SidebarWidget.vue
similarity index 100%
rename from resources/vue/components/SidebarWidget.vue
rename to resources/vue/components/base/SidebarWidget.vue
diff --git a/resources/vue/components/StudipActionMenu.vue b/resources/vue/components/base/StudipActionMenu.vue
similarity index 98%
rename from resources/vue/components/StudipActionMenu.vue
rename to resources/vue/components/base/StudipActionMenu.vue
index bb189f8892b..bfafb5eebec 100644
--- a/resources/vue/components/StudipActionMenu.vue
+++ b/resources/vue/components/base/StudipActionMenu.vue
@@ -74,7 +74,7 @@
 </template>
 
 <script>
-import { $gettext } from '../../assets/javascripts/lib/gettext';
+import { $gettext } from '@/assets/javascripts/lib/gettext';
 
 export default {
     name: 'studip-action-menu',
diff --git a/resources/vue/components/StudipAssetImg.vue b/resources/vue/components/base/StudipAssetImg.vue
similarity index 100%
rename from resources/vue/components/StudipAssetImg.vue
rename to resources/vue/components/base/StudipAssetImg.vue
diff --git a/resources/vue/components/StudipDateTime.vue b/resources/vue/components/base/StudipDateTime.vue
similarity index 100%
rename from resources/vue/components/StudipDateTime.vue
rename to resources/vue/components/base/StudipDateTime.vue
diff --git a/resources/vue/components/StudipDialog.vue b/resources/vue/components/base/StudipDialog.vue
similarity index 100%
rename from resources/vue/components/StudipDialog.vue
rename to resources/vue/components/base/StudipDialog.vue
diff --git a/resources/vue/components/StudipFileSize.vue b/resources/vue/components/base/StudipFileSize.vue
similarity index 100%
rename from resources/vue/components/StudipFileSize.vue
rename to resources/vue/components/base/StudipFileSize.vue
diff --git a/resources/vue/components/StudipFolderSize.vue b/resources/vue/components/base/StudipFolderSize.vue
similarity index 100%
rename from resources/vue/components/StudipFolderSize.vue
rename to resources/vue/components/base/StudipFolderSize.vue
diff --git a/resources/vue/components/StudipIcon.vue b/resources/vue/components/base/StudipIcon.vue
similarity index 100%
rename from resources/vue/components/StudipIcon.vue
rename to resources/vue/components/base/StudipIcon.vue
diff --git a/resources/vue/components/StudipMessageBox.vue b/resources/vue/components/base/StudipMessageBox.vue
similarity index 100%
rename from resources/vue/components/StudipMessageBox.vue
rename to resources/vue/components/base/StudipMessageBox.vue
diff --git a/resources/vue/components/StudipMultiPersonSearch.vue b/resources/vue/components/base/StudipMultiPersonSearch.vue
similarity index 100%
rename from resources/vue/components/StudipMultiPersonSearch.vue
rename to resources/vue/components/base/StudipMultiPersonSearch.vue
diff --git a/resources/vue/components/StudipProxiedCheckbox.vue b/resources/vue/components/base/StudipProxiedCheckbox.vue
similarity index 100%
rename from resources/vue/components/StudipProxiedCheckbox.vue
rename to resources/vue/components/base/StudipProxiedCheckbox.vue
diff --git a/resources/vue/components/StudipProxyCheckbox.vue b/resources/vue/components/base/StudipProxyCheckbox.vue
similarity index 100%
rename from resources/vue/components/StudipProxyCheckbox.vue
rename to resources/vue/components/base/StudipProxyCheckbox.vue
diff --git a/resources/vue/components/StudipSelect.vue b/resources/vue/components/base/StudipSelect.vue
similarity index 100%
rename from resources/vue/components/StudipSelect.vue
rename to resources/vue/components/base/StudipSelect.vue
diff --git a/resources/vue/components/StudipTooltipIcon.vue b/resources/vue/components/base/StudipTooltipIcon.vue
similarity index 100%
rename from resources/vue/components/StudipTooltipIcon.vue
rename to resources/vue/components/base/StudipTooltipIcon.vue
diff --git a/resources/vue/components/StudipWysiwyg.vue b/resources/vue/components/base/StudipWysiwyg.vue
similarity index 83%
rename from resources/vue/components/StudipWysiwyg.vue
rename to resources/vue/components/base/StudipWysiwyg.vue
index c7de9c7f6e5..61c65ab4fda 100644
--- a/resources/vue/components/StudipWysiwyg.vue
+++ b/resources/vue/components/base/StudipWysiwyg.vue
@@ -1,6 +1,5 @@
 <script>
-import { ClassicEditor, BalloonEditor } from '../../assets/javascripts/chunks/wysiwyg.js';
-import {h, resolveComponent} from "vue";
+import { defineAsyncComponent, h, resolveComponent} from "vue";
 
 export default {
     name: 'studip-wysiwyg',
@@ -32,9 +31,9 @@ export default {
         editor() {
             switch (this.editorType) {
                 case 'classic':
-                    return ClassicEditor;
+                    return this.ClassicEditor;
                 case 'balloon':
-                    return BalloonEditor;
+                    return this.BalloonEditor;
             }
             throw new Error('Unknown `editorType`');
         },
@@ -62,8 +61,11 @@ export default {
             }
         }
     },
-    created() {
+    async created() {
         STUDIP.loadChunk('mathjax');
+        const { BalloonEditor, ClassicEditor } =  await STUDIP.loadChunk('wysiwyg');
+        this.BalloonEditor = BalloonEditor;
+        this.ClassicEditor = ClassicEditor;
     },
     render() {
         return h(resolveComponent('ckeditor'), {
diff --git a/resources/vue/components/blubber/SearchWidget.vue b/resources/vue/components/blubber/SearchWidget.vue
index 016f5036c91..d7e02891c9e 100644
--- a/resources/vue/components/blubber/SearchWidget.vue
+++ b/resources/vue/components/blubber/SearchWidget.vue
@@ -37,7 +37,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 
 let nextId = 0;
 
diff --git a/resources/vue/components/blubber/ThreadsWidget.vue b/resources/vue/components/blubber/ThreadsWidget.vue
index 72b1bf1a649..9bf9996f3e4 100644
--- a/resources/vue/components/blubber/ThreadsWidget.vue
+++ b/resources/vue/components/blubber/ThreadsWidget.vue
@@ -40,7 +40,7 @@
     </SidebarWidget>
 </template>
 <script>
-import SidebarWidget from '../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 
 export default {
     emits: ['load-more-threads', 'select-thread'],
diff --git a/resources/vue/components/courseware/CoursewareActivityItem.vue b/resources/vue/components/courseware/CoursewareActivityItem.vue
index 9520d36047f..a762b7aef2d 100644
--- a/resources/vue/components/courseware/CoursewareActivityItem.vue
+++ b/resources/vue/components/courseware/CoursewareActivityItem.vue
@@ -16,7 +16,7 @@
 </template>
 
 <script>
-import StudipIcon from './../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 import { mapGetters } from 'vuex';
 
diff --git a/resources/vue/components/courseware/CoursewareAdminTemplates.vue b/resources/vue/components/courseware/CoursewareAdminTemplates.vue
index 057aee670a4..10f7300736e 100644
--- a/resources/vue/components/courseware/CoursewareAdminTemplates.vue
+++ b/resources/vue/components/courseware/CoursewareAdminTemplates.vue
@@ -113,8 +113,8 @@
 </template>
 
 <script>
-import StudipActionMenu from '../StudipActionMenu.vue';
-import StudipDialog from './../StudipDialog.vue';
+import StudipActionMenu from '@/vue/components/base/StudipActionMenu.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 
 import JSZip from 'jszip';
 import { mapActions, mapGetters } from 'vuex';
diff --git a/resources/vue/components/courseware/CoursewareContentBookmarks.vue b/resources/vue/components/courseware/CoursewareContentBookmarks.vue
index 83d0927ddf0..95f734644ac 100644
--- a/resources/vue/components/courseware/CoursewareContentBookmarks.vue
+++ b/resources/vue/components/courseware/CoursewareContentBookmarks.vue
@@ -37,7 +37,7 @@
 
 <script>
 import { mapActions, mapGetters } from 'vuex';
-import StudipIcon from '../StudipIcon.vue'
+import StudipIcon from '@/vue/components/base/StudipIcon.vue'
 export default {
     name: 'courseware-content-bookmarks',
     components: {
diff --git a/resources/vue/components/courseware/CoursewareContentLinks.vue b/resources/vue/components/courseware/CoursewareContentLinks.vue
index 407543940df..b8df1d79942 100644
--- a/resources/vue/components/courseware/CoursewareContentLinks.vue
+++ b/resources/vue/components/courseware/CoursewareContentLinks.vue
@@ -70,9 +70,9 @@
 </template>
 
 <script>
-import StudipActionMenu from './../StudipActionMenu.vue';
+import StudipActionMenu from '@/vue/components/base/StudipActionMenu.vue';
 import { mapActions, mapGetters } from 'vuex';
-import StudipIcon from '../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     name: 'courseware-content-links',
diff --git a/resources/vue/components/courseware/CoursewareContentShared.vue b/resources/vue/components/courseware/CoursewareContentShared.vue
index 993a08bc3a9..b2d944b3560 100644
--- a/resources/vue/components/courseware/CoursewareContentShared.vue
+++ b/resources/vue/components/courseware/CoursewareContentShared.vue
@@ -84,9 +84,9 @@
 <script>
 import CoursewareContentPermissions from './CoursewareContentPermissions.vue';
 import { mapActions, mapGetters } from 'vuex';
-import StudipActionMenu from './../StudipActionMenu.vue';
-import StudipDialog from '../StudipDialog.vue';
-import StudipIcon from '../StudipIcon.vue';
+import StudipActionMenu from '@/vue/components/base/StudipActionMenu.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     name: 'courseware-content-shared',
diff --git a/resources/vue/components/courseware/blocks/CoursewareBiographyAchievementsBlock.vue b/resources/vue/components/courseware/blocks/CoursewareBiographyAchievementsBlock.vue
index e664a9896cd..b55af767a37 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBiographyAchievementsBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBiographyAchievementsBlock.vue
@@ -80,7 +80,7 @@
 <script>
 import BlockComponents from './block-components.js';
 import blockMixin from '@/vue/mixins/courseware/block.js';
-import StudipWysiwyg from '../../StudipWysiwyg.vue';
+import StudipWysiwyg from '@/vue/components/base/StudipWysiwyg.vue';
 import { mapActions } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/blocks/CoursewareBiographyGoalsBlock.vue b/resources/vue/components/courseware/blocks/CoursewareBiographyGoalsBlock.vue
index cba078fdbac..f20416fd609 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBiographyGoalsBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBiographyGoalsBlock.vue
@@ -43,7 +43,7 @@
 <script>
 import BlockComponents from './block-components.js';
 import blockMixin from '@/vue/mixins/courseware/block.js';
-import StudipWysiwyg from '../../StudipWysiwyg.vue';
+import StudipWysiwyg from '@/vue/components/base/StudipWysiwyg.vue';
 import { mapActions } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue b/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue
index 468809074f8..0ca99effc8d 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue
@@ -18,7 +18,7 @@
 </template>
 
 <script>
-import StudipActionMenu from '../../StudipActionMenu.vue';
+import StudipActionMenu from '@/vue/components/base/StudipActionMenu.vue';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlubberComment.vue b/resources/vue/components/courseware/blocks/CoursewareBlubberComment.vue
index 5fc28bd1463..6432922965c 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBlubberComment.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBlubberComment.vue
@@ -71,7 +71,7 @@
 </template>
 
 <script>
-import StudipDialog from '../../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 import StudipProgressIndicator from '../../StudipProgressIndicator.vue';
 import { mapActions, mapGetters } from 'vuex';
 
diff --git a/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue
index 02e56ac9340..2651a07a086 100644
--- a/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue
@@ -92,8 +92,8 @@ import CoursewareBlockDiscussion from './CoursewareBlockDiscussion.vue';
 import CoursewareBlockEdit from './CoursewareBlockEdit.vue';
 import CoursewareBlockExportOptions from './CoursewareBlockExportOptions.vue';
 import CoursewareBlockInfo from './CoursewareBlockInfo.vue';
-import StudipDialog from '../../StudipDialog.vue';
-import StudipIcon from '../../StudipIcon.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 import blockMixin from '@/vue/mixins/courseware/block.js';
 import { mapActions, mapGetters } from 'vuex';
 
diff --git a/resources/vue/components/courseware/blocks/CoursewareTextBlock.vue b/resources/vue/components/courseware/blocks/CoursewareTextBlock.vue
index c0003717a13..dc376c3acaf 100644
--- a/resources/vue/components/courseware/blocks/CoursewareTextBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareTextBlock.vue
@@ -25,9 +25,11 @@
 import BlockComponents from './block-components.js';
 import blockMixin from '@/vue/mixins/courseware/block.js';
 import FindAndReplace from '@ckeditor/ckeditor5-find-and-replace/src/findandreplace';
-import { ClassicEditor } from '@/assets/javascripts/chunks/wysiwyg';
+import { defineAsyncComponent } from 'vue';
 import { mapActions } from 'vuex';
 
+const ClassicEditor = defineAsyncComponent(() => STUDIP.loadChunk('wysiwyg').then(({ ClassicEditor }) => ClassicEditor));
+
 export default {
     name: 'courseware-text-block',
     mixins: [blockMixin],
diff --git a/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue b/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue
index b2eb6248726..abaa0198364 100644
--- a/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue
+++ b/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue
@@ -135,7 +135,7 @@
 
 <script>
 import CoursewareContainerActions from './CoursewareContainerActions.vue';
-import StudipDialog from '../../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 import { mapGetters, mapActions } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/containers/container-components.js b/resources/vue/components/courseware/containers/container-components.js
index c920dae3464..a32af9445b4 100644
--- a/resources/vue/components/courseware/containers/container-components.js
+++ b/resources/vue/components/courseware/containers/container-components.js
@@ -34,7 +34,7 @@ import CoursewareTypewriterBlock from '../blocks/CoursewareTypewriterBlock.vue';
 import CoursewareVideoBlock from '../blocks/CoursewareVideoBlock.vue';
 //layout
 import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
-import StudipIcon from '../../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 import StudipProgressIndicator from '../../StudipProgressIndicator.vue';
 
 const ContainerComponents = {
diff --git a/resources/vue/components/courseware/layouts/CoursewareCallToActionBox.vue b/resources/vue/components/courseware/layouts/CoursewareCallToActionBox.vue
index ffe3953fad5..fc4300c059d 100644
--- a/resources/vue/components/courseware/layouts/CoursewareCallToActionBox.vue
+++ b/resources/vue/components/courseware/layouts/CoursewareCallToActionBox.vue
@@ -12,7 +12,7 @@
 </template>
 
 <script>
-import StudipIcon from '../../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     name: 'courseware-call-to-action-box',
diff --git a/resources/vue/components/courseware/layouts/CoursewareCollapsibleBox.vue b/resources/vue/components/courseware/layouts/CoursewareCollapsibleBox.vue
index 7e41a92f16d..182217d57fb 100644
--- a/resources/vue/components/courseware/layouts/CoursewareCollapsibleBox.vue
+++ b/resources/vue/components/courseware/layouts/CoursewareCollapsibleBox.vue
@@ -13,7 +13,7 @@
 </template>
 
 <script>
-import StudipIcon from '../../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     name: 'courseware-collapsible-box',
diff --git a/resources/vue/components/courseware/structural-element/CoursewareRibbon.vue b/resources/vue/components/courseware/structural-element/CoursewareRibbon.vue
index 5a9e98a7ba8..792be3830c6 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareRibbon.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareRibbon.vue
@@ -33,7 +33,6 @@
 import ContentBar from '../../ContentBar.vue';
 import { mapActions, mapGetters } from 'vuex';
 import CoursewareRibbonToolbar from './CoursewareRibbonToolbar.vue';
-import { store } from '../../../../assets/javascripts/chunks/vue';
 import { defineComponent } from "vue";
 
 export default defineComponent({
@@ -70,13 +69,7 @@ export default defineComponent({
             showToolbar: 'showToolbar',
         }),
         consumeMode(): boolean {
-            // TODO ensure that there is only one global StudipStore / 'studip' store module
-            //  across Courseware and chunks/vue.js.
-            // Currently, the 'studip' module of the courseware store is deceivingly named.
-            // It is a completely different store than the one in chunks/vue.js.
-            // It just happens to have a module with the same name, 'studip'.
-            // So, to access the global studipStore, we have to import it and access it like this.
-            return store.state.studip.consumeMode;
+            return this.$store.state.studip.consumeMode;
         },
         strings() {
             return {
diff --git a/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue b/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue
index 902241b9296..3d6e66216d5 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue
@@ -48,7 +48,6 @@ import CoursewareToolsContents from './CoursewareToolsContents.vue';
 import CoursewareToolsUnits from './CoursewareToolsUnits.vue';
 import { FocusTrap } from 'focus-trap-vue';
 import { mapActions, mapGetters } from 'vuex';
-import { store } from "../../../../assets/javascripts/chunks/vue";
 
 export default {
     name: 'courseware-ribbon-toolbar',
@@ -74,7 +73,7 @@ export default {
     },
     computed: {
         consumeMode() {
-          return store.state.studip.consumeMode;
+            return this.$store.state.studip.consumeMode;
         },
         ...mapGetters({
             userIsTeacher: 'userIsTeacher',
diff --git a/resources/vue/components/courseware/structural-element/CoursewareSearchResults.vue b/resources/vue/components/courseware/structural-element/CoursewareSearchResults.vue
index 961fae3f51b..9ed5f461bda 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareSearchResults.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareSearchResults.vue
@@ -46,7 +46,7 @@
 
 <script>
 import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
-import StudipIcon from '../../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 import { mapActions, mapGetters } from 'vuex';
 import ContentBar from "../../ContentBar.vue";
 
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue
index bf2af11afbe..0a02a98a0d9 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue
@@ -475,17 +475,16 @@ import colorMixin from '@/vue/mixins/courseware/colors.js';
 import wizardMixin from '@/vue/mixins/courseware/wizard.js';
 import CoursewareCallToActionBox from '../layouts/CoursewareCallToActionBox.vue';
 import CoursewareDateInput from '../layouts/CoursewareDateInput.vue';
-import StudipDialog from '../../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 import { FocusTrap } from 'focus-trap-vue';
 import FeedbackDialog from '../../feedback/FeedbackDialog.vue';
 import FeedbackCreateDialog from '../../feedback/FeedbackCreateDialog.vue';
 import StudipFiveStars from '../../feedback/StudipFiveStars.vue';
-import StudipMessageBox from '../../StudipMessageBox.vue';
+import StudipMessageBox from '@/vue/components/base/StudipMessageBox.vue';
 import StudipProgressIndicator from '../../StudipProgressIndicator.vue';
 import draggable from 'vuedraggable';
 import containerMixin from '@/vue/mixins/courseware/container.js';
 import { mapActions, mapGetters } from 'vuex';
-import { store } from "../../../../assets/javascripts/chunks/vue";
 
 export default {
     name: 'courseware-structural-element',
@@ -567,7 +566,7 @@ export default {
 
     computed: {
         consumeMode() {
-            return store.state.studip.consumeMode;
+            return this.$store.state.studip.consumeMode;
         },
         ...mapGetters({
             courseware: 'courseware',
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogAdd.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogAdd.vue
index 235fa5219a7..c622daee0b2 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogAdd.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogAdd.vue
@@ -152,7 +152,7 @@
 
 <script>
 import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
-import StudipSelect from '../../StudipSelect.vue';
+import StudipSelect from '@/vue/components/base/StudipSelect.vue';
 import StudipWizardDialog from '../../StudipWizardDialog.vue';
 import colorMixin from '@/vue/mixins/courseware/colors.js';
 import wizardMixin from '@/vue/mixins/courseware/wizard.js';
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogCopy.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogCopy.vue
index 9652bfb64a2..935add5d648 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogCopy.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogCopy.vue
@@ -183,7 +183,7 @@
 import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
 import CoursewareStructuralElementSelector from './CoursewareStructuralElementSelector.vue';
 import colorMixin from '@/vue/mixins/courseware/colors.js';
-import StudipSelect from '../../StudipSelect.vue';
+import StudipSelect from '@/vue/components/base/StudipSelect.vue';
 import StudipWizardDialog from '../../StudipWizardDialog.vue';
 
 import { mapActions, mapGetters } from 'vuex'
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPermissions.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPermissions.vue
index fe44bee1ebd..f18d6acb1a7 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPermissions.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPermissions.vue
@@ -256,7 +256,7 @@
 
 <script>
 import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
-import Datepicker from './../../Datepicker.vue';
+import Datepicker from '@/vue/components/base/Datepicker.vue';
 import axios from 'axios';
 import { mapActions, mapGetters } from 'vuex';
 export default {
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPublicLink.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPublicLink.vue
index 37a32228f5c..b5e4572578d 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPublicLink.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogPublicLink.vue
@@ -24,7 +24,7 @@
     </studip-dialog>
 </template>
 <script>
-import Datepicker from './../../Datepicker.vue';
+import Datepicker from '@/vue/components/base/Datepicker.vue';
 import { mapActions } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/structural-element/PublicCoursewareStructuralElement.vue b/resources/vue/components/courseware/structural-element/PublicCoursewareStructuralElement.vue
index 7e5ef490235..9f77685c60a 100644
--- a/resources/vue/components/courseware/structural-element/PublicCoursewareStructuralElement.vue
+++ b/resources/vue/components/courseware/structural-element/PublicCoursewareStructuralElement.vue
@@ -80,7 +80,6 @@ import CoursewareCompanionOverlay from '../layouts/CoursewareCompanionOverlay.vu
 
 import { mapActions, mapGetters } from 'vuex';
 import ContentBar from "../../ContentBar.vue";
-import { store } from "../../../../assets/javascripts/chunks/vue";
 
 export default {
     name: 'public-courseware-structural-element',
@@ -104,7 +103,7 @@ export default {
 
     computed: {
         consumeMode() {
-          return store.state.studip.consumeMode;
+            return this.$store.state.studip.consumeMode;
         },
         ...mapGetters({
             courseware: 'courseware',
diff --git a/resources/vue/components/courseware/tasks/CoursewareDashboardStudents.vue b/resources/vue/components/courseware/tasks/CoursewareDashboardStudents.vue
index f38d908ee49..75549bbf63d 100644
--- a/resources/vue/components/courseware/tasks/CoursewareDashboardStudents.vue
+++ b/resources/vue/components/courseware/tasks/CoursewareDashboardStudents.vue
@@ -88,14 +88,13 @@
 </template>
 
 <script>
-import _ from 'lodash';
 import { mapActions, mapGetters } from 'vuex';
 import CompanionBox from '../layouts/CoursewareCompanionBox.vue';
 import CoursewareTasksDialogDistribute from './CoursewareTasksDialogDistribute.vue';
 import PeerReviewProcessStatus from './peer-review/ProcessStatus.vue';
-import StudipActionMenu from '../../StudipActionMenu.vue';
+import StudipActionMenu from '@/vue/components/base/StudipActionMenu.vue';
 import StudipDate from '../../StudipDate.vue';
-import StudipIcon from '../../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 import TaskGroupsAddSolversDialog from './TaskGroupsAddSolversDialog.vue';
 import TaskGroupsDeleteDialog from './TaskGroupsDeleteDialog.vue';
 import TaskGroupsModifyDeadlineDialog from './TaskGroupsModifyDeadlineDialog.vue';
diff --git a/resources/vue/components/courseware/tasks/CoursewareDashboardTasksList.vue b/resources/vue/components/courseware/tasks/CoursewareDashboardTasksList.vue
index 41ac8736bed..df3a316c0ef 100644
--- a/resources/vue/components/courseware/tasks/CoursewareDashboardTasksList.vue
+++ b/resources/vue/components/courseware/tasks/CoursewareDashboardTasksList.vue
@@ -132,9 +132,9 @@
 </template>
 <script>
 import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
-import StudipIcon from '../../StudipIcon.vue';
-import StudipActionMenu from '../../StudipActionMenu.vue';
-import StudipDialog from '../../StudipDialog.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
+import StudipActionMenu from '@/vue/components/base/StudipActionMenu.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 import taskHelperMixin from '../../../mixins/courseware/task-helper.js';
 import { mapActions, mapGetters } from 'vuex';
 
diff --git a/resources/vue/components/courseware/tasks/peer-review/AssessmentDialog.vue b/resources/vue/components/courseware/tasks/peer-review/AssessmentDialog.vue
index 1cac7949b71..17c82c56f5f 100644
--- a/resources/vue/components/courseware/tasks/peer-review/AssessmentDialog.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/AssessmentDialog.vue
@@ -40,7 +40,7 @@ import ResultsTypeFreetext from './assessment-types/results/Freetext.vue';
 import ResultsTypeTable from './assessment-types/results/Table.vue';
 import { getProcessStatus, ProcessStatus } from './definitions.ts';
 import CompanionBox from '../../layouts/CoursewareCompanionBox.vue';
-import StudipDialog from '../../../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/tasks/peer-review/AssessmentTypeEditorDialog.vue b/resources/vue/components/courseware/tasks/peer-review/AssessmentTypeEditorDialog.vue
index 679e0330140..7e28e25e569 100644
--- a/resources/vue/components/courseware/tasks/peer-review/AssessmentTypeEditorDialog.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/AssessmentTypeEditorDialog.vue
@@ -21,7 +21,7 @@
 import { mapGetters } from 'vuex';
 import EditorForm from './assessment-types/editors/EditorForm.vue';
 import EditorTable from './assessment-types/editors/EditorTable.vue';
-import StudipDialog from '../../../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 import { ASSESSMENT_TYPES } from './process-configuration';
 
 const getConfiguration = (process) => process?.attributes?.configuration ?? {};
diff --git a/resources/vue/components/courseware/tasks/peer-review/PairingEditor.vue b/resources/vue/components/courseware/tasks/peer-review/PairingEditor.vue
index 09ff0237c66..823644dca68 100644
--- a/resources/vue/components/courseware/tasks/peer-review/PairingEditor.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/PairingEditor.vue
@@ -89,9 +89,8 @@
 </template>
 
 <script>
-import _ from 'lodash';
 import { mapGetters } from 'vuex';
-import StudipIcon from '../../../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     components: { StudipIcon },
diff --git a/resources/vue/components/courseware/tasks/peer-review/PairingEditorDialog.vue b/resources/vue/components/courseware/tasks/peer-review/PairingEditorDialog.vue
index 29aef1c5434..cda5f663128 100644
--- a/resources/vue/components/courseware/tasks/peer-review/PairingEditorDialog.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/PairingEditorDialog.vue
@@ -22,7 +22,7 @@
 <script>
 import { mapGetters } from 'vuex';
 import PairingEditor from './PairingEditor.vue';
-import StudipDialog from '../../../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 import ProgressIndicator from '../../../StudipProgressIndicator.vue';
 
 const objId = ({ id, type }) => ({ id, type });
diff --git a/resources/vue/components/courseware/tasks/peer-review/ProcessConfiguration.vue b/resources/vue/components/courseware/tasks/peer-review/ProcessConfiguration.vue
index e973a5003a2..5955781ffbd 100644
--- a/resources/vue/components/courseware/tasks/peer-review/ProcessConfiguration.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/ProcessConfiguration.vue
@@ -23,7 +23,7 @@
 </template>
 
 <script>
-import { ProcessConfiguration, ASSESSMENT_TYPES } from './process-configuration';
+import { ASSESSMENT_TYPES } from './process-configuration';
 
 export default {
     props: {
diff --git a/resources/vue/components/courseware/tasks/peer-review/ProcessCreateDialog.vue b/resources/vue/components/courseware/tasks/peer-review/ProcessCreateDialog.vue
index 1615107cf83..2980e5b298a 100644
--- a/resources/vue/components/courseware/tasks/peer-review/ProcessCreateDialog.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/ProcessCreateDialog.vue
@@ -43,8 +43,8 @@
 import AssessmentTypeEditor from './AssessmentTypeEditor.vue';
 import CompanionBox from '../../layouts/CoursewareCompanionBox.vue';
 import ProcessCreateForm from './ProcessCreateForm.vue';
-import StudipDialog from '../../../StudipDialog.vue';
-import { defaultConfiguration, ProcessConfiguration } from './process-configuration';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
+import { defaultConfiguration } from './process-configuration';
 
 export default {
     components: { AssessmentTypeEditor, CompanionBox, ProcessCreateForm, StudipDialog },
diff --git a/resources/vue/components/courseware/tasks/peer-review/ProcessCreateForm.vue b/resources/vue/components/courseware/tasks/peer-review/ProcessCreateForm.vue
index 6685baa6963..71f28a6349e 100644
--- a/resources/vue/components/courseware/tasks/peer-review/ProcessCreateForm.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/ProcessCreateForm.vue
@@ -123,7 +123,7 @@
 <script>
 import LabelRequired from '../../../forms/LabelRequired.vue';
 import PeerReviewProcessConfiguration from './ProcessConfiguration.vue';
-import { ASSESSMENT_TYPES, CONFIGURATION_SETS, ProcessConfiguration } from './process-configuration';
+import { ASSESSMENT_TYPES, CONFIGURATION_SETS } from './process-configuration';
 
 let nextId = 0;
 
diff --git a/resources/vue/components/courseware/tasks/peer-review/ProcessDurationDialog.vue b/resources/vue/components/courseware/tasks/peer-review/ProcessDurationDialog.vue
index 46eae8f449e..0cd59178bdb 100644
--- a/resources/vue/components/courseware/tasks/peer-review/ProcessDurationDialog.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/ProcessDurationDialog.vue
@@ -40,7 +40,7 @@
 import { mapGetters } from 'vuex';
 import LabelRequired from '../../../forms/LabelRequired.vue';
 import StudipDate from '../../../StudipDate.vue';
-import StudipDialog from '../../../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 
 const midnight = (_date) => {
     const date = new Date(_date);
diff --git a/resources/vue/components/courseware/tasks/peer-review/ProcessEditDialog.vue b/resources/vue/components/courseware/tasks/peer-review/ProcessEditDialog.vue
index 751f9523641..0f88dcb1861 100644
--- a/resources/vue/components/courseware/tasks/peer-review/ProcessEditDialog.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/ProcessEditDialog.vue
@@ -20,9 +20,9 @@
 <script>
 import { mapGetters } from 'vuex';
 import { $gettext, $gettextInterpolate } from '../../../../../assets/javascripts/lib/gettext';
-import StudipDialog from '../../../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 import ProcessCreateForm from './ProcessCreateForm.vue';
-import { defaultConfiguration, ProcessConfiguration } from './process-configuration';
+import { defaultConfiguration } from './process-configuration';
 
 export default {
     components: { ProcessCreateForm, StudipDialog },
diff --git a/resources/vue/components/courseware/tasks/peer-review/ProcessStatus.vue b/resources/vue/components/courseware/tasks/peer-review/ProcessStatus.vue
index 666514d8aa7..4388d79f1e1 100644
--- a/resources/vue/components/courseware/tasks/peer-review/ProcessStatus.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/ProcessStatus.vue
@@ -11,7 +11,7 @@
     </span>
 </template>
 <script>
-import StudipIcon from '../../../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 import { getProcessStatus, ProcessStatus } from './definitions';
 
 export default {
diff --git a/resources/vue/components/courseware/tasks/peer-review/ProcessesList.vue b/resources/vue/components/courseware/tasks/peer-review/ProcessesList.vue
index 59c80ffa769..f7964e6de86 100644
--- a/resources/vue/components/courseware/tasks/peer-review/ProcessesList.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/ProcessesList.vue
@@ -72,7 +72,6 @@
 </template>
 
 <script>
-import _ from 'lodash';
 import { mapActions, mapGetters } from 'vuex';
 import CompanionBox from '../../layouts/CoursewareCompanionBox.vue';
 import ProcessStatusIcon from './ProcessStatus.vue';
diff --git a/resources/vue/components/courseware/tasks/peer-review/ResultDialog.vue b/resources/vue/components/courseware/tasks/peer-review/ResultDialog.vue
index 5dc28df0474..1186ae960c9 100644
--- a/resources/vue/components/courseware/tasks/peer-review/ResultDialog.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/ResultDialog.vue
@@ -18,7 +18,7 @@
 import ResultForm from './assessment-types/results/Form.vue';
 import ResultFreetext from './assessment-types/results/Freetext.vue';
 import ResultTable from './assessment-types/results/Table.vue';
-import StudipDialog from '../../../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/tasks/peer-review/assessment-types/editors/EditorForm.vue b/resources/vue/components/courseware/tasks/peer-review/assessment-types/editors/EditorForm.vue
index f6995b1cc1d..2138ecc5492 100644
--- a/resources/vue/components/courseware/tasks/peer-review/assessment-types/editors/EditorForm.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/assessment-types/editors/EditorForm.vue
@@ -60,12 +60,11 @@
     </CoursewareTabs>
 </template>
 <script>
-import StudipActionMenu from '../../../../../StudipActionMenu.vue';
+import StudipActionMenu from '@/vue/components/base/StudipActionMenu.vue';
 import StudipArticle from '../../../../../StudipArticle.vue';
 import LabelRequired from '../../../../../forms/LabelRequired.vue';
 import CoursewareTab from '../../../../layouts/CoursewareTab.vue';
 import CoursewareTabs from '../../../../layouts/CoursewareTabs.vue';
-import { EditorFormCriterium, FormAssessmentPayload } from '../../process-configuration';
 
 export default {
     components: { CoursewareTab, CoursewareTabs, LabelRequired, StudipActionMenu, StudipArticle },
diff --git a/resources/vue/components/courseware/tasks/peer-review/assessment-types/editors/EditorTable.vue b/resources/vue/components/courseware/tasks/peer-review/assessment-types/editors/EditorTable.vue
index 96511ad4f78..a30034ac881 100644
--- a/resources/vue/components/courseware/tasks/peer-review/assessment-types/editors/EditorTable.vue
+++ b/resources/vue/components/courseware/tasks/peer-review/assessment-types/editors/EditorTable.vue
@@ -58,7 +58,6 @@
 import LabelRequired from '../../../../../forms/LabelRequired.vue';
 import CoursewareTab from '../../../../layouts/CoursewareTab.vue';
 import CoursewareTabs from '../../../../layouts/CoursewareTabs.vue';
-import { EditorTableCriterium, TableAssessmentPayload } from '../../process-configuration';
 
 export default {
     components: { CoursewareTab, CoursewareTabs, LabelRequired },
diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue
index f8f392a7a86..80955c8c1d5 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue
@@ -93,7 +93,7 @@ import CoursewareCollapsibleBox from '../layouts/CoursewareCollapsibleBox.vue';
 import containerMixin from '@/vue/mixins/courseware/container.js';
 import clipboardMixin from '@/vue/mixins/courseware/clipboard.js';
 import draggable from 'vuedraggable';
-import StudipDialog from '../../StudipDialog.vue';
+import StudipDialog from '@/vue/components/base/StudipDialog.vue';
 
 import { mapActions, mapGetters } from 'vuex';
 
diff --git a/resources/vue/components/courseware/unit/CoursewareShelfDialogAdd.vue b/resources/vue/components/courseware/unit/CoursewareShelfDialogAdd.vue
index 36d4c566c7a..b5c5fa6ddaf 100644
--- a/resources/vue/components/courseware/unit/CoursewareShelfDialogAdd.vue
+++ b/resources/vue/components/courseware/unit/CoursewareShelfDialogAdd.vue
@@ -149,7 +149,7 @@
 import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
 import StockImageSelectableImageCard from '../../stock-images/SelectableImageCard.vue';
 import StockImageSelector from '../../stock-images/SelectorDialog.vue';
-import StudipSelect from '../../StudipSelect.vue';
+import StudipSelect from '@/vue/components/base/StudipSelect.vue';
 import StudipWizardDialog from '../../StudipWizardDialog.vue';
 import colorMixin from '@/vue/mixins/courseware/colors.js';
 import { mapActions, mapGetters } from 'vuex';
diff --git a/resources/vue/components/courseware/unit/CoursewareShelfDialogCopy.vue b/resources/vue/components/courseware/unit/CoursewareShelfDialogCopy.vue
index 0d17cc1841d..56d754107c5 100644
--- a/resources/vue/components/courseware/unit/CoursewareShelfDialogCopy.vue
+++ b/resources/vue/components/courseware/unit/CoursewareShelfDialogCopy.vue
@@ -168,7 +168,7 @@
 <script>
 import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
 import colorMixin from '@/vue/mixins/courseware/colors.js';
-import StudipSelect from '../../StudipSelect.vue';
+import StudipSelect from '@/vue/components/base/StudipSelect.vue';
 import StudipWizardDialog from '../../StudipWizardDialog.vue';
 
 import { mapActions, mapGetters } from 'vuex'
diff --git a/resources/vue/components/courseware/unit/CoursewareShelfDialogTopics.vue b/resources/vue/components/courseware/unit/CoursewareShelfDialogTopics.vue
index e105c319837..d5520c8eb2e 100644
--- a/resources/vue/components/courseware/unit/CoursewareShelfDialogTopics.vue
+++ b/resources/vue/components/courseware/unit/CoursewareShelfDialogTopics.vue
@@ -91,7 +91,7 @@
 import CoursewareCollapsibleBox from '../layouts/CoursewareCollapsibleBox.vue';
 import StockImageSelectableImageCard from '../../stock-images/SelectableImageCard.vue';
 import StockImageSelector from '../../stock-images/SelectorDialog.vue';
-import StudipSelect from '../../StudipSelect.vue';
+import StudipSelect from '@/vue/components/base/StudipSelect.vue';
 import colorMixin from '@/vue/mixins/courseware/colors.js';
 
 import { mapActions, mapGetters } from 'vuex';
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissions.vue b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissions.vue
index 88128f3815c..e19bab50746 100644
--- a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissions.vue
+++ b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogPermissions.vue
@@ -1,586 +1,586 @@
-<template>
-    <studip-dialog
-        :title="dialogTitle"
-        :confirm-text="$gettext('Speichern')"
-        confirm-class="accept"
-        :close-text="$gettext('Schließen')"
-        close-class="cancel"
-        :height="height"
-        :width="width"
-        @close="$emit('close')"
-        @confirm="storePermissions"
-    >
-        <template v-slot:dialogContent>
-            <div class="cw-permissions-form-wrapper">
-                <form class="default cw-permissions-form-radioset" @submit.prevent="">
-                    <div class="cw-radioset-wrapper" role="group" aria-labelledby="permission-type">
-                        <p class="sr-only" id="permission-type">{{ $gettext('Typ') }}</p>
-                        <div class="cw-radioset">
-                            <div class="cw-radioset-box" :class="[permissionType === 'all' ? 'selected' : '']">
-                                <input
-                                    type="radio"
-                                    id="permission-type-all"
-                                    value="all"
-                                    v-model="permissionType"
-                                    @change="updatePermissionType"
-                                />
-                                <label for="permission-type-all">
-                                    <div
-                                        class="label-icon all"
-                                        :class="[permissionType === 'all' ? 'selected' : '']"
-                                    ></div>
-                                    <div class="label-text">
-                                        <span>{{ $gettext('alle Studierenden') }}</span>
-                                    </div>
-                                </label>
-                            </div>
-                            <div class="cw-radioset-box" :class="[permissionType === 'users' ? 'selected' : '']">
-                                <input
-                                    type="radio"
-                                    id="permission-type-users"
-                                    value="users"
-                                    v-model="permissionType"
-                                    @change="updatePermissionType"
-                                />
-                                <label for="permission-type-users">
-                                    <div
-                                        class="label-icon users"
-                                        :class="[permissionType === 'users' ? 'selected' : '']"
-                                    ></div>
-                                    <div class="label-text">
-                                        <span>{{ $gettext('ausgewählte Studierende') }}</span>
-                                    </div>
-                                </label>
-                            </div>
-                            <div class="cw-radioset-box" :class="[permissionType === 'groups' ? 'selected' : '']">
-                                <input
-                                    type="radio"
-                                    id="permission-type-groups"
-                                    value="groups"
-                                    v-model="permissionType"
-                                    @change="updatePermissionType"
-                                />
-                                <label for="permission-type-groups">
-                                    <div
-                                        class="label-icon groups"
-                                        :class="[permissionType === 'groups' ? 'selected' : '']"
-                                    ></div>
-                                    <div class="label-text">
-                                        <span>{{ $gettext('Gruppen') }}</span>
-                                    </div>
-                                </label>
-                            </div>
-                        </div>
-                    </div>
-                </form>
-
-                <form class="default cw-form-selects" @submit.prevent="">
-                    <div class="cw-form-selects-row">
-                        <label>
-                            {{ $gettext('Sichtbar') }}
-                            <select v-model="visible" @change="updateVisibile">
-                                <option value="always">{{ $gettext('Immer') }}</option>
-                                <option value="period">{{ $gettext('Zeitraum') }}</option>
-                                <option v-if="permissionType === 'all'" value="never">{{ $gettext('Nie') }}</option>
-                            </select>
-                        </label>
-                        <template v-if="visible === 'period'">
-                            <label>
-                                {{ $gettext('von') }}
-                                <datepicker v-model="visibleStartDate" :placeholder="$gettext('unbegrenzt')" />
-                            </label>
-                            <label>
-                                {{ $gettext('bis') }}
-                                <datepicker v-model="visibleEndDate" :placeholder="$gettext('unbegrenzt')" />
-                            </label>
-                        </template>
-                    </div>
-                    <div class="cw-form-selects-row">
-                        <label
-                            >{{ $gettext('Bearbeitbar') }}
-                            <select v-model="writable" @change="updateWritable">
-                                <option v-if="permissionType === 'all'" value="never">{{ $gettext('Nie') }}</option>
-                                <option value="always">{{ $gettext('Immer') }}</option>
-                                <option value="period">{{ $gettext('Zeitraum') }}</option>
-                            </select>
-                        </label>
-                        <template v-if="writable === 'period'">
-                            <div>
-                                <label>
-                                    {{ $gettext('von') }}
-                                    <datepicker v-model="writableStartDate" :placeholder="$gettext('unbegrenzt')" />
-                                </label>
-                            </div>
-                            <div>
-                                <label>
-                                    {{ $gettext('bis') }}
-                                    <datepicker v-model="writableEndDate" :placeholder="$gettext('unbegrenzt')" />
-                                </label>
-                            </div>
-                        </template>
-                    </div>
-                </form>
-            </div>
-            <div v-if="permissionType === 'all'" class="cw-contents-overview-teaser">
-                <div class="cw-contents-overview-teaser-content">
-                    <header>{{ $gettext('Rechte und Sichtbarkeit') }}</header>
-                    <p>
-                        {{
-                            $gettext(
-                                'Hier stellen Sie für das gesamte Lernmaterial ein, welche Teilnehmenden aus Ihrer Veranstaltung alle Coursewareseiten dieses Materials sehen bzw. bearbeiten können. Falls Sie für Ihr Lehrszenario eine feinere Einstellungsmöglichkeit benötigen, können Sie direkt im Lernmaterial die „Rechte und Sichtbarkeit“ an den einzelnen Seiten einstellen.'
-                            )
-                        }}
-                    </p>
-                    <p>
-                        {{
-                            $gettext(
-                                'Entscheiden Sie sich zunächst ob „alle Studierende“ die gleichen Rechte erhalten sollen, oder ob „einzelne Studierende“ oder zuvor erstellte „Gruppen“ unterschiedliche Rechte benötigen.  Die Einstellung „einzelne Studierende“ oder „Gruppen“ bietet sich beispielsweise dann an, wenn Sie eine Courseware von einer Kleingruppe bearbeiten lassen wollen. Anschließend können Sie einstellen, in welchem Zeitraum diese Rechte gelten.'
-                            )
-                        }}
-                    </p>
-                </div>
-            </div>
-
-            <table v-if="permissionType === 'users'" class="default permission-table">
-                <caption>
-                    {{ $gettext('Studierende') }}
-                </caption>
-                <thead>
-                    <tr>
-                        <th>{{ $gettext('Name') }}</th>
-                        <th>
-                            {{ $gettext('Sichtbar') }}
-                            <input type="checkbox" v-model="visibleAll" @change="updatewritableAll" />
-                        </th>
-                        <th>
-                            {{ $gettext('Bearbeitbar') }}
-                            <input type="checkbox" v-model="writableAll" @change="updateVisibleAll" />
-                        </th>
-                    </tr>
-                </thead>
-                <tbody>
-                    <tr v-if="autorMembers.length === 0">
-                        <td colspan="3">{{ $gettext('Es wurden keine Einträge gefunden.') }}</td>
-                    </tr>
-                    <tr v-for="autor in autorMembers" :key="autor.id">
-                        <td>{{ autor.formattedname }}</td>
-                        <td>
-                            <input
-                                v-if="!visibleAll"
-                                type="checkbox"
-                                :value="autor.id"
-                                v-model="visibleApprovalUsers"
-                                @change="updateUserVisible(autor)"
-                            />
-                            <studip-icon v-else shape="accept" role="info" :size="14" />
-                        </td>
-                        <td>
-                            <input
-                                v-if="!writableAll"
-                                type="checkbox"
-                                :value="autor.id"
-                                v-model="writableApprovalUsers"
-                                @change="updateUserWritable(autor)"
-                            />
-                            <studip-icon v-else shape="accept" role="info" :size="14" />
-                        </td>
-                    </tr>
-                </tbody>
-            </table>
-            <template v-if="permissionType === 'groups'">
-                <table v-if="groups.length > 0" class="default permission-table">
-                    <caption>
-                        {{ $gettext('Gruppen') }}
-                    </caption>
-                    <thead>
-                        <tr>
-                            <th>{{ $gettext('Name') }}</th>
-                            <th>
-                                {{ $gettext('Sichtbar') }}
-                                <input type="checkbox" v-model="visibleAll" :disabled="writableAll" />
-                            </th>
-                            <th>
-                                {{ $gettext('Bearbeitbar') }}
-                                <input type="checkbox" v-model="writableAll" @change="updateVisibleAll" />
-                            </th>
-                        </tr>
-                    </thead>
-                    <tbody>
-                        <tr v-if="groups.length === 0">
-                            <td colspan="3">{{ $gettext('Es wurden keine Einträge gefunden.') }}</td>
-                        </tr>
-                        <tr v-for="group in groups" :key="group.id">
-                            <td>{{ group.name }}</td>
-                            <td>
-                                <input
-                                    v-if="!visibleAll"
-                                    type="checkbox"
-                                    :value="group.id"
-                                    v-model="visibleApprovalGroups"
-                                    @change="updateGroupVisible(group)"
-                                />
-                                <studip-icon v-else shape="accept" role="info" :size="14" />
-                            </td>
-                            <td>
-                                <input
-                                    v-if="!writableAll"
-                                    type="checkbox"
-                                    :value="group.id"
-                                    v-model="writableApprovalGroups"
-                                    @change="updateGroupWritable(group)"
-                                />
-                                <studip-icon v-else shape="accept" role="info" :size="14" />
-                            </td>
-                        </tr>
-                    </tbody>
-                </table>
-                <courseware-companion-box
-                    v-else
-                    :msgCompanion="$gettext('Sie haben noch keine Gruppen erstellt. Mit Gruppen können Sie die Sichtbarkeits- und Bearbeitungsrechte anschließend besonders unkompliziert an Arbeitsgruppen vergeben.')"
-                    mood="pointing"
-                >
-                    <template #companionActions>
-                        <a :href="statusGroupsUrl"><button class="button">{{ $gettext('Zu den Gruppen der Veranstaltung') }}</button></a>
-                    </template>
-                </courseware-companion-box>
-            </template>
-        </template>
-    </studip-dialog>
-</template>
-<script>
-import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
-import Datepicker from './../../Datepicker.vue';
-import axios from 'axios';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
-    name: 'courseware-unit-item-dialog-permissions',
-    emits: ['close'],
-    components: {
-        CoursewareCompanionBox,
-        Datepicker,
-    },
-    props: {
-        unit: {
-            type: Object,
-            required: true,
-        },
-        unitName: {
-            type: String,
-            required: true
-        }
-    },
-    data() {
-        return {
-            permissionType: 'all',
-            visible: 'always',
-            visibleAll: false,
-            visibleApprovalUsers: [],
-            visibleApprovalGroups: [],
-            visibleStartDate: null,
-            visibleEndDate: null,
-            writable: 'never',
-            writableAll: false,
-            writableStartDate: null,
-            writableEndDate: null,
-            writableApprovalUsers: [],
-            writableApprovalGroups: [],
-            height: '680',
-            width: '870',
-            currentSemester: null,
-        };
-    },
-    computed: {
-        ...mapGetters({
-            context: 'context',
-            relatedCourseMemberships: 'course-memberships/related',
-            relatedCourseStatusGroups: 'status-groups/related',
-            relatedUser: 'users/related',
-        }),
-        users() {
-            const parent = { type: 'courses', id: this.context.id };
-            const relationship = 'memberships';
-            const memberships = this.relatedCourseMemberships({ parent, relationship });
-
-            return (
-                memberships?.map((membership) => {
-                    const parent = { type: membership.type, id: membership.id };
-                    const member = this.relatedUser({ parent, relationship: 'user' });
-
-                    return {
-                        id: member.id,
-                        formattedname: member.attributes['formatted-name'],
-                        username: member.attributes['username'],
-                        perm: membership.attributes['permission'],
-                    };
-                }) ?? []
-            );
-        },
-        statusGroupsUrl() {
-            return STUDIP.URLHelper.getURL('dispatch.php/course/statusgroups');
-        },
-        autorMembers() {
-            if (Object.keys(this.users).length === 0 && this.users.constructor === Object) {
-                return [];
-            }
-
-            let members = this.users.filter(function (user) {
-                return user.perm === 'autor';
-            });
-
-            return members;
-        },
-        groups() {
-            const parent = { type: 'courses', id: this.context.id };
-            const relationship = 'status-groups';
-            const statusGroups = this.relatedCourseStatusGroups({ parent, relationship });
-
-            return (
-                statusGroups?.map((statusGroup) => {
-                    return {
-                        id: statusGroup.id,
-                        name: statusGroup.attributes['name'],
-                    };
-                }) ?? []
-            );
-        },
-        periodsValid() {
-            if (this.writable !== 'period' || this.visible !== 'period') {
-                return true;
-            }
-            return this.visibleStartDate <= this.writableStartDate
-                && (
-                     this.visibleEndDate === null
-                     || this.visibleEndDate >= this.writableEndDate
-                );
-        },
-        semesterDates() {
-            const date = Date.now() / 1000;
-            let startDate = date;
-            let endDate = date;
-            if (this.currentSemester) {
-                startDate = new Date(this.currentSemester.attributes.start).getTime() / 1000;
-                endDate = new Date(this.currentSemester.attributes.end).getTime() / 1000;
-            }
-
-            return { start: startDate, end: endDate };
-        },
-        dialogTitle() {
-            return this.$gettext('Rechte und Sichtbarkeit') + ': ' + this.unitName;
-        }
-    },
-    methods: {
-        ...mapActions({
-            loadCourseMemberships: 'course-memberships/loadRelated',
-            loadCourseStatusGroups: 'status-groups/loadRelated',
-            updateUnit: 'courseware-units/update',
-            loadUnit: 'courseware-units/loadById',
-            companionWarning: 'companionWarning',
-        }),
-        setDimensions() {
-            this.height = Math.min((window.innerHeight * 0.8).toFixed(0), 680).toString();
-            this.width = Math.min(window.innerWidth * 0.8, 870).toFixed(0);
-        },
-        initData() {
-            this.permissionType = this.unit.attributes['permission-type'];
-            this.visible = this.unit.attributes['visible'];
-            this.visibleAll = this.unit.attributes['visible-all'];
-            this.visibleStartDate = this.unit.attributes['visible-start-date']
-                ? new Date(this.unit.attributes['visible-start-date']).getTime() / 1000
-                : null;
-            this.visibleEndDate = this.unit.attributes['visible-end-date']
-                ? new Date(this.unit.attributes['visible-end-date']).getTime() / 1000
-                : null;
-            this.writable = this.unit.attributes['writable'];
-            this.writableAll = this.unit.attributes['writable-all'];
-            this.writableStartDate = this.unit.attributes['writable-start-date']
-                ? new Date(this.unit.attributes['writable-start-date']).getTime() / 1000
-                : null;
-            this.writableEndDate = this.unit.attributes['writable-end-date']
-                ? new Date(this.unit.attributes['writable-end-date']).getTime() / 1000
-                : null;
-            if (this.permissionType === 'users') {
-                this.visibleApprovalUsers = this.unit.attributes['visible-approval'];
-                this.writableApprovalUsers = this.unit.attributes['writable-approval'];
-            }
-            if (this.permissionType === 'groups') {
-                this.visibleApprovalGroups = this.unit.attributes['visible-approval'];
-                this.writableApprovalGroups = this.unit.attributes['writable-approval'];
-            }
-
-            axios
-                .get(STUDIP.URLHelper.getURL('jsonapi.php/v1/semesters', { 'filter[current]': true }, true))
-                .then((response) => {
-                    this.currentSemester = response.data.data[0];
-                })
-                .catch(() => {
-                    this.currentSemester = null;
-                });
-        },
-        async storePermissions() {
-            let visibleApproval = [];
-            let writableApproval = [];
-            if (this.permissionType === 'users') {
-                visibleApproval = this.visibleApprovalUsers;
-                writableApproval = this.writableApprovalUsers;
-            }
-            if (this.permissionType === 'groups') {
-                visibleApproval = this.visibleApprovalGroups;
-                writableApproval = this.writableApprovalGroups;
-            }
-
-            if (this.visible === 'period' && this.visibleStartDate === null && this.visibleEndDate === null) {
-                this.visible = 'always';
-            }
-
-            if (this.writable === 'period' && this.writableStartDate === null && this.writableEndDate === null) {
-                this.visible = 'always';
-            }
-
-            if (
-                this.visible === 'period' &&
-                this.visibleStartDate !== null &&
-                this.visibleEndDate !== null &&
-                this.visibleStartDate > this.visibleEndDate
-            ) {
-                this.companionWarning({
-                    info: this.$gettext(
-                        'Das Enddatum des Sichtbarkeitszeitraums darf nicht vor dem Startdatum liegen.'
-                    ),
-                });
-                return false;
-            }
-
-            if (
-                this.writable === 'period' &&
-                this.writableStartDate !== null &&
-                this.writableEndDate !== null &&
-                this.writableStartDate > this.writableEndDate
-            ) {
-                this.companionWarning({
-                    info: this.$gettext('Das Enddatum des Bearbeitungszeitraums darf nicht vor dem Startdatum liegen.'),
-                });
-                return false;
-            }
-
-            if (!this.periodsValid) {
-                this.companionWarning({
-                    info: this.$gettext('Der Bearbeitungszeitraum muss innerhalb des Sichtbarkeitszeitraums liegen.'),
-                });
-                return false;
-            }
-
-            const unit = {
-                id: this.unit.id,
-                type: 'courseware-units',
-                attributes: {
-                    'permission-scope': 'unit',
-                    'permission-type': this.permissionType,
-                    visible: this.visible,
-                    'visible-all': this.visibleAll && this.permissionType !== 'all' ? 1 : 0,
-                    'visible-start-date':
-                        this.visible === 'period' ? new Date(this.visibleStartDate * 1000).toISOString() : null,
-                    'visible-end-date':
-                        this.visible === 'period' ? new Date(this.visibleEndDate * 1000).toISOString() : null,
-                    'visible-approval': JSON.stringify(visibleApproval),
-                    writable: this.writable,
-                    'writable-all': this.writableAll && this.permissionType !== 'all' ? 1 : 0,
-                    'writable-start-date':
-                        this.writable === 'period' ? new Date(this.writableStartDate * 1000).toISOString() : null,
-                    'writable-end-date':
-                        this.writable === 'period' ? new Date(this.writableEndDate * 1000).toISOString() : null,
-                    'writable-approval': JSON.stringify(writableApproval),
-                },
-            };
-            this.$emit('close');
-            await this.updateUnit(unit);
-            await this.loadUnit({ id: this.unit.id });
-        },
-
-        updatePermissionType() {
-            if (this.permissionType !== 'all') {
-                if (this.visible === 'never') {
-                    this.visible = 'always';
-                }
-                if (this.writable === 'never') {
-                    this.writable = 'always';
-                }
-            } else {
-                if (this.writable === 'always') {
-                    this.writable = 'never';
-                }
-            }
-        },
-        updateVisibile() {
-            if (this.visible === 'never' && this.permissionType === 'all') {
-                this.writable = 'never';
-            }
-            if (this.visible === 'period') {
-                if (this.writable === 'always') {
-                    this.writable = 'period';
-                    this.writableStartDate = this.writableStartDate ?? this.semesterDates.start;
-                    this.writableEndDate = this.writableEndDate ?? this.semesterDates.end;
-                }
-
-                this.visibleStartDate = this.visibleStartDate ?? this.semesterDates.start;
-                this.visibleEndDate = this.visibleEndDate ?? this.semesterDates.end;
-            }
-        },
-        updateWritable() {
-            if (this.writable === 'always') {
-                this.visible = 'always';
-            }
-            if (this.writable === 'period' && this.permissionType === 'all' && this.visible !== 'always') {
-                this.visible = 'period';
-                this.visibleStartDate = this.visibleStartDate ?? this.semesterDates.start;
-                this.visibleEndDate = this.visibleEndDate ?? this.semesterDates.end;
-            }
-            if (this.writable === 'period') {
-                this.writableStartDate = this.writableStartDate ?? this.semesterDates.start;
-                this.writableEndDate = this.writableEndDate ?? this.semesterDates.end;
-            }
-        },
-        updateUserWritable(user) {
-            if (this.writableApprovalUsers.includes(user.id) && !this.visibleApprovalUsers.includes(user.id)) {
-                this.visibleApprovalUsers.push(user.id);
-            }
-        },
-        updateUserVisible(user) {
-            if (this.writableApprovalUsers.includes(user.id) && !this.visibleApprovalUsers.includes(user.id)) {
-                this.writableApprovalUsers = this.writableApprovalUsers.filter((id) => id !== user.id);
-            }
-        },
-        updateGroupWritable(group) {
-            if (this.writableApprovalGroups.includes(group.id) && !this.visibleApprovalGroups.includes(group.id)) {
-                this.visibleApprovalGroups.push(group.id);
-            }
-        },
-        updateGroupVisible(group) {
-            if (this.writableApprovalGroups.includes(group.id) && !this.visibleApprovalGroups.includes(group.id)) {
-                this.writableApprovalGroups = this.writableApprovalGroups.filter((id) => id !== group.id);
-            }
-        },
-        updateVisibleAll() {
-            if (this.writableAll) {
-                this.visibleAll = true;
-            }
-        },
-        updatewritableAll() {
-            if (!this.visibleAll) {
-                this.writableAll = false;
-            }
-        },
-    },
-    mounted() {
-        this.setDimensions();
-        this.initData();
-        const parent = { type: 'courses', id: this.context.id };
-        let options = {
-            include: 'user',
-            'page[limit]': 10000,
-        };
-        this.loadCourseMemberships({ parent, relationship: 'memberships', options: options });
-        this.loadCourseStatusGroups({ parent, relationship: 'status-groups' });
-    },
-};
-</script>
+<template>
+    <studip-dialog
+        :title="dialogTitle"
+        :confirm-text="$gettext('Speichern')"
+        confirm-class="accept"
+        :close-text="$gettext('Schließen')"
+        close-class="cancel"
+        :height="height"
+        :width="width"
+        @close="$emit('close')"
+        @confirm="storePermissions"
+    >
+        <template v-slot:dialogContent>
+            <div class="cw-permissions-form-wrapper">
+                <form class="default cw-permissions-form-radioset" @submit.prevent="">
+                    <div class="cw-radioset-wrapper" role="group" aria-labelledby="permission-type">
+                        <p class="sr-only" id="permission-type">{{ $gettext('Typ') }}</p>
+                        <div class="cw-radioset">
+                            <div class="cw-radioset-box" :class="[permissionType === 'all' ? 'selected' : '']">
+                                <input
+                                    type="radio"
+                                    id="permission-type-all"
+                                    value="all"
+                                    v-model="permissionType"
+                                    @change="updatePermissionType"
+                                />
+                                <label for="permission-type-all">
+                                    <div
+                                        class="label-icon all"
+                                        :class="[permissionType === 'all' ? 'selected' : '']"
+                                    ></div>
+                                    <div class="label-text">
+                                        <span>{{ $gettext('alle Studierenden') }}</span>
+                                    </div>
+                                </label>
+                            </div>
+                            <div class="cw-radioset-box" :class="[permissionType === 'users' ? 'selected' : '']">
+                                <input
+                                    type="radio"
+                                    id="permission-type-users"
+                                    value="users"
+                                    v-model="permissionType"
+                                    @change="updatePermissionType"
+                                />
+                                <label for="permission-type-users">
+                                    <div
+                                        class="label-icon users"
+                                        :class="[permissionType === 'users' ? 'selected' : '']"
+                                    ></div>
+                                    <div class="label-text">
+                                        <span>{{ $gettext('ausgewählte Studierende') }}</span>
+                                    </div>
+                                </label>
+                            </div>
+                            <div class="cw-radioset-box" :class="[permissionType === 'groups' ? 'selected' : '']">
+                                <input
+                                    type="radio"
+                                    id="permission-type-groups"
+                                    value="groups"
+                                    v-model="permissionType"
+                                    @change="updatePermissionType"
+                                />
+                                <label for="permission-type-groups">
+                                    <div
+                                        class="label-icon groups"
+                                        :class="[permissionType === 'groups' ? 'selected' : '']"
+                                    ></div>
+                                    <div class="label-text">
+                                        <span>{{ $gettext('Gruppen') }}</span>
+                                    </div>
+                                </label>
+                            </div>
+                        </div>
+                    </div>
+                </form>
+
+                <form class="default cw-form-selects" @submit.prevent="">
+                    <div class="cw-form-selects-row">
+                        <label>
+                            {{ $gettext('Sichtbar') }}
+                            <select v-model="visible" @change="updateVisibile">
+                                <option value="always">{{ $gettext('Immer') }}</option>
+                                <option value="period">{{ $gettext('Zeitraum') }}</option>
+                                <option v-if="permissionType === 'all'" value="never">{{ $gettext('Nie') }}</option>
+                            </select>
+                        </label>
+                        <template v-if="visible === 'period'">
+                            <label>
+                                {{ $gettext('von') }}
+                                <datepicker v-model="visibleStartDate" :placeholder="$gettext('unbegrenzt')" />
+                            </label>
+                            <label>
+                                {{ $gettext('bis') }}
+                                <datepicker v-model="visibleEndDate" :placeholder="$gettext('unbegrenzt')" />
+                            </label>
+                        </template>
+                    </div>
+                    <div class="cw-form-selects-row">
+                        <label
+                            >{{ $gettext('Bearbeitbar') }}
+                            <select v-model="writable" @change="updateWritable">
+                                <option v-if="permissionType === 'all'" value="never">{{ $gettext('Nie') }}</option>
+                                <option value="always">{{ $gettext('Immer') }}</option>
+                                <option value="period">{{ $gettext('Zeitraum') }}</option>
+                            </select>
+                        </label>
+                        <template v-if="writable === 'period'">
+                            <div>
+                                <label>
+                                    {{ $gettext('von') }}
+                                    <datepicker v-model="writableStartDate" :placeholder="$gettext('unbegrenzt')" />
+                                </label>
+                            </div>
+                            <div>
+                                <label>
+                                    {{ $gettext('bis') }}
+                                    <datepicker v-model="writableEndDate" :placeholder="$gettext('unbegrenzt')" />
+                                </label>
+                            </div>
+                        </template>
+                    </div>
+                </form>
+            </div>
+            <div v-if="permissionType === 'all'" class="cw-contents-overview-teaser">
+                <div class="cw-contents-overview-teaser-content">
+                    <header>{{ $gettext('Rechte und Sichtbarkeit') }}</header>
+                    <p>
+                        {{
+                            $gettext(
+                                'Hier stellen Sie für das gesamte Lernmaterial ein, welche Teilnehmenden aus Ihrer Veranstaltung alle Coursewareseiten dieses Materials sehen bzw. bearbeiten können. Falls Sie für Ihr Lehrszenario eine feinere Einstellungsmöglichkeit benötigen, können Sie direkt im Lernmaterial die „Rechte und Sichtbarkeit“ an den einzelnen Seiten einstellen.'
+                            )
+                        }}
+                    </p>
+                    <p>
+                        {{
+                            $gettext(
+                                'Entscheiden Sie sich zunächst ob „alle Studierende“ die gleichen Rechte erhalten sollen, oder ob „einzelne Studierende“ oder zuvor erstellte „Gruppen“ unterschiedliche Rechte benötigen.  Die Einstellung „einzelne Studierende“ oder „Gruppen“ bietet sich beispielsweise dann an, wenn Sie eine Courseware von einer Kleingruppe bearbeiten lassen wollen. Anschließend können Sie einstellen, in welchem Zeitraum diese Rechte gelten.'
+                            )
+                        }}
+                    </p>
+                </div>
+            </div>
+
+            <table v-if="permissionType === 'users'" class="default permission-table">
+                <caption>
+                    {{ $gettext('Studierende') }}
+                </caption>
+                <thead>
+                    <tr>
+                        <th>{{ $gettext('Name') }}</th>
+                        <th>
+                            {{ $gettext('Sichtbar') }}
+                            <input type="checkbox" v-model="visibleAll" @change="updatewritableAll" />
+                        </th>
+                        <th>
+                            {{ $gettext('Bearbeitbar') }}
+                            <input type="checkbox" v-model="writableAll" @change="updateVisibleAll" />
+                        </th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <tr v-if="autorMembers.length === 0">
+                        <td colspan="3">{{ $gettext('Es wurden keine Einträge gefunden.') }}</td>
+                    </tr>
+                    <tr v-for="autor in autorMembers" :key="autor.id">
+                        <td>{{ autor.formattedname }}</td>
+                        <td>
+                            <input
+                                v-if="!visibleAll"
+                                type="checkbox"
+                                :value="autor.id"
+                                v-model="visibleApprovalUsers"
+                                @change="updateUserVisible(autor)"
+                            />
+                            <studip-icon v-else shape="accept" role="info" :size="14" />
+                        </td>
+                        <td>
+                            <input
+                                v-if="!writableAll"
+                                type="checkbox"
+                                :value="autor.id"
+                                v-model="writableApprovalUsers"
+                                @change="updateUserWritable(autor)"
+                            />
+                            <studip-icon v-else shape="accept" role="info" :size="14" />
+                        </td>
+                    </tr>
+                </tbody>
+            </table>
+            <template v-if="permissionType === 'groups'">
+                <table v-if="groups.length > 0" class="default permission-table">
+                    <caption>
+                        {{ $gettext('Gruppen') }}
+                    </caption>
+                    <thead>
+                        <tr>
+                            <th>{{ $gettext('Name') }}</th>
+                            <th>
+                                {{ $gettext('Sichtbar') }}
+                                <input type="checkbox" v-model="visibleAll" :disabled="writableAll" />
+                            </th>
+                            <th>
+                                {{ $gettext('Bearbeitbar') }}
+                                <input type="checkbox" v-model="writableAll" @change="updateVisibleAll" />
+                            </th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr v-if="groups.length === 0">
+                            <td colspan="3">{{ $gettext('Es wurden keine Einträge gefunden.') }}</td>
+                        </tr>
+                        <tr v-for="group in groups" :key="group.id">
+                            <td>{{ group.name }}</td>
+                            <td>
+                                <input
+                                    v-if="!visibleAll"
+                                    type="checkbox"
+                                    :value="group.id"
+                                    v-model="visibleApprovalGroups"
+                                    @change="updateGroupVisible(group)"
+                                />
+                                <studip-icon v-else shape="accept" role="info" :size="14" />
+                            </td>
+                            <td>
+                                <input
+                                    v-if="!writableAll"
+                                    type="checkbox"
+                                    :value="group.id"
+                                    v-model="writableApprovalGroups"
+                                    @change="updateGroupWritable(group)"
+                                />
+                                <studip-icon v-else shape="accept" role="info" :size="14" />
+                            </td>
+                        </tr>
+                    </tbody>
+                </table>
+                <courseware-companion-box
+                    v-else
+                    :msgCompanion="$gettext('Sie haben noch keine Gruppen erstellt. Mit Gruppen können Sie die Sichtbarkeits- und Bearbeitungsrechte anschließend besonders unkompliziert an Arbeitsgruppen vergeben.')"
+                    mood="pointing"
+                >
+                    <template #companionActions>
+                        <a :href="statusGroupsUrl"><button class="button">{{ $gettext('Zu den Gruppen der Veranstaltung') }}</button></a>
+                    </template>
+                </courseware-companion-box>
+            </template>
+        </template>
+    </studip-dialog>
+</template>
+<script>
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import Datepicker from '@/vue/components/base/Datepicker.vue';
+import axios from 'axios';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-unit-item-dialog-permissions',
+    emits: ['close'],
+    components: {
+        CoursewareCompanionBox,
+        Datepicker,
+    },
+    props: {
+        unit: {
+            type: Object,
+            required: true,
+        },
+        unitName: {
+            type: String,
+            required: true
+        }
+    },
+    data() {
+        return {
+            permissionType: 'all',
+            visible: 'always',
+            visibleAll: false,
+            visibleApprovalUsers: [],
+            visibleApprovalGroups: [],
+            visibleStartDate: null,
+            visibleEndDate: null,
+            writable: 'never',
+            writableAll: false,
+            writableStartDate: null,
+            writableEndDate: null,
+            writableApprovalUsers: [],
+            writableApprovalGroups: [],
+            height: '680',
+            width: '870',
+            currentSemester: null,
+        };
+    },
+    computed: {
+        ...mapGetters({
+            context: 'context',
+            relatedCourseMemberships: 'course-memberships/related',
+            relatedCourseStatusGroups: 'status-groups/related',
+            relatedUser: 'users/related',
+        }),
+        users() {
+            const parent = { type: 'courses', id: this.context.id };
+            const relationship = 'memberships';
+            const memberships = this.relatedCourseMemberships({ parent, relationship });
+
+            return (
+                memberships?.map((membership) => {
+                    const parent = { type: membership.type, id: membership.id };
+                    const member = this.relatedUser({ parent, relationship: 'user' });
+
+                    return {
+                        id: member.id,
+                        formattedname: member.attributes['formatted-name'],
+                        username: member.attributes['username'],
+                        perm: membership.attributes['permission'],
+                    };
+                }) ?? []
+            );
+        },
+        statusGroupsUrl() {
+            return STUDIP.URLHelper.getURL('dispatch.php/course/statusgroups');
+        },
+        autorMembers() {
+            if (Object.keys(this.users).length === 0 && this.users.constructor === Object) {
+                return [];
+            }
+
+            let members = this.users.filter(function (user) {
+                return user.perm === 'autor';
+            });
+
+            return members;
+        },
+        groups() {
+            const parent = { type: 'courses', id: this.context.id };
+            const relationship = 'status-groups';
+            const statusGroups = this.relatedCourseStatusGroups({ parent, relationship });
+
+            return (
+                statusGroups?.map((statusGroup) => {
+                    return {
+                        id: statusGroup.id,
+                        name: statusGroup.attributes['name'],
+                    };
+                }) ?? []
+            );
+        },
+        periodsValid() {
+            if (this.writable !== 'period' || this.visible !== 'period') {
+                return true;
+            }
+            return this.visibleStartDate <= this.writableStartDate
+                && (
+                     this.visibleEndDate === null
+                     || this.visibleEndDate >= this.writableEndDate
+                );
+        },
+        semesterDates() {
+            const date = Date.now() / 1000;
+            let startDate = date;
+            let endDate = date;
+            if (this.currentSemester) {
+                startDate = new Date(this.currentSemester.attributes.start).getTime() / 1000;
+                endDate = new Date(this.currentSemester.attributes.end).getTime() / 1000;
+            }
+
+            return { start: startDate, end: endDate };
+        },
+        dialogTitle() {
+            return this.$gettext('Rechte und Sichtbarkeit') + ': ' + this.unitName;
+        }
+    },
+    methods: {
+        ...mapActions({
+            loadCourseMemberships: 'course-memberships/loadRelated',
+            loadCourseStatusGroups: 'status-groups/loadRelated',
+            updateUnit: 'courseware-units/update',
+            loadUnit: 'courseware-units/loadById',
+            companionWarning: 'companionWarning',
+        }),
+        setDimensions() {
+            this.height = Math.min((window.innerHeight * 0.8).toFixed(0), 680).toString();
+            this.width = Math.min(window.innerWidth * 0.8, 870).toFixed(0);
+        },
+        initData() {
+            this.permissionType = this.unit.attributes['permission-type'];
+            this.visible = this.unit.attributes['visible'];
+            this.visibleAll = this.unit.attributes['visible-all'];
+            this.visibleStartDate = this.unit.attributes['visible-start-date']
+                ? new Date(this.unit.attributes['visible-start-date']).getTime() / 1000
+                : null;
+            this.visibleEndDate = this.unit.attributes['visible-end-date']
+                ? new Date(this.unit.attributes['visible-end-date']).getTime() / 1000
+                : null;
+            this.writable = this.unit.attributes['writable'];
+            this.writableAll = this.unit.attributes['writable-all'];
+            this.writableStartDate = this.unit.attributes['writable-start-date']
+                ? new Date(this.unit.attributes['writable-start-date']).getTime() / 1000
+                : null;
+            this.writableEndDate = this.unit.attributes['writable-end-date']
+                ? new Date(this.unit.attributes['writable-end-date']).getTime() / 1000
+                : null;
+            if (this.permissionType === 'users') {
+                this.visibleApprovalUsers = this.unit.attributes['visible-approval'];
+                this.writableApprovalUsers = this.unit.attributes['writable-approval'];
+            }
+            if (this.permissionType === 'groups') {
+                this.visibleApprovalGroups = this.unit.attributes['visible-approval'];
+                this.writableApprovalGroups = this.unit.attributes['writable-approval'];
+            }
+
+            axios
+                .get(STUDIP.URLHelper.getURL('jsonapi.php/v1/semesters', { 'filter[current]': true }, true))
+                .then((response) => {
+                    this.currentSemester = response.data.data[0];
+                })
+                .catch(() => {
+                    this.currentSemester = null;
+                });
+        },
+        async storePermissions() {
+            let visibleApproval = [];
+            let writableApproval = [];
+            if (this.permissionType === 'users') {
+                visibleApproval = this.visibleApprovalUsers;
+                writableApproval = this.writableApprovalUsers;
+            }
+            if (this.permissionType === 'groups') {
+                visibleApproval = this.visibleApprovalGroups;
+                writableApproval = this.writableApprovalGroups;
+            }
+
+            if (this.visible === 'period' && this.visibleStartDate === null && this.visibleEndDate === null) {
+                this.visible = 'always';
+            }
+
+            if (this.writable === 'period' && this.writableStartDate === null && this.writableEndDate === null) {
+                this.visible = 'always';
+            }
+
+            if (
+                this.visible === 'period' &&
+                this.visibleStartDate !== null &&
+                this.visibleEndDate !== null &&
+                this.visibleStartDate > this.visibleEndDate
+            ) {
+                this.companionWarning({
+                    info: this.$gettext(
+                        'Das Enddatum des Sichtbarkeitszeitraums darf nicht vor dem Startdatum liegen.'
+                    ),
+                });
+                return false;
+            }
+
+            if (
+                this.writable === 'period' &&
+                this.writableStartDate !== null &&
+                this.writableEndDate !== null &&
+                this.writableStartDate > this.writableEndDate
+            ) {
+                this.companionWarning({
+                    info: this.$gettext('Das Enddatum des Bearbeitungszeitraums darf nicht vor dem Startdatum liegen.'),
+                });
+                return false;
+            }
+
+            if (!this.periodsValid) {
+                this.companionWarning({
+                    info: this.$gettext('Der Bearbeitungszeitraum muss innerhalb des Sichtbarkeitszeitraums liegen.'),
+                });
+                return false;
+            }
+
+            const unit = {
+                id: this.unit.id,
+                type: 'courseware-units',
+                attributes: {
+                    'permission-scope': 'unit',
+                    'permission-type': this.permissionType,
+                    visible: this.visible,
+                    'visible-all': this.visibleAll && this.permissionType !== 'all' ? 1 : 0,
+                    'visible-start-date':
+                        this.visible === 'period' ? new Date(this.visibleStartDate * 1000).toISOString() : null,
+                    'visible-end-date':
+                        this.visible === 'period' ? new Date(this.visibleEndDate * 1000).toISOString() : null,
+                    'visible-approval': JSON.stringify(visibleApproval),
+                    writable: this.writable,
+                    'writable-all': this.writableAll && this.permissionType !== 'all' ? 1 : 0,
+                    'writable-start-date':
+                        this.writable === 'period' ? new Date(this.writableStartDate * 1000).toISOString() : null,
+                    'writable-end-date':
+                        this.writable === 'period' ? new Date(this.writableEndDate * 1000).toISOString() : null,
+                    'writable-approval': JSON.stringify(writableApproval),
+                },
+            };
+            this.$emit('close');
+            await this.updateUnit(unit);
+            await this.loadUnit({ id: this.unit.id });
+        },
+
+        updatePermissionType() {
+            if (this.permissionType !== 'all') {
+                if (this.visible === 'never') {
+                    this.visible = 'always';
+                }
+                if (this.writable === 'never') {
+                    this.writable = 'always';
+                }
+            } else {
+                if (this.writable === 'always') {
+                    this.writable = 'never';
+                }
+            }
+        },
+        updateVisibile() {
+            if (this.visible === 'never' && this.permissionType === 'all') {
+                this.writable = 'never';
+            }
+            if (this.visible === 'period') {
+                if (this.writable === 'always') {
+                    this.writable = 'period';
+                    this.writableStartDate = this.writableStartDate ?? this.semesterDates.start;
+                    this.writableEndDate = this.writableEndDate ?? this.semesterDates.end;
+                }
+
+                this.visibleStartDate = this.visibleStartDate ?? this.semesterDates.start;
+                this.visibleEndDate = this.visibleEndDate ?? this.semesterDates.end;
+            }
+        },
+        updateWritable() {
+            if (this.writable === 'always') {
+                this.visible = 'always';
+            }
+            if (this.writable === 'period' && this.permissionType === 'all' && this.visible !== 'always') {
+                this.visible = 'period';
+                this.visibleStartDate = this.visibleStartDate ?? this.semesterDates.start;
+                this.visibleEndDate = this.visibleEndDate ?? this.semesterDates.end;
+            }
+            if (this.writable === 'period') {
+                this.writableStartDate = this.writableStartDate ?? this.semesterDates.start;
+                this.writableEndDate = this.writableEndDate ?? this.semesterDates.end;
+            }
+        },
+        updateUserWritable(user) {
+            if (this.writableApprovalUsers.includes(user.id) && !this.visibleApprovalUsers.includes(user.id)) {
+                this.visibleApprovalUsers.push(user.id);
+            }
+        },
+        updateUserVisible(user) {
+            if (this.writableApprovalUsers.includes(user.id) && !this.visibleApprovalUsers.includes(user.id)) {
+                this.writableApprovalUsers = this.writableApprovalUsers.filter((id) => id !== user.id);
+            }
+        },
+        updateGroupWritable(group) {
+            if (this.writableApprovalGroups.includes(group.id) && !this.visibleApprovalGroups.includes(group.id)) {
+                this.visibleApprovalGroups.push(group.id);
+            }
+        },
+        updateGroupVisible(group) {
+            if (this.writableApprovalGroups.includes(group.id) && !this.visibleApprovalGroups.includes(group.id)) {
+                this.writableApprovalGroups = this.writableApprovalGroups.filter((id) => id !== group.id);
+            }
+        },
+        updateVisibleAll() {
+            if (this.writableAll) {
+                this.visibleAll = true;
+            }
+        },
+        updatewritableAll() {
+            if (!this.visibleAll) {
+                this.writableAll = false;
+            }
+        },
+    },
+    mounted() {
+        this.setDimensions();
+        this.initData();
+        const parent = { type: 'courses', id: this.context.id };
+        let options = {
+            include: 'user',
+            'page[limit]': 10000,
+        };
+        this.loadCourseMemberships({ parent, relationship: 'memberships', options: options });
+        this.loadCourseStatusGroups({ parent, relationship: 'status-groups' });
+    },
+};
+</script>
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue b/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue
index 9d21d4cd76b..da19e5207dc 100644
--- a/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue
+++ b/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue
@@ -55,7 +55,7 @@
 import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
 import CoursewareUnitProgressItem from './CoursewareUnitProgressItem.vue';
 import CoursewareProgressCircle from './CoursewareProgressCircle.vue';
-import StudipIcon from '../../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 import { mapGetters } from 'vuex';
 
diff --git a/resources/vue/components/courseware/widgets/CoursewareActionWidget.vue b/resources/vue/components/courseware/widgets/CoursewareActionWidget.vue
index 9a179c3aec3..78b8d703613 100644
--- a/resources/vue/components/courseware/widgets/CoursewareActionWidget.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareActionWidget.vue
@@ -18,7 +18,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 import { mapActions } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterType.vue b/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterType.vue
index 09ced68f6c8..1a993543767 100644
--- a/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterType.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterType.vue
@@ -30,7 +30,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 
 import { mapActions } from 'vuex';
 
diff --git a/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterUnit.vue b/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterUnit.vue
index dc10e17a072..4cc9c65662d 100644
--- a/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterUnit.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareActivitiesWidgetFilterUnit.vue
@@ -18,7 +18,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 
 import { mapActions, mapGetters } from 'vuex';
 
diff --git a/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterCreated.vue b/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterCreated.vue
index 5b0d5af8872..ecf82ff7f2e 100644
--- a/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterCreated.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterCreated.vue
@@ -21,7 +21,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 import { mapActions } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterType.vue b/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterType.vue
index 4d92294b77a..f3ff8369511 100644
--- a/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterType.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareCommentsOverviewWidgetFilterType.vue
@@ -21,7 +21,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 import { mapActions } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/widgets/CoursewareExportWidget.vue b/resources/vue/components/courseware/widgets/CoursewareExportWidget.vue
index b31ee20b4a0..152bff91bf1 100644
--- a/resources/vue/components/courseware/widgets/CoursewareExportWidget.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareExportWidget.vue
@@ -26,7 +26,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 import CoursewareExport from '@/vue/mixins/courseware/export.js';
 import { mapActions, mapGetters } from 'vuex';
 
diff --git a/resources/vue/components/courseware/widgets/CoursewareImportWidget.vue b/resources/vue/components/courseware/widgets/CoursewareImportWidget.vue
index c0bfd5ba9e0..b720e960d4a 100644
--- a/resources/vue/components/courseware/widgets/CoursewareImportWidget.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareImportWidget.vue
@@ -23,7 +23,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 import { mapActions, mapGetters } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/widgets/CoursewareSearchWidget.vue b/resources/vue/components/courseware/widgets/CoursewareSearchWidget.vue
index fdc239dd7e5..23bd0688507 100644
--- a/resources/vue/components/courseware/widgets/CoursewareSearchWidget.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareSearchWidget.vue
@@ -32,8 +32,8 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
-import StudipIcon from '../../StudipIcon.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 import { mapActions, mapGetters } from 'vuex';
 import axios from 'axios';
diff --git a/resources/vue/components/courseware/widgets/CoursewareShelfActionWidget.vue b/resources/vue/components/courseware/widgets/CoursewareShelfActionWidget.vue
index c7232c38078..7f37aa8e13c 100644
--- a/resources/vue/components/courseware/widgets/CoursewareShelfActionWidget.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareShelfActionWidget.vue
@@ -13,7 +13,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 import { mapActions } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/widgets/CoursewareShelfImportWidget.vue b/resources/vue/components/courseware/widgets/CoursewareShelfImportWidget.vue
index fd74bf00f7f..c1f16abeabb 100644
--- a/resources/vue/components/courseware/widgets/CoursewareShelfImportWidget.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareShelfImportWidget.vue
@@ -18,7 +18,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 import { mapActions } from 'vuex';
 
 export default {
diff --git a/resources/vue/components/courseware/widgets/CoursewareTasksActionWidget.vue b/resources/vue/components/courseware/widgets/CoursewareTasksActionWidget.vue
index 3ba88654ed2..8c6e079b20e 100644
--- a/resources/vue/components/courseware/widgets/CoursewareTasksActionWidget.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareTasksActionWidget.vue
@@ -36,7 +36,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 
 import { mapActions } from 'vuex';
 
@@ -64,15 +64,15 @@ export default {
 
 <style scoped>
 .cw-action-widget-task-groups-add-solvers {
-    background-image: url('../images/icons/blue/add.svg');
+    background-image: url('/assets/images/icons/blue/add.svg');
     background-size: 20px;
 }
 .cw-action-widget-task-groups-deadline {
-    background-image: url('../images/icons/blue/date.svg');
+    background-image: url('/assets/images/icons/blue/date.svg');
     background-size: 20px;
 }
 .cw-action-widget-task-groups-delete {
-    background-image: url('../images/icons/blue/trash.svg');
+    background-image: url('/assets/images/icons/blue/trash.svg');
     background-size: 20px;
 }
 </style>
diff --git a/resources/vue/components/feedback/StudipFiveStars.vue b/resources/vue/components/feedback/StudipFiveStars.vue
index 1a4f40b327f..7f211f01c63 100644
--- a/resources/vue/components/feedback/StudipFiveStars.vue
+++ b/resources/vue/components/feedback/StudipFiveStars.vue
@@ -5,7 +5,7 @@
 </template>
 
 <script>
-import StudipIcon from './../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 export default {
     name: 'studip-five-stars',
     components: {
diff --git a/resources/vue/components/feedback/StudipFiveStarsInput.vue b/resources/vue/components/feedback/StudipFiveStarsInput.vue
index 73448528f55..3c8ab565d21 100644
--- a/resources/vue/components/feedback/StudipFiveStarsInput.vue
+++ b/resources/vue/components/feedback/StudipFiveStarsInput.vue
@@ -15,7 +15,7 @@
     </div>
 </template>
 <script>
-import StudipIcon from './../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 export default {
     name: 'studip-five-stars-input',
     components: {
diff --git a/resources/vue/components/form_inputs/CalendarPermissionsTable.vue b/resources/vue/components/form_inputs/CalendarPermissionsTable.vue
index e958a02c580..90daf8b7ce5 100644
--- a/resources/vue/components/form_inputs/CalendarPermissionsTable.vue
+++ b/resources/vue/components/form_inputs/CalendarPermissionsTable.vue
@@ -50,7 +50,7 @@
 </template>
 
 <script>
-import StudipMessageBox from "../StudipMessageBox.vue";
+import StudipMessageBox from "@/vue/components/base/StudipMessageBox.vue";
 
 export default {
     name: "calendar-permissions-table",
diff --git a/resources/vue/components/form_inputs/DateListInput.vue b/resources/vue/components/form_inputs/DateListInput.vue
index c2ad1acfa80..d0ba2ec33b7 100644
--- a/resources/vue/components/form_inputs/DateListInput.vue
+++ b/resources/vue/components/form_inputs/DateListInput.vue
@@ -21,7 +21,7 @@
 </template>
 
 <script>
-import StudipDateTime from "../StudipDateTime.vue";
+import StudipDateTime from "@/vue/components/base/StudipDateTime.vue";
 
 export default {
     name: "date-list-input",
diff --git a/resources/vue/components/form_inputs/MyCoursesColouredTable.vue b/resources/vue/components/form_inputs/MyCoursesColouredTable.vue
index d8431dd9999..9798ad4197f 100644
--- a/resources/vue/components/form_inputs/MyCoursesColouredTable.vue
+++ b/resources/vue/components/form_inputs/MyCoursesColouredTable.vue
@@ -56,7 +56,7 @@
 </template>
 
 <script>
-import StudipMessageBox from "../StudipMessageBox.vue";
+import StudipMessageBox from "@/vue/components/base/StudipMessageBox.vue";
 
 export default {
     name: 'my-courses-coloured-table',
diff --git a/resources/vue/components/form_inputs/QuicksearchListInput.vue b/resources/vue/components/form_inputs/QuicksearchListInput.vue
index d1f1fede534..55f054d060f 100644
--- a/resources/vue/components/form_inputs/QuicksearchListInput.vue
+++ b/resources/vue/components/form_inputs/QuicksearchListInput.vue
@@ -27,7 +27,7 @@
 </template>
 
 <script>
-import quicksearch from '../Quicksearch.vue';
+import quicksearch from '@/vue/components/base/Quicksearch.vue';
 
 export default {
     name: 'QuicksearchList',
diff --git a/resources/vue/components/massmail/MassMailMessagesList.vue b/resources/vue/components/massmail/MassMailMessagesList.vue
index 60d5cbe8d1f..d693331f414 100644
--- a/resources/vue/components/massmail/MassMailMessagesList.vue
+++ b/resources/vue/components/massmail/MassMailMessagesList.vue
@@ -75,8 +75,8 @@
 
 <script>
 import StudipProgressIndicator from '../StudipProgressIndicator.vue';
-import StudipActionMenu from '../StudipActionMenu.vue';
-import SidebarWidget from '../SidebarWidget.vue';
+import StudipActionMenu from '@/vue/components/base/StudipActionMenu.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 
 export default {
     name: 'MassMailMessagesList',
diff --git a/resources/vue/components/massmail/MassMailPermissions.vue b/resources/vue/components/massmail/MassMailPermissions.vue
index b493dc5fd1b..1bd90c3a7dd 100644
--- a/resources/vue/components/massmail/MassMailPermissions.vue
+++ b/resources/vue/components/massmail/MassMailPermissions.vue
@@ -52,7 +52,7 @@
 
 <script>
 import StudipProgressIndicator from "../StudipProgressIndicator.vue";
-import StudipActionMenu from "../StudipActionMenu.vue";
+import StudipActionMenu from "@/vue/components/base/StudipActionMenu.vue";
 
 export default {
     name: 'MassMailPermissions',
diff --git a/resources/vue/components/questionnaires/QuestionnaireEditor.vue b/resources/vue/components/questionnaires/QuestionnaireEditor.vue
index 6ea8d642e60..ac59788e765 100644
--- a/resources/vue/components/questionnaires/QuestionnaireEditor.vue
+++ b/resources/vue/components/questionnaires/QuestionnaireEditor.vue
@@ -167,9 +167,9 @@
 <script>
 import draggable from 'vuedraggable';
 import md5 from 'md5';
-import StudipIcon from '../StudipIcon.vue';
-import StudipActionMenu from '../StudipActionMenu.vue';
-import Datetimepicker from '../Datetimepicker.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
+import StudipActionMenu from '@/vue/components/base/StudipActionMenu.vue';
+import Datetimepicker from '@/vue/components/base/Datetimepicker.vue';
 import {defineAsyncComponent} from 'vue';
 
 const loadedComponents = {};
@@ -209,8 +209,8 @@ export default {
             const componentInfo = this.questionTypes[this.data.questions[index].questiontype].component;
             if (loadedComponents[componentInfo[0]] === undefined) {
                 loadedComponents[componentInfo[0]] = componentInfo[1] === ''
-                    ? defineAsyncComponent(() => import(`./${componentInfo[0]}.vue`))
-                    : defineAsyncComponent(() => import(/* webpackIgnore: true */componentInfo[1]));
+                    ? defineAsyncComponent(() => import(`./question-types/${componentInfo[0]}.vue`))
+                    : defineAsyncComponent(() => import(/* @vite-ignore */ componentInfo[1]));
             }
 
             return loadedComponents[componentInfo[0]];
diff --git a/resources/vue/components/questionnaires/AutomatedDataEdit.vue b/resources/vue/components/questionnaires/question-types/AutomatedDataEdit.vue
similarity index 96%
rename from resources/vue/components/questionnaires/AutomatedDataEdit.vue
rename to resources/vue/components/questionnaires/question-types/AutomatedDataEdit.vue
index f37bba909f9..b7ecdf72cfb 100644
--- a/resources/vue/components/questionnaires/AutomatedDataEdit.vue
+++ b/resources/vue/components/questionnaires/question-types/AutomatedDataEdit.vue
@@ -30,7 +30,7 @@
 
 <script>
 import axios from 'axios';
-import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
+import { QuestionnaireComponent } from '../../../mixins/QuestionnaireComponent';
 
 export default {
     name: 'automated-data-edit',
diff --git a/resources/vue/components/questionnaires/FreetextEdit.vue b/resources/vue/components/questionnaires/question-types/FreetextEdit.vue
similarity index 81%
rename from resources/vue/components/questionnaires/FreetextEdit.vue
rename to resources/vue/components/questionnaires/question-types/FreetextEdit.vue
index 7b0ad533e2d..f47ef4868c6 100644
--- a/resources/vue/components/questionnaires/FreetextEdit.vue
+++ b/resources/vue/components/questionnaires/question-types/FreetextEdit.vue
@@ -13,8 +13,8 @@
 </template>
 
 <script>
-import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
-import StudipWysiwyg from "../StudipWysiwyg.vue";
+import { QuestionnaireComponent } from '../../../mixins/QuestionnaireComponent';
+import StudipWysiwyg from "@/vue/components/base/StudipWysiwyg.vue";
 
 export default {
     extends: QuestionnaireComponent,
diff --git a/resources/vue/components/questionnaires/LikertEdit.vue b/resources/vue/components/questionnaires/question-types/LikertEdit.vue
similarity index 89%
rename from resources/vue/components/questionnaires/LikertEdit.vue
rename to resources/vue/components/questionnaires/question-types/LikertEdit.vue
index c25126c0276..6383f46942f 100644
--- a/resources/vue/components/questionnaires/LikertEdit.vue
+++ b/resources/vue/components/questionnaires/question-types/LikertEdit.vue
@@ -41,10 +41,10 @@
 </template>
 
 <script>
-import { $gettext } from '../../../assets/javascripts/lib/gettext';
-import InputArray from "./InputArray.vue";
-import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
-import StudipWysiwyg from "../StudipWysiwyg.vue";
+import { $gettext } from '../../../../assets/javascripts/lib/gettext';
+import InputArray from "../InputArray.vue";
+import { QuestionnaireComponent } from '../../../mixins/QuestionnaireComponent';
+import StudipWysiwyg from "@/vue/components/base/StudipWysiwyg.vue";
 
 // This is necesssar since $gettext does not seem to work in data() or created()
 const default_values = () => ({
diff --git a/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue b/resources/vue/components/questionnaires/question-types/QuestionnaireInfoEdit.vue
similarity index 88%
rename from resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue
rename to resources/vue/components/questionnaires/question-types/QuestionnaireInfoEdit.vue
index 828d24e7c39..ae547e381f7 100644
--- a/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue
+++ b/resources/vue/components/questionnaires/question-types/QuestionnaireInfoEdit.vue
@@ -14,8 +14,8 @@
 </template>
 
 <script>
-import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
-import StudipWysiwyg from "../StudipWysiwyg.vue";
+import { QuestionnaireComponent } from '../../../mixins/QuestionnaireComponent';
+import StudipWysiwyg from "@/vue/components/base/StudipWysiwyg.vue";
 
 export default {
     name: 'questionnaire-info-edit',
diff --git a/resources/vue/components/questionnaires/RangescaleEdit.vue b/resources/vue/components/questionnaires/question-types/RangescaleEdit.vue
similarity index 93%
rename from resources/vue/components/questionnaires/RangescaleEdit.vue
rename to resources/vue/components/questionnaires/question-types/RangescaleEdit.vue
index b3ff7da61ef..52b7975cd9e 100644
--- a/resources/vue/components/questionnaires/RangescaleEdit.vue
+++ b/resources/vue/components/questionnaires/question-types/RangescaleEdit.vue
@@ -50,9 +50,9 @@
 </template>
 
 <script>
-import InputArray from './InputArray.vue';
-import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
-import StudipWysiwyg from "../StudipWysiwyg.vue";
+import InputArray from '../InputArray.vue';
+import { QuestionnaireComponent } from '../../../mixins/QuestionnaireComponent';
+import StudipWysiwyg from "@/vue/components/base/StudipWysiwyg.vue";
 
 export default {
     name: 'rangescale-edit',
diff --git a/resources/vue/components/questionnaires/VoteEdit.vue b/resources/vue/components/questionnaires/question-types/VoteEdit.vue
similarity index 86%
rename from resources/vue/components/questionnaires/VoteEdit.vue
rename to resources/vue/components/questionnaires/question-types/VoteEdit.vue
index b4855cabf91..86dae6ce0e6 100644
--- a/resources/vue/components/questionnaires/VoteEdit.vue
+++ b/resources/vue/components/questionnaires/question-types/VoteEdit.vue
@@ -24,9 +24,9 @@
 </template>
 
 <script>
-import InputArray from "./InputArray.vue";
-import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
-import StudipWysiwyg from "../StudipWysiwyg.vue";
+import InputArray from "../InputArray.vue";
+import { QuestionnaireComponent } from '../../../mixins/QuestionnaireComponent';
+import StudipWysiwyg from "@/vue/components/base/StudipWysiwyg.vue";
 
 export default {
     extends: QuestionnaireComponent,
diff --git a/resources/vue/components/responsive/NavigationItem.vue b/resources/vue/components/responsive/NavigationItem.vue
index 8db20f8d380..1e9393e0a86 100644
--- a/resources/vue/components/responsive/NavigationItem.vue
+++ b/resources/vue/components/responsive/NavigationItem.vue
@@ -59,7 +59,7 @@
 
 <script lang="ts">
 import { defineComponent } from 'vue';
-import StudipIcon from '../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default defineComponent({
     name: 'NavigationItem',
diff --git a/resources/vue/components/responsive/ResponsiveContentBar.vue b/resources/vue/components/responsive/ResponsiveContentBar.vue
index b9a5d3aa35c..63938adf5a3 100644
--- a/resources/vue/components/responsive/ResponsiveContentBar.vue
+++ b/resources/vue/components/responsive/ResponsiveContentBar.vue
@@ -25,7 +25,7 @@
 </template>
 
 <script>
-import StudipIcon from '../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     name: 'ResponsiveContentBar',
diff --git a/resources/vue/components/responsive/ResponsiveNavigation.vue b/resources/vue/components/responsive/ResponsiveNavigation.vue
index 33b3ccbc246..81350fb51ba 100644
--- a/resources/vue/components/responsive/ResponsiveNavigation.vue
+++ b/resources/vue/components/responsive/ResponsiveNavigation.vue
@@ -113,7 +113,7 @@
 
 <script>
 import NavigationItem from './NavigationItem.vue';
-import StudipIcon from '../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 import ResponsiveContentBar from './ResponsiveContentBar.vue';
 import ResponsiveSkipLinks from './ResponsiveSkipLinks.vue';
 import { FocusTrap } from 'focus-trap-vue';
diff --git a/resources/vue/components/stock-images/ActionsWidget.vue b/resources/vue/components/stock-images/ActionsWidget.vue
index 4553249648c..b55c5c0ba99 100644
--- a/resources/vue/components/stock-images/ActionsWidget.vue
+++ b/resources/vue/components/stock-images/ActionsWidget.vue
@@ -15,7 +15,7 @@
     </SidebarWidget>
 </template>
 <script>
-import SidebarWidget from '../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 
 export default {
     emits: ['initiateUpload', 'initiateZipUpload'],
diff --git a/resources/vue/components/stock-images/ColorFilterWidget.vue b/resources/vue/components/stock-images/ColorFilterWidget.vue
index d9546fed822..9febd3f1176 100644
--- a/resources/vue/components/stock-images/ColorFilterWidget.vue
+++ b/resources/vue/components/stock-images/ColorFilterWidget.vue
@@ -22,7 +22,7 @@
 </template>
 <script>
 import { colors as selectableColors } from './colors.js';
-import SidebarWidget from '../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 
 export default {
     emits: ['update:filters'],
diff --git a/resources/vue/components/stock-images/OrientationFilterWidget.vue b/resources/vue/components/stock-images/OrientationFilterWidget.vue
index 933b66a4b5a..bc18049ce28 100644
--- a/resources/vue/components/stock-images/OrientationFilterWidget.vue
+++ b/resources/vue/components/stock-images/OrientationFilterWidget.vue
@@ -13,7 +13,7 @@
     </SidebarWidget>
 </template>
 <script>
-import SidebarWidget from '../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 import { orientations } from './filters.js';
 
 export default {
diff --git a/resources/vue/components/stock-images/Page.vue b/resources/vue/components/stock-images/Page.vue
index b419121ca45..7d04db71d72 100644
--- a/resources/vue/components/stock-images/Page.vue
+++ b/resources/vue/components/stock-images/Page.vue
@@ -63,7 +63,7 @@ import OrientationFilterWidget from './OrientationFilterWidget.vue';
 import SearchWidget from './SearchWidget.vue';
 import UploadDialog from './UploadDialog.vue';
 import ZipUploadDialog from './ZipUploadDialog.vue';
-import StudipMessageBox from '../StudipMessageBox.vue';
+import StudipMessageBox from '@/vue/components/base/StudipMessageBox.vue';
 import StudipProgressIndicator from '../StudipProgressIndicator.vue';
 import { searchFilterAndSortImages } from './filters.js';
 
diff --git a/resources/vue/components/stock-images/SearchWidget.vue b/resources/vue/components/stock-images/SearchWidget.vue
index 7c09973dccc..2a5c4a535ea 100644
--- a/resources/vue/components/stock-images/SearchWidget.vue
+++ b/resources/vue/components/stock-images/SearchWidget.vue
@@ -36,7 +36,7 @@
     </SidebarWidget>
 </template>
 <script>
-import SidebarWidget from '../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 
 export default {
     emits: ['search'],
diff --git a/resources/vue/components/stock-images/SelectableImageCard.vue b/resources/vue/components/stock-images/SelectableImageCard.vue
index e6d4c73a3d8..0046ebe62a2 100644
--- a/resources/vue/components/stock-images/SelectableImageCard.vue
+++ b/resources/vue/components/stock-images/SelectableImageCard.vue
@@ -43,6 +43,6 @@ export default {
     -webkit-box-orient: vertical;
 }
 .stock-images-image-card__thumbnail {
-    background-image: url(../images/checkered-background.png);
+    background-image: url(/assets/images/checkered-background.png);
 }
 </style>
diff --git a/resources/vue/components/tree/AssignLinkWidget.vue b/resources/vue/components/tree/AssignLinkWidget.vue
index 7ca99ff3242..a54d6e82112 100644
--- a/resources/vue/components/tree/AssignLinkWidget.vue
+++ b/resources/vue/components/tree/AssignLinkWidget.vue
@@ -11,8 +11,8 @@
 </template>
 
 <script>
-import SidebarWidget from '../SidebarWidget.vue';
-import StudipIcon from '../StudipIcon.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 import { TreeMixin } from '../../mixins/TreeMixin';
 
 export default {
diff --git a/resources/vue/components/tree/StudipTreeNode.vue b/resources/vue/components/tree/StudipTreeNode.vue
index 78691c890a0..02effcccf5a 100644
--- a/resources/vue/components/tree/StudipTreeNode.vue
+++ b/resources/vue/components/tree/StudipTreeNode.vue
@@ -46,10 +46,10 @@
 
 <script>
 import { TreeMixin } from '../../mixins/TreeMixin';
-import StudipIcon from '../StudipIcon.vue';
-import StudipAssetImg from '../StudipAssetImg.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
+import StudipAssetImg from '@/vue/components/base/StudipAssetImg.vue';
 import axios from 'axios';
-import StudipTooltipIcon from '../StudipTooltipIcon.vue';
+import StudipTooltipIcon from '@/vue/components/base/StudipTooltipIcon.vue';
 
 export default {
     name: 'StudipTreeNode',
diff --git a/resources/vue/components/tree/StudipTreeTable.vue b/resources/vue/components/tree/StudipTreeTable.vue
index 92015a3a6ba..86a5eba4892 100644
--- a/resources/vue/components/tree/StudipTreeTable.vue
+++ b/resources/vue/components/tree/StudipTreeTable.vue
@@ -166,7 +166,7 @@ import AssignLinkWidget from "./AssignLinkWidget.vue";
 import StudipPagination from "../StudipPagination.vue";
 import StudipTreeTableRows from "./StudipTreeTableRows.vue";
 import TreeCourseDetails from "./TreeCourseDetails.vue";
-import StudipIcon from "../StudipIcon.vue";
+import StudipIcon from "@/vue/components/base/StudipIcon.vue";
 
 export default {
     name: 'StudipTreeTable',
diff --git a/resources/vue/components/tree/StudipTreeTableRows.vue b/resources/vue/components/tree/StudipTreeTableRows.vue
index e13b97d82fe..4c5e1a0ba4d 100644
--- a/resources/vue/components/tree/StudipTreeTableRows.vue
+++ b/resources/vue/components/tree/StudipTreeTableRows.vue
@@ -44,7 +44,7 @@
     </tr>
 </template>
 <script>
-import StudipIcon from "../StudipIcon.vue";
+import StudipIcon from "@/vue/components/base/StudipIcon.vue";
 import TreeNodeCourseInfo from "./TreeNodeCourseInfo.vue";
 import {TreeMixin} from "../../mixins/TreeMixin";
 
diff --git a/resources/vue/components/tree/StudipTreeViewWidget.vue b/resources/vue/components/tree/StudipTreeViewWidget.vue
index 30e2d5c30d3..e1bb6d371f2 100644
--- a/resources/vue/components/tree/StudipTreeViewWidget.vue
+++ b/resources/vue/components/tree/StudipTreeViewWidget.vue
@@ -22,7 +22,7 @@
 </template>
 
 <script>
-import SidebarWidget from '../SidebarWidget.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
 
 export default {
     name: 'StudipTreeViewWidget',
diff --git a/resources/vue/components/tree/TreeBreadcrumb.vue b/resources/vue/components/tree/TreeBreadcrumb.vue
index 00486dfd9bc..10da5ec904f 100644
--- a/resources/vue/components/tree/TreeBreadcrumb.vue
+++ b/resources/vue/components/tree/TreeBreadcrumb.vue
@@ -50,7 +50,7 @@
 
 <script>
 import { TreeMixin } from '../../mixins/TreeMixin';
-import StudipIcon from '../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 import StudipTreeNode from './StudipTreeNode.vue';
 import axios from 'axios';
 
diff --git a/resources/vue/components/tree/TreeExportWidget.vue b/resources/vue/components/tree/TreeExportWidget.vue
index 61500f53cb8..c369395ec8d 100644
--- a/resources/vue/components/tree/TreeExportWidget.vue
+++ b/resources/vue/components/tree/TreeExportWidget.vue
@@ -11,8 +11,8 @@
 
 <script>
 import axios from 'axios';
-import SidebarWidget from '../SidebarWidget.vue';
-import StudipIcon from '../StudipIcon.vue';
+import SidebarWidget from '@/vue/components/base/SidebarWidget.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     name: 'TreeExportWidget',
diff --git a/resources/vue/components/tree/TreeNodeCoursePath.vue b/resources/vue/components/tree/TreeNodeCoursePath.vue
index 71f69eab0b7..1bf67096e53 100644
--- a/resources/vue/components/tree/TreeNodeCoursePath.vue
+++ b/resources/vue/components/tree/TreeNodeCoursePath.vue
@@ -20,7 +20,7 @@
 </template>
 <script>
 import axios from 'axios';
-import StudipIcon from '../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 
 export default {
     name: 'TreeNodeCoursePath',
diff --git a/resources/vue/components/tree/TreeSearchResult.vue b/resources/vue/components/tree/TreeSearchResult.vue
index 8ca4794a6b8..6efbd758fe1 100644
--- a/resources/vue/components/tree/TreeSearchResult.vue
+++ b/resources/vue/components/tree/TreeSearchResult.vue
@@ -66,10 +66,10 @@
 <script>
 import { TreeMixin } from '../../mixins/TreeMixin';
 import StudipProgressIndicator from '../StudipProgressIndicator.vue';
-import StudipIcon from '../StudipIcon.vue';
+import StudipIcon from '@/vue/components/base/StudipIcon.vue';
 import TreeNodeCoursePath from './TreeNodeCoursePath.vue';
 import TreeCourseDetails from './TreeCourseDetails.vue';
-import StudipMessageBox from "../StudipMessageBox.vue";
+import StudipMessageBox from "@/vue/components/base/StudipMessageBox.vue";
 
 export default {
     name: 'TreeSearchResult',
diff --git a/resources/vue/store/StudipStore.js b/resources/vue/store/StudipStore.js
index 7c404a4f18b..cbf88eecad3 100644
--- a/resources/vue/store/StudipStore.js
+++ b/resources/vue/store/StudipStore.js
@@ -1,5 +1,3 @@
-import { eventBus, store } from '../../assets/javascripts/chunks/vue';
-
 const studipStore = {
     namespaced: true,
 
@@ -14,11 +12,11 @@ const studipStore = {
             return state[key];
         },
     },
+    mutations: {
+        switchConsumeMode(state, mode) {
+            state.consumeMode = !!mode;
+        },
+    },
 };
 
-// Make the current state of "focus mode" (fullscreen) available to Vue components.
-eventBus.on('switch-focus-mode', (mode) => {
-    store.state.studip.consumeMode = mode;
-});
-
 export default studipStore;
diff --git a/templates/helpbar/helpbar.php b/templates/helpbar/helpbar.php
index 6f8efbbb1d5..a98a6da71ec 100644
--- a/templates/helpbar/helpbar.php
+++ b/templates/helpbar/helpbar.php
@@ -49,6 +49,6 @@
 </div>
 <? if (!empty($tour_data['active_tour_id'])) : ?>
     <script>
-        STUDIP.Tour.init('<?=$tour_data['active_tour_id']?>', '<?=$tour_data['active_tour_step_nr']?>')
+       STUDIP.ready(() => STUDIP.Tour.init('<?=$tour_data['active_tour_id']?>', '<?=$tour_data['active_tour_step_nr']?>'));
     </script>
 <? endif ?>
diff --git a/templates/layouts/base.php b/templates/layouts/base.php
index 26f641424fc..b65282233d9 100644
--- a/templates/layouts/base.php
+++ b/templates/layouts/base.php
@@ -33,7 +33,7 @@ $lang_attr = str_replace('_', '-', $_SESSION['_language']);
         <?= htmlReady(PageLayout::getTitle() . ' - ' . Config::get()->UNI_NAME_CLEAN) ?>
     </title>
     <script>
-        CKEDITOR_BASEPATH = "<?= Assets::url('javascripts/ckeditor/') ?>";
+        window.CKEDITOR_BASEPATH = "<?= Assets::url('javascripts/ckeditor/') ?>";
         String.locale = "<?= htmlReady(strtr($_SESSION['_language'], '_', '-')) ?>";
 
         document.querySelector('html').classList.replace('no-js', 'js');
@@ -70,18 +70,45 @@ $lang_attr = str_replace('_', '-', $_SESSION['_language']);
                 'ENABLE_COURSESET_FCFS' => (bool) Config::get()->ENABLE_COURSESET_FCFS
             ]) ?>,
             jsonapi_schemas: <?= json_encode($getJsonApiSchemas()) ?>,
-        }
+        };
+
+        (() => {
+            const handlers = [];
+            let nextId = 1;
+
+            function addHandler(callback, type = false, top = false) {
+                const handler = { type, callback, handlerId: nextId++ };
+                top ? handlers.unshift(handler) : handlers.push(handler);
+                return window.STUDIP;
+            }
+
+            const ready = (callback, top = false) => addHandler(callback, false, top);
+            ready.trigger = function (type, context = document) {
+                console.debug("ready.trigger", type, context);
+                const event = { target: context, eventId: nextId++ };
+
+                handlers
+                    .filter((handler) => !handler.type || handler.type === type)
+                    .forEach((handler) => handler.callback(event));
+
+                $(document).trigger($.Event("studip-ready", event));
+            };
+
+            const domReady = (callback, top = false) => addHandler(callback, "dom", top);
+            const dialogReady = (callback, top = false) => addHandler(callback, "dialog", top);
+
+            Object.assign(window.STUDIP, { ready, domReady, dialogReady });
+        })();
     </script>
 
     <?= PageLayout::getHeadElements() ?>
 
     <script>
-        setTimeout(() => {
-            // This needs to be put in a timeout since otherwise it will not match
+        STUDIP.ready(() => {
             if (STUDIP.Responsive.isResponsive()) {
                 document.querySelector('html').classList.add('responsive-display');
             }
-        }, 0);
+        });
     </script>
 </head>
 
@@ -130,6 +157,12 @@ if (Studip\Debug\DebugBar::isActivated()) {
     echo app()->get(\DebugBar\DebugBar::class)->getJavascriptRenderer()->render();
 }
 ?>
+
+    <script type="module">
+        $(document)
+            .ready(() => STUDIP.Gettext.setLocale().then(() => STUDIP.ready.trigger('dom')))
+            .on('dialog-update', (event, data) => STUDIP.ready.trigger('dialog', data.dialog));
+    </script>
 </body>
 </html>
 <?php NotificationCenter::postNotification('PageDidRender', PageLayout::getBodyElementId());
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 00000000000..7ea907859c1
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,87 @@
+import vue from '@vitejs/plugin-vue';
+import path from 'path';
+import { defineConfig } from 'vite';
+import { viteStaticCopy } from 'vite-plugin-static-copy';
+// import { federation } from '@module-federation/vite';
+
+const entryFileNames = `assets/javascripts/[name].js`;
+const chunkFileNames = `assets/javascripts/[hash].chunk.js`;
+const assets = [
+    {
+        regex: /\.css$/,
+        output: 'assets/stylesheets/[name][extname]',
+    },
+    {
+        regex: /\.js$/,
+        output: 'assets/javascripts/[name][extname]',
+    },
+];
+
+function assetFileNames(info) {
+    if (info?.name) {
+        const result = assets.find((a) => a.regex.test(info.name));
+        if (result) {
+            return result.output;
+        }
+    }
+
+    return '[name][extname]';
+}
+
+export default defineConfig({
+    build: {
+        manifest: true,
+        rollupOptions: {
+            input: {
+                'studip-bootstrap': './resources/assets/javascripts/entry-bootstrap.js',
+                'studip-legacy-libs': './resources/assets/javascripts/entry-legacy-libs.js',
+                'studip-lib': './resources/assets/javascripts/entry-lib.js',
+                'studip-statusgroups': './resources/assets/javascripts/entry-statusgroups.js',
+                'studip-installer': './resources/assets/javascripts/entry-installer.js',
+                // stylesheets
+                print: './resources/assets/stylesheets/print.scss',
+                accessibility: './resources/assets/stylesheets/highcontrast.scss',
+            },
+            output: {
+                entryFileNames,
+                assetFileNames,
+                chunkFileNames,
+            },
+            // experimentalLogSideEffects: true,
+        },
+        assetsDir: './',
+        copyPublicDir: false,
+        emptyOutDir: false,
+        outDir: './public/',
+        publicDir: path.resolve(__dirname, './public'),
+        sourcemap: true,
+        target: 'es2022',
+    },
+    plugins: [
+        viteStaticCopy({
+            targets: [
+                ...[
+                    './node_modules/jquery/dist/jquery.min.js',
+                    './node_modules/jquery-ui/dist/jquery-ui.min.js',
+                    './node_modules/select2/dist/js/select2.full.min.js',
+                    './node_modules/tablesorter/dist/js/jquery.tablesorter.combined.min.js',
+                    './node_modules/jquery-ui-timepicker-addon/dist/jquery-ui-timepicker-addon.min.js',
+                    './node_modules/jquery.scrollto/jquery.scrollTo.min.js',
+                    './node_modules/jquery.qrcode/jquery.qrcode.min.js',
+                    './node_modules/jquery-ui-touch-punch/jquery.ui.touch-punch.min.js',
+                    './node_modules/lodash/lodash.min.js',
+                ].map((src) => ({ src, dest: 'assets/javascripts/' })),
+                {
+                    src: './node_modules/jquery-ui-timepicker-addon/dist/jquery-ui-timepicker-addon.min.css',
+                    dest: 'assets/stylesheets/',
+                },
+            ],
+        }),
+        vue(),
+    ],
+    resolve: {
+        alias: {
+            '@': path.resolve(__dirname, './resources'),
+        },
+    },
+});
-- 
GitLab