hotfix (dataset): implement reject to reviewer functionality for editors

- Added "rejected_to_reviewer" state to the `ServerStates` enum.
- Implemented routes and controller actions (`rejectToReviewer`, `rejectToReviewerUpdate`) for editors to reject datasets back to reviewers with a rejection note.
- Added UI elements (button) in the editor dataset index and publish views to trigger the "reject to reviewer" action.
- Updated the reviewer dataset index view to display datasets in the "rejected_to_reviewer" state and show the editor's rejection note.
- Modified the reviewer dataset review page to allow reviewers to view and accept datasets that have been rejected back to them by editors.
- Updated the database migration to include the "rejected_to_reviewer" state in the `documents_server_state_check` constraint.
- Updated dependencies (pinia, redis).
This commit is contained in:
Kaimbacher 2025-05-02 14:35:58 +02:00
parent c245c8e97d
commit be6b38d0a3
15 changed files with 1647 additions and 404 deletions

View file

@ -248,6 +248,10 @@ export default class DatasetsController {
if (dataset.reject_reviewer_note != null) {
dataset.reject_reviewer_note = null;
}
if (dataset.reject_editor_note != null) {
dataset.reject_editor_note = null;
}
//save main and additional titles
const reviewer_id = request.input('reviewer_id', null);
@ -286,6 +290,8 @@ export default class DatasetsController {
});
}
public async rejectUpdate({ request, response, auth }: HttpContext) {
const authUser = auth.user!;
@ -372,7 +378,7 @@ export default class DatasetsController {
.toRoute('editor.dataset.list');
}
public async publish({ request, inertia, response }: HttpContext) {
public async publish({ request, inertia, response, auth }: HttpContext) {
const id = request.param('id');
const dataset = await Dataset.query()
@ -396,8 +402,14 @@ export default class DatasetsController {
.back();
}
return inertia.render('Editor/Dataset/Publish', {
dataset,
can: {
reject: await auth.user?.can(['dataset-editor-reject']),
publish: await auth.user?.can(['dataset-publish']),
},
});
}
@ -439,6 +451,119 @@ export default class DatasetsController {
}
}
public async rejectToReviewer({ request, inertia, response }: HttpContext) {
const id = request.param('id');
const dataset = await Dataset.query()
.where('id', id)
.preload('reviewer', (builder) => {
builder.select('id', 'login', 'email');
})
.firstOrFail();
const validStates = ['reviewed'];
if (!validStates.includes(dataset.server_state)) {
// session.flash('errors', 'Invalid server state!');
return response
.flash(
'warning',
`Invalid server state. Dataset with id ${id} cannot be rejected to the reviewer. Datset has server state ${dataset.server_state}.`,
)
.redirect()
.toRoute('editor.dataset.list');
}
return inertia.render('Editor/Dataset/RejectToReviewer', {
dataset,
});
}
public async rejectToReviewerUpdate({ request, response, auth }: HttpContext) {
const authUser = auth.user!;
const id = request.param('id');
const dataset = await Dataset.query()
.where('id', id)
.preload('reviewer', (builder) => {
builder.select('id', 'login', 'email');
})
.firstOrFail();
const newSchema = vine.object({
server_state: vine.string().trim(),
reject_editor_note: vine.string().trim().minLength(10).maxLength(500),
send_mail: vine.boolean().optional(),
});
try {
// await request.validate({ schema: newSchema });
const validator = vine.compile(newSchema);
await request.validateUsing(validator);
} catch (error) {
// return response.badRequest(error.messages);
throw error;
}
const validStates = ['reviewed'];
if (!validStates.includes(dataset.server_state)) {
// throw new Error('Invalid server state!');
// return response.flash('warning', 'Invalid server state. Dataset cannot be released to editor').redirect().back();
return response
.flash(
`Invalid server state. Dataset with id ${id} cannot be rejected to reviewer. Datset has server state ${dataset.server_state}.`,
'warning',
)
.redirect()
.toRoute('editor.dataset.list');
}
dataset.server_state = 'rejected_to_reviewer';
const rejectEditorNote = request.input('reject_editor_note', '');
dataset.reject_editor_note = rejectEditorNote;
// add logic for sending reject message
const sendMail = request.input('send_email', false);
// const validRecipientEmail = await this.checkEmailDomain('arno.kaimbacher@outlook.at');
const validationResult = await validate({
email: dataset.reviewer.email,
validateSMTP: false,
});
const validRecipientEmail: boolean = validationResult.valid;
await dataset.save();
let emailStatusMessage = '';
if (sendMail == true) {
if (dataset.reviewer.email && validRecipientEmail) {
try {
await mail.send((message) => {
message.to(dataset.reviewer.email).subject('Dataset Rejection Notification').html(`
<p>Dear ${dataset.reviewer.login},</p>
<p>Your dataset with ID ${dataset.id} has been rejected.</p>
<p>Reason for rejection: ${rejectEditorNote}</p>
<p>Best regards,<br>Your Tethys editor: ${authUser.login}</p>
`);
});
emailStatusMessage = ` A rejection email was successfully sent to ${dataset.reviewer.email}.`;
} catch (error) {
logger.error(error);
return response
.flash('Dataset has not been rejected due to an email error: ' + error.message, 'error')
.toRoute('editor.dataset.list');
}
} else {
emailStatusMessage = ` However, the email could not be sent because the submitter's email address (${dataset.reviewer.email}) is not valid.`;
}
}
return response
.flash(
`You have successfully rejected dataset ${dataset.id} reviewed by ${dataset.reviewer.login}.${emailStatusMessage}`,
'message',
)
.toRoute('editor.dataset.list');
}
public async doiCreate({ request, inertia }: HttpContext) {
const id = request.param('id');
const dataset = await Dataset.query()

View file

@ -9,6 +9,7 @@ import vine from '@vinejs/vine';
import mail from '@adonisjs/mail/services/main';
import logger from '@adonisjs/core/services/logger';
import { validate } from 'deep-email-validator';
import File from '#models/file';
interface Dictionary {
[index: string]: string;
@ -38,13 +39,21 @@ export default class DatasetsController {
}
datasets.orderBy(attribute, sortOrder);
} else {
// users.orderBy('created_at', 'desc');
datasets.orderBy('id', 'asc');
// datasets.orderBy('id', 'asc');
// Custom ordering to prioritize rejected_editor state
datasets.orderByRaw(`
CASE
WHEN server_state = 'rejected_to_reviewer' THEN 0
ELSE 1
END ASC,
id ASC
`);
}
// const users = await User.query().orderBy('login').paginate(page, limit);
const myDatasets = await datasets
.where('server_state', 'approved')
// .where('server_state', 'approved')
.whereIn('server_state', ['approved', 'rejected_to_reviewer'])
.where('reviewer_id', user.id)
.preload('titles')
@ -63,6 +72,51 @@ export default class DatasetsController {
}
public async review({ request, inertia, response, auth }: HttpContext) {
const id = request.param('id');
const datasetQuery = Dataset.query().where('id', id);
datasetQuery
.preload('titles', (query) => query.orderBy('id', 'asc'))
.preload('descriptions', (query) => query.orderBy('id', 'asc'))
.preload('coverage')
.preload('licenses')
.preload('authors', (query) => query.orderBy('pivot_sort_order', 'asc'))
.preload('contributors', (query) => query.orderBy('pivot_sort_order', 'asc'))
// .preload('subjects')
.preload('subjects', (builder) => {
builder.orderBy('id', 'asc').withCount('datasets');
})
.preload('references')
.preload('project')
.preload('files', (query) => {
query.orderBy('sort_order', 'asc'); // Sort by sort_order column
});
const dataset = await datasetQuery.firstOrFail();
const validStates = ['approved', 'rejected_to_reviewer'];
if (!validStates.includes(dataset.server_state)) {
// session.flash('errors', 'Invalid server state!');
return response
.flash(
'warning',
`Invalid server state. Dataset with id ${id} cannot be reviewed. Datset has server state ${dataset.server_state}.`,
)
.redirect()
.toRoute('reviewer.dataset.list');
}
return inertia.render('Reviewer/Dataset/Review', {
dataset,
can: {
review: await auth.user?.can(['dataset-review']),
reject: await auth.user?.can(['dataset-review-reject']),
},
});
}
public async review_old({ request, inertia, response, auth }: HttpContext) {
const id = request.param('id');
const dataset = await Dataset.query()
.where('id', id)
@ -170,7 +224,7 @@ export default class DatasetsController {
// const { id } = params;
const dataset = await Dataset.findOrFail(id);
const validStates = ['approved'];
const validStates = ['approved', 'rejected_to_reviewer'];
if (!validStates.includes(dataset.server_state)) {
// throw new Error('Invalid server state!');
// return response.flash('warning', 'Invalid server state. Dataset cannot be released to editor').redirect().back();
@ -184,6 +238,10 @@ export default class DatasetsController {
}
dataset.server_state = 'reviewed';
// if editor has rejected to reviewer:
if (dataset.reject_editor_note != null) {
dataset.reject_editor_note = null;
}
try {
// await dataset.related('editor').associate(user); // speichert schon ab
@ -207,7 +265,7 @@ export default class DatasetsController {
})
.firstOrFail();
const validStates = ['approved'];
const validStates = ['approved', 'rejected_to_reviewer'];
if (!validStates.includes(dataset.server_state)) {
// session.flash('errors', 'Invalid server state!');
return response
@ -254,12 +312,12 @@ export default class DatasetsController {
throw error;
}
const validStates = ['approved'];
const validStates = ['approved', 'rejected_to_reviewer'];
if (!validStates.includes(dataset.server_state)) {
// throw new Error('Invalid server state!');
// return response.flash('warning', 'Invalid server state. Dataset cannot be released to editor').redirect().back();
return response
.flash(
.flash(
`Invalid server state. Dataset with id ${id} cannot be rejected. Datset has server state ${dataset.server_state}.`,
'warning',
)
@ -311,4 +369,17 @@ export default class DatasetsController {
.toRoute('reviewer.dataset.list')
.flash(`You have rejected dataset ${dataset.id}! to editor ${dataset.editor.login}`, 'message');
}
public async download({ params, response }: HttpContext) {
const id = params.id;
// Find the file by ID
const file = await File.findOrFail(id);
// const filePath = await drive.use('local').getUrl('/'+ file.filePath)
const filePath = file.filePath;
const fileExt = file.filePath.split('.').pop() || '';
// Set the response headers and download the file
response.header('Content-Type', file.mime_type || 'application/octet-stream');
response.attachment(`${file.label}.${fileExt}`);
return response.download(filePath);
}
}

View file

@ -21,6 +21,7 @@ export enum ServerStates {
rejected_reviewer = 'rejected_reviewer',
rejected_editor = 'rejected_editor',
reviewed = 'reviewed',
rejected_to_reviewer = 'rejected_to_reviewer',
}
// for table dataset_titles

View file

@ -86,3 +86,22 @@ export default class Documents extends BaseSchema {
// CONSTRAINT documents_server_state_check CHECK (server_state::text = ANY (ARRAY['deleted'::character varying::text, 'inprogress'::character varying::text, 'published'::character varying::text, 'released'::character varying::text, 'editor_accepted'::character varying::text, 'approved'::character varying::text, 'rejected_reviewer'::character varying::text, 'rejected_editor'::character varying::text, 'reviewed'::character varying::text])),
// CONSTRAINT documents_type_check CHECK (type::text = ANY (ARRAY['analysisdata'::character varying::text, 'measurementdata'::character varying::text, 'monitoring'::character varying::text, 'remotesensing'::character varying::text, 'gis'::character varying::text, 'models'::character varying::text, 'mixedtype'::character varying::text]))
// )
// ALTER TABLE documents DROP CONSTRAINT documents_server_state_check;
// ALTER TABLE documents
// ADD CONSTRAINT documents_server_state_check CHECK (
// server_state::text = ANY (ARRAY[
// 'deleted',
// 'inprogress',
// 'published',
// 'released',
// 'editor_accepted',
// 'approved',
// 'rejected_reviewer',
// 'rejected_editor',
// 'reviewed',
// 'rejected_to_reviewer' -- new value added
// ]::text[])
// );

777
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -58,7 +58,7 @@
"eslint-plugin-prettier": "^5.0.0-alpha.2",
"hot-hook": "^0.4.0",
"numeral": "^2.0.6",
"pinia": "^2.0.30",
"pinia": "^3.0.2",
"pino-pretty": "^13.0.0",
"postcss-loader": "^8.1.1",
"prettier": "^3.4.2",
@ -116,7 +116,7 @@
"notiwind": "^2.0.0",
"pg": "^8.9.0",
"qrcode": "^1.5.3",
"redis": "^4.6.10",
"redis": "^5.0.0",
"reflect-metadata": "^0.2.1",
"saxon-js": "^2.5.0",
"toastify-js": "^1.12.0",

View file

@ -0,0 +1,75 @@
<script setup lang="ts">
import { computed, useSlots } from 'vue';
const props = defineProps({
title: {
type: String,
default: null,
},
icon: {
type: String,
default: null,
},
showHeaderIcon: {
type: Boolean,
default: true,
},
headerIcon: {
type: String,
default: null,
},
rounded: {
type: String,
default: 'rounded-xl',
},
hasFormData: Boolean,
empty: Boolean,
form: Boolean,
hoverable: Boolean,
modal: Boolean,
});
const emit = defineEmits(['header-icon-click', 'submit']);
const is = computed(() => (props.form ? 'form' : 'div'));
const slots = useSlots();
// const footer = computed(() => slots.footer && !!slots.footer());
const componentClass = computed(() => {
const base = [props.rounded, props.modal ? 'dark:bg-slate-900' : 'dark:bg-slate-900/70'];
if (props.hoverable) {
base.push('hover:shadow-lg transition-shadow duration-500');
}
return base;
});
// const headerIconClick = () => {
// emit('header-icon-click');
// };
// const submit = (e) => {
// emit('submit', e);
// };
</script>
<template>
<component :is="is" :class="componentClass" class="bg-white flex flex-col border border-gray-100 dark:border-slate-800 mb-4">
<div v-if="empty" class="text-center py-24 text-gray-500 dark:text-slate-400">
<p>Nothing's here</p>
</div>
<div v-else class="flex-1" :class="[!hasFormData && 'p-6']">
<slot />
</div>
</component>
</template>

View file

@ -0,0 +1,74 @@
<script setup lang="ts">
import { computed } from 'vue';
import { mdiLicense } from '@mdi/js';
const props = defineProps({
path: {
type: String,
required: true
},
size: {
type: Number,
default: 24
},
viewBox: {
type: String,
default: '0 0 24 24'
},
color: {
type: String,
default: 'currentColor'
},
className: {
type: String,
default: ''
}
});
// Define all the SVG paths we need
const svgPaths = {
// Document/File icons
document: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
documentPlus: 'M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
// Communication icons
email: 'M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z',
// Identity/User icons
idCard: '10 2a1 1 0 00-1 1v1a1 1 0 002 0V3a1 1 0 00-1-1zM4 4h3a3 3 0 006 0h3a2 2 0 012 2v9a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2zm2.5 7a1.5 1.5 0 100-3 1.5 1.5 0 000 3zm2.45 4a2.5 2.5 0 10-4.9 0h4.9zM12 9a1 1 0 100 2h3a1 1 0 100-2h-3zm-1 4a1 1 0 011-1h2a1 1 0 110 2h-2a1 1 0 01-1-1z',
// Language/Translation icons
// language: 'M7 2a1 1 0 011 1v1h3a1 1 0 110 2H9.578a18.87 18.87 0 01-1.724 4.78c.29.354.596.696.914 1.026a1 1 0 11-1.44 1.389c-.188-.196-.373-.396-.554-.6a19.098 19.098 0 01-3.107 3.567 1 1 0 01-1.334-1.49 17.087 17.087 0 003.13-3.733 18.992 18.992 0 01-1.487-2.494 1 1 0 111.79-.89c.234.47.489.928.764 1.372.417-.934.752-1.913.997-2.927H3a1 1 0 110-2h3V3a1 1 0 011-1zm6 6a1 1 0 01.894.553l2.991 5.982a.869.869 0 01.02.037l.99 1.98a1 1 0 11-1.79.895L15.383 16h-4.764l-.724 1.447a1 1 0 11-1.788-.894l.99-1.98.019-.038 2.99-5.982A1 1 0 0113 8zm-1.382 6h2.764L13 11.236 11.618 14z',
language: 'M12 2a10 10 0 1 0 0 20a10 10 0 1 0 0-20zm0 0c2.5 0 4.5 4.5 4.5 10s-2 10-4.5 10-4.5-4.5-4.5-10 2-10 4.5-10zm0 0a10 10 0 0 1 0 20a10 10 0 0 1 0-20z',
// License/Legal icons
// license: 'M10 2a1 1 0 00-1 1v1.323l-3.954 1.582A1 1 0 004 6.32V16a1 1 0 001.555.832l3-1.2a1 1 0 01.8 0l3 1.2a1 1 0 001.555-.832V6.32a1 1 0 00-1.046-.894L9 4.877V3a1 1 0 00-1-1zm0 14.5a.5.5 0 01-.5-.5v-4a.5.5 0 011 0v4a.5.5 0 01-.5.5zm1.5-10.5a.5.5 0 11-1 0 .5.5 0 011 0z',
license: mdiLicense,
// Building/Organization icons
building: 'M4 4a2 2 0 012-2h8a2 2 0 012 2v12a1 1 0 110 2h-3a1 1 0 01-1-1v-2a1 1 0 00-1-1H9a1 1 0 00-1 1v2a1 1 0 01-1 1H4a1 1 0 110-2V4zm3 1h2v2H7V5zm2 4H7v2h2V9zm2-4h2v2h-2V5zm2 4h-2v2h2V9z',
// Book/Publication icons
book: 'M9 4.804A7.968 7.968 0 005.5 4c-1.255 0-2.443.29-3.5.804v10A7.969 7.969 0 015.5 14c1.669 0 3.218.51 4.5 1.385A7.962 7.962 0 0114.5 14c1.255 0 2.443.29 3.5.804v-10A7.968 7.968 0 0014.5 4c-1.255 0-2.443.29-3.5.804V12a1 1 0 11-2 0V4.804z',
// Download icon
download: 'M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4'
};
const pathData = computed(() => {
return svgPaths[props.path] || props.path;
});
const sizeStyle = computed(() => {
return {
width: `${props.size}px`,
height: `${props.size}px`
};
});
</script>
<template>
<svg :style="sizeStyle" :class="className" :viewBox="viewBox" xmlns="http://www.w3.org/2000/svg" fill="none"
:stroke="color" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path :d="pathData" />
</svg>
</template>

View file

@ -0,0 +1,124 @@
<script setup>
import { onMounted, ref, watch } from 'vue';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
const DEFAULT_BASE_LAYER_NAME = 'BaseLayer';
const DEFAULT_BASE_LAYER_ATTRIBUTION = '&copy; <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors';
const props = defineProps({
coverage: {
type: Object,
required: true
},
height: {
type: String,
default: '250px'
},
mapId: {
type: String,
default: 'view-map'
}
});
const map = ref(null);
const mapContainer = ref(null);
onMounted(() => {
initializeMap();
});
watch(() => props.coverage, (newCoverage) => {
if (map.value && newCoverage) {
updateBounds();
}
}, { deep: true });
const initializeMap = () => {
// Create the map with minimal controls
map.value = L.map(mapContainer.value, {
zoomControl: false,
attributionControl: false,
dragging: false,
scrollWheelZoom: false,
doubleClickZoom: false,
boxZoom: false,
tap: false,
keyboard: false,
touchZoom: false
});
// // Add a simple tile layer (OpenStreetMap)
let osmGgray = new L.TileLayer.WMS('https://ows.terrestris.de/osm-gray/service', {
format: 'image/png',
attribution: DEFAULT_BASE_LAYER_ATTRIBUTION,
layers: 'OSM-WMS',
});
let layerOptions = {
label: DEFAULT_BASE_LAYER_NAME,
visible: true,
layer: osmGgray,
};
layerOptions.layer.addTo(map.value);
// Add a light-colored rectangle for the coverage area
updateBounds();
};
const updateBounds = () => {
if (!props.coverage || !map.value) return;
// Clear any existing layers except the base tile layer
map.value.eachLayer(layer => {
if (layer instanceof L.Rectangle) {
map.value.removeLayer(layer);
}
});
// Create bounds from the coverage coordinates
const bounds = L.latLngBounds(
[props.coverage.y_min, props.coverage.x_min],
[props.coverage.y_max, props.coverage.x_max]
);
// Add a rectangle with emerald styling
L.rectangle(bounds, {
color: '#10b981', // emerald-500
weight: 2,
fillColor: '#d1fae5', // emerald-100
fillOpacity: 0.5
}).addTo(map.value);
// Fit the map to the bounds with some padding
map.value.fitBounds(bounds, {
padding: [20, 20]
});
};
</script>
<template>
<div class="map-container bg-emerald-50 dark:bg-emerald-900/30
rounded-lg shadow-sm overflow-hidden">
<div :id="mapId" ref="mapContainer" :style="{ height: height }" class="w-full"></div>
</div>
</template>
<style scoped>
/* Ensure the Leaflet container has proper styling */
:deep(.leaflet-container) {
background-color: #f0fdf4;
/* emerald-50 */
}
/* Dark mode adjustments */
@media (prefers-color-scheme: dark) {
:deep(.leaflet-container) {
background-color: rgba(6, 78, 59, 0.3);
/* emerald-900/30 */
}
:deep(.leaflet-tile) {
filter: brightness(0.8) contrast(1.2) grayscale(0.3);
}
}
</style>

View file

@ -227,7 +227,11 @@ const formatServerState = (state: string) => {
color="info" :icon="mdiLibraryShelves" :label="'Classify'" small
class="col-span-1">
</BaseButton>
<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" />
<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

View file

@ -10,7 +10,7 @@ import FormControl from '@/Components/FormControl.vue';
import BaseButton from '@/Components/BaseButton.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
import { mdiArrowLeftBoldOutline, mdiReiterate } from '@mdi/js';
import { mdiArrowLeftBoldOutline, mdiReiterate, mdiBookEdit, mdiUndo } from '@mdi/js';
import FormValidationErrors from '@/Components/FormValidationErrors.vue';
const props = defineProps({
@ -18,6 +18,10 @@ const props = defineProps({
type: Object,
default: () => ({}),
},
can: {
type: Object,
default: () => ({}),
},
});
const flash: Ref<any> = computed(() => {
@ -93,7 +97,11 @@ const handleSubmit = async (e) => {
</p>
<BaseButtons>
<BaseButton type="submit" color="info" label="Set published"
:class="{ 'opacity-25': form.processing }" :disabled="form.processing" />
:class="{ 'opacity-25': form.processing }" :disabled="form.processing" :icon="mdiBookEdit" small />
<BaseButton v-if="can.reject && (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" />
</BaseButtons>
</template>
</CardBox>

View file

@ -0,0 +1,117 @@
<script setup lang="ts">
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import SectionMain from '@/Components/SectionMain.vue';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
import { useForm, Head, usePage } from '@inertiajs/vue3';
import { computed, Ref } from 'vue';
import CardBox from '@/Components/CardBox.vue';
import FormField from '@/Components/FormField.vue';
import FormControl from '@/Components/FormControl.vue';
import BaseButton from '@/Components/BaseButton.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
import { mdiArrowLeftBoldOutline, mdiReiterate } from '@mdi/js';
import FormValidationErrors from '@/Components/FormValidationErrors.vue';
const props = defineProps({
dataset: {
type: Object,
default: () => ({}),
},
});
// Define the computed property for the label
const computedLabel = computed(() => {
return `Reject to reviewer: ${props.dataset.reviewer?.login || 'Unknown User'}`;
});
const computedEmailLabel = computed(() => {
return props.dataset.reviewer?.email || '';
});
const flash: Ref<any> = computed(() => {
return usePage().props.flash;
});
const errors: Ref<any> = computed(() => {
return usePage().props.errors;
});
const form = useForm({
server_state: 'rejected_to_reviewer',
reject_editor_note: '',
send_email: false,
});
const handleSubmit = async (e: SubmitEvent) => {
e.preventDefault();
await form.put(stardust.route('editor.dataset.rejectToReviewerUpdate', [props.dataset.id]));
// await form.put(stardust.route('editor.dataset.update', [props.dataset.id]));
};
</script>
<template>
<LayoutAuthenticated>
<Head title="Reject reviewed dataset" />
<SectionMain>
<SectionTitleLineWithButton :icon="mdiReiterate" title="Reject reviewed dataset to reviewer" main>
<BaseButton :route-name="stardust.route('editor.dataset.list')" :icon="mdiArrowLeftBoldOutline"
label="Back" color="white" rounded-full small />
</SectionTitleLineWithButton>
<CardBox form @submit.prevent="handleSubmit">
<FormValidationErrors v-bind:errors="errors" />
<FormField label="server state" :class="{ 'text-red-400': form.errors.server_state }">
<FormControl v-model="form.server_state" type="text" placeholder="-- server state --"
:is-read-only="true" :error="form.errors.server_state">
<div class="text-red-400 text-sm" v-if="form.errors.server_state">
{{ form.errors.server_state }}
</div>
</FormControl>
</FormField>
<FormField label="reject note" :class="{ 'text-red-400': form.errors.reject_editor_note }">
<FormControl v-model="form.reject_editor_note" type="textarea"
placeholder="-- reject note for reviewer --" :error="form.errors.reject_editor_note">
<div class="text-red-400 text-sm" v-if="form.errors.reject_editor_note">
{{ form.errors.reject_editor_note }}
</div>
</FormControl>
</FormField>
<!-- <FormControl
type="checkbox"
v-model="form.send_email"
:error="form.errors.send_email">
</FormControl> -->
<FormField label="Email Notification">
<label for="send_email" class="flex items-center mr-6 mb-3">
<input type="checkbox" id="send_email" v-model="form.send_email" class="mr-2" />
<span class="check"></span>
<a class="pl-2 " target="_blank">send email to reviewer
<span class="text-blue-600 hover:underline">
{{ computedEmailLabel }}
</span>
</a>
</label>
</FormField>
<div v-if="flash && flash.warning" class="flex flex-col mt-6 animate-fade-in">
<div class="bg-yellow-500 border-l-4 border-orange-400 text-white p-4" role="alert">
<p class="font-bold">Be Warned</p>
<p>{{ flash.warning }}</p>
</div>
</div>
<template #footer>
<BaseButtons>
<BaseButton type="submit" color="info" :label="computedLabel"
:class="{ 'opacity-25': form.processing }" :disabled="form.processing" />
</BaseButtons>
</template>
</CardBox>
</SectionMain>
</LayoutAuthenticated>
</template>

View file

@ -51,6 +51,8 @@ const getRowClass = (dataset) => {
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 = '';
}
@ -131,6 +133,21 @@ const formatServerState = (state: string) => {
</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_to_reviewer' && dataset.reject_editor_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_editor_note }}
</p>
<div class="absolute -top-1 left-1 w-2 h-2 bg-white transform rotate-45">
</div>
</div>
</div>
</td>
@ -146,12 +163,13 @@ const formatServerState = (state: string) => {
<td
class="py-4 whitespace-nowrap text-right text-sm font-medium text-gray-700">
<BaseButtons type="justify-start lg:justify-end" no-wrap>
<BaseButton v-if="can.review && (dataset.server_state == 'approved')"
<BaseButton
v-if="can.reject && (dataset.server_state == 'approved' || dataset.server_state == 'rejected_to_reviewer')"
:route-name="stardust.route('reviewer.dataset.review', [dataset.id])"
color="info" :icon="mdiGlasses" :label="'View'" small />
<BaseButton
v-if="can.reject && (dataset.server_state == 'approved')"
v-if="can.reject && (dataset.server_state == 'approved' || dataset.server_state == 'rejected_to_reviewer')"
:route-name="stardust.route('reviewer.dataset.reject', [dataset.id])"
color="info" :icon="mdiReiterate" :label="'Reject'" small />
</BaseButtons>

View file

@ -10,17 +10,20 @@ import BaseButtons from '@/Components/BaseButtons.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
import { mdiArrowLeftBoldOutline, mdiGlasses } from '@mdi/js';
import FormValidationErrors from '@/Components/FormValidationErrors.vue';
import { mdiReiterate } from '@mdi/js';
import { mdiReiterate, mdiBookOpenPageVariant, mdiFinance } from '@mdi/js';
import MapComponentView from '@/Components/Map/MapComponentView.vue';
import IconSvg from '@/Components/Icons/IconSvg.vue';
import CardBoxSimple from '@/Components/CardBoxSimple.vue';
const props = defineProps({
dataset: {
type: Object,
default: () => ({}),
},
fields: {
type: Object,
required: true,
},
// fields: {
// type: Object,
// required: true,
// },
can: {
type: Object,
default: () => ({}),
@ -34,18 +37,6 @@ const errors: Ref<any> = computed(() => {
return usePage().props.errors;
});
// const form = useForm({
// preferred_reviewer: '',
// preferred_reviewer_email: '',
// preferation: 'yes_preferation',
// // preferation: '',
// // isPreferationRequired: false,
// });
// const isPreferationRequired = computed(() => form.preferation === 'yes_preferation');
const handleSubmit = async (e) => {
e.preventDefault();
@ -53,6 +44,19 @@ const handleSubmit = async (e) => {
// await form.put(stardust.route('dataset.releaseUpdate', [props.dataset.id]));
// // await form.put(stardust.route('editor.dataset.update', [props.dataset.id]));
};
const getFileSize = (file: File) => {
if (file.size > 1024) {
if (file.size > 1048576) {
return Math.round(file.size / 1048576) + 'mb';
} else {
return Math.round(file.size / 1024) + 'kb';
}
} else {
return file.size + 'b';
}
}
</script>
<template>
@ -64,7 +68,7 @@ const handleSubmit = async (e) => {
<BaseButton :route-name="stardust.route('reviewer.dataset.list')" :icon="mdiArrowLeftBoldOutline"
label="Back" color="white" rounded-full small />
</SectionTitleLineWithButton>
<CardBox form @submit.prevent="handleSubmit">
<component is="form" form @submit.prevent="handleSubmit">
<FormValidationErrors v-bind:errors="errors" />
<div v-if="flash && flash.warning" class="flex flex-col mt-6 animate-fade-in">
@ -74,26 +78,546 @@ const handleSubmit = async (e) => {
</div>
</div>
<div class="flex flex-col">
<div class="flex flex-row items-center justify-between dark:bg-slate-900 bg-gray-200 p-2 mb-2"
v-for="(fieldValue, field) in fields" :key="field">
<label :for="field" class="font-bold h-6 mt-3 text-xs leading-8 uppercase">{{ field }}</label>
<span class="text-sm text-gray-600" v-html="fieldValue"></span>
<!-- <div class="mb-4"> -->
<CardBoxSimple>
<div class="mb-6">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Language</h4>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm">
<div class="flex items-center">
<IconSvg path="language" :size="20"
className="mr-2 text-emerald-600 dark:text-emerald-400" />
<span class="text-emerald-700 dark:text-emerald-300 font-medium">
{{ dataset.language || 'Not specified' }}
</span>
</div>
</div>
</div>
</div>
<template #footer>
<BaseButtons>
<!-- <BaseButton type="submit" color="info" label="Receive"
<!-- Licenses -->
<div class="mb-6">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Licenses</h4>
<div v-if="dataset.licenses && dataset.licenses.length > 0" class="space-y-2">
<div v-for="license in dataset.licenses" :key="license.id" class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm hover:bg-emerald-100 dark:hover:bg-emerald-900/50 transition-colors">
<div class="flex items-center">
<IconSvg path="license" :size="20"
className="mr-2 text-emerald-600 dark:text-emerald-400" />
<span class="text-emerald-700 dark:text-emerald-300">
{{ license.name }}
</span>
</div>
</div>
</div>
<div v-else class="text-center py-4">
<p class="text-gray-500 dark:text-gray-400 italic">No licenses specified</p>
</div>
<div v-if="dataset.licenses && dataset.licenses.length > 0"
class="mt-3 text-xs text-gray-500 dark:text-gray-400">
Total licenses: {{ dataset.licenses.length }}
</div>
</div>
<div class="flex flex-col md:flex-row gap-4 mb-6">
<!-- (3) dataset_type -->
<div class="w-full mx-2 flex-1">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Dataset Type</h4>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm">
<div class="flex items-center">
<IconSvg path="book" :size="20"
className="mr-2 text-emerald-600 dark:text-emerald-400" />
<span class="text-emerald-700 dark:text-emerald-300 font-medium">
{{ dataset.type || 'Not specified' }}
</span>
</div>
</div>
</div>
<!-- (4) creating_corporation -->
<div class="w-full mx-2 flex-1">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Creating
Corporation
</h4>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm">
<div class="flex items-center">
<IconSvg path="building" :size="20"
className="mr-2 text-emerald-600 dark:text-emerald-400" />
<span class="text-emerald-700 dark:text-emerald-300 font-medium">
{{ dataset.creating_corporation || 'Not specified' }}
</span>
</div>
</div>
</div>
</div>
<div class="flex flex-col md:flex-row gap-4 mb-6">
<!-- (9) project_id -->
<div class="w-full mx-2 flex-1">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Project</h4>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm">
<span class="text-emerald-700 dark:text-emerald-300">
{{ dataset.project?.label || 'Not specified' }}
</span>
</div>
</div>
<!-- (10) embargo_date -->
<div class="w-full mx-2 flex-1">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Embargo Date</h4>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm">
<span v-if="dataset.embargo_date" class="text-emerald-700 dark:text-emerald-300">
{{ dataset.embargo_date }}
</span>
<span v-else class="text-gray-500 dark:text-gray-400 italic">
No embargo date set
</span>
</div>
</div>
</div>
</CardBoxSimple>
<!-- (5) titles -->
<CardBox class="mb-6 shadow" :has-form-data="false" title="Titles" :icon="mdiFinance"
:show-header-icon="false">
<div class="p-4">
<!-- Main Title (highlighted) -->
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Main Title</h4>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border-l-4 border border-emerald-300 dark:border-emerald-700
rounded-lg p-4 shadow-sm mb-4">
<div class="flex justify-between items-start mb-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-200
dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200">
{{ dataset.titles[0].language }}
</span>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-200
dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200">
Main
</span>
</div>
<h3
class="text-lg font-medium text-emerald-800 dark:text-emerald-200 whitespace-pre-line break-words overflow-wrap-anywhere">
{{ dataset.titles[0].value }}
</h3>
</div>
<!-- Additional titles -->
<div v-if="dataset.titles.length > 1">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Additional Titles
</h4>
<div class="grid gap-3">
<template v-for="(title, index) in dataset.titles" :key="index">
<div v-if="title.type != 'Main'"
class="bg-emerald-50/70 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm hover:bg-emerald-50 dark:hover:bg-emerald-900/30 transition-colors">
<div class="flex justify-between items-start mb-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
bg-emerald-100 dark:bg-emerald-800/70 text-emerald-700 dark:text-emerald-300">
{{ title.language }}
</span>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
bg-emerald-100 dark:bg-emerald-800/70 text-emerald-700 dark:text-emerald-300">
{{ title.type }}
</span>
</div>
<p
class="text-emerald-700 dark:text-emerald-300 font-medium whitespace-pre-line break-words overflow-wrap-anywhere">
{{ title.value }}
</p>
</div>
</template>
</div>
</div>
<div class="mt-3 text-xs text-gray-500 dark:text-gray-400">
Total titles: {{ dataset.titles.length }}
</div>
</div>
</CardBox>
<!-- (6) descriptions -->
<CardBox class="mb-6 shadow" :has-form-data="false" title="Descriptions" :icon="mdiFinance"
:show-header-icon="false">
<!-- Main Abstract (highlighted) -->
<div class="p-4">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Main Abstract</h4>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border-l-4 border border-emerald-300 dark:border-emerald-700
rounded-lg p-4 shadow-sm mb-4">
<div class="flex justify-between items-start mb-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-200
dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200">
{{ dataset.descriptions[0].language }}
</span>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-200
dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200">
Abstract
</span>
</div>
<div class="prose prose-emerald dark:prose-invert max-w-none">
<p
class="text-emerald-800 dark:text-emerald-200 whitespace-pre-line break-words overflow-wrap-anywhere">
{{ dataset.descriptions[0].value }}
</p>
</div>
</div>
<!-- Additional descriptions -->
<div v-if="dataset.descriptions.length > 1">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Additional
Descriptions</h4>
<div class="grid gap-3">
<div v-for="(item, index) in dataset.descriptions" :key="index">
<div v-if="item.type != 'Abstract'" class="bg-emerald-50/70 dark:bg-emerald-900/20 border border-emerald-200
dark:border-emerald-800
rounded-lg p-3 shadow-sm hover:bg-emerald-50 dark:hover:bg-emerald-900/30
transition-colors">
<div class="flex justify-between items-start mb-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
bg-emerald-100 dark:bg-emerald-800/70 text-emerald-700 dark:text-emerald-300">
{{ item.language }}
</span>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
bg-emerald-100 dark:bg-emerald-800/70 text-emerald-700 dark:text-emerald-300">
{{ item.type }}
</span>
</div>
<p
class="text-emerald-700 dark:text-emerald-300 text-sm whitespace-pre-line break-words overflow-wrap-anywhere">
{{ item.value }}
</p>
</div>
</div>
</div>
</div>
<div class="mt-3 text-xs text-gray-500 dark:text-gray-400">
Total descriptions: {{ dataset.descriptions.length }}
</div>
</div>
</CardBox>
<!-- (7) authors -->
<CardBox class="mb-6 shadow" has-table title="Creators" :icon="mdiBookOpenPageVariant"
:show-header-icon="false">
<div v-if="dataset.authors.length === 0" class="text-center py-6">
<p class="text-gray-500 dark:text-gray-400 italic">No authors defined</p>
</div>
<div v-else class="p-4">
<!-- <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Dataset Authors:</h4> -->
<div class="grid gap-3">
<div v-for="(author, index) in dataset.authors" :key="index" class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm hover:bg-emerald-100 dark:hover:bg-emerald-900/50 transition-colors">
<div class="flex flex-col md:flex-row md:items-center">
<div class="flex-1">
<p class="font-medium text-emerald-700 dark:text-emerald-300">
{{ author.academic_title }} {{ author.first_name }} {{ author.last_name
}}
</p>
<div class="mt-2 grid grid-cols-1 md:grid-cols-2 gap-2">
<div v-if="author.email" class="flex items-center text-sm">
<IconSvg path="email" :size="16"
className="mr-1 text-emerald-600 dark:text-emerald-400" />
<span class="text-emerald-600 dark:text-emerald-400">{{ author.email
}}</span>
</div>
<div v-if="author.identifier_orcid" class="flex items-center text-sm">
<IconSvg path="idCard" :size="16"
className="mr-1 text-emerald-600 dark:text-emerald-400">
</IconSvg>
<span class="text-emerald-600 dark:text-emerald-400">ORCID: {{
author.identifier_orcid
}}</span>
</div>
</div>
</div>
<div v-if="author.academic_title" class="mt-2 md:mt-0 md:ml-4">
<span class="bg-emerald-200 dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200
text-xs px-2 py-1 rounded-full">
{{ author.academic_title }}
</span>
</div>
</div>
</div>
</div>
<div v-if="dataset.authors.length > 0" class="mt-3 text-xs text-gray-500 dark:text-gray-400">
Total authors: {{ dataset.authors.length }}
</div>
</div>
</CardBox>
<!-- (8) contributors -->
<CardBox class="mb-6 shadow" has-table title="Contributors" :icon="mdiBookOpenPageVariant"
:show-header-icon="false">
<div v-if="dataset.contributors.length === 0" class="text-center py-6">
<p class="text-gray-500 dark:text-gray-400 italic">No contributors defined</p>
</div>
<div v-else class="p-4">
<!-- <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Dataset Contributors:
</h4> -->
<div class="grid gap-3">
<div v-for="(contributor, index) in dataset.contributors" :key="index" class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm hover:bg-emerald-100 dark:hover:bg-emerald-900/50 transition-colors">
<div class="flex flex-col md:flex-row md:items-center">
<div class="flex-1">
<div class="flex flex-wrap items-center gap-2">
<p class="font-medium text-emerald-700 dark:text-emerald-300">
{{ contributor.academic_title }} {{ contributor.first_name }} {{
contributor.last_name
}}
</p>
<span v-if="contributor.pivot_contributor_type" class="bg-emerald-200 dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200
text-xs px-2 py-1 rounded-full">
{{ contributor.pivot_contributor_type }}
</span>
</div>
<div class="mt-2 grid grid-cols-1 md:grid-cols-2 gap-2">
<div v-if="contributor.email" class="flex items-center text-sm">
<IconSvg path="email" :size="16"
className="mr-1 text-emerald-600 dark:text-emerald-400" />
<span class="text-emerald-600 dark:text-emerald-400">{{
contributor.email }}</span>
</div>
<div v-if="contributor.identifier_orcid" class="flex items-center text-sm">
<IconSvg path="idCard" :size="16"
className="mr-1 text-emerald-600 dark:text-emerald-400">
</IconSvg>
<span class="text-emerald-600 dark:text-emerald-400">ORCID: {{
contributor.identifier_orcid }}</span>
</div>
</div>
</div>
<div v-if="contributor.academic_title" class="mt-2 md:mt-0 md:ml-4">
<span class="bg-emerald-200 dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200
text-xs px-2 py-1 rounded-full">
{{ contributor.academic_title }}
</span>
</div>
</div>
</div>
</div>
<div v-if="dataset.contributors.length > 0"
class="mt-3 text-xs text-gray-500 dark:text-gray-400">
Total contributors: {{ dataset.contributors.length }}
</div>
</div>
</CardBox>
<!-- Map component -->
<CardBoxSimple>
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Geographic Coverage</h4>
<!-- Map container with emerald styling -->
<div class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg shadow-sm overflow-hidden">
<!-- The actual map component -->
<div class="h-64 rounded-md overflow-hidden">
<!-- Use the simplified map component -->
<MapComponentView v-if="dataset.coverage" :coverage="dataset.coverage" height="250px"
:mapId="'dataset-review-map'" />
</div>
<!-- Optional: Add a caption or description -->
<div class="mt-2 text-xs text-emerald-600 dark:text-emerald-400 text-center">
Geographic extent of the dataset
</div>
</div>
<!-- Coordinates display below the map -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 mt-3">
<!-- x min -->
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">X Min
(Longitude)</label>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-2.5 shadow-sm">
<span class="text-emerald-700 dark:text-emerald-300">
{{ dataset.coverage.x_min }}
</span>
</div>
</div>
<!-- x max -->
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">X Max
(Longitude)</label>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-2.5 shadow-sm">
<span class="text-emerald-700 dark:text-emerald-300">
{{ dataset.coverage.x_max }}
</span>
</div>
</div>
<!-- y min -->
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Y Min
(Latitude)</label>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-2.5 shadow-sm">
<span class="text-emerald-700 dark:text-emerald-300">
{{ dataset.coverage.y_min }}
</span>
</div>
</div>
<!-- y max -->
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Y Max
(Latitude)</label>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-2.5 shadow-sm">
<span class="text-emerald-700 dark:text-emerald-300">
{{ dataset.coverage.y_max }}
</span>
</div>
</div>
</div>
</CardBoxSimple>
<!-- References -->
<CardBoxSimple>
<div v-if="dataset.references.length === 0" class="text-center py-6">
<p class="text-gray-500 dark:text-gray-400 italic">No references added.</p>
</div>
<div v-else class="p-4">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Dataset References:
</h4>
<div class="grid gap-3">
<div v-for="(item, index) in dataset.references" :key="index" class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm hover:bg-emerald-100 dark:hover:bg-emerald-900/50 transition-colors">
<div class="flex flex-col md:flex-row md:items-center md:justify-between">
<div class="flex-1">
<p class="font-medium text-emerald-700 dark:text-emerald-300">{{ item.value
}}</p>
<p class="text-sm text-emerald-600 dark:text-emerald-400 mt-1">{{ item.label
}}</p>
</div>
<div class="flex mt-2 md:mt-0 space-x-2">
<span class="bg-emerald-200 dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200
text-xs px-2 py-1 rounded-full">
{{ item.type }}
</span>
<span class="bg-emerald-200 dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200
text-xs px-2 py-1 rounded-full">
{{ item.relation }}
</span>
</div>
</div>
</div>
</div>
<div v-if="dataset.references.length > 0" class="mt-3 text-xs text-gray-500 dark:text-gray-400">
Total references: {{ dataset.references.length }}
</div>
</div>
</CardBoxSimple>
<!-- Keywords -->
<CardBoxSimple>
<div v-if="dataset.subjects.length === 0" class="text-center py-6">
<p class="text-gray-500 dark:text-gray-400 italic">No keywords added.</p>
</div>
<div v-else class="p-4">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Keywords/Subjects:
</h4>
<div class="flex flex-wrap gap-2">
<div v-for="(subject, index) in dataset.subjects" :key="index" class="bg-emerald-50 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300
px-3 py-1.5 rounded-full text-sm font-medium border border-emerald-200
dark:border-emerald-800 shadow-sm hover:bg-emerald-100
dark:hover:bg-emerald-900/50 transition-colors">
<span>{{ subject.value }}</span>
<span class="ml-1 text-xs text-emerald-600 dark:text-emerald-400">({{ subject.type
}})</span>
</div>
</div>
<div v-if="dataset.subjects.length > 0" class="mt-3 text-xs text-gray-500 dark:text-gray-400">
Total keywords: {{ dataset.subjects.length }}
</div>
</div>
</CardBoxSimple>
<!-- download file list -->
<CardBoxSimple>
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Dataset Files</h4>
<div v-if="dataset.files && dataset.files.length > 0" class="space-y-2">
<div v-for="file in dataset.files" :key="file.id" class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm hover:bg-emerald-100 dark:hover:bg-emerald-900/50 transition-colors">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3 flex-1">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-emerald-600 dark:text-emerald-400"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-emerald-700 dark:text-emerald-300 truncate">
{{ file.label }}
</p>
<p class="text-xs text-emerald-600/70 dark:text-emerald-400/70 truncate">
{{ getFileSize(file) }}
</p>
</div>
</div>
<div class="ml-2 flex-shrink-0">
<a v-if="file.id != undefined"
:href="stardust.route('reviewer.file.download', [file.id])" class="inline-flex items-center px-3 py-1.5 border border-emerald-300 dark:border-emerald-700
text-xs font-medium rounded-full text-emerald-700 bg-emerald-100
dark:text-emerald-200 dark:bg-emerald-800/70 hover:bg-emerald-200
dark:hover:bg-emerald-800 transition-colors">
<IconSvg path="download" :size="20" className="mr-1" />
Download
</a>
</div>
</div>
</div>
</div>
<div v-else
class="text-center py-6 bg-emerald-50/50 dark:bg-emerald-900/10 rounded-lg border border-emerald-100 dark:border-emerald-900/30">
<svg xmlns="http://www.w3.org/2000/svg"
class="h-12 w-12 mx-auto text-emerald-300 dark:text-emerald-700" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<p class="mt-2 text-emerald-600 dark:text-emerald-400 italic">No files attached to this dataset
</p>
</div>
<div v-if="dataset.files && dataset.files.length > 0"
class="mt-3 text-xs text-gray-500 dark:text-gray-400">
Total files: {{ dataset.files.length }}
</div>
</CardBoxSimple>
<BaseButtons>
<!-- <BaseButton type="submit" color="info" label="Receive"
:class="{ 'opacity-25': router.processing }" :disabled="form.processing" /> -->
<BaseButton type="submit" color="info" label="Accept" />
<BaseButton v-if="can.reject && (dataset.server_state == 'approved')"
:route-name="stardust.route('reviewer.dataset.reject', [dataset.id])" color="info"
:icon="mdiReiterate" :label="'Reject'" />
</BaseButtons>
</template>
</CardBox>
<BaseButton v-if="can.reject && (dataset.server_state == 'approved' || dataset.server_state == 'rejected_to_reviewer')" type="submit" color="info" label="Accept" small />
<BaseButton
v-if="can.reject && (dataset.server_state == 'approved' || dataset.server_state == 'rejected_to_reviewer')"
:route-name="stardust.route('reviewer.dataset.reject', [dataset.id])" color="info"
:icon="mdiReiterate" :label="'Reject'" small />
</BaseButtons>
</component>
</SectionMain>
</LayoutAuthenticated>
</template>
<style scoped>
.break-words {
word-break: break-word;
}
.overflow-wrap-anywhere {
overflow-wrap: anywhere;
}
</style>

View file

@ -392,7 +392,16 @@ router
.as('editor.file.download')
.where('id', router.matchers.number())
.use([middleware.auth(), middleware.can(['dataset-editor-update'])]);
router
.get('dataset/:id/rejectToReviewer', [EditorDatasetController, 'rejectToReviewer'])
.as('editor.dataset.rejectToReviewer')
.where('id', router.matchers.number())
.use([middleware.auth(), middleware.can(['dataset-editor-reject'])]);
router
.put('dataset/:id/rejectToReviewer', [EditorDatasetController, 'rejectToReviewerUpdate'])
.as('editor.dataset.rejectToReviewerUpdate')
.where('id', router.matchers.number())
.use([middleware.auth(), middleware.can(['dataset-editor-reject'])]);
router
.get('dataset/:id/publish', [EditorDatasetController, 'publish'])
.as('editor.dataset.publish')
@ -437,6 +446,11 @@ router
.as('reviewer.dataset.reviewUpdate')
.where('id', router.matchers.number())
.use([middleware.auth(), middleware.can(['dataset-review'])]);
router
.get('/file/download/:id', [ReviewerDatasetController, 'download'])
.as('reviewer.file.download')
.where('id', router.matchers.number())
.use([middleware.auth(), middleware.can(['dataset-review'])]);
router
.get('dataset/:id/reject', [ReviewerDatasetController, 'reject'])
.as('reviewer.dataset.reject')