routing inside url

This commit is contained in:
Julian Krauser 2024-09-01 19:19:48 +02:00
parent 2d0fb30558
commit 6247c385c3
15 changed files with 278 additions and 203 deletions

View file

@ -1,6 +1,6 @@
<template>
<footer
v-if="authCheck && routeName == 'admin'"
v-if="authCheck && routeName.includes('admin')"
class="md:hidden flex flex-row h-16 justify-center md:justify-normal p-1 bg-white"
>
<div class="w-full flex flex-row gap-2 h-full align-middle">
@ -23,7 +23,7 @@ export default defineComponent({
...mapState(useAuthStore, ["authCheck"]),
...mapState(useNavigationStore, ["topLevel"]),
routeName() {
return this.$route.name;
return typeof this.$route.name == "string" ? this.$route.name : "";
},
},
});

View file

@ -5,7 +5,7 @@
<h1 v-if="false" class="font-bold text-3xl w-fit whitespace-nowrap">Mitgliederverwaltung</h1>
</RouterLink>
<div class="flex flex-row gap-2 items-center">
<div v-if="authCheck && routeName == 'admin'" class="hidden md:flex flex-row gap-2 h-full align-middle">
<div v-if="authCheck && routeName.includes('admin')" class="hidden md:flex flex-row gap-2 h-full align-middle">
<TopLevelLink v-for="item in topLevel" :key="item.key" :link="item" />
</div>
<UserMenu v-if="authCheck" />
@ -29,7 +29,7 @@ export default defineComponent({
...mapState(useAuthStore, ["authCheck"]),
...mapState(useNavigationStore, ["topLevel"]),
routeName() {
return this.$route.name;
return typeof this.$route.name == "string" ? this.$route.name : "";
},
},
});

View file

