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
|
@ -2,7 +2,7 @@
|
|||
|--------------------------------------------------------------------------
|
||||
| Preloaded File - node ace make:preload rules/dependentArrayMinLength
|
||||
|--------------------------------------------------------------------------
|
||||
|*/
|
||||
*/
|
||||
|
||||
import { FieldContext } from '@vinejs/vine/types';
|
||||
import vine, { VineArray } from '@vinejs/vine';
|
||||
|
@ -17,39 +17,75 @@ type Options = {
|
|||
};
|
||||
|
||||
async function dependentArrayMinLength(value: unknown, options: Options, field: FieldContext) {
|
||||
const fileInputs = field.data[options.dependentArray]; // Access the dependent array
|
||||
const isArrayValue = Array.isArray(value);
|
||||
const isArrayFileInputs = Array.isArray(fileInputs);
|
||||
|
||||
if (isArrayValue && isArrayFileInputs) {
|
||||
if (value.length >= options.min) {
|
||||
return true; // Valid if the main array length meets the minimum
|
||||
} else if (value.length === 0 && fileInputs.length >= options.min) {
|
||||
return true; // Valid if the main array is empty and the dependent array meets the minimum
|
||||
} else {
|
||||
field.report(
|
||||
`At least {{ min }} item for {{field}} field must be defined`,
|
||||
'array.dependentArrayMinLength',
|
||||
field,
|
||||
options,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Report if either value or dependentArray is not an array
|
||||
const dependentArrayValue = field.data[options.dependentArray];
|
||||
|
||||
// Both values can be null/undefined or arrays, but not other types
|
||||
const isMainValueValid = value === null || value === undefined || Array.isArray(value);
|
||||
const isDependentValueValid = dependentArrayValue === null || dependentArrayValue === undefined || Array.isArray(dependentArrayValue);
|
||||
|
||||
if (!isMainValueValid || !isDependentValueValid) {
|
||||
field.report(
|
||||
`Both the {{field}} field and dependent array {{dependentArray}} must be arrays.`,
|
||||
`Invalid file data format. Please contact support if this error persists.`,
|
||||
'array.dependentArrayMinLength',
|
||||
field,
|
||||
options,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert null/undefined to empty arrays for length checking
|
||||
const mainArray = Array.isArray(value) ? value : [];
|
||||
const dependentArray = Array.isArray(dependentArrayValue) ? dependentArrayValue : [];
|
||||
|
||||
// Calculate total count across both arrays
|
||||
const totalCount = mainArray.length + dependentArray.length;
|
||||
|
||||
// Check if minimum requirement is met
|
||||
if (totalCount >= options.min) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Special case: if dependent array has items, main array can be empty/null
|
||||
if (dependentArray.length >= options.min && mainArray.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Determine appropriate error message based on context
|
||||
const hasExistingFiles = dependentArray.length > 0;
|
||||
const hasNewFiles = mainArray.length > 0;
|
||||
|
||||
if (!hasExistingFiles && !hasNewFiles) {
|
||||
// No files at all
|
||||
field.report(
|
||||
`Your dataset must include at least {{ min }} file. Please upload a new file to continue.`,
|
||||
'array.dependentArrayMinLength',
|
||||
field,
|
||||
options,
|
||||
);
|
||||
} else if (hasExistingFiles && !hasNewFiles && dependentArray.length < options.min) {
|
||||
// Has existing files but marked for deletion, no new files
|
||||
field.report(
|
||||
`You have marked all existing files for deletion. Please upload at least {{ min }} new file or keep some existing files.`,
|
||||
'array.dependentArrayMinLength',
|
||||
field,
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
// Generic fallback message
|
||||
field.report(
|
||||
`Your dataset must have at least {{ min }} file. You can either upload new files or keep existing ones.`,
|
||||
'array.dependentArrayMinLength',
|
||||
field,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
return false; // Invalid if none of the conditions are met
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export const dependentArrayMinLengthRule = vine.createRule(dependentArrayMinLength);
|
||||
|
||||
// Extend the VineArray interface with the same type parameters
|
||||
// Extend the VineArray interface
|
||||
declare module '@vinejs/vine' {
|
||||
interface VineArray<Schema extends SchemaTypes> {
|
||||
dependentArrayMinLength(options: Options): this;
|
||||
|
@ -58,4 +94,4 @@ declare module '@vinejs/vine' {
|
|||
|
||||
VineArray.macro('dependentArrayMinLength', function <Schema extends SchemaTypes>(this: VineArray<Schema>, options: Options) {
|
||||
return this.use(dependentArrayMinLengthRule(options));
|
||||
});
|
||||
});
|
175
start/rules/orcid.ts
Normal file
175
start/rules/orcid.ts
Normal file
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Preloaded File - node ace make:preload rules/orcid
|
||||
| ❯ Do you want to register the preload file in .adonisrc.ts file? (y/N) · true
|
||||
| DONE: create start/rules/orcid.ts
|
||||
| DONE: update adonisrc.ts file
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import vine, { VineString } from '@vinejs/vine';
|
||||
import { FieldContext } from '@vinejs/vine/types';
|
||||
|
||||
/**
|
||||
* ORCID Validator Implementation
|
||||
*
|
||||
* Validates ORCID identifiers using both format validation and checksum verification.
|
||||
* ORCID (Open Researcher and Contributor ID) is a persistent digital identifier
|
||||
* that distinguishes researchers and supports automated linkages between them
|
||||
* and their professional activities.
|
||||
*
|
||||
* Format: 0000-0000-0000-0000 (where the last digit can be X for checksum 10)
|
||||
* Algorithm: MOD-11-2 checksum validation as per ISO/IEC 7064:2003
|
||||
*
|
||||
* @param value - The ORCID value to validate
|
||||
* @param _options - Unused options parameter (required by VineJS signature)
|
||||
* @param field - VineJS field context for error reporting
|
||||
*/
|
||||
async function orcidValidator(value: unknown, _options: undefined, field: FieldContext) {
|
||||
/**
|
||||
* Type guard: We only validate string values
|
||||
* The "string" rule should handle type validation before this rule runs
|
||||
*/
|
||||
if (typeof value !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle optional fields: Skip validation for empty strings
|
||||
* This allows the field to be truly optional when used with .optional()
|
||||
*/
|
||||
if (value.trim() === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the ORCID value:
|
||||
* - Remove any whitespace characters
|
||||
* - Convert to uppercase (for potential X check digit)
|
||||
*/
|
||||
const cleanOrcid = value.replace(/\s/g, '').toUpperCase();
|
||||
|
||||
/**
|
||||
* Format Validation
|
||||
*
|
||||
* ORCID format regex breakdown:
|
||||
* ^(\d{4}-){3} - Three groups of exactly 4 digits followed by hyphen
|
||||
* \d{3} - Three more digits
|
||||
* [\dX]$ - Final character: either digit or 'X' (for checksum 10)
|
||||
*
|
||||
* Valid examples: 0000-0002-1825-0097, 0000-0002-1825-009X
|
||||
*/
|
||||
const orcidRegex = /^(\d{4}-){3}\d{3}[\dX]$/;
|
||||
|
||||
if (!orcidRegex.test(cleanOrcid)) {
|
||||
field.report('ORCID must be in format: 0000-0000-0000-0000 or 0000-0000-0000-000X', 'orcid', field);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checksum Validation - MOD-11-2 Algorithm
|
||||
*
|
||||
* This implements the official ORCID checksum algorithm based on ISO/IEC 7064:2003
|
||||
* to verify mathematical validity and detect typos or invalid identifiers.
|
||||
*/
|
||||
|
||||
// Step 1: Extract digits and separate check digit
|
||||
const digits = cleanOrcid.replace(/-/g, ''); // Remove hyphens: "0000000218250097"
|
||||
const baseDigits = digits.slice(0, -1); // First 15 digits: "000000021825009"
|
||||
const checkDigit = digits.slice(-1); // Last character: "7"
|
||||
|
||||
/**
|
||||
* Step 2: Calculate checksum using MOD-11-2 algorithm
|
||||
*
|
||||
* For each digit from left to right:
|
||||
* 1. Add the digit to running total
|
||||
* 2. Multiply result by 2
|
||||
*
|
||||
* Example for "000000021825009":
|
||||
* - Start with total = 0
|
||||
* - Process each digit: total = (total + digit) * 2
|
||||
* - Continue until all 15 digits are processed
|
||||
*/
|
||||
let total = 0;
|
||||
for (const digit of baseDigits) {
|
||||
total = (total + parseInt(digit)) * 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 3: Calculate expected check digit
|
||||
*
|
||||
* Formula: (12 - (total % 11)) % 11
|
||||
* - Get remainder when total is divided by 11
|
||||
* - Subtract from 12 and take modulo 11 again
|
||||
* - If result is 10, use 'X' (since we need single character)
|
||||
*
|
||||
* Example: total = 1314
|
||||
* - remainder = 1314 % 11 = 5
|
||||
* - result = (12 - 5) % 11 = 7
|
||||
* - expectedCheckDigit = "7"
|
||||
*/
|
||||
const remainder = total % 11;
|
||||
const result = (12 - remainder) % 11;
|
||||
const expectedCheckDigit = result === 10 ? 'X' : result.toString();
|
||||
|
||||
/**
|
||||
* Step 4: Verify checksum matches
|
||||
*
|
||||
* Compare the actual check digit with the calculated expected value.
|
||||
* If they don't match, the ORCID is invalid (likely contains typos or is fabricated).
|
||||
*/
|
||||
if (checkDigit !== expectedCheckDigit) {
|
||||
field.report('Invalid ORCID checksum', 'orcid', field);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we reach this point, the ORCID is valid (both format and checksum)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the VineJS validation rule
|
||||
*
|
||||
* This creates a reusable rule that can be chained with other VineJS validators
|
||||
*/
|
||||
const orcidRule = vine.createRule(orcidValidator);
|
||||
|
||||
/**
|
||||
* TypeScript module declaration
|
||||
*
|
||||
* Extends the VineString interface to include our custom orcid() method.
|
||||
* This enables TypeScript autocompletion and type checking when using the rule.
|
||||
*/
|
||||
declare module '@vinejs/vine' {
|
||||
interface VineString {
|
||||
/**
|
||||
* Validates that a string is a valid ORCID identifier
|
||||
*
|
||||
* Checks both format (0000-0000-0000-0000) and mathematical validity
|
||||
* using the MOD-11-2 checksum algorithm.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Usage in validation schema
|
||||
* identifier_orcid: vine.string().trim().maxLength(255).orcid().optional()
|
||||
* ```
|
||||
*
|
||||
* @returns {this} The VineString instance for method chaining
|
||||
*/
|
||||
orcid(): this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the macro with VineJS
|
||||
*
|
||||
* This adds the .orcid() method to all VineString instances,
|
||||
* allowing it to be used in validation schemas.
|
||||
*
|
||||
* Usage example:
|
||||
* ```typescript
|
||||
* vine.string().orcid().optional()
|
||||
* ```
|
||||
*/
|
||||
VineString.macro('orcid', function (this: VineString) {
|
||||
return this.use(orcidRule());
|
||||
});
|
Loading…
Add table
editor.link_modal.header
Reference in a new issue