diff --git a/aleksis/core/assets/app/apollo.js b/aleksis/core/assets/app/apollo.js index 5d0860f7c8ad5f5d81994684b353e3c824ea9cd9..84c47c6bb5278a1a811900e168c02aa820ee9a7d 100644 --- a/aleksis/core/assets/app/apollo.js +++ b/aleksis/core/assets/app/apollo.js @@ -99,7 +99,6 @@ const links = [ }), ]; - /** Upstream Apollo GraphQL client */ const apolloClient = new ApolloClient({ typeDefs, diff --git a/aleksis/core/assets/components/app/AccountMenu.vue b/aleksis/core/assets/components/app/AccountMenu.vue new file mode 100644 index 0000000000000000000000000000000000000000..d70de72bca3ca9e2ee7e54980398ed55a3f228e6 --- /dev/null +++ b/aleksis/core/assets/components/app/AccountMenu.vue @@ -0,0 +1,73 @@ +<template> + <v-menu offset-y> + <template #activator="{ on, attrs }"> + <v-avatar v-bind="attrs" v-on="on"> + <img + v-if=" + systemProperties.sitePreferences.accountPersonPreferPhoto && + whoAmI.person.photo && + whoAmI.person.photo.url + " + :src="whoAmI.person.photo.url" + :alt="whoAmI.person.fullName" + :title="whoAmI.person.fullName" + /> + <img + v-else-if="whoAmI.person.avatarUrl" + :src="whoAmI.person.avatarUrl" + :alt="whoAmI.person.fullName + '(' + $t('person.avatar') + ')'" + :title="whoAmI.person.fullName + '(' + $t('person.avatar') + ')'" + /> + <v-icon v-else>mdi-person</v-icon> + </v-avatar> + </template> + <v-list> + <v-subheader> + {{ + $t( + whoAmI && whoAmI.isImpersonate + ? "person.impersonation.impersonating" + : "person.logged_in_as" + ) + }} + {{ whoAmI.person.fullName ? whoAmI.person.fullName : whoAmI.username }} + </v-subheader> + <v-list-item + v-if="whoAmI && whoAmI.isImpersonate" + :to="{ name: 'impersonate.stop', query: { next: $route.path } }" + > + <v-list-item-icon> + <v-icon> mdi-stop </v-icon> + </v-list-item-icon> + <v-list-item-title> + {{ $t("person.impersonation.stop") }} + </v-list-item-title> + </v-list-item> + <div v-for="menuItem in accountMenu" :key="menuItem.name"> + <v-divider v-if="menuItem.divider"></v-divider> + <v-list-item + :to="{ name: menuItem.name }" + :target="menuItem.newTab ? '_blank' : '_self'" + > + <v-list-item-icon> + <v-icon v-if="menuItem.icon">{{ menuItem.icon }}</v-icon> + </v-list-item-icon> + <v-list-item-title>{{ $t(menuItem.titleKey) }} </v-list-item-title> + </v-list-item> + </div> + </v-list> + </v-menu> +</template> + +<script> +export default { + name: "AccountMenu", + props: { + accountMenu: Array, + systemProperties: Object, + whoAmI: Object, + }, +}; +</script> + +<style></style> diff --git a/aleksis/core/assets/components/app/App.vue b/aleksis/core/assets/components/app/App.vue index baf2e69d7d22eef7f1e218926475d99f2340eca3..9c114d06c93947d41e07d1973627c373540063de 100644 --- a/aleksis/core/assets/components/app/App.vue +++ b/aleksis/core/assets/components/app/App.vue @@ -9,84 +9,11 @@ <v-app v-cloak> <splash v-if="$apollo.loading && !systemProperties" splash /> <div v-else> - <v-navigation-drawer app v-model="drawer"> - <v-list nav dense shaped> - <v-list-item class="logo"> - <a - id="logo-container" - @click="$router.push({ name: 'dashboard' })" - class="brand-logo" - > - <brand-logo - :site-preferences="systemProperties.sitePreferences" - /> - </a> - </v-list-item> - <v-list-item class="search"> - <sidenav-search /> - </v-list-item> - <v-list-item-group :value="$route.name" v-if="sideNavMenu"> - <div v-for="menuItem in sideNavMenu" :key="menuItem.name"> - <v-list-group - v-if="menuItem.subMenu.length > 0" - href="#!" - :prepend-icon="menuItem.icon" - :value="$route.matched.slice(-2).shift().name === menuItem.name" - > - <template #activator> - <v-list-item-title - >{{ $t(menuItem.titleKey) }} - </v-list-item-title> - </template> - <v-list-item - v-for="subMenuItem in menuItem.subMenu" - exact - :to="{ name: subMenuItem.name }" - :target="subMenuItem.newTab ? '_blank' : '_self'" - :key="subMenuItem.name" - :value="subMenuItem.name" - > - <v-list-item-icon> - <v-icon v-if="subMenuItem.icon" - >{{ subMenuItem.icon }} - </v-icon> - </v-list-item-icon> - <v-list-item-title - >{{ $t(subMenuItem.titleKey) }} - </v-list-item-title> - </v-list-item> - </v-list-group> - <v-list-item - v-else - exact - :to="{ name: menuItem.name }" - :target="menuItem.newTab ? '_blank' : '_self'" - :value="menuItem.name" - > - <v-list-item-icon> - <v-icon v-if="menuItem.icon">{{ menuItem.icon }}</v-icon> - </v-list-item-icon> - <v-list-item-title>{{ - $t(menuItem.titleKey) - }}</v-list-item-title> - </v-list-item> - </div> - </v-list-item-group> - <v-list-item v-else> - <v-skeleton-loader class="ma-2" type="list-item-avatar" /> - </v-list-item> - </v-list> - - <template #append> - <div class="pa-4 d-flex justify-center align-center"> - <v-spacer /> - <language-form - :available-languages="systemProperties.availableLanguages" - /> - <v-spacer /> - </div> - </template> - </v-navigation-drawer> + <side-nav + v-model="drawer" + :system-properties="systemProperties" + :side-nav-menu="sideNavMenu" + ></side-nav> <v-app-bar app :color="$vuetify.theme.dark ? undefined : 'primary white--text'" @@ -120,74 +47,11 @@ </v-btn> <div v-if="whoAmI && whoAmI.isAuthenticated" class="d-flex"> <notification-list v-if="!whoAmI.person.isDummy" /> - <v-menu offset-y> - <template #activator="{ on, attrs }"> - <v-avatar v-bind="attrs" v-on="on"> - <img - v-if=" - systemProperties.sitePreferences.accountPersonPreferPhoto && - whoAmI.person.photo && - whoAmI.person.photo.url - " - :src="whoAmI.person.photo.url" - :alt="whoAmI.person.fullName" - :title="whoAmI.person.fullName" - /> - <img - v-else-if="whoAmI.person.avatarUrl" - :src="whoAmI.person.avatarUrl" - :alt=" - whoAmI.person.fullName + '(' + $t('person.avatar') + ')' - " - :title=" - whoAmI.person.fullName + '(' + $t('person.avatar') + ')' - " - /> - <v-icon v-else>mdi-person</v-icon> - </v-avatar> - </template> - <v-list> - <v-subheader> - {{ - $t( - whoAmI && whoAmI.isImpersonate - ? "person.impersonation.impersonating" - : "person.logged_in_as" - ) - }} - {{ - whoAmI.person.fullName - ? whoAmI.person.fullName - : whoAmI.username - }} - </v-subheader> - <v-list-item - v-if="whoAmI && whoAmI.isImpersonate" - :to="{ name: 'impersonate.stop', query: { next: $route.path } }" - > - <v-list-item-icon> - <v-icon> mdi-stop </v-icon> - </v-list-item-icon> - <v-list-item-title> - {{ $t("person.impersonation.stop") }} - </v-list-item-title> - </v-list-item> - <div v-for="menuItem in accountMenu" :key="menuItem.name"> - <v-divider v-if="menuItem.divider"></v-divider> - <v-list-item - :to="{ name: menuItem.name }" - :target="menuItem.newTab ? '_blank' : '_self'" - > - <v-list-item-icon> - <v-icon v-if="menuItem.icon">{{ menuItem.icon }}</v-icon> - </v-list-item-icon> - <v-list-item-title - >{{ $t(menuItem.titleKey) }} - </v-list-item-title> - </v-list-item> - </div> - </v-list> - </v-menu> + <account-menu + :account-menu="accountMenu" + :system-properties="systemProperties" + :who-am-i="whoAmI" + ></account-menu> </div> </v-app-bar> <v-main> @@ -342,12 +206,11 @@ <script> import BroadcastChannelNotification from "./BroadcastChannelNotification.vue"; -import LanguageForm from "./LanguageForm.vue"; +import AccountMenu from "./AccountMenu.vue"; import NotificationList from "../notifications/NotificationList.vue"; -import SidenavSearch from "./SidenavSearch.vue"; import CeleryProgressBottom from "../celery_progress/CeleryProgressBottom.vue"; import Splash from "./Splash.vue"; -import BrandLogo from "./BrandLogo.vue"; +import SideNav from "./SideNav.vue"; import SnackbarItem from "./SnackbarItem.vue"; import gqlWhoAmI from "./whoAmI.graphql"; @@ -411,13 +274,12 @@ export default { }, name: "App", components: { - BrandLogo, + AccountMenu, BroadcastChannelNotification, - LanguageForm, NotificationList, - SidenavSearch, CeleryProgressBottom, Splash, + SideNav, SnackbarItem, }, mixins: [useRegisterSWMixin, offlineMixin, menusMixin], diff --git a/aleksis/core/assets/components/app/LanguageForm.vue b/aleksis/core/assets/components/app/LanguageForm.vue index 7757b9f46622ad0ba3e0158be75f85220a5f0454..355f719e45eb6e77f2ca6e439687986cbbf788de 100644 --- a/aleksis/core/assets/components/app/LanguageForm.vue +++ b/aleksis/core/assets/components/app/LanguageForm.vue @@ -8,6 +8,7 @@ menu-props="auto" outlined color="primary" + hide-details="auto" single-line return-object dense diff --git a/aleksis/core/assets/components/app/SideNav.vue b/aleksis/core/assets/components/app/SideNav.vue new file mode 100644 index 0000000000000000000000000000000000000000..e3452851ffa0f79768c02163177acc98e63114da --- /dev/null +++ b/aleksis/core/assets/components/app/SideNav.vue @@ -0,0 +1,95 @@ +<template> + <v-navigation-drawer app> + <v-list nav dense shaped> + <v-list-item class="logo"> + <a + id="logo-container" + @click="$router.push({ name: 'dashboard' })" + class="brand-logo" + > + <brand-logo :site-preferences="systemProperties.sitePreferences" /> + </a> + </v-list-item> + <v-list-item class="search"> + <sidenav-search /> + </v-list-item> + <v-list-item-group :value="$route.name" v-if="sideNavMenu"> + <div v-for="menuItem in sideNavMenu" :key="menuItem.name"> + <v-list-group + v-if="menuItem.subMenu.length > 0" + href="#!" + :prepend-icon="menuItem.icon" + :value="$route.matched.slice(-2).shift().name === menuItem.name" + > + <template #activator> + <v-list-item-title + >{{ $t(menuItem.titleKey) }} + </v-list-item-title> + </template> + <v-list-item + v-for="subMenuItem in menuItem.subMenu" + exact + :to="{ name: subMenuItem.name }" + :target="subMenuItem.newTab ? '_blank' : '_self'" + :key="subMenuItem.name" + :value="subMenuItem.name" + > + <v-list-item-icon> + <v-icon v-if="subMenuItem.icon">{{ subMenuItem.icon }} </v-icon> + </v-list-item-icon> + <v-list-item-title + >{{ $t(subMenuItem.titleKey) }} + </v-list-item-title> + </v-list-item> + </v-list-group> + <v-list-item + v-else + exact + :to="{ name: menuItem.name }" + :target="menuItem.newTab ? '_blank' : '_self'" + :value="menuItem.name" + > + <v-list-item-icon> + <v-icon v-if="menuItem.icon">{{ menuItem.icon }}</v-icon> + </v-list-item-icon> + <v-list-item-title>{{ $t(menuItem.titleKey) }}</v-list-item-title> + </v-list-item> + </div> + </v-list-item-group> + <template v-else> + <v-skeleton-loader class="ma-2" type="list-item-avatar@5" /> + </template> + </v-list> + + <template #append> + <div class="pa-4 d-flex justify-center align-center"> + <v-spacer /> + <language-form + :available-languages="systemProperties.availableLanguages" + /> + <v-spacer /> + </div> + </template> + </v-navigation-drawer> +</template> + +<script> +import BrandLogo from "./BrandLogo.vue"; +import LanguageForm from "./LanguageForm.vue"; +import SidenavSearch from "./SidenavSearch.vue"; + +export default { + name: "SideNav", + components: { + BrandLogo, + LanguageForm, + SidenavSearch, + }, + props: { + sideNavMenu: Array, + systemProperties: Object, + }, +}; +</script> + +<style scoped></style> diff --git a/aleksis/core/assets/components/person/AdditionalImage.vue b/aleksis/core/assets/components/person/AdditionalImage.vue index 943c8a198cd8373d0d4f42b75613c7b527b5629f..f483705806bb5eb718ccf136a4773b4a5bac7130 100644 --- a/aleksis/core/assets/components/person/AdditionalImage.vue +++ b/aleksis/core/assets/components/person/AdditionalImage.vue @@ -13,14 +13,7 @@ </v-card> </template> <v-sheet - class=" - d-flex - justify-center - align-center - flex-column - text-center - transparent - " + class="d-flex justify-center align-center flex-column text-center transparent" > <v-img :src="src" diff --git a/aleksis/core/assets/index.js b/aleksis/core/assets/index.js index d681aca2f7a7e48e9a446baf5ab248126e991102..459978ed1d533e73ea5b03bf8f8ef700cefcfeb7 100644 --- a/aleksis/core/assets/index.js +++ b/aleksis/core/assets/index.js @@ -1,6 +1,6 @@ /* * Main entrypoint of AlekSIS0-ore. - * + * * This script sets up all necessary Vue plugins and defines the Vue app. */ diff --git a/aleksis/core/assets/mixins/menus.js b/aleksis/core/assets/mixins/menus.js index a7bace279eac72bf5b724044061d23d1ba4d30a0..8afed7f424ebf1bd4b33b316dc311f14831161fc 100644 --- a/aleksis/core/assets/mixins/menus.js +++ b/aleksis/core/assets/mixins/menus.js @@ -2,7 +2,7 @@ import gqlCustomMenu from "../components/app/customMenu.graphql"; /** * Vue mixin containing menu generation code. - * + * * Only used by main App component, but factored out for readability. */ const menusMixin = { diff --git a/aleksis/core/assets/mixins/offline.js b/aleksis/core/assets/mixins/offline.js index 40b10b3b41ba1a8d15585e37ef7e05789a534db2..7bdd551a33a1361120a41b448cd7aa640935e40d 100644 --- a/aleksis/core/assets/mixins/offline.js +++ b/aleksis/core/assets/mixins/offline.js @@ -2,12 +2,12 @@ import gqlPing from "../components/app/ping.graphql"; /** * Mixin for handling of offline state / background queries. - * + * * This handles three scenarios: * - The navigator reports that it is in offline mode * - The global offline flag was set due to network errors from queries * - The navigator reports the page to be invisible - * + * * The main goal is to save bandwidth, energy and server load in error * conditions, or when the page is not in focus. This is achieved by a * fallback strategy, where all background queries are stopped in offline diff --git a/aleksis/core/assets/routeValidators.js b/aleksis/core/assets/routeValidators.js index af9a2ad41c4c98e6c62c116227a4ba7c4fef7d05..1df949d0349412be9a50f81c53837dab7210d80b 100644 --- a/aleksis/core/assets/routeValidators.js +++ b/aleksis/core/assets/routeValidators.js @@ -1,6 +1,6 @@ /** * Check whether the user is logged in on the AlekSIS server. - * + * * @param {Object} whoAmI The person object as returned by the whoAmI query * @returns true if the user is logged in, false if not */ diff --git a/aleksis/core/assets/routes.js b/aleksis/core/assets/routes.js index d969797e8e591363c4fcc5d24c0b7e4cd1fc5ab7..0a9572671ca4fe72ffddd4ed3e46704b82782a8b 100644 --- a/aleksis/core/assets/routes.js +++ b/aleksis/core/assets/routes.js @@ -1,6 +1,6 @@ /* * Vue router definitions for all of AlekSIS. - * + * * This module defines the routes of AlekSIS-Core and also loads * and adds all routes from known apps. */ diff --git a/aleksis/core/vite.config.js b/aleksis/core/vite.config.js index 3f18652120fdd11e74932ed9a0c598a679d8b36b..7954a54fb9d9327f9a5a2a38510d064b4c41d099 100644 --- a/aleksis/core/vite.config.js +++ b/aleksis/core/vite.config.js @@ -147,7 +147,9 @@ export default defineConfig({ base: "/", workbox: { navigateFallback: "/", - navigateFallbackAllowlist: [new RegExp("^/(?!(django|admin|graphql|__icons__))[^.]*$")], + navigateFallbackAllowlist: [ + new RegExp("^/(?!(django|admin|graphql|__icons__))[^.]*$"), + ], additionalManifestEntries: ["/", "/django/offline/"], inlineWorkboxRuntime: true, modifyURLPrefix: { @@ -156,7 +158,9 @@ export default defineConfig({ globPatterns: ["**/*.{js,css,eot,woff,woff2,ttf}"], runtimeCaching: [ { - urlPattern: new RegExp("^/(?!(django|admin|graphql|__icons__))[^.]*$"), + urlPattern: new RegExp( + "^/(?!(django|admin|graphql|__icons__))[^.]*$" + ), handler: "CacheFirst", }, {