@ -1,16 +1,14 @@
<template>
<div
v-if="link"
class="cursor-pointer w-full px-2 py-3"
:class="
activeLink?.key == link.key
? 'rounded-r-lg bg-red-200 border-l-4 border-l-primary'
: 'pl-3 hover:bg-red-200 rounded-lg'
"
@click="setLink(link.key)"
>
{{ link.title }}
</div>
<RouterLink v-if="link" v-slot="{ isExactActive }" :to="{ name: `admin-${activeNavigation}-${link.key}` }">
<p
class="cursor-pointer w-full px-2 py-3"
:class="
isExactActive ? 'rounded-r-lg bg-red-200 border-l-4 border-l-primary' : 'pl-3 hover:bg-red-200 rounded-lg'
"
>
{{ link.title }}
</p>
</RouterLink>
</template>
<script setup lang="ts">
@ -20,6 +18,7 @@ import { useNavigationStore, type navigationLinkModel } from "@/stores/admin/nav
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { RouterLink } from "vue-router";
export default defineComponent({
props: {
link: {
@ -28,11 +27,7 @@ export default defineComponent({
},
},
computed: {
...mapState(useNavigationStore, ["activeLink"]),
},
methods: {
...mapActions(useNavigationStore, ["setLink"]),
...mapState(useNavigationStore, ["activeNavigation"]),
},
});
</script>
@/stores/contest/viewManager

View file

@ -1,25 +1,30 @@
<template>
<div
<RouterLink
v-if="link"
class="cursor-pointer w-full flex flex-col md:flex-row items-center md:gap-2 justify-center p-1 md:rounded-full md:px-3 font-medium text-center text-base self-center"
:class="
activeNavigation == link.key
? 'text-primary md:bg-primary md:text-white'
: 'text-gray-700 hover:text-accent md:hover:bg-accent md:hover:text-white'
"
@click="setTopLevel(link.key, disableSubLink)"
:to="{ name: `admin-${link.key}-${!disableSubLink ? link.levelDefault : 'default'}` }"
class="cursor-pointer w-full flex items-center justify-center self-center"
>
{{ link.title }}
</div>
<p
class="cursor-pointer w-full flex flex-col md:flex-row items-center md:gap-2 justify-center p-1 md:rounded-full md:px-3 font-medium text-center text-base self-center"
:class="
activeNavigation == link.key
? 'text-primary md:bg-primary md:text-white'
: 'text-gray-700 hover:text-accent md:hover:bg-accent md:hover:text-white'
"
>
{{ link.title }}
</p>
</RouterLink>
</template>
<script setup lang="ts">
import { mapState, mapActions } from "pinia";
import { useNavigationStore, type topLevelNavigationModel } from "@/stores/admin/navigation";
import { mapState } from "pinia";
</script>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { RouterLink } from "vue-router";
export default defineComponent({
props: {
link: {
@ -34,8 +39,5 @@ export default defineComponent({
computed: {
...mapState(useNavigationStore, ["activeNavigation"]),
},
methods: {
...mapActions(useNavigationStore, ["setTopLevel"]),
},
});
</script>

View file

@ -2,16 +2,18 @@
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>{{ role.role }} <small v-if="role.permissions?.isAdmin">(Admin)</small></p>
<PencilIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
<PencilIcon class="w-5 h-5 p-1 box-content cursor-pointer" @click="openUpdateModal" />
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { defineComponent, defineAsyncComponent, markRaw, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import { PencilIcon } from "@heroicons/vue/outline";
import type { RoleViewModel } from "@/viewmodels/admin/role.models";
import { useModalStore } from "@/stores/modal";
import { useNavigationStore } from "@/stores/admin/navigation";
</script>
<script lang="ts">
@ -23,5 +25,14 @@ export default defineComponent({
return {};
},
mounted() {},
methods: {
...mapActions(useModalStore, ["openModal"]),
openUpdateModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/user/role/UpdateRoleModal.vue"))),
this.role.id
);
},
},
});
</script>

View file

@ -0,0 +1,54 @@
<template>
<div class="flex flex-col items-center gap-2 w-full max-w-6xl h-1/2 max-h-3/4">
<div class="flex flex-col">
<p class="text-xl font-medium">Rolle bearbeiten</p>
</div>
<form class="flex flex-col gap-4 py-2 w-full max-w-xl" @submit.prevent="">
<div>
<label for="role">Rollenbezeichnung</label>
<input type="text" id="role" required />
</div>
<div class="flex flex-row gap-2">
<button primary type="submit" :disabled="createStatus == 'loading' || createStatus?.status == 'success'">
speichern
</button>
<Spinner v-if="createStatus == 'loading'" class="my-auto" />
<SuccessCheckmark v-else-if="createStatus?.status == 'success'" />
<FailureXMark v-else-if="createStatus?.status == 'failed'" />
</div>
</form>
<div class="flex flex-row self-end mt-auto">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal">schließen</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { useRoleStore } from "@/stores/admin/role";
import Permission from "../../Permission.vue";
</script>
<script lang="ts">
export default defineComponent({
mounted() {
this.fetchRoleById(this.data);
},
computed: {
...mapState(useRoleStore, ["createStatus", "role"]),
...mapState(useModalStore, ["data"]),
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useRoleStore, ["fetchRoleById"]),
},
});
</script>

View file

@ -6,7 +6,7 @@
>
<slot name="sidebar"></slot>
</div>
<div class="max-w-full grow flex-col gap-4" :class="defaultRoute && defaultSidebar ? 'hidden md:flex' : 'flex'">
<div class="max-w-full grow flex-col gap-2" :class="defaultRoute && defaultSidebar ? 'hidden md:flex' : 'flex'">
<slot name="main"></slot>
</div>
</div>
@ -29,7 +29,7 @@ export default defineComponent({
computed: {
...mapState(useNavigationStore, ["activeLink"]),
defaultRoute() {
return this.activeLink == null;
return ((this.$route?.name as string) ?? "").includes("-default");
},
},
});

24
src/router/adminGuard.ts Normal file
View file

@ -0,0 +1,24 @@
import NProgress from "nprogress";
import { useAbilityStore } from "../stores/ability";
import { useNavigationStore } from "../stores/admin/navigation";
export async function abilityAndNavUpdate(to: any, from: any, next: any) {
NProgress.start();
const ability = useAbilityStore();
const navigation = useNavigationStore();
let type = to.meta.type;
let section = to.meta.section;
let module = to.meta.module;
navigation.activeNavigation = to.name.split("-")[1];
navigation.activeLink = to.name.split("-")[2];
if (ability.can(type, section, module)) {
NProgress.done();
next();
} else {
NProgress.done();
next(false);
}
}

View file

@ -4,6 +4,8 @@ import Login from "../views/Login.vue";
import { isAuthenticated } from "./authGuards";
import { loadAccountData } from "./accountGuard";
import { isSetup } from "./setupGuard";
import { abilityAndNavUpdate } from "./adminGuard";
import type { PermissionType, PermissionSection, PermissionModule } from "../types/permissionTypes";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -41,6 +43,110 @@ const router = createRouter({
name: "admin",
component: () => import("../views/admin/View.vue"),
beforeEnter: [isAuthenticated],
children: [
{
path: "",
name: "admin-default",
component: () => import("../views/RouterView.vue"),
},
{
path: "club",
name: "admin-club",
component: () => import("../views/RouterView.vue"),
meta: { type: "read", section: "club" },
beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-club-default",
component: () => import("../views/admin/ViewSelect.vue"),
},
{
path: "members",
name: "admin-club-members",
component: () => import("../views/admin/members/Overview.vue"),
},
{
path: "calendar",
name: "admin-club-calendar",
component: () => import("../views/admin/members/Overview.vue"),
},
{
path: "newsletter",
name: "admin-club-newsletter",
component: () => import("../views/admin/members/Overview.vue"),
},
{
path: "protocol",
name: "admin-club-protocol",
component: () => import("../views/admin/members/Overview.vue"),
},
],
},
{
path: "settings",
name: "admin-settings",
component: () => import("../views/RouterView.vue"),
meta: { type: "read", section: "settings" },
beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-settings-default",
component: () => import("../views/admin/ViewSelect.vue"),
},
{
path: "qualification",
name: "admin-settings-qualification",
component: () => import("../views/admin/members/Overview.vue"),
},
{
path: "award",
name: "admin-settings-award",
component: () => import("../views/admin/members/Overview.vue"),
},
{
path: "executive-position",
name: "admin-settings-executive_position",
component: () => import("../views/admin/members/Overview.vue"),
},
{
path: "communication",
name: "admin-settings-communication",
component: () => import("../views/admin/members/Overview.vue"),
},
],
},
{
path: "user",
name: "admin-user",
component: () => import("../views/RouterView.vue"),
meta: { type: "read", section: "user" },
beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-user-default",
component: () => import("../views/admin/ViewSelect.vue"),
},
{
path: "user",
name: "admin-user-user",
component: () => import("../views/admin/user/User.vue"),
},
{
path: "role",
name: "admin-user-role",
component: () => import("../views/admin/user/Role.vue"),
},
],
},
{
path: ":pathMatch(.*)*",
name: "admin-404",
component: () => import("../views/notFound.vue"),
},
],
},
{
path: "/nopermissions",
@ -56,3 +162,11 @@ const router = createRouter({
});
export default router;
declare module "vue-router" {
interface RouteMeta {
type?: PermissionType | "admin";
section?: PermissionSection;
module?: PermissionModule;
}
}

View file

@ -1,6 +1,6 @@
import { defineStore } from "pinia";
import { shallowRef, defineAsyncComponent } from "vue";
import { useAbilityStore } from "../ability";
import router from "../../router";
export interface navigationModel {
club: navigationSplitModel;
@ -26,17 +26,15 @@ export interface topLevelNavigationModel {
export interface navigationLinkModel {
key: string;
title: string;
component: any;
}
export const useNavigationStore = defineStore("navigation", {
state: () => {
return {
activeNavigation: "club" as topLevelNavigationType,
activeLink: null as null | navigationLinkModel,
activeLink: null as null | string,
topLevel: [] as Array<topLevelNavigationModel>,
navigation: {} as navigationModel,
componentOverwrite: null as null | any,
};
},
getters: {
@ -45,42 +43,10 @@ export const useNavigationStore = defineStore("navigation", {
(state.topLevel.find((elem) => elem.key == state.activeNavigation) ?? {}) as topLevelNavigationModel,
},
actions: {
setTopLevel(key: topLevelNavigationType, disableSubLink: boolean = true) {
let level = this.topLevel.find((e) => e.key == key) ?? null;
if (!level) {
this.activeNavigation = "club";
if (!disableSubLink) this.setLink(this.topLevel.find((e) => e.key == "club")?.levelDefault ?? null);
else this.setLink(null);
} else {
this.activeNavigation = level.key;
if (!disableSubLink) this.setLink(level.levelDefault);
else this.setLink(null);
}
this.resetComponentOverwrite();
},
setLink(key: string | null) {
let nav = this.navigation[this.activeNavigation];
if (!nav) {
this.activeLink = null;
return;
}
let links = [...Object.values(nav.main), ...Object.values(nav.top ?? {})];
this.activeLink = links.find((e) => e.key == key) ?? null;
this.resetComponentOverwrite();
},
setTopLevelNav(topLeveLinks: Array<topLevelNavigationModel>) {
this.topLevel = topLeveLinks;
},
setComponentOverwrite(component: any) {
this.componentOverwrite = component;
},
resetComponentOverwrite() {
this.componentOverwrite = null;
},
resetNavigation() {
this.$reset();
},
updateTopLevel() {
updateTopLevel(first: boolean = false) {
const abilityStore = useAbilityStore();
this.topLevel = [
...(abilityStore.canSection("read", "club")
@ -88,7 +54,7 @@ export const useNavigationStore = defineStore("navigation", {
{
key: "club",
title: "Verein",
levelDefault: "#members",
levelDefault: "members",
} as topLevelNavigationModel,
]
: []),
@ -97,7 +63,7 @@ export const useNavigationStore = defineStore("navigation", {
{
key: "settings",
title: "Einstellungen",
levelDefault: "#qualification",
levelDefault: "qualification",
} as topLevelNavigationModel,
]
: []),
@ -106,124 +72,51 @@ export const useNavigationStore = defineStore("navigation", {
{
key: "user",
title: "Benutzer",
levelDefault: "#user",
levelDefault: "user",
} as topLevelNavigationModel,
]
: []),
];
if (this.topLevel.findIndex((e) => e.key == this.activeNavigation) == -1)
this.activeNavigation = this.topLevel[0]?.key ?? "club";
if (this.topLevel.findIndex((e) => e.key == this.activeNavigation) == -1 && !first)
router.push({ name: `admin-${this.topLevel[0]?.key ?? "club"}-default` });
},
updateNavigation() {
updateNavigation(first: boolean = false) {
const abilityStore = useAbilityStore();
this.navigation = {
club: {
mainTitle: "Verein",
main: [
...(abilityStore.can("read", "club", "members")
? [
{
key: "#members",
title: "Mitglieder",
component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))),
},
]
: []),
...(abilityStore.can("read", "club", "calendar")
? [
{
key: "#calendar",
title: "Termine",
component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))),
},
]
: []),
...(abilityStore.can("read", "club", "newsletter")
? [
{
key: "#newsletter",
title: "Newsletter",
component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))),
},
]
: []),
...(abilityStore.can("read", "club", "protocoll")
? [
{
key: "#protocol",
title: "Protokolle",
component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))),
},
]
: []),
...(abilityStore.can("read", "club", "members") ? [{ key: "members", title: "Mitglieder" }] : []),
...(abilityStore.can("read", "club", "calendar") ? [{ key: "calendar", title: "Termine" }] : []),
...(abilityStore.can("read", "club", "newsletter") ? [{ key: "newsletter", title: "Newsletter" }] : []),
...(abilityStore.can("read", "club", "protocoll") ? [{ key: "protocol", title: "Protokolle" }] : []),
],
},
settings: {
mainTitle: "Einstellungen",
main: [
...(abilityStore.can("read", "settings", "qualification")
? [
{
key: "#qualification",
title: "Qualifikationen",
component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))),
},
]
: []),
...(abilityStore.can("read", "settings", "award")
? [
{
key: "#award",
title: "Auszeichnungen",
component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))),
},
]
? [{ key: "qualification", title: "Qualifikationen" }]
: []),
...(abilityStore.can("read", "settings", "award") ? [{ key: "award", title: "Auszeichnungen" }] : []),
...(abilityStore.can("read", "settings", "executive_position")
? [
{
key: "#executive_position",
title: "Vereinsämter",
component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))),
},
]
? [{ key: "executive_position", title: "Vereinsämter" }]
: []),
...(abilityStore.can("read", "settings", "communication")
? [
{
key: "#communication",
title: "Mitgliederdaten",
component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))),
},
]
? [{ key: "communication", title: "Mitgliederdaten" }]
: []),
],
},
user: {
mainTitle: "Benutzer",
main: [
...(abilityStore.can("read", "user", "user")
? [
{
key: "#user",
title: "Benutzer",
component: shallowRef(defineAsyncComponent(() => import("@/views/admin/user/User.vue"))),
},
]
: []),
...(abilityStore.can("read", "user", "role")
? [
{
key: "#role",
title: "Rollen",
component: shallowRef(defineAsyncComponent(() => import("@/views/admin/user/Role.vue"))),
},
]
: []),
...(abilityStore.can("read", "user", "user") ? [{ key: "user", title: "Benutzer" }] : []),
...(abilityStore.can("read", "user", "role") ? [{ key: "role", title: "Rollen" }] : []),
],
},
} as navigationModel;
if (this.topLevel.findIndex((e) => e.key == this.activeLink?.key) == -1) this.setLink(null);
if (this.topLevel.findIndex((e) => e.key == this.activeLink) == -1 && !first)
router.push({ name: `admin-${this.activeNavigation}-default` });
},
},
});

View file

@ -25,7 +25,7 @@ export const useRoleStore = defineStore("role", {
this.loadingAll = "failed";
});
},
fetchRolesById(id: number) {
fetchRoleById(id: number) {
this.role = null;
this.loadingSingle = "loading";
http

View file

@ -24,7 +24,7 @@ export const useUserStore = defineStore("user", {
this.loadingAll = "failed";
});
},
fetchUsersById(id: number) {
fetchUserById(id: number) {
this.user = null;
this.loadingSingle = "loading";
http

View file

@ -1,6 +1,8 @@
<template>
<div v-if="!defaultRoute && showBack" class="flex md:hidden flex-row items-baseline">
<p v-if="!defaultRoute && showBack" class="text-primary" @click="setLink(null)">zur Übersicht</p>
<RouterLink v-if="!defaultRoute && showBack" :to="{ name: `${rootRoute}-default` }" class="mid:hidden text-primary">
zur Übersicht
</RouterLink>
</div>
<slot v-if="headerInsert" name="headerInsert"></slot>
<div
@ -37,7 +39,10 @@ export default defineComponent({
computed: {
...mapState(useNavigationStore, ["activeLink"]),
defaultRoute() {
return this.activeLink == null;
return ((this.$route?.name as string) ?? "").includes("-default");
},
rootRoute() {
return ((this.$route?.name as string) ?? "").split("-")[0];
},
diffMain() {
return this.$slots.diffMain;
@ -49,8 +54,5 @@ export default defineComponent({
return window.matchMedia("(min-width: 800px)").matches;
},
},
methods: {
...mapActions(useNavigationStore, ["setLink"]),
},
});
</script>

View file

@ -15,8 +15,7 @@
</SidebarTemplate>
</template>
<template #main>
<component v-if="display" :is="displayed" />
<div v-else class="w-full h-full bg-white rounded-lg"></div>
<RouterView />
</template>
</SidebarLayout>
</template>
@ -29,6 +28,7 @@ import SidebarLayout from "@/layouts/Sidebar.vue";
import SidebarTemplate from "@/templates/Sidebar.vue";
import RoutingLink from "@/components/admin/RoutingLink.vue";
import { useAbilityStore } from "../../stores/ability";
import RouterView from "../RouterView.vue";
</script>
<script lang="ts">
@ -40,44 +40,21 @@ export default defineComponent({
},
},
computed: {
...mapState(useNavigationStore, [
"activeNavigationObject",
"activeTopLevelObject",
"activeLink",
"componentOverwrite",
]),
display(): boolean {
return this.activeLink?.component || this.componentOverwrite;
},
displayed() {
if (this.componentOverwrite != null) {
return this.componentOverwrite;
} else {
return this.activeLink?.component;
}
},
...mapState(useNavigationStore, ["activeNavigationObject", "activeTopLevelObject", "activeLink"]),
},
created() {
useAbilityStore().$subscribe(() => {
this.updateTopLevel();
this.updateNavigation();
});
this.updateTopLevel();
this.updateNavigation();
this.setLink(this.activeTopLevelObject.levelDefault);
this.updateTopLevel(true);
this.updateNavigation(true);
},
beforeUnmount() {
this.resetNavigation();
},
methods: {
...mapActions(useNavigationStore, [
"setLink",
"resetNavigation",
"setTopLevel",
"updateTopLevel",
"updateNavigation",
]),
...mapActions(useNavigationStore, ["resetNavigation", "updateTopLevel", "updateNavigation"]),
},
});
</script>

View file

@ -0,0 +1,3 @@
<template>
<div class="w-full h-full bg-white rounded-md flex items-center justify-center">bitte auswählen</div>
</template>