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,15 +1,11 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
// import { MainService } from '@/Stores/main';
import { StyleService } from '@/Stores/style.service';
import { mdiTrashCan } from '@mdi/js';
// import CardBoxModal from '@/Components/CardBoxModal.vue';
// import TableCheckboxCell from '@/Components/TableCheckboxCell.vue';
import BaseLevel from '@/Components/BaseLevel.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import BaseButton from '@/Components/BaseButton.vue';
import { Subject } from '@/Dataset';
// import FormField from '@/Components/FormField.vue';
import FormControl from '@/Components/FormControl.vue';
import SearchCategoryAutocomplete from '@/Components/SearchCategoryAutocomplete.vue';
import { mdiRefresh } from '@mdi/js';
@ -47,14 +43,10 @@ const deletetSubjects = computed({
});
const styleService = StyleService();
// const mainService = MainService();
const items = computed(() => props.keywords);
// const isModalActive = ref(false);
// const isModalDangerActive = ref(false);
const perPage = ref(5);
const currentPage = ref(0);
// const checkedRows = ref([]);
const itemsPaginated = computed(() => {
return items.value.slice(perPage.value * currentPage.value, perPage.value * (currentPage.value + 1));
@ -75,7 +67,6 @@ const pagesList = computed(() => {
});
const removeItem = (key: number) => {
// items.value.splice(key, 1);
const item = items.value[key];
// If the item has an ID, add it to the delete list
@ -95,7 +86,6 @@ const addToDeleteList = (subject: Subject) => {
}
};
// Helper function to reactivate a subject (remove from delete list)
const reactivateSubject = (index: number) => {
const newList = [...props.subjectsToDelete];
@ -111,22 +101,18 @@ const reactivateSubject = (index: number) => {
const isKeywordReadOnly = (item: Subject) => {
return (item.dataset_count ?? 0) > 1 || item.type !== 'uncontrolled';
};
const formatError = (error: string | string[] | undefined | null): string => {
if (!error) return '';
return Array.isArray(error) ? error.join(', ') : error;
};
</script>
<template>
<!-- <div v-if="checkedRows.length" class="p-3 bg-gray-100/50 dark:bg-slate-800">
<span v-for="checkedRow in checkedRows" :key="checkedRow.id"
class="inline-block px-2 py-1 rounded-sm mr-2 text-sm bg-gray-100 dark:bg-slate-700">
{{ checkedRow.name }}
</span>
</div> -->
<table>
<thead>
<tr>
<!-- <th v-if="checkable" /> -->
<!-- <th class="hidden lg:table-cell"></th> -->
<th scope="col">Type</th>
<th scope="col" class="relative">
Value
@ -150,14 +136,14 @@ const isKeywordReadOnly = (item: Subject) => {
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in itemsPaginated" :key="index">
<tr v-for="(item, index) in itemsPaginated" :key="item.id ?? index">
<td data-label="Type" scope="row">
<FormControl required v-model="item.type"
@update:modelValue="() => { item.value = ''; }" :type="'select'"
placeholder="[Enter Language]" :options="props.subjectTypes">
<div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.type`]">
{{ errors[`subjects.${index}.type`].join(', ') }}
{{ formatError(errors[`subjects.${index}.type`]) }}
</div>
</FormControl>
</td>
@ -170,14 +156,14 @@ const isKeywordReadOnly = (item: Subject) => {
}
" :is-read-only="item.dataset_count > 1">
<div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.value`]">
{{ errors[`subjects.${index}.value`].join(', ') }}
{{ formatError(errors[`subjects.${index}.value`]) }}
</div>
</SearchCategoryAutocomplete>
<FormControl v-else required v-model="item.value" type="text" placeholder="[enter keyword value]"
:borderless="true" :is-read-only="item.dataset_count > 1">
<div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.value`]">
{{ errors[`subjects.${index}.value`].join(', ') }}
{{ formatError(errors[`subjects.${index}.value`]) }}
</div>
</FormControl>
</td>
@ -186,7 +172,7 @@ const isKeywordReadOnly = (item: Subject) => {
<FormControl required v-model="item.language" :type="'select'" placeholder="[Enter Lang]"
:options="{ de: 'de', en: 'en' }" :is-read-only="isKeywordReadOnly(item)">
<div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.language`]">
{{ errors[`subjects.${index}.language`].join(', ') }}
{{ formatError(errors[`subjects.${index}.language`]) }}
</div>
</FormControl>
</td>
@ -199,7 +185,6 @@ const isKeywordReadOnly = (item: Subject) => {
<td class="before:hidden lg:w-1 whitespace-nowrap" scope="row">
<BaseButtons type="justify-start lg:justify-end" no-wrap>
<!-- <BaseButton color="info" :icon="mdiEye" small @click="isModalActive = true" /> -->
<BaseButton color="danger" :icon="mdiTrashCan" small @click.prevent="removeItem(index)" />
</BaseButtons>
</td>
@ -207,7 +192,6 @@ const isKeywordReadOnly = (item: Subject) => {
</tbody>
</table>
<!-- :class="[ pagesList.length > 1 ? 'block' : 'hidden']" -->
<div class="p-3 lg:px-6 border-t border-gray-100 dark:border-slate-800">
<BaseLevel>
<BaseButtons>
@ -218,8 +202,9 @@ const isKeywordReadOnly = (item: Subject) => {
</BaseLevel>
</div>
<div class="text-red-400 text-sm" v-if="errors.subjects && Array.isArray(errors.subjects)">
{{ errors.subjects.join(', ') }}
<!-- Aggregate error for the whole subjects collection, e.g. "at least 3 keywords must be defined" -->
<div class="text-red-400 text-sm" v-if="errors.subjects">
{{ formatError(errors.subjects) }}
</div>
<!-- Subjects to delete section -->
@ -261,7 +246,7 @@ const isKeywordReadOnly = (item: Subject) => {
</ul>
</div>
</template>
<style scoped>
@ -269,7 +254,7 @@ const isKeywordReadOnly = (item: Subject) => {
background: gray;
}
tr:nth-child(od) {
tr:nth-child(od) {
background: white;
} */
</style>