From d2c7c69e75f4ba549413584d9c803b90073d328f Mon Sep 17 00:00:00 2001
From: Jan-Hendrik Willms <tleilax+github@gmail.com>
Date: Fri, 31 May 2024 09:18:26 +0200
Subject: [PATCH] integration complete

---
 app/controllers/my_courses.php                | 26 ++----
 app/views/my_courses/index.php                | 11 ++-
 lib/classes/StudipController.php              | 36 +-------
 lib/classes/VueApp.php                        | 92 +++++++++++++++----
 .../javascripts/bootstrap/my-courses.js       | 22 -----
 resources/assets/javascripts/bootstrap/vue.js | 81 +++++++++-------
 resources/assets/javascripts/entry-base.js    |  2 -
 .../assets/javascripts/lib/studip-vue.js      | 12 ---
 templates/vue-app.php                         | 10 ++
 9 files changed, 149 insertions(+), 143 deletions(-)
 delete mode 100644 resources/assets/javascripts/bootstrap/my-courses.js
 create mode 100644 templates/vue-app.php

diff --git a/app/controllers/my_courses.php b/app/controllers/my_courses.php
index c5c10dd27d1..e17fc91b38b 100644
--- a/app/controllers/my_courses.php
+++ b/app/controllers/my_courses.php
@@ -113,19 +113,13 @@ class MyCoursesController extends AuthenticatedController
         }
 
         $this->setupSidebar($sem_key, $group_field, $this->check_for_new($sem_courses, $group_field));
-        $data = $this->getMyCoursesData($sem_courses, $group_field);
 
-        PageLayout::addHeadElement(
-            'script',
-            ['type' => 'text/javascript'],
-            'window.STUDIP.MyCoursesData = ' . json_encode($data) . ';'
-        );
-
-        $this->render_vue_app(
-            Studip\VueApp::create('MyCourses')
-                ->withStore('MyCoursesStore')
-                ->withStoreData('mycourses', $data)
-        );
+        $this->vueApp = Studip\VueApp::create('MyCourses')
+            ->withStore(
+                'MyCoursesStore',
+                'mycourses',
+                $this->getMyCoursesData($sem_courses, $group_field)
+            );
     }
 
     /**
@@ -803,10 +797,10 @@ class MyCoursesController extends AuthenticatedController
         }
 
         return [
-            'courses' => $this->sanitizeNavigations(array_map([$this, 'convertCourse'], $temp_courses)),
-            'groups'  => $groups,
-            'user_id' => $GLOBALS['user']->id,
-            'config'  => [
+            'setCourses' => $this->sanitizeNavigations(array_map([$this, 'convertCourse'], $temp_courses)),
+            'setGroups'  => $groups,
+            'setUserId'  => $GLOBALS['user']->id,
+            'setConfig'  => [
                 'allow_dozent_visibility'  => Config::get()->ALLOW_DOZENT_VISIBILITY,
                 'open_groups'              => array_values($GLOBALS['user']->cfg->MY_COURSES_OPEN_GROUPS),
                 'sem_number'               => Config::get()->IMPORTANT_SEMNUMBER,
diff --git a/app/views/my_courses/index.php b/app/views/my_courses/index.php
index 1bbfd7388e8..8ec7703a80b 100644
--- a/app/views/my_courses/index.php
+++ b/app/views/my_courses/index.php
@@ -1,10 +1,15 @@
+<?php
+/**
+ * @var Studip\VueApp $vueApp
+ * @var array $my_bosses
+ * @var array $waiting_list
+ */
+?>
 <? if ($waiting_list) : ?>
     <?= $this->render_partial('my_courses/waiting_list.php', compact('waiting_list')) ?>
 <? endif ?>
 
-<div class="my-courses-vue-app">
-    <my-courses />
-</div>
+<?= $vueApp->render() ?>
 
 <? if (count($my_bosses) > 0) : ?>
     <?= $this->render_partial('my_courses/_deputy_bosses'); ?>
diff --git a/lib/classes/StudipController.php b/lib/classes/StudipController.php
index ad6a4811291..d19da920a3b 100644
--- a/lib/classes/StudipController.php
+++ b/lib/classes/StudipController.php
@@ -590,41 +590,7 @@ abstract class StudipController extends Trails\Controller
 
     public function render_vue_app(\Studip\VueApp $app): void
     {
-        $attributes = [
-            'data-vue-app' => json_encode([
-                'components' => [$app->getBaseComponent()],
-                'store'      => $app->getStore(),
-            ]),
-            'is' => $app->getBaseComponent(),
-
-            ...$app->getProps(),
-        ];
-
-        $content = '';
-
-        if ($app->getStoreData()) {
-            $content .= '<script>';
-            foreach ($app->getStoreData() as $key => $data) {
-                $content .= sprintf(
-                    "window.STUDIP.Vue.setStoreData('%s', %s);",
-                    $key,
-                    json_encode($data),
-                );
-            }
-            $content .= '</script>';
-        }
-
-        $content .= '<div ' . arrayToHtmlAttributes($attributes) . '></div>';
-
-
-        if ($this->layout) {
-            $content = $this->get_template_factory()->render(
-                $this->layout,
-                ['content_for_layout' => $content]
-            );
-        }
-
-        $this->render_text($content);
+        $this->render_template($app->getTemplate(), $this->layout);
     }
 
     /**
diff --git a/lib/classes/VueApp.php b/lib/classes/VueApp.php
index 6c40c9e9763..16ebf545ac5 100644
--- a/lib/classes/VueApp.php
+++ b/lib/classes/VueApp.php
@@ -1,26 +1,50 @@
 <?php
 namespace Studip;
 
-class VueApp
+use Flexi\Template;
+use Stringable;
+
+final class VueApp implements Stringable
 {
     public static function create(string $base_component, array $props = []): VueApp
     {
         return new self($base_component, $props);
     }
 
-    protected ?string $store = null;
-    protected array $storeData = [];
+    private array $components = [];
+    private array $stores = [];
+    private array $storeData = [];
 
-    public function __construct(
-        protected string $base_component,
-        protected array $props = []
+    private function __construct(
+        private string  $base_component,
+        private array $props = []
     ) {
     }
 
     public function withBaseComponent(string $base_component): VueApp
     {
-        $this->base_component = $base_component;
-        return $this;
+        $clone = clone $this;
+        $clone->base_component = $base_component;
+
+        return $clone;
+    }
+
+    public function withComponents(string ...$components): VueApp
+    {
+        $clone = clone $this;
+        foreach ($components as $component) {
+            $clone = $clone->withAddedComponent($component);
+        }
+        return $clone;
+    }
+
+    public function withAddedComponent(string $component): VueApp
+    {
+        $clone = clone $this;
+        if (!in_array($component, $clone->components)) {
+            $clone->components[] = $component;
+        }
+        return $clone;
     }
 
     public function getBaseComponent(): string
@@ -30,8 +54,9 @@ class VueApp
 
     public function withProps(array $props): VueApp
     {
-        $this->props = $props;
-        return $this;
+        $clone = clone $this;
+        $clone->props = $props;
+        return $clone;
     }
 
     public function getProps(): array
@@ -39,25 +64,52 @@ class VueApp
         return $this->props;
     }
 
-    public function withStore(?string $store): VueApp
+    public function withStore(?string $store, ?string $index = null, ?array $data = null): VueApp
     {
-        $this->store = $store;
-        return $this;
-    }
+        $clone = clone $this;
 
-    public function getStore(): ?string
-    {
-        return $this->store;
+        $clone->stores[$index ?? $store] = $store;
+
+        if ($data !== null) {
+            $clone->storeData[$index ?? $store] = $data;
+        }
+
+        return $clone;
     }
 
-    public function withStoreData(string $key, array $data): VueApp
+    public function getStores(): array
     {
-        $this->storeData[$key] = $data;
-        return $this;
+        return $this->stores;
     }
 
     public function getStoreData(): array
     {
         return $this->storeData;
     }
+
+    public function getTemplate(): Template
+    {
+        $template = $GLOBALS['template_factory']->open('vue-app.php');
+        $template->attributes = [
+            'data-vue-app' => json_encode([
+                'components' => [$this->base_component, ...$this->components],
+                'stores'     => $this->stores,
+            ]),
+            'is' => $this->base_component,
+
+            ...$this->props,
+        ];
+        $template->storeData = $this->storeData;
+        return $template;
+    }
+
+    public function render(): string
+    {
+        return $this->getTemplate()->render();
+    }
+
+    public function __toString(): string
+    {
+        return $this->render();
+    }
 }
diff --git a/resources/assets/javascripts/bootstrap/my-courses.js b/resources/assets/javascripts/bootstrap/my-courses.js
deleted file mode 100644
index 40e0c24dc8d..00000000000
--- a/resources/assets/javascripts/bootstrap/my-courses.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import MyCourses from '../../../vue/components/MyCourses.vue';
-import storeConfig from '../../../vue/store/MyCoursesStore.js';
-
-STUDIP.domReady(async () => {
-    if ($('.my-courses-vue-app').length === 0) {
-        return;
-    }
-
-    const { createApp, store } = await STUDIP.Vue.load();
-
-    store.registerModule('mycourses', storeConfig);
-
-    store.commit('mycourses/setCourses', window.STUDIP.MyCoursesData['courses']);
-    store.commit('mycourses/setGroups', window.STUDIP.MyCoursesData['groups']);
-    store.commit('mycourses/setUserId', window.STUDIP.MyCoursesData['user_id']);
-    store.commit('mycourses/setConfig', window.STUDIP.MyCoursesData['config']);
-
-    const vm = createApp({
-        components: { MyCourses }
-    });
-    vm.$mount('.my-courses-vue-app');
-});
diff --git a/resources/assets/javascripts/bootstrap/vue.js b/resources/assets/javascripts/bootstrap/vue.js
index b3341d06da0..2c64b1095f6 100644
--- a/resources/assets/javascripts/bootstrap/vue.js
+++ b/resources/assets/javascripts/bootstrap/vue.js
@@ -6,21 +6,14 @@
  * Eg. ./components/ExampleComponent.vue -> <example-component></example-component>
  */
 STUDIP.ready(() => {
-    $('[data-vue-app]').each(function () {
-        if ($(this).is('[data-vue-app-created]')) {
-            return;
-        }
-
-        const config = Object.assign({}, {
-            id: false,
-            components: [],
-            store: false
-        }, $(this).data().vueApp);
-
-        let data = {};
-        if (config.id && window.STUDIP.AppData && window.STUDIP.AppData[config.id] !== undefined) {
-            data = window.STUDIP.AppData[config.id];
-        }
+    document.querySelectorAll('[data-vue-app]:not([data-vue-app-created])').forEach((node) => {
+        const config = Object.assign(
+            {
+                components: [],
+                stores: {}
+            },
+            JSON.parse(node.dataset.vueApp)
+        );
 
         let components = {};
         config.components.forEach(component => {
@@ -28,29 +21,51 @@ STUDIP.ready(() => {
         });
 
         STUDIP.Vue.load().then(async ({createApp, store}) => {
-            let vm;
-            if (config.store) {
-                const storeConfig = await import(`../../../vue/store/${config.store}.js`);
-                console.log('store', storeConfig.default);
+            for (const [index, name] of Object.entries(config.stores)) {
+                import(`../../../vue/store/${name}.js`).then(storeConfig => {
+                    store.registerModule(index, storeConfig.default);
 
-                store.registerModule(config.id, storeConfig.default);
-
-                Object.keys(data).forEach(command => {
-                    store.commit(`${config.id}/${command}`, data[command]);
+                    const dataElement = document.getElementById(`vue-store-data-${index}`);
+                    if (dataElement) {
+                        const data = JSON.parse(dataElement.innerText);
+                        Object.keys(data).forEach(command => {
+                            store.commit(`${index}/${command}`, data[command]);
+                        });
+                    }
                 });
-                vm = createApp({components});
-            } else {
-                vm = createApp({data, components});
+
             }
-            // import myCoursesStore from '../stores/MyCoursesStore.js';
-            //
-            // myCoursesStore.namespaced = true;
-            //
-            // store.registerModule('my-courses', myCoursesStore);
+            createApp({
+                components,
+                store,
 
-            vm.$mount(this);
+                beforeCreate() {
+                    STUDIP.Vue.emit('VueAppWillCreate', this);
+                },
+                created() {
+                    STUDIP.Vue.emit('VueAppDidCreate', this);
+                },
+                beforeMount() {
+                    STUDIP.Vue.emit('VueAppWillMount', this);
+                },
+                mounted() {
+                    STUDIP.Vue.emit('VueAppDidMount', this);
+                },
+                beforeUpdate() {
+                    STUDIP.Vue.emit('VueAppWillUpdate', this);
+                },
+                updated() {
+                    STUDIP.Vue.emit('VueAppDidUpdate', this);
+                },
+                beforeDestroy() {
+                    STUDIP.Vue.emit('VueAppWillDestroy', this);
+                },
+                destroyed() {
+                    STUDIP.Vue.emit('VueAppDidDestroy', this);
+                },
+            }).$mount(node);
         });
 
-        $(this).attr('data-vue-app-created', '');
+        node.dataset.vueAppCreated = 'true';
     });
 });
diff --git a/resources/assets/javascripts/entry-base.js b/resources/assets/javascripts/entry-base.js
index 26834395e6d..0b10750cd13 100644
--- a/resources/assets/javascripts/entry-base.js
+++ b/resources/assets/javascripts/entry-base.js
@@ -16,8 +16,6 @@ import "./init.js"
 import "./bootstrap/responsive.js"
 import "./bootstrap/vue.js"
 
-import "./bootstrap/my-courses.js";
-
 import "./studip-ui.js"
 import "./bootstrap/fullscreen.js"
 import "./bootstrap/tfa.js"
diff --git a/resources/assets/javascripts/lib/studip-vue.js b/resources/assets/javascripts/lib/studip-vue.js
index 395add9d141..e70b1c040c7 100644
--- a/resources/assets/javascripts/lib/studip-vue.js
+++ b/resources/assets/javascripts/lib/studip-vue.js
@@ -1,7 +1,5 @@
 class Vue
 {
-    static #storeData = {};
-
     static async load()
     {
         return STUDIP.loadChunk('vue');
@@ -18,16 +16,6 @@ class Vue
         const { eventBus } = await this.load();
         eventBus.emit(...args);
     }
-
-    static setStoreData(key, data)
-    {
-        this.#storeData[key] = data;
-    }
-
-    static getStoreData(key)
-    {
-        return this.#storeData[key] ?? null;
-    }
 }
 
 export default Vue;
diff --git a/templates/vue-app.php b/templates/vue-app.php
new file mode 100644
index 00000000000..ba2bbdccc3e
--- /dev/null
+++ b/templates/vue-app.php
@@ -0,0 +1,10 @@
+<?php
+/**
+ * @var array $attributes
+ * @var array $storeData
+ */
+?>
+<? foreach ($storeData as $store => $data): ?>
+<script type="application/json" id="vue-store-data-<?= htmlReady($store) ?>"><?= json_encode($data) ?></script>
+<? endforeach; ?>
+<div <?= arrayToHtmlAttributes($attributes) ?>></div>
-- 
GitLab