import { Notify } from 'quasar'; import { api } from '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: Promise>>[ /*INSERT_PLUGIN_LIST*/ ], plugins: new Map(), }; 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({ 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 = 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) => (b.order || b.priority) - (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); });