352 lines
10 KiB
TypeScript
352 lines
10 KiB
TypeScript
import { Notify } from 'quasar';
|
|
import { api } from 'src/boot/axios';
|
|
import { boot } from 'quasar/wrappers';
|
|
import routes from 'src/router/routes';
|
|
import { AxiosResponse } from 'axios';
|
|
import { RouteRecordRaw } from 'vue-router';
|
|
import { FG_Plugin } from '@flaschengeist/types';
|
|
|
|
/****************************************************
|
|
******** Internal area for some magic **************
|
|
****************************************************/
|
|
|
|
declare type ImportPlgn = { default: FG_Plugin.Plugin };
|
|
|
|
function validatePlugin(plugin: FG_Plugin.Plugin) {
|
|
return (
|
|
typeof plugin.name === 'string' &&
|
|
typeof plugin.id === 'string' &&
|
|
plugin.id.length > 0 &&
|
|
typeof plugin.version === 'string'
|
|
);
|
|
}
|
|
|
|
/* eslint-disable */
|
|
// This functions are used by webpack magic
|
|
// Called when import promise resolved
|
|
function success(value: ImportPlgn, path: string) {
|
|
if (validatePlugin(value.default)) PLUGINS.plugins.set(value.default.id, value.default);
|
|
else failure(path);
|
|
}
|
|
// Called when import promise rejected
|
|
function failure(path = 'unknown') {
|
|
console.error(`Plugin ${path} could not be found and not imported`);
|
|
}
|
|
/* eslint-enable */
|
|
|
|
// Here does some magic happens, WebPack will automatically replace the following comment with the import statements
|
|
const PLUGINS = {
|
|
context: <Array<() => Promise<ImportPlgn>>>[
|
|
/*INSERT_PLUGIN_LIST*/
|
|
],
|
|
plugins: new Map<string, FG_Plugin.Plugin>(),
|
|
};
|
|
|
|
interface BackendPlugin {
|
|
permissions: string[];
|
|
version: string;
|
|
}
|
|
|
|
interface BackendPlugins {
|
|
[key: string]: BackendPlugin;
|
|
}
|
|
|
|
interface Backend {
|
|
plugins: BackendPlugins;
|
|
version: string;
|
|
}
|
|
export { Backend };
|
|
|
|
// Handle Notifications
|
|
export const translateNotification = (note: FG.Notification): FG_Plugin.Notification => note;
|
|
|
|
// Combine routes, shortcuts and widgets from plugins
|
|
|
|
/**
|
|
* Helper function, set permissions from MenuRoute to meta from RouteRecordRaw
|
|
* @param object MenuRoute to set route meta
|
|
*/
|
|
function setPermissions(object: FG_Plugin.MenuRoute) {
|
|
if (object.permissions !== undefined) {
|
|
if (object.route.meta === undefined) object.route.meta = {};
|
|
object.route.meta['permissions'] = object.permissions;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function to convert MenuRoute to the parents RouteRecordRaw
|
|
* @param parent Parent RouteRecordRaw
|
|
* @param children MenuRoute to convert
|
|
*/
|
|
function convertRoutes(parent: RouteRecordRaw, children?: FG_Plugin.MenuRoute[]) {
|
|
if (children !== undefined) {
|
|
children.forEach((child) => {
|
|
setPermissions(child);
|
|
convertRoutes(child.route, child.children);
|
|
if (parent.children === undefined) parent.children = [];
|
|
parent.children.push(child.route);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Combines routes from plugin MenuRoute to Vue-Router RouteRecordRaw to get a clean route-tree
|
|
* @param target
|
|
* @param source
|
|
* @param mainPath
|
|
*/
|
|
function combineMenuRoutes(
|
|
target: RouteRecordRaw[],
|
|
source: FG_Plugin.MenuRoute[],
|
|
mainPath: '/' | '/in' = '/'
|
|
): RouteRecordRaw[] {
|
|
// Search parent
|
|
target.forEach((target) => {
|
|
if (target.path === mainPath) {
|
|
// Parent found = target
|
|
source.forEach((sourceMainConfig: FG_Plugin.MenuRoute) => {
|
|
// Check if source is already in target
|
|
const targetMainConfig = target.children?.find((targetMainConfig: RouteRecordRaw) => {
|
|
return sourceMainConfig.route.path === targetMainConfig.path;
|
|
});
|
|
// Already in target routes, add only children
|
|
if (targetMainConfig) {
|
|
convertRoutes(targetMainConfig, sourceMainConfig.children);
|
|
} else {
|
|
// Append to target
|
|
if (target.children === undefined) {
|
|
target.children = [];
|
|
}
|
|
convertRoutes(sourceMainConfig.route, sourceMainConfig.children);
|
|
if (
|
|
sourceMainConfig.children &&
|
|
sourceMainConfig.children.length > 0 &&
|
|
!sourceMainConfig.route.component
|
|
)
|
|
Object.assign(sourceMainConfig.route, {
|
|
component: () => import('src/components/navigation/EmptyParent.vue'),
|
|
});
|
|
target.children.push(sourceMainConfig.route);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
return target;
|
|
}
|
|
|
|
function combineRoutes(
|
|
target: RouteRecordRaw[],
|
|
source: FG_Plugin.NamedRouteRecordRaw[],
|
|
mainPath: '/' | '/in'
|
|
) {
|
|
// Search parent
|
|
target.forEach((target) => {
|
|
if (target.path === mainPath) {
|
|
// Parent found = target
|
|
source.forEach((sourceRoute) => {
|
|
// Check if source is already in target
|
|
const targetRoot = target.children?.find(
|
|
(targetRoot) => sourceRoute.path === targetRoot.path
|
|
);
|
|
// Already in target routes, add only children
|
|
if (targetRoot) {
|
|
if (targetRoot.children === undefined) targetRoot.children = [];
|
|
targetRoot.children.push(...(sourceRoute.children || []));
|
|
} else {
|
|
// Append to target
|
|
if (target.children === undefined) target.children = [];
|
|
if (
|
|
sourceRoute.children &&
|
|
sourceRoute.children.length > 0 &&
|
|
sourceRoute.component === undefined
|
|
)
|
|
Object.assign(sourceRoute, {
|
|
component: () => import('src/components/navigation/EmptyParent.vue'),
|
|
});
|
|
target.children.push(sourceRoute);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Combine MenuRoutes into Flaschengeist MenuLinks for the main menu
|
|
* @param target Flaschengeist list of menu links
|
|
* @param source MenuRoutes to combine
|
|
*/
|
|
function combineMenuLinks(target: FG_Plugin.MenuLink[], source: FG_Plugin.MenuRoute) {
|
|
let idx = target.findIndex((link) => link.title == source.title);
|
|
// Link not found, add new one
|
|
if (idx === -1) {
|
|
idx += target.push({
|
|
title: source.title,
|
|
icon: source.icon,
|
|
link: source.route.name,
|
|
permissions: source.permissions,
|
|
});
|
|
}
|
|
if (target[idx].children === undefined) {
|
|
target[idx].children = [];
|
|
}
|
|
source.children?.forEach((sourceChild) => {
|
|
target[idx].children?.push({
|
|
title: sourceChild.title,
|
|
icon: sourceChild.icon,
|
|
link: sourceChild.route.name,
|
|
permissions: sourceChild.permissions,
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Combine shortcuts from Plugin MenuRouts into the Flaschenbeist Shortcut list
|
|
* @param target Flaschengeist list of shortcuts
|
|
* @param source MenuRoutes to extract shortcuts from
|
|
*/
|
|
function combineShortcuts(target: FG_Plugin.Shortcut[], source: FG_Plugin.MenuRoute[]) {
|
|
source.forEach((route) => {
|
|
if (route.shortcut) {
|
|
target.push(<FG_Plugin.Shortcut>{
|
|
link: route.route.name,
|
|
icon: route.icon,
|
|
permissions: route.permissions,
|
|
});
|
|
}
|
|
if (route.children) {
|
|
combineShortcuts(target, route.children);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Load a Flaschengeist plugin
|
|
* @param loadedPlugins Flaschgeist object
|
|
* @param plugin Plugin to load
|
|
* @param router VueRouter instance
|
|
*/
|
|
function loadPlugin(
|
|
loadedPlugins: FG_Plugin.Flaschengeist,
|
|
plugin: FG_Plugin.Plugin,
|
|
backend: Backend
|
|
) {
|
|
// Check if already loaded
|
|
if (loadedPlugins.plugins.findIndex((p) => p.name === plugin.name) !== -1) return true;
|
|
|
|
// Check backend dependencies
|
|
if (
|
|
!plugin.requiredModules.every(
|
|
(required) =>
|
|
backend.plugins[required[0]] !== undefined &&
|
|
(required.length == 1 ||
|
|
true) /* validate the version, semver440 from python is... tricky on node*/
|
|
)
|
|
) {
|
|
console.error(`Plugin ${plugin.name}: Backend modules not satisfied`);
|
|
return false;
|
|
}
|
|
|
|
// Start combining and loading routes, shortcuts etc
|
|
if (plugin.internalRoutes) {
|
|
combineRoutes(loadedPlugins.routes, plugin.internalRoutes, '/in');
|
|
}
|
|
|
|
if (plugin.innerRoutes) {
|
|
// Routes for Vue Router
|
|
combineMenuRoutes(loadedPlugins.routes, plugin.innerRoutes, '/in');
|
|
// Combine links for menu
|
|
plugin.innerRoutes.forEach((route) => combineMenuLinks(loadedPlugins.menuLinks, route));
|
|
// Combine shortcuts
|
|
combineShortcuts(loadedPlugins.shortcuts, plugin.innerRoutes);
|
|
}
|
|
|
|
if (plugin.outerRoutes) {
|
|
combineMenuRoutes(loadedPlugins.routes, plugin.outerRoutes);
|
|
combineShortcuts(loadedPlugins.outerShortcuts, plugin.outerRoutes);
|
|
}
|
|
|
|
if (plugin.widgets.length > 0) {
|
|
plugin.widgets.forEach((widget) => (widget.name = plugin.name + '_' + widget.name));
|
|
Array.prototype.push.apply(loadedPlugins.widgets, plugin.widgets);
|
|
}
|
|
|
|
loadedPlugins.plugins.push({
|
|
name: plugin.name,
|
|
version: plugin.version,
|
|
notification: plugin.notification?.bind({}) || translateNotification,
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Loading backend information
|
|
* @returns Backend object or null
|
|
*/
|
|
async function getBackend() {
|
|
try {
|
|
const { data }: AxiosResponse<Backend> = await api.get('/');
|
|
return data;
|
|
} catch (e) {
|
|
console.warn(e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Boot file, load all required plugins, check for dependencies
|
|
*/
|
|
export default boot(async ({ router, app }) => {
|
|
const backend = await getBackend();
|
|
if (!backend || typeof backend !== 'object' || !('plugins' in backend)) {
|
|
console.log('Backend error');
|
|
router.isReady().finally(() => void router.push({ name: 'offline', params: { refresh: 1 } }));
|
|
return;
|
|
}
|
|
|
|
const loadedPlugins: FG_Plugin.Flaschengeist = {
|
|
routes,
|
|
plugins: [],
|
|
menuLinks: [],
|
|
shortcuts: [],
|
|
outerShortcuts: [],
|
|
widgets: [],
|
|
};
|
|
|
|
const BreakError = {};
|
|
try {
|
|
PLUGINS.plugins.forEach((plugin, name) => {
|
|
if (!loadPlugin(loadedPlugins, plugin, backend)) {
|
|
void router.push({ name: 'error' });
|
|
|
|
Notify.create({
|
|
type: 'negative',
|
|
message: `Fehler beim Laden: Bitte wende dich an den Admin (error: PNF-${name}!`,
|
|
timeout: 10000,
|
|
progress: true,
|
|
});
|
|
throw BreakError;
|
|
}
|
|
});
|
|
} catch (e) {
|
|
if (e !== BreakError) throw e;
|
|
}
|
|
|
|
// Sort widgets by priority
|
|
/** @todo Remove priority with first beta */
|
|
loadedPlugins.widgets.sort(
|
|
(a, b) => <number>(b.order || b.priority) - <number>(a.order || a.priority)
|
|
);
|
|
/** @todo Can be cleaned up with first beta */
|
|
loadedPlugins.menuLinks.sort((a, b) => {
|
|
const diff = a.order && b.order ? b.order - a.order : 0;
|
|
return diff ? diff : a.title.toString().localeCompare(b.title.toString());
|
|
});
|
|
|
|
// Add loaded routes to router
|
|
loadedPlugins.routes.forEach((route) => router.addRoute(route));
|
|
|
|
// save plugins in VM-variable
|
|
app.provide('flaschengeist', loadedPlugins);
|
|
});
|