tethys.backend/resources/js/Pages/Editor/Dataset/Edit.vue
Arno Kaimbacher 5efddc2a58
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 6s
feat: add dataset change detection and form submission composables
- Implemented `useDatasetChangeDetection` for tracking unsaved changes in dataset forms, including comparisons for licenses, basic properties, files, coverage, and more.
- Added `useDatasetFormSubmission` for handling dataset form submissions with validation, success/error handling, and auto-save functionality.
2026-01-12 17:02:47 +01:00

1207 lines
61 KiB
Vue

<!-- 1. Add progress indicator at the top -->
<template>
<LayoutAuthenticated>
<Head title="Edit dataset" />
<!-- Progress Bar for Unsaved Changes -->
<div v-if="hasUnsavedChanges" class="fixed top-0 left-0 right-0 z-50 bg-amber-500 text-white px-4 py-2.5 text-sm shadow-lg h-14">
<div class="container mx-auto flex items-center justify-between h-full">
<div class="flex items-center gap-2">
<svg class="animate-pulse w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clip-rule="evenodd"
/>
</svg>
<span class="font-medium">You have unsaved changes</span>
</div>
<BaseButton @click.prevent="submit" label="Save Now" color="white" small :disabled="form.processing" />
</div>
</div>
<SectionMain>
<!-- Enhanced Title Section -->
<SectionTitleLineWithButton :icon="mdiImageText" title="Update Dataset" main>
<div class="flex items-center gap-3">
<!-- Dataset Status Badge -->
<BaseButton
:route-name="stardust.route('editor.dataset.list')"
:icon="mdiArrowLeftBoldOutline"
label="Back"
color="white"
rounded-full
small
/>
</div>
</SectionTitleLineWithButton>
<!-- Success/Error Notifications -->
<NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
{{ flash.message }}
</NotificationBar>
<FormValidationErrors v-bind:errors="errors" />
<UnsavedChangesWarning
:show="hasUnsavedChanges"
:changes-summary="getChangesSummary()"
:show-details="true"
:show-actions="false"
:show-auto-save-progress="true"
:auto-save-delay="30"
@save.prevent="submit"
/>
<!-- Main Form with Sections -->
<CardBox class="shadow-lg">
<!-- Collapsible Sections for Better Organization -->
<!-- Section 1: Basic Information -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<span class="w-8 h-8 rounded-full bg-blue-500 text-white flex items-center justify-center text-sm">1</span>
Basic Information
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 ml-10">
<!-- (1) Language -->
<FormField
label="Language *"
help="required: select dataset main language"
:class="{ 'text-red-400': form.errors.language }"
>
<FormControl
required
v-model="form.language"
type="select"
placeholder="[Enter Language]"
:errors="form.errors.language"
:options="{ de: 'de', en: 'en' }"
>
<div class="text-red-400 text-sm" v-if="form.errors.language">
{{ form.errors.language.join(', ') }}
</div>
</FormControl>
</FormField>
<!-- (3) dataset_type -->
<FormField label="Dataset Type *" help="required: dataset type" :class="{ 'text-red-400': form.errors.type }">
<FormControl
required
v-model="form.type"
:type="'select'"
placeholder="-- select type --"
:errors="form.errors.type"
:options="doctypes"
>
<div class="text-red-400 text-sm" v-if="form.errors.type && Array.isArray(form.errors.type)">
{{ form.errors.type.join(', ') }}
</div>
</FormControl>
</FormField>
<!-- (4) creating_corporation -->
<FormField
label="Creating Corporation *"
:class="{ 'text-red-400': form.errors.creating_corporation }"
class="w-full mx-2 flex-1"
>
<FormControl
required
v-model="form.creating_corporation"
type="text"
placeholder="[enter creating corporation]"
:is-read-only="true"
>
<div
class="text-red-400 text-sm"
v-if="form.errors.creating_corporation && Array.isArray(form.errors.creating_corporation)"
>
{{ form.errors.creating_corporation.join(', ') }}
</div>
</FormControl>
</FormField>
</div>
</div>
<BaseDivider />
<!-- Section 2: Licenses -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<span class="w-8 h-8 rounded-full bg-green-500 text-white flex items-center justify-center text-sm">2</span>
Licenses
</h2>
<div class="ml-10">
<!-- (2) licenses -->
<FormField label="Select License" wrap-body :class="{ 'text-red-400': form.errors.licenses }">
<FormCheckRadioGroup type="radio" v-model="form.licenses" name="licenses" is-column :options="licenses" />
</FormField>
</div>
</div>
<BaseDivider />
<!-- Section 3: Titles -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<span class="w-8 h-8 rounded-full bg-purple-500 text-white flex items-center justify-center text-sm">3</span>
Titles
</h2>
<!-- (5) titles -->
<CardBox
class="ml-10 shadow-md"
title="Main & Additional Titles"
:icon="mdiFinance"
:header-icon="mdiPlusCircle"
@header-icon-click="addTitle()"
>
<div class="flex flex-col md:flex-row">
<FormField
label="Main Title *"
help="required: main title"
:class="{ 'text-red-400': form.errors['titles.0.value'] }"
class="w-full mr-1 flex-1"
>
<FormControl
required
v-model="form.titles[0].value"
type="textarea"
placeholder="[enter main title]"
:show-char-count="true"
:max-input-length="255"
>
<div
class="text-red-400 text-sm"
v-if="form.errors['titles.0.value'] && Array.isArray(form.errors['titles.0.value'])"
>
{{ form.errors['titles.0.value'].join(', ') }}
</div>
</FormControl>
</FormField>
<FormField
label="Main Title Language*"
help="required: main title language"
:class="{ 'text-red-400': form.errors['titles.0.language'] }"
class="w-full ml-1 flex-1"
>
<FormControl required v-model="form.titles[0].language" type="text" :is-read-only="true">
<div
class="text-red-400 text-sm"
v-if="form.errors['titles.0.language'] && Array.isArray(form.errors['titles.0.language'])"
>
{{ form.errors['titles.0.language'].join(', ') }}
</div>
</FormControl>
</FormField>
</div>
<label v-if="form.titles.length > 1">additional titles </label>
<!-- <BaseButton :icon="mdiPlusCircle" @click.prevent="addTitle()" color="modern" rounded-full small /> -->
<table>
<thead>
<tr>
<!-- <th v-if="checkable" /> -->
<th>Title Value</th>
<th>Title Type</th>
<th>Title Language</th>
<th />
</tr>
</thead>
<tbody>
<template v-for="(title, index) in form.titles" :key="index">
<tr v-if="title.type != 'Main'">
<!-- <td scope="row">{{ index + 1 }}</td> -->
<td data-label="Title Value">
<FormControl
required
v-model="form.titles[index].value"
type="textarea"
placeholder="[enter main title]"
>
<div class="text-red-400 text-sm" v-if="form.errors[`titles.${index}.value`]">
{{ form.errors[`titles.${index}.value`].join(', ') }}
</div>
</FormControl>
</td>
<td data-label="Title Type">
<FormControl
required
v-model="form.titles[index].type"
type="select"
:options="titletypes"
placeholder="[select title type]"
>
<div class="text-red-400 text-sm" v-if="Array.isArray(form.errors[`titles.${index}.type`])">
{{ form.errors[`titles.${index}.type`].join(', ') }}
</div>
</FormControl>
</td>
<td data-label="Title Language">
<FormControl
required
v-model="form.titles[index].language"
type="select"
:options="{ de: 'de', en: 'en' }"
placeholder="[select title language]"
>
<div class="text-red-400 text-sm" v-if="form.errors[`titles.${index}.language`]">
{{ form.errors[`titles.${index}.language`].join(', ') }}
</div>
</FormControl>
</td>
<td class="before:hidden lg:w-1 whitespace-nowrap">
<BaseButtons type="justify-start lg:justify-end" no-wrap>
<!-- <BaseButton color="info" :icon="mdiEye" small @click="isModalActive = true" /> -->
<BaseButton
color="danger"
:icon="mdiTrashCan"
small
v-if="title.id == undefined"
@click.prevent="removeTitle(index)"
/>
</BaseButtons>
</td>
</tr>
</template>
</tbody>
</table>
</CardBox>
</div>
<BaseDivider />
<!-- Section 4: Descriptions -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<span class="w-8 h-8 rounded-full bg-orange-500 text-white flex items-center justify-center text-sm">4</span>
Descriptions
</h2>
<!-- (6) descriptions -->
<CardBox
class="ml-10 shadow-md"
title="Abstract & Additional Descriptions"
:icon="mdiFinance"
:header-icon="mdiPlusCircle"
@header-icon-click="addDescription()"
>
<div class="flex flex-col md:flex-row">
<FormField
label="Main Abstract *"
help="required: main abstract"
:class="{ 'text-red-400': form.errors['descriptions.0.value'] }"
class="w-full mr-1 flex-1"
>
<FormControl
required
v-model="form.descriptions[0].value"
type="textarea"
placeholder="[enter main abstract]"
:show-char-count="true"
:max-input-length="2500"
>
<div
class="text-red-400 text-sm"
v-if="form.errors['descriptions.0.value'] && Array.isArray(form.errors['descriptions.0.value'])"
>
{{ form.errors['descriptions.0.value'].join(', ') }}
</div>
</FormControl>
</FormField>
<FormField
label="Main Description Language*"
help="required: main abstract language"
:class="{ 'text-red-400': form.errors['descriptions.0.language'] }"
class="w-full ml-1 flex-1"
>
<FormControl required v-model="form.descriptions[0].language" type="text" :is-read-only="true">
<div
class="text-red-400 text-sm"
v-if="form.errors['descriptions.0.value'] && Array.isArray(form.errors['descriptions.0.language'])"
>
{{ form.errors['descriptions.0.language'].join(', ') }}
</div>
</FormControl>
</FormField>
</div>
<table>
<thead>
<tr>
<!-- <th v-if="checkable" /> -->
<th>Title Value</th>
<th>Title Type</th>
<th>Title Language</th>
<th />
</tr>
</thead>
<tbody>
<template v-for="(item, index) in form.descriptions" :key="index">
<tr v-if="item.type != 'Abstract'">
<!-- <td scope="row">{{ index + 1 }}</td> -->
<td data-label="Description Value">
<FormControl
required
v-model="form.descriptions[index].value"
type="textarea"
placeholder="[enter main title]"
>
<div class="text-red-400 text-sm" v-if="form.errors[`descriptions.${index}.value`]">
{{ form.errors[`descriptions.${index}.value`].join(', ') }}
</div>
</FormControl>
</td>
<td data-label="Description Type">
<FormControl
required
v-model="form.descriptions[index].type"
type="select"
:options="descriptiontypes"
placeholder="[select title type]"
>
<div
class="text-red-400 text-sm"
v-if="Array.isArray(form.errors[`descriptions.${index}.type`])"
>
{{ form.errors[`descriptions.${index}.type`].join(', ') }}
</div>
</FormControl>
</td>
<td data-label="Description Language">
<FormControl
required
v-model="form.descriptions[index].language"
type="select"
:options="{ de: 'de', en: 'en' }"
placeholder="[select title language]"
>
<div class="text-red-400 text-sm" v-if="form.errors[`descriptions.${index}.language`]">
{{ form.errors[`descriptions.${index}.language`].join(', ') }}
</div>
</FormControl>
</td>
<td class="before:hidden lg:w-1 whitespace-nowrap">
<BaseButtons type="justify-start lg:justify-end" no-wrap>
<!-- <BaseButton color="info" :icon="mdiEye" small @click="isModalActive = true" /> -->
<BaseButton
color="danger"
:icon="mdiTrashCan"
small
v-if="item.id == undefined"
@click.prevent="removeDescription(index)"
/>
</BaseButtons>
</td>
</tr>
</template>
</tbody>
</table>
</CardBox>
</div>
<BaseDivider />
<!-- Section 5: People (Authors & Contributors) -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<span class="w-8 h-8 rounded-full bg-pink-500 text-white flex items-center justify-center text-sm">5</span>
Creators & Contributors
</h2>
<div class="ml-10 space-y-6">
<!-- (7) authors -->
<CardBox
class="shadow-md"
has-table
title="Creators"
:icon="mdiBookOpenPageVariant"
:header-icon="mdiPlusCircle"
v-on:header-icon-click="addNewAuthor()"
>
<div
class="mb-4 p-3 bg-blue-50 dark:bg-slate-800 rounded-lg text-sm text-blue-800 dark:text-blue-300 flex items-start gap-2"
>
<BaseIcon :path="mdiAlertBoxOutline" class="flex-shrink-0 mt-0.5" />
<span>Search for existing persons or add new ones manually. Creators are displayed in citation order.</span>
</div>
<div class="mb-2 text-gray-600 text-sm flex items-center gap-2">
<span>Add creators by searching existing persons or manually adding new ones.</span>
<BaseIcon
:path="mdiAlertBoxOutline"
class="text-blue-400"
title="You can add existing persons via search, or create new ones manually."
/>
</div>
<div class="flex flex-col md:flex-row gap-4 mb-4">
<div class="flex-1">
<!-- <label class="block text-xs font-semibold mb-1">Find existing person</label> -->
<SearchAutocomplete
source="/api/persons"
:response-property="'first_name'"
placeholder="Search for existing persons..."
v-on:person="onAddAuthor"
/>
</div>
<!-- <div class="flex-1 flex items-end">
<BaseButton
:icon="mdiPlusCircle"
@click.prevent="addNewAuthor"
color="modern"
rounded-full
small
title="Add new creator manually"
/>
</div> -->
</div>
<TablePersons
:persons="form.authors"
v-if="form.authors.length > 0"
:errors="form.errors"
:relation="'authors'"
/>
<div class="text-red-400 text-sm" v-if="form.errors.authors && Array.isArray(form.errors.authors)">
{{ form.errors.authors.join(', ') }}
</div>
</CardBox>
<!-- (8) contributors -->
<CardBox
class="shadow-md"
has-table
title="Contributors"
:icon="mdiBookOpenPageVariant"
:header-icon="mdiPlusCircle"
v-on:header-icon-click="addNewContributor()"
>
<div class="mb-2 text-gray-600 text-sm flex items-center gap-2">
<span>Add contributors by searching existing persons or manually adding new ones.</span>
<BaseIcon
:path="mdiAlertBoxOutline"
class="text-blue-400"
title="You can add existing persons via search, or create new ones manually."
/>
</div>
<SearchAutocomplete
source="/api/persons"
:response-property="'first_name'"
placeholder="search in person table...."
v-on:person="onAddContributor"
>
</SearchAutocomplete>
<TablePersons
:persons="form.contributors"
v-if="form.contributors.length > 0"
:contributortypes="contributorTypes"
:errors="form.errors"
:relation="'contributors'"
/>
<div class="text-red-400 text-sm" v-if="form.errors.contributors && Array.isArray(form.errors.contributors)">
{{ form.errors.contributors.join(', ') }}
</div>
</CardBox>
</div>
</div>
<BaseDivider />
<!-- Section 6: Additional Metadata -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<span class="w-8 h-8 rounded-full bg-indigo-500 text-white flex items-center justify-center text-sm">6</span>
Additional Metadata
</h2>
<div class="ml-10 space-y-6">
<!-- Project & Embargo -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- (9) project_id -->
<FormField
label="Project.."
help="project is optional"
:class="{ 'text-red-400': form.errors.project_id }"
class="w-full mx-2 flex-1"
>
<FormControl
required
v-model="form.project_id"
:type="'select'"
placeholder="[Select Project]"
:errors="form.errors.project_id"
:options="projects"
>
<div class="text-red-400 text-sm" v-if="form.errors.project_id">
{{ form.errors.project_id.join(', ') }}
</div>
</FormControl>
</FormField>
<!-- (10) embargo_date -->
<FormField
label="Embargo Date.."
help="embargo date is optional"
:class="{ 'text-red-400': form.errors.embargo_date }"
class="w-full mx-2 flex-1"
>
<FormControl
v-model="form.embargo_date"
:type="'date'"
placeholder="date('y-m-d')"
:errors="form.errors.embargo_date"
>
<div class="text-red-400 text-sm" v-if="form.errors.embargo_date">
{{ form.errors.embargo_date.join(', ') }}
</div>
</FormControl>
</FormField>
</div>
<!-- Keywords -->
<CardBox
class="shadow-md"
has-table
title="Keywords"
:icon="mdiEarthPlus"
:header-icon="mdiPlusCircle"
@header-icon-click="addKeyword"
>
<TableKeywords
:keywords="form.subjects"
:errors="form.errors"
:subjectTypes="subjectTypes"
v-model:subjects-to-delete="form.subjectsToDelete"
v-if="form.subjects.length > 0"
/>
</CardBox>
<!-- References -->
<CardBox
class="shadow-md"
has-table
title="Dataset References"
:icon="mdiEarthPlus"
:header-icon="mdiPlusCircle"
@header-icon-click="addReference"
>
<!-- Message when no references exist -->
<div v-if="form.references.length === 0" class="text-center py-4">
<p class="text-gray-600">No references added yet.</p>
<p class="text-gray-400">Click the plus icon above to add a new reference.</p>
</div>
<!-- Reference form -->
<table class="table-fixed border-green-900" v-if="form.references.length">
<thead>
<tr>
<th class="w-4/12">Value</th>
<th class="w-2/12">Type</th>
<th class="w-3/12">Relation</th>
<th class="w-2/12">Label</th>
<th class="w-1/12"></th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in form.references">
<td data-label="Reference Value">
<!-- <input name="Reference Value" class="form-control"
placeholder="[VALUE]" v-model="item.value" /> -->
<FormControl
required
v-model="item.value"
:type="'text'"
placeholder="[VALUE]"
:errors="form.errors.embargo_date"
>
<div
class="text-red-400 text-sm"
v-if="
form.errors[`references.${index}.value`] &&
Array.isArray(form.errors[`references.${index}.value`])
"
>
{{ form.errors[`references.${index}.value`].join(', ') }}
</div>
</FormControl>
</td>
<td>
<FormControl
required
v-model="form.references[index].type"
type="select"
:options="referenceIdentifierTypes"
placeholder="[type]"
>
<div
class="text-red-400 text-sm"
v-if="Array.isArray(form.errors[`references.${index}.type`])"
>
{{ form.errors[`references.${index}.type`].join(', ') }}
</div>
</FormControl>
</td>
<td>
<FormControl
required
v-model="form.references[index].relation"
type="select"
:options="relationTypes"
placeholder="[relation type]"
>
<div
class="text-red-400 text-sm"
v-if="Array.isArray(form.errors[`references.${index}.relation`])"
>
{{ form.errors[`references.${index}.relation`].join(', ') }}
</div>
</FormControl>
</td>
<td data-label="Reference Label">
<!-- <input name="Reference Label" class="form-control" v-model="item.label" /> -->
<FormControl
required
v-model="form.references[index].label"
type="text"
placeholder="[reference label]"
>
<div class="text-red-400 text-sm" v-if="form.errors[`references.${index}.label`]">
{{ form.errors[`references.${index}.label`].join(', ') }}
</div>
</FormControl>
</td>
<td class="before:hidden lg:w-1 whitespace-nowrap">
<!-- <BaseButton color="info" :icon="mdiEye" small @click="isModalActive = true" /> -->
<BaseButton color="danger" :icon="mdiTrashCan" small @click.prevent="removeReference(index)" />
</td>
</tr>
</tbody>
</table>
<!-- References to delete section -->
<div v-if="form.referencesToDelete && form.referencesToDelete.length > 0" class="mt-8">
<h1 class="pt-8 pb-3 font-semibold sm:text-lg text-gray-900">References To Delete</h1>
<ul class="flex flex-1 flex-wrap -m-1">
<li
v-for="(element, index) in form.referencesToDelete"
:key="index"
class="block p-1 w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/6 xl:w-1/8 h-40"
>
<article
tabindex="0"
class="bg-red-100 group w-full h-full rounded-md cursor-pointer relative shadow-sm overflow-hidden"
>
<section
class="flex flex-col rounded-md text-xs break-words w-full h-full z-20 absolute top-0 py-2 px-3"
>
<h1
class="flex-1 text-gray-700 group-hover:text-blue-800 font-medium text-sm mb-1 truncate overflow-hidden whitespace-nowrap"
>
{{ element.value }}
</h1>
<div class="flex flex-col mt-auto">
<p class="p-1 size text-xs text-gray-700">
<span class="font-semibold">Type:</span> {{ element.type }}
</p>
<p class="p-1 size text-xs text-gray-700">
<span class="font-semibold">Relation:</span> {{ element.relation }}
</p>
<div class="flex justify-end mt-1">
<button
class="restore ml-auto focus:outline-none hover:bg-gray-300 p-1 rounded-md text-gray-800"
@click.prevent="restoreReference(index)"
>
<svg viewBox="0 0 24 24" class="w-5 h-5">
<path fill="currentColor" :d="mdiRestore"></path>
</svg>
</button>
</div>
</div>
</section>
</article>
</li>
</ul>
</div>
</CardBox>
</div>
</div>
<BaseDivider />
<!-- Section 7: Geographic Coverage -->
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<span class="w-8 h-8 rounded-full bg-teal-500 text-white flex items-center justify-center text-sm">7</span>
Geographic Coverage
</h2>
<div class="ml-10">
<MapComponent
v-if="form.coverage"
:mapOptions="mapOptions"
:baseMaps="baseMaps"
:fitBounds="fitBounds"
:coverage="form.coverage"
:mapId="mapId"
class="mb-4 rounded-lg overflow-hidden shadow-md"
/>
</div>
<div class="flex flex-col md:flex-row ml-10">
<!-- x min and max -->
<FormField
label="Coverage X Min"
:class="{ 'text-red-400': form.errors['coverage.x_min'] }"
class="w-full mx-2 flex-1"
>
<FormControl required v-model="form.coverage.x_min" type="text" placeholder="[enter x_min]">
<div
class="text-red-400 text-sm"
v-if="form.errors['coverage.x_min'] && Array.isArray(form.errors['coverage.x_min'])"
>
{{ form.errors['coverage.x_min'].join(', ') }}
</div>
</FormControl>
</FormField>
<FormField
label="Coverage X Max"
:class="{ 'text-red-400': form.errors['coverage.x_max'] }"
class="w-full mx-2 flex-1"
>
<FormControl required v-model="form.coverage.x_max" type="text" placeholder="[enter x_max]">
<div
class="text-red-400 text-sm"
v-if="form.errors['coverage.x_max'] && Array.isArray(form.errors['coverage.x_max'])"
>
{{ form.errors['coverage.x_max'].join(', ') }}
</div>
</FormControl>
</FormField>
<!-- y min and max -->
<FormField
label="Coverage Y Min"
:class="{ 'text-red-400': form.errors['coverage.y_min'] }"
class="w-full mx-2 flex-1"
>
<FormControl required v-model="form.coverage.y_min" type="text" placeholder="[enter y_min]">
<div
class="text-red-400 text-sm"
v-if="form.errors['coverage.y_min'] && Array.isArray(form.errors['coverage.y_min'])"
>
{{ form.errors['coverage.y_min'].join(', ') }}
</div>
</FormControl>
</FormField>
<FormField
label="Coverage Y Max"
:class="{ 'text-red-400': form.errors['coverage.y_max'] }"
class="w-full mx-2 flex-1"
>
<FormControl required v-model="form.coverage.y_max" type="text" placeholder="[enter y_max]">
<div
class="text-red-400 text-sm"
v-if="form.errors['coverage.y_max'] && Array.isArray(form.errors['coverage.y_max'])"
>
{{ form.errors['coverage.y_max'].join(', ') }}
</div>
</FormControl>
</FormField>
</div>
</div>
<BaseDivider />
<!-- Section 8: Files -->
<div v-if="form.files && form.files.length > 0" class="mb-8 ">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<span class="w-8 h-8 rounded-full bg-cyan-500 text-white flex items-center justify-center text-sm">8</span>
Files
</h2>
<div class="mt-10 bg-white rounded-lg shadow overflow-hidden">
<ul class="divide-y divide-gray-200">
<li v-for="file in form.files" :key="file.id"
class="px-4 py-3 flex items-center justify-between hover:bg-gray-50">
<div class="flex items-center space-x-3 flex-1">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-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-gray-900 truncate">
{{ file.label }}
</p>
<p class="text-xs text-gray-500 truncate">
{{ getFileSize(file) }}
</p>
</div>
</div>
<div class="ml-2 flex-shrink-0 flex space-x-2">
<a v-if="file.id != undefined"
:href="stardust.route('editor.file.download', [file.id])"
class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-blue-700 bg-blue-100 hover:bg-blue-200">
Download
</a>
</div>
</li>
</ul>
</div>
</div>
<!-- Enhanced Footer with Action Buttons -->
<template #footer>
<div class="flex items-center justify-between">
<div class="text-sm text-gray-500 dark:text-gray-400">
<span v-if="hasUnsavedChanges" class="flex items-center gap-2">
<svg class="w-4 h-4 text-amber-500" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z"
clip-rule="evenodd"
/>
</svg>
<span class="font-medium">{{ getChangesSummary().length }} unsaved change(s)</span>
</span>
<span v-else class="flex items-center gap-2 text-green-600 dark:text-green-400">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd"
/>
</svg>
<span class="font-medium">All changes saved</span>
</span>
</div>
<BaseButtons>
<BaseButton
v-if="can.edit"
@click.stop="submit"
:disabled="form.processing"
label="Save Changes"
color="info"
:icon="mdiDisc"
:class="{ 'opacity-25': form.processing }"
class="shadow-md hover:shadow-lg transition-shadow"
/>
</BaseButtons>
</div>
</template>
</CardBox>
<!-- Enhanced Loading Spinner Overlay -->
<div
v-if="form.processing"
class="fixed inset-0 flex items-center justify-center bg-gray-900 bg-opacity-75 z-50 backdrop-blur-sm"
>
<div class="bg-white dark:bg-slate-800 rounded-lg p-8 shadow-2xl flex flex-col items-center gap-4">
<svg class="animate-spin h-12 w-12 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<p class="text-lg font-semibold text-gray-900 dark:text-white">Saving changes...</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Please wait while we update your dataset</p>
</div>
</div>
</SectionMain>
</LayoutAuthenticated>
</template>
<script setup lang="ts">
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import { useForm, Head, usePage } from '@inertiajs/vue3';
import { useDatasetFormSubmission } from '@/composables/useDatasetFormSubmission';
import { useDatasetChangeDetection } from '@/composables/useDatasetChangeDetection';
import { computed, ComputedRef, ref } from 'vue';
import { Dataset, Title, Subject, Person } from '@/Dataset';
import { stardust } from '@eidellev/adonis-stardust/client';
import FormField from '@/Components/FormField.vue';
import FormControl from '@/Components/FormControl.vue';
import SectionMain from '@/Components/SectionMain.vue';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
import FormCheckRadioGroup from '@/Components/FormCheckRadioGroup.vue';
import BaseButton from '@/Components/BaseButton.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import BaseDivider from '@/Components/BaseDivider.vue';
import CardBox from '@/Components/CardBox.vue';
import MapComponent from '@/Components/Map/map.component.vue';
import SearchAutocomplete from '@/Components/SearchAutocomplete.vue';
import TablePersons from '@/Components/TablePersons.vue';
import TableKeywords from '@/Components/TableKeywords.vue';
import FormValidationErrors from '@/Components/FormValidationErrors.vue';
import { MapOptions } from '@/Components/Map/MapOptions';
import { LatLngBoundsExpression } from 'leaflet';
import { LayerOptions } from '@/Components/Map/LayerOptions';
import BaseIcon from '@/Components/BaseIcon.vue';
import {
mdiImageText,
mdiArrowLeftBoldOutline,
mdiPlusCircle,
mdiFinance,
mdiTrashCan,
mdiBookOpenPageVariant,
mdiEarthPlus,
mdiAlertBoxOutline,
mdiRestore,
mdiDisc,
} from '@mdi/js';
import { notify } from '@/notiwind';
import NotificationBar from '@/Components/NotificationBar.vue';
import UnsavedChangesWarning from '@/Components/UnsavedChangesWarning.vue';
const props = defineProps({
licenses: {
type: Object,
default: () => ({}),
},
languages: {
type: Object,
default: () => ({}),
},
doctypes: {
type: Object,
default: () => ({}),
},
titletypes: {
type: Object,
default: () => ({}),
},
projects: {
type: Object,
default: () => ({}),
},
descriptiontypes: {
type: Object,
default: () => ({}),
},
contributorTypes: {
type: Object,
default: () => ({}),
},
subjectTypes: {
type: Object,
default: () => ({}),
},
referenceIdentifierTypes: {
type: Object,
default: () => ({}),
},
relationTypes: {
type: Object,
default: () => ({}),
},
dataset: {
type: Object,
default: () => ({}),
},
can: {
type: Object,
default: () => ({}),
},
});
const flash: ComputedRef<any> = computed(() => {
return usePage().props.flash;
});
const errors: ComputedRef<any> = computed(() => {
return usePage().props.errors;
});
// Create a reactive copy of the original dataset
const originalDataset = ref(JSON.parse(JSON.stringify(props.dataset)));
const mapOptions: MapOptions = {
center: [48.208174, 16.373819],
zoom: 3,
zoomControl: false,
attributionControl: false,
};
const baseMaps: Map<string, LayerOptions> = new Map<string, LayerOptions>();
const fitBounds: LatLngBoundsExpression = [
[46.4318173285, 9.47996951665],
[49.0390742051, 16.9796667823],
];
const mapId = 'test';
// props.dataset.filesToDelete = [];
props.dataset.subjectsToDelete = [];
props.dataset.referencesToDelete = [];
let form = useForm<Dataset>(props.dataset as Dataset);
const {
submit,
// submitWithAutoSave,
} = useDatasetFormSubmission(form, originalDataset);
const {
hasUnsavedChanges,
getChangesSummary,
} = useDatasetChangeDetection(form, originalDataset);
const addTitle = () => {
let newTitle: Title = { value: '', language: '', type: '' };
form.titles.push(newTitle);
};
const removeTitle = (key: any) => {
form.titles.splice(key, 1);
};
const addDescription = () => {
let newDescription = { value: '', language: '', type: '' };
form.descriptions.push(newDescription);
};
const removeDescription = (key: any) => {
form.descriptions.splice(key, 1);
};
const addNewAuthor = () => {
let newAuthor = {
status: false,
first_name: '',
last_name: '',
email: '',
academic_title: '',
identifier_orcid: '',
name_type: 'Personal',
};
form.authors.push(newAuthor);
};
const onAddAuthor = (person: Person) => {
if (form.authors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as author' }, 4000);
} else if (form.contributors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as contributor' });
} else {
form.authors.push(person);
notify({ type: 'info', text: 'person has been successfully added as author' });
}
};
const addNewContributor = () => {
let newContributor = {
status: false,
first_name: '',
last_name: '',
email: '',
academic_title: '',
identifier_orcid: '',
name_type: 'Personal',
pivot: { contributor_type: '' },
};
form.contributors.push(newContributor);
};
const onAddContributor = (person: Person) => {
if (form.contributors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as contributor' }, 4000);
} else if (form.authors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as author' }, 4000);
} else {
// person.pivot = { contributor_type: '' };
// // person.pivot = { name_type: '', contributor_type: '' };
form.contributors.push(person);
notify({ type: 'info', text: 'person has been successfully added as contributor' }, 4000);
}
};
const addKeyword = () => {
let newSubject: Subject = { value: '', language: '', type: 'uncontrolled' };
//this.dataset.files.push(uploadedFiles[i]);
form.subjects.push(newSubject);
};
const addReference = () => {
let newReference = { value: '', label: '', relation: '', type: '' };
//this.dataset.files.push(uploadedFiles[i]);
form.references.push(newReference);
};
const removeReference = (key: any) => {
const reference = form.references[key];
// If the reference has an ID, it exists in the database
// and should be added to referencesToDelete
if (reference.id) {
// Initialize referencesToDelete array if it doesn't exist
if (!form.referencesToDelete) {
form.referencesToDelete = [];
}
// Add to referencesToDelete
form.referencesToDelete.push(reference);
}
// Remove from form.references array
form.references.splice(key, 1);
};
const restoreReference = (index: number) => {
// Get the reference from referencesToDelete
const reference = form.referencesToDelete[index];
// Add it back to form.references
form.references.push(reference);
// Remove it from referencesToDelete
form.referencesToDelete.splice(index, 1);
};
// const onMapInitialized = (newItem: any) => {
// console.log(newItem);
// };
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>
<style scoped>
.max-w-2xl {
max-width: 2xl;
}
.text-2xl {
font-size: 2xl;
}
.font-bold {
font-weight: bold;
}
.mb-4 {
margin-bottom: 1rem;
}
.block {
display: block;
}
.text-gray-700 {
color: #4b5563;
}
.shadow {
box-shadow:
0 0 0 1px rgba(66, 72, 78, 0.05),
0 1px 2px 0 rgba(66, 72, 78, 0.08),
0 2px 4px 0 rgba(66, 72, 78, 0.12),
0 4px 8px 0 rgba(66, 72, 78, 0.16);
}
</style>