Squashed commit of the following:
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 40s

commit 579f0878e5240dc17db69be1e0b0c0f5af7ef9fe
Author: Arno Kaimbacher <arno.kaimbacher@geosphere.at>
Date:   Tue Jun 9 09:25:44 2026 +0200

    feat: Refactor error handling in Dataset Edit form and improve validation messages

    - Updated error handling in the Dataset Edit form to use a centralized formatError function for displaying validation messages.
    - Enhanced user feedback by ensuring that error messages are displayed consistently across various fields.
    - Modified the validation rule for arrayContainsTypes to provide clearer error messages for missing main and translated titles/abstracts.
    - Introduced a new ValidationService to manage manual construction of validation errors.
    - Updated Vite configuration to streamline asset loading and improve performance.
    - Adjusted Inertia setup to utilize dynamic imports for page-specific assets.
    - Cleaned up unnecessary comments and code in various files for better readability.

commit 5efddc2a58c0e164fef585cc7344c06155dbc2c1
Author: Arno Kaimbacher <arno.kaimbacher@geosphere.at>
Date:   Mon Jan 12 17:02:47 2026 +0100

    feat: add dataset change detection and form submission composables

    - Implemented `useDatasetChangeDetection` for tracking unsaved changes in dataset forms, including comparisons for licenses, basic properties, files, coverage, and more.
    - Added `useDatasetFormSubmission` for handling dataset form submissions with validation, success/error handling, and auto-save functionality.
This commit is contained in:
Kaimbacher 2026-06-09 09:35:15 +02:00
commit 9368a0dd8d
38 changed files with 5588 additions and 6181 deletions

View file

