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
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:
parent
a4e6f88e07
commit
88e37bfee8
8 changed files with 785 additions and 465 deletions
|
|
@ -105,6 +105,7 @@ export default class DatasetController {
|
||||||
'reviewed',
|
'reviewed',
|
||||||
'rejected_editor',
|
'rejected_editor',
|
||||||
'rejected_reviewer',
|
'rejected_reviewer',
|
||||||
|
'rejected_to_reviewer',
|
||||||
])
|
])
|
||||||
.where('account_id', user.id)
|
.where('account_id', user.id)
|
||||||
.preload('titles')
|
.preload('titles')
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@ import vine from '@vinejs/vine';
|
||||||
|
|
||||||
export const createProjectValidator = vine.compile(
|
export const createProjectValidator = vine.compile(
|
||||||
vine.object({
|
vine.object({
|
||||||
label: vine.string().trim().minLength(1).maxLength(50),
|
label: vine.string().trim().minLength(1).maxLength(50) .regex(/^[a-z0-9-]+$/),
|
||||||
name: vine
|
name: vine
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.minLength(3)
|
.minLength(3)
|
||||||
.maxLength(255)
|
.maxLength(255)
|
||||||
.regex(/^[a-z0-9-]+$/),
|
.regex(/^[a-zA-Z0-9äöüßÄÖÜ\s-]+$/),
|
||||||
description: vine.string().trim().maxLength(255).minLength(5).optional(),
|
description: vine.string().trim().maxLength(255).minLength(5).optional(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -22,7 +22,7 @@ export const updateProjectValidator = vine.compile(
|
||||||
.trim()
|
.trim()
|
||||||
.minLength(3)
|
.minLength(3)
|
||||||
.maxLength(255)
|
.maxLength(255)
|
||||||
.regex(/^[a-z0-9-]+$/),
|
.regex(/^[a-zA-Z0-9äöüßÄÖÜ\s-]+$/),
|
||||||
description: vine.string().trim().maxLength(255).minLength(5).optional(),
|
description: vine.string().trim().maxLength(255).minLength(5).optional(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ const submit = async () => {
|
||||||
|
|
||||||
<CardBox form @submit.prevent="submit()" class="shadow-lg">
|
<CardBox form @submit.prevent="submit()" class="shadow-lg">
|
||||||
<div class="grid grid-cols-1 gap-6">
|
<div class="grid grid-cols-1 gap-6">
|
||||||
<FormField label="Label" help="Required. Displayed project label" :class="{ 'text-red-400': form.errors.label }">
|
<FormField label="Label" help="Lowercase letters, numbers, and hyphens only" :class="{ 'text-red-400': form.errors.label }">
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="form.label"
|
v-model="form.label"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -56,7 +56,7 @@ const submit = async () => {
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="Name"
|
label="Name"
|
||||||
help="Required. Project identifier (slug, lowercase, no spaces)"
|
help="Required. Project title shown to users"
|
||||||
:class="{ 'text-red-400': form.errors.name }"
|
:class="{ 'text-red-400': form.errors.name }"
|
||||||
>
|
>
|
||||||
<FormControl
|
<FormControl
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,12 @@ const submit = async () => {
|
||||||
<div class="grid grid-cols-1 gap-6">
|
<div class="grid grid-cols-1 gap-6">
|
||||||
<FormField
|
<FormField
|
||||||
label="Label"
|
label="Label"
|
||||||
help="Project label (read-only)"
|
help="Lowercase letters, numbers, and hyphens only"
|
||||||
>
|
>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="form.label"
|
v-model="form.label"
|
||||||
type="text"
|
type="text"
|
||||||
|
help="Lowercase letters, numbers, and hyphens only"
|
||||||
:is-read-only=true
|
:is-read-only=true
|
||||||
class="bg-gray-100 dark:bg-slate-800 cursor-not-allowed opacity-75"
|
class="bg-gray-100 dark:bg-slate-800 cursor-not-allowed opacity-75"
|
||||||
/>
|
/>
|
||||||
|
|
@ -60,7 +61,7 @@ const submit = async () => {
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="Name"
|
label="Name"
|
||||||
help="Required. Project identifier (slug)"
|
help="Required. Project title shown to users"
|
||||||
:class="{ 'text-red-400': form.errors.name }"
|
:class="{ 'text-red-400': form.errors.name }"
|
||||||
>
|
>
|
||||||
<FormControl
|
<FormControl
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,12 @@
|
||||||
// import { Head, Link, useForm, usePage } from '@inertiajs/inertia-vue3';
|
// import { Head, Link, useForm, usePage } from '@inertiajs/inertia-vue3';
|
||||||
import { Head, usePage } from '@inertiajs/vue3';
|
import { Head, usePage } from '@inertiajs/vue3';
|
||||||
import { ComputedRef } from 'vue';
|
import { ComputedRef } from 'vue';
|
||||||
import { mdiSquareEditOutline, mdiAlertBoxOutline, mdiShareVariant, mdiBookEdit, mdiUndo, mdiLibraryShelves } from '@mdi/js';
|
import { mdiSquareEditOutline, mdiAlertBoxOutline, mdiShareVariant, mdiBookEdit, mdiUndo, mdiLibraryShelves, mdiAccountArrowLeft, mdiAccountArrowRight, mdiFingerprint, mdiPublish, mdiChevronDown, mdiChevronUp, mdiTrayArrowDown, mdiCheckDecagram } from '@mdi/js';
|
||||||
import { computed } from 'vue';
|
import { computed, ref, onMounted } from 'vue';
|
||||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||||
import SectionMain from '@/Components/SectionMain.vue';
|
import SectionMain from '@/Components/SectionMain.vue';
|
||||||
import BaseButton from '@/Components/BaseButton.vue';
|
import BaseButton from '@/Components/BaseButton.vue';
|
||||||
|
import BaseIcon from '@/Components/BaseIcon.vue';
|
||||||
import CardBox from '@/Components/CardBox.vue';
|
import CardBox from '@/Components/CardBox.vue';
|
||||||
import BaseButtons from '@/Components/BaseButtons.vue';
|
import BaseButtons from '@/Components/BaseButtons.vue';
|
||||||
import NotificationBar from '@/Components/NotificationBar.vue';
|
import NotificationBar from '@/Components/NotificationBar.vue';
|
||||||
|
|
@ -26,78 +27,93 @@ const props = defineProps({
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
// user: {
|
|
||||||
// type: Object,
|
|
||||||
// default: () => ({}),
|
|
||||||
// }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const flash: ComputedRef<any> = computed(() => {
|
const flash: ComputedRef<any> = computed(() => {
|
||||||
// let test = usePage();
|
|
||||||
// console.log(test);
|
|
||||||
return usePage().props.flash;
|
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) => {
|
const getRowClass = (dataset) => {
|
||||||
// (props.options ? 'select' : props.type)
|
// Return Tailwind classes that will be defined in tailwind.config
|
||||||
let rowclass = '';
|
const stateClasses = {
|
||||||
if (dataset.server_state == 'released') {
|
'released': 'bg-released dark:bg-released-dark',
|
||||||
rowclass = 'bg-released';
|
'editor_accepted': 'bg-editor-accepted dark:bg-editor-accepted-dark',
|
||||||
} else if (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer') {
|
'rejected_reviewer': 'bg-rejected-reviewer dark:bg-rejected-reviewer-dark',
|
||||||
rowclass = 'bg-editor-accepted';
|
'reviewed': 'bg-reviewed dark:bg-reviewed-dark',
|
||||||
} else if (dataset.server_state == 'reviewed') {
|
'published': 'bg-published dark:bg-published-dark',
|
||||||
rowclass = 'bg-reviewed';
|
};
|
||||||
} else if (dataset.server_state == 'published') {
|
|
||||||
rowclass = 'bg-published';
|
|
||||||
} else {
|
|
||||||
rowclass = '';
|
|
||||||
}
|
|
||||||
return rowclass;
|
|
||||||
};
|
|
||||||
|
|
||||||
// New method to format server state
|
return stateClasses[dataset.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
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutAuthenticated>
|
<LayoutAuthenticated>
|
||||||
|
|
||||||
<Head title="Editor Datasets" />
|
<Head title="Editor Datasets" />
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
|
|
||||||
|
|
||||||
<NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
|
<NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
|
||||||
{{ flash.message }}
|
{{ flash.message }}
|
||||||
</NotificationBar>
|
</NotificationBar>
|
||||||
|
|
@ -108,6 +124,80 @@ const formatServerState = (state: string) => {
|
||||||
{{ flash.error }}
|
{{ flash.error }}
|
||||||
</NotificationBar>
|
</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 -->
|
<!-- table -->
|
||||||
<CardBox class="mb-6" has-table>
|
<CardBox class="mb-6" has-table>
|
||||||
|
|
@ -115,172 +205,144 @@ const formatServerState = (state: string) => {
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
|
<th>Title</th>
|
||||||
<!-- <Sort label="Dataset Title" attribute="title" :search="form.search" /> -->
|
<th>Submitter</th>
|
||||||
Title
|
<th>State</th>
|
||||||
</th>
|
<th>Editor</th>
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
|
<th>Modified</th>
|
||||||
Submitter
|
<th v-if="can.edit || can.delete">Actions</th>
|
||||||
</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>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody class="bg-white divide-y divide-gray-200">
|
<tbody>
|
||||||
<tr v-for="dataset in props.datasets.data" :key="dataset.id"
|
<tr v-for="dataset in props.datasets.data" :key="dataset.id"
|
||||||
:class="[getRowClass(dataset)]">
|
:class="['hover:bg-gray-50 dark:hover:bg-slate-800 transition-colors', getRowClass(dataset)]">
|
||||||
<td data-label="Login"
|
<td data-label="Title">
|
||||||
class="py-4 whitespace-nowrap text-gray-700 table-title">
|
<div class="max-w-xs">
|
||||||
<!-- <Link v-bind:href="stardust.route('settings.user.show', [user.id])"
|
<span
|
||||||
class="no-underline hover:underline text-cyan-600 dark:text-cyan-400">
|
class="text-gray-700 dark:text-gray-300 text-sm font-medium"
|
||||||
{{ user.login }}
|
:title="dataset.main_title"
|
||||||
</Link> -->
|
>
|
||||||
<!-- {{ user.id }} -->
|
{{ truncateTitle(dataset.main_title) }}
|
||||||
{{ dataset.main_title }}
|
</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 whitespace-nowrap text-gray-700">
|
<td data-label="Submitter">
|
||||||
<div class="text-sm">{{ dataset.user.login }}</div>
|
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{{ dataset.user.login }}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 whitespace-nowrap text-gray-700">
|
<td data-label="State">
|
||||||
<div class="text-sm"> {{ formatServerState(dataset.server_state) }}</div>
|
<div class="flex items-center gap-2">
|
||||||
<div v-if="dataset.server_state === 'rejected_reviewer' && dataset.reject_reviewer_note"
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium capitalize"
|
||||||
class="inline-block relative ml-2 group">
|
:class="getStateColor(dataset.server_state)">
|
||||||
<button
|
{{ getLabel(dataset.server_state) }}
|
||||||
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">
|
</span>
|
||||||
i
|
<div v-if="dataset.server_state === 'rejected_reviewer' && dataset.reject_reviewer_note"
|
||||||
</button>
|
class="relative group">
|
||||||
<div
|
<button
|
||||||
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">
|
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">
|
||||||
<p class="text-gray-700 max-h-40 overflow-y-auto overflow-x-hidden whitespace-normal break-words">
|
i
|
||||||
{{ dataset.reject_reviewer_note }}
|
</button>
|
||||||
</p>
|
<div
|
||||||
<div class="absolute -top-1 left-1 w-2 h-2 bg-white transform rotate-45">
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="py-4 whitespace-nowrap text-gray-700"
|
<td data-label="Editor" v-if="dataset.server_state === 'released'">
|
||||||
v-if="dataset.server_state === 'released'">
|
<span class="text-sm text-gray-600 dark:text-gray-400 italic" :title="dataset.server_date_modified">
|
||||||
<div class="text-sm" :title="dataset.server_date_modified">
|
Preferred: {{ dataset.preferred_reviewer }}
|
||||||
Preferred reviewer: {{ dataset.preferred_reviewer }}
|
</span>
|
||||||
</div>
|
|
||||||
</td>
|
</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'">
|
v-else-if="dataset.server_state === 'editor_accepted' || dataset.server_state === 'rejected_reviewer'">
|
||||||
<div class="text-sm" :title="dataset.server_date_modified">
|
<span class="text-sm text-gray-600 dark:text-gray-400 italic" :title="dataset.server_date_modified">
|
||||||
In approval by: {{ dataset.editor?.login }}
|
In approval: {{ dataset.editor?.login }}
|
||||||
</div>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 whitespace-nowrap text-gray-700" v-else>
|
<td data-label="Editor" v-else>
|
||||||
<div class="text-sm">{{ dataset.editor?.login }}</div>
|
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{{ dataset.editor?.login || '—' }}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td data-label="modified" class="py-4 whitespace-nowrap text-gray-700">
|
<td data-label="Modified">
|
||||||
<div class="text-sm" :title="dataset.server_date_modified">
|
<span class="text-sm text-gray-600 dark:text-gray-400" :title="dataset.server_date_modified">
|
||||||
{{ dataset.server_date_modified }}
|
{{ dataset.server_date_modified }}
|
||||||
</div>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td v-if="can.edit || can.delete" class="before:hidden lg:w-1 whitespace-nowrap">
|
||||||
class="py-4 whitespace-nowrap text-right text-sm font-medium text-gray-700 dark:text-white">
|
<BaseButtons type="justify-start lg:justify-end" no-wrap>
|
||||||
<div type="justify-start lg:justify-end" class="grid grid-cols-2 gap-x-2 gap-y-2"
|
|
||||||
no-wrap>
|
|
||||||
|
|
||||||
<BaseButton v-if="can.receive && (dataset.server_state == 'released')"
|
<BaseButton v-if="can.receive && (dataset.server_state == 'released')"
|
||||||
:route-name="stardust.route('editor.dataset.receive', [dataset.id])"
|
:route-name="stardust.route('editor.dataset.receive', [dataset.id])"
|
||||||
color="info" :icon="mdiSquareEditOutline" :label="'Receive edit task'" small
|
color="info" :icon="mdiTrayArrowDown" small
|
||||||
class="col-span-1" />
|
title="Receive edit task" />
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="can.approve && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
v-if="can.approve && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
||||||
:route-name="stardust.route('editor.dataset.approve', [dataset.id])"
|
:route-name="stardust.route('editor.dataset.approve', [dataset.id])"
|
||||||
color="info" :icon="mdiShareVariant" :label="'Approve'" small
|
color="success" :icon="mdiCheckDecagram" small
|
||||||
class="col-span-1" />
|
title="Approve (Send to Reviewer)" />
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="can.approve && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
v-if="can.approve && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
||||||
:route-name="stardust.route('editor.dataset.reject', [dataset.id])"
|
:route-name="stardust.route('editor.dataset.reject', [dataset.id])"
|
||||||
color="info" :icon="mdiUndo" label="Reject" small class="col-span-1">
|
color="danger" :icon="mdiAccountArrowLeft" small
|
||||||
</BaseButton>
|
title="Reject to Submitter" />
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="can.edit && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
v-if="can.edit && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
||||||
:route-name="stardust.route('editor.dataset.edit', [Number(dataset.id)])"
|
:route-name="stardust.route('editor.dataset.edit', [Number(dataset.id)])"
|
||||||
color="info" :icon="mdiSquareEditOutline" :label="'Edit'" small
|
color="info" :icon="mdiSquareEditOutline" small
|
||||||
class="col-span-1">
|
title="Edit" />
|
||||||
</BaseButton>
|
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="can.edit && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
v-if="can.edit && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
||||||
:route-name="stardust.route('editor.dataset.categorize', [dataset.id])"
|
:route-name="stardust.route('editor.dataset.categorize', [dataset.id])"
|
||||||
color="info" :icon="mdiLibraryShelves" :label="'Classify'" small
|
color="info" :icon="mdiLibraryShelves" small
|
||||||
class="col-span-1">
|
title="Classify" />
|
||||||
</BaseButton>
|
|
||||||
|
|
||||||
<BaseButton v-if="can.publish && (dataset.server_state == 'reviewed')"
|
<BaseButton v-if="can.publish && (dataset.server_state == 'reviewed')"
|
||||||
:route-name="stardust.route('editor.dataset.rejectToReviewer', [dataset.id])"
|
:route-name="stardust.route('editor.dataset.rejectToReviewer', [dataset.id])"
|
||||||
color="info" :icon="mdiUndo" :label="'Reject To Reviewer'" small
|
color="warning" :icon="mdiAccountArrowRight" small
|
||||||
class="col-span-1" />
|
title="Reject to Reviewer" />
|
||||||
|
|
||||||
<BaseButton v-if="can.publish && (dataset.server_state == 'reviewed')"
|
<BaseButton v-if="can.publish && (dataset.server_state == 'reviewed')"
|
||||||
:route-name="stardust.route('editor.dataset.publish', [dataset.id])"
|
:route-name="stardust.route('editor.dataset.publish', [dataset.id])"
|
||||||
color="info" :icon="mdiBookEdit" :label="'Publish'" small
|
color="success" :icon="mdiPublish" small
|
||||||
class="col-span-1" />
|
title="Publish" />
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="can.publish && (dataset.server_state == 'published' && !dataset.identifier)"
|
v-if="can.publish && (dataset.server_state == 'published' && !dataset.identifier)"
|
||||||
:route-name="stardust.route('editor.dataset.doi', [dataset.id])"
|
:route-name="stardust.route('editor.dataset.doi', [dataset.id])"
|
||||||
color="info" :icon="mdiBookEdit" :label="'Mint DOI'" small
|
color="info" :icon="mdiFingerprint" small
|
||||||
class="col-span-1 last-in-row" />
|
title="Mint DOI" />
|
||||||
|
</BaseButtons>
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<!-- Show warning message if datasets are not defined or empty -->
|
<div class="text-center py-12">
|
||||||
<div class="bg-yellow-200 text-yellow-800 rounded-md p-4">
|
<div class="flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
|
||||||
<p>No datasets defined.</p>
|
<p class="text-lg font-medium mb-2">No datasets found</p>
|
||||||
<!-- You can add more descriptive text here -->
|
<p class="text-sm">Datasets will appear here when they are submitted</p>
|
||||||
|
</div>
|
||||||
</div>
|
</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">
|
<div class="py-4">
|
||||||
<Pagination v-bind:data="datasets.meta" />
|
<Pagination v-bind:data="datasets.meta" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
</LayoutAuthenticated>
|
</LayoutAuthenticated>
|
||||||
</template>
|
</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>
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Head, usePage } from '@inertiajs/vue3';
|
import { Head, usePage } from '@inertiajs/vue3';
|
||||||
import { ComputedRef } from 'vue';
|
import { ComputedRef } from 'vue';
|
||||||
import { mdiAlertBoxOutline, mdiGlasses, mdiReiterate } from '@mdi/js';
|
import { mdiAlertBoxOutline, mdiGlasses, mdiAccountArrowLeft, mdiChevronDown, mdiChevronUp } from '@mdi/js';
|
||||||
import { computed } from 'vue';
|
import { computed, ref, onMounted } from 'vue';
|
||||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||||
import SectionMain from '@/Components/SectionMain.vue';
|
import SectionMain from '@/Components/SectionMain.vue';
|
||||||
import BaseButton from '@/Components/BaseButton.vue';
|
import BaseButton from '@/Components/BaseButton.vue';
|
||||||
|
import BaseIcon from '@/Components/BaseIcon.vue';
|
||||||
import CardBox from '@/Components/CardBox.vue';
|
import CardBox from '@/Components/CardBox.vue';
|
||||||
import BaseButtons from '@/Components/BaseButtons.vue';
|
import BaseButtons from '@/Components/BaseButtons.vue';
|
||||||
import NotificationBar from '@/Components/NotificationBar.vue';
|
import NotificationBar from '@/Components/NotificationBar.vue';
|
||||||
|
|
@ -25,63 +26,83 @@ const props = defineProps({
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
// user: {
|
|
||||||
// type: Object,
|
|
||||||
// default: () => ({}),
|
|
||||||
// }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const flash: ComputedRef<any> = computed(() => {
|
const flash: ComputedRef<any> = computed(() => {
|
||||||
// let test = usePage();
|
|
||||||
// console.log(test);
|
|
||||||
return usePage().props.flash;
|
return usePage().props.flash;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Legend visibility state with localStorage persistence
|
||||||
|
const showLegend = ref(true);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const savedState = localStorage.getItem('reviewerDatasetLegendVisible');
|
||||||
|
if (savedState !== null) {
|
||||||
|
showLegend.value = savedState === 'true';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleLegend = () => {
|
||||||
|
showLegend.value = !showLegend.value;
|
||||||
|
localStorage.setItem('reviewerDatasetLegendVisible', String(showLegend.value));
|
||||||
|
};
|
||||||
|
|
||||||
const getRowClass = (dataset) => {
|
const getRowClass = (dataset) => {
|
||||||
// (props.options ? 'select' : props.type)
|
// Return Tailwind classes that will be defined in tailwind.config
|
||||||
let rowclass = '';
|
const stateClasses = {
|
||||||
if (dataset.server_state == 'approved') {
|
'approved': 'bg-approved dark:bg-approved-dark',
|
||||||
rowclass = 'bg-approved';
|
'rejected_reviewer': 'bg-rejected-reviewer dark:bg-rejected-reviewer-dark',
|
||||||
} else if (dataset.server_state == 'rejected_reviewer') {
|
'rejected_to_reviewer': 'bg-rejected-reviewer dark:bg-rejected-reviewer-dark',
|
||||||
rowclass = 'bg-rejected-reviewer';
|
'reviewed': 'bg-reviewed dark:bg-reviewed-dark',
|
||||||
} else if (dataset.server_state == 'reviewed') {
|
};
|
||||||
rowclass = 'bg-reviewed';
|
|
||||||
} else if (dataset.server_state == 'released') {
|
return stateClasses[dataset.server_state] || '';
|
||||||
rowclass = 'bg-released';
|
|
||||||
} else if (dataset.server_state == 'published') {
|
|
||||||
rowclass = 'bg-published';
|
|
||||||
} else if (dataset.server_state == 'rejected_to_reviewer') {
|
|
||||||
rowclass = 'bg-rejected-reviewer';
|
|
||||||
} else {
|
|
||||||
rowclass = '';
|
|
||||||
}
|
|
||||||
return rowclass;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// New method to format server state
|
// Method to get state badge color
|
||||||
const formatServerState = (state: string) => {
|
const getStateColor = (state: string) => {
|
||||||
if (state === 'inprogress') {
|
const stateColors = {
|
||||||
return 'draft';
|
'inprogress': 'bg-sky-200 text-sky-900 dark:bg-sky-900 dark:text-sky-200',
|
||||||
} else if (state === 'released') {
|
'released': 'bg-blue-300 text-blue-900 dark:bg-blue-900 dark:text-blue-200',
|
||||||
return 'submitted';
|
'editor_accepted': 'bg-teal-200 text-teal-900 dark:bg-teal-900 dark:text-teal-200',
|
||||||
} else if (state === 'approved') {
|
'rejected_reviewer': 'bg-amber-200 text-amber-900 dark:bg-amber-900 dark:text-amber-200',
|
||||||
return 'ready for review';
|
'rejected_to_reviewer': 'bg-yellow-200 text-yellow-900 dark:bg-yellow-900 dark:text-yellow-200',
|
||||||
} else if (state === 'reviewer_accepted') {
|
'rejected_editor': 'bg-rose-300 text-rose-900 dark:bg-rose-900 dark:text-rose-200',
|
||||||
return 'in review';
|
'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',
|
||||||
return state; // Return the original state for other cases
|
'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';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const truncateTitle = (text: string, length = 50) => {
|
||||||
|
if (!text) return '';
|
||||||
|
return text.length > length ? text.substring(0, length) + '...' : text;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dynamic legend definitions
|
||||||
|
const datasetStates = [
|
||||||
|
{ key: 'approved', label: 'Ready for Review' },
|
||||||
|
{ key: 'rejected_to_reviewer', label: 'Rejected To Reviewer' }
|
||||||
|
];
|
||||||
|
const getLabel = (key: string) => {
|
||||||
|
return datasetStates.find(s => s.key === key)?.label || 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableActions = [
|
||||||
|
{ icon: mdiGlasses, label: 'View / Review', color: 'text-blue-500' },
|
||||||
|
{ icon: mdiAccountArrowLeft, label: 'Reject to Editor', color: 'text-yellow-600' },
|
||||||
|
];
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutAuthenticated>
|
<LayoutAuthenticated>
|
||||||
|
<Head title="Reviewer Datasets" />
|
||||||
<Head title="Dataset List" />
|
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<!-- <FormValidationErrors v-bind:errors="errors" /> -->
|
|
||||||
<NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
|
<NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
|
||||||
{{ flash.message }}
|
{{ flash.message }}
|
||||||
</NotificationBar>
|
</NotificationBar>
|
||||||
|
|
@ -92,86 +113,162 @@ const formatServerState = (state: string) => {
|
||||||
{{ flash.error }}
|
{{ flash.error }}
|
||||||
</NotificationBar>
|
</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 -->
|
<!-- table -->
|
||||||
<CardBox class="mb-6" has-table>
|
<CardBox class="mb-6" has-table>
|
||||||
<div v-if="props.datasets.data.length > 0">
|
<div v-if="props.datasets.data.length > 0">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
|
<th>Title</th>
|
||||||
<!-- <Sort label="Dataset Title" attribute="title" :search="form.search" /> -->
|
<th>ID</th>
|
||||||
Title
|
<th>State</th>
|
||||||
</th>
|
<th>Editor</th>
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
|
<th>Remaining Time</th>
|
||||||
ID
|
<th v-if="can.edit || can.delete">Actions</th>
|
||||||
</th>
|
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
|
|
||||||
<!-- <Sort label="Email" attribute="email" :search="form.search" /> -->
|
|
||||||
State
|
|
||||||
</th>
|
|
||||||
|
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider">
|
|
||||||
Editor
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
|
|
||||||
Remaining Time
|
|
||||||
</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>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="dataset in props.datasets.data" :key="dataset.id" :class="[getRowClass(dataset)]">
|
<tr v-for="dataset in props.datasets.data" :key="dataset.id"
|
||||||
<td data-label="Login"
|
:class="['hover:bg-gray-50 dark:hover:bg-slate-800 transition-colors', getRowClass(dataset)]">
|
||||||
class="py-4 whitespace-nowrap text-gray-700">
|
<td data-label="Title">
|
||||||
<div class="text-sm table-title">{{ dataset.main_title }}</div>
|
<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>
|
||||||
<td class="py-4 whitespace-nowrap text-gray-700">
|
<td data-label="ID">
|
||||||
<div class="text-sm">{{ dataset.id }}</div>
|
<span class="text-sm text-gray-700 dark:text-gray-300 font-mono">
|
||||||
|
{{ dataset.id }}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 whitespace-nowrap text-gray-700">
|
<td data-label="State">
|
||||||
<div class="text-sm">{{ formatServerState(dataset.server_state) }}</div>
|
<div class="flex items-center gap-2">
|
||||||
<div v-if="dataset.server_state === 'rejected_to_reviewer' && dataset.reject_editor_note"
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium capitalize"
|
||||||
class="inline-block relative ml-2 group">
|
:class="getStateColor(dataset.server_state)">
|
||||||
<button
|
{{ getLabel(dataset.server_state) }}
|
||||||
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">
|
</span>
|
||||||
i
|
<div v-if="dataset.server_state === 'rejected_to_reviewer' && dataset.reject_editor_note"
|
||||||
</button>
|
class="relative group">
|
||||||
<div
|
<button
|
||||||
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">
|
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">
|
||||||
<p class="text-gray-700 max-h-40 overflow-y-auto overflow-x-hidden whitespace-normal break-words">
|
i
|
||||||
{{ dataset.reject_editor_note }}
|
</button>
|
||||||
</p>
|
<div
|
||||||
<div class="absolute -top-1 left-1 w-2 h-2 bg-white transform rotate-45">
|
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_editor_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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<td data-label="Editor">
|
||||||
<td class="py-4 whitespace-nowrap text-gray-700">
|
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
<div class="text-sm">{{ dataset.editor?.login }}</div>
|
{{ dataset.editor?.login || '—' }}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td data-label="modified" class="py-4 whitespace-nowrap text-gray-700">
|
<td data-label="Remaining Time">
|
||||||
<div class="text-sm" :title="dataset.remaining_time">
|
<span class="text-sm text-gray-600 dark:text-gray-400" :title="dataset.remaining_time">
|
||||||
{{ dataset.remaining_time + ' days' }}
|
{{ dataset.remaining_time }} days
|
||||||
</div>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td v-if="can.reject" class="before:hidden lg:w-1 whitespace-nowrap">
|
||||||
class="py-4 whitespace-nowrap text-right text-sm font-medium text-gray-700">
|
|
||||||
<BaseButtons type="justify-start lg:justify-end" no-wrap>
|
<BaseButtons type="justify-start lg:justify-end" no-wrap>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="can.reject && (dataset.server_state == 'approved' || dataset.server_state == 'rejected_to_reviewer')"
|
v-if="can.reject && (dataset.server_state == 'approved' || dataset.server_state == 'rejected_to_reviewer')"
|
||||||
:route-name="stardust.route('reviewer.dataset.review', [dataset.id])"
|
:route-name="stardust.route('reviewer.dataset.review', [dataset.id])"
|
||||||
color="info" :icon="mdiGlasses" :label="'View'" small />
|
color="info" :icon="mdiGlasses" small
|
||||||
|
title="View / Review" />
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="can.reject && (dataset.server_state == 'approved' || dataset.server_state == 'rejected_to_reviewer')"
|
v-if="can.reject && (dataset.server_state == 'approved' || dataset.server_state == 'rejected_to_reviewer')"
|
||||||
:route-name="stardust.route('reviewer.dataset.reject', [dataset.id])"
|
:route-name="stardust.route('reviewer.dataset.reject', [dataset.id])"
|
||||||
color="info" :icon="mdiReiterate" :label="'Reject'" small />
|
color="warning" :icon="mdiAccountArrowLeft" small
|
||||||
|
title="Reject to Editor" />
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -179,21 +276,16 @@ const formatServerState = (state: string) => {
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<!-- Show warning message if datasets are not defined or empty -->
|
<div class="text-center py-12">
|
||||||
<div class="bg-yellow-200 text-yellow-800 rounded-md p-4">
|
<div class="flex flex-col items-center justify-center text-gray-500 dark:text-gray-400">
|
||||||
<p>No datasets defined.</p>
|
<p class="text-lg font-medium mb-2">No datasets found</p>
|
||||||
<!-- You can add more descriptive text here -->
|
<p class="text-sm">Datasets will appear here when they are ready for review</p>
|
||||||
|
</div>
|
||||||
</div>
|
</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">
|
<div class="py-4">
|
||||||
<Pagination v-bind:data="datasets.meta" />
|
<Pagination v-bind:data="datasets.meta" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
|
|
@ -201,15 +293,24 @@ const formatServerState = (state: string) => {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="css">
|
<style scoped lang="css">
|
||||||
.table-title {
|
/* Background colors are now defined in tailwind.config.js */
|
||||||
max-width: 200px;
|
/* .bg-approved {
|
||||||
/* set a maximum width */
|
@apply bg-approved dark:bg-approved-dark;
|
||||||
overflow: hidden;
|
|
||||||
/* hide overflow */
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
/* show ellipsis for overflowed text */
|
|
||||||
white-space: nowrap;
|
|
||||||
/* prevent wrapping */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-rejected-reviewer {
|
||||||
|
@apply bg-rejected-reviewer dark:bg-rejected-reviewer-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-reviewed {
|
||||||
|
@apply bg-reviewed dark:bg-reviewed-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-released {
|
||||||
|
@apply bg-released dark:bg-released-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-published {
|
||||||
|
@apply bg-published dark:bg-published-dark;
|
||||||
|
} */
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// import { Head, Link, useForm, usePage } from '@inertiajs/inertia-vue3';
|
|
||||||
import { Head, usePage } from '@inertiajs/vue3';
|
import { Head, usePage } from '@inertiajs/vue3';
|
||||||
import { ComputedRef } from 'vue';
|
import { ComputedRef } from 'vue';
|
||||||
import { mdiSquareEditOutline, mdiTrashCan, mdiAlertBoxOutline, mdiLockOpen, mdiLibraryShelves } from '@mdi/js';
|
import { mdiSquareEditOutline, mdiTrashCan, mdiAlertBoxOutline, mdiLockOpen, mdiLibraryShelves, mdiChevronDown, mdiChevronUp } from '@mdi/js';
|
||||||
import { computed } from 'vue';
|
import { computed, ref, onMounted } from 'vue';
|
||||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||||
import SectionMain from '@/Components/SectionMain.vue';
|
import SectionMain from '@/Components/SectionMain.vue';
|
||||||
import BaseButton from '@/Components/BaseButton.vue';
|
import BaseButton from '@/Components/BaseButton.vue';
|
||||||
|
import BaseIcon from '@/Components/BaseIcon.vue';
|
||||||
import CardBox from '@/Components/CardBox.vue';
|
import CardBox from '@/Components/CardBox.vue';
|
||||||
import BaseButtons from '@/Components/BaseButtons.vue';
|
import BaseButtons from '@/Components/BaseButtons.vue';
|
||||||
import NotificationBar from '@/Components/NotificationBar.vue';
|
import NotificationBar from '@/Components/NotificationBar.vue';
|
||||||
import Pagination from '@/Components/Pagination.vue';
|
import Pagination from '@/Components/Pagination.vue';
|
||||||
import { stardust } from '@eidellev/adonis-stardust/client';
|
import { stardust } from '@eidellev/adonis-stardust/client';
|
||||||
|
import Label from '@/Components/unused/Label.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
datasets: {
|
datasets: {
|
||||||
|
|
@ -26,63 +27,120 @@ const props = defineProps({
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
// user: {
|
|
||||||
// type: Object,
|
|
||||||
// default: () => ({}),
|
|
||||||
// }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const flash: ComputedRef<any> = computed(() => {
|
const flash: ComputedRef<any> = computed(() => {
|
||||||
// let test = usePage();
|
|
||||||
// console.log(test);
|
|
||||||
return usePage().props.flash;
|
return usePage().props.flash;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Legend visibility state with localStorage persistence
|
||||||
|
const showLegend = ref(true);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const savedState = localStorage.getItem('submitterDatasetLegendVisible');
|
||||||
|
if (savedState !== null) {
|
||||||
|
showLegend.value = savedState === 'true';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleLegend = () => {
|
||||||
|
showLegend.value = !showLegend.value;
|
||||||
|
localStorage.setItem('submitterDatasetLegendVisible', String(showLegend.value));
|
||||||
|
};
|
||||||
|
|
||||||
const validStates = ['inprogress', 'rejected_editor'];
|
const validStates = ['inprogress', 'rejected_editor'];
|
||||||
|
|
||||||
|
|
||||||
const getRowClass = (dataset) => {
|
const getRowClass = (dataset) => {
|
||||||
// (props.options ? 'select' : props.type)
|
// Return Tailwind classes that will be defined in tailwind.config
|
||||||
let rowclass = '';
|
const stateClasses = {
|
||||||
if (dataset.server_state == 'inprogress') {
|
'inprogress': 'bg-inprogress dark:bg-inprogress-dark',
|
||||||
rowclass = 'bg-inprogress';
|
'released': 'bg-released dark:bg-released-dark',
|
||||||
} else if (dataset.server_state == 'released') {
|
'editor_accepted': 'bg-editor-accepted dark:bg-editor-accepted-dark',
|
||||||
rowclass = 'bg-released';
|
'rejected_reviewer': 'bg-rejected-reviewer dark:bg-rejected-reviewer-dark',
|
||||||
} else if (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer') {
|
'rejected_to_reviewer': 'bg-rejected-reviewer dark:bg-rejected-reviewer-dark',
|
||||||
rowclass = 'bg-editor-accepted';
|
|
||||||
} else if (dataset.server_state == 'approved') {
|
'approved': 'bg-approved dark:bg-approved-dark',
|
||||||
rowclass = 'bg-approved';
|
'reviewed': 'bg-reviewed dark:bg-reviewed-dark',
|
||||||
} else if (dataset.server_state == 'reviewed') {
|
'published': 'bg-published dark:bg-published-dark',
|
||||||
rowclass = 'bg-reviewed';
|
'rejected_editor': 'bg-rejected-editor dark:bg-rejected-editor-dark',
|
||||||
} else if (dataset.server_state == 'rejected_editor') {
|
};
|
||||||
rowclass = 'bg-rejected-editor';
|
|
||||||
} else {
|
return stateClasses[dataset.server_state] || '';
|
||||||
rowclass = '';
|
|
||||||
}
|
|
||||||
return rowclass;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// New method to format server state
|
// New method to format server state
|
||||||
const formatServerState = (state: string) => {
|
// const formatServerState = (state: string) => {
|
||||||
if (state === 'inprogress') {
|
// if (state === 'inprogress') {
|
||||||
return 'draft';
|
// return 'draft';
|
||||||
} else if (state === 'released') {
|
// } else if (state === 'released') {
|
||||||
return 'submitted';
|
// return 'submitted';
|
||||||
} else if (state === 'approved') {
|
// } else if (state === 'editor_accepted') {
|
||||||
return 'ready for review';
|
// return 'in approval';
|
||||||
} else if (state === 'reviewer_accepted') {
|
// } else if (state === 'approved') {
|
||||||
return 'in review';
|
// return 'ready for review';
|
||||||
}
|
// } else if (state === 'reviewer_accepted') {
|
||||||
return state; // Return the original state for other cases
|
// return 'in review';
|
||||||
|
// } else if (state === 'rejected_editor') {
|
||||||
|
// return 'rejected by editor';
|
||||||
|
// }
|
||||||
|
// return state;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Method to get state badge color
|
||||||
|
// 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: 'inprogress', label: 'Draft' },
|
||||||
|
{ 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_editor', label: 'Rejected by Editor' },
|
||||||
|
{ key: 'rejected_reviewer', label: 'Rejected by Reviewer' },
|
||||||
|
{ key: 'rejected_to_reviewer', label: 'Rejected To Reviewer' }
|
||||||
|
];
|
||||||
|
const getLabel = (key: string) => {
|
||||||
|
return datasetStates.find(s => s.key === key)?.label || 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableActions = [
|
||||||
|
{ icon: mdiLockOpen, label: 'Release (Submit)', color: 'text-blue-500' },
|
||||||
|
{ icon: mdiSquareEditOutline, label: 'Edit', color: 'text-blue-500' },
|
||||||
|
{ icon: mdiLibraryShelves, label: 'Classify', color: 'text-blue-500' },
|
||||||
|
{ icon: mdiTrashCan, label: 'Delete', color: 'text-red-500' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const truncateTitle = (text: string, length = 50) => {
|
||||||
|
if (!text) return '';
|
||||||
|
return text.length > length ? text.substring(0, length) + '...' : text;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutAuthenticated>
|
<LayoutAuthenticated>
|
||||||
|
<Head title="My Datasets" />
|
||||||
<Head title="Dataset List" />
|
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<!-- <FormValidationErrors v-bind:errors="errors" /> -->
|
|
||||||
<NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
|
<NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
|
||||||
{{ flash.message }}
|
{{ flash.message }}
|
||||||
</NotificationBar>
|
</NotificationBar>
|
||||||
|
|
@ -90,86 +148,170 @@ const formatServerState = (state: string) => {
|
||||||
{{ flash.warning }}
|
{{ flash.warning }}
|
||||||
</NotificationBar>
|
</NotificationBar>
|
||||||
|
|
||||||
<!-- table -->
|
<!-- Legend -->
|
||||||
<CardBox class="mb-6" has-table>
|
<CardBox class="mb-4">
|
||||||
<table class="w-full table-fixed">
|
<!-- Legend Header with Toggle -->
|
||||||
<thead>
|
<div
|
||||||
<tr>
|
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"
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
|
@click="toggleLegend"
|
||||||
<!-- <Sort label="Dataset Title" attribute="title" :search="form.search" /> -->
|
>
|
||||||
Dataset Title
|
<div class="flex items-center gap-2">
|
||||||
</th>
|
<svg class="w-5 h-5 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
|
<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" />
|
||||||
<!-- <Sort label="Email" attribute="email" :search="form.search" /> -->
|
</svg>
|
||||||
Server State
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
</th>
|
Legend - States & Actions
|
||||||
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
|
</h3>
|
||||||
Date of last modification
|
</div>
|
||||||
</th>
|
<div class="flex items-center gap-2">
|
||||||
<th scope="col" class="relative px-6 py-3 dark:text-white" v-if="can.edit || can.delete">
|
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
<span class="sr-only">Actions</span>
|
{{ showLegend ? 'Click to hide' : 'Click to show' }}
|
||||||
</th>
|
</span>
|
||||||
</tr>
|
<BaseIcon
|
||||||
</thead>
|
:path="showLegend ? mdiChevronUp : mdiChevronDown"
|
||||||
|
:size="20"
|
||||||
|
class="text-gray-500 dark:text-gray-400 transition-transform"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<tbody class="bg-white divide-y divide-gray-200">
|
<!-- Collapsible Legend Content -->
|
||||||
<tr v-for="dataset in props.datasets.data" :key="dataset.id" :class="getRowClass(dataset)">
|
<transition
|
||||||
<td data-label="Login"
|
enter-active-class="transition-all duration-300 ease-out"
|
||||||
class="py-4 whitespace-nowrap text-gray-700 table-title">
|
enter-from-class="max-h-0 opacity-0"
|
||||||
<!-- <Link v-bind:href="stardust.route('settings.user.show', [user.id])"
|
enter-to-class="max-h-96 opacity-100"
|
||||||
class="no-underline hover:underline text-cyan-600 dark:text-cyan-400">
|
leave-active-class="transition-all duration-300 ease-in"
|
||||||
{{ user.login }}
|
leave-from-class="max-h-96 opacity-100"
|
||||||
</Link> -->
|
leave-to-class="max-h-0 opacity-0"
|
||||||
<!-- {{ user.id }} -->
|
>
|
||||||
{{ dataset.main_title }}
|
<div v-show="showLegend" class="overflow-hidden">
|
||||||
</td>
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-4">
|
||||||
<td class="py-4 whitespace-nowrap text-gray-700">
|
<!-- State Colors Legend -->
|
||||||
{{ formatServerState(dataset.server_state) }}
|
<div>
|
||||||
<div v-if="dataset.server_state === 'rejected_editor' && dataset.reject_editor_note"
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3 flex items-center gap-2">
|
||||||
class="inline-block relative ml-2 group">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<button
|
<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" />
|
||||||
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">
|
</svg>
|
||||||
i
|
Dataset States
|
||||||
</button>
|
</h3>
|
||||||
<div
|
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||||
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">
|
<div v-for="state in datasetStates" :key="state.key" class="flex items-center gap-2">
|
||||||
<p
|
<span class="inline-flex items-center px-2 py-0.5 rounded-full" :class="getStateColor(state.key)">
|
||||||
class="text-gray-700 max-h-40 overflow-y-auto overflow-x-hidden whitespace-normal break-words">
|
{{ state.label }}
|
||||||
{{ dataset.reject_editor_note }}
|
</span>
|
||||||
</p>
|
|
||||||
<div class="absolute -top-1 left-1 w-2 h-2 bg-white transform rotate-45">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
|
|
||||||
<td data-label="modified" class="py-4 whitespace-nowrap text-gray-700">
|
<!-- Actions Legend -->
|
||||||
<div class="text-sm" :title="dataset.server_date_modified">
|
<div>
|
||||||
{{ dataset.server_date_modified }}
|
<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>
|
||||||
</td>
|
</div>
|
||||||
<td
|
</div>
|
||||||
class="py-4 whitespace-nowrap text-right text-sm font-medium text-gray-700">
|
</div>
|
||||||
<BaseButtons v-if="validStates.includes(dataset.server_state)"
|
</transition>
|
||||||
type="justify-start lg:justify-end" no-wrap>
|
</CardBox>
|
||||||
<!-- release created dataset -->
|
|
||||||
<BaseButton v-if="can.edit"
|
<!-- table -->
|
||||||
:route-name="stardust.route('dataset.release', [dataset.id])" color="info"
|
<CardBox class="mb-6" has-table>
|
||||||
:icon="mdiLockOpen" :label="'Release'" small />
|
<div v-if="props.datasets.data.length > 0">
|
||||||
<BaseButton v-if="can.edit"
|
<table>
|
||||||
:route-name="stardust.route('dataset.edit', [dataset.id])" color="info"
|
<thead>
|
||||||
:icon="mdiSquareEditOutline" :label="'Edit'" small />
|
<tr>
|
||||||
<BaseButton v-if="can.edit"
|
<th>Title</th>
|
||||||
:route-name="stardust.route('dataset.categorize', [dataset.id])" color="info"
|
<th>State</th>
|
||||||
:icon="mdiLibraryShelves" :label="'Classify'" small />
|
<th>Modified</th>
|
||||||
<BaseButton v-if="can.delete" color="danger"
|
<th v-if="can.edit || can.delete">Actions</th>
|
||||||
:route-name="stardust.route('dataset.delete', [dataset.id])" :icon="mdiTrashCan"
|
</tr>
|
||||||
small />
|
</thead>
|
||||||
</BaseButtons>
|
|
||||||
</td>
|
<tbody>
|
||||||
</tr>
|
<tr v-for="dataset in props.datasets.data" :key="dataset.id"
|
||||||
</tbody>
|
:class="['hover:bg-gray-50 dark:hover:bg-slate-800 transition-colors', getRowClass(dataset)]">
|
||||||
</table>
|
<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 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_editor' && dataset.reject_editor_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_editor_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 data-label="Modified">
|
||||||
|
<span class="text-sm text-gray-600 dark:text-gray-400" :title="dataset.server_date_modified">
|
||||||
|
{{ dataset.server_date_modified }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td v-if="can.edit || can.delete" class="before:hidden lg:w-1 whitespace-nowrap">
|
||||||
|
<BaseButtons v-if="validStates.includes(dataset.server_state)"
|
||||||
|
type="justify-start lg:justify-end" no-wrap>
|
||||||
|
<BaseButton v-if="can.edit"
|
||||||
|
:route-name="stardust.route('dataset.release', [dataset.id])" color="info"
|
||||||
|
:icon="mdiLockOpen" small
|
||||||
|
title="Release (Submit)" />
|
||||||
|
<BaseButton v-if="can.edit"
|
||||||
|
:route-name="stardust.route('dataset.edit', [dataset.id])" color="info"
|
||||||
|
:icon="mdiSquareEditOutline" small
|
||||||
|
title="Edit" />
|
||||||
|
<BaseButton v-if="can.edit"
|
||||||
|
:route-name="stardust.route('dataset.categorize', [dataset.id])" color="info"
|
||||||
|
:icon="mdiLibraryShelves" small
|
||||||
|
title="Classify" />
|
||||||
|
<BaseButton v-if="can.delete" color="danger"
|
||||||
|
:route-name="stardust.route('dataset.delete', [dataset.id])" :icon="mdiTrashCan"
|
||||||
|
small
|
||||||
|
title="Delete" />
|
||||||
|
</BaseButtons>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<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">Create your first dataset to get started</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="py-4">
|
<div class="py-4">
|
||||||
<Pagination v-bind:data="datasets.meta" />
|
<Pagination v-bind:data="datasets.meta" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -179,56 +321,28 @@ const formatServerState = (state: string) => {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="css">
|
<style scoped lang="css">
|
||||||
.table-title {
|
/* Background colors are now defined in tailwind.config.js */
|
||||||
max-width: 200px;
|
/* .bg-inprogress {
|
||||||
/* set a maximum width */
|
@apply bg-inprogress dark:bg-inprogress-dark;
|
||||||
overflow: hidden;
|
|
||||||
/* hide overflow */
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
/* show ellipsis for overflowed text */
|
|
||||||
white-space: nowrap;
|
|
||||||
/* prevent wrapping */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-fixed {
|
.bg-released {
|
||||||
table-layout: fixed;
|
@apply bg-released dark:bg-released-dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .pure-table tr.released {
|
.bg-editor-accepted {
|
||||||
background-color: rgb(52 211 153);
|
@apply bg-editor-accepted dark:bg-editor-accepted-dark;
|
||||||
color: gray;
|
}
|
||||||
|
|
||||||
|
.bg-approved {
|
||||||
|
@apply bg-approved dark:bg-approved-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-reviewed {
|
||||||
|
@apply bg-reviewed dark:bg-reviewed-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-rejected-editor {
|
||||||
|
@apply bg-rejected-editor dark:bg-rejected-editor-dark;
|
||||||
} */
|
} */
|
||||||
|
|
||||||
/* .pure-table tr.inprogress {
|
|
||||||
padding: 0.8em;
|
|
||||||
background-color: rgb(94 234 212);
|
|
||||||
color: gray;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .pure-table tr.editor_accepted {
|
|
||||||
background-color: rgb(125 211 252);
|
|
||||||
color: gray;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .pure-table tr.rejected_reviewer {
|
|
||||||
padding: 0.8em;
|
|
||||||
background-color: orange;
|
|
||||||
color: gray;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .pure-table tr.rejected_editor {
|
|
||||||
background-color: orange;
|
|
||||||
color: gray;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .pure-table tr.reviewed {
|
|
||||||
background-color: yellow;
|
|
||||||
color: gray;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* .pure-table tr.approved {
|
|
||||||
background-color: rgb(86, 86, 241);
|
|
||||||
color: whitesmoke;
|
|
||||||
|
|
||||||
}*/
|
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -13,8 +13,49 @@ module.exports = {
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
backgroundImage: {
|
backgroundImage: {
|
||||||
'radio-checked': "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z' /%3E%3C/svg%3E\")",
|
'radio-checked':
|
||||||
'checkbox-checked': "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:%23fff' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E\")",
|
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z' /%3E%3C/svg%3E\")",
|
||||||
|
'checkbox-checked':
|
||||||
|
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:%23fff' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E\")",
|
||||||
|
},
|
||||||
|
backgroundColor: {
|
||||||
|
// Draft / In Progress - Light blue-gray
|
||||||
|
'draft': 'rgb(224 242 254)', // sky-100
|
||||||
|
'draft-dark': 'rgb(12 74 110 / 0.3)', // sky-900/30
|
||||||
|
'inprogress': 'rgb(224 242 254)', // sky-100
|
||||||
|
'inprogress-dark': 'rgb(12 74 110 / 0.3)', // sky-900/30
|
||||||
|
|
||||||
|
// Released / Submitted - Bright blue
|
||||||
|
'released': 'rgb(191 219 254)', // blue-200
|
||||||
|
'released-dark': 'rgb(30 58 138 / 0.3)', // blue-900/30
|
||||||
|
|
||||||
|
// Editor Accepted - Blue-green (teal)
|
||||||
|
'editor-accepted': 'rgb(204 251 241)', // teal-100
|
||||||
|
'editor-accepted-dark': 'rgb(19 78 74 / 0.3)', // teal-900/30
|
||||||
|
|
||||||
|
// Rejected by Reviewer - Yellow-orange (amber)
|
||||||
|
'rejected-reviewer': 'rgb(254 243 199)', // amber-100
|
||||||
|
'rejected-reviewer-dark': 'rgb(120 53 15 / 0.3)', // amber-900/30
|
||||||
|
|
||||||
|
// Rejected by Editor - Rose/Red (back to submitter)
|
||||||
|
'rejected-editor': 'rgb(254 205 211)', // rose-200
|
||||||
|
'rejected-editor-dark': 'rgb(136 19 55 / 0.3)', // rose-900/30
|
||||||
|
|
||||||
|
// Approved / Ready for Review - Cyan (blue-green)
|
||||||
|
'approved': 'rgb(207 250 254)', // cyan-100
|
||||||
|
'approved-dark': 'rgb(22 78 99 / 0.3)', // cyan-900/30
|
||||||
|
|
||||||
|
// Reviewer Accepted / In Review - Lime yellow-green
|
||||||
|
'reviewer-accepted': 'rgb(236 252 203)', // lime-100
|
||||||
|
'reviewer-accepted-dark': 'rgb(54 83 20 / 0.3)', // lime-900/30
|
||||||
|
|
||||||
|
// Reviewed - Soft yellow
|
||||||
|
'reviewed': 'rgb(254 240 138)', // yellow-200
|
||||||
|
'reviewed-dark': 'rgb(113 63 18 / 0.3)', // yellow-900/30
|
||||||
|
|
||||||
|
// Published - Fresh green
|
||||||
|
'published': 'rgb(187 247 208)', // green-200
|
||||||
|
'published-dark': 'rgb(20 83 45 / 0.3)', // green-900/30
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
'primary': '#22C55E',
|
'primary': '#22C55E',
|
||||||
|
|
@ -30,7 +71,7 @@ module.exports = {
|
||||||
'lime': {
|
'lime': {
|
||||||
DEFAULT: '#BFCE40',
|
DEFAULT: '#BFCE40',
|
||||||
dark: 'rgba(5,46,55,0.7)',
|
dark: 'rgba(5,46,55,0.7)',
|
||||||
50: '#FBFCF7',
|
50: '#FBFCF7',
|
||||||
100: '#F8FBE1',
|
100: '#F8FBE1',
|
||||||
200: '#EEF69E',
|
200: '#EEF69E',
|
||||||
300: '#DCEC53',
|
300: '#DCEC53',
|
||||||
|
|
@ -40,7 +81,7 @@ module.exports = {
|
||||||
700: '#357C06',
|
700: '#357C06',
|
||||||
800: '#295B09',
|
800: '#295B09',
|
||||||
900: '#20450A',
|
900: '#20450A',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Inter', ...defaultTheme.fontFamily.sans],
|
sans: ['Inter', ...defaultTheme.fontFamily.sans],
|
||||||
|
|
@ -106,7 +147,7 @@ module.exports = {
|
||||||
{ values: theme('asideScrollbars') },
|
{ values: theme('asideScrollbars') },
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
plugin(function({ addUtilities }) {
|
plugin(function ({ addUtilities }) {
|
||||||
const newUtilities = {
|
const newUtilities = {
|
||||||
'.drag-none': {
|
'.drag-none': {
|
||||||
'-webkit-user-drag': 'none',
|
'-webkit-user-drag': 'none',
|
||||||
|
|
@ -115,8 +156,8 @@ module.exports = {
|
||||||
'-o-user-drag': 'none',
|
'-o-user-drag': 'none',
|
||||||
'user-drag': 'none',
|
'user-drag': 'none',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
addUtilities(newUtilities)
|
addUtilities(newUtilities);
|
||||||
}),
|
}),
|
||||||
// As of Tailwind CSS v3.3, the `@tailwindcss/line-clamp` plugin is now included by default
|
// As of Tailwind CSS v3.3, the `@tailwindcss/line-clamp` plugin is now included by default
|
||||||
// require('@tailwindcss/line-clamp'),
|
// require('@tailwindcss/line-clamp'),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue