Compare commits

..

2 commits

Author SHA1 Message Date
6b04ad9910 Release: merge develop for v2.1.0
Some checks failed
CI Pipeline / japa-tests (push) Failing after 1s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 7s
2025-11-13 11:42:59 +01:00
fbc34a7456 Merge branch 'develop'
Some checks failed
CI Pipeline / japa-tests (push) Has been cancelled
nerge latest hotfix updates from develop branch into master branch
2025-07-03 10:44:39 +02:00
8 changed files with 595 additions and 1676 deletions

View file

@ -236,7 +236,7 @@ export default class DatasetsController {
throw error; throw error;
} }
const id = request.param('id'); const id = request.param('id');
const user = auth.user; const user = auth.user;
if (!user) { if (!user) {
return response.flash('You must be logged in to edit a dataset.', 'error').redirect().toRoute('app.login.show'); return response.flash('You must be logged in to edit a dataset.', 'error').redirect().toRoute('app.login.show');
} }
@ -619,7 +619,7 @@ export default class DatasetsController {
}); });
} }
public async doiStore({ request, response, session, auth }: HttpContext) { public async doiStore({ request, response, auth }: HttpContext) {
const dataId = request.param('publish_id'); const dataId = request.param('publish_id');
const user = auth.user; const user = auth.user;
if (!user) { if (!user) {
@ -627,7 +627,10 @@ export default class DatasetsController {
} }
// Load dataset with minimal required relationships // Load dataset with minimal required relationships
const dataset = await Dataset.query().where('editor_id', user.id).where('publish_id', dataId).firstOrFail(); const dataset = await Dataset.query()
.where('editor_id', user.id) // Ensure the user is the editor of the dataset
.where('publish_id', dataId)
.firstOrFail();
const prefix = process.env.DATACITE_PREFIX || ''; const prefix = process.env.DATACITE_PREFIX || '';
const base_domain = process.env.BASE_DOMAIN || ''; const base_domain = process.env.BASE_DOMAIN || '';
@ -636,44 +639,30 @@ export default class DatasetsController {
const xmlMeta = (await Index.getDoiRegisterString(dataset)) as string; const xmlMeta = (await Index.getDoiRegisterString(dataset)) as string;
// Prepare DOI registration data // Prepare DOI registration data
const doiValue = `${prefix}/tethys.${dataset.publish_id}`; const doiValue = `${prefix}/tethys.${dataset.publish_id}`; //'10.21388/tethys.213'
const landingPageUrl = `https://doi.${getDomain(base_domain)}/${prefix}/tethys.${dataset.publish_id}`; const landingPageUrl = `https://doi.${getDomain(base_domain)}/${prefix}/tethys.${dataset.publish_id}`; //https://doi.dev.tethys.at/10.21388/tethys.213
// Register DOI with DataCite
const doiClient = new DoiClient();
const dataciteResponse = await doiClient.registerDoi(doiValue, xmlMeta, landingPageUrl);
if (dataciteResponse?.status !== 201) {
const message = `Unexpected DataCite MDS response code ${dataciteResponse?.status}`;
throw new DoiClientException(dataciteResponse?.status, message);
}
// DOI registration successful - persist and index
try { try {
// Register DOI with DataCite // Save identifier
const doiClient = new DoiClient();
const dataciteResponse = await doiClient.registerDoi(doiValue, xmlMeta, landingPageUrl);
if (dataciteResponse?.status !== 201) {
const message = `Unexpected DataCite MDS response code ${dataciteResponse?.status}`;
throw new DoiClientException(dataciteResponse?.status, message);
}
// DOI registration successful - persist and index
await this.persistDoiAndIndex(dataset, doiValue); await this.persistDoiAndIndex(dataset, doiValue);
return response return response.toRoute('editor.dataset.list').flash('message', 'You have successfully created a DOI for the dataset!');
.flash('message', 'You have successfully created a DOI for the dataset!')
.redirect()
.toRoute('editor.dataset.list');
} catch (error) { } catch (error) {
// logger.error(`${__filename}: DOI registration failed for dataset ${dataset.id}: ${error.message}`); logger.error(`${__filename}: Failed to persist DOI and index dataset ${dataset.id}: ${error.message}`);
throw new HttpException(error.message);
if (error instanceof DoiClientException) {
// Flash error for Inertia to pick up
session.flash('errors', {
doi: `DOI registration failed: ${error.message}`,
});
// Optionally also flash a warning for your warning display
session.flash('warning', error.message);
} else {
session.flash('errors', {
general: `An unexpected error occurred: ${error.message}`,
});
}
return response.redirect().back();
} }
// return response.toRoute('editor.dataset.list').flash('message', xmlMeta);
} }
/** /**
@ -821,10 +810,6 @@ export default class DatasetsController {
referenceIdentifierTypes: Object.entries(ReferenceIdentifierTypes).map(([key, value]) => ({ value: key, label: value })), referenceIdentifierTypes: Object.entries(ReferenceIdentifierTypes).map(([key, value]) => ({ value: key, label: value })),
relationTypes: Object.entries(RelationTypes).map(([key, value]) => ({ value: key, label: value })), relationTypes: Object.entries(RelationTypes).map(([key, value]) => ({ value: key, label: value })),
doctypes: DatasetTypes, doctypes: DatasetTypes,
can: {
edit: await auth.user?.can(['dataset-editor-update']),
// delete: await auth.user?.can(['dataset-delete']),
},
}); });
} }

View file

@ -17,7 +17,7 @@ interface XslTParameter {
} }
export default { export default {
// opensearchNode: process.env.OPENSEARCH_HOST || 'localhost', // opensearchNode: process.env.OPENSEARCH_HOST || 'localhost',
client: new Client({ node: `${process.env.OPENSEARCH_HOST || 'localhost'}` }), // replace with your OpenSearch endpoint client: new Client({ node: `http://${process.env.OPENSEARCH_HOST || 'localhost'}` }), // replace with your OpenSearch endpoint
async getDoiRegisterString(dataset: Dataset): Promise<string | undefined> { async getDoiRegisterString(dataset: Dataset): Promise<string | undefined> {
try { try {

File diff suppressed because one or more lines are too long

View file

@ -62,19 +62,15 @@
</xsl:choose> </xsl:choose>
<!--<datacite:creator>--> <!--<datacite:creator>-->
<xsl:if test="PersonAuthor[normalize-space(concat(@FirstName, @LastName)) != '']"> <creators>
<creators> <xsl:apply-templates select="PersonAuthor" mode="oai_datacite">
<xsl:apply-templates select="PersonAuthor" mode="oai_datacite"> <xsl:sort select="@SortOrder"/>
<xsl:sort select="@SortOrder"/> </xsl:apply-templates>
</xsl:apply-templates> </creators>
</creators> <titles>
</xsl:if> <xsl:apply-templates select="TitleMain" mode="oai_datacite" />
<xsl:if test="TitleMain[normalize-space(@Value) != ''] or TitleAdditional[normalize-space(@Value) != '']"> <xsl:apply-templates select="TitleAdditional" mode="oai_datacite" />
<titles> </titles>
<xsl:apply-templates select="TitleMain" mode="oai_datacite" />
<xsl:apply-templates select="TitleAdditional" mode="oai_datacite" />
</titles>
</xsl:if>
<publisher> <publisher>
<!-- <xsl:value-of select="@PublisherName" /> --> <!-- <xsl:value-of select="@PublisherName" /> -->
<xsl:value-of select="@CreatingCorporation" /> <xsl:value-of select="@CreatingCorporation" />
@ -82,26 +78,22 @@
<publicationYear> <publicationYear>
<xsl:value-of select="ServerDatePublished/@Year" /> <xsl:value-of select="ServerDatePublished/@Year" />
</publicationYear> </publicationYear>
<xsl:if test="Subject[normalize-space(@Value) != '']"> <subjects>
<subjects> <xsl:apply-templates select="Subject" mode="oai_datacite" />
<xsl:apply-templates select="Subject" mode="oai_datacite" /> </subjects>
</subjects>
</xsl:if>
<language> <language>
<xsl:value-of select="@Language" /> <xsl:value-of select="@Language" />
</language> </language>
<xsl:if test="PersonContributor[normalize-space(concat(@FirstName, @LastName)) != '']"> <xsl:if test="PersonContributor">
<contributors> <contributors>
<xsl:apply-templates select="PersonContributor" mode="oai_datacite"> <xsl:apply-templates select="PersonContributor" mode="oai_datacite">
<xsl:sort select="@SortOrder"/> <xsl:sort select="@SortOrder"/>
</xsl:apply-templates> </xsl:apply-templates>
</contributors> </contributors>
</xsl:if> </xsl:if>
<xsl:if test="(EmbargoDate and ($unixTimestamp &lt; EmbargoDate/@UnixTimestamp)) or CreatedAt"> <dates>
<dates> <xsl:call-template name="RdrDate2" />
<xsl:call-template name="RdrDate2" /> </dates>
</dates>
</xsl:if>
<version> <version>
<xsl:choose> <xsl:choose>
<xsl:when test="@Version"> <xsl:when test="@Version">
@ -117,46 +109,42 @@
<!-- <xsl:value-of select="@Type" /> --> <!-- <xsl:value-of select="@Type" /> -->
</resourceType> </resourceType>
<xsl:if test="normalize-space(@landingpage) != ''"> <alternateIdentifiers>
<alternateIdentifiers> <xsl:call-template name="AlternateIdentifier" />
<xsl:call-template name="AlternateIdentifier" /> </alternateIdentifiers>
</alternateIdentifiers>
</xsl:if>
<xsl:if test="Reference[normalize-space(@Type) != '' and normalize-space(@Relation) != '']"> <xsl:if test="Reference">
<relatedIdentifiers> <relatedIdentifiers>
<xsl:apply-templates select="Reference" mode="oai_datacite" /> <xsl:apply-templates select="Reference" mode="oai_datacite" />
</relatedIdentifiers> </relatedIdentifiers>
</xsl:if> </xsl:if>
<xsl:if test="Licence[normalize-space(@Name) != '' or normalize-space(@Url) != '']"> <rightsList>
<rightsList> <xsl:apply-templates select="Licence" mode="oai_datacite" />
<xsl:apply-templates select="Licence" mode="oai_datacite" /> </rightsList>
</rightsList> <sizes>
</xsl:if> <size>
<xsl:if test="File"> <xsl:value-of select="count(File)" />
<sizes> <xsl:text> datasets</xsl:text>
<size> </size>
<xsl:value-of select="count(File)" /> </sizes>
<xsl:text> datasets</xsl:text> <formats>
</size> <xsl:apply-templates select="File/@MimeType" mode="oai_datacite" />
</sizes> </formats>
</xsl:if> <descriptions>
<xsl:if test="File[normalize-space(@MimeType) != '']"> <xsl:apply-templates select="TitleAbstract" mode="oai_datacite" />
<formats> <xsl:apply-templates select="TitleAbstractAdditional" mode="oai_datacite" />
<xsl:apply-templates select="File/@MimeType" mode="oai_datacite" /> </descriptions>
</formats> <geoLocations>
</xsl:if> <xsl:apply-templates select="Coverage" mode="oai_datacite" />
<xsl:if test="TitleAbstract[normalize-space(@Value) != ''] or TitleAbstractAdditional[normalize-space(@Value) != '']"> <!-- <geoLocation>
<descriptions> <geoLocationBox>
<xsl:apply-templates select="TitleAbstract" mode="oai_datacite" /> <westBoundLongitude>6.58987</westBoundLongitude>
<xsl:apply-templates select="TitleAbstractAdditional" mode="oai_datacite" /> <eastBoundLongitude>6.83639</eastBoundLongitude>
</descriptions> <southBoundLatitude>50.16</southBoundLatitude>
</xsl:if> <northBoundLatitude>50.18691</northBoundLatitude>
<xsl:if test="Coverage[normalize-space(@XMin) != '' and normalize-space(@XMax) != '' and normalize-space(@YMin) != '' and normalize-space(@YMax) != '']"> </geoLocationBox>
<geoLocations> </geoLocation> -->
<xsl:apply-templates select="Coverage" mode="oai_datacite" /> </geoLocations>
</geoLocations>
</xsl:if>
</resource> </resource>
</xsl:template> </xsl:template>
@ -188,54 +176,54 @@
<xsl:template match="Coverage" mode="oai_datacite" <xsl:template match="Coverage" mode="oai_datacite"
xmlns="http://datacite.org/schema/kernel-4"> xmlns="http://datacite.org/schema/kernel-4">
<xsl:if test="normalize-space(@XMin) != '' and normalize-space(@XMax) != '' and normalize-space(@YMin) != '' and normalize-space(@YMax) != ''"> <geoLocation>
<geoLocation> <geoLocationBox>
<geoLocationBox> <westBoundLongitude>
<westBoundLongitude><xsl:value-of select="@XMin" /></westBoundLongitude> <xsl:value-of select="@XMin" />
<eastBoundLongitude><xsl:value-of select="@XMax" /></eastBoundLongitude> </westBoundLongitude>
<southBoundLatitude><xsl:value-of select="@YMin" /></southBoundLatitude> <eastBoundLongitude>
<northBoundLatitude><xsl:value-of select="@YMax" /></northBoundLatitude> <xsl:value-of select="@XMax" />
</geoLocationBox> </eastBoundLongitude>
</geoLocation> <southBoundLatitude>
</xsl:if> <xsl:value-of select="@YMin" />
</southBoundLatitude>
<northBoundLatitude>
<xsl:value-of select="@YMax" />
</northBoundLatitude>
</geoLocationBox>
</geoLocation>
</xsl:template> </xsl:template>
<!-- TitleAbstract template -->
<xsl:template match="TitleAbstract" mode="oai_datacite" <xsl:template match="TitleAbstract" mode="oai_datacite"
xmlns="http://datacite.org/schema/kernel-4"> xmlns="http://datacite.org/schema/kernel-4">
<xsl:if test="normalize-space(@Value) != ''"> <description>
<description> <xsl:attribute name="xml:lang">
<xsl:attribute name="xml:lang"> <xsl:value-of select="@Language" />
<xsl:value-of select="@Language" /> </xsl:attribute>
<xsl:if test="@Type != ''">
<xsl:attribute name="descriptionType">
<!-- <xsl:value-of select="@Type" /> -->
<xsl:text>Abstract</xsl:text>
</xsl:attribute> </xsl:attribute>
<xsl:if test="@Type != ''"> </xsl:if>
<xsl:attribute name="descriptionType"> <xsl:value-of select="@Value" />
<xsl:text>Abstract</xsl:text> </description>
</xsl:attribute>
</xsl:if>
<xsl:value-of select="@Value" />
</description>
</xsl:if>
</xsl:template> </xsl:template>
<!-- TitleAbstractAdditional template -->
<xsl:template match="TitleAbstractAdditional" mode="oai_datacite" <xsl:template match="TitleAbstractAdditional" mode="oai_datacite"
xmlns="http://datacite.org/schema/kernel-4"> xmlns="http://datacite.org/schema/kernel-4">
<xsl:if test="normalize-space(@Value) != ''"> <description>
<description> <xsl:attribute name="xml:lang">
<xsl:attribute name="xml:lang"> <xsl:value-of select="@Language" />
<xsl:value-of select="@Language" /> </xsl:attribute>
<xsl:if test="@Type != ''">
<xsl:attribute name="descriptionType">
<xsl:call-template name="CamelCaseWord">
<xsl:with-param name="text" select="@Type" />
</xsl:call-template>
</xsl:attribute> </xsl:attribute>
<xsl:if test="@Type != ''"> </xsl:if>
<xsl:attribute name="descriptionType"> <xsl:value-of select="@Value" />
<xsl:call-template name="CamelCaseWord"> </description>
<xsl:with-param name="text" select="@Type" />
</xsl:call-template>
</xsl:attribute>
</xsl:if>
<xsl:value-of select="@Value" />
</description>
</xsl:if>
</xsl:template> </xsl:template>
<xsl:template name="CamelCaseWord"> <xsl:template name="CamelCaseWord">
@ -268,7 +256,6 @@
<xsl:template match="TitleMain" mode="oai_datacite" <xsl:template match="TitleMain" mode="oai_datacite"
xmlns="http://datacite.org/schema/kernel-4"> xmlns="http://datacite.org/schema/kernel-4">
<xsl:if test="normalize-space(@Value) != ''">
<title> <title>
<xsl:if test="@Language != ''"> <xsl:if test="@Language != ''">
<xsl:attribute name="xml:lang"> <xsl:attribute name="xml:lang">
@ -282,12 +269,9 @@
</xsl:if> </xsl:if>
<xsl:value-of select="@Value" /> <xsl:value-of select="@Value" />
</title> </title>
</xsl:if>
</xsl:template> </xsl:template>
<xsl:template match="TitleAdditional" mode="oai_datacite" <xsl:template match="TitleAdditional" mode="oai_datacite"
xmlns="http://datacite.org/schema/kernel-4"> xmlns="http://datacite.org/schema/kernel-4">
<xsl:if test="normalize-space(@Value) != ''">
<title> <title>
<xsl:if test="@Language != ''"> <xsl:if test="@Language != ''">
<xsl:attribute name="xml:lang"> <xsl:attribute name="xml:lang">
@ -310,70 +294,61 @@
</xsl:choose> </xsl:choose>
<xsl:value-of select="@Value" /> <xsl:value-of select="@Value" />
</title> </title>
</xsl:if>
</xsl:template> </xsl:template>
<xsl:template match="Subject" mode="oai_datacite" <xsl:template match="Subject" mode="oai_datacite"
xmlns="http://datacite.org/schema/kernel-4"> xmlns="http://datacite.org/schema/kernel-4">
<xsl:if test="normalize-space(@Value) != ''"> <subject>
<subject> <xsl:if test="@Language != ''">
<xsl:if test="@Language != ''"> <xsl:attribute name="xml:lang">
<xsl:attribute name="xml:lang"> <xsl:value-of select="@Language" />
<xsl:value-of select="@Language" /> </xsl:attribute>
</xsl:attribute> </xsl:if>
</xsl:if> <xsl:value-of select="@Value" />
<xsl:value-of select="@Value" /> </subject>
</subject>
</xsl:if>
</xsl:template> </xsl:template>
<xsl:template name="AlternateIdentifier" match="AlternateIdentifier" mode="oai_datacite" <xsl:template name="AlternateIdentifier" match="AlternateIdentifier" mode="oai_datacite"
xmlns="http://datacite.org/schema/kernel-4"> xmlns="http://datacite.org/schema/kernel-4">
<xsl:if test="normalize-space(@landingpage) != ''"> <alternateIdentifier>
<alternateIdentifier> <xsl:attribute name="alternateIdentifierType">
<xsl:attribute name="alternateIdentifierType"> <xsl:text>url</xsl:text>
<xsl:text>url</xsl:text> </xsl:attribute>
</xsl:attribute> <!-- <xsl:variable name="identifier" select="concat($repURL, '/dataset/', @Id)" /> -->
<xsl:value-of select="@landingpage" /> <xsl:value-of select="@landingpage" />
</alternateIdentifier> </alternateIdentifier>
</xsl:if>
</xsl:template> </xsl:template>
<xsl:template match="Reference" mode="oai_datacite" <xsl:template match="Reference" mode="oai_datacite"
xmlns="http://datacite.org/schema/kernel-4"> xmlns="http://datacite.org/schema/kernel-4">
<xsl:if test="normalize-space(@Type) != '' and normalize-space(@Relation) != ''"> <relatedIdentifier>
<relatedIdentifier> <xsl:attribute name="relatedIdentifierType">
<xsl:attribute name="relatedIdentifierType"> <xsl:value-of select="@Type" />
<xsl:value-of select="@Type" /> </xsl:attribute>
</xsl:attribute> <xsl:attribute name="relationType">
<xsl:attribute name="relationType"> <xsl:value-of select="@Relation" />
<xsl:value-of select="@Relation" /> </xsl:attribute>
</xsl:attribute> <xsl:value-of select="@Value" />
<xsl:value-of select="@Value" /> </relatedIdentifier>
</relatedIdentifier>
</xsl:if>
</xsl:template> </xsl:template>
<!-- PersonContributor template -->
<xsl:template match="PersonContributor" mode="oai_datacite" <xsl:template match="PersonContributor" mode="oai_datacite"
xmlns="http://datacite.org/schema/kernel-4"> xmlns="http://datacite.org/schema/kernel-4">
<xsl:if test="normalize-space(concat(@FirstName, @LastName)) != ''"> <contributor>
<contributor> <xsl:if test="@ContributorType != ''">
<xsl:if test="@ContributorType != ''"> <xsl:attribute name="contributorType">
<xsl:attribute name="contributorType"> <xsl:value-of select="@ContributorType" />
<xsl:value-of select="@ContributorType" /> </xsl:attribute>
</xsl:attribute> </xsl:if>
</xsl:if> <contributorName>
<contributorName> <!-- <xsl:if test="@NameType != ''">
<!-- <xsl:if test="@NameType != ''"> <xsl:attribute name="nameType">
<xsl:attribute name="nameType"> <xsl:value-of select="@NameType" />
<xsl:value-of select="@NameType" /> </xsl:attribute>
</xsl:attribute> </xsl:if> -->
</xsl:if> --> <xsl:value-of select="concat(@FirstName, ' ',@LastName)" />
<xsl:value-of select="concat(@FirstName, ' ', @LastName)" /> </contributorName>
</contributorName> </contributor>
</contributor>
</xsl:if>
</xsl:template> </xsl:template>
<xsl:template match="PersonAuthor" mode="oai_datacite" <xsl:template match="PersonAuthor" mode="oai_datacite"
@ -428,11 +403,9 @@
<xsl:template match="File/@MimeType" mode="oai_datacite" <xsl:template match="File/@MimeType" mode="oai_datacite"
xmlns="http://datacite.org/schema/kernel-4"> xmlns="http://datacite.org/schema/kernel-4">
<xsl:if test="normalize-space(.) != ''"> <format>
<format> <xsl:value-of select="." />
<xsl:value-of select="." /> </format>
</format>
</xsl:if>
</xsl:template> </xsl:template>
<xsl:template match="Licence" mode="oai_datacite" <xsl:template match="Licence" mode="oai_datacite"

View file

@ -2,7 +2,7 @@
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 SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue'; import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
import { router, Head, usePage } from '@inertiajs/vue3'; import { router, Head, usePage } from '@inertiajs/vue3';
import { computed, Ref } from 'vue'; import { computed, Ref } from 'vue';
import CardBox from '@/Components/CardBox.vue'; import CardBox from '@/Components/CardBox.vue';
import BaseButton from '@/Components/BaseButton.vue'; import BaseButton from '@/Components/BaseButton.vue';
@ -51,11 +51,6 @@ const handleSubmit = async (e) => {
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox form @submit.prevent="handleSubmit"> <CardBox form @submit.prevent="handleSubmit">
<FormValidationErrors v-bind:errors="errors" /> <FormValidationErrors v-bind:errors="errors" />
<!-- Your existing warning display will show the warning -->
<!-- <div v-if="flash && flash.warning">
<p>{{ flash.warning }}</p>
</div> -->
<!-- <div class="flex flex-col md:flex-row items-center"> --> <!-- <div class="flex flex-col md:flex-row items-center"> -->
<!-- <div class="w-full"> <!-- <div class="w-full">

File diff suppressed because it is too large Load diff

View file

@ -1,475 +0,0 @@
// ====================================================================
// FILE: composables/useDatasetChangeDetection.ts
// ====================================================================
import { computed, Ref } from 'vue';
import type { Dataset } from '@/Dataset';
import { InertiaForm } from '@inertiajs/vue3';
interface ComparisonOptions {
orderSensitive?: boolean;
compareKey?: string;
}
export function useDatasetChangeDetection(
form: InertiaForm<Dataset>,
originalDataset: Ref<Dataset>
) {
/**
* Compare arrays with order sensitivity
*/
const compareArraysWithOrder = (
current: any[],
original: any[],
compareKey?: string
): boolean => {
if (current.length !== original.length) return true;
for (let i = 0; i < current.length; i++) {
const currentItem = current[i];
const originalItem = original[i];
if (compareKey && currentItem[compareKey] !== originalItem[compareKey]) {
return true;
}
if (JSON.stringify(currentItem) !== JSON.stringify(originalItem)) {
return true;
}
}
return false;
};
/**
* Compare arrays without order sensitivity (content-based)
*/
const compareArraysContent = (current: any[], original: any[]): boolean => {
if (current.length !== original.length) return true;
const normalizedCurrent = current
.map((item) => JSON.stringify(item))
.sort();
const normalizedOriginal = original
.map((item) => JSON.stringify(item))
.sort();
return (
JSON.stringify(normalizedCurrent) !== JSON.stringify(normalizedOriginal)
);
};
/**
* Check if licenses have changed
*/
const hasLicenseChanges = (): boolean => {
const originalLicenses = Array.isArray(originalDataset.value.licenses)
? originalDataset.value.licenses
.map((l) => (typeof l === 'object' ? l.id.toString() : String(l)))
.sort()
: [];
const currentLicenses = Array.isArray(form.licenses)
? form.licenses
.map((l) => (typeof l === 'object' ? l.id.toString() : String(l)))
.sort()
: [];
return JSON.stringify(currentLicenses) !== JSON.stringify(originalLicenses);
};
/**
* Check if basic properties have changed
*/
const hasBasicPropertyChanges = (): boolean => {
const original = originalDataset.value;
return (
form.language !== original.language ||
form.type !== original.type ||
form.creating_corporation !== original.creating_corporation ||
Number(form.project_id) !== Number(original.project_id) ||
form.embargo_date !== original.embargo_date
);
};
/**
* Check if there are items marked for deletion
*/
const hasDeletionChanges = (): boolean => {
return (
(form.subjectsToDelete?.length ?? 0) > 0 ||
(form.referencesToDelete?.length ?? 0) > 0
);
};
/**
* Check if files have changed
*/
const hasFileChanges = (): boolean => {
const currentFiles = form.files || [];
const originalFiles = originalDataset.value.files || [];
// Check for new files
const newFiles = currentFiles.filter((f) => !f.id);
if (newFiles.length > 0) return true;
// Check for deleted files
const originalFileIds = originalFiles.map((f) => f.id).filter(Boolean);
const currentFileIds = currentFiles.map((f) => f.id).filter(Boolean);
if (!originalFileIds.every((id) => currentFileIds.includes(id))) {
return true;
}
// Check for file order changes
return compareArraysWithOrder(currentFiles, originalFiles, 'sort_order');
};
/**
* Check if coverage has changed
*/
const hasCoverageChanges = (): boolean => {
const currentCoverage = form.coverage || {};
const originalCoverage = originalDataset.value.coverage || {};
return (
Number(currentCoverage.x_min) !== Number(originalCoverage.x_min) ||
Number(currentCoverage.x_max) !== Number(originalCoverage.x_max) ||
Number(currentCoverage.y_min) !== Number(originalCoverage.y_min) ||
Number(currentCoverage.y_max) !== Number(originalCoverage.y_max)
);
};
/**
* Main change detection computed property
*/
const hasUnsavedChanges = computed(() => {
// Check if form is processing
if (form.processing) return true;
const original = originalDataset.value;
// Check basic properties
if (hasBasicPropertyChanges()) return true;
// Check deletion arrays
if (hasDeletionChanges()) return true;
// Check licenses
if (hasLicenseChanges()) return true;
// Check files (order-sensitive)
if (hasFileChanges()) return true;
// Check authors (order-sensitive)
if (
compareArraysWithOrder(
form.authors || [],
original.authors || []
)
) {
return true;
}
// Check contributors (order-sensitive)
if (
compareArraysWithOrder(
form.contributors || [],
original.contributors || []
)
) {
return true;
}
// Check titles (order-sensitive)
if (compareArraysWithOrder(form.titles, original.titles)) {
return true;
}
// Check descriptions (order-sensitive)
if (compareArraysWithOrder(form.descriptions, original.descriptions)) {
return true;
}
// Check subjects/keywords (order-insensitive)
if (
compareArraysContent(
form.subjects || [],
original.subjects || []
)
) {
return true;
}
// Check references (order-insensitive)
if (
compareArraysContent(
form.references || [],
original.references || []
)
) {
return true;
}
// Check coverage
if (hasCoverageChanges()) return true;
return false;
});
/**
* Analyze array changes with detailed information
*/
const analyzeArrayChanges = (
current: any[],
original: any[],
itemName: string
): string[] => {
const changes: string[] = [];
// Check for count changes
if (current.length !== original.length) {
const diff = current.length - original.length;
if (diff > 0) {
changes.push(`${diff} ${itemName}(s) added`);
} else {
changes.push(`${Math.abs(diff)} ${itemName}(s) removed`);
}
}
// Check for order changes (only if same count)
if (current.length === original.length && current.length > 1) {
const currentIds = current.map((item) => item.id).filter(Boolean);
const originalIds = original.map((item) => item.id).filter(Boolean);
if (currentIds.length === originalIds.length && currentIds.length > 0) {
const orderChanged = currentIds.some(
(id, index) => id !== originalIds[index]
);
if (orderChanged) {
changes.push(`${itemName} order changed`);
}
}
}
// Check for content changes
if (current.length === original.length) {
const contentChanged =
JSON.stringify(current) !== JSON.stringify(original);
const orderChanged = changes.some((change) =>
change.includes('order changed')
);
if (contentChanged && !orderChanged) {
changes.push(`${itemName} content modified`);
}
}
return changes;
};
/**
* Generate detailed changes summary
*/
const getChangesSummary = (): string[] => {
const changes: string[] = [];
const original = originalDataset.value;
// Basic property changes
if (form.language !== original.language) {
changes.push('Language changed');
}
if (form.type !== original.type) {
changes.push('Dataset type changed');
}
if (form.creating_corporation !== original.creating_corporation) {
changes.push('Creating corporation changed');
}
if (Number(form.project_id) !== Number(original.project_id)) {
changes.push('Project changed');
}
if (form.embargo_date !== original.embargo_date) {
changes.push('Embargo date changed');
}
// Deletion tracking
if ((form.subjectsToDelete?.length ?? 0) > 0) {
changes.push(
`${form.subjectsToDelete.length} keyword(s) marked for deletion`
);
}
if ((form.referencesToDelete?.length ?? 0) > 0) {
changes.push(
`${form.referencesToDelete.length} reference(s) marked for deletion`
);
}
// License changes
if (hasLicenseChanges()) {
changes.push('Licenses modified');
}
// Files analysis
const currentFiles = form.files || [];
const originalFiles = original.files || [];
const newFiles = currentFiles.filter((f) => !f.id);
if (newFiles.length > 0) {
changes.push(`${newFiles.length} new file(s) added`);
}
const existingCurrentFiles = currentFiles.filter((f) => f.id);
const existingOriginalFiles = originalFiles.filter((f) => f.id);
if (
existingCurrentFiles.length === existingOriginalFiles.length &&
existingCurrentFiles.length > 1
) {
const currentOrder = existingCurrentFiles.map((f) => f.id);
const originalOrder = existingOriginalFiles.map((f) => f.id);
const orderChanged = currentOrder.some(
(id, index) => id !== originalOrder[index]
);
if (orderChanged) {
changes.push('File order changed');
}
}
// Authors and contributors
changes.push(
...analyzeArrayChanges(
form.authors || [],
original.authors || [],
'author'
)
);
changes.push(
...analyzeArrayChanges(
form.contributors || [],
original.contributors || [],
'contributor'
)
);
// Titles analysis
if (JSON.stringify(form.titles) !== JSON.stringify(original.titles)) {
if (form.titles.length !== original.titles.length) {
const diff = form.titles.length - original.titles.length;
changes.push(
diff > 0
? `${diff} title(s) added`
: `${Math.abs(diff)} title(s) removed`
);
} else if (form.titles.length > 0) {
if (form.titles[0]?.value !== original.titles[0]?.value) {
changes.push('Main title changed');
}
const otherTitlesChanged = form.titles
.slice(1)
.some(
(title, index) =>
JSON.stringify(title) !== JSON.stringify(original.titles[index + 1])
);
if (otherTitlesChanged) {
changes.push('Additional titles modified');
}
}
}
// Descriptions analysis
if (
JSON.stringify(form.descriptions) !==
JSON.stringify(original.descriptions)
) {
if (form.descriptions.length !== original.descriptions.length) {
const diff = form.descriptions.length - original.descriptions.length;
changes.push(
diff > 0
? `${diff} description(s) added`
: `${Math.abs(diff)} description(s) removed`
);
} else if (form.descriptions.length > 0) {
if (
form.descriptions[0]?.value !== original.descriptions[0]?.value
) {
changes.push('Main abstract changed');
}
const otherDescChanged = form.descriptions
.slice(1)
.some(
(desc, index) =>
JSON.stringify(desc) !==
JSON.stringify(original.descriptions[index + 1])
);
if (otherDescChanged) {
changes.push('Additional descriptions modified');
}
}
}
// Subjects/Keywords analysis
const currentSubjects = form.subjects || [];
const originalSubjects = original.subjects || [];
if (currentSubjects.length !== originalSubjects.length) {
const diff = currentSubjects.length - originalSubjects.length;
changes.push(
diff > 0
? `${diff} keyword(s) added`
: `${Math.abs(diff)} keyword(s) removed`
);
} else if (currentSubjects.length > 0) {
const currentSubjectsNormalized = currentSubjects
.map((s) => JSON.stringify(s))
.sort();
const originalSubjectsNormalized = originalSubjects
.map((s) => JSON.stringify(s))
.sort();
if (
JSON.stringify(currentSubjectsNormalized) !==
JSON.stringify(originalSubjectsNormalized)
) {
changes.push('Keywords modified');
}
}
// References analysis
const currentRefs = form.references || [];
const originalRefs = original.references || [];
if (currentRefs.length !== originalRefs.length) {
const diff = currentRefs.length - originalRefs.length;
changes.push(
diff > 0
? `${diff} reference(s) added`
: `${Math.abs(diff)} reference(s) removed`
);
} else if (currentRefs.length > 0) {
const currentRefsNormalized = currentRefs
.map((r) => JSON.stringify(r))
.sort();
const originalRefsNormalized = originalRefs
.map((r) => JSON.stringify(r))
.sort();
if (
JSON.stringify(currentRefsNormalized) !==
JSON.stringify(originalRefsNormalized)
) {
changes.push('References modified');
}
}
// Coverage changes
if (hasCoverageChanges()) {
changes.push('Geographic coverage changed');
}
return changes;
};
return {
hasUnsavedChanges,
getChangesSummary,
compareArraysWithOrder,
compareArraysContent,
};
}

View file

@ -1,217 +0,0 @@
// ====================================================================
// FILE: composables/useDatasetFormSubmission.ts
// ====================================================================
import { Ref } from 'vue';
import type { Dataset, License } from '@/Dataset';
import { InertiaForm } from '@inertiajs/vue3';
import { stardust } from '@eidellev/adonis-stardust/client';
import { notify } from '@/notiwind';
interface SubmissionOptions {
onSuccess?: (updatedDataset: Dataset) => void;
onError?: (errors: any) => void;
showNotification?: boolean;
}
export function useDatasetFormSubmission(
form: InertiaForm<Dataset>,
originalDataset: Ref<Dataset>
) {
/**
* Check if object has id attribute (type guard)
*/
const hasIdAttribute = (obj: License | number): obj is License => {
return typeof obj === 'object' && 'id' in obj;
};
/**
* Transform licenses for submission
*/
const transformLicenses = (): string[] => {
return form.licenses.map((obj) => {
if (hasIdAttribute(obj)) {
return obj.id.toString();
}
return String(obj);
});
};
/**
* Validate form before submission
*/
const validateForm = (): { valid: boolean; errors: string[] } => {
const errors: string[] = [];
// Required field validations
if (!form.language) {
errors.push('Language is required');
}
if (!form.type) {
errors.push('Dataset type is required');
}
if (!form.creating_corporation) {
errors.push('Creating corporation is required');
}
if (!form.titles || !form.titles[0]?.value) {
errors.push('Main title is required');
}
if (!form.descriptions || !form.descriptions[0]?.value) {
errors.push('Main abstract is required');
}
return {
valid: errors.length === 0,
errors,
};
};
/**
* Handle successful submission
*/
const handleSubmitSuccess = (
updatedDataset: Dataset,
showNotification: boolean = true
) => {
// Clear deletion arrays
if (updatedDataset.subjectsToDelete) {
updatedDataset.subjectsToDelete = [];
}
if (updatedDataset.referencesToDelete) {
updatedDataset.referencesToDelete = [];
}
// Update form with fresh data from server
Object.keys(updatedDataset).forEach((key) => {
if (key !== 'licenses' && key in form) {
form[key] = updatedDataset[key];
}
});
// Clear form errors
form.clearErrors();
// Update original dataset reference
originalDataset.value = JSON.parse(JSON.stringify(updatedDataset));
// Show success notification
if (showNotification) {
notify(
{
type: 'success',
title: 'Success',
text: 'Dataset updated successfully',
},
4000,
);
}
};
/**
* Handle submission errors
*/
const handleSubmitError = (errors: any) => {
console.error('Submission errors:', errors);
notify(
{
type: 'error',
title: 'Error',
text: 'Failed to update dataset. Please check the form for errors.',
},
5000,
);
};
/**
* Submit form with auto-save behavior
*/
const submitWithAutoSave = async (
options: SubmissionOptions = {}
): Promise<void> => {
try {
const route = stardust.route('editor.dataset.update', [form.id]);
const licenses = transformLicenses();
await form
.transform((data) => ({
...data,
licenses,
rights: 'true',
}))
.put(route, {
onSuccess: (page) => {
const updatedDataset = page.props.dataset || form.data();
handleSubmitSuccess(
updatedDataset,
options.showNotification ?? true
);
if (options.onSuccess) {
options.onSuccess(updatedDataset);
}
},
onError: (errors) => {
handleSubmitError(errors);
if (options.onError) {
options.onError(errors);
}
},
});
} catch (error) {
console.error('Unexpected error during submission:', error);
notify(
{
type: 'error',
title: 'Error',
text: 'An unexpected error occurred. Please try again.',
},
5000,
);
}
};
/**
* Standard submit with validation
*/
const submit = async (
options: SubmissionOptions = {}
): Promise<void> => {
// Validate form first
const validation = validateForm();
if (!validation.valid) {
notify(
{
type: 'error',
title: 'Validation Error',
text: validation.errors.join(', '),
},
5000,
);
return;
}
await submitWithAutoSave({
...options,
showNotification: true,
});
};
/**
* Silent submit without notification (for auto-save)
*/
const submitSilently = async (): Promise<void> => {
await submitWithAutoSave({
showNotification: false,
});
};
return {
submit,
submitWithAutoSave,
submitSilently,
validateForm,
transformLicenses,
};
}