feat: Enhance Dataset Index with Dynamic Legend and Improved State Management for submitter, editor and reviewer
Some checks failed
build.yaml / feat: Enhance Dataset Index with Dynamic Legend and Improved State Management for submitter, editor and reviewer (push) Failing after 0s

- Added a collapsible legend to display dataset states and available actions.
- Implemented localStorage persistence for legend visibility.
- Refactored dataset state handling with dynamic classes and labels.
- Improved table layout and styling for better user experience.
- Updated Tailwind CSS configuration to define new background colors for dataset states.
This commit is contained in:
Kaimbacher 2025-10-30 14:42:36 +01:00
commit 88e37bfee8
8 changed files with 785 additions and 465 deletions

View file

@ -2,11 +2,12 @@
// import { Head, Link, useForm, usePage } from '@inertiajs/inertia-vue3';
import { Head, usePage } from '@inertiajs/vue3';
import { ComputedRef } from 'vue';
import { mdiSquareEditOutline, mdiAlertBoxOutline, mdiShareVariant, mdiBookEdit, mdiUndo, mdiLibraryShelves } from '@mdi/js';
import { computed } from 'vue';
import { mdiSquareEditOutline, mdiAlertBoxOutline, mdiShareVariant, mdiBookEdit, mdiUndo, mdiLibraryShelves, mdiAccountArrowLeft, mdiAccountArrowRight, mdiFingerprint, mdiPublish, mdiChevronDown, mdiChevronUp, mdiTrayArrowDown, mdiCheckDecagram } from '@mdi/js';
import { computed, ref, onMounted } from 'vue';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import SectionMain from '@/Components/SectionMain.vue';
import BaseButton from '@/Components/BaseButton.vue';
import BaseIcon from '@/Components/BaseIcon.vue';
import CardBox from '@/Components/CardBox.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import NotificationBar from '@/Components/NotificationBar.vue';
@ -26,78 +27,93 @@ const props = defineProps({
type: Object,
default: () => ({}),
},
// user: {
// type: Object,
// default: () => ({}),
// }
});
const flash: ComputedRef<any> = computed(() => {
// let test = usePage();
// console.log(test);
return usePage().props.flash;
});
// Legend visibility state with localStorage persistence
const showLegend = ref(true);
onMounted(() => {
const savedState = localStorage.getItem('datasetLegendVisible');
if (savedState !== null) {
showLegend.value = savedState === 'true';
}
});
const toggleLegend = () => {
showLegend.value = !showLegend.value;
localStorage.setItem('datasetLegendVisible', String(showLegend.value));
};
// const getRowClass = (dataset) => {
// // (props.options ? 'select' : props.type)
// let rowclass = '';
// if (dataset.server_state == 'accepted') {
// rowclass = 'bg-accepted';
// } else if (dataset.server_state == 'rejected_reviewer') {
// rowclass = 'bg-rejected-reviewer';
// } else if (dataset.server_state == 'reviewed') {
// rowclass = 'bg-reviewed';
// } else if (dataset.server_state == 'released') {
// rowclass = 'bg-released';
// } else if (dataset.server_state == 'published') {
// rowclass = 'bg-published';
// } else {
// rowclass = '';
// }
// return rowclass;
// };
const getRowClass = (dataset) => {
// (props.options ? 'select' : props.type)
let rowclass = '';
if (dataset.server_state == 'released') {
rowclass = 'bg-released';
} else if (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer') {
rowclass = 'bg-editor-accepted';
} else if (dataset.server_state == 'reviewed') {
rowclass = 'bg-reviewed';
} else if (dataset.server_state == 'published') {
rowclass = 'bg-published';
} else {
rowclass = '';
}
return rowclass;
};
// New method to format server state
const formatServerState = (state: string) => {
if (state === 'inprogress') {
return 'draft';
} else if (state === 'released') {
return 'submitted';
} else if (state === 'approved') {
return 'ready for review';
} else if (state === 'reviewer_accepted') {
return 'in review';
}
return state; // Return the original state for other cases
// Return Tailwind classes that will be defined in tailwind.config
const stateClasses = {
'released': 'bg-released dark:bg-released-dark',
'editor_accepted': 'bg-editor-accepted dark:bg-editor-accepted-dark',
'rejected_reviewer': 'bg-rejected-reviewer dark:bg-rejected-reviewer-dark',
'reviewed': 'bg-reviewed dark:bg-reviewed-dark',
'published': 'bg-published dark:bg-published-dark',
};
return stateClasses[dataset.server_state] || '';
};
// Method to get state badge color
const getStateColor = (state: string) => {
const stateColors = {
'inprogress': 'bg-sky-200 text-sky-900 dark:bg-sky-900 dark:text-sky-200',
'released': 'bg-blue-300 text-blue-900 dark:bg-blue-900 dark:text-blue-200',
'editor_accepted': 'bg-teal-200 text-teal-900 dark:bg-teal-900 dark:text-teal-200',
'rejected_reviewer': 'bg-amber-200 text-amber-900 dark:bg-amber-900 dark:text-amber-200',
'rejected_to_reviewer': 'bg-yellow-200 text-yellow-900 dark:bg-yellow-900 dark:text-yellow-200',
'rejected_editor': 'bg-rose-300 text-rose-900 dark:bg-rose-900 dark:text-rose-200',
'reviewed': 'bg-yellow-300 text-yellow-900 dark:bg-yellow-900 dark:text-yellow-200',
'published': 'bg-green-300 text-green-900 dark:bg-green-900 dark:text-green-200',
'approved': 'bg-cyan-200 text-cyan-900 dark:bg-cyan-900 dark:text-cyan-200',
'reviewer_accepted': 'bg-lime-200 text-lime-900 dark:bg-lime-900 dark:text-lime-200',
};
return stateColors[state] || 'bg-sky-200 text-sky-900 dark:bg-sky-900 dark:text-sky-200';
};
// Dynamic legend definitions
const datasetStates = [
{ key: 'released', label: 'Submitted' },
{ key: 'editor_accepted', label: 'In Approval' },
// { key: 'approved', label: 'Ready for Review' },
// { key: 'reviewer_accepted', label: 'In Review' },
{ key: 'reviewed', label: 'Reviewed' },
{ key: 'published', label: 'Published' },
{ key: 'rejected_reviewer', label: 'Rejected by Reviewer' },
];
const getLabel = (key: string) => {
return datasetStates.find(s => s.key === key)?.label || 'Unknown'
}
const availableActions = [
{ icon: mdiSquareEditOutline, label: 'Edit', color: 'text-blue-500' },
{ icon: mdiTrayArrowDown, label: 'Receive', color: 'text-cyan-500' },
{ icon: mdiCheckDecagram, label: 'Approve (Send to Reviewer)', color: 'text-teal-600' },
{ icon: mdiAccountArrowLeft, label: 'Reject to Submitter', color: 'text-amber-600' },
{ icon: mdiAccountArrowRight, label: 'Reject to Reviewer', color: 'text-yellow-600' },
{ icon: mdiLibraryShelves, label: 'Classify', color: 'text-blue-500' },
{ icon: mdiPublish, label: 'Publish', color: 'text-green-600' },
{ icon: mdiFingerprint, label: 'Mint DOI', color: 'text-cyan-600' },
];
const truncateTitle = (text: string, length = 50) => {
if (!text) return '';
return text.length > length ? text.substring(0, length) + '...' : text;
};
</script>
<template>
<LayoutAuthenticated>
<Head title="Editor Datasets" />
<SectionMain>
<NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
{{ flash.message }}
</NotificationBar>
@ -108,6 +124,80 @@ const formatServerState = (state: string) => {
{{ flash.error }}
</NotificationBar>
<!-- Legend -->
<CardBox class="mb-4">
<!-- Legend Header with Toggle -->
<div
class="flex items-center justify-between cursor-pointer select-none p-4 border-b border-gray-200 dark:border-slate-700 hover:bg-gray-50 dark:hover:bg-slate-800/50 transition-colors"
@click="toggleLegend"
>
<div class="flex items-center gap-2">
<svg class="w-5 h-5 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">
Legend - States & Actions
</h3>
</div>
<div class="flex items-center gap-2">
<span class="text-xs text-gray-500 dark:text-gray-400">
{{ showLegend ? 'Click to hide' : 'Click to show' }}
</span>
<BaseIcon
:path="showLegend ? mdiChevronUp : mdiChevronDown"
:size="20"
class="text-gray-500 dark:text-gray-400 transition-transform"
/>
</div>
</div>
<!-- Collapsible Legend Content -->
<transition
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="max-h-0 opacity-0"
enter-to-class="max-h-96 opacity-100"
leave-active-class="transition-all duration-300 ease-in"
leave-from-class="max-h-96 opacity-100"
leave-to-class="max-h-0 opacity-0"
>
<div v-show="showLegend" class="overflow-hidden">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-4">
<!-- State Colors Legend -->
<div>
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
</svg>
Dataset States
</h3>
<div class="grid grid-cols-2 gap-2 text-xs">
<div v-for="state in datasetStates" :key="state.key" class="flex items-center gap-2">
<span class="inline-flex items-center px-2 py-0.5 rounded-full" :class="getStateColor(state.key)">
{{ state.label }}
</span>
</div>
</div>
</div>
<!-- Actions Legend -->
<div>
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
Available Actions
</h3>
<div class="grid grid-cols-1 gap-1.5 text-xs">
<div v-for="action in availableActions" :key="action.label" class="flex items-center gap-2">
<BaseIcon :path="action.icon" :size="16" :class="action.color" />
<span class="text-gray-700 dark:text-gray-300">{{ action.label }}</span>
</div>
</div>
</div>
</div>
</div>
</transition>
</CardBox>
<!-- table -->
<CardBox class="mb-6" has-table>
@ -115,172 +205,144 @@ const formatServerState = (state: string) => {
<table>
<thead>
<tr>
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
<!-- <Sort label="Dataset Title" attribute="title" :search="form.search" /> -->
Title
</th>
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
Submitter
</th>
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
State
</th>
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
Editor
</th>
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
Date of last modification
</th>
<th scope="col" class="relative px-6 py-3 dark:text-white" v-if="can.edit || can.delete">
<span class="sr-only">Actions</span>
</th>
<th>Title</th>
<th>Submitter</th>
<th>State</th>
<th>Editor</th>
<th>Modified</th>
<th v-if="can.edit || can.delete">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tbody>
<tr v-for="dataset in props.datasets.data" :key="dataset.id"
:class="[getRowClass(dataset)]">
<td data-label="Login"
class="py-4 whitespace-nowrap text-gray-700 table-title">
<!-- <Link v-bind:href="stardust.route('settings.user.show', [user.id])"
class="no-underline hover:underline text-cyan-600 dark:text-cyan-400">
{{ user.login }}
</Link> -->
<!-- {{ user.id }} -->
{{ dataset.main_title }}
:class="['hover:bg-gray-50 dark:hover:bg-slate-800 transition-colors', getRowClass(dataset)]">
<td data-label="Title">
<div class="max-w-xs">
<span
class="text-gray-700 dark:text-gray-300 text-sm font-medium"
:title="dataset.main_title"
>
{{ truncateTitle(dataset.main_title) }}
</span>
</div>
</td>
<td class="py-4 whitespace-nowrap text-gray-700">
<div class="text-sm">{{ dataset.user.login }}</div>
<td data-label="Submitter">
<span class="text-sm text-gray-700 dark:text-gray-300">
{{ dataset.user.login }}
</span>
</td>
<td class="py-4 whitespace-nowrap text-gray-700">
<div class="text-sm"> {{ formatServerState(dataset.server_state) }}</div>
<div v-if="dataset.server_state === 'rejected_reviewer' && dataset.reject_reviewer_note"
class="inline-block relative ml-2 group">
<button
class="w-5 h-5 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 max-h-40 overflow-y-auto overflow-x-hidden whitespace-normal break-words">
{{ dataset.reject_reviewer_note }}
</p>
<div class="absolute -top-1 left-1 w-2 h-2 bg-white transform rotate-45">
<td data-label="State">
<div class="flex items-center gap-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium capitalize"
:class="getStateColor(dataset.server_state)">
{{ getLabel(dataset.server_state) }}
</span>
<div v-if="dataset.server_state === 'rejected_reviewer' && dataset.reject_reviewer_note"
class="relative group">
<button
class="w-5 h-5 rounded-full bg-gray-200 dark:bg-slate-700 text-gray-600 dark:text-gray-300 text-xs flex items-center justify-center focus:outline-none hover:bg-gray-300 dark:hover:bg-slate-600 transition-colors">
i
</button>
<div
class="absolute left-0 top-full mt-1 w-64 bg-white dark:bg-slate-800 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 border border-gray-200 dark:border-slate-700">
<p class="text-gray-700 dark:text-gray-300 max-h-40 overflow-y-auto overflow-x-hidden whitespace-normal break-words">
{{ dataset.reject_reviewer_note }}
</p>
<div class="absolute -top-1 left-1 w-2 h-2 bg-white dark:bg-slate-800 border-l border-t border-gray-200 dark:border-slate-700 transform rotate-45">
</div>
</div>
</div>
</div>
</td>
<td class="py-4 whitespace-nowrap text-gray-700"
v-if="dataset.server_state === 'released'">
<div class="text-sm" :title="dataset.server_date_modified">
Preferred reviewer: {{ dataset.preferred_reviewer }}
</div>
<td data-label="Editor" v-if="dataset.server_state === 'released'">
<span class="text-sm text-gray-600 dark:text-gray-400 italic" :title="dataset.server_date_modified">
Preferred: {{ dataset.preferred_reviewer }}
</span>
</td>
<td class="py-4 whitespace-nowrap text-gray-700"
<td data-label="Editor"
v-else-if="dataset.server_state === 'editor_accepted' || dataset.server_state === 'rejected_reviewer'">
<div class="text-sm" :title="dataset.server_date_modified">
In approval by: {{ dataset.editor?.login }}
</div>
<span class="text-sm text-gray-600 dark:text-gray-400 italic" :title="dataset.server_date_modified">
In approval: {{ dataset.editor?.login }}
</span>
</td>
<td class="py-4 whitespace-nowrap text-gray-700" v-else>
<div class="text-sm">{{ dataset.editor?.login }}</div>
<td data-label="Editor" v-else>
<span class="text-sm text-gray-700 dark:text-gray-300">
{{ dataset.editor?.login || '—' }}
</span>
</td>
<td data-label="modified" class="py-4 whitespace-nowrap text-gray-700">
<div class="text-sm" :title="dataset.server_date_modified">
<td data-label="Modified">
<span class="text-sm text-gray-600 dark:text-gray-400" :title="dataset.server_date_modified">
{{ dataset.server_date_modified }}
</div>
</span>
</td>
<td
class="py-4 whitespace-nowrap text-right text-sm font-medium text-gray-700 dark:text-white">
<div type="justify-start lg:justify-end" class="grid grid-cols-2 gap-x-2 gap-y-2"
no-wrap>
<td v-if="can.edit || can.delete" class="before:hidden lg:w-1 whitespace-nowrap">
<BaseButtons type="justify-start lg:justify-end" no-wrap>
<BaseButton v-if="can.receive && (dataset.server_state == 'released')"
:route-name="stardust.route('editor.dataset.receive', [dataset.id])"
color="info" :icon="mdiSquareEditOutline" :label="'Receive edit task'" small
class="col-span-1" />
color="info" :icon="mdiTrayArrowDown" small
title="Receive edit task" />
<BaseButton
v-if="can.approve && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
:route-name="stardust.route('editor.dataset.approve', [dataset.id])"
color="info" :icon="mdiShareVariant" :label="'Approve'" small
class="col-span-1" />
color="success" :icon="mdiCheckDecagram" small
title="Approve (Send to Reviewer)" />
<BaseButton
v-if="can.approve && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
:route-name="stardust.route('editor.dataset.reject', [dataset.id])"
color="info" :icon="mdiUndo" label="Reject" small class="col-span-1">
</BaseButton>
color="danger" :icon="mdiAccountArrowLeft" small
title="Reject to Submitter" />
<BaseButton
v-if="can.edit && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
:route-name="stardust.route('editor.dataset.edit', [Number(dataset.id)])"
color="info" :icon="mdiSquareEditOutline" :label="'Edit'" small
class="col-span-1">
</BaseButton>
color="info" :icon="mdiSquareEditOutline" small
title="Edit" />
<BaseButton
v-if="can.edit && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
:route-name="stardust.route('editor.dataset.categorize', [dataset.id])"
color="info" :icon="mdiLibraryShelves" :label="'Classify'" small
class="col-span-1">
</BaseButton>
color="info" :icon="mdiLibraryShelves" small
title="Classify" />
<BaseButton v-if="can.publish && (dataset.server_state == 'reviewed')"
:route-name="stardust.route('editor.dataset.rejectToReviewer', [dataset.id])"
color="info" :icon="mdiUndo" :label="'Reject To Reviewer'" small
class="col-span-1" />
color="warning" :icon="mdiAccountArrowRight" small
title="Reject to Reviewer" />
<BaseButton v-if="can.publish && (dataset.server_state == 'reviewed')"
:route-name="stardust.route('editor.dataset.publish', [dataset.id])"
color="info" :icon="mdiBookEdit" :label="'Publish'" small
class="col-span-1" />
color="success" :icon="mdiPublish" small
title="Publish" />
<BaseButton
v-if="can.publish && (dataset.server_state == 'published' && !dataset.identifier)"
:route-name="stardust.route('editor.dataset.doi', [dataset.id])"
color="info" :icon="mdiBookEdit" :label="'Mint DOI'" small
class="col-span-1 last-in-row" />
</div>
color="info" :icon="mdiFingerprint" small
title="Mint DOI" />
</BaseButtons>
</td>
</tr>
</tbody>
</table>
</div>
<div v-else>
<!-- Show warning message if datasets are not defined or empty -->
<div class="bg-yellow-200 text-yellow-800 rounded-md p-4">
<p>No datasets defined.</p>
<!-- You can add more descriptive text here -->
<div class="text-center py-12">
<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 datasets found</p>
<p class="text-sm">Datasets will appear here when they are submitted</p>
</div>
</div>
</div>
<!-- <BaseButton
v-if="can.edit && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
:route-name="stardust.route('editor.dataset.edit', [dataset.id])" color="info"
:icon="mdiSquareEditOutline" :label="'Edit'" small /> -->
<div class="py-4">
<Pagination v-bind:data="datasets.meta" />
</div>
</CardBox>
</SectionMain>
</LayoutAuthenticated>
</template>
<style scoped lang="css">
.table-title {
max-width: 200px;
/* set a maximum width */
overflow: hidden;
/* hide overflow */
text-overflow: ellipsis;
/* show ellipsis for overflowed text */
white-space: nowrap;
/* prevent wrapping */
}
</style>
</template>