Skip to content
Snippets Groups Projects
Verified Commit 59587d87 authored by Nik | Klampfradler's avatar Nik | Klampfradler
Browse files

[Refactor] Comment JS infrastructure code

parent 9369709a
No related branches found
No related tags found
1 merge request!1123Resolve "Finalise Vuetify app as SPA"
Pipeline #107984 failed
/*
* Configuration for Apollo provider, client, and caches.
*/
import { ApolloClient, HttpLink, from } from "@/apollo-boost";
import VueApollo from "@/vue-apollo";
import { onError } from "@/apollo-link-error";
import { RetryLink } from "@/apollo-link-retry";
......@@ -10,8 +13,17 @@ import gql from "@/graphql-tag";
import gqlSnackbarItems from "../components/app/snackbarItems.graphql";
// Cache for GraphQL query results in memory and persistent across sessions
const cache = new InMemoryCache();
await persistCache({
cache: cache,
storage: new LocalStorageWrapper(window.localStorage),
});
/**
* Type definitions for Apollo/s local state management
* cf. https://www.apollographql.com/docs/react/local-state/local-state-management/
*/
const typeDefs = gql`
type snackbarItem {
id: ID!
......@@ -26,28 +38,27 @@ const typeDefs = gql`
}
type Mutation {
checkSnackbarItem(id: ID!): Boolean
setLoading(state: Boolean!): Boolean
toggleSnackbarItem(id: ID!): Boolean
}
`;
/** Resolvers for local state management */
const resolvers = {
Mutation: {
// eslint-disable-next-line no-unused-vars
checkSnackbarItem: (_, { id }, { cache }) => {
/** Toggle snackbar item read or unread, given its ID */
toggleSnackbarItem: (_, { id }, { cache }) => {
const data = cache.readQuery({ query: gqlSnackbarItems });
const currentItem = data.snackbarItems.find((item) => item.id === id);
currentItem.read = !currentItem.read;
cache.writeQuery({ query: gqlSnackbarItems, data });
return currentItem.read;
},
// eslint-disable-next-line
setLoading: (_, { state }, {}) => {
return true;
},
},
};
/**
* Utility function to add a snackbar on GraphQL errors.
*/
function addErrorSnackbarItem(messageKey) {
let uuid = crypto.randomUUID();
cache.writeQuery({
......@@ -69,28 +80,32 @@ function addErrorSnackbarItem(messageKey) {
});
}
const retryLink = new RetryLink();
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) addErrorSnackbarItem("graphql.snackbar_error_message");
if (networkError) app.offline = true;
});
const httpLink = new HttpLink({
uri: window.location.origin + "/graphql/",
});
await persistCache({
cache: cache,
storage: new LocalStorageWrapper(window.localStorage),
});
// Define Apollo links for handling query operations.
const links = [
// Automatically retry failed queries
new RetryLink(),
// Add custom error handlers
onError(({ graphQLErrors, networkError }) => {
// Add a snackbar on all errors returned by the GraphQL endpoint
if (graphQLErrors) addErrorSnackbarItem("graphql.snackbar_error_message");
// 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
if (networkError) app.offline = true;
}),
// Finally, the HTTP link to the real backend (Django)
new HttpLink({
uri: window.location.origin + "/graphql/",
}),
];
/** Upstream Apollo GraphQL client */
const apolloClient = new ApolloClient({
typeDefs,
resolvers,
cache,
link: from([retryLink, errorLink, httpLink]),
link: from(links),
});
const apolloOpts = {
......
/** Date.time formats for VueI18n */
const dateTimeFormats = {
en: {
short: {
......
/*
* Configuration for VueI18n
*/
import dateTimeFormats from "./dateTimeFormats.js";
const i18nOpts = {
......
/*
* Configuration for Vue router
*/
import routes from "../routes.js";
const routerOpts = {
......
/*
* Configuration for Vuetify
*/
import "@/@mdi/font/css/materialdesignicons.css";
import "@/vuetify/dist/vuetify.min.css";
......
......@@ -2,7 +2,7 @@
<v-snackbar :value="!snackbarItem.read" :color="snackbarItem.color">
{{ $t(snackbarItem.messageKey) }}
<template #action="{ attrs }">
<v-btn icon @click="checkSnackbarItem(snackbarItem.id)"
<v-btn icon @click="toggleSnackbarItem(snackbarItem.id)"
><v-icon>mdi-close</v-icon>
</v-btn>
</template>
......@@ -10,7 +10,7 @@
</template>
<script>
import gqlCheckSnackbarItem from "./checkSnackbarItem.graphql";
import gqlCheckSnackbarItem from "./toggleSnackbarItem.graphql";
export default {
name: "SnackbarItem",
......@@ -21,7 +21,7 @@ export default {
},
},
methods: {
checkSnackbarItem(id) {
toggleSnackbarItem(id) {
this.$apollo.mutate({
mutation: gqlCheckSnackbarItem,
variables: { id },
......
mutation ($id: String!) {
checkSnackbarItem(id: $id) @client
toggleSnackbarItem(id: $id) @client
}
/*
* Main entrypoint of AlekSIS0-ore.
*
* This script sets up all necessary Vue plugins and defines the Vue app.
* For convenience, the Vue app instance will be made globally available
* as window.app.
*/
import Vue from "vue";
import Vuetify from "@/vuetify";
import VueI18n from "@/vue-i18n";
......@@ -6,15 +14,18 @@ import VueApollo from "@/vue-apollo";
import AleksisVue from "./plugins/aleksis.js";
// Install the AleksisVue plugin first and let it do early setup
Vue.use(AleksisVue);
Vue.$configureSentry();
Vue.$registerGlobalComponents();
// Third-party plugins
Vue.use(Vuetify);
Vue.use(VueI18n);
Vue.use(VueRouter);
Vue.use(VueApollo);
// All of these imports yield config objects to be passed to the plugin constructors
import vuetifyOpts from "./app/vuetify.js";
import i18nOpts from "./app/i18n.js";
import routerOpts from "./app/router.js";
......@@ -25,6 +36,7 @@ const vuetify = new Vuetify(vuetifyOpts);
const router = new VueRouter(routerOpts);
const apolloProvider = new VueApollo(apolloOpts);
// Parent component rendering the UI and all features outside the specific pages
import App from "./components/app/App.vue";
const app = new Vue({
......@@ -41,6 +53,7 @@ const app = new Vue({
i18n,
});
// Late setup for some plugins handed off to out ALeksisVue plugin
app.$loadAppMessages();
app.$setupNavigationGuards();
......
export default {
/**
* Vue mixin to register the PWA service worker once the main
* component gets ready.
*/
const useRegisterSWMixin = {
name: "useRegisterSW",
data() {
return {
......@@ -55,3 +59,5 @@ export default {
},
},
};
export default useRegisterSWMixin;
/*
* Plugin to collect AlekSIS-specific Vue utilities.
*/
// aleksisAppImporter is a virtual module defined in Vite config
import { appObjects, appMessages } from "aleksisAppImporter";
import MessageBox from "../components/MessageBox.vue";
const AleksisVue = {};
console.debug("Defining AleksisVue plugin");
const AleksisVue = {};
AleksisVue.install = function (Vue, options) {
/**
* The browser title when the app was loaded.
*
* Thus, it is injected from Django in the vue_index template.
*/
Vue.$pageBaseTitle = document.title;
/**
* Configure Sentry if desired.
*
* It depends on Sentry settings being passed as a DOM object by Django
* in the vue_index template.
*/
Vue.$configureSentry = function () {
if (document.getElementById("sentry_settings") !== null) {
const Sentry = import("@sentry/vue");
......@@ -32,9 +47,21 @@ AleksisVue.install = function (Vue, options) {
}
};
/**
* Register all global components that shall be reusable by apps.
*/
Vue.$registerGlobalComponents = function () {
Vue.component(MessageBox.name, MessageBox);
Vue.component("message-box", () => import("../components/MessageBox.vue"));
};
/**
* Set the page title.
*
* This will automatically add the base title discovered at app loading time.
*
* @param {string} title Specific title to set, or null.
* @param {Object} route Route to discover title from, or null.
*/
Vue.prototype.$setPageTitle = function (title, route) {
let titleParts = [];
......@@ -55,6 +82,9 @@ AleksisVue.install = function (Vue, options) {
document.title = newTitle;
};
/**
* Load i18n messages from all known AlekSIS apps.
*/
Vue.prototype.$loadAppMessages = function () {
console.log(this);
for (const messages of Object.values(appMessages)) {
......@@ -64,9 +94,13 @@ AleksisVue.install = function (Vue, options) {
}
};
/**
* Add navigation guards to account for global loading state and page titles.
*/
Vue.prototype.$setupNavigationGuards = function () {
// eslint-disable-next-line no-unused-vars
this.$router.afterEach((to, from, next) => {
console.debug("Setting new page title due to route change");
window.app.$setPageTitle(null, to);
});
......@@ -80,13 +114,6 @@ AleksisVue.install = function (Vue, options) {
this.$router.afterEach((to, from) => {
window.app.contentLoading = false;
});
// eslint-disable-next-line no-unused-vars
this.$router.beforeEach((to, from, next) => {
console.debug("Setting new page title due to route change");
window.app.$setPageTitle(null, to);
next();
});
};
};
......
/**
* 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
*/
const notLoggedInValidator = (whoAmI) => {
return !whoAmI || whoAmI.isAnonymous;
};
......
/*
* 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.
*/
// aleksisAppImporter is a virtual module defined in Vite config
import { appObjects } from "aleksisAppImporter";
import { notLoggedInValidator } from "./routeValidators";
......@@ -772,6 +780,7 @@ for (const [appName, appRoutes] of Object.entries(appObjects)) {
});
}
// Fallback route defined last to ensure 404 view works
routes.push({
path: "/*",
component: () => import("./components/app/Error404.vue"),
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment