hot-fix: Add ORCID validation and improve dataset editing UX
### Major Features - Add comprehensive ORCID validation with checksum verification - Implement unsaved changes detection and auto-save functionality - Enhanced form component reactivity and state management ### ORCID Implementation - Create custom VineJS ORCID validation rule with MOD-11-2 algorithm - Add ORCID fields to Person model and TablePersons component - Update dataset validators to include ORCID validation - Add descriptive placeholder text for ORCID input fields ### UI/UX Improvements - Add UnsavedChangesWarning component with detailed change tracking - Improve FormCheckRadio and FormCheckRadioGroup reactivity - Enhanced BaseButton with proper disabled state handling - Better error handling and user feedback in file validation ### Data Management - Implement sophisticated change detection for all dataset fields - Add proper handling of array ordering for authors/contributors - Improve license selection with better state management - Enhanced subject/keyword processing with duplicate detection ### Technical Improvements - Optimize search indexing with conditional updates based on modification dates - Update person model column mapping for ORCID - Improve validation error messages and user guidance - Better handling of file uploads and deletion tracking ### Dependencies - Update various npm packages (AWS SDK, Babel, Vite, etc.) - Add baseline-browser-mapping for better browser compatibility ### Bug Fixes - Fix form reactivity issues with checkbox/radio groups - Improve error handling in file validation rules - Better handling of edge cases in change detection
This commit is contained in:
parent
06ed2f3625
commit
8f67839f93
16 changed files with 2657 additions and 1168 deletions
|
@ -232,7 +232,7 @@ export default class DatasetController {
|
|||
.maxLength(255)
|
||||
.email()
|
||||
.normalizeEmail()
|
||||
.isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }),
|
||||
.isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }),
|
||||
first_name: vine.string().trim().minLength(3).maxLength(255).optional().requiredWhen('name_type', '=', 'Personal'),
|
||||
last_name: vine.string().trim().minLength(3).maxLength(255),
|
||||
}),
|
||||
|
@ -248,7 +248,7 @@ export default class DatasetController {
|
|||
.maxLength(255)
|
||||
.email()
|
||||
.normalizeEmail()
|
||||
.isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }),
|
||||
.isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }),
|
||||
first_name: vine.string().trim().minLength(3).maxLength(255).optional().requiredWhen('name_type', '=', 'Personal'),
|
||||
last_name: vine.string().trim().minLength(3).maxLength(255),
|
||||
pivot_contributor_type: vine.enum(Object.keys(ContributorTypes)),
|
||||
|
@ -983,19 +983,6 @@ export default class DatasetController {
|
|||
const years = Array.from({ length: currentYear - 1990 + 1 }, (_, index) => 1990 + index);
|
||||
|
||||
const licenses = await License.query().select('id', 'name_long').where('active', 'true').pluck('name_long', 'id');
|
||||
// const userHasRoles = user.roles;
|
||||
// const datasetHasLicenses = await dataset.related('licenses').query().pluck('id');
|
||||
// const checkeds = dataset.licenses.first().id;
|
||||
|
||||
// const doctypes = {
|
||||
// analysisdata: { label: 'Analysis', value: 'analysisdata' },
|
||||
// measurementdata: { label: 'Measurements', value: 'measurementdata' },
|
||||
// monitoring: 'Monitoring',
|
||||
// remotesensing: 'Remote Sensing',
|
||||
// gis: 'GIS',
|
||||
// models: 'Models',
|
||||
// mixedtype: 'Mixed Type',
|
||||
// };
|
||||
|
||||
return inertia.render('Submitter/Dataset/Edit', {
|
||||
dataset,
|
||||
|
@ -1015,7 +1002,7 @@ export default class DatasetController {
|
|||
referenceIdentifierTypes: Object.entries(ReferenceIdentifierTypes).map(([key, value]) => ({ value: key, label: value })),
|
||||
relationTypes: Object.entries(RelationTypes).map(([key, value]) => ({ value: key, label: value })),
|
||||
doctypes: DatasetTypes,
|
||||
can: {
|
||||
can: {
|
||||
edit: await auth.user?.can(['dataset-edit']),
|
||||
delete: await auth.user?.can(['dataset-delete']),
|
||||
},
|
||||
|
@ -1163,42 +1150,93 @@ export default class DatasetController {
|
|||
}
|
||||
}
|
||||
|
||||
// Process all subjects/keywords from the request
|
||||
const subjects = request.input('subjects');
|
||||
// ============================================
|
||||
// IMPROVED SUBJECTS PROCESSING
|
||||
// ============================================
|
||||
const subjects = request.input('subjects', []);
|
||||
const currentDatasetSubjectIds = new Set<number>();
|
||||
|
||||
for (const subjectData of subjects) {
|
||||
// Case 1: Subject already exists in the database (has an ID)
|
||||
let subjectToRelate: Subject;
|
||||
|
||||
// Case 1: Subject has an ID (existing subject being updated)
|
||||
if (subjectData.id) {
|
||||
// Retrieve the existing subject
|
||||
const existingSubject = await Subject.findOrFail(subjectData.id);
|
||||
|
||||
// Update subject properties from the request data
|
||||
existingSubject.value = subjectData.value;
|
||||
existingSubject.type = subjectData.type;
|
||||
existingSubject.external_key = subjectData.external_key;
|
||||
// Check if the updated value conflicts with another existing subject
|
||||
const duplicateSubject = await Subject.query()
|
||||
.where('value', subjectData.value)
|
||||
.where('type', subjectData.type)
|
||||
.where('language', subjectData.language || 'en') // Default language if not provided
|
||||
.where('id', '!=', subjectData.id) // Exclude the current subject
|
||||
.first();
|
||||
|
||||
// Only save if there are actual changes
|
||||
if (existingSubject.$isDirty) {
|
||||
await existingSubject.save();
|
||||
if (duplicateSubject) {
|
||||
// A duplicate exists - use the existing duplicate instead
|
||||
subjectToRelate = duplicateSubject;
|
||||
|
||||
// Check if the original subject should be deleted (if it's only used by this dataset)
|
||||
const originalSubjectUsage = await Subject.query()
|
||||
.where('id', existingSubject.id)
|
||||
.withCount('datasets')
|
||||
.firstOrFail();
|
||||
|
||||
if (originalSubjectUsage.$extras.datasets_count <= 1) {
|
||||
// Only used by this dataset, safe to delete after detaching
|
||||
await dataset.useTransaction(trx).related('subjects').detach([existingSubject.id]);
|
||||
await existingSubject.useTransaction(trx).delete();
|
||||
} else {
|
||||
// Used by other datasets, just detach from this one
|
||||
await dataset.useTransaction(trx).related('subjects').detach([existingSubject.id]);
|
||||
}
|
||||
} else {
|
||||
// No duplicate found, update the existing subject
|
||||
existingSubject.value = subjectData.value;
|
||||
existingSubject.type = subjectData.type;
|
||||
existingSubject.language = subjectData.language;
|
||||
existingSubject.external_key = subjectData.external_key;
|
||||
|
||||
if (existingSubject.$isDirty) {
|
||||
await existingSubject.useTransaction(trx).save();
|
||||
}
|
||||
|
||||
subjectToRelate = existingSubject;
|
||||
}
|
||||
|
||||
// Note: The relationship between dataset and subject is already established,
|
||||
// so we don't need to attach it again
|
||||
}
|
||||
// Case 2: New subject being added (no ID)
|
||||
else {
|
||||
// Check if a subject with the same value and type already exists in the database
|
||||
const subject = await Subject.firstOrNew({ value: subjectData.value, type: subjectData.type }, subjectData);
|
||||
// Use firstOrNew to either find existing or create new subject
|
||||
subjectToRelate = await Subject.firstOrNew(
|
||||
{
|
||||
value: subjectData.value,
|
||||
type: subjectData.type,
|
||||
language: subjectData.language || 'en',
|
||||
},
|
||||
{
|
||||
value: subjectData.value,
|
||||
type: subjectData.type,
|
||||
language: subjectData.language || 'en',
|
||||
external_key: subjectData.external_key,
|
||||
},
|
||||
);
|
||||
|
||||
if (subject.$isNew === true) {
|
||||
// If it's a completely new subject, create and associate it with the dataset
|
||||
await dataset.useTransaction(trx).related('subjects').save(subject);
|
||||
} else {
|
||||
// If the subject already exists, just create the relationship
|
||||
await dataset.useTransaction(trx).related('subjects').attach([subject.id]);
|
||||
if (subjectToRelate.$isNew) {
|
||||
await subjectToRelate.useTransaction(trx).save();
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the relationship exists between dataset and subject
|
||||
const relationshipExists = await dataset.related('subjects').query().where('subject_id', subjectToRelate.id).first();
|
||||
|
||||
if (!relationshipExists) {
|
||||
await dataset.useTransaction(trx).related('subjects').attach([subjectToRelate.id]);
|
||||
}
|
||||
|
||||
// Track which subjects should remain associated with this dataset
|
||||
currentDatasetSubjectIds.add(subjectToRelate.id);
|
||||
}
|
||||
|
||||
// Handle explicit deletions
|
||||
const subjectsToDelete = request.input('subjectsToDelete', []);
|
||||
for (const subjectData of subjectsToDelete) {
|
||||
if (subjectData.id) {
|
||||
|
@ -1211,16 +1249,16 @@ export default class DatasetController {
|
|||
.withCount('datasets')
|
||||
.firstOrFail();
|
||||
|
||||
// Check if the subject is used by multiple datasets
|
||||
if (subject.$extras.datasets_count > 1) {
|
||||
// If used by multiple datasets, just detach it from the current dataset
|
||||
await dataset.useTransaction(trx).related('subjects').detach([subject.id]);
|
||||
} else {
|
||||
// If only used by this dataset, delete the subject completely
|
||||
// Detach the subject from this dataset
|
||||
await dataset.useTransaction(trx).related('subjects').detach([subject.id]);
|
||||
|
||||
await dataset.useTransaction(trx).related('subjects').detach([subject.id]);
|
||||
// If this was the only dataset using this subject, delete it entirely
|
||||
if (subject.$extras.datasets_count <= 1) {
|
||||
await subject.useTransaction(trx).delete();
|
||||
}
|
||||
|
||||
// Remove from current set if it was added earlier
|
||||
currentDatasetSubjectIds.delete(subjectData.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
editor.link_modal.header
Reference in a new issue