release v2.0.0 #4
|
@ -1 +1 @@
|
||||||
Subproject commit d0e503b1bfc65f64216042d0fe39daf5377b7901
|
Subproject commit ac6d0693a062f60d539d7f6d8fdee00fbcc528c7
|
|
@ -3,7 +3,7 @@ import { FG_Plugin } from 'src/plugins';
|
||||||
import routes from 'src/router/routes';
|
import routes from 'src/router/routes';
|
||||||
import { api } from 'boot/axios';
|
import { api } from 'boot/axios';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { Router, RouteRecordRaw } from 'vue-router';
|
import { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
const config: { [key: string]: Array<string> } = {
|
const config: { [key: string]: Array<string> } = {
|
||||||
// Do not change required Modules !!
|
// Do not change required Modules !!
|
||||||
|
@ -12,9 +12,33 @@ const config: { [key: string]: Array<string> } = {
|
||||||
loadModules: ['Balance', 'Schedule', 'Pricelist'],
|
loadModules: ['Balance', 'Schedule', 'Pricelist'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* Stop!
|
||||||
|
|
||||||
// do not change anything here !!
|
// do not change anything here !!
|
||||||
|
|
||||||
// combine routes from source to target
|
// 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 {
|
interface BackendPlugin {
|
||||||
permissions: string[];
|
permissions: string[];
|
||||||
|
@ -22,44 +46,60 @@ interface BackendPlugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BackendPlugins {
|
interface BackendPlugins {
|
||||||
[key: string]: BackendPlugin | null;
|
[key: string]: BackendPlugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Backend {
|
interface Backend {
|
||||||
plugins: BackendPlugins[];
|
plugins: BackendPlugins;
|
||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Backend };
|
export { Backend };
|
||||||
|
|
||||||
function setPermissions(object: FG_Plugin.PluginRouteConfig) {
|
// 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.permissions !== undefined) {
|
||||||
if (object.route.meta === undefined) object.route.meta = {};
|
if (object.route.meta === undefined) object.route.meta = {};
|
||||||
object.route.meta['permissions'] = object.permissions;
|
object.route.meta['permissions'] = object.permissions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertRoutes(parent: RouteRecordRaw, children?: FG_Plugin.PluginRouteConfig[]) {
|
/**
|
||||||
if (children === undefined) return;
|
* 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) => {
|
children.forEach((child) => {
|
||||||
setPermissions(child);
|
setPermissions(child);
|
||||||
convertRoutes(child.route, child.children);
|
convertRoutes(child.route, child.children);
|
||||||
if (parent.children === undefined) parent.children = [];
|
if (parent.children === undefined) parent.children = [];
|
||||||
parent.children.push(child.route);
|
parent.children.push(child.route);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function combineRoutes(
|
/**
|
||||||
|
* 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[],
|
target: RouteRecordRaw[],
|
||||||
source: FG_Plugin.PluginRouteConfig[],
|
source: FG_Plugin.MenuRoute[],
|
||||||
mainPath: '/' | '/main' = '/'
|
mainPath: '/' | '/in' = '/'
|
||||||
): RouteRecordRaw[] {
|
): RouteRecordRaw[] {
|
||||||
// Search parent
|
// Search parent
|
||||||
target.forEach((target) => {
|
target.forEach((target) => {
|
||||||
if (target.path === mainPath) {
|
if (target.path === mainPath) {
|
||||||
// Parent found = target
|
// Parent found = target
|
||||||
source.forEach((sourceMainConfig: FG_Plugin.PluginRouteConfig) => {
|
source.forEach((sourceMainConfig: FG_Plugin.MenuRoute) => {
|
||||||
// Check if source is already in target
|
// Check if source is already in target
|
||||||
const targetMainConfig = target.children?.find((targetMainConfig: RouteRecordRaw) => {
|
const targetMainConfig = target.children?.find((targetMainConfig: RouteRecordRaw) => {
|
||||||
return sourceMainConfig.route.path === targetMainConfig.path;
|
return sourceMainConfig.route.path === targetMainConfig.path;
|
||||||
|
@ -89,193 +129,229 @@ function combineRoutes(
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
// combine Links of Plugins from source to target
|
function combineRoutes(
|
||||||
function combineMainLinks(
|
target: RouteRecordRaw[],
|
||||||
target: FG_Plugin.PluginMainLink[],
|
source: FG_Plugin.NamedRouteRecordRaw[],
|
||||||
source: FG_Plugin.PluginRouteConfig
|
mainPath: '/' | '/in'
|
||||||
): FG_Plugin.PluginMainLink[] {
|
) {
|
||||||
const targetPluginMainLink: FG_Plugin.PluginMainLink | undefined = target.find(
|
// Search parent
|
||||||
(targetPluginMainLink: FG_Plugin.PluginMainLink) => {
|
target.forEach((target) => {
|
||||||
return targetPluginMainLink.title == source.title;
|
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
|
||||||
);
|
);
|
||||||
if (targetPluginMainLink) {
|
// Already in target routes, add only children
|
||||||
source.children?.forEach((sourcePluginChildLink: FG_Plugin.PluginRouteConfig) => {
|
if (targetRoot) {
|
||||||
targetPluginMainLink.children.push(<FG_Plugin.PluginChildLink>{
|
if (targetRoot.children === undefined) targetRoot.children = [];
|
||||||
title: sourcePluginChildLink.title,
|
targetRoot.children.push(...(sourceRoute.children || []));
|
||||||
icon: sourcePluginChildLink.icon,
|
|
||||||
link: sourcePluginChildLink.route.name,
|
|
||||||
name: sourcePluginChildLink.route.name,
|
|
||||||
permissions: sourcePluginChildLink.permissions,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
const mainLink: FG_Plugin.PluginMainLink = <FG_Plugin.PluginMainLink>{
|
// 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,
|
title: source.title,
|
||||||
icon: source.icon,
|
icon: source.icon,
|
||||||
link: source.route.name,
|
link: source.route.name,
|
||||||
name: source.route.name,
|
|
||||||
permissions: source.permissions,
|
permissions: source.permissions,
|
||||||
};
|
});
|
||||||
source.children?.forEach((child) => {
|
|
||||||
if (mainLink.children === undefined) {
|
|
||||||
mainLink.children = [];
|
|
||||||
}
|
}
|
||||||
mainLink.children.push(<FG_Plugin.PluginChildLink>{
|
if (target[idx].children === undefined) {
|
||||||
title: child.title,
|
target[idx].children = [];
|
||||||
icon: child.icon,
|
}
|
||||||
link: child.route.name,
|
source.children?.forEach((sourceChild) => {
|
||||||
name: child.route.name,
|
target[idx].children?.push({
|
||||||
permissions: child.permissions,
|
title: sourceChild.title,
|
||||||
|
icon: sourceChild.icon,
|
||||||
|
link: sourceChild.route.name,
|
||||||
|
permissions: sourceChild.permissions,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
target.push(mainLink);
|
|
||||||
}
|
|
||||||
return target;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadShortCuts(
|
/**
|
||||||
target: FG_Plugin.ShortCutLink[],
|
* Combine shortcuts from Plugin MenuRouts into the Flaschenbeist Shortcut list
|
||||||
source: FG_Plugin.PluginRouteConfig[]
|
* @param target Flaschengeist list of shortcuts
|
||||||
): FG_Plugin.ShortCutLink[] {
|
* @param source MenuRoutes to extract shortcuts from
|
||||||
|
*/
|
||||||
|
function combineShortcuts(target: FG_Plugin.Shortcut[], source: FG_Plugin.MenuRoute[]) {
|
||||||
source.forEach((route) => {
|
source.forEach((route) => {
|
||||||
if (route.shortcut) {
|
if (route.shortcut) {
|
||||||
target.push(<FG_Plugin.ShortCutLink>{
|
target.push(<FG_Plugin.Shortcut>{
|
||||||
link: route.route.name,
|
link: route.route.name,
|
||||||
icon: route.icon,
|
icon: route.icon,
|
||||||
permissions: route.permissions,
|
permissions: route.permissions,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (route.children) {
|
if (route.children) {
|
||||||
target = loadShortCuts(target, route.children);
|
combineShortcuts(target, route.children);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return target;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// loade plugins
|
/**
|
||||||
|
* Load a Flaschengeist plugin
|
||||||
|
* @param loadedPlugins Flaschgeist object
|
||||||
|
* @param pluginName Plugin to load
|
||||||
|
* @param context RequireContext of plugins
|
||||||
|
* @param router VueRouter instance
|
||||||
|
*/
|
||||||
function loadPlugin(
|
function loadPlugin(
|
||||||
loadedPlugins: FG_Plugin.Flaschengeist,
|
loadedPlugins: FG_Plugin.Flaschengeist,
|
||||||
modules: string[],
|
pluginName: string,
|
||||||
backendpromise: Promise<Backend | null>,
|
context: __WebpackModuleApi.RequireContext,
|
||||||
plugins: FG_Plugin.Plugin[],
|
backend: Backend
|
||||||
router: Router
|
) {
|
||||||
): FG_Plugin.Flaschengeist {
|
// Check if already loaded
|
||||||
modules.forEach((requiredModule) => {
|
if (loadedPlugins.plugins.findIndex((p) => p.name === pluginName) !== -1) return true;
|
||||||
const plugin = plugins.find((plugin) => {
|
|
||||||
return plugin.name == requiredModule;
|
// Search if plugin is installed
|
||||||
});
|
const available = context.keys();
|
||||||
if (plugin) {
|
const plugin = available.includes(`./${pluginName.toLowerCase()}/plugin.ts`)
|
||||||
if (plugin.mainRoutes) {
|
? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
loadedPlugins.routes = combineRoutes(loadedPlugins.routes, plugin.mainRoutes, '/main');
|
<FG_Plugin.Plugin>context(`./${pluginName.toLowerCase()}/plugin.ts`).default
|
||||||
plugin.mainRoutes.forEach((route) => {
|
: undefined;
|
||||||
loadedPlugins.mainLinks = combineMainLinks(loadedPlugins.mainLinks, route);
|
|
||||||
});
|
if (!plugin) {
|
||||||
loadedPlugins.shortcuts = loadShortCuts(loadedPlugins.shortcuts, plugin.mainRoutes);
|
// 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;
|
||||||
}
|
}
|
||||||
if (plugin.outRoutes) {
|
|
||||||
loadedPlugins.routes = combineRoutes(loadedPlugins.routes, plugin.outRoutes);
|
// Check frontend dependencies
|
||||||
loadedPlugins.shortcutsOut = loadShortCuts(loadedPlugins.shortcutsOut, plugin.outRoutes);
|
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) {
|
if (plugin.widgets.length > 0) {
|
||||||
plugin.widgets.forEach((widget) => (widget.name = plugin.name + '_' + widget.name));
|
plugin.widgets.forEach((widget) => (widget.name = plugin.name + '_' + widget.name));
|
||||||
Array.prototype.push.apply(loadedPlugins.widgets, plugin.widgets);
|
Array.prototype.push.apply(loadedPlugins.widgets, plugin.widgets);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadedPlugins.plugins.push({
|
loadedPlugins.plugins.push({
|
||||||
name: plugin.name,
|
name: plugin.name,
|
||||||
version: plugin.version,
|
version: plugin.version,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
console.exception(`Could not find required Plugin ${requiredModule}`);
|
return plugin;
|
||||||
router.push({ name: 'error' }).catch((e) => {
|
|
||||||
console.warn(e);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
return loadedPlugins;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getBackend(): Promise<Backend | null> {
|
/**
|
||||||
let backend: Backend | null = null;
|
* Loading backend information
|
||||||
|
* @returns Backend object or null
|
||||||
|
*/
|
||||||
|
async function getBackend() {
|
||||||
try {
|
try {
|
||||||
const response: AxiosResponse<Backend> = await api.get('/');
|
const { data }: AxiosResponse<Backend> = await api.get('/');
|
||||||
backend = response.data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
|
||||||
return backend;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default boot(({ router, app }) => {
|
/**
|
||||||
const plugins: FG_Plugin.Plugin[] = [];
|
* Boot file, load all required plugins, check for dependencies
|
||||||
|
*/
|
||||||
|
export default boot(async ({ router, app }) => {
|
||||||
|
const backend = await getBackend();
|
||||||
|
if (!backend) {
|
||||||
|
void router.push({ name: 'error' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const backendPromise = getBackend();
|
const loadedPlugins: FG_Plugin.Flaschengeist = {
|
||||||
|
|
||||||
let loadedPlugins: FG_Plugin.Flaschengeist = {
|
|
||||||
routes,
|
routes,
|
||||||
plugins: [],
|
plugins: [],
|
||||||
mainLinks: [],
|
menuLinks: [],
|
||||||
shortcuts: [],
|
shortcuts: [],
|
||||||
shortcutsOut: [],
|
outerShortcuts: [],
|
||||||
widgets: [],
|
widgets: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// get all plugins
|
// get all plugins
|
||||||
const pluginsContext = require.context('src/plugins', true, /.+\/plugin.ts$/);
|
const pluginsContext = require.context('src/plugins', true, /.+\/plugin.ts$/);
|
||||||
pluginsContext.keys().forEach((fileName: string) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
||||||
plugins.push(pluginsContext(fileName).default);
|
|
||||||
});
|
|
||||||
|
|
||||||
// check dependencies
|
// Start loading plugins
|
||||||
backendPromise
|
// Load required modules:
|
||||||
.then((backend) => {
|
config.requiredModules.forEach((required) => {
|
||||||
if (backend) {
|
const plugin = loadPlugin(loadedPlugins, required, pluginsContext, backend);
|
||||||
plugins.forEach((plugin: FG_Plugin.Plugin) => {
|
if (!plugin) {
|
||||||
plugin.requiredModules.forEach((requiredModule: string) => {
|
void router.push({ name: 'error' });
|
||||||
if (
|
return;
|
||||||
!(
|
|
||||||
config.requiredModules.includes(requiredModule) ||
|
|
||||||
config.loadModules.includes(requiredModule)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
console.error(`Plugin ${plugin.name} need Plugin ${requiredModule}`);
|
|
||||||
router.push({ name: 'error' }).catch((e) => {
|
|
||||||
console.warn(e);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
plugin.requiredBackendModules.forEach((requiredBackendModule: string) => {
|
|
||||||
if (!(requiredBackendModule in backend.plugins)) {
|
// Load user defined plugins
|
||||||
console.error(
|
config.loadModules.forEach((required) => {
|
||||||
`Plugin ${plugin.name} need Plugin ${requiredBackendModule} in backend.`
|
const plugin = loadPlugin(loadedPlugins, required, pluginsContext, backend);
|
||||||
);
|
if (!plugin) {
|
||||||
router.push({ name: 'error' }).catch((err) => {
|
void router.push({ name: 'error' });
|
||||||
console.warn(err);
|
return;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
// load plugins
|
|
||||||
loadedPlugins = loadPlugin(
|
|
||||||
loadedPlugins,
|
|
||||||
config.requiredModules,
|
|
||||||
backendPromise,
|
|
||||||
plugins,
|
|
||||||
router
|
|
||||||
);
|
|
||||||
loadedPlugins = loadPlugin(loadedPlugins, config.loadModules, backendPromise, plugins, router);
|
|
||||||
|
|
||||||
|
// Sort widgets by priority
|
||||||
loadedPlugins.widgets.sort((a, b) => b.priority - a.priority);
|
loadedPlugins.widgets.sort((a, b) => b.priority - a.priority);
|
||||||
|
|
||||||
|
// Add loaded routes to router
|
||||||
loadedPlugins.routes.forEach((route) => router.addRoute(route));
|
loadedPlugins.routes.forEach((route) => router.addRoute(route));
|
||||||
|
|
||||||
// save plugins in VM-variable
|
// save plugins in VM-variable
|
||||||
|
|
|
@ -1,50 +1,35 @@
|
||||||
<template>
|
<template>
|
||||||
<q-item v-if="isGranted" clickable tag="a" target="self" :to="{ name: link }">
|
<q-item v-if="isGranted" clickable tag="a" target="self" :to="{ name: entry.link }">
|
||||||
<q-item-section v-if="icon" avatar>
|
<q-item-section v-if="entry.icon" avatar>
|
||||||
<q-icon :name="icon" />
|
<q-icon :name="entry.icon" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>{{ title }}</q-item-label>
|
<q-item-label>{{ title }}</q-item-label>
|
||||||
<!--<q-item-label caption>
|
|
||||||
{{ caption }}
|
|
||||||
</q-item-label>-->
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent } from 'vue';
|
import { computed, defineComponent, PropType } from 'vue';
|
||||||
import { hasPermissions } from 'src/utils/permission';
|
import { hasPermissions } from 'src/utils/permission';
|
||||||
|
import { FG_Plugin } from 'src/plugins';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'EssentialLink',
|
name: 'EssentialLink',
|
||||||
props: {
|
props: {
|
||||||
title: {
|
entry: {
|
||||||
type: String,
|
type: Object as PropType<FG_Plugin.MenuLink>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
caption: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
type: String,
|
|
||||||
default: 'dashboard',
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
default: () => Array<string>(),
|
|
||||||
type: Array as () => Array<string>,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const isGranted = computed(() => hasPermissions(props.permissions));
|
const isGranted = computed(() => hasPermissions(props.entry.permissions || []));
|
||||||
return { isGranted };
|
const title = computed(() =>
|
||||||
|
typeof props.entry.title === 'object' ? props.entry.title.value : props.entry.title
|
||||||
|
);
|
||||||
|
return { isGranted, title };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
<template>
|
|
||||||
<q-btn v-if="isGranted" flat dense :icon="icon" :to="{ name: link }" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent } from 'vue';
|
|
||||||
import { hasPermissions } from 'src/utils/permission';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'ShortCutLink',
|
|
||||||
props: {
|
|
||||||
link: {
|
|
||||||
required: true,
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
required: true,
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
permissions: {
|
|
||||||
default: undefined,
|
|
||||||
type: Array as () => Array<string>,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const isGranted = computed(() => hasPermissions(props.permissions || []));
|
|
||||||
return { isGranted };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<q-btn v-if="isGranted" flat dense :icon="shortcut.icon" :to="{ name: shortcut.link }" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, PropType } from 'vue';
|
||||||
|
import { hasPermissions } from 'src/utils/permission';
|
||||||
|
import { FG_Plugin } from 'src/plugins';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ShortcutLink',
|
||||||
|
props: {
|
||||||
|
shortcut: {
|
||||||
|
required: true,
|
||||||
|
type: Object as PropType<FG_Plugin.Shortcut>,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const isGranted = computed(() => hasPermissions(props.shortcut.permissions || []));
|
||||||
|
return { isGranted };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -39,7 +39,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, PropType } from 'vue';
|
import { computed, defineComponent, PropType } from 'vue';
|
||||||
import { date as q_date } from 'quasar';
|
import { date as q_date } from 'quasar';
|
||||||
import { stringIsDate, stringIsTime, stringIsDateTime } from 'src/utils/validators';
|
import { stringIsDate, stringIsTime, stringIsDateTime, Validator } from 'src/utils/validators';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'IsoDateInput',
|
name: 'IsoDateInput',
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
<q-layout view="hHh lpr lFf">
|
<q-layout view="hHh lpr lFf">
|
||||||
<q-header elevated class="bg-primary text-white">
|
<q-header elevated class="bg-primary text-white">
|
||||||
<q-toolbar>
|
<q-toolbar>
|
||||||
<!-- Button um Navigationsleiset ein und auszublenden. Nötig bei Desktop? -->
|
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="!leftDrawerOpen"
|
v-if="!leftDrawerOpen"
|
||||||
dense
|
dense
|
||||||
|
@ -22,15 +21,11 @@
|
||||||
</q-toolbar-title>
|
</q-toolbar-title>
|
||||||
|
|
||||||
<!-- Hier kommen die Shortlinks hin -->
|
<!-- Hier kommen die Shortlinks hin -->
|
||||||
<div>
|
<shortcut-link
|
||||||
<short-cut-link
|
v-for="(shortcut, index) in shortcuts"
|
||||||
v-for="(shortcut, index) in flaschengeist.shortcuts"
|
|
||||||
:key="'shortcut' + index"
|
:key="'shortcut' + index"
|
||||||
:link="shortcut.link"
|
:shortcut="shortcut"
|
||||||
:icon="shortcut.icon"
|
|
||||||
:permissions="shortcut.permissions"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<q-btn flat round dense icon="mdi-exit-to-app" @click="logout()" />
|
<q-btn flat round dense icon="mdi-exit-to-app" @click="logout()" />
|
||||||
</q-toolbar>
|
</q-toolbar>
|
||||||
</q-header>
|
</q-header>
|
||||||
|
@ -46,26 +41,17 @@
|
||||||
<!-- Plugins -->
|
<!-- Plugins -->
|
||||||
<q-list>
|
<q-list>
|
||||||
<essential-link
|
<essential-link
|
||||||
v-for="(link, index) in flaschengeist.mainLinks"
|
v-for="(entry, index) in mainLinks"
|
||||||
:key="'plugin' + index"
|
:key="'plugin' + index"
|
||||||
:title="typeof link.title === 'object' ? link.title.value : link.title"
|
:entry="entry"
|
||||||
:link="link.link"
|
|
||||||
:icon="link.icon"
|
|
||||||
:permissions="link.permissions"
|
|
||||||
/>
|
/>
|
||||||
</q-list>
|
|
||||||
<q-separator />
|
<q-separator />
|
||||||
|
|
||||||
<!-- Plugin functions -->
|
<!-- Plugin functions -->
|
||||||
<!-- <router-view name="plugin-nav" /> -->
|
|
||||||
<q-list>
|
|
||||||
<essential-link
|
<essential-link
|
||||||
v-for="(link, index) in pluginChildLinks"
|
v-for="(entry, index) in subLinks"
|
||||||
:key="'childPlugin' + index"
|
:key="'childPlugin' + index"
|
||||||
:title="link.title"
|
:entry="entry"
|
||||||
:link="link.link"
|
|
||||||
:icon="link.icon"
|
|
||||||
:permissions="link.permissions"
|
|
||||||
/>
|
/>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
|
||||||
|
@ -84,12 +70,9 @@
|
||||||
<q-separator />
|
<q-separator />
|
||||||
|
|
||||||
<essential-link
|
<essential-link
|
||||||
v-for="(link, index) in links"
|
v-for="(entry, index) in essentials"
|
||||||
:key="'main' + index"
|
:key="'essential' + index"
|
||||||
:title="link.title"
|
:entry="entry"
|
||||||
:link="link.link"
|
|
||||||
:icon="link.icon"
|
|
||||||
:permissions="link.permissions"
|
|
||||||
/>
|
/>
|
||||||
</q-drawer>
|
</q-drawer>
|
||||||
<q-page-container>
|
<q-page-container>
|
||||||
|
@ -99,54 +82,37 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import EssentialLink from 'components/navigation/EssentialLink.vue';
|
import EssentialLink from 'src/components/navigation/EssentialLink.vue';
|
||||||
import ShortCutLink from 'components/navigation/ShortCutLink.vue';
|
import ShortcutLink from 'src/components/navigation/ShortcutLink.vue';
|
||||||
import { Screen } from 'quasar';
|
import { Screen } from 'quasar';
|
||||||
import { defineComponent, ref, inject, computed } from 'vue';
|
import { defineComponent, ref, inject, computed } from 'vue';
|
||||||
import { useMainStore } from 'src/store';
|
import { useMainStore } from 'src/store';
|
||||||
import { FG_Plugin } from 'src/plugins';
|
import { FG_Plugin } from 'src/plugins';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
const links = [
|
const essentials: FG_Plugin.MenuLink[] = [
|
||||||
{
|
{
|
||||||
name: 'about',
|
|
||||||
title: 'Über Flaschengeist',
|
title: 'Über Flaschengeist',
|
||||||
link: 'about',
|
link: 'about',
|
||||||
icon: 'mdi-information',
|
icon: 'mdi-information',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const shortcuts = [
|
|
||||||
{
|
|
||||||
link: 'about',
|
|
||||||
icon: 'mdi-information',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
link: 'user',
|
|
||||||
icon: 'mdi-account',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
link: 'user-plugin1',
|
|
||||||
icon: 'mdi-account-plus',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'MainLayout',
|
name: 'MainLayout',
|
||||||
components: { EssentialLink, ShortCutLink },
|
components: { EssentialLink, ShortcutLink },
|
||||||
setup() {
|
setup() {
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const mainStore = useMainStore();
|
const mainStore = useMainStore();
|
||||||
const flaschengeist = inject<FG_Plugin.Flaschengeist>('flaschengeist');
|
const flaschengeist = inject<FG_Plugin.Flaschengeist>('flaschengeist');
|
||||||
const leftDrawer = ref(false);
|
const leftDrawer = ref(false);
|
||||||
|
const shortcuts = flaschengeist?.shortcuts;
|
||||||
|
const mainLinks = flaschengeist?.menuLinks;
|
||||||
|
|
||||||
const leftDrawerOpen = ref(
|
const leftDrawerOpen = computed({
|
||||||
computed({
|
|
||||||
get: () => (leftDrawer.value || Screen.gt.sm ? true : false),
|
get: () => (leftDrawer.value || Screen.gt.sm ? true : false),
|
||||||
set: (val: boolean) => (leftDrawer.value = val),
|
set: (val: boolean) => (leftDrawer.value = val),
|
||||||
})
|
});
|
||||||
);
|
|
||||||
const leftDrawerMini = ref(false);
|
const leftDrawerMini = ref(false);
|
||||||
|
|
||||||
function leftDrawerClicker() {
|
function leftDrawerClicker() {
|
||||||
|
@ -155,20 +121,9 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pluginChildLinks = computed(() => {
|
const subLinks = computed(() => {
|
||||||
const link: FG_Plugin.PluginMainLink | undefined = flaschengeist?.mainLinks.find(
|
const matched = router.currentRoute.value.matched[1];
|
||||||
(plugin: FG_Plugin.PluginMainLink) => {
|
return flaschengeist?.menuLinks.find((link) => matched.name == link.link)?.children;
|
||||||
if (route.matched.length > 1) {
|
|
||||||
return plugin.name == route.matched[1].name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (link == undefined) {
|
|
||||||
return [];
|
|
||||||
} else {
|
|
||||||
return link.children;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
|
@ -177,14 +132,14 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
essentials,
|
||||||
leftDrawerOpen,
|
leftDrawerOpen,
|
||||||
leftDrawerMini,
|
leftDrawerMini,
|
||||||
leftDrawerClicker,
|
leftDrawerClicker,
|
||||||
links,
|
|
||||||
pluginChildLinks,
|
|
||||||
shortcuts,
|
|
||||||
logout,
|
logout,
|
||||||
flaschengeist,
|
mainLinks,
|
||||||
|
shortcuts,
|
||||||
|
subLinks,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,30 +8,13 @@
|
||||||
</q-avatar>
|
</q-avatar>
|
||||||
<span class="gt-xs"> Flaschengeist </span>
|
<span class="gt-xs"> Flaschengeist </span>
|
||||||
</q-toolbar-title>
|
</q-toolbar-title>
|
||||||
<div>
|
<shortcut-link
|
||||||
<short-cut-link
|
|
||||||
v-for="(shortcut, index) in shortcuts"
|
v-for="(shortcut, index) in shortcuts"
|
||||||
:key="'shortcut' + index"
|
:key="'shortcut' + index"
|
||||||
:link="shortcut.link"
|
:shortcut="shortcut"
|
||||||
:icon="shortcut.icon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<q-btn
|
|
||||||
v-if="$route.name != 'about_out'"
|
|
||||||
flat
|
|
||||||
round
|
|
||||||
dense
|
|
||||||
icon="mdi-information"
|
|
||||||
@click="$router.push({ name: 'about_out' })"
|
|
||||||
/>
|
|
||||||
<q-btn
|
|
||||||
v-if="$route.name != 'login'"
|
|
||||||
flat
|
|
||||||
round
|
|
||||||
dense
|
|
||||||
icon="mdi-login-variant"
|
|
||||||
@click="$router.push({ name: 'login' })"
|
|
||||||
/>
|
/>
|
||||||
|
<shortcut-link v-if="$route.name != 'about_out'" :shortcut="about" />
|
||||||
|
<shortcut-link v-if="$route.name != 'login'" :shortcut="login" />
|
||||||
</q-toolbar>
|
</q-toolbar>
|
||||||
</q-header>
|
</q-header>
|
||||||
|
|
||||||
|
@ -44,14 +27,18 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { FG_Plugin } from 'src/plugins';
|
import { FG_Plugin } from 'src/plugins';
|
||||||
import { defineComponent, inject } from 'vue';
|
import { defineComponent, inject } from 'vue';
|
||||||
import ShortCutLink from 'components/navigation/ShortCutLink.vue';
|
import ShortcutLink from 'components/navigation/ShortcutLink.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'OutLayout',
|
name: 'OutLayout',
|
||||||
components: { ShortCutLink },
|
components: { ShortcutLink },
|
||||||
setup() {
|
setup() {
|
||||||
const shortcuts = inject<FG_Plugin.Flaschengeist>('flaschengeist')?.shortcutsOut || [];
|
const flaschengeist = inject<FG_Plugin.Flaschengeist>('flaschengeist');
|
||||||
return { shortcuts };
|
const shortcuts = flaschengeist?.outerShortcuts || [];
|
||||||
|
const about: FG_Plugin.Shortcut = { icon: 'mdi-information', link: 'about_out' };
|
||||||
|
const login: FG_Plugin.Shortcut = { icon: 'mdi-login-variant', link: 'login' };
|
||||||
|
|
||||||
|
return { about, login, shortcuts };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,66 +1,100 @@
|
||||||
import { RouteRecordRaw } from 'vue-router';
|
import { RouteRecordRaw, RouteRecordName } from 'vue-router';
|
||||||
import { Component, ComputedRef } from 'vue';
|
import { Component, ComputedRef } from 'vue';
|
||||||
|
|
||||||
declare global {
|
|
||||||
type Validator = (value: unknown) => boolean | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare namespace FG_Plugin {
|
declare namespace FG_Plugin {
|
||||||
interface ShortCutLink {
|
/**
|
||||||
link: string;
|
* Interface defining a Flaschengeist plugin
|
||||||
|
*/
|
||||||
|
interface Plugin {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
widgets: Widget[];
|
||||||
|
/** Pther frontend modules needed for this plugin to work correctly */
|
||||||
|
requiredModules: string[];
|
||||||
|
/** Backend modules needed for this plugin to work correctly */
|
||||||
|
requiredBackendModules: string[];
|
||||||
|
/** Menu entries for authenticated users */
|
||||||
|
innerRoutes?: MenuRoute[];
|
||||||
|
/** Public menu entries (without authentification) */
|
||||||
|
outerRoutes?: MenuRoute[];
|
||||||
|
/** Routes without menu links, for internal usage */
|
||||||
|
internalRoutes?: NamedRouteRecordRaw[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the loaded state of the Flaschengeist
|
||||||
|
*/
|
||||||
|
interface Flaschengeist {
|
||||||
|
/** All loaded plugins */
|
||||||
|
plugins: LoadedPlugin[];
|
||||||
|
/** All routes, combined from all plugins */
|
||||||
|
routes: RouteRecordRaw[];
|
||||||
|
/** All menu entries */
|
||||||
|
menuLinks: MenuLink[];
|
||||||
|
/** All inner shortcuts */
|
||||||
|
shortcuts: Shortcut[];
|
||||||
|
/** All outer shortcuts */
|
||||||
|
outerShortcuts: Shortcut[];
|
||||||
|
/** All widgets */
|
||||||
|
widgets: Widget[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loaded Flaschengeist plugin
|
||||||
|
*/
|
||||||
|
interface LoadedPlugin {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a shortcut link
|
||||||
|
*/
|
||||||
|
interface Shortcut {
|
||||||
|
link: RouteRecordName;
|
||||||
icon: string;
|
icon: string;
|
||||||
permissions?: string[];
|
permissions?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PluginRouteConfig {
|
/**
|
||||||
|
* Defines a main menu entry along with the route
|
||||||
|
* Used when defining a plugin
|
||||||
|
*/
|
||||||
|
interface MenuRoute extends MenuEntry {
|
||||||
|
route: NamedRouteRecordRaw;
|
||||||
|
shortcut?: boolean;
|
||||||
|
children?: this[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type NamedRouteRecordRaw = RouteRecordRaw & {
|
||||||
|
name: RouteRecordName;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a menu entry in the main menu
|
||||||
|
*/
|
||||||
|
interface MenuLink extends MenuEntry {
|
||||||
|
/** Name of the target route */
|
||||||
|
link: RouteRecordName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base interface for internal use
|
||||||
|
*/
|
||||||
|
interface MenuEntry {
|
||||||
title: string | ComputedRef<string>;
|
title: string | ComputedRef<string>;
|
||||||
icon: string;
|
icon: string;
|
||||||
route: RouteRecordRaw;
|
|
||||||
shortcut?: boolean;
|
|
||||||
children?: PluginRouteConfig[];
|
|
||||||
permissions?: string[];
|
permissions?: string[];
|
||||||
|
children?: this[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Widget object for the dashboard
|
||||||
|
*/
|
||||||
interface Widget {
|
interface Widget {
|
||||||
name: string;
|
name: string;
|
||||||
priority: number;
|
priority: number;
|
||||||
permissions: FG.Permission[];
|
permissions: FG.Permission[];
|
||||||
widget: Component;
|
widget: Component;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Plugin {
|
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
widgets: Widget[];
|
|
||||||
requiredModules: string[];
|
|
||||||
requiredBackendModules: string[];
|
|
||||||
mainRoutes?: PluginRouteConfig[];
|
|
||||||
outRoutes?: PluginRouteConfig[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PluginMainLink extends PluginChildLink {
|
|
||||||
children: PluginChildLink[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PluginChildLink {
|
|
||||||
name: string;
|
|
||||||
title: string;
|
|
||||||
link: string;
|
|
||||||
icon: string;
|
|
||||||
permissions?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LoadedPlugin {
|
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Flaschengeist {
|
|
||||||
plugins: LoadedPlugin[];
|
|
||||||
routes: RouteRecordRaw[];
|
|
||||||
mainLinks: PluginMainLink[];
|
|
||||||
shortcuts: ShortCutLink[];
|
|
||||||
shortcutsOut: ShortCutLink[];
|
|
||||||
widgets: Widget[];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { defineAsyncComponent } from 'vue';
|
||||||
|
|
||||||
const plugin: FG_Plugin.Plugin = {
|
const plugin: FG_Plugin.Plugin = {
|
||||||
name: 'Balance',
|
name: 'Balance',
|
||||||
mainRoutes: routes,
|
innerRoutes: routes,
|
||||||
requiredModules: ['User'],
|
requiredModules: ['User'],
|
||||||
requiredBackendModules: ['balance'],
|
requiredBackendModules: ['balance'],
|
||||||
version: '0.0.2',
|
version: '0.0.2',
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { FG_Plugin } from 'src/plugins';
|
import { FG_Plugin } from 'src/plugins';
|
||||||
import permissions from '../permissions';
|
import permissions from '../permissions';
|
||||||
|
|
||||||
const mainRoutes: FG_Plugin.PluginRouteConfig[] = [
|
const mainRoutes: FG_Plugin.MenuRoute[] = [
|
||||||
{
|
{
|
||||||
title: 'Gerücht',
|
title: 'Gerücht',
|
||||||
icon: 'mdi-cash-100',
|
icon: 'mdi-cash-100',
|
||||||
|
|
|
@ -1,21 +1,13 @@
|
||||||
import routes from './routes';
|
import { innerRoutes } from './routes';
|
||||||
import { FG_Plugin } from 'src/plugins';
|
import { FG_Plugin } from 'src/plugins';
|
||||||
|
|
||||||
const plugin: FG_Plugin.Plugin = {
|
const plugin: FG_Plugin.Plugin = {
|
||||||
name: 'Pricelist',
|
name: 'Pricelist',
|
||||||
mainRoutes: routes,
|
innerRoutes,
|
||||||
requiredModules: [],
|
requiredModules: [],
|
||||||
requiredBackendModules: ['pricelist'],
|
requiredBackendModules: ['pricelist'],
|
||||||
version: '0.0.1',
|
version: '0.0.1',
|
||||||
widgets: [],
|
widgets: [],
|
||||||
// widgets: [
|
|
||||||
// {
|
|
||||||
// priority: 1,
|
|
||||||
// name: 'greeting',
|
|
||||||
// permissions: []
|
|
||||||
// widget: () => import('./components/Widget.vue')
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default plugin;
|
export default plugin;
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import { FG_Plugin } from 'src/plugins';
|
import { FG_Plugin } from 'src/plugins';
|
||||||
const mainRoutes: FG_Plugin.PluginRouteConfig[] = [
|
|
||||||
|
export const innerRoutes: FG_Plugin.MenuRoute[] = [
|
||||||
{
|
{
|
||||||
title: 'Getränke',
|
title: 'Getränke',
|
||||||
icon: 'mdi-glass-mug-variant',
|
icon: 'mdi-glass-mug-variant',
|
||||||
route: {
|
route: {
|
||||||
path: 'drinks',
|
path: 'drinks',
|
||||||
name: 'drinks',
|
name: 'drinks',
|
||||||
redirect: { name: 'drinks-pricelist' }
|
redirect: { name: 'drinks-pricelist' },
|
||||||
},
|
},
|
||||||
permissions: ['user'],
|
permissions: ['user'],
|
||||||
children: [
|
children: [
|
||||||
|
@ -18,8 +19,8 @@ const mainRoutes: FG_Plugin.PluginRouteConfig[] = [
|
||||||
route: {
|
route: {
|
||||||
path: 'pricelist',
|
path: 'pricelist',
|
||||||
name: 'drinks-pricelist',
|
name: 'drinks-pricelist',
|
||||||
component: () => import('../pages/PricelistP.vue')
|
component: () => import('../pages/PricelistP.vue'),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Einstellungen',
|
title: 'Einstellungen',
|
||||||
|
@ -29,11 +30,9 @@ const mainRoutes: FG_Plugin.PluginRouteConfig[] = [
|
||||||
route: {
|
route: {
|
||||||
path: 'settings',
|
path: 'settings',
|
||||||
name: 'drinks-settings',
|
name: 'drinks-settings',
|
||||||
component: () => import('../pages/Settings.vue')
|
component: () => import('../pages/Settings.vue'),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default mainRoutes;
|
|
||||||
|
|
|
@ -53,11 +53,15 @@
|
||||||
locale="de-de"
|
locale="de-de"
|
||||||
style="height: 100%; min-height: 400px"
|
style="height: 100%; min-height: 400px"
|
||||||
>
|
>
|
||||||
<template #day="{ scope: { timestamp } }" style="min-height: 200px">
|
<template #day="{ scope: { timestamp } }">
|
||||||
<template v-if="!events[timestamp.weekday]" style="min-height: 200px"> </template>
|
<div itemref="" class="q-pb-sm" style="min-height: 200px">
|
||||||
<template v-for="(agenda, index) in events[timestamp.weekday]" :key="agenda.id">
|
<eventslot
|
||||||
<eventslot v-model="events[timestamp.weekday][index]" />
|
v-for="(agenda, index) in events[timestamp.weekday]"
|
||||||
</template>
|
:key="index"
|
||||||
|
v-model="events[timestamp.weekday][index]"
|
||||||
|
@removeEvent="remove"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</q-calendar-agenda>
|
</q-calendar-agenda>
|
||||||
</div>
|
</div>
|
||||||
|
@ -86,7 +90,8 @@ export default defineComponent({
|
||||||
|
|
||||||
const calendarRealView = computed(() => (calendarDays.value != 7 ? 'day' : 'week'));
|
const calendarRealView = computed(() => (calendarDays.value != 7 ? 'day' : 'week'));
|
||||||
const calendarDays = computed(() =>
|
const calendarDays = computed(() =>
|
||||||
calendarView.value == 'day' ? 1 : windowWidth.value < 1000 ? 3 : 7
|
// <= 1023 is the breakpoint for sm to md
|
||||||
|
calendarView.value == 'day' ? 1 : windowWidth.value <= 1023 ? 3 : 7
|
||||||
);
|
);
|
||||||
const events = ref<Agendas>({});
|
const events = ref<Agendas>({});
|
||||||
|
|
||||||
|
@ -102,6 +107,22 @@ export default defineComponent({
|
||||||
await loadAgendas();
|
await loadAgendas();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function remove(id: number) {
|
||||||
|
if (await store.removeEvent(id)) {
|
||||||
|
// Successfull removed
|
||||||
|
for (const idx in events.value) {
|
||||||
|
const i = events.value[idx].findIndex((event) => event.id === id);
|
||||||
|
if (i !== -1) {
|
||||||
|
events.value[idx].splice(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not found, this means our eventa are outdated
|
||||||
|
await loadAgendas();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadAgendas() {
|
async function loadAgendas() {
|
||||||
const selected = new Date(selectedDate.value);
|
const selected = new Date(selectedDate.value);
|
||||||
console.log(selected);
|
console.log(selected);
|
||||||
|
@ -180,6 +201,7 @@ export default defineComponent({
|
||||||
updateProxy,
|
updateProxy,
|
||||||
saveNewSelectedDate,
|
saveNewSelectedDate,
|
||||||
proxyDate,
|
proxyDate,
|
||||||
|
remove,
|
||||||
calendarDays,
|
calendarDays,
|
||||||
calendarView,
|
calendarView,
|
||||||
calendarRealView,
|
calendarRealView,
|
||||||
|
|
|
@ -1,26 +1,52 @@
|
||||||
<template>
|
<template>
|
||||||
<q-card
|
<q-card
|
||||||
class="justify-start content-center items-center rounded-borders border-primary shadow-5 q-mb-xs"
|
class="q-mx-xs q-mt-sm justify-start content-center items-center rounded-borders shadow-5"
|
||||||
bordered
|
bordered
|
||||||
>
|
>
|
||||||
<header class="text-primary q-px-xs">
|
<q-card-section class="text-primary q-pa-xs">
|
||||||
<div class="col text-weight-bolder">
|
<div class="text-weight-bolder text-center" style="font-size: 1.5vw">
|
||||||
{{ event.type.name }}
|
{{ event.type.name }}
|
||||||
|
<template v-if="event.name"
|
||||||
|
>: <span style="font-size: 1.2vw">{{ event.name }}</span>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="event.description" class="col text-weight-medium" style="font-size: 10px">
|
<div v-if="event.description" class="text-weight-medium" style="font-size: 1vw">
|
||||||
Info
|
|
||||||
{{ event.description }}
|
{{ event.description }}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</q-card-section>
|
||||||
<div v-for="(job, index) in event.jobs" :key="index">
|
<q-separator />
|
||||||
<q-separator style="justify-start content-center" />
|
<q-card-section class="q-pa-xs">
|
||||||
<JobSlot v-model="event.jobs[index]" :event-id="event.id" />
|
<!-- Jobs -->
|
||||||
</div>
|
<JobSlot
|
||||||
|
v-for="(job, index) in event.jobs"
|
||||||
|
:key="index"
|
||||||
|
v-model="event.jobs[index]"
|
||||||
|
class="col q-my-xs"
|
||||||
|
:event-id="event.id"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions v-if="canEdit || canDelete" vertical align="center">
|
||||||
|
<router-link v-if="canEdit" :to="{ name: 'events-edit', params: { id: event.id } }">
|
||||||
|
<template #default>
|
||||||
|
<q-btn color="secondary" flat label="Bearbeiten" style="min-width: 95%" />
|
||||||
|
</template>
|
||||||
|
</router-link>
|
||||||
|
<q-btn
|
||||||
|
v-if="canDelete"
|
||||||
|
color="negative"
|
||||||
|
flat
|
||||||
|
label="Löschen"
|
||||||
|
style="min-width: 95%"
|
||||||
|
@click="remove"
|
||||||
|
/>
|
||||||
|
</q-card-actions>
|
||||||
</q-card>
|
</q-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, computed, PropType } from 'vue';
|
import { defineComponent, computed, PropType } from 'vue';
|
||||||
|
import { hasPermission } from 'src/utils/permission';
|
||||||
|
import { PERMISSIONS } from 'src/plugins/schedule/permissions';
|
||||||
import JobSlot from './JobSlot.vue';
|
import JobSlot from './JobSlot.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
@ -32,13 +58,26 @@ export default defineComponent({
|
||||||
type: Object as PropType<FG.Event>,
|
type: Object as PropType<FG.Event>,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
emits: { 'update:modelValue': (val: FG.Event) => !!val },
|
emits: {
|
||||||
|
'update:modelValue': (val: FG.Event) => !!val,
|
||||||
|
removeEvent: (val: number) => typeof val === 'number',
|
||||||
|
},
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
|
const canDelete = computed(() => hasPermission(PERMISSIONS.DELETE));
|
||||||
|
const canEdit = computed(() => hasPermission(PERMISSIONS.EDIT));
|
||||||
const event = computed({
|
const event = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
set: (v) => emit('update:modelValue', v),
|
set: (v) => emit('update:modelValue', v),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function remove() {
|
||||||
|
emit('removeEvent', props.modelValue.id);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
canDelete,
|
||||||
|
canEdit,
|
||||||
|
remove,
|
||||||
event,
|
event,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<q-card-section>
|
<q-card bordered>
|
||||||
<div class="text-weight-medium q-px-xs">
|
<div class="text-weight-medium q-px-xs">
|
||||||
{{ asHour(modelValue.start) }}
|
{{ asHour(modelValue.start) }}
|
||||||
<template v-if="modelValue.end">- {{ asHour(modelValue.end) }}</template>
|
<template v-if="modelValue.end">- {{ asHour(modelValue.end) }}</template>
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
<q-btn v-if="isEnrolled" flat color="negative" label="Austragen" @click="signOutFromJob" />
|
<q-btn v-if="isEnrolled" flat color="negative" label="Austragen" @click="signOutFromJob" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { defineAsyncComponent } from 'vue';
|
import { defineAsyncComponent } from 'vue';
|
||||||
import mainRoutes from './routes';
|
import { innerRoutes, privateRoutes } from './routes';
|
||||||
import { FG_Plugin } from 'src/plugins';
|
import { FG_Plugin } from 'src/plugins';
|
||||||
|
|
||||||
const plugin: FG_Plugin.Plugin = {
|
const plugin: FG_Plugin.Plugin = {
|
||||||
name: 'Schedule',
|
name: 'Schedule',
|
||||||
mainRoutes,
|
innerRoutes,
|
||||||
|
internalRoutes: privateRoutes,
|
||||||
requiredModules: ['User'],
|
requiredModules: ['User'],
|
||||||
requiredBackendModules: ['events'],
|
requiredBackendModules: ['events'],
|
||||||
version: '0.0.1',
|
version: '0.0.1',
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { FG_Plugin } from 'src/plugins';
|
import { FG_Plugin } from 'src/plugins';
|
||||||
import { PERMISSIONS } from '../permissions';
|
import { PERMISSIONS } from '../permissions';
|
||||||
|
|
||||||
const mainRoutes: FG_Plugin.PluginRouteConfig[] = [
|
export const innerRoutes: FG_Plugin.MenuRoute[] = [
|
||||||
{
|
{
|
||||||
title: 'Dienste',
|
title: 'Dienste',
|
||||||
icon: 'mdi-briefcase',
|
icon: 'mdi-briefcase',
|
||||||
|
@ -47,4 +47,10 @@ const mainRoutes: FG_Plugin.PluginRouteConfig[] = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default mainRoutes;
|
export const privateRoutes: FG_Plugin.NamedRouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
name: 'events-edit',
|
||||||
|
path: 'schedule/edit/:id',
|
||||||
|
redirect: { name: 'schedule-overview' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
|
@ -85,6 +85,16 @@ export const useScheduleStore = defineStore({
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async removeEvent(id: number) {
|
||||||
|
try {
|
||||||
|
await api.delete(`/schedule/events/${id}`);
|
||||||
|
} catch (e) {
|
||||||
|
const error = <AxiosError>e;
|
||||||
|
if (error.response && error.response.status === 404) return false;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
async removeEventType(id: number) {
|
async removeEventType(id: number) {
|
||||||
await api.delete(`/schedule/event-types/${id}`);
|
await api.delete(`/schedule/event-types/${id}`);
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<q-page v-if="checkMain" padding>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<q-list v-for="(mainRoute, index) in mainRoutes" :key="'mainRoute' + index">
|
|
||||||
<essential-link
|
|
||||||
v-for="(route, index2) in mainRoute.children"
|
|
||||||
:key="'route' + index2"
|
|
||||||
:title="route.title"
|
|
||||||
:icon="route.icon"
|
|
||||||
:link="route.name"
|
|
||||||
:permissions="route.permissions"
|
|
||||||
/>
|
|
||||||
</q-list>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-page>
|
|
||||||
<router-view />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import { computed, defineComponent } from 'vue';
|
|
||||||
import mainRoutes from 'src/plugins/user/routes';
|
|
||||||
import EssentialLink from 'src/components/navigation/EssentialLink.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
// name: 'PageName'
|
|
||||||
components: { EssentialLink },
|
|
||||||
setup() {
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const checkMain = computed(() => {
|
|
||||||
return route.matched.length == 2;
|
|
||||||
});
|
|
||||||
return { checkMain, mainRoutes };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -4,7 +4,7 @@ import { defineAsyncComponent } from 'vue';
|
||||||
|
|
||||||
const plugin: FG_Plugin.Plugin = {
|
const plugin: FG_Plugin.Plugin = {
|
||||||
name: 'User',
|
name: 'User',
|
||||||
mainRoutes: routes,
|
innerRoutes: routes,
|
||||||
requiredModules: [],
|
requiredModules: [],
|
||||||
requiredBackendModules: ['auth'],
|
requiredBackendModules: ['auth'],
|
||||||
version: '0.0.1',
|
version: '0.0.1',
|
||||||
|
|
|
@ -2,14 +2,14 @@ import { FG_Plugin } from 'src/plugins';
|
||||||
import { useMainStore } from 'src/store';
|
import { useMainStore } from 'src/store';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const mainRoutes: FG_Plugin.PluginRouteConfig[] = [
|
const mainRoutes: FG_Plugin.MenuRoute[] = [
|
||||||
{
|
{
|
||||||
get title() {
|
get title() {
|
||||||
return computed(() => useMainStore().user?.display_name || 'Not loaded');
|
return computed(() => useMainStore().currentUser.display_name);
|
||||||
},
|
},
|
||||||
icon: 'mdi-account',
|
icon: 'mdi-account',
|
||||||
permissions: ['user'],
|
permissions: ['user'],
|
||||||
route: { path: 'user', name: 'user', component: () => import('../pages/MainPage.vue') },
|
route: { path: 'user', name: 'user', redirect: { name: 'user-settings' } },
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: 'Einstellungen',
|
title: 'Einstellungen',
|
||||||
|
|
|
@ -24,7 +24,7 @@ const routes: RouteRecordRaw[] = [
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/main',
|
path: '/in',
|
||||||
redirect: 'dashboard',
|
redirect: 'dashboard',
|
||||||
component: () => import('layouts/MainLayout.vue'),
|
component: () => import('layouts/MainLayout.vue'),
|
||||||
meta: { permissions: ['user'] },
|
meta: { permissions: ['user'] },
|
||||||
|
|
|
@ -4,11 +4,3 @@ declare module '*.vue' {
|
||||||
const component: ComponentOptions;
|
const component: ComponentOptions;
|
||||||
export default component;
|
export default component;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
// Mocks all files ending in `.vue` showing them as plain Vue instances
|
|
||||||
declare module '*.vue' {
|
|
||||||
import Vue from 'vue';
|
|
||||||
export default Vue;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
export type Validator = (value: unknown) => boolean | string;
|
||||||
|
|
||||||
export function notEmpty(val: unknown) {
|
export function notEmpty(val: unknown) {
|
||||||
return !!val || 'Feld darf nicht leer sein!';
|
return !!val || 'Feld darf nicht leer sein!';
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue