// ==================================================================== // 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, originalDataset: Ref ) { /** * 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, }; }