release v2.0.0 #4

Merged
crimsen merged 481 commits from develop into master 2024-01-18 15:15:08 +00:00
25 changed files with 526 additions and 474 deletions
Showing only changes of commit e0046aa7d2 - Show all commits

@ -1 +1 @@
Subproject commit d0e503b1bfc65f64216042d0fe39daf5377b7901 Subproject commit ac6d0693a062f60d539d7f6d8fdee00fbcc528c7

View File

@ -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
children.forEach((child) => { * @param children MenuRoute to convert
setPermissions(child); */
convertRoutes(child.route, child.children); function convertRoutes(parent: RouteRecordRaw, children?: FG_Plugin.MenuRoute[]) {
if (parent.children === undefined) parent.children = []; if (children !== undefined) {
parent.children.push(child.route); children.forEach((child) => {
}); setPermissions(child);
convertRoutes(child.route, child.children);
if (parent.children === undefined) parent.children = [];
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) => {
if (targetPluginMainLink) { // Check if source is already in target
source.children?.forEach((sourcePluginChildLink: FG_Plugin.PluginRouteConfig) => { const targetRoot = target.children?.find(
targetPluginMainLink.children.push(<FG_Plugin.PluginChildLink>{ (targetRoot) => sourceRoute.path === targetRoot.path
title: sourcePluginChildLink.title, );
icon: sourcePluginChildLink.icon, // Already in target routes, add only children
link: sourcePluginChildLink.route.name, if (targetRoot) {
name: sourcePluginChildLink.route.name, if (targetRoot.children === undefined) targetRoot.children = [];
permissions: sourcePluginChildLink.permissions, 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);
}
}); });
}); }
} else { });
const mainLink: FG_Plugin.PluginMainLink = <FG_Plugin.PluginMainLink>{ }
/**
* 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>{
title: child.title,
icon: child.icon,
link: child.route.name,
name: child.route.name,
permissions: child.permissions,
});
}); });
target.push(mainLink);
} }
return target; 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,
});
});
} }
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;
});
if (plugin) {
if (plugin.mainRoutes) {
loadedPlugins.routes = combineRoutes(loadedPlugins.routes, plugin.mainRoutes, '/main');
plugin.mainRoutes.forEach((route) => {
loadedPlugins.mainLinks = combineMainLinks(loadedPlugins.mainLinks, route);
});
loadedPlugins.shortcuts = loadShortCuts(loadedPlugins.shortcuts, plugin.mainRoutes);
}
if (plugin.outRoutes) {
loadedPlugins.routes = combineRoutes(loadedPlugins.routes, plugin.outRoutes);
loadedPlugins.shortcutsOut = loadShortCuts(loadedPlugins.shortcutsOut, plugin.outRoutes);
}
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,
});
} else {
console.exception(`Could not find required Plugin ${requiredModule}`);
router.push({ name: 'error' }).catch((e) => {
console.warn(e);
});
}
});
return loadedPlugins;
}
async function getBackend(): Promise<Backend | null> { // Search if plugin is installed
let backend: Backend | null = null; const available = context.keys();
try { const plugin = available.includes(`./${pluginName.toLowerCase()}/plugin.ts`)
const response: AxiosResponse<Backend> = await api.get('/'); ? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
backend = response.data; <FG_Plugin.Plugin>context(`./${pluginName.toLowerCase()}/plugin.ts`).default
} catch (e) { : undefined;
console.warn(e);
return null; if (!plugin) {
} finally { // Plugin is not found, results in an error
return backend; 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,
});
return plugin;
} }
} }
export default boot(({ router, app }) => { /**
const plugins: FG_Plugin.Plugin[] = []; * Loading backend information
* @returns Backend object or null
*/
async function getBackend() {
try {
const { data }: AxiosResponse<Backend> = await api.get('/');
return data;
} catch (e) {
console.warn(e);
return null;
}
}
const backendPromise = getBackend(); /**
* 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;
}
let loadedPlugins: FG_Plugin.Flaschengeist = { const 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 // Start loading plugins
plugins.push(pluginsContext(fileName).default); // Load required modules:
config.requiredModules.forEach((required) => {
const plugin = loadPlugin(loadedPlugins, required, pluginsContext, backend);
if (!plugin) {
void router.push({ name: 'error' });
return;
}
}); });
// check dependencies // Load user defined plugins
backendPromise config.loadModules.forEach((required) => {
.then((backend) => { const plugin = loadPlugin(loadedPlugins, required, pluginsContext, backend);
if (backend) { if (!plugin) {
plugins.forEach((plugin: FG_Plugin.Plugin) => { void router.push({ name: 'error' });
plugin.requiredModules.forEach((requiredModule: string) => { return;
if ( }
!( });
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)) {
console.error(
`Plugin ${plugin.name} need Plugin ${requiredBackendModule} in backend.`
);
router.push({ name: 'error' }).catch((err) => {
console.warn(err);
});
}
});
});
}
})
.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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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',

View File

@ -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" :shortcut="shortcut"
:link="shortcut.link" />
: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,
}; };
}, },
}); });

View File

@ -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" :shortcut="shortcut"
:link="shortcut.link"
: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>

128
src/plugins.d.ts vendored
View File

@ -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[];
}
} }

View File

@ -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',

View File

@ -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',

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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,
}; };
}, },

View File

@ -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">

View File

@ -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',

View File

@ -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' },
},
];

View File

@ -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}`);

View File

@ -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>

View File

@ -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',

View File

@ -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',

View File

@ -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'] },

8
src/shims-vue.d.ts vendored
View File

@ -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;
}
*/

View File

@ -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!';
} }