import { boot } from 'quasar/wrappers'; import { FG_Plugin } from 'src/plugins'; import routes from 'src/router/routes'; import { api } from 'boot/axios'; import { AxiosResponse } from 'axios'; import { RouteRecordRaw } from 'vue-router'; import { Notify } from 'quasar'; const config: { [key: string]: Array } = { // Do not change required Modules !! requiredModules: ['User'], // here you can import plugins. loadModules: ['Balance', 'Schedule', 'Pricelist'], }; /* Stop! // do not change anything here !! // You can not even read? I said stop! // Really are you stupid? Stop scrolling down here! // Every line you scroll down, an unicorn will die painfully! // Ok you must hate unicorns... But what if I say you I joked... Baby otters will die! .-"""-. / o\ | o 0).-. | .-;(_/ .-. \ / /)).---._| `\ , '. ' /(( `'-./ _/| \ .' ) .-.;` / '. | `\-' '._ -' / ``""--`------` */ /**************************************************** ******** Internal area for some magic ************** ****************************************************/ 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 pluginName Plugin to load * @param context RequireContext of plugins * @param router VueRouter instance */ function loadPlugin( loadedPlugins: FG_Plugin.Flaschengeist, pluginName: string, context: __WebpackModuleApi.RequireContext, backend: Backend ) { // Check if already loaded if (loadedPlugins.plugins.findIndex((p) => p.name === pluginName) !== -1) return true; // Search if plugin is installed const available = context.keys(); const plugin = available.includes(`./${pluginName.toLowerCase()}/plugin.ts`) ? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access context(`./${pluginName.toLowerCase()}/plugin.ts`).default : undefined; if (!plugin) { // Plugin is not found, results in an error console.exception(`Could not find required Plugin ${pluginName}`); return false; } else { // Plugin found. Check backend dependencies if ( !plugin.requiredBackendModules.every((required) => backend.plugins[required] !== undefined) ) { console.error(`Plugin ${pluginName}: Backend modules not satisfied`); return false; } // Check frontend dependencies if ( !plugin.requiredModules.every((required) => loadPlugin(loadedPlugins, required, context, backend) ) ) { console.error(`Plugin ${pluginName}: 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 plugin; } } /** * 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 === null) { void router.push({ name: 'error' }); return; } const loadedPlugins: FG_Plugin.Flaschengeist = { routes, plugins: [], menuLinks: [], shortcuts: [], outerShortcuts: [], widgets: [], }; // get all plugins const pluginsContext = require.context('src/plugins', true, /.+\/plugin.ts$/); // Start loading plugins // Load required modules, if not found or error when loading this will forward the user to the error page config.requiredModules.forEach((required) => { const plugin = loadPlugin(loadedPlugins, required, pluginsContext, backend); if (!plugin) { void router.push({ name: 'error' }); return; } }); // Load user defined plugins // If there is an error with loading a plugin, the user will get informed. const failed: string[] = []; config.loadModules.forEach((required) => { const plugin = loadPlugin(loadedPlugins, required, pluginsContext, backend); if (!plugin) { failed.push(required); } }); if (failed.length > 0) { // Log failed plugins console.error('Could not load all plugins', failed); // Inform user about error Notify.create({ type: 'negative', message: 'Fehler beim Laden: Nicht alle Funktionen stehen zur Verfügung. Bitte wende dich an den Admin!', timeout: 10000, progress: true, }); } // Sort widgets by priority loadedPlugins.widgets.sort((a, b) => b.priority - a.priority); // Add loaded routes to router loadedPlugins.routes.forEach((route) => router.addRoute(route)); // save plugins in VM-variable app.provide('flaschengeist', loadedPlugins); });