[Vue3][Quasar2] Fixed Store and Router, breaking changes in both.

This commit is contained in:
Ferdinand Thiessen 2021-01-30 04:19:30 +01:00
parent 0efe445864
commit d147e538d1
9 changed files with 157 additions and 134 deletions

View File

@ -1,12 +1,11 @@
import { boot } from 'quasar/wrappers'; import { boot } from 'quasar/wrappers';
import { RouteConfig } from 'vue-router';
import { VueRouter } from 'vue-router/types/router';
import { Store } from 'vuex'; import { Store } from 'vuex';
import { StateInterface } from 'src/store'; import { StateInterface } from 'src/store';
import { FG_Plugin } from 'src/plugins'; import { FG_Plugin } from 'src/plugins';
import routes from 'src/router/routes'; import routes from 'src/router/routes';
import { axios } from 'boot/axios'; import { axios } from 'boot/axios';
import { AxiosResponse } from 'axios'; import { AxiosResponse } from 'axios';
import { Router, RouteRecordRaw } from 'vue-router';
const config = { const config = {
// Do not change required Modules !! // Do not change required Modules !!
@ -35,21 +34,27 @@ interface Backend {
export { Backend }; export { Backend };
function setPermissions(object: FG_Plugin.PluginRouteConfig) {
if (object.route.meta === undefined) object.route.meta = {};
object.route.meta['permissions'] = object.permissions;
}
function combineRoutes( function combineRoutes(
target: RouteConfig[], target: RouteRecordRaw[],
source: FG_Plugin.PluginRouteConfig[], source: FG_Plugin.PluginRouteConfig[],
mainPath: '/' | '/main' = '/' mainPath: '/' | '/main' = '/'
): RouteConfig[] { ): RouteRecordRaw[] {
target.forEach((target) => { target.forEach((target) => {
if (target.path === mainPath) { if (target.path === mainPath) {
source.forEach((sourceMainConfig: FG_Plugin.PluginRouteConfig) => { source.forEach((sourceMainConfig: FG_Plugin.PluginRouteConfig) => {
const targetMainConfig = target.children?.find((targetMainConfig: RouteConfig) => { const targetMainConfig = target.children?.find((targetMainConfig: RouteRecordRaw) => {
return sourceMainConfig.path === targetMainConfig.path; return sourceMainConfig.route.path === targetMainConfig.path;
}); });
if (targetMainConfig) { if (targetMainConfig) {
const sourceChildren: RouteConfig[] = []; const sourceChildren: RouteRecordRaw[] = [];
sourceMainConfig.children?.forEach((child) => { sourceMainConfig.children?.forEach((child) => {
sourceChildren.push(<RouteConfig>child); setPermissions(child);
sourceChildren.push(child.route);
}); });
if (targetMainConfig.children) { if (targetMainConfig.children) {
targetMainConfig.children = Object.assign(targetMainConfig.children, sourceChildren); targetMainConfig.children = Object.assign(targetMainConfig.children, sourceChildren);
@ -63,10 +68,14 @@ function combineRoutes(
if ( if (
sourceMainConfig.children && sourceMainConfig.children &&
sourceMainConfig.children.length > 0 && sourceMainConfig.children.length > 0 &&
!sourceMainConfig.component !sourceMainConfig.route.component
) )
sourceMainConfig.component = () => import('src/components/navigation/EmptyParent.vue'); target.children.push({
target.children.push(<RouteConfig>sourceMainConfig); component: () => import('src/components/navigation/EmptyParent.vue'),
name: sourceMainConfig.route.name,
path: sourceMainConfig.route.path,
});
else target.children.push(sourceMainConfig.route);
} }
}); });
} }
@ -90,18 +99,18 @@ function combineMainLinks(
targetPluginMainLink.children.push(<FG_Plugin.PluginChildLink>{ targetPluginMainLink.children.push(<FG_Plugin.PluginChildLink>{
title: sourcePluginChildLink.title, title: sourcePluginChildLink.title,
icon: sourcePluginChildLink.icon, icon: sourcePluginChildLink.icon,
link: sourcePluginChildLink.name, link: sourcePluginChildLink.route.name,
name: sourcePluginChildLink.name, name: sourcePluginChildLink.route.name,
permissions: sourcePluginChildLink.meta?.permissions, permissions: sourcePluginChildLink.permissions,
}); });
}); });
} else { } else {
const mainLink: FG_Plugin.PluginMainLink = <FG_Plugin.PluginMainLink>{ const mainLink: FG_Plugin.PluginMainLink = <FG_Plugin.PluginMainLink>{
title: source.title, title: source.title,
icon: source.icon, icon: source.icon,
link: source.name, link: source.route.name,
name: source.name, name: source.route.name,
permissions: source.meta?.permissions, permissions: source.permissions,
}; };
source.children?.forEach((child) => { source.children?.forEach((child) => {
if (mainLink.children === undefined) { if (mainLink.children === undefined) {
@ -110,9 +119,9 @@ function combineMainLinks(
mainLink.children.push(<FG_Plugin.PluginChildLink>{ mainLink.children.push(<FG_Plugin.PluginChildLink>{
title: child.title, title: child.title,
icon: child.icon, icon: child.icon,
link: child.name, link: child.route.name,
name: child.name, name: child.route.name,
permissions: child.meta?.permissions, permissions: child.permissions,
}); });
}); });
target.push(mainLink); target.push(mainLink);
@ -127,9 +136,9 @@ function loadShortCuts(
source.forEach((route) => { source.forEach((route) => {
if (route.shortcut) { if (route.shortcut) {
target.push(<FG_Plugin.ShortCutLink>{ target.push(<FG_Plugin.ShortCutLink>{
link: route.name, link: route.route.name,
icon: route.icon, icon: route.icon,
permissions: route.meta?.permissions, permissions: route.permissions,
}); });
} }
if (route.children) { if (route.children) {
@ -146,7 +155,7 @@ function loadPlugin(
backendpromise: Promise<Backend | null>, backendpromise: Promise<Backend | null>,
plugins: FG_Plugin.Plugin[], plugins: FG_Plugin.Plugin[],
store: Store<any>, store: Store<any>,
router: VueRouter router: Router
): FG_Plugin.LoadedPlugins { ): FG_Plugin.LoadedPlugins {
modules.forEach((requiredModule) => { modules.forEach((requiredModule) => {
const plugin = plugins.find((plugin) => { const plugin = plugins.find((plugin) => {
@ -202,7 +211,7 @@ async function getBackend(): Promise<Backend | null> {
// "async" is optional; // "async" is optional;
// more info on params: https://quasar.dev/quasar-cli/cli-documentation/boot-files#Anatomy-of-a-boot-file // more info on params: https://quasar.dev/quasar-cli/cli-documentation/boot-files#Anatomy-of-a-boot-file
export default boot<Store<StateInterface>>(({ Vue, router, store }) => { export default boot<Store<StateInterface>>(({ router, store, app }) => {
const plugins: FG_Plugin.Plugin[] = []; const plugins: FG_Plugin.Plugin[] = [];
const backendPromise = getBackend(); const backendPromise = getBackend();
@ -279,10 +288,8 @@ export default boot<Store<StateInterface>>(({ Vue, router, store }) => {
loadedPlugins.widgets.sort((a, b) => b.priority - a.priority); loadedPlugins.widgets.sort((a, b) => b.priority - a.priority);
// add new routes for plugins, flatten them to allow empty parent routes loadedPlugins.routes.forEach((route) => router.addRoute(route));
router.addRoutes(loadedPlugins.routes);
// save plugins in VM-variable // save plugins in VM-variable
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access app.provide('flaschengeistPlugins', loadedPlugins);
Vue.prototype.$flaschengeistPlugins = loadedPlugins;
}); });

View File

@ -102,10 +102,10 @@
import EssentialLink from 'components/navigation/EssentialLink.vue'; import EssentialLink from 'components/navigation/EssentialLink.vue';
import ShortCutLink from 'components/navigation/ShortCutLink.vue'; import ShortCutLink from 'components/navigation/ShortCutLink.vue';
import { Screen, Loading } from 'quasar'; import { Screen, Loading } from 'quasar';
import { defineComponent, ref, computed } from '@vue/composition-api'; import { defineComponent, ref, inject, computed } from 'vue';
import { Store } from 'vuex';
import { StateInterface } from 'src/store';
import { FG_Plugin } from 'src/plugins'; import { FG_Plugin } from 'src/plugins';
import { useRoute } from 'vue-router';
import { useStore } from 'vuex';
const links = [ const links = [
{ {
@ -131,16 +131,13 @@ const shortcuts = [
}, },
]; ];
declare module 'vue/types/vue' {
interface Vue {
$flaschengeistPlugins: FG_Plugin.LoadedPlugins;
}
}
export default defineComponent({ export default defineComponent({
name: 'MainLayout', name: 'MainLayout',
components: { EssentialLink, ShortCutLink }, components: { EssentialLink, ShortCutLink },
setup(_, ctx) { setup() {
const route = useRoute();
const store = useStore();
const plugins = inject<FG_Plugin.LoadedPlugins>('flaschengeistPlugins');
const leftDrawer = ref(false); const leftDrawer = ref(false);
const leftDrawerOpen = ref( const leftDrawerOpen = ref(
@ -158,12 +155,10 @@ export default defineComponent({
} }
const pluginChildLinks = computed(() => { const pluginChildLinks = computed(() => {
const link: const link: FG_Plugin.PluginMainLink | undefined = plugins?.mainLinks.find(
| FG_Plugin.PluginMainLink
| undefined = ctx.root.$flaschengeistPlugins.mainLinks.find(
(plugin: FG_Plugin.PluginMainLink) => { (plugin: FG_Plugin.PluginMainLink) => {
if (ctx.root.$route.matched.length > 1) { if (route.matched.length > 1) {
return plugin.name == ctx.root.$route.matched[1].name; return plugin.name == route.matched[1].name;
} }
} }
); );
@ -177,7 +172,7 @@ export default defineComponent({
function logout() { function logout() {
Loading.show({ message: 'Session wird abgemeldet' }); Loading.show({ message: 'Session wird abgemeldet' });
(<Store<StateInterface>>ctx.root.$store).dispatch('session/logout').finally(() => { store.dispatch('session/logout').finally(() => {
Loading.hide(); Loading.hide();
}); });
} }

18
src/plugins.d.ts vendored
View File

@ -1,7 +1,6 @@
import { RouteConfig } from 'vue-router'; import { RouteRecordRaw } from 'vue-router';
import { Module } from 'vuex'; import { Module } from 'vuex';
import { StateInterface } from 'src/store'; import { Component } from 'vue';
import { AsyncComponentPromise } from 'vue/types/options';
declare namespace FG_Plugin { declare namespace FG_Plugin {
interface ShortCutLink { interface ShortCutLink {
@ -10,19 +9,20 @@ declare namespace FG_Plugin {
permissions?: string[]; permissions?: string[];
} }
interface PluginRouteConfig extends RouteConfig { interface PluginRouteConfig {
shortcut?: boolean;
title: string; title: string;
icon: string; icon: string;
route: RouteRecordRaw;
shortcut?: boolean;
children?: PluginRouteConfig[]; children?: PluginRouteConfig[];
meta?: { permissions?: string[] }; permissions?: string[];
} }
interface Widget { interface Widget {
name: string; name: string;
priority: number; priority: number;
permissions: FG.Permission[]; permissions: FG.Permission[];
widget: AsyncComponentPromise; widget: Component;
} }
interface Plugin { interface Plugin {
@ -33,7 +33,7 @@ declare namespace FG_Plugin {
requiredBackendModules: string[]; requiredBackendModules: string[];
mainRoutes?: PluginRouteConfig[]; mainRoutes?: PluginRouteConfig[];
outRoutes?: PluginRouteConfig[]; outRoutes?: PluginRouteConfig[];
store?: Map<string, Module<any, StateInterface>>; store?: Map<string, Module<any, any>>;
} }
interface PluginMainLink extends PluginChildLink { interface PluginMainLink extends PluginChildLink {
@ -55,7 +55,7 @@ declare namespace FG_Plugin {
interface LoadedPlugins { interface LoadedPlugins {
plugins: LoadedPlugin[]; plugins: LoadedPlugin[];
routes: RouteConfig[]; routes: RouteRecordRaw[];
mainLinks: PluginMainLink[]; mainLinks: PluginMainLink[];
shortcuts: ShortCutLink[]; shortcuts: ShortCutLink[];
shortcutsOut: ShortCutLink[]; shortcutsOut: ShortCutLink[];

View File

@ -5,35 +5,43 @@ const mainRoutes: FG_Plugin.PluginRouteConfig[] = [
{ {
title: 'Gerücht', title: 'Gerücht',
icon: 'mdi-cash-100', icon: 'mdi-cash-100',
path: 'balance', permissions: ['user'],
name: 'balance', route: {
redirect: { name: 'balance-view' }, path: 'balance',
meta: { permissions: ['user'] }, name: 'balance',
redirect: { name: 'balance-view' },
},
children: [ children: [
{ {
title: 'Übersicht', title: 'Übersicht',
icon: 'mdi-cash-check', icon: 'mdi-cash-check',
path: 'overview', permissions: [permissions.SHOW],
name: 'balance-view', route: {
meta: { permissions: [permissions.SHOW] }, path: 'overview',
component: () => import('../pages/Overview.vue'), name: 'balance-view',
component: () => import('../pages/Overview.vue'),
},
}, },
{ {
title: 'Buchen', title: 'Buchen',
icon: 'mdi-cash-plus', icon: 'mdi-cash-plus',
path: 'change',
name: 'balance-change',
shortcut: true, shortcut: true,
meta: { permissions: [permissions.DEBIT_OWN, permissions.SHOW] }, permissions: [permissions.DEBIT_OWN, permissions.SHOW],
component: () => import('../pages/MainPage.vue'), route: {
path: 'change',
name: 'balance-change',
component: () => import('../pages/MainPage.vue'),
},
}, },
{ {
title: 'Verwaltung', title: 'Verwaltung',
icon: 'mdi-account-cash', icon: 'mdi-account-cash',
path: 'admin', permissions: [permissions.SET_LIMIT, permissions.SHOW_OTHER],
name: 'balance-admin', route: {
meta: { permissions: [permissions.SET_LIMIT, permissions.SHOW_OTHER] }, path: 'admin',
component: () => import('../pages/Admin.vue'), name: 'balance-admin',
component: () => import('../pages/Admin.vue'),
},
}, },
], ],
}, },

View File

@ -4,37 +4,44 @@ const mainRoutes: FG_Plugin.PluginRouteConfig[] = [
{ {
title: 'Dienste', title: 'Dienste',
icon: 'mdi-briefcase', icon: 'mdi-briefcase',
path: 'schedule', permissions: ['user'],
name: 'schedule', route: {
component: () => import('../pages/MainPage.vue'), path: 'schedule',
meta: { permissions: ['user'] }, name: 'schedule',
component: () => import('../pages/MainPage.vue'),
},
children: [ children: [
{ {
title: 'Dienstübersicht', title: 'Dienstübersicht',
icon: 'mdi-account-group', icon: 'mdi-account-group',
path: 'schedule-overview',
name: 'schedule-overview',
shortcut: true, shortcut: true,
meta: { permissions: [] }, permissions: [],
component: () => import('../pages/Overview.vue'), route: {
path: 'schedule-overview',
name: 'schedule-overview',
component: () => import('../pages/Overview.vue'),
},
}, },
{ {
title: 'Dienstverwaltung', title: 'Dienstverwaltung',
icon: 'mdi-account-details', icon: 'mdi-account-details',
path: 'schedule-management',
name: 'schedule-management',
shortcut: false, shortcut: false,
meta: { permissions: [] }, route: {
component: () => import('../pages/Management.vue'), path: 'schedule-management',
name: 'schedule-management',
component: () => import('../pages/Management.vue'),
},
}, },
{ {
title: 'Dienstanfragen', title: 'Dienstanfragen',
icon: 'mdi-account-switch', icon: 'mdi-account-switch',
path: 'schedule-requests',
name: 'schedule-requests',
shortcut: false, shortcut: false,
meta: { permissions: [] }, route: {
component: () => import('../pages/Requests.vue'), path: 'schedule-requests',
name: 'schedule-requests',
component: () => import('../pages/Requests.vue'),
},
}, },
], ],
}, },

View File

@ -3,28 +3,30 @@ const mainRoutes: FG_Plugin.PluginRouteConfig[] = [
{ {
title: 'loadFromStore("user/displayName")', title: 'loadFromStore("user/displayName")',
icon: 'mdi-account', icon: 'mdi-account',
path: 'user', permissions: ['user'],
name: 'user', route: { path: 'user', name: 'user', component: () => import('../pages/MainPage.vue') },
component: () => import('../pages/MainPage.vue'),
meta: { permissions: ['user'] },
children: [ children: [
{ {
title: 'Einstellungen', title: 'Einstellungen',
icon: 'mdi-account-edit', icon: 'mdi-account-edit',
path: 'settings',
name: 'user-settings',
shortcut: true, shortcut: true,
meta: { permissions: ['user'] }, permissions: ['user'],
component: () => import('../pages/Settings.vue'), route: {
path: 'settings',
name: 'user-settings',
component: () => import('../pages/Settings.vue'),
},
}, },
{ {
title: 'Admin', title: 'Admin',
icon: 'mdi-cog', icon: 'mdi-cog',
path: 'admin',
name: 'admin-settings',
shortcut: false, shortcut: false,
meta: { permissions: ['users_edit_other'] }, permissions: ['users_edit_other'],
component: () => import('../pages/AdminSettings.vue'), route: {
path: 'admin',
name: 'admin-settings',
component: () => import('../pages/AdminSettings.vue'),
},
}, },
], ],
}, },

View File

@ -1,24 +1,42 @@
import { route } from 'quasar/wrappers'; import { route } from 'quasar/wrappers';
import VueRouter from 'vue-router'; import {
import { Store } from 'vuex'; createRouter,
import { StateInterface } from 'src/store'; createMemoryHistory,
createWebHistory,
createWebHashHistory,
} from 'vue-router';
import routes from './routes';
/* /*
* If not building with SSR mode, you can * If not building with SSR mode, you can
* directly export the Router instantiation * directly export the Router instantiation;
*
* The function below can be async too; either use
* async/await or return a Promise which resolves
* with the Router instance.
*/ */
export const Router: VueRouter = new VueRouter({
scrollBehavior: () => ({ x: 0, y: 0 }),
// Leave these as is and change from quasar.conf.js instead! function router(/* { store, ssrContext } */) {
// quasar.conf.js -> build -> vueRouterMode const createHistory =
// quasar.conf.js -> build -> publicPath process.env.MODE === 'ssr'
mode: process.env.VUE_ROUTER_MODE, ? createMemoryHistory
base: process.env.VUE_ROUTER_BASE, : process.env.VUE_ROUTER_MODE === 'history'
}); ? createWebHistory
: createWebHashHistory;
export default route<Store<StateInterface>>(function ({ Vue }) { const Router = createRouter({
Vue.use(VueRouter); scrollBehavior: () => ({ left: 0, top: 0 }),
routes,
// Leave this as is and make changes in quasar.conf.js instead!
// quasar.conf.js -> build -> vueRouterMode
// quasar.conf.js -> build -> publicPath
history: createHistory(process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE),
});
return Router; return Router;
}); }
export default route(router);
export const Router = router();

View File

@ -1,6 +1,6 @@
import { RouteConfig } from 'vue-router'; import { RouteRecordRaw } from 'vue-router';
const routes: RouteConfig[] = [ const routes: RouteRecordRaw[] = [
{ {
path: '/', path: '/',
redirect: 'login', redirect: 'login',
@ -56,7 +56,7 @@ const routes: RouteConfig[] = [
// Always leave this as last one, // Always leave this as last one,
// but you can also remove it // but you can also remove it
{ {
path: '*', path: '/:catchAll(.*)*',
redirect: 'login', redirect: 'login',
}, },
]; ];

View File

@ -1,27 +1,13 @@
import { store } from 'quasar/wrappers'; import { store } from 'quasar/wrappers';
import { SessionInterface } from 'src/plugins/user/store/session'; import { createStore } from 'vuex';
import Vuex from 'vuex';
import { UserStateInterface } from 'src/plugins/user/store/user';
/* export default store(function (/* { ssrContext } */) {
* If not building with SSR mode, you can const Store = createStore({
* directly export the Store instantiation
*/
export interface StateInterface {
user: UserStateInterface;
session: SessionInterface;
[key: string]: any;
}
export default store(function ({ Vue }) {
Vue.use(Vuex);
const Store = new Vuex.Store<StateInterface>({
modules: {}, modules: {},
// enable strict mode (adds overhead!) // enable strict mode (adds overhead!)
// for dev mode only // for dev mode and --debug builds only
strict: !!process.env.DEV, strict: !!process.env.DEBUGGING,
}); });
return Store; return Store;