dashboard/shell/initialize/entry-helpers.js

304 lines
7.6 KiB
JavaScript

import { updatePageTitle } from '@shell/utils/title';
import { getVendor } from '@shell/config/private-label';
import middleware from '@shell/config/middleware.js';
import {
middlewareSeries,
getMatchedComponents,
setContext,
globalHandleError,
} from '@shell/utils/nuxt.js';
// Global variable used on mount, updated on route change and used in the render function
let app;
/**
* Add error handler debugging capabilities
* @param {*} vueApp Vue instance
*/
export const loadDebugger = (vueApp) => {
const debug = process.env.dev;
if (debug) {
const defaultErrorHandler = vueApp.config.errorHandler;
vueApp.config.errorHandler = async(err, vm, info, ...rest) => {
// Call other handler if exist
let handled = null;
if (typeof defaultErrorHandler === 'function') {
handled = defaultErrorHandler(err, vm, info, ...rest);
}
if (handled === true) {
return handled;
}
if (vm && vm.$root) {
const nuxtApp = Object.keys(window.$globalApp)
.find((nuxtInstance) => vm.$root[nuxtInstance]);
// Show Nuxt Error Page
if (nuxtApp && vm.$root[nuxtApp].error && info !== 'render function') {
const vueApp = vm.$root[nuxtApp];
vueApp.error(err);
}
}
if (typeof defaultErrorHandler === 'function') {
return handled;
}
// Log to console
if (process.env.NODE_ENV !== 'production') {
console.error(err); // eslint-disable-line no-console
} else {
console.error(err.message || err); // eslint-disable-line no-console
}
};
}
};
/**
* Trigger errors
* @param {*} app App view instance
*/
const checkForErrors = (app) => {
// Hide error component if no error
if (app._hadError && app._dateLastError === app.$options.nuxt.dateErr) {
app.error();
}
};
/**
* Add middleware to the Vue instance
* @param {*} Components List of Vue components
* @param {*} context App context
* @returns
*/
function callMiddleware(Components, context) {
let midd = [];
let unknownMiddleware = false;
Components.forEach((Component) => {
if (Component.options.middleware) {
midd = midd.concat(Component.options.middleware);
}
});
midd = midd.map((name) => {
if (typeof name === 'function') {
return name;
}
if (typeof middleware[name] !== 'function') {
unknownMiddleware = true;
this.error({ statusCode: 500, message: `Unknown middleware ${ name }` });
}
return middleware[name];
});
if (unknownMiddleware) {
return;
}
return middlewareSeries(midd, context);
}
/**
* Render function used by the router guards
* @param {*} to Route
* @param {*} from Route
* @param {*} next callback
* @param {*} app
* @returns
*/
async function render(to, from, next) {
if (this._routeChanged === false && this._paramChanged === false && this._queryChanged === false) {
return next();
}
// nextCalled is true when redirected
let nextCalled = false;
const _next = (path) => {
if (from.path === path.path && this.$loading.finish) {
this.$loading.finish();
}
if (from.path !== path.path && this.$loading.pause) {
this.$loading.pause();
}
if (nextCalled) {
return;
}
nextCalled = true;
next(path);
};
// Update context
await setContext(app, {
route: to,
from,
next: _next.bind(this)
});
this._dateLastError = app.nuxt.dateErr;
this._hadError = Boolean(app.nuxt.err);
// Get route's matched components
const matches = [];
const Components = getMatchedComponents(to, matches);
// If no Components matched, generate 404
if (!Components.length) {
// Call the authenticated middleware. This used to attempt to load the error layout but because it was missing it would:
// 1. load the default layout instead
// 2. then call the authenticated middleware
// 3. Authenticated middleware would then load plugins and check to see if there was a valid route and navigate to that if it existed
// 4. This would allow harvester cluster pages to load on page reload
// We should really make authenticated middleware do less...
await callMiddleware.call(this, [{ options: { middleware: ['authenticated'] } }], app.context);
// We used to have i18n middleware which was called each time we called middleware. This is also needed to support harvester because of the way harvester loads as outlined in the comment above
await this.$store.dispatch('i18n/init');
if (nextCalled) {
return;
}
// Show error page
this.error({ statusCode: 404, message: 'This page could not be found' });
return next();
}
try {
// Call middleware
await callMiddleware.call(this, Components, app.context);
if (nextCalled) {
return;
}
if (app.context._errored) {
return next();
}
// Call middleware for layout
await callMiddleware.call(this, Components, app.context);
if (nextCalled) {
return;
}
if (app.context._errored) {
return next();
}
// Call .validate()
let isValid = true;
try {
for (const Component of Components) {
if (typeof Component.options.validate !== 'function') {
continue;
}
isValid = await Component.options.validate(app.context);
if (!isValid) {
break;
}
}
} catch (validationError) {
// ...If .validate() threw an error
this.error({
statusCode: validationError.statusCode || '500',
message: validationError.message
});
return next();
}
// ...If .validate() returned false
if (!isValid) {
this.error({ statusCode: 404, message: 'This page could not be found' });
return next();
}
// If not redirected
if (!nextCalled) {
if (this.$loading.finish && !this.$loading.manual) {
this.$loading.finish();
}
next();
}
} catch (err) {
const error = err || {};
globalHandleError(error);
this.error(error);
next();
}
}
/**
* Mounts the Vue app to the DOM element
* @param {Object} appPartials - App view partials
* @param {Object} VueClass - Vue instance
*/
export async function mountApp(appPartials, VueClass) {
// Set global variables
app = appPartials.app;
const router = appPartials.router;
// Create Vue instance
const vueApp = new VueClass(app);
// Mounts Vue app to DOM element
const mount = () => {
vueApp.$mount('#app');
};
// Initialize error handler
vueApp.$loading = {}; // To avoid error while vueApp.$nuxt does not exist
// Add beforeEach router hooks
router.beforeEach(render.bind(vueApp));
router.afterEach((from, to) => {
updatePageTitle(getVendor());
});
// First render on client-side
const clientFirstMount = () => {
checkForErrors(vueApp);
mount();
};
// fix: force next tick to avoid having same timestamp when an error happen on spa fallback
await new Promise((resolve) => setTimeout(resolve, 0));
render.call(vueApp, router.currentRoute, router.currentRoute, (path) => {
// If not redirected
if (!path) {
clientFirstMount();
return;
}
// Add a one-time afterEach hook to
// mount the app wait for redirect and route gets resolved
const unregisterHook = router.afterEach((to, from) => {
unregisterHook();
clientFirstMount();
});
// Push the path and let route to be resolved
router.push(path, undefined, (err) => {
if (err) {
const errorHandler = vueApp.config.errorHandler || console.error; // eslint-disable-line no-console
errorHandler(err);
}
});
});
}