From 15429d184fdf4b14395717e12461bd30f368add8 Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Sat, 8 Apr 2023 15:11:54 +0200
Subject: [PATCH] Improve lifecycle of SPA

---
 CHANGELOG.rst                                 |  8 ++++++++
 aleksis/core/apps.py                          |  6 ++++++
 aleksis/core/frontend/app/apollo.js           |  4 ++--
 .../components/LegacyBaseTemplate.vue         | 19 +++++++++++++++----
 aleksis/core/frontend/components/app/App.vue  | 17 ++++++++++++++---
 aleksis/core/frontend/index.js                | 16 +++++++++++++++-
 aleksis/core/frontend/mixins/aleksis.js       |  2 +-
 aleksis/core/frontend/plugins/aleksis.js      | 14 ++++++++++++--
 8 files changed, 73 insertions(+), 13 deletions(-)

diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 4f114a3f4..8be5817d9 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -15,11 +15,19 @@ Added
 * GraphQL schema for Rooms
 * [Dev] UpdateIndicator Vue Component to display the status of interactive pages
 
+Changed
+~~~~~~~
+
+* Show message on successful logout to inform users properly.
 
 Fixed
 ~~~~~
 
 * GraphQL endpoints for groups, persons, and notifications didn't expose all necessary fields.
+* Loading indicator in toolbar was not shown at the complete loading progress.
+* 404 page was sometimes shown while the page was still loading.
+* Setting of page height in the iframe was not working correctly.
+* App switched to offline state when the user was logged out/in.
 
 `3.0b3`_ - 2023-03-19
 ---------------------
diff --git a/aleksis/core/apps.py b/aleksis/core/apps.py
index 9b0518472..0287a0fd0 100644
--- a/aleksis/core/apps.py
+++ b/aleksis/core/apps.py
@@ -3,6 +3,7 @@ from typing import Any, Optional
 import django.apps
 from django.apps import apps
 from django.conf import settings
+from django.contrib import messages
 from django.http import HttpRequest
 from django.utils.module_loading import autodiscover_modules
 from django.utils.translation import gettext as _
@@ -144,6 +145,11 @@ class CoreConfig(AppConfig):
             # Save the associated person to pick up defaults
             user.person.save()
 
