Merge branch 'develop' into milestone/ff-admin-unit

# Conflicts:
#	src/components/Modal.vue
#	src/components/TextCopy.vue
This commit is contained in:
Julian Krauser 2025-07-23 10:45:04 +02:00
commit fed08e0232
11 changed files with 264 additions and 279 deletions

View file

@ -2,11 +2,11 @@
<Modal />
<ContextMenu />
<Header @contextmenu.prevent />
<div class="grow overflow-x-hidden overflow-y-auto p-2 md:p-4" @contextmenu.prevent>
<AppHeader />
<div class="grow overflow-x-hidden overflow-y-auto p-2 md:p-4">
<RouterView />
</div>
<Footer @contextmenu.prevent />
<AppFooter />
<Notification />
<Teleport to="head">
@ -18,10 +18,11 @@
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
import { onLongPress } from "@vueuse/core";
import { RouterView } from "vue-router";
import Header from "./components/Header.vue";
import Footer from "./components/Footer.vue";
import AppHeader from "./components/Header.vue";
import AppFooter from "./components/Footer.vue";
import { mapActions, mapState } from "pinia";
import { useAuthStore } from "./stores/auth";
import { isAuthenticatedPromise } from "./router/authGuard";
@ -31,6 +32,7 @@ import Notification from "./components/Notification.vue";
import { config } from "./config";
import { useConfigurationStore } from "@/stores/configuration";
import { resetAllPiniaStores } from "@/helpers/piniaReset";
import { useContextMenuStore } from "./stores/context-menu";
</script>
<script lang="ts">
@ -40,6 +42,11 @@ export default defineComponent({
...mapState(useConfigurationStore, ["clubName"]),
},
mounted() {
document.body.addEventListener("contextmenu", (event) => {
this.handleContextMenu(event);
});
onLongPress(document.body, this.handleContextMenu);
resetAllPiniaStores();
this.configure();
@ -52,6 +59,21 @@ export default defineComponent({
},
methods: {
...mapActions(useConfigurationStore, ["configure"]),
...mapActions(useContextMenuStore, ["openContextMenu"]),
handleContextMenu(e: MouseEvent) {
e.preventDefault();
// TODO allow contextmenu on elements with special attribute with reduced selection
const target = e.target as HTMLElement | null;
if (!target) return;
if (["INPUT", "TEXTAREA", "P", "H1", "H2", "H3", "H4"].includes((target as HTMLElement).nodeName)) {
this.openContextMenu(e, {
component_ref: markRaw(defineAsyncComponent(() => import("@/components/CopyPasteContextMenu.vue"))),
data: ["INPUT", "TEXTAREA"].includes((e.target as HTMLElement).nodeName) ? "" : "nopaste",
});
}
},
},
});
</script>

View file

@ -1,10 +1,9 @@
<template>
<div
ref="contextMenu"
class="absolute flex flex-col gap-1 border border-gray-400 bg-white rounded-md select-none text-left shadow-md z-50 p-1"
class="absolute flex flex-col gap-1 border border-gray-400 bg-white rounded-md select-none text-left shadow-md z-[100] p-1"
v-show="show"
:style="contextMenuStyle"
@contextmenu.prevent
@click="closeContextMenu"
>
<component :is="component_ref" :data="data" />

View file

@ -0,0 +1,61 @@
<template>
<div class="flex flex-row gap-2 cursor-pointer hover:bg-gray-300 p-1 rounded-md" @click="copy">
<DocumentDuplicateIcon class="w-5 h-5" />
<p>kopieren</p>
</div>
<div
v-if="data != 'nopaste'"
class="flex flex-row gap-2 cursor-pointer hover:bg-gray-300 p-1 rounded-md"
@click="paste"
>
<ClipboardDocumentIcon class="w-5 h-5" />
<p>einfügen</p>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { ClipboardDocumentIcon, DocumentDuplicateIcon } from "@heroicons/vue/24/outline";
import { mapState } from "pinia";
import { useContextMenuStore } from "@/stores/context-menu";
</script>
<script lang="ts">
export default defineComponent({
props: ["data"],
data() {
return {
selectedText: "",
};
},
computed: {
...mapState(useContextMenuStore, ["clickedOnEl"]),
},
mounted() {
this.selectedText =
document.getSelection()?.toString() || this.clickedOnEl.value || this.clickedOnEl.innerText || "";
let selection = document.getSelection()?.toString();
console.log(selection);
if (selection == "") {
console.log("jo");
const range = document.createRange();
range.selectNode(this.clickedOnEl);
console.log(range);
window.getSelection()?.removeAllRanges();
window.getSelection()?.addRange(range);
}
},
methods: {
copy() {
navigator.clipboard.writeText(this.selectedText);
},
paste() {
const el = this.clickedOnEl;
navigator.clipboard.readText().then((e) => {
el.value = e;
});
},
},
});
</script>

View file

@ -3,14 +3,11 @@
ref="contextMenu"
class="absolute inset-0 w-full h-full flex justify-center items-center bg-black/50 select-none z-50 p-2"
v-show="show"
@contextmenu.prevent
>
<!-- @click="closeModal" -->
<component
:is="component_ref"
:data="data"
:callback="callback"
@click.stop
class="p-4 bg-white rounded-lg max-h-[95%] overflow-y-auto"
/>
</div>

View file

@ -1,6 +1,6 @@
<template>
<div class="flex w-full relative">
<input type="text" :value="copyText" />
<input type="text" readonly :value="copyText" />
<ClipboardIcon
class="w-5 h-5 p-2 box-content absolute right-1 top-1/2 -translate-y-1/2 bg-white cursor-pointer"
@click="copyToClipboard"

View file

@ -0,0 +1,52 @@
<template>
<div class="flex flex-col gap-1">
<p>
<span class="font-semibold text-lg">{{ version.title }}</span> vom
{{
new Date(version.isoDate).toLocaleDateString("de", {
month: "2-digit",
day: "2-digit",
year: "numeric",
})
}}
</p>
<div class="versionDisplay flex flex-col" v-html="version['content:encoded']"></div>
</div>
</template>
<script setup lang="ts">
import type { Release } from "@/viewmodels/version.models";
import { defineComponent, type PropType } from "vue";
</script>
<script lang="ts">
export default defineComponent({
props: {
version: {
type: Object as PropType<Release>,
required: true,
},
},
});
</script>
<style lang="css" scoped>
@reference "@/main.css";
.versionDisplay :deep() ul {
list-style: none;
padding-left: 10px;
padding-bottom: 5px;
}
.versionDisplay :deep() ul li::before {
content: "-";
margin-right: 10px;
color: black;
font-weight: 600;
}
.versionDisplay :deep() a {
@apply text-primary;
}
</style>

View file

@ -8,6 +8,7 @@ export const useContextMenuStore = defineStore("context-menu", {
show: false,
component_ref: null as any,
data: null as any,
clickedOnEl: null as any,
};
},
getters: {
@ -16,16 +17,18 @@ export const useContextMenuStore = defineStore("context-menu", {
},
},
actions: {
openContextMenu(e: MouseEvent, content: { component_ref: any; data: any }) {
openContextMenu(e: MouseEvent, content: { component_ref: any; data?: any }) {
this.component_ref = content.component_ref;
this.data = content.data;
this.contextX = e.pageX;
this.contextY = e.pageY;
this.clickedOnEl = e.target;
this.show = true;
},
closeContextMenu() {
this.component_ref = null;
this.data = null;
this.clickedOnEl = null;
this.show = false;
},
},

View file

@ -11,7 +11,7 @@
</small>
</h1>
<p>
V{{ clientVersion }} ({{
v{{ clientVersion }} ({{
new Date(clientVersionRelease).toLocaleDateString("de", {
month: "2-digit",
day: "2-digit",
@ -23,19 +23,7 @@
</p>
</div>
<div class="grow flex flex-col gap-4 overflow-y-scroll">
<div v-for="version in newerClientVersions">
<p>
<span class="font-semibold text-lg">V{{ version.title }}</span> vom
{{
new Date(version.isoDate).toLocaleDateString("de", {
month: "2-digit",
day: "2-digit",
year: "numeric",
})
}}
</p>
<div class="flex flex-col" v-html="version['content:encoded']"></div>
</div>
<VersionItem v-for="version in newerClientVersions" :key="version.title" :version="version" />
<div v-if="newerClientVersions.length == 0" class="flex items-center justify-center">
<p>Der Client ist auf der neuesten Version.</p>
</div>
@ -50,7 +38,7 @@
</small>
</h1>
<p>
V{{ serverVersion }} ({{
v{{ serverVersion }} ({{
new Date(serverVersionRelease).toLocaleDateString("de", {
month: "2-digit",
day: "2-digit",
@ -61,20 +49,8 @@
}})
</p>
</div>
<div class="grow flex flex-col gap-2 overflow-y-scroll">
<div v-for="version in newerServerVersions">
<p>
<span class="font-semibold text-lg">V{{ version.title }}</span> vom
{{
new Date(version.isoDate).toLocaleDateString("de", {
month: "2-digit",
day: "2-digit",
year: "numeric",
})
}}
</p>
<div class="flex flex-col" v-html="version['content:encoded']"></div>
</div>
<div class="grow flex flex-col gap-4 overflow-y-scroll">
<VersionItem v-for="version in newerServerVersions" :key="version.title" :version="version" />
<div v-if="newerServerVersions.length == 0" class="flex items-center justify-center">
<p>Der Server ist auf der neuesten Version.</p>
</div>
@ -90,6 +66,7 @@ import { defineComponent } from "vue";
import MainTemplate from "@/templates/Main.vue";
import clientPackage from "../../../../../package.json";
import type { Releases } from "@/viewmodels/version.models";
import VersionItem from "@/components/admin/management/version/VersionItem.vue";
</script>
<script lang="ts">
@ -113,11 +90,11 @@ export default defineComponent({
},
serverVersionRelease() {
if (!this.serverRss) return "";
return this.serverRss.items.find((i) => i.title == this.serverVersion)?.isoDate ?? "";
return this.serverRss.items.find((i) => i.title == `v${this.serverVersion}`)?.isoDate ?? "";
},
clientVersionRelease() {
if (!this.clientRss) return "";
return this.clientRss.items.find((i) => i.title == this.clientVersion)?.isoDate ?? "";
return this.clientRss.items.find((i) => i.title == `v${this.clientVersion}`)?.isoDate ?? "";
},
},
mounted() {

View file

@ -16,7 +16,7 @@
<div class="flex flex-row gap-2">
<button type="submit" primary :disabled="resetStatus == 'loading' || resetStatus == 'success'">
TOTP zurücksetzen
Zugangsdaten zurücksetzen
</button>
<Spinner v-if="resetStatus == 'loading'" class="my-auto" />
<SuccessCheckmark v-else-if="resetStatus == 'success'" />