feat: add dataset change detection and form submission composables
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 6s
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 6s
- 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.
This commit is contained in:
parent
0680879e2f
commit
5efddc2a58
4 changed files with 1474 additions and 436 deletions
475
resources/js/composables/useDatasetChangeDetection.ts
Normal file
475
resources/js/composables/useDatasetChangeDetection.ts
Normal file
|
|
@ -0,0 +1,475 @@
|
|||
// ====================================================================
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue