hotfix: enhance editor dataset management and UI improvements

- Implemented dataset editing functionality for editor roles, including fetching, updating, and categorizing datasets.
- Added routes and controller actions for editing, updating, and categorizing datasets within the editor interface.
- Integrated UI components for managing dataset metadata, subjects, references, and files.
- Enhanced keyword management with features for adding, editing, and deleting keywords, including handling keywords used by multiple datasets.
- Improved reference management with features for adding, editing, and deleting dataset references.
- Added validation for dataset updates using the `updateEditorDatasetValidator`.
- Updated the dataset edit form to include components for managing titles, descriptions, authors, contributors, licenses, coverage, subjects, references, and files.
- Implemented transaction management for dataset updates to ensure data consistency.
- Added a download route for files associated with datasets.
- Improved the UI for displaying and interacting with datasets in the editor index view, including adding edit and categorize buttons.
- Fixed an issue where the file size was not correctly calculated.
- Added a tooltip to the keyword value column in the TableKeywords component to explain the editability of keywords.
- Added a section to display keywords that are marked for deletion.
- Added a section to display references that are marked for deletion.
- Added a restore button to the references to delete section to restore references.
- Updated the SearchCategoryAutocomplete component to support read-only mode.
- Updated the FormControl component to support read-only mode.
- Added icons and styling improvements to various components.
- Added a default value for subjectsToDelete and referencesToDelete in the dataset model.
- Updated the FooterBar component to use the JustboilLogo component.
- Updated the app.ts file to fetch chart data without a year parameter.
- Updated the Login.vue file to invert the logo in dark mode.
- Updated the AccountInfo.vue file to add a Head component.
This commit is contained in:
Kaimbacher 2025-04-08 14:16:35 +02:00
parent 10d159a57a
commit f04c1f6327
30 changed files with 2284 additions and 539 deletions

View file

@ -15,9 +15,10 @@ const year = computed(() => new Date().getFullYear());
<!-- Get more with <a href="https://tailwind-vue.justboil.me/" target="_blank" class="text-blue-600">Premium
version</a> -->
</div>
<div class="md:py-3">
<div class="md:py-1">
<a href="https://www.tethys.at" target="_blank">
<JustboilLogo class="w-auto h-8 md:h-6" />
<!-- <JustboilLogo class="w-auto h-8 md:h-6" /> -->
<JustboilLogo class="w-auto h-12 md:h-10 dark:invert" />
</a>
</div>
</BaseLevel>

File diff suppressed because one or more lines are too long

View file

@ -169,13 +169,10 @@ const showAbout = async () => {
</NavBarItem>
<NavBarItem v-if="userHasRoles(['reviewer'])" :route-name="'reviewer.dataset.list'">
<NavBarItemLabel :icon="mdiGlasses" label="Reviewer Menu" />
</NavBarItem>
<!-- <NavBarItem>
<NavBarItemLabel :icon="mdiEmail" label="Messages" />
</NavBarItem> -->
<NavBarItem @click="showAbout">
</NavBarItem>
<!-- <NavBarItem @click="showAbout">
<NavBarItemLabel :icon="mdiInformationVariant" label="About" />
</NavBarItem>
</NavBarItem> -->
<BaseDivider nav-bar />
<NavBarItem @click="logout">
<NavBarItemLabel :icon="mdiLogout" label="Log Out" />

View file

@ -5,7 +5,7 @@
<div class="relative" data-te-dropdown-ref>
<button id="states-button" data-dropdown-toggle="dropdown-states"
class="whitespace-nowrap h-12 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center text-gray-500 bg-gray-100 border border-gray-300 rounded-l-lg hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 dark:focus:ring-gray-700 dark:text-white dark:border-gray-600"
type="button" @click.prevent="showStates">
type="button" :disabled="isReadOnly" @click.prevent="showStates">
<!-- <svg aria-hidden="true" class="h-3 mr-2" viewBox="0 0 15 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" width="14" height="12" rx="2" fill="white" />
<mask id="mask0_12694_49953" style="mask-type: alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="15" height="12">
@ -65,7 +65,7 @@
</svg> -->
<!-- eng -->
{{ language }}
<svg aria-hidden="true" class="w-4 h-4 ml-1" fill="currentColor" viewBox="0 0 20 20"
<svg aria-hidden="true" class="w-4 h-4 ml-1" fill="currentColor" viewBox="0 0 20 20" v-if="!isReadOnly"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
@ -93,7 +93,7 @@
<!-- :class="inputElClass" -->
<!-- class="block p-2.5 w-full z-20 text-sm text-gray-900 bg-gray-50 rounded-r-lg border-l-gray-50 border-l-2 border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-l-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500" -->
<input v-model="computedValue" type="text" :name="props.name" autocomplete="off" :class="inputElClass"
placeholder="Search Keywords..." required @input="handleInput" />
placeholder="Search Keywords..." required @input="handleInput" :readonly="isReadOnly" />
<!-- v-model="data.search" -->
<svg class="w-4 h-4 absolute left-2.5 top-3.5" v-show="computedValue.length < 2"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@ -101,12 +101,12 @@
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<svg class="w-4 h-4 absolute left-2.5 top-3.5" v-show="computedValue.length >= 2"
<svg class="w-4 h-4 absolute left-2.5 top-3.5" v-show="computedValue.length >= 2 && !isReadOnly"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" @click="() => {
computedValue = '';
data.isOpen = false;
}
">
">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
@ -166,6 +166,10 @@ let props = defineProps({
type: String,
default: '',
},
isReadOnly: {
type: Boolean,
default: false,
},
required: Boolean,
borderless: Boolean,
transparent: Boolean,
@ -190,11 +194,18 @@ const inputElClass = computed(() => {
'dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500',
'h-12',
props.borderless ? 'border-0' : 'border',
props.transparent ? 'bg-transparent' : 'bg-white dark:bg-slate-800',
props.transparent && 'bg-transparent',
// props.isReadOnly ? 'bg-gray-50 dark:bg-slate-600' : 'bg-white dark:bg-slate-800',
];
// if (props.icon) {
base.push('pl-10');
if (props.isReadOnly) {
// Read-only: no focus ring, grayed-out text and border, and disabled cursor.
base.push('bg-gray-50', 'dark:bg-slate-600', 'border', 'border-gray-300', 'dark:border-slate-600', 'text-gray-500', 'cursor-not-allowed', 'focus:outline-none', 'focus:ring-0', 'focus:border-gray-300');
} else {
// Actionable field: focus ring, white/dark background, and darker border.
base.push('bg-white dark:bg-slate-800', 'focus:ring focus:outline-none', 'border', 'border-gray-700');
}
// }
return base;
});

View file

@ -12,6 +12,7 @@ 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';
const props = defineProps({
checkable: Boolean,
@ -27,6 +28,22 @@ const props = defineProps({
type: Object,
default: () => ({}),
},
subjectsToDelete: {
type: Array<Subject>,
default: [],
}
});
const emit = defineEmits(['update:subjectsToDelete']);
// Create a computed property for subjectsToDelete with getter and setter
const deletetSubjects = computed({
get: () => props.subjectsToDelete,
set: (values: Array<Subject>) => {
props.subjectsToDelete.length = 0;
props.subjectsToDelete.push(...values);
emit('update:subjectsToDelete', values);
}
});
const styleService = StyleService();
@ -58,21 +75,45 @@ 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
if (item.id) {
addToDeleteList(item);
}
// Remove from the visible list
items.value.splice(key, 1);
};
// Helper function to add a subject to the delete list
const addToDeleteList = (subject: Subject) => {
if (subject.id) {
const newList = [...props.subjectsToDelete, subject];
deletetSubjects.value = newList;
}
};
// Helper function to reactivate a subject (remove from delete list)
const reactivateSubject = (index: number) => {
const newList = [...props.subjectsToDelete];
const removedSubject = newList.splice(index, 1)[0];
deletetSubjects.value = newList;
// Add the subject back to the keywords list if it's not already there
if (removedSubject && !props.keywords.some(k => k.id === removedSubject.id)) {
props.keywords.push(removedSubject);
}
};
const isKeywordReadOnly = (item: Subject) => {
return (item.dataset_count ?? 0) > 1 || item.type !== 'uncontrolled';
};
</script>
<template>
<!-- <CardBoxModal v-model="isModalActive" title="Sample modal">
<p>Lorem ipsum dolor sit amet <b>adipiscing elit</b></p>
<p>This is sample modal</p>
</CardBoxModal>
<CardBoxModal v-model="isModalDangerActive" large-title="Please confirm" button="danger" has-cancel>
<p>Lorem ipsum dolor sit amet <b>adipiscing elit</b></p>
<p>This is sample modal</p>
</CardBoxModal> -->
<!-- <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"
@ -87,17 +128,34 @@ const removeItem = (key: number) => {
<!-- <th v-if="checkable" /> -->
<!-- <th class="hidden lg:table-cell"></th> -->
<th scope="col">Type</th>
<th scope="col">Value</th>
<th scope="col" class="relative">
Value
<div class="inline-block relative ml-1 group">
<button
class="w-4 h-4 rounded-full bg-gray-200 text-gray-600 text-xs flex items-center justify-center focus:outline-none hover:bg-gray-300">
i
</button>
<div
class="absolute left-0 top-full mt-1 w-64 bg-white shadow-lg rounded-md p-3 text-xs text-left z-50 transform scale-0 origin-top-left transition-transform duration-100 group-hover:scale-100">
<p class="text-gray-700">
Keywords are only editable if they are used by a single dataset (Usage Count = 1)".
</p>
<div class="absolute -top-1 left-1 w-2 h-2 bg-white transform rotate-45"></div>
</div>
</div>
</th>
<th scope="col">Language</th>
<th scope="col">Usage Count</th>
<th scope="col" />
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in itemsPaginated" :key="index">
<td data-label="Type" scope="row">
<FormControl required v-model="item.type" @update:modelValue="() => {item.external_key = undefined; item.value= '';}" :type="'select'" placeholder="[Enter Language]" :options="props.subjectTypes">
<FormControl required v-model="item.type"
@update:modelValue="() => { item.external_key = undefined; 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(', ') }}
</div>
@ -105,22 +163,19 @@ const removeItem = (key: number) => {
</td>
<td data-label="Value" scope="row">
<SearchCategoryAutocomplete
v-if="item.type !== 'uncontrolled'"
v-model="item.value"
@subject="
(result) => {
item.language = result.language;
item.external_key = result.uri;
}
"
>
<SearchCategoryAutocomplete v-if="item.type !== 'uncontrolled'" v-model="item.value" @subject="
(result) => {
item.language = result.language;
item.external_key = result.uri;
}
" :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(', ') }}
</div>
</SearchCategoryAutocomplete>
<FormControl v-else required v-model="item.value" type="text" placeholder="[enter keyword value]" :borderless="true">
<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(', ') }}
</div>
@ -128,23 +183,24 @@ const removeItem = (key: number) => {
</td>
<td data-label="Language" scope="row">
<FormControl
required
v-model="item.language"
:type="'select'"
placeholder="[Enter Lang]"
:options="{ de: 'de', en: 'en' }"
:is-read-only="item.type != 'uncontrolled'"
>
<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(', ') }}
</div>
</FormControl>
</td>
<td data-label="Usage Count" scope="row">
<div class="text-center">
{{ item.dataset_count || 1 }}
</div>
</td>
<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 v-if="index > 2" color="danger" :icon="mdiTrashCan" small @click.prevent="removeItem(index)" />
<BaseButton color="danger" :icon="mdiTrashCan" small @click.prevent="removeItem(index)" />
</BaseButtons>
</td>
</tr>
@ -155,15 +211,8 @@ const removeItem = (key: number) => {
<div class="p-3 lg:px-6 border-t border-gray-100 dark:border-slate-800">
<BaseLevel>
<BaseButtons>
<BaseButton
v-for="page in pagesList"
:key="page"
:active="page === currentPage"
:label="page + 1"
small
:outline="styleService.darkMode"
@click="currentPage = page"
/>
<BaseButton v-for="page in pagesList" :key="page" :active="page === currentPage" :label="page + 1" small
:outline="styleService.darkMode" @click="currentPage = page" />
</BaseButtons>
<small>Page {{ currentPageHuman }} of {{ numPages }}</small>
</BaseLevel>
@ -172,6 +221,47 @@ const removeItem = (key: number) => {
<div class="text-red-400 text-sm" v-if="errors.subjects && Array.isArray(errors.subjects)">
{{ errors.subjects.join(', ') }}
</div>
<!-- Subjects to delete section -->
<div v-if="deletetSubjects.length > 0" class="mt-8">
<h1 class="pt-8 pb-3 font-semibold sm:text-lg text-gray-900">Keywords To Delete</h1>
<ul id="deleteSubjects" tag="ul" class="flex flex-1 flex-wrap -m-1">
<li v-for="(element, index) in deletetSubjects" :key="index"
class="block p-1 w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/6 xl:w-1/8 h-32">
<article tabindex="0"
class="bg-red-100 group w-full h-full rounded-md cursor-pointer relative shadow-sm overflow-hidden">
<section
class="flex flex-col rounded-md text-xs break-words w-full h-full z-20 absolute top-0 py-2 px-3">
<h1 class="flex-1 text-gray-700 group-hover:text-blue-800 font-medium text-sm mb-1">{{
element.value }}</h1>
<div class="flex items-center justify-between mt-auto">
<div class="flex flex-col">
<p class="p-1 size text-xs text-gray-700">
<span class="font-semibold">Type:</span> {{ element.type }}
</p>
<p class="p-1 size text-xs text-gray-700" v-if="element.dataset_count">
<span class="font-semibold">Used by:</span>
<span
class="inline-flex items-center justify-center bg-gray-200 text-gray-800 rounded-full w-5 h-5 text-xs">
{{ element.dataset_count }}
</span> datasets
</p>
</div>
<button
class="delete ml-auto focus:outline-none hover:bg-gray-300 p-1 rounded-md text-gray-800"
@click.prevent="reactivateSubject(index)">
<svg viewBox="0 0 24 24" class="w-5 h-5">
<path fill="currentColor" :d="mdiRefresh"></path>
</svg>
</button>
</div>
</section>
</article>
</li>
</ul>
</div>
</template>
<style scoped>