From e6a11cb96f7a0259554580369f41b2caf259c650 Mon Sep 17 00:00:00 2001
From: Marcus Eibrink-Lunzenauer <lunzenauer@elan-ev.de>
Date: Fri, 3 Dec 2021 10:35:40 +0100
Subject: [PATCH] Marry course manager with the caching structure loading.

Closes #446.
---
 .../Courseware/StructuralElementsCopy.php     |  34 +++--
 .../courseware/CoursewareCourseManager.vue    |   3 +-
 .../CoursewareManagerCopySelector.vue         |  11 +-
 .../courseware/CoursewareManagerElement.vue   |   6 +-
 .../vue/components/courseware/ManagerApp.vue  |  16 ++-
 .../vue/store/courseware/courseware.module.js |  19 ++-
 .../vue/store/courseware/structure.module.js  | 128 +++++++++++-------
 7 files changed, 132 insertions(+), 85 deletions(-)

diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCopy.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCopy.php
index b63628e0114..9622fb66ab1 100755
--- a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCopy.php
+++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCopy.php
@@ -26,24 +26,36 @@ class StructuralElementsCopy extends NonJsonApiController
     {
         $data = $request->getParsedBody()['data'];
 
-        $remote_element = \Courseware\StructuralElement::find($data['element']['id']);
-        $parent_element = \Courseware\StructuralElement::find($data['parent_id']);
-        if (!Authority::canCreateContainer($user = $this->getUser($request), $parent_element)) {
+        $sourceElement = StructuralElement::find($args['id']);
+        $newParent = StructuralElement::find($data['parent_id']);
+        if (!Authority::canCreateContainer($user = $this->getUser($request), $newParent)) {
             throw new AuthorizationFailedException();
         }
 
-        $new_element = $this->copyElement($user, $remote_element, $parent_element);
+        $newElement = $this->copyElement($user, $sourceElement, $newParent);
 
-        $response = $response->withHeader('Content-Type', 'application/json');
-        $response->getBody()->write((string) json_encode($new_element));
-
-        return $response;
+        return $this->redirectToStructuralElement($response, $newElement);
     }
 
-    private function copyElement(\User $user, \Courseware\StructuralElement $remote_element, \Courseware\StructuralElement $parent_element)
+    private function copyElement(\User $user, StructuralElement $sourceElement, StructuralElement $newParent)
     {
-        $new_element = $remote_element->copy($user, $parent_element);
+        $new_element = $sourceElement->copy($user, $newParent);
 
         return $new_element;
     }
-}
\ No newline at end of file
+
+    /**
+     * @SuppressWarnings(PHPMD.Superglobals)
+     */
+    private function redirectToStructuralElement(Response $response, StructuralElement $resource): Response
+    {
+        $pathinfo = $this->getSchema($resource)
+            ->getSelfLink($resource)
+            ->getStringRepresentation($this->container->get('json-api-integration-urlPrefix'));
+        $old = \URLHelper::setBaseURL($GLOBALS['ABSOLUTE_URI_STUDIP']);
+        $url = \URLHelper::getURL($pathinfo, [], true);
+        \URLHelper::setBaseURL($old);
+
+        return $response->withHeader('Location', $url)->withStatus(303);
+    }
+}
diff --git a/resources/vue/components/courseware/CoursewareCourseManager.vue b/resources/vue/components/courseware/CoursewareCourseManager.vue
index 4992fc79c3b..71011cfd93a 100755
--- a/resources/vue/components/courseware/CoursewareCourseManager.vue
+++ b/resources/vue/components/courseware/CoursewareCourseManager.vue
@@ -88,7 +88,7 @@
             </courseware-tab>
 
             <courseware-tab :name="$gettext('Kopieren')">
-                <courseware-manager-copy-selector @loadSelf="reloadElements"/>
+                <courseware-manager-copy-selector @loadSelf="reloadElements" @reloadElement="reloadElements" />
             </courseware-tab>
 
             <courseware-tab :name="$gettext('Importieren')">
@@ -233,6 +233,7 @@ export default {
         async reloadElements() {
             await this.setCurrentId(this.currentId);
             await this.setSelfId(this.selfId);
+            this.$emit("reload");
         },
         async setCurrentId(target) {
             this.currentId = target;
diff --git a/resources/vue/components/courseware/CoursewareManagerCopySelector.vue b/resources/vue/components/courseware/CoursewareManagerCopySelector.vue
index 602a0b4d2a6..621fe862c03 100755
--- a/resources/vue/components/courseware/CoursewareManagerCopySelector.vue
+++ b/resources/vue/components/courseware/CoursewareManagerCopySelector.vue
@@ -32,6 +32,7 @@
                     :currentElement="remoteElement"
                     @selectElement="setRemoteId"
                     @loadSelf="loadSelf"
+                    @reloadElement="reloadElement"
                 />
                 <courseware-companion-box
                     v-if="remoteId === '' && hasRemoteCid"
@@ -106,9 +107,9 @@ export default {
     },
     methods: {
         ...mapActions({
+            loadAnotherCourseware: 'courseware-structure/loadAnotherCourseware',
             loadUsersCourses: 'loadUsersCourses',
             loadStructuralElement: 'loadStructuralElement',
-            loadRemoteCoursewareStructure: 'loadRemoteCoursewareStructure',
             loadSemester: 'semesters/loadById',
         }),
         selectSource(source) {
@@ -116,16 +117,15 @@ export default {
         },
         async loadRemoteCourseware(cid) {
             this.remoteCid = cid;
-            this.remoteCoursewareInstance = await this.loadRemoteCoursewareStructure({rangeId: this.remoteCid, rangeType: 'courses'});
+            this.remoteCoursewareInstance = await this.loadAnotherCourseware({ id: this.remoteCid, type: 'courses'});
             if (this.remoteCoursewareInstance !== null) {
                 this.setRemoteId(this.remoteCoursewareInstance.relationships.root.data.id);
             } else {
                 this.remoteId = '';
             }
-            
         },
         async loadOwnCourseware() {
-            this.ownCoursewareInstance = await this.loadRemoteCoursewareStructure({rangeId: this.userId, rangeType: 'users'});
+            this.ownCoursewareInstance = await this.loadAnotherCourseware({ id: this.userId, type: 'users' });
             if (this.ownCoursewareInstance !== null) {
                 this.setOwnId(this.ownCoursewareInstance.relationships.root.data.id);
             } else {
@@ -188,6 +188,9 @@ export default {
             }
 
             return 'seminar';
+        },
+        reloadElement() {
+            this.$emit("reloadElement");
         }
     },
     async mounted() {
diff --git a/resources/vue/components/courseware/CoursewareManagerElement.vue b/resources/vue/components/courseware/CoursewareManagerElement.vue
index 14a22822bf8..7c0d3e37569 100755
--- a/resources/vue/components/courseware/CoursewareManagerElement.vue
+++ b/resources/vue/components/courseware/CoursewareManagerElement.vue
@@ -309,7 +309,7 @@ export default {
                 let element = data.element;
                 if (source === 'self') {
                     element.relationships.parent.data.id = this.filingData.parentItem.id;
-                    element.attributes.position = this.childrenById(this.filingData.parentItem.id).length;
+                    element.attributes.position = this.childrenById(this.filingData.parentItem.id).length + 1;
                     await this.lockObject({ id: element.id, type: 'courseware-structural-elements' });
                     await this.updateStructuralElement({
                         element: element,
@@ -343,7 +343,7 @@ export default {
                 let container = data.container;
                 if (source === 'self') {
                     container.relationships['structural-element'].data.id = this.filingData.parentItem.id;
-                    container.attributes.position = this.filingData.parentItem.relationships.containers.data.length;
+                    container.attributes.position = this.filingData.parentItem.relationships.containers.data.length + 1;
                     await this.lockObject({id: container.id, type: 'courseware-containers'});
                     await this.updateContainer({
                         container: container,
@@ -399,7 +399,7 @@ export default {
                     await this.unlockObject({id: destinationContainer.id, type: 'courseware-containers'});
 
                     block.relationships.container.data.id = this.filingData.parentItem.id;
-                    block.attributes.position = this.filingData.parentItem.relationships.blocks.data.length;
+                    block.attributes.position = this.filingData.parentItem.relationships.blocks.data.length + 1;
                     await this.lockObject({id: block.id, type: 'courseware-blocks'});
                     await this.updateBlock({
                         block: block,
diff --git a/resources/vue/components/courseware/ManagerApp.vue b/resources/vue/components/courseware/ManagerApp.vue
index 57d71e587b9..ed493e70b2c 100755
--- a/resources/vue/components/courseware/ManagerApp.vue
+++ b/resources/vue/components/courseware/ManagerApp.vue
@@ -1,5 +1,5 @@
 <template>
-    <courseware-course-manager></courseware-course-manager>
+    <courseware-course-manager @reload="rebuildStructure"></courseware-course-manager>
 </template>
 
 <script>
@@ -20,17 +20,21 @@ export default {
             invalidateStructureCache: 'courseware-structure/invalidateCache',
             loadCoursewareStructure: 'courseware-structure/load',
         }),
+        async rebuildStructure() {
+            // compute order of structural elements once more
+            await this.buildStructure();
+            console.debug("built structure")
+
+            // throw away stale cache
+            this.invalidateStructureCache();
+        },
     },
     async mounted() {
         await this.loadCoursewareStructure();
     },
     watch: {
         async structuralElements(newElements, oldElements) {
-            // compute order of structural elements once more
-            await this.buildStructure();
-
-            // throw away stale cache
-            this.invalidateStructureCache();
+            this.rebuildStructure();
         },
     },
 };
diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js
index 4111eb2b943..949b574a237 100755
--- a/resources/vue/store/courseware/courseware.module.js
+++ b/resources/vue/store/courseware/courseware.module.js
@@ -317,17 +317,14 @@ export const actions = {
             // console.log(resp);
         });
     },
-    copyStructuralElement({ getters }, { parentId, element }) {
-        const copy = {
-            data: {
-                element: element,
-                parent_id: parentId,
-            },
-        };
-
-        return state.httpClient.post(`courseware-structural-elements/${element.id}/copy`, copy).then((resp) => {
-            // console.log(resp);
-        });
+    copyStructuralElement({ dispatch, getters }, { parentId, element }) {
+        const copy = { data: { parent_id: parentId, }, };
+
+        return state.httpClient.post(`courseware-structural-elements/${element.id}/copy`, copy)
+            .then(({ data }) => {
+                const id = data.data.id;
+                dispatch('loadStructuralElement', id);
+            });
     },
 
     lockObject({ dispatch, getters }, { id, type }) {
diff --git a/resources/vue/store/courseware/structure.module.js b/resources/vue/store/courseware/structure.module.js
index fdfe7627585..0b71b090785 100644
--- a/resources/vue/store/courseware/structure.module.js
+++ b/resources/vue/store/courseware/structure.module.js
@@ -31,14 +31,21 @@ export const mutations = {
 
 const actions = {
     build({ commit, rootGetters }) {
-        const structuralElements = rootGetters['courseware-structural-elements/all'];
-        const root = findRoot(structuralElements);
+        const instance = rootGetters['courseware'];
+        if (!instance) {
+            throw new Error('Could not find current courseware');
+        }
+        const root = rootGetters['courseware-structural-elements/related']({
+            parent: { id: instance.id, type: instance.type },
+            relationship: 'root',
+        });
         if (!root) {
             commit('reset');
 
             return;
         }
 
+        const structuralElements = rootGetters['courseware-structural-elements/all'];
         const children = structuralElements.reduce((memo, element) => {
             const parent = element.relationships.parent?.data?.id ?? null;
             if (parent) {
@@ -81,43 +88,70 @@ const actions = {
         }
     },
 
+    // load the structure of the current courseware
     async load({ commit, dispatch, rootGetters }) {
-        const parent = rootGetters['context'];
-        const relationship = 'courseware';
-        const options = {
-            include: 'bookmarks,root',
-        };
+        const context = rootGetters['context'];
+        const instance = await dispatch('loadInstance', context);
+        commit('coursewareSet', instance, { root: true });
 
-        // get courseware instance
-        await dispatch(`courseware-instances/loadRelated`, { parent, relationship, options }, { root: true });
-        const courseware = rootGetters['courseware-instances/all'][0];
-        commit('coursewareSet', courseware, { root: true });
+        const root = rootGetters['courseware-structural-elements/related']({
+            parent: { id: instance.id, type: instance.type },
+            relationship: 'root',
+        });
+        if (!root) {
+            throw new Error(`Could not find root of courseware { id: ${instance.id}, type: ${instance.type}`);
+        }
+
+        dispatch('fetchDescendantsWithCaching', { root });
 
-        // load descendants
-        dispatch('fetchDescendants');
+        return instance;
     },
 
-    async fetchDescendants({ dispatch, rootGetters, commit }) {
-        // get root of that instance
-        const courseware = rootGetters['courseware'];
-        if (!courseware) {
-            return;
-        }
-        const rootElement = rootGetters['courseware-structural-elements/related']({
-            parent: { id: courseware.id, type: courseware.type },
+    // load the structure of a specified courseware
+    async loadAnotherCourseware({ commit, dispatch, rootGetters }, context) {
+        const instance = await dispatch('loadInstance', context);
+
+        const root = rootGetters['courseware-structural-elements/related']({
+            parent: { id: instance.id, type: instance.type },
             relationship: 'root',
         });
-        if (!rootElement) {
-            return;
+        if (!root) {
+            throw new Error(`Could not find root of courseware { id: ${instance.id}, type: ${instance.type}`);
         }
 
+        await dispatch('loadDescendants', { root });
+
+        return instance;
+    },
+
+    loadInstance({ commit, dispatch, rootGetters }, context) {
+        const parent = context;
+        const relationship = 'courseware';
+        const options = {
+            include: 'bookmarks,root',
+        };
+
+        return dispatch(
+            `courseware-instances/loadRelated`,
+            {
+                parent,
+                relationship,
+                options,
+            },
+            { root: true }
+        ).then(() => {
+            return rootGetters['courseware-instances/related']({ parent, relationship });
+        });
+    },
+
+    async fetchDescendantsWithCaching({ dispatch, rootGetters, commit }, { root }) {
         const cache = window.STUDIP.Cache.getInstance('courseware');
-        const cacheKey = `descendants/${rootElement.id}/${rootGetters['userId']}`;
+        const cacheKey = `descendants/${root.id}/${rootGetters['userId']}`;
 
-        await unpickleDescendants();
-        revalidateDescendants();
+        await unpickleStaleDescendants();
+        return revalidateDescendants();
 
-        function unpickleDescendants() {
+        function unpickleStaleDescendants() {
             try {
                 const descendants = cache.get(cacheKey);
                 const cacheHit = descendants !== undefined;
@@ -130,22 +164,7 @@ const actions = {
         }
 
         function revalidateDescendants() {
-            return loadDescendants().then(removeStaleElements).then(pickleDescendants);
-        }
-
-        function loadDescendants() {
-            const parent = { id: rootElement.id, type: rootElement.type };
-            const relationship = 'descendants';
-            const options = {
-                'page[offset]': 0,
-                'page[limit]': 10000,
-            };
-
-            return dispatch(
-                'courseware-structural-elements/loadRelated',
-                { parent, relationship, options },
-                { root: true }
-            );
+            return dispatch('loadDescendants', { root }).then(removeStaleElements).then(pickleDescendants);
         }
 
         function pickleDescendants() {
@@ -156,9 +175,9 @@ const actions = {
 
         function removeStaleElements() {
             const idsToKeep = [
-                rootElement.id,
+                root.id,
                 ...rootGetters['courseware-structural-elements/related']({
-                    parent: rootElement,
+                    parent: root,
                     relationship: 'descendants',
                 }).map(({ id }) => id),
             ];
@@ -168,11 +187,22 @@ const actions = {
                 .forEach((id) => commit('courseware-structural-elements/REMOVE_RECORD', { id }, { root: true }));
         }
     },
-};
 
-function findRoot(nodes) {
-    return nodes.find((node) => !node.relationships.parent?.data);
-}
+    loadDescendants({ dispatch }, { root }) {
+        const parent = { id: root.id, type: root.type };
+        const relationship = 'descendants';
+        const options = {
+            'page[offset]': 0,
+            'page[limit]': 10000,
+        };
+
+        return dispatch(
+            'courseware-structural-elements/loadRelated',
+            { parent, relationship, options },
+            { root: true }
+        );
+    },
+};
 
 function* visitTree(tree, current) {
     if (current) {
-- 
GitLab