From 06380e48c5e05082c514c9c7efc8daa97d719371 Mon Sep 17 00:00:00 2001 From: Julian Krauser <jkrauser209@gmail.com> Date: Tue, 29 Apr 2025 13:10:30 +0200 Subject: [PATCH] Settings form and handling --- .../admin/management/setting/AppSetting.vue | 65 +++++++--- .../management/setting/BackupSetting.vue | 49 +++++--- .../admin/management/setting/BaseSetting.vue | 79 ++++++++++++ .../management/setting/ClubImageSetting.vue | 90 +++++++++++++ .../admin/management/setting/ClubSetting.vue | 107 ++++++++++------ .../admin/management/setting/MailSetting.vue | 119 ++++++++++++------ .../management/setting/SessionSetting.vue | 74 ++++++++--- src/stores/admin/management/setting.ts | 28 ++++- src/stores/configuration.ts | 6 +- .../admin/management/setting/Setting.vue | 3 + 10 files changed, 485 insertions(+), 135 deletions(-) create mode 100644 src/components/admin/management/setting/BaseSetting.vue create mode 100644 src/components/admin/management/setting/ClubImageSetting.vue diff --git a/src/components/admin/management/setting/AppSetting.vue b/src/components/admin/management/setting/AppSetting.vue index 234a4ab..787dbe1 100644 --- a/src/components/admin/management/setting/AppSetting.vue +++ b/src/components/admin/management/setting/AppSetting.vue @@ -1,42 +1,67 @@ <template> - <div class="flex flex-col w-full"> - <div class="border-l-3 border-l-primary p-2 rounded-t-lg bg-red-200"> - <p class="text-lg font-semibold">Anwendungs Einstellungen</p> + <BaseSetting title="Anwendungs Einstellungen" :submit-function="submit" v-slot="{ enableEdit }"> + <div class="w-full"> + <label for="custom_login_message">Nachricht unter Login (optional)</label> + <input + id="custom_login_message" + type="text" + :readonly="!enableEdit" + :value="appSettings['app.custom_login_message']" + /> </div> - <div class="border-l-3 border-l-primary p-2 rounded-b-lg"> - <div class="w-full"> - <label for="name">Vereins-Name</label> - <input id="name" type="text" readonly :value="appSettings['app.custom_login_message']" /> - </div> - <div class="w-full flex flex-row items-center gap-2"> - <div - v-if="true" - class="border-2 border-gray-500 rounded-sm" - :class="appSettings['app.show_link_to_calendar'] ? 'bg-gray-500' : 'h-3 w-3'" - > - <CheckIcon v-if="appSettings['app.show_link_to_calendar']" class="h-2.5 w-2.5 stroke-4 text-white" /> - </div> - <input v-else id="name" type="checkbox" :checked="appSettings['app.show_link_to_calendar']" /> - <label for="name">Kalender-Link anzeigen</label> + <div class="w-full flex flex-row items-center gap-2"> + <div + v-if="!enableEdit" + class="border-2 border-gray-500 rounded-sm" + :class="appSettings['app.show_link_to_calendar'] ? 'bg-gray-500' : 'h-3.5 w-3.5'" + > + <CheckIcon v-if="appSettings['app.show_link_to_calendar']" class="h-2.5 w-2.5 stroke-4 text-white" /> </div> + <input v-else id="show_link_to_calendar" type="checkbox" :checked="appSettings['app.show_link_to_calendar']" /> + <label for="show_link_to_calendar">Kalender-Link anzeigen</label> </div> - </div> + </BaseSetting> </template> <script setup lang="ts"> +import { useAbilityStore } from "@/stores/ability"; import { useSettingStore } from "@/stores/admin/management/setting"; import { CheckIcon } from "@heroicons/vue/24/outline"; -import { mapState } from "pinia"; +import { mapActions, mapState } from "pinia"; import { defineComponent } from "vue"; +import BaseSetting from "./BaseSetting.vue"; </script> <script lang="ts"> export default defineComponent({ + data() { + return { + enableEdit: false as boolean, + status: undefined as undefined | "loading" | "success" | "failed", + }; + }, computed: { ...mapState(useSettingStore, ["readByTopic"]), + ...mapState(useAbilityStore, ["can"]), appSettings() { return this.readByTopic("app"); }, }, + methods: { + ...mapActions(useSettingStore, ["updateSettings"]), + submit(e: any) { + const formData = e.target.elements; + return this.updateSettings([ + { + key: "app.custom_login_message", + value: formData.custom_login_message.value || null, + }, + { + key: "app.show_link_to_calendar", + value: formData.show_link_to_calendar.checked || null, + }, + ]); + }, + }, }); </script> diff --git a/src/components/admin/management/setting/BackupSetting.vue b/src/components/admin/management/setting/BackupSetting.vue index f18298a..f71a36a 100644 --- a/src/components/admin/management/setting/BackupSetting.vue +++ b/src/components/admin/management/setting/BackupSetting.vue @@ -1,34 +1,53 @@ <template> - <div class="flex flex-col w-full"> - <div class="border-l-3 border-l-primary p-2 rounded-t-lg bg-red-200"> - <p class="text-lg font-semibold">Backup Einstellungen</p> + <BaseSetting title="Backup Einstellungen" :submit-function="submit" v-slot="{ enableEdit }"> + <div class="w-full"> + <label for="copies">Anzahl paralleler Backups (optional)</label> + <input id="copies" type="text" :readonly="!enableEdit" :value="backupSettings['backup.copies']" /> </div> - <div class="border-l-3 border-l-primary p-2 rounded-b-lg"> - <div class="w-full"> - <label for="name">Anzahl paralleler Backups</label> - <input id="name" type="text" readonly :value="backupSettings['backup.copies']" /> - </div> - <div class="w-full"> - <label for="name">Intervall zur Backup-Erstellung</label> - <input id="name" type="text" readonly :value="backupSettings['backup.interval']" /> - </div> - </div> - </div> + <div class="w-full"> + <label for="interval">Intervall zur Backup-Erstellung (optional)</label> + <input id="interval" type="text" :readonly="!enableEdit" :value="backupSettings['backup.interval']" /></div + ></BaseSetting> </template> <script setup lang="ts"> +import { useAbilityStore } from "@/stores/ability"; import { useSettingStore } from "@/stores/admin/management/setting"; -import { mapState } from "pinia"; +import { mapActions, mapState } from "pinia"; import { defineComponent } from "vue"; +import BaseSetting from "./BaseSetting.vue"; </script> <script lang="ts"> export default defineComponent({ + data() { + return { + enableEdit: false as boolean, + status: undefined as undefined | "loading" | "success" | "failed", + }; + }, computed: { ...mapState(useSettingStore, ["readByTopic"]), + ...mapState(useAbilityStore, ["can"]), backupSettings() { return this.readByTopic("backup"); }, }, + methods: { + ...mapActions(useSettingStore, ["updateSettings"]), + submit(e: any) { + const formData = e.target.elements; + return this.updateSettings([ + { + key: "backup.copies", + value: formData.copies.value || null, + }, + { + key: "backup.interval", + value: formData.interval.value || null, + }, + ]); + }, + }, }); </script> diff --git a/src/components/admin/management/setting/BaseSetting.vue b/src/components/admin/management/setting/BaseSetting.vue new file mode 100644 index 0000000..96c1308 --- /dev/null +++ b/src/components/admin/management/setting/BaseSetting.vue @@ -0,0 +1,79 @@ +<template> + <form class="flex flex-col w-full" @submit.prevent="submit"> + <div class="flex flex-row gap-2 items-center border-l-3 border-l-primary p-2 rounded-t-lg bg-red-200"> + <p class="text-lg font-semibold grow">{{ title }}</p> + <Spinner v-if="status == 'loading'" /> + <SuccessCheckmark v-else-if="status == 'success'" /> + <FailureXMark v-else-if="status == 'failed'" /> + <div v-else-if="enableEdit" class="flex flex-row gap-2"> + <button type="submit" class="!w-fit !h-fit !p-0"> + <CheckIcon class="h-5 w-5 cursor-pointer" /> + </button> + <button type="reset" class="!w-fit !h-fit !p-0" @click="enableEdit = false"> + <XMarkIcon class="h-5 w-5 cursor-pointer" /> + </button> + </div> + <PencilSquareIcon + v-else-if="can('create', 'management', 'setting')" + class="h-5 w-5 cursor-pointer" + @click="enableEdit = true" + /> + </div> + <div class="border-l-3 border-l-primary p-2 rounded-b-lg"> + <slot :enableEdit="enableEdit"></slot> + </div> + </form> +</template> + +<script setup lang="ts"> +import FailureXMark from "@/components/FailureXMark.vue"; +import Spinner from "@/components/Spinner.vue"; +import SuccessCheckmark from "@/components/SuccessCheckmark.vue"; +import { useAbilityStore } from "@/stores/ability"; +import { CheckIcon, PencilSquareIcon, XMarkIcon } from "@heroicons/vue/24/outline"; +import { mapActions, mapState } from "pinia"; +import { defineComponent } from "vue"; +import type { PropType } from "vue"; +</script> + +<script lang="ts"> +export default defineComponent({ + props: { + title: { + type: String, + required: true, + }, + submitFunction: { + type: Function as PropType<(e: any) => Promise<any>>, + required: true, + }, + }, + data() { + return { + enableEdit: false as boolean, + status: undefined as undefined | "loading" | "success" | "failed", + }; + }, + computed: { + ...mapState(useAbilityStore, ["can"]), + }, + methods: { + submit(e: any) { + this.status = "loading"; + this.submitFunction(e) + .then(() => { + this.status = "success"; + }) + .catch(() => { + this.status = "failed"; + }) + .finally(() => { + setTimeout(() => { + this.enableEdit = false; + this.status = undefined; + }, 2000); + }); + }, + }, +}); +</script> diff --git a/src/components/admin/management/setting/ClubImageSetting.vue b/src/components/admin/management/setting/ClubImageSetting.vue new file mode 100644 index 0000000..e2ab129 --- /dev/null +++ b/src/components/admin/management/setting/ClubImageSetting.vue @@ -0,0 +1,90 @@ +<template> + <BaseSetting title="Vereins-Auftritt Einstellungen" :submit-function="submit" v-slot="{ enableEdit }"> + <div class="w-full"> + <p>Vereins-Icon</p> + <AppIcon v-if="clubSettings['club.icon'] != '' && !overwriteIcon" class="h-10! max-w-full mx-auto" /> + <img v-else-if="overwriteIcon" ref="icon_img" class="hidden w-full h-20 object-contain" /> + <div + v-else + class="flex h-10 w-full border-2 border-gray-300 rounded-md items-center justify-center text-sm cursor-pointer" + @click="($refs.icon as HTMLInputElement).click()" + > + Kein eigenes Icon ausgewählt + </div> + <input class="hidden!" type="file" ref="icon" accept="image/*" @change="previewImage('icon')" /> + </div> + <div class="w-full"> + <p>Vereins-Logo</p> + <AppLogo v-if="clubSettings['club.logo'] != '' && !overwriteLogo" class="h-10! max-w-full mx-auto" /> + <img v-else-if="overwriteLogo" ref="logo_img" class="hidden w-full h-20 object-contain" /> + <div + v-else + class="flex h-10 w-full border-2 border-gray-300 rounded-md items-center justify-center text-sm cursor-pointer" + @click="($refs.logo as HTMLInputElement).click()" + > + Kein eigenes Logo ausgewählt + </div> + <input class="hidden!" type="file" ref="logo" accept="image/*" @change="previewImage('logo')" /></div + ></BaseSetting> +</template> + +<script setup lang="ts"> +import { defineComponent } from "vue"; +import { mapActions, mapState } from "pinia"; +import { useSettingStore } from "@/stores/admin/management/setting"; +import AppIcon from "@/components/AppIcon.vue"; +import AppLogo from "@/components/AppLogo.vue"; +import { useAbilityStore } from "@/stores/ability"; +import type { SettingString } from "@/types/settingTypes"; +import BaseSetting from "./BaseSetting.vue"; +</script> + +<script lang="ts"> +export default defineComponent({ + data() { + return { + overwriteIcon: false as boolean, + overwriteLogo: false as boolean, + }; + }, + computed: { + ...mapState(useSettingStore, ["readByTopic"]), + ...mapState(useAbilityStore, ["can"]), + clubSettings() { + return this.readByTopic("club"); + }, + }, + methods: { + ...mapActions(useSettingStore, ["updateSettings", "uploadImage"]), + previewImage(inputname: "icon" | "logo") { + let input = this.$refs[inputname] as HTMLInputElement; + let previewElement = this.$refs[inputname + "_img"] as HTMLImageElement; + if (input.files && input.files[0]) { + const reader = new FileReader(); + + reader.onload = function (e) { + previewElement.src = e.target?.result as string; + previewElement.style.display = "block"; + }; + + reader.readAsDataURL(input.files[0]); + } else { + previewElement.src = ""; + previewElement.style.display = "none"; + } + }, + submit(e: any) { + return this.uploadImage([ + { + key: "club.icon" as SettingString, + value: (this.$refs.icon as HTMLInputElement).files?.[0], + }, + { + key: "club.logo" as SettingString, + value: (this.$refs.logo as HTMLInputElement).files?.[0], + }, + ]); + }, + }, +}); +</script> diff --git a/src/components/admin/management/setting/ClubSetting.vue b/src/components/admin/management/setting/ClubSetting.vue index 51dfb40..8b6cbcc 100644 --- a/src/components/admin/management/setting/ClubSetting.vue +++ b/src/components/admin/management/setting/ClubSetting.vue @@ -1,58 +1,89 @@ <template> - <div class="flex flex-col w-full"> - <div class="border-l-3 border-l-primary p-2 rounded-t-lg bg-red-200"> - <p class="text-lg font-semibold">Vereins Einstellungen</p> + <BaseSetting title="Vereins Einstellungen" :submit-function="submit" v-slot="{ enableEdit }"> + <div class="w-full"> + <label for="clubname">Vereins-Name (optional)</label> + <input id="clubname" type="text" :readonly="!enableEdit" :value="clubSettings['club.name']" /> </div> - <div class="border-l-3 border-l-primary p-2 rounded-b-lg"> - <div class="w-full"> - <p>Vereins-Icon</p> - <AppIcon v-if="clubSettings['club.icon'] != ''" class="h-10! max-w-full mx-auto" /> - <div v-else class="flex h-10 w-full border-2 border-gray-300 rounded-md items-center justify-center text-sm"> - Kein Icon hochgeladen - </div> - </div> - <div class="w-full"> - <p>Vereins-Logo</p> - <AppLogo v-if="clubSettings['club.logo'] != ''" class="h-10! max-w-full mx-auto" /> - <div v-else class="flex h-10 w-full border-2 border-gray-300 rounded-md items-center justify-center text-sm"> - Kein Logo hochgeladen - </div> - </div> - <div class="w-full"> - <label for="name">Vereins-Name</label> - <input id="name" type="text" readonly :value="clubSettings['club.name']" /> - </div> - <div class="w-full"> - <label for="imprint">Vereins-Impressum Link</label> - <input id="imprint" type="url" readonly :value="clubSettings['club.imprint']" /> - </div> - <div class="w-full"> - <label for="icon">Vereins-Datenschutz Link</label> - <input id="privacy" type="url" readonly :value="clubSettings['club.privacy']" /> - </div> - <div class="w-full"> - <label for="website">Vereins-Webseite Link</label> - <input id="website" type="url" readonly :value="clubSettings['club.website']" /> - </div> + <div class="w-full"> + <label for="imprint">Vereins-Impressum Link (optional)</label> + <input id="imprint" type="url" :readonly="!enableEdit" :value="clubSettings['club.imprint']" /> </div> - </div> + <div class="w-full"> + <label for="privacy">Vereins-Datenschutz Link (optional)</label> + <input id="privacy" type="url" :readonly="!enableEdit" :value="clubSettings['club.privacy']" /> + </div> + <div class="w-full"> + <label for="website">Vereins-Webseite Link (optional)</label> + <input id="website" type="url" :readonly="!enableEdit" :value="clubSettings['club.website']" /></div + ></BaseSetting> </template> <script setup lang="ts"> +import { defineComponent } from "vue"; +import { mapActions, mapState } from "pinia"; +import { useSettingStore } from "@/stores/admin/management/setting"; import AppIcon from "@/components/AppIcon.vue"; import AppLogo from "@/components/AppLogo.vue"; -import { useSettingStore } from "@/stores/admin/management/setting"; -import { mapState } from "pinia"; -import { defineComponent } from "vue"; +import { useAbilityStore } from "@/stores/ability"; +import type { SettingString } from "@/types/settingTypes"; +import BaseSetting from "./BaseSetting.vue"; </script> <script lang="ts"> export default defineComponent({ + data() { + return { + overwriteIcon: false as boolean, + overwriteLogo: false as boolean, + }; + }, computed: { ...mapState(useSettingStore, ["readByTopic"]), + ...mapState(useAbilityStore, ["can"]), clubSettings() { return this.readByTopic("club"); }, }, + methods: { + ...mapActions(useSettingStore, ["updateSettings", "uploadImage"]), + previewImage(inputname: "icon" | "logo") { + let input = this.$refs[inputname] as HTMLInputElement; + let previewElement = this.$refs[inputname + "_img"] as HTMLImageElement; + if (input.files && input.files[0]) { + const reader = new FileReader(); + + reader.onload = function (e) { + previewElement.src = e.target?.result as string; + previewElement.style.display = "block"; + }; + + reader.readAsDataURL(input.files[0]); + } else { + previewElement.src = ""; + previewElement.style.display = "none"; + } + }, + submit(e: any) { + const formData = e.target.elements; + return this.updateSettings([ + { + key: "club.name", + value: formData.clubname.value || null, + }, + { + key: "club.imprint", + value: formData.imprint.value || null, + }, + { + key: "club.privacy", + value: formData.privacy.value || null, + }, + { + key: "club.website", + value: formData.website.value || null, + }, + ]); + }, + }, }); </script> diff --git a/src/components/admin/management/setting/MailSetting.vue b/src/components/admin/management/setting/MailSetting.vue index b945b2a..64ca03d 100644 --- a/src/components/admin/management/setting/MailSetting.vue +++ b/src/components/admin/management/setting/MailSetting.vue @@ -1,57 +1,100 @@ <template> - <div class="flex flex-col w-full"> - <div class="border-l-3 border-l-primary p-2 rounded-t-lg bg-red-200"> - <p class="text-lg font-semibold">E-Mail Einstellungen</p> + <BaseSetting title="E-Mail Einstellungen" :submit-function="submit" v-slot="{ enableEdit }"> + <div class="w-full"> + <label for="email">Mailadresse</label> + <input id="email" type="email" autocomplete="email" :readonly="!enableEdit" :value="mailSettings['mail.email']" /> </div> - <div class="border-l-3 border-l-primary p-2 rounded-b-lg"> - <div class="w-full"> - <label for="name">Mailadresse</label> - <input id="name" type="text" readonly :value="mailSettings['mail.email']" /> - </div> - <div class="w-full"> - <label for="name">Benutzername</label> - <input id="name" type="text" readonly :value="mailSettings['mail.username']" /> - </div> - <div class="w-full"> - <label for="name">Server-Host</label> - <input id="name" type="text" readonly :value="mailSettings['mail.host']" /> - </div> - <div class="w-full"> - <label for="name">Server-Port</label> - <input id="name" type="text" readonly :value="mailSettings['mail.port']" /> - </div> - <div class="w-full flex flex-row items-center gap-2"> - <div - v-if="true" - class="border-2 border-gray-500 rounded-sm" - :class="mailSettings['mail.secure'] ? 'bg-gray-500' : 'h-3 w-3'" - > - <CheckIcon v-if="mailSettings['mail.secure']" class="h-2.5 w-2.5 stroke-4 text-white" /> - </div> - <input v-else id="name" type="checkbox" :checked="mailSettings['mail.secure']" /> - <label for="name">Secure-Verbindung</label> - </div> - <div class="w-full"> - <label for="name">Passwort</label> - <input id="name" type="password" readonly /> - </div> + <div class="w-full"> + <label for="username">Benutzername</label> + <input + id="username" + type="text" + :readonly="!enableEdit" + autocomplete="username" + :value="mailSettings['mail.username']" + /> </div> - </div> + <div class="w-full"> + <label for="host">Server-Host</label> + <input id="host" type="text" :readonly="!enableEdit" :value="mailSettings['mail.host']" /> + </div> + <div class="w-full"> + <label for="port">Server-Port (25, 465, 587)</label> + <input id="port" type="number" :readonly="!enableEdit" :value="mailSettings['mail.port']" /> + </div> + <div class="w-full flex flex-row items-center gap-2"> + <div + v-if="!enableEdit" + class="border-2 border-gray-500 rounded-sm" + :class="mailSettings['mail.secure'] ? 'bg-gray-500' : 'h-3.5 w-3.5'" + > + <CheckIcon v-if="mailSettings['mail.secure']" class="h-2.5 w-2.5 stroke-4 text-white" /> + </div> + <input v-else id="secure" type="checkbox" :checked="mailSettings['mail.secure']" /> + <label for="secure">Secure-Verbindung (setzen bei Port 465)</label> + </div> + <div class="w-full"> + <label for="password">Passwort (optional)</label> + <input id="password" type="password" :readonly="!enableEdit" autocomplete="new-password" /> + </div> + </BaseSetting> </template> <script setup lang="ts"> -import { useSettingStore } from "@/stores/admin/management/setting"; -import { mapState } from "pinia"; import { defineComponent } from "vue"; +import { CheckIcon } from "@heroicons/vue/24/outline"; +import { mapActions, mapState } from "pinia"; +import { useSettingStore } from "@/stores/admin/management/setting"; +import { useAbilityStore } from "@/stores/ability"; +import BaseSetting from "./BaseSetting.vue"; </script> <script lang="ts"> export default defineComponent({ + data() { + return { + enableEdit: false as boolean, + status: undefined as undefined | "loading" | "success" | "failed", + }; + }, computed: { ...mapState(useSettingStore, ["readByTopic"]), + ...mapState(useAbilityStore, ["can"]), mailSettings() { return this.readByTopic("mail"); }, }, + methods: { + ...mapActions(useSettingStore, ["updateSettings"]), + submit(e: any) { + const formData = e.target.elements; + return this.updateSettings([ + { + key: "mail.email", + value: formData.email.value, + }, + { + key: "mail.username", + value: formData.username.value, + }, + { + key: "mail.host", + value: formData.host.value, + }, + { + key: "mail.port", + value: formData.port.value, + }, + { + key: "mail.secure", + value: formData.secure.checked, + }, + { + key: "mail.password", + value: formData.password.value || null, + }, + ]); + }, + }, }); </script> diff --git a/src/components/admin/management/setting/SessionSetting.vue b/src/components/admin/management/setting/SessionSetting.vue index e93aee8..712ea75 100644 --- a/src/components/admin/management/setting/SessionSetting.vue +++ b/src/components/admin/management/setting/SessionSetting.vue @@ -1,38 +1,76 @@ <template> - <div class="flex flex-col w-full"> - <div class="border-l-3 border-l-primary p-2 rounded-t-lg bg-red-200"> - <p class="text-lg font-semibold">Login-Session Einstellungen</p> + <BaseSetting title="Login-Session Einstellungen" :submit-function="submit" v-slot="{ enableEdit }"> + <div class="w-full"> + <label for="jwt_expiration">JWT-Gültigkeitsdauer (optional)</label> + <input + id="jwt_expiration" + type="text" + :readonly="!enableEdit" + :value="sessionSettings['session.jwt_expiration']" + /> </div> - <div class="border-l-3 border-l-primary p-2 rounded-b-lg"> - <div class="w-full"> - <label for="name">JWT-Gültigkeitsdauer</label> - <input id="name" type="text" readonly :value="sessionSettings['session.jwt_expiration']" /> - </div> - <div class="w-full"> - <label for="name">Session-Gültigkeitsdauer</label> - <input id="name" type="text" readonly :value="sessionSettings['session.refresh_expiration']" /> - </div> - <div class="w-full"> - <label for="name">Sesion-Gültigkeitsdauer PWA</label> - <input id="name" type="text" readonly :value="sessionSettings['session.pwa_refresh_expiration']" /> - </div> + <div class="w-full"> + <label for="refresh_expiration">Session-Gültigkeitsdauer (optional)</label> + <input + id="refresh_expiration" + type="text" + :readonly="!enableEdit" + :value="sessionSettings['session.refresh_expiration']" + /> </div> - </div> + <div class="w-full"> + <label for="pwa_refresh_expiration">Sesion-Gültigkeitsdauer PWA (optional)</label> + <input + id="pwa_refresh_expiration" + type="text" + :readonly="!enableEdit" + :value="sessionSettings['session.pwa_refresh_expiration']" + /></div + ></BaseSetting> </template> <script setup lang="ts"> +import { useAbilityStore } from "@/stores/ability"; import { useSettingStore } from "@/stores/admin/management/setting"; -import { mapState } from "pinia"; +import { mapActions, mapState } from "pinia"; import { defineComponent } from "vue"; +import BaseSetting from "./BaseSetting.vue"; </script> <script lang="ts"> export default defineComponent({ + data() { + return { + enableEdit: false as boolean, + status: undefined as undefined | "loading" | "success" | "failed", + }; + }, computed: { ...mapState(useSettingStore, ["readByTopic"]), + ...mapState(useAbilityStore, ["can"]), sessionSettings() { return this.readByTopic("session"); }, }, + methods: { + ...mapActions(useSettingStore, ["updateSettings"]), + submit(e: any) { + const formData = e.target.elements; + return this.updateSettings([ + { + key: "session.jwt_expiration", + value: formData.jwt_expiration.value || null, + }, + { + key: "session.refresh_expiration", + value: formData.refresh_expiration.value || null, + }, + { + key: "session.pwa_refresh_expiration", + value: formData.pwa_refresh_expiration.value || null, + }, + ]); + }, + }, }); </script> diff --git a/src/stores/admin/management/setting.ts b/src/stores/admin/management/setting.ts index 174141a..defb78b 100644 --- a/src/stores/admin/management/setting.ts +++ b/src/stores/admin/management/setting.ts @@ -50,14 +50,32 @@ export const useSettingStore = defineStore("setting", { return res; }); }, + async uploadImage(data: { key: SettingString; value?: File }[]): Promise<AxiosResponse<any, any>> { + const formData = new FormData(); + for (let entry of data) { + if (entry.value) formData.append(entry.key, entry.value); + } + return await http.put("/admin/setting/img", formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }); + }, + async updateSettings<K extends SettingString>( + data: { key: K; value: SettingValueMapping[K] }[] + ): Promise<AxiosResponse<any, any>> { + return await http.put("/admin/setting", data); + }, async updateSetting<K extends SettingString>( key: K, - val: SettingValueMapping[K] + value: SettingValueMapping[K] ): Promise<AxiosResponse<any, any>> { - return await http.put("/admin/setting", { - setting: key, - value: val, - }); + return await http.put("/admin/setting", [ + { + setting: key, + value: value, + }, + ]); }, async resetSetting(key: SettingString): Promise<AxiosResponse<any, any>> { return await http.delete(`/admin/setting/${key}`); diff --git a/src/stores/configuration.ts b/src/stores/configuration.ts index c33803a..1e8bfd8 100644 --- a/src/stores/configuration.ts +++ b/src/stores/configuration.ts @@ -10,6 +10,8 @@ export const useConfigurationStore = defineStore("configuration", { clubWebsite: "", appCustom_login_message: "", appShow_link_to_calendar: false as boolean, + + serverOffline: false as boolean, }; }, actions: { @@ -24,7 +26,9 @@ export const useConfigurationStore = defineStore("configuration", { this.appCustom_login_message = res.data["app.custom_login_message"]; this.appShow_link_to_calendar = res.data["app.show_link_to_calendar"]; }) - .catch(() => {}); + .catch(() => { + this.serverOffline = true; + }); }, }, }); diff --git a/src/views/admin/management/setting/Setting.vue b/src/views/admin/management/setting/Setting.vue index 2220105..25e602a 100644 --- a/src/views/admin/management/setting/Setting.vue +++ b/src/views/admin/management/setting/Setting.vue @@ -6,6 +6,8 @@ </div> </template> <template #main> + <p>Hinweis: Optionale Felder können leer gelassen werden und nutzen dann einen Fallback-Werte.</p> + <ClubImageSetting /> <ClubSetting /> <AppSetting /> <MailSetting /> @@ -26,6 +28,7 @@ import AppSetting from "@/components/admin/management/setting/AppSetting.vue"; import MailSetting from "@/components/admin/management/setting/MailSetting.vue"; import SessionSetting from "@/components/admin/management/setting/SessionSetting.vue"; import BackupSetting from "@/components/admin/management/setting/BackupSetting.vue"; +import ClubImageSetting from "@/components/admin/management/setting/ClubImageSetting.vue"; </script> <script lang="ts">