+    def user_logged_out(
+        self, sender: type, request: Optional[HttpRequest], user: "User", **kwargs
+    ) -> None:
+        messages.success(request, _("You have been logged out successfully."))
+
     @classmethod
     def get_all_scopes(cls) -> dict[str, str]:
         scopes = {
diff --git a/aleksis/core/frontend/app/apollo.js b/aleksis/core/frontend/app/apollo.js
index 1b23d43fb..267997b9f 100644
--- a/aleksis/core/frontend/app/apollo.js
+++ b/aleksis/core/frontend/app/apollo.js
@@ -70,7 +70,7 @@ const apolloOpts = {
           }
           // Add a snackbar on all errors returned by the GraphQL endpoint
           //  If App is offline, don't add snackbar since only the ping query is active
-          if (!vm.$root.offline) {
+          if (!vm.$root.offline && !vm.$root.invalidation) {
             vm.$root.snackbarItems.push({
               id: crypto.randomUUID(),
               timeout: 5000,
@@ -79,7 +79,7 @@ const apolloOpts = {
             });
           }
         }
-        if (networkError) {
+        if (networkError && !vm.$root.invalidation) {
           // Set app offline globally on network errors
           //  This will cause the offline logic to kick in, starting a ping check or
           //  similar recovery strategies depending on the app/navigator state
diff --git a/aleksis/core/frontend/components/LegacyBaseTemplate.vue b/aleksis/core/frontend/components/LegacyBaseTemplate.vue
index d28621a14..30ef189b7 100644
--- a/aleksis/core/frontend/components/LegacyBaseTemplate.vue
+++ b/aleksis/core/frontend/components/LegacyBaseTemplate.vue
@@ -78,12 +78,23 @@ export default {
       this.$root.$setPageTitle(title);
 
       // Adapt height of IFrame according to the height of its contents once and observe height changes
-      this.iFrameHeight =
-        this.$refs.contentIFrame.contentDocument.body.scrollHeight;
-      new ResizeObserver(() => {
+      if (
+        this.$refs.contentIFrame.contentDocument &&
+        this.$refs.contentIFrame.contentDocument.body
+      ) {
         this.iFrameHeight =
           this.$refs.contentIFrame.contentDocument.body.scrollHeight;
-      }).observe(this.$refs.contentIFrame.contentDocument.body);
+        new ResizeObserver(() => {
+          if (
+            this.$refs.contentIFrame &&
+            this.$refs.contentIFrame.contentDocument &&
+            this.$refs.contentIFrame.contentDocument.body
+          ) {
+            this.iFrameHeight =
+              this.$refs.contentIFrame.contentDocument.body.scrollHeight;
+          }
+        }).observe(this.$refs.contentIFrame.contentDocument.body);
+      }
 
       this.$root.contentLoading = false;
     },
diff --git a/aleksis/core/frontend/components/app/App.vue b/aleksis/core/frontend/components/app/App.vue
index e11725a36..6db2abd0a 100644
--- a/aleksis/core/frontend/components/app/App.vue
+++ b/aleksis/core/frontend/components/app/App.vue
@@ -45,7 +45,10 @@
         >
           <v-icon>mdi-update</v-icon>
         </v-btn>
-        <div v-if="whoAmI && whoAmI.isAuthenticated" class="d-flex">
+        <div
+          v-if="whoAmI && whoAmI.isAuthenticated && whoAmI.person"
+          class="d-flex"
+        >
           <notification-list v-if="!whoAmI.person.isDummy" />
           <account-menu
             :account-menu="accountMenu"
@@ -83,7 +86,7 @@
           </div>
 
           <error-page
-            v-if="error404"
+            v-if="error404 && !$root.contentLoading"
             short-error-message-key="network_errors.error_404"
             long-error-message-key="network_errors.page_not_found"
             redirect-button-text-key="network_errors.back_to_start"
@@ -97,6 +100,7 @@
               checkPermission($route.meta.permission) ||
               $route.name === 'dashboard'
             "
+            @mounted="routeComponentMounted"
           />
           <error-page
             v-else-if="
@@ -253,6 +257,13 @@ export default {
       pollInterval: 1000,
     },
   },
+  methods: {
+    routeComponentMounted() {
+      if (!this.$root.isLegacyBaseTemplate) {
+        this.$root.contentLoading = false;
+      }
+    },
+  },
   watch: {
     systemProperties: function (newProperties) {
       this.$vuetify.theme.themes.light.primary =
@@ -272,7 +283,7 @@ export default {
     },
     $route: {
       handler(newRoute) {
-        if (newRoute.matched.length == 0) {
+        if (newRoute.matched.length === 0) {
           this.error404 = true;
         } else {
           this.error404 = false;
diff --git a/aleksis/core/frontend/index.js b/aleksis/core/frontend/index.js
index 25df11b34..f59aa8578 100644
--- a/aleksis/core/frontend/index.js
+++ b/aleksis/core/frontend/index.js
@@ -65,11 +65,25 @@ const app = new Vue({
   render: (h) => h(App),
   data: () => ({
     showCacheAlert: false,
-    contentLoading: false,
+    contentLoading: true,
     offline: false,
     backgroundActive: true,
+    invalidation: false,
     snackbarItems: [],
   }),
+  computed: {
+    matchedComponents() {
+      if (this.$route.matched.length > 0) {
+        return this.$route.matched.map(
+          (route) => route.components.default.name
+        );
+      }
+      return [];
+    },
+    isLegacyBaseTemplate() {
+      return this.matchedComponents.includes("LegacyBaseTemplate");
+    },
+  },
   router,
   i18n,
 });
diff --git a/aleksis/core/frontend/mixins/aleksis.js b/aleksis/core/frontend/mixins/aleksis.js
index 036e2eac7..59204a7f6 100644
--- a/aleksis/core/frontend/mixins/aleksis.js
+++ b/aleksis/core/frontend/mixins/aleksis.js
@@ -20,7 +20,7 @@ const aleksisMixin = {
     },
   },
   mounted() {
-    this.$root.contentLoading = false;
+    this.$emit("mounted");
   },
   beforeDestroy() {
     // Unregister all safely added event listeners as to not leak them
diff --git a/aleksis/core/frontend/plugins/aleksis.js b/aleksis/core/frontend/plugins/aleksis.js
index 8c221c6d6..c35b9127f 100644
--- a/aleksis/core/frontend/plugins/aleksis.js
+++ b/aleksis/core/frontend/plugins/aleksis.js
@@ -123,15 +123,19 @@ AleksisVue.install = function (Vue) {
   Vue.prototype.$invalidateState = function () {
     console.info("Invalidating application state");
 
+    this.invalidation = true;
+
     this.$apollo
       .getClient()
       .resetStore()
       .then(
-        function () {
+        () => {
           console.info("GraphQL cache cleared");
+          this.invalidation = false;
         },
-        function (error) {
+        (error) => {
           console.error("Could not clear GraphQL cache:", error);
+          this.invalidation = false;
         }
       );
   };
@@ -156,6 +160,12 @@ AleksisVue.install = function (Vue) {
 
     // eslint-disable-next-line no-unused-vars
     this.$router.afterEach((to, from) => {
+      if (vm.isLegacyBaseTemplate) {
+        // Skip resetting loading state for legacy pages
+        // as they are probably not finished with loading yet
+        // LegacyBaseTemplate will reset the loading state later
+        return;
+      }
       vm.contentLoading = false;
     });
 
-- 
GitLab