@ -1,13 +1,22 @@
<script lang="ts" setup>
import { Head, usePage } from '@inertiajs/vue3';
import { mdiLicense, mdiCheckCircle, mdiCloseCircle, mdiAlertBoxOutline } from '@mdi/js';
import {
mdiLicense,
mdiCheckCircle,
mdiCloseCircle,
mdiAlertBoxOutline,
mdiFileDocumentOutline,
mdiCheckCircleOutline,
mdiPauseCircleOutline,
} from '@mdi/js';
import { computed, ComputedRef } from 'vue';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import SectionMain from '@/Components/SectionMain.vue';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
import BaseButton from '@/Components/BaseButton.vue';
import CardBox from '@/Components/CardBox.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import BaseIcon from '@/Components/BaseIcon.vue';
import CardBox from '@/Components/CardBox.vue';
import NotificationBar from '@/Components/NotificationBar.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
@ -32,6 +41,8 @@ const props = defineProps({
const flash: ComputedRef<any> = computed(() => usePage().props.flash);
const licenseCount = computed(() => props.licenses.length);
const activeCount = computed(() => props.licenses.filter((l) => l.active).length);
const inactiveCount = computed(() => licenseCount.value - activeCount.value);
const getLicenseColor = (index: number) => {
const colors = [
@ -51,18 +62,47 @@ const getLicenseColor = (index: number) => {
<Head title="Licenses" />
<SectionMain>
<SectionTitleLineWithButton :icon="mdiLicense" title="Licenses" main>
<div class="flex items-center gap-3">
<span class="text-sm text-gray-500 dark:text-gray-400 font-medium">
{{ licenseCount }} {{ licenseCount === 1 ? 'license' : 'licenses' }}
</span>
</div>
<span class="text-sm text-gray-500 dark:text-gray-400 font-medium">
{{ licenseCount }} {{ licenseCount === 1 ? 'license' : 'licenses' }}
</span>
</SectionTitleLineWithButton>
<NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
{{ flash.message }}
</NotificationBar>
<CardBox class="mb-6" has-table>
<!-- Summary stats -->
<div class="reveal grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
<div class="flex items-center gap-4 rounded-xl border-l-4 border-blue-500 bg-white dark:bg-slate-900/40 shadow-sm p-4">
<div class="flex items-center justify-center w-11 h-11 rounded-lg bg-blue-100 dark:bg-blue-900/40 text-blue-600 dark:text-blue-300">
<BaseIcon :path="mdiFileDocumentOutline" size="22" />
</div>
<div>
<p class="text-2xl font-bold text-gray-900 dark:text-white leading-none">{{ licenseCount }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Total</p>
</div>
</div>
<div class="flex items-center gap-4 rounded-xl border-l-4 border-emerald-500 bg-white dark:bg-slate-900/40 shadow-sm p-4">
<div class="flex items-center justify-center w-11 h-11 rounded-lg bg-emerald-100 dark:bg-emerald-900/40 text-emerald-600 dark:text-emerald-300">
<BaseIcon :path="mdiCheckCircleOutline" size="22" />
</div>
<div>
<p class="text-2xl font-bold text-gray-900 dark:text-white leading-none">{{ activeCount }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Active</p>
</div>
</div>
<div class="flex items-center gap-4 rounded-xl border-l-4 border-gray-400 bg-white dark:bg-slate-900/40 shadow-sm p-4">
<div class="flex items-center justify-center w-11 h-11 rounded-lg bg-gray-100 dark:bg-slate-700 text-gray-600 dark:text-gray-300">
<BaseIcon :path="mdiPauseCircleOutline" size="22" />
</div>
<div>
<p class="text-2xl font-bold text-gray-900 dark:text-white leading-none">{{ inactiveCount }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Inactive</p>
</div>
</div>
</div>
<CardBox class="reveal reveal-1 mb-6" has-table>
<table>
<thead>
<tr>
@ -75,9 +115,12 @@ const getLicenseColor = (index: number) => {
<tbody>
<tr v-if="licenses.length === 0">
<td colspan="4" class="text-center py-12">
<td :colspan="can.edit ? 4 : 3" class="text-center py-14">
<div class="flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
<p class="text-lg font-medium mb-2">No licenses found</p>
<div class="flex items-center justify-center w-16 h-16 rounded-full bg-gray-100 dark:bg-slate-800 mb-4">
<BaseIcon :path="mdiLicense" size="32" class="text-gray-400" />
</div>
<p class="text-lg font-medium mb-1">No licenses found</p>
<p class="text-sm">Licenses will appear here once configured</p>
</div>
</td>
@ -157,3 +200,29 @@ const getLicenseColor = (index: number) => {
</SectionMain>
</LayoutAuthenticated>
</template>
<style scoped>
@keyframes fade-up {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.reveal {
animation: fade-up 0.5s ease-out both;
}
.reveal-1 {
animation-delay: 0.1s;
}
@media (prefers-reduced-motion: reduce) {
.reveal {
animation: none;
}
}
</style>

View file

@ -1,6 +1,13 @@
<script lang="ts" setup>
import { Head, useForm } from '@inertiajs/vue3';
import { mdiFolderPlus, mdiArrowLeftBoldOutline, mdiFormTextarea, mdiContentSave } from '@mdi/js';
import {
mdiFolderPlus,
mdiArrowLeftBoldOutline,
mdiFormTextarea,
mdiContentSave,
mdiTagOutline,
mdiText,
} from '@mdi/js';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import SectionMain from '@/Components/SectionMain.vue';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
@ -37,46 +44,48 @@ const submit = async () => {
/>
</SectionTitleLineWithButton>
<CardBox form @submit.prevent="submit()" class="shadow-lg">
<div class="grid grid-cols-1 gap-6">
<FormField label="Label" help="Lowercase letters, numbers, and hyphens only" :class="{ 'text-red-400': form.errors.label }">
<CardBox form @submit.prevent="submit()" class="relative overflow-hidden shadow-lg">
<!-- Subtle accent bar -->
<div class="absolute inset-x-0 top-0 h-1 bg-gradient-to-r from-blue-500 via-indigo-500 to-blue-500"></div>
<div class="grid grid-cols-1 gap-7 pt-2">
<!-- Label -->
<FormField
label="Label"
help="Lowercase letters, numbers, and hyphens only"
:errors="form.errors.label"
>
<FormControl
v-model="form.label"
v-model="form.label"
:icon="mdiTagOutline"
type="text"
placeholder="e.g., my-awesome-project"
placeholder="e.g., my-awesome-project"
required
:error="form.errors.label"
class="transition-all focus:ring-2 focus:ring-blue-500"
>
<div class="text-red-400 text-sm mt-1" v-if="form.errors.label">
{{ form.errors.label }}
</div>
</FormControl>
class="font-mono transition-all duration-200 focus-within:ring-2 focus-within:ring-blue-500/40"
/>
</FormField>
<!-- Name -->
<FormField
label="Name"
help="Required. Project title shown to users"
:class="{ 'text-red-400': form.errors.name }"
:errors="form.errors.name"
>
<FormControl
v-model="form.name"
:icon="mdiText"
type="text"
placeholder="Enter a descriptive titel..."
placeholder="Enter a descriptive title..."
required
:error="form.errors.name"
class="font-mono transition-all focus:ring-2 focus:ring-blue-500"
>
<div class="text-red-400 text-sm mt-1" v-if="form.errors.name">
{{ form.errors.name }}
</div>
</FormControl>
class="transition-all duration-200 focus-within:ring-2 focus-within:ring-blue-500/40"
/>
</FormField>
<!-- Description -->
<FormField
label="Description"
help="Optional. Detailed description of the project"
:class="{ 'text-red-400': form.errors.description }"
:errors="form.errors.description"
>
<FormControl
v-model="form.description"
@ -84,19 +93,19 @@ const submit = async () => {
name="description"
type="textarea"
placeholder="Describe what this project is about..."
:error="form.errors.description"
class="transition-all focus:ring-2 focus:ring-blue-500"
>
<div class="text-red-400 text-sm mt-1" v-if="form.errors.description">
{{ form.errors.description }}
</div>
</FormControl>
class="transition-all duration-200 focus-within:ring-2 focus-within:ring-blue-500/40"
/>
</FormField>
</div>
<template #footer>
<BaseButtons class="justify-between">
<BaseButton :route-name="stardust.route('settings.project.index')" label="Cancel" color="white" outline />
<BaseButton
:route-name="stardust.route('settings.project.index')"
label="Cancel"
color="white"
outline
/>
<BaseButton
type="submit"
color="info"
@ -104,7 +113,7 @@ const submit = async () => {
label="Create Project"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
class="transition-all hover:shadow-lg"
class="transition-all hover:shadow-lg hover:-translate-y-0.5"
/>
</BaseButtons>
</template>
@ -116,15 +125,17 @@ const submit = async () => {
>
<div class="flex items-start gap-4">
<div class="flex-shrink-0">
<div class="w-10 h-10 rounded-full bg-blue-500 dark:bg-blue-600 flex items-center justify-center">
<div
class="w-10 h-10 rounded-full bg-blue-500 dark:bg-blue-600 flex items-center justify-center shadow-md shadow-blue-500/30"
>
<span class="text-white text-lg">💡</span>
</div>
</div>
<div class="flex-1">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">Quick Tips</h3>
<ul class="text-sm text-gray-700 dark:text-gray-300 space-y-1">
<li> <strong>Label</strong> is a technical identifier (use lowercase and hyphens) </li>
<li> <strong>Name</strong> is what users will see in the interface - short title</li>
<li> <strong>Label</strong> is a technical identifier (use lowercase and hyphens)</li>
<li> <strong>Name</strong> is what users will see in the interface short title</li>
<li> <strong>Description</strong> helps team members understand the project's purpose</li>
</ul>
</div>
@ -132,4 +143,4 @@ const submit = async () => {
</CardBox>
</SectionMain>
</LayoutAuthenticated>
</template>
</template>

View file

@ -1,6 +1,13 @@
<script lang="ts" setup>
import { Head, useForm } from '@inertiajs/vue3';
import { mdiFolderEdit, mdiArrowLeftBoldOutline, mdiFormTextarea, mdiContentSave } from '@mdi/js';
import {
mdiFolderEdit,
mdiArrowLeftBoldOutline,
mdiFormTextarea,
mdiContentSave,
mdiTagOutline,
mdiText,
} from '@mdi/js';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import SectionMain from '@/Components/SectionMain.vue';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
@ -43,45 +50,40 @@ const submit = async () => {
small
/>
</SectionTitleLineWithButton>
<CardBox form @submit.prevent="submit()" class="shadow-lg">
<div class="grid grid-cols-1 gap-6">
<FormField
label="Label"
help="Lowercase letters, numbers, and hyphens only"
>
<FormControl
v-model="form.label"
type="text"
help="Lowercase letters, numbers, and hyphens only"
:is-read-only=true
class="bg-gray-100 dark:bg-slate-800 cursor-not-allowed opacity-75"
<CardBox form @submit.prevent="submit()" class="relative overflow-hidden shadow-lg">
<!-- Subtle accent bar -->
<div class="absolute inset-x-0 top-0 h-1 bg-gradient-to-r from-blue-500 via-indigo-500 to-blue-500"></div>
<div class="grid grid-cols-1 gap-7 pt-2">
<!-- Label (read-only) -->
<FormField label="Label" help="Lowercase letters, numbers, and hyphens only">
<FormControl
v-model="form.label"
:icon="mdiTagOutline"
type="text"
:is-read-only="true"
class="font-mono bg-gray-100 dark:bg-slate-800 cursor-not-allowed opacity-75"
/>
</FormField>
<FormField
label="Name"
help="Required. Project title shown to users"
:class="{ 'text-red-400': form.errors.name }"
>
<FormControl
v-model="form.name"
type="text"
placeholder="Enter Name"
required
:error="form.errors.name"
class="font-mono transition-all focus:ring-2 focus:ring-blue-500"
>
<div class="text-red-400 text-sm mt-1" v-if="form.errors.name">
{{ form.errors.name }}
</div>
</FormControl>
<!-- Name -->
<FormField label="Name" help="Required. Project title shown to users" :errors="form.errors.name">
<FormControl
v-model="form.name"
:icon="mdiText"
type="text"
placeholder="Enter Name"
required
class="transition-all duration-200 focus-within:ring-2 focus-within:ring-blue-500/40"
/>
</FormField>
<!-- Description -->
<FormField
label="Description"
help="Optional. Detailed description of the project"
:class="{ 'text-red-400': form.errors.description }"
:errors="form.errors.description"
>
<FormControl
v-model="form.description"
@ -89,13 +91,8 @@ const submit = async () => {
name="description"
type="textarea"
placeholder="Enter project description..."
:error="form.errors.description"
class="transition-all focus:ring-2 focus:ring-blue-500"
>
<div class="text-red-400 text-sm mt-1" v-if="form.errors.description">
{{ form.errors.description }}
</div>
</FormControl>
class="transition-all duration-200 focus-within:ring-2 focus-within:ring-blue-500/40"
/>
</FormField>
</div>
@ -114,17 +111,19 @@ const submit = async () => {
label="Save Changes"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
class="transition-all hover:shadow-lg"
class="transition-all hover:shadow-lg hover:-translate-y-0.5"
/>
</BaseButtons>
</template>
</CardBox>
<!-- Project Info Card -->
<CardBox class="mt-6 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-slate-800 dark:to-slate-900">
<CardBox class="mt-6 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-slate-800 dark:to-slate-900 border-l-4 border-blue-500">
<div class="flex items-start gap-4">
<div class="flex-shrink-0">
<div class="w-12 h-12 rounded-lg bg-blue-500 dark:bg-blue-600 flex items-center justify-center">
<div
class="w-12 h-12 rounded-lg bg-blue-500 dark:bg-blue-600 flex items-center justify-center shadow-md shadow-blue-500/30"
>
<span class="text-white text-xl font-bold">
{{ project.label.charAt(0).toUpperCase() }}
</span>
@ -139,11 +138,11 @@ const submit = async () => {
</p>
<div class="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-500">
<span>
<span class="font-medium">Created:</span>
<span class="font-medium">Created:</span>
{{ new Date(project.created_at).toLocaleDateString() }}
</span>
<span>
<span class="font-medium">Updated:</span>
<span class="font-medium">Updated:</span>
{{ new Date(project.updated_at).toLocaleDateString() }}
</span>
</div>

View file

@ -27,9 +27,20 @@ const form = useForm({
permissions: [],
});
const submit = async () => {
await form.post(stardust.route('settings.role.store'));
const submit = () => {
form.post(stardust.route('settings.role.store'), {
preserveScroll: true,
onSuccess: () => form.reset(),
});
};
/**
* Sicherer Helper für die Fehleranzeige
*/
// const formatError = (error: string | string[] | undefined) => {
// if (!error) return '';
// return Array.isArray(error) ? error.join(', ') : error;
// };
</script>
<template>
@ -46,58 +57,48 @@ const submit = async () => {
small
/>
</SectionTitleLineWithButton>
<!-- <CardBox form @submit.prevent="form.post(stardust.route('role.store'))"> -->
<CardBox form @submit.prevent="submit()">
<FormField label="Name" help="Required. Role name" :class="{ 'text-red-400': form.errors.name }">
<FormControl v-model="form.name" type="text" placeholder="Enter Name" required :error="form.errors.name">
<div class="text-red-400 text-sm" v-if="form.errors.name">
{{ form.errors.name }}
</div>
<CardBox form @submit.prevent="submit">
<FormField label="Name" help="Required. Technical role name" :errors="form.errors.name">
<FormControl v-model="form.name" type="text" placeholder="e.g. manager" :error="form.errors.name"> </FormControl>
</FormField>
<FormField label="Display Name" help="Optional. Readable name" :errors="form.errors.display_name">
<FormControl v-model="form.display_name" placeholder="e.g. Project Manager" :error="form.errors.display_name">
</FormControl>
</FormField>
<FormField label="Display Name" help="Optional. Display name" :class="{ 'text-red-400': form.errors.display_name }">
<FormControl v-model="form.display_name" name="display_name" :error="form.errors.display_name">
<div class="text-red-400 text-sm" v-if="form.errors.display_name">
{{ form.errors.display_name }}
</div>
</FormControl>
</FormField>
<FormField
label="Description"
help="Optional. Description of new role"
:class="{ 'text-red-400': form.errors.description }"
>
<FormField label="Description" help="Optional. What does this role do?" :errors="form.errors.description">
<FormControl
v-model="form.description"
v-bind:icon="mdiFormTextarea"
name="display_name"
:type="'textarea'"
:icon="mdiFormTextarea"
type="textarea"
placeholder="Role description..."
:error="form.errors.description"
>
<div class="text-red-400 text-sm" v-if="form.errors.description">
{{ form.errors.description }}
</div>
</FormControl>
</FormField>
<BaseDivider />
<FormField label="Permissions" wrap-body>
<FormField
label="Permissions"
wrap-body
:class="{ 'text-red-400': form.errors.permissions }"
:errors="form.errors.permissions"
>
<FormCheckRadioGroup v-model="form.permissions" name="permissions" is-column :options="props.permissions" />
<!-- <div class="text-red-400 text-sm mt-1" v-if="form.errors.permissions">
{{ formatError(form.errors.permissions) }}
</div> -->
</FormField>
<div class="text-red-400 text-sm" v-if="form.errors.permissions && Array.isArray(form.errors.permissions)">
<!-- {{ errors.password_confirmation }} -->
{{ form.errors.permissions.join(', ') }}
</div>
<template #footer>
<BaseButtons>
<BaseButton
type="submit"
color="info"
label="Submit"
label="Create Role"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
/>

View file

@ -14,31 +14,45 @@ import BaseButtons from '@/Components/BaseButtons.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
const props = defineProps({
role: {
type: Object,
default: () => ({}),
},
permissions: {
type: Object,
default: () => ({}),
},
roleHasPermissions: {
type: Object,
default: () => ({}),
},
role: { type: Object, default: () => ({}) },
permissions: { type: Object, default: () => ({}) },
roleHasPermissions: { type: Object, default: () => ({}) },
});
const form = useForm({
_method: 'put',
name: props.role.name,
description: props.role.description,
permissions: props.roleHasPermissions,
display_name: props.role.display_name || '', // Neu hinzugefügt
description: props.role.description || '',
permissions: props.roleHasPermissions || [],
});
const submit = async () => {
// await Inertia.post(stardust.route('user.store'), form); old
await form.put(stardust.route('settings.role.update', [props.role.id]));
// await router.put(stardust.route('settings.role.update', [props.role.id]), form);
const submit = () => {
form.put(stardust.route('settings.role.update', [props.role.id]), {
preserveScroll: true,
});
};
/**
* Sicherer Helper für die Fehleranzeige
*/
// const formatError = (error: string | string[] | undefined) => {
// if (!error) return '';
// return Array.isArray(error) ? error.join(', ') : error;
// };
/**
* AUTOMATISCHER ERROR-CLEANER
* Löscht Fehlermeldungen sofort, wenn der User mit der Korrektur beginnt.
*/
// watch(() => ({ ...form.data() }), (newData, oldData) => {
// for (const key in newData) {
// // Vergleich via JSON.stringify für Arrays (permissions)
// if (JSON.stringify(newData[key]) !== JSON.stringify(oldData[key]) && form.errors[key]) {
// form.clearErrors(key as any);
// }
// }
// }, { deep: true });
</script>
<template>
@ -55,50 +69,38 @@ const submit = async () => {
small
/>
</SectionTitleLineWithButton>
<!-- <CardBox form @submit.prevent="form.put(stardust.route('role.update', [props.role.id]))"> -->
<CardBox form @submit.prevent="submit()">
<FormField label="Name" :class="{ 'text-red-400': form.errors.name }">
<FormControl v-model="form.name" type="text" placeholder="Enter Name" required :error="form.errors.name" :is-read-only=true>
<div class="text-red-400 text-sm" v-if="form.errors.name">
{{ form.errors.name }}
</div>
</FormControl>
<CardBox form @submit.prevent="submit">
<FormField label="System Name" help="Technical identifier, cannot be changed." :errors="form.errors.name">
<FormControl v-model="form.name" :error="form.errors.name" :is-read-only="true" />
</FormField>
<FormField
label="Description"
help="Optional. Description of new role"
:class="{ 'text-red-400': form.errors.description }"
>
<FormField label="Display Name" help="User-friendly name for this role." :errors="form.errors.display_name">
<FormControl v-model="form.display_name" placeholder="e.g. Administrator" :error="form.errors.display_name" />
</FormField>
<FormField label="Description" :errors="form.errors.description">
<FormControl
v-model="form.description"
v-bind:icon="mdiFormTextarea"
name="display_name"
:type="'textarea'"
:icon="mdiFormTextarea"
type="textarea"
placeholder="Role description..."
:error="form.errors.description"
>
<div class="text-red-400 text-sm" v-if="form.errors.description">
{{ form.errors.description }}
</div>
</FormControl>
/>
</FormField>
<BaseDivider />
<FormField label="Permissions" wrap-body>
<FormField label="Permissions" :errors="form.errors.permissions">
<FormCheckRadioGroup v-model="form.permissions" name="permissions" is-column :options="props.permissions" />
</FormField>
<div class="text-red-400 text-sm" v-if="form.errors.permissions && Array.isArray(form.errors.permissions)">
<!-- {{ errors.password_confirmation }} -->
{{ form.errors.permissions.join(', ') }}
</div>
<template #footer>
<BaseButtons>
<BaseButton
type="submit"
color="info"
label="Submit"
label="Update Role"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
/>

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Head, useForm, router } from '@inertiajs/vue3';
import { Head, useForm } from '@inertiajs/vue3';
import { mdiAccountKey, mdiArrowLeftBoldOutline } from '@mdi/js';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import SectionMain from '@/Components/SectionMain.vue';
@ -13,29 +13,27 @@ import BaseDivider from '@/Components/BaseDivider.vue';
import BaseButton from '@/Components/BaseButton.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
// import { Inertia } from '@inertiajs/inertia';
import PasswordMeter from '@/Components/SimplePasswordMeter/password-meter.vue';
const enabled = ref(false);
const handleScore = (score: number) => {
if (score >= 4){
enabled.value = true;
} else {
enabled.value = false;
}
};
const props = defineProps({
roles: {
type: Object,
default: () => ({}),
},
// Globale Errors als Fallback, falls nicht über useForm gearbeitet wird
errors: {
type: Object,
default: () => ({}),
},
});
const enabled = ref(false);
const handleScore = (score: number) => {
// Passwort muss stark genug sein (Score >= 4)
enabled.value = score >= 4;
};
const form = useForm({
login: '',
first_name: '',
@ -46,24 +44,30 @@ const form = useForm({
roles: [],
});
const submit = async () => {
// await router.post(stardust.route('settings.user.store'), form);
await form.post(stardust.route('settings.user.store'), {
const submit = () => {
// await router.post(stardust.route('settings.user.store'), form);
form.post(stardust.route('settings.user.store'), {
preserveScroll: true,
onSuccess: () => {
form.reset();
enabled.value = false;
},
onError: () => {
if (form.errors.new_password) {
form.reset('new_password');
form.reset('new_password', 'password_confirmation');
enabled.value = false;
// newPasswordInput.value.focus();
// newPasswordInput.value?.focus();
}
},
});
};
/**
* Helper um Fehler sicher anzuzeigen (String oder Array)
*/
const formatError = (error: string | string[] | undefined) => {
if (!error) return '';
return Array.isArray(error) ? error.join(', ') : error;
};
</script>
<template>
@ -80,78 +84,68 @@ const submit = async () => {
small
/>
</SectionTitleLineWithButton>
<!-- @submit.prevent="form.post(stardust.route('settings.user.store'))" -->
<CardBox form @submit.prevent="submit()">
<FormField label="Login" :class="{ 'text-red-400': errors.login }">
<FormControl v-model="form.login" type="text" placeholder="Enter Login" :errors="errors.login">
<div class="text-red-400 text-sm" v-if="errors.login && Array.isArray(errors.login)">
<!-- {{ errors.login }} -->
{{ errors.login.join(', ') }}
<CardBox form @submit.prevent="submit">
<FormField label="Login" :class="{ 'text-red-400': form.errors.login }">
<FormControl v-model="form.login" type="text" placeholder="Enter Login" :errors="form.errors.login">
<div class="text-red-400 text-sm mt-1" v-if="form.errors.login">
{{ formatError(form.errors.login) }}
</div>
</FormControl>
</FormField>
<FormField label="First Name" :class="{ 'text-red-400': errors.first_name }">
<FormControl v-model="form.first_name" type="text" placeholder="Enter First Name" :errors="errors.first_name">
<div class="text-red-400 text-sm" v-if="errors.first_name && Array.isArray(errors.first_name)">
{{ errors.first_name.join(', ') }}
<FormField label="First Name" :class="{ 'text-red-400': form.errors.first_name }">
<FormControl v-model="form.first_name" type="text" placeholder="Enter First Name" :errors="form.errors.first_name">
<div class="text-red-400 text-sm mt-1" v-if="form.errors.first_name">
{{ formatError(form.errors.first_name) }}
</div>
</FormControl>
</FormField>
<FormField label="Last Name" :class="{ 'text-red-400': errors.last_name }">
<FormControl v-model="form.last_name" type="text" placeholder="Enter Last Name" :errors="errors.last_name">
<div class="text-red-400 text-sm" v-if="errors.last_name && Array.isArray(errors.last_name)">
{{ errors.last_name.join(', ') }}
<FormField label="Last Name" :class="{ 'text-red-400': form.errors.last_name }">
<FormControl v-model="form.last_name" type="text" placeholder="Enter Last Name" :errors="form.errors.last_name">
<div class="text-red-400 text-sm mt-1" v-if="form.errors.last_name">
{{ formatError(form.errors.last_name) }}
</div>
</FormControl>
</FormField>
<FormField label="Email" :class="{ 'text-red-400': errors.email }">
<FormControl v-model="form.email" type="text" placeholder="Enter Email" :errors="errors.email">
<div class="text-red-400 text-sm" v-if="errors.email && Array.isArray(errors.email)">
<!-- {{ errors.email }} -->
{{ errors.email.join(', ') }}
<FormField label="Email" :class="{ 'text-red-400': form.errors.email }">
<FormControl v-model="form.email" type="text" placeholder="Enter Email" :errors="form.errors.email">
<div class="text-red-400 text-sm mt-1" v-if="form.errors.email">
{{ formatError(form.errors.email) }}
</div>
</FormControl>
</FormField>
<!-- <FormField label="Password" :class="{ 'text-red-400': errors.password }">
<FormControl v-model="form.password" type="password" placeholder="Enter Password" :errors="errors.password">
<div class="text-red-400 text-sm" v-if="errors.password && Array.isArray(errors.password)">
{{ errors.password.join(', ') }}
</div>
</FormControl>
</FormField>
<password-meter :password="form.password" @score="handleScore" /> -->
<PasswordMeter v-model="form.new_password" :errors="form.errors" @score="handleScore" />
<!-- <FormField label="Password" :class="{ 'text-red-400': form.errors.new_password }">
<div class="text-red-400 text-sm mt-1" v-if="form.errors.new_password">
{{ formatError(form.errors.new_password) }}
</div>
</FormField> -->
<PasswordMeter v-model="form.new_password" :errors="form.errors" @score="handleScore" />
<FormField label="Password Confirmation" :class="{ 'text-red-400': errors.password_confirmation }">
<FormField label="Password Confirmation" :class="{ 'text-red-400': form.errors.password_confirmation }">
<FormControl
v-model="form.password_confirmation"
type="password"
placeholder="Enter Password Confirmation"
:errors="errors.password"
placeholder="Confirm Password"
:errors="form.errors.password_confirmation"
>
<div
class="text-red-400 text-sm"
v-if="errors.password_confirmation && Array.isArray(errors.password_confirmation)"
>
<!-- {{ errors.password_confirmation }} -->
{{ errors.password_confirmation.join(', ') }}
<div class="text-red-400 text-sm mt-1" v-if="form.errors.password_confirmation">
{{ formatError(form.errors.password_confirmation) }}
</div>
</FormControl>
</FormField>
<BaseDivider />
<FormField label="Roles" wrap-body :class="{ 'text-red-400': errors.roles }">
<FormField label="Roles" wrap-body :class="{ 'text-red-400': form.errors.roles }">
<FormCheckRadioGroup v-model="form.roles" name="roles" is-column :options="props.roles" />
<div class="text-red-400 text-sm mt-1" v-if="form.errors.roles">
{{ formatError(form.errors.roles) }}
</div>
</FormField>
<div class="text-red-400 text-sm" v-if="errors.roles && Array.isArray(errors.roles)">
<!-- {{ errors.password_confirmation }} -->
{{ errors.roles.join(', ') }}
</div>
<template #footer>
<BaseButtons>
@ -159,12 +153,12 @@ const submit = async () => {
type="submit"
color="info"
label="Submit"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing == true || enabled == false"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing || !enabled"
/>
</BaseButtons>
</template>
</CardBox>
</SectionMain>
</LayoutAuthenticated>
</template>
</template>

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Head, useForm, router } from '@inertiajs/vue3';
import { Head, useForm } from '@inertiajs/vue3';
import { mdiAccountKey, mdiArrowLeftBoldOutline } from '@mdi/js';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import SectionMain from '@/Components/SectionMain.vue';
@ -13,29 +13,18 @@ import BaseDivider from '@/Components/BaseDivider.vue';
import BaseButton from '@/Components/BaseButton.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
// import { Inertia } from '@inertiajs/inertia';
import PasswordMeter from '@/Components/SimplePasswordMeter/password-meter.vue';
const enabled = ref(false);
const props = defineProps({
user: {
type: Object,
default: () => ({}),
},
roles: {
type: Object,
default: () => ({}),
},
userHasRoles: {
type: Object,
default: () => ({}),
},
errors: {
type: Object,
default: () => ({}),
},
user: { type: Object, default: () => ({}) },
roles: { type: Object, default: () => ({}) },
userHasRoles: { type: Object, default: () => ({}) },
errors: { type: Object, default: () => ({}) }, // Fallback
});
// enabled ist true, solange kein (schwaches) Passwort eingegeben wird
const enabled = ref(true);
const form = useForm({
_method: 'put',
login: props.user.login,
@ -47,30 +36,34 @@ const form = useForm({
roles: props.userHasRoles, // fill actual user roles from db
});
const submit = async () => {
// await Inertia.post(stardust.route('user.store'), form);
// await router.put(stardust.route('settings.user.update', [props.user.id]), form);
await form.put(stardust.route('settings.user.update', [props.user.id]), {
const submit = () => {
form.put(stardust.route('settings.user.update', [props.user.id]), {
preserveScroll: true,
onSuccess: () => {
form.reset();
// Bei Erfolg Passwortfelder leeren
form.reset('new_password', 'password_confirmation');
},
onError: () => {
if (form.errors.new_password) {
form.reset('new_password');
form.reset('new_password', 'password_confirmation');
enabled.value = false;
// newPasswordInput.value.focus();
// newPasswordInput.value?.focus();
}
},
});
};
const formatError = (error: string | string[] | undefined) => {
if (!error) return '';
return Array.isArray(error) ? error.join(', ') : error;
};
const handleScore = (score: number) => {
if (score >= 4){
// Wenn das Feld leer ist, ist der Status egal (Passwort wird nicht geändert)
// Wenn etwas drin steht, muss der Score >= 4 sein
if (form.new_password === '') {
enabled.value = true;
} else {
enabled.value = false;
enabled.value = score >= 4;
}
};
</script>
@ -89,89 +82,87 @@ const handleScore = (score: number) => {
small
/>
</SectionTitleLineWithButton>
<!-- <CardBox form @submit.prevent="form.put(stardust.route('user.update', [props.user.id]))"> -->
<CardBox form @submit.prevent="submit()">
<FormField label="Enter Login" :class="{ 'text-red-400': errors.name }">
<FormControl v-model="form.login" type="text" placeholder="Name" :errors="errors.login">
<div class="text-red-400 text-sm" v-if="errors.login">
{{ errors.login }}
<CardBox form @submit.prevent="submit">
<FormField label="Login" :class="{ 'text-red-400': form.errors.login }">
<FormControl v-model="form.login" type="text" placeholder="Login" :errors="form.errors.login">
<div class="text-red-400 text-sm mt-1" v-if="form.errors.login">
{{ formatError(form.errors.login) }}
</div>
</FormControl>
</FormField>
<FormField label="First Name" :class="{ 'text-red-400': errors.first_name }">
<FormControl v-model="form.first_name" type="text" placeholder="Enter First Name" :errors="errors.first_name">
<div class="text-red-400 text-sm" v-if="errors.first_name && Array.isArray(errors.first_name)">
{{ errors.first_name.join(', ') }}
<FormField label="First Name" :class="{ 'text-red-400': form.errors.first_name }">
<FormControl v-model="form.first_name" type="text" placeholder="First Name" :errors="form.errors.first_name">
<div class="text-red-400 text-sm mt-1" v-if="form.errors.first_name">
{{ formatError(form.errors.first_name) }}
</div>
</FormControl>
</FormField>
<FormField label="Last Name" :class="{ 'text-red-400': errors.last_name }">
<FormControl v-model="form.last_name" type="text" placeholder="Enter Last Name" :errors="errors.last_name">
<div class="text-red-400 text-sm" v-if="errors.last_name && Array.isArray(errors.last_name)">
{{ errors.last_name.join(', ') }}
<FormField label="Last Name" :class="{ 'text-red-400': form.errors.last_name }">
<FormControl v-model="form.last_name" type="text" placeholder="Last Name" :errors="form.errors.last_name">
<div class="text-red-400 text-sm mt-1" v-if="form.errors.last_name">
{{ formatError(form.errors.last_name) }}
</div>
</FormControl>
</FormField>
<FormField label="Enter Email" :class="{ 'text-red-400': errors.email }">
<FormControl v-model="form.email" type="text" placeholder="Email" :errors="errors.email">
<div class="text-red-400 text-sm" v-if="errors.email && Array.isArray(errors.email)">
{{ errors.email.join(', ') }}
<FormField label="Email" :class="{ 'text-red-400': form.errors.email }">
<FormControl v-model="form.email" type="text" placeholder="Email" :errors="form.errors.email">
<div class="text-red-400 text-sm mt-1" v-if="form.errors.email">
{{ formatError(form.errors.email) }}
</div>
</FormControl>
</FormField>
<!-- <FormField label="Password" :class="{ 'text-red-400': errors.password }">
<FormControl v-model="form.password" type="password" placeholder="Enter Password" :errors="errors.password">
<div class="text-red-400 text-sm" v-if="errors.password">
{{ errors.password }}
</div>
</FormControl>
</FormField> -->
<div class="py-4">
<PasswordMeter
field-label="Reset User Password (leave blank to keep current)"
:show-required-message="false"
v-model="form.new_password"
:errors="form.errors"
@score="handleScore"
/>
<!-- <div class="text-red-400 text-sm mt-1" v-if="form.errors.new_password">
{{ formatError(form.errors.new_password) }}
</div> -->
</div>
<PasswordMeter field-label="Reset User Password" :show-required-message="false" ref="newPasswordInput" v-model="form.new_password" :errors="form.errors" @score="handleScore" />
<FormField label="Password Confirmation" :class="{ 'text-red-400': errors.password_confirmation }">
<FormField label="Password Confirmation" :class="{ 'text-red-400': form.errors.password_confirmation }">
<FormControl
v-model="form.password_confirmation"
type="password"
placeholder="Enter Password Confirmation"
:errors="errors.password"
placeholder="Confirm New Password"
:errors="form.errors.password_confirmation"
>
<div
class="text-red-400 text-sm"
v-if="errors.password_confirmation && Array.isArray(errors.password_confirmation)"
>
{{ errors.password_confirmation.join(', ') }}
<div class="text-red-400 text-sm mt-1" v-if="form.errors.password_confirmation">
{{ formatError(form.errors.password_confirmation) }}
</div>
</FormControl>
</FormField>
<BaseDivider />
<FormField label="Roles" wrap-body :class="{ 'text-red-400': errors.roles }">
<FormField label="Roles" wrap-body :class="{ 'text-red-400': form.errors.roles }">
<FormCheckRadioGroup v-model="form.roles" name="roles" is-column :options="props.roles" />
<div class="text-red-400 text-sm mt-1" v-if="form.errors.roles">
{{ formatError(form.errors.roles) }}
</div>
</FormField>
<div class="text-red-400 text-sm" v-if="errors.roles && Array.isArray(errors.roles)">
<!-- {{ errors.password_confirmation }} -->
{{ errors.roles.join(', ') }}
</div>
<template #footer>
<BaseButtons>
<BaseButton
type="submit"
color="info"
label="Submit"
label="Update User"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing == true|| (form.new_password != '' && enabled == false)"
:disabled="form.processing || !enabled"
/>
</BaseButtons>
</template>
</CardBox>
</SectionMain>
</LayoutAuthenticated>
</template>
</template>