From b5c3ea103b56f658841d07c5e0241193a4352af2 Mon Sep 17 00:00:00 2001 From: Bheb Developer Date: Mon, 25 Aug 2025 04:22:17 +0530 Subject: [PATCH 1/5] Fix: Add import override confirmation for existing data - Add confirmation dialog when importing data if existing snippets/profile present - Ask user to confirm before replacing existing data with imported data - Apply to both JSON and CSV import functionality - Prevents accidental data loss during import operations - Addresses issue #1: Profile import skipped when existing data present Fixes #1 --- js/app.js | 53 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/js/app.js b/js/app.js index db2f702..07f5b82 100644 --- a/js/app.js +++ b/js/app.js @@ -859,10 +859,11 @@ class CompyApp { * Accepts both legacy array-only exports and the newer object format containing { items, profileName }. * @param {string} jsonText - Raw JSON string */ - importJSON(jsonText) { + async importJSON(jsonText) { try { const parsed = JSON.parse(jsonText); let items = []; + let profileName = ''; if (Array.isArray(parsed)) { // Legacy format: array of items @@ -870,14 +871,35 @@ class CompyApp { } else if (parsed && typeof parsed === 'object' && Array.isArray(parsed.items)) { // New format with profile items = parsed.items; - - if (typeof parsed.profileName === 'string' && parsed.profileName.trim()) { - updateProfile(parsed.profileName.trim()); - } + profileName = (parsed.profileName || '').trim(); } else { throw new Error('Invalid JSON format'); } + // Check if there's existing data and ask for confirmation + const currentState = getState(); + const hasExistingData = currentState.items.length > 0 || currentState.profileName; + + if (hasExistingData) { + const confirmed = await this.confirmationManager.show({ + title: 'Replace Existing Data?', + message: `You have existing data in Compy:\n\n• ${currentState.items.length} snippets\n• Profile: ${currentState.profileName || 'Not set'}\n\nImporting will replace all existing data. This action cannot be undone.\n\nWould you like to continue and replace your existing data?`, + confirmText: 'Replace Data', + cancelText: 'Cancel Import', + variant: 'danger' + }); + + if (!confirmed) { + this.showNotification('Import cancelled', 'info'); + return; + } + } + + // Update profile if provided + if (profileName) { + updateProfile(profileName); + } + let importCount = 0; for (const item of items) { if (this.addImportedItem(item)) { @@ -910,7 +932,7 @@ class CompyApp { * * @param {string} csvText - Raw CSV string from uploaded file */ - importCSV(csvText) { + async importCSV(csvText) { try { // Split into lines and filter out empty lines // Handle both Windows (\r\n) and Unix (\n) line endings @@ -970,6 +992,25 @@ class CompyApp { console.log('CSV column mapping:', columnMapping); + // Check if there's existing data and ask for confirmation + const currentState = getState(); + const hasExistingData = currentState.items.length > 0 || currentState.profileName; + + if (hasExistingData) { + const confirmed = await this.confirmationManager.show({ + title: 'Replace Existing Data?', + message: `You have existing data in Compy:\n\n• ${currentState.items.length} snippets\n• Profile: ${currentState.profileName || 'Not set'}\n\nImporting will replace all existing data. This action cannot be undone.\n\nWould you like to continue and replace your existing data?`, + confirmText: 'Replace Data', + cancelText: 'Cancel Import', + variant: 'danger' + }); + + if (!confirmed) { + this.showNotification('Import cancelled', 'info'); + return; + } + } + // PHASE 3: Process data rows let importCount = 0; let skippedCount = 0; From f3c5e31dfd0334cb49453542636a6c6c8da7ecb9 Mon Sep 17 00:00:00 2001 From: Bheb Developer Date: Mon, 25 Aug 2025 04:35:30 +0530 Subject: [PATCH 2/5] Enhanced import with Add/Replace options --- js/app.js | 211 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 195 insertions(+), 16 deletions(-) diff --git a/js/app.js b/js/app.js index 07f5b82..2828e90 100644 --- a/js/app.js +++ b/js/app.js @@ -876,23 +876,32 @@ class CompyApp { throw new Error('Invalid JSON format'); } - // Check if there's existing data and ask for confirmation + // Check if there's existing data and ask for import options const currentState = getState(); const hasExistingData = currentState.items.length > 0 || currentState.profileName; + let shouldClearExisting = false; + if (hasExistingData) { - const confirmed = await this.confirmationManager.show({ - title: 'Replace Existing Data?', - message: `You have existing data in Compy:\n\n• ${currentState.items.length} snippets\n• Profile: ${currentState.profileName || 'Not set'}\n\nImporting will replace all existing data. This action cannot be undone.\n\nWould you like to continue and replace your existing data?`, - confirmText: 'Replace Data', - cancelText: 'Cancel Import', - variant: 'danger' + const importOption = await this.showImportOptionsDialog({ + existingCount: currentState.items.length, + existingProfile: currentState.profileName || 'Not set', + importingCount: items.length, + importingProfile: profileName || 'Not set' }); - if (!confirmed) { + if (importOption === 'cancel') { this.showNotification('Import cancelled', 'info'); return; + } else if (importOption === 'replace') { + shouldClearExisting = true; } + // if importOption === 'add', we just continue without clearing + } + + // Clear existing data if user chose replace + if (shouldClearExisting) { + this.clearAllData(); } // Update profile if provided @@ -992,23 +1001,39 @@ class CompyApp { console.log('CSV column mapping:', columnMapping); - // Check if there's existing data and ask for confirmation + // Count items that will be imported + let itemsToImport = 0; + for (let i = headerIndex + 1; i < lines.length; i++) { + const line = lines[i].trim(); + if (line) itemsToImport++; + } + + // Check if there's existing data and ask for import options const currentState = getState(); const hasExistingData = currentState.items.length > 0 || currentState.profileName; + let shouldClearExisting = false; + if (hasExistingData) { - const confirmed = await this.confirmationManager.show({ - title: 'Replace Existing Data?', - message: `You have existing data in Compy:\n\n• ${currentState.items.length} snippets\n• Profile: ${currentState.profileName || 'Not set'}\n\nImporting will replace all existing data. This action cannot be undone.\n\nWould you like to continue and replace your existing data?`, - confirmText: 'Replace Data', - cancelText: 'Cancel Import', - variant: 'danger' + const importOption = await this.showImportOptionsDialog({ + existingCount: currentState.items.length, + existingProfile: currentState.profileName || 'Not set', + importingCount: itemsToImport, + importingProfile: 'From CSV' }); - if (!confirmed) { + if (importOption === 'cancel') { this.showNotification('Import cancelled', 'info'); return; + } else if (importOption === 'replace') { + shouldClearExisting = true; } + // if importOption === 'add', we just continue without clearing + } + + // Clear existing data if user chose replace + if (shouldClearExisting) { + this.clearAllData(); } // PHASE 3: Process data rows @@ -1133,6 +1158,160 @@ class CompyApp { this.modalManager.open('#backupsModal'); } + /** + * Show a custom three-option dialog for import operations. + * @param {Object} options - Dialog configuration + * @param {number} options.existingCount - Number of existing items + * @param {string} options.existingProfile - Current profile name + * @param {number} options.importingCount - Number of items to import + * @param {string} options.importingProfile - Profile from import + * @returns {Promise<'cancel'|'add'|'replace'>} - User's choice + */ + async showImportOptionsDialog({ existingCount, existingProfile, importingCount, importingProfile }) { + return new Promise((resolve) => { + const modal = document.createElement('div'); + modal.className = 'modal'; + modal.innerHTML = ` + + `; + + // Add temporary styles + const style = document.createElement('style'); + style.textContent = ` + .import-options-modal { + max-width: 500px; + text-align: center; + } + .import-comparison { + display: flex; + gap: 2rem; + margin: 1.5rem 0; + text-align: left; + } + .import-section { + flex: 1; + padding: 1rem; + border: 1px solid var(--border-color); + border-radius: 8px; + background: var(--card-bg); + } + .import-section h3 { + margin: 0 0 0.5rem 0; + color: var(--text-primary); + font-size: 1rem; + } + .import-section p { + margin: 0.25rem 0; + color: var(--text-secondary); + } + .import-message { + margin: 1.5rem 0 1rem 0; + color: var(--text-primary); + font-weight: 500; + } + .import-actions { + display: flex; + gap: 1rem; + justify-content: center; + margin-top: 1.5rem; + } + .danger-btn { + background: var(--color-danger); + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 6px; + cursor: pointer; + font-size: 0.9rem; + } + .danger-btn:hover { + background: var(--color-danger-hover, #d32f2f); + } + @media (max-width: 600px) { + .import-comparison { + flex-direction: column; + gap: 1rem; + } + .import-actions { + flex-direction: column; + } + } + `; + document.head.appendChild(style); + + // Add modal to DOM + document.body.appendChild(modal); + + // Add event handlers + const cleanup = () => { + document.body.removeChild(modal); + document.head.removeChild(style); + }; + + modal.querySelector('#importCancel').addEventListener('click', () => { + cleanup(); + resolve('cancel'); + }); + + modal.querySelector('#importAdd').addEventListener('click', () => { + cleanup(); + resolve('add'); + }); + + modal.querySelector('#importReplace').addEventListener('click', () => { + cleanup(); + resolve('replace'); + }); + + // Show modal + requestAnimationFrame(() => { + modal.classList.add('open'); + modal.querySelector('#importAdd').focus(); + }); + }); + } + + /** + * Clear all existing data (items and profile). + * Used when user chooses "Replace All" during import. + */ + clearAllData() { + // Clear all items by setting empty array + const currentState = getState(); + + // Remove all items one by one + currentState.items.forEach(item => { + deleteItem(item.id); + }); + + // Clear profile + updateProfile(''); + + // Clear any active filters and search + updateFilterTags([]); + updateSearch(''); + } + /** * Register global UI event handlers for header actions, forms, tags, and overlays. */ From 82ee540c1f0b9195b9519a17bc098a3c7f0c8cb2 Mon Sep 17 00:00:00 2001 From: Bheb Developer Date: Mon, 25 Aug 2025 05:15:05 +0530 Subject: [PATCH 3/5] feat: enhance import functionality with options to add or replace existing data and added confirmation dialog --- README.md | 6 ++ js/app.js | 278 +++++++++++++++++++++++++++++++----------------------- 2 files changed, 166 insertions(+), 118 deletions(-) diff --git a/README.md b/README.md index bdd0837..55d0f59 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,12 @@ ES modules variant (optional, modern browsers): - New format: { profileName: string, items: Item[] } - Backward‑compatible: plain array of items +Import behavior when existing data is present: +- If you already have snippets or a profile set, importing will prompt you with options: + - Add to Existing: Only new items are added. Exact duplicates (same text, desc, and sensitive flag) are skipped. + - Replace All: Clears all existing snippets and profile after a final confirmation, then imports everything from the file. +- A final confirmation dialog protects against accidental data loss when choosing Replace All. + Example (JSON, recommended): ```json diff --git a/js/app.js b/js/app.js index 2828e90..a3f407a 100644 --- a/js/app.js +++ b/js/app.js @@ -881,9 +881,10 @@ class CompyApp { const hasExistingData = currentState.items.length > 0 || currentState.profileName; let shouldClearExisting = false; + let importOption = 'add'; if (hasExistingData) { - const importOption = await this.showImportOptionsDialog({ + importOption = await this.showImportOptionsDialog({ existingCount: currentState.items.length, existingProfile: currentState.profileName || 'Not set', importingCount: items.length, @@ -894,11 +895,27 @@ class CompyApp { this.showNotification('Import cancelled', 'info'); return; } else if (importOption === 'replace') { + // Final confirmation to prevent accidental data loss + const confirmed = await this.confirmationManager.show({ + title: 'Replace All Data', + message: `This will delete ${currentState.items.length} existing snippets and reset your profile ("${currentState.profileName || 'Not set'}"). This cannot be undone.\n\nProceed with replace?`, + confirmText: 'Replace All', + cancelText: 'Cancel', + variant: 'danger' + }); + if (!confirmed) { + this.showNotification('Replace cancelled', 'info'); + return; + } shouldClearExisting = true; } // if importOption === 'add', we just continue without clearing } + // Prepare duplicate-detection set (based on existing items when adding) + const buildSig = (i) => `${(i.text || '').trim()}||${(i.desc || '').trim()}||${i.sensitive ? '1' : '0'}`; + const dedupeSet = shouldClearExisting ? new Set() : new Set((currentState.items || []).map(buildSig)); + // Clear existing data if user chose replace if (shouldClearExisting) { this.clearAllData(); @@ -910,13 +927,19 @@ class CompyApp { } let importCount = 0; + let skippedCount = 0; for (const item of items) { - if (this.addImportedItem(item)) { + if (this.addImportedItem(item, dedupeSet)) { importCount++; + } else { + skippedCount++; } } - this.showNotification(`Imported ${importCount} items`); + const message = skippedCount > 0 + ? `Imported ${importCount} items (${skippedCount} skipped as duplicates or invalid)` + : `Imported ${importCount} items`; + this.showNotification(message); } catch (error) { console.error('JSON import failed:', error); @@ -954,6 +977,7 @@ class CompyApp { // Remove BOM (Byte Order Mark) that may be present in UTF-8 files from Excel const firstLine = parseCSVLine(lines[0].replace(/^\uFEFF/, '')); let headerIndex = 0; // Track where the actual data headers start + let importedProfileName = null; // capture profile name if provided in metadata // PHASE 1: Check for optional profile metadata block // Format: single column 'profileName' followed by data line @@ -966,10 +990,9 @@ class CompyApp { const profileData = parseCSVLine(profileLine); const profileName = (profileData[0] || '').trim(); - // Update profile if valid name provided + // Capture profile to apply later (after choosing Add/Replace) if (profileName) { - console.log('Importing profile name:', profileName); - updateProfile(profileName); + importedProfileName = profileName; } } @@ -1013,9 +1036,10 @@ class CompyApp { const hasExistingData = currentState.items.length > 0 || currentState.profileName; let shouldClearExisting = false; + let importOption = 'add'; if (hasExistingData) { - const importOption = await this.showImportOptionsDialog({ + importOption = await this.showImportOptionsDialog({ existingCount: currentState.items.length, existingProfile: currentState.profileName || 'Not set', importingCount: itemsToImport, @@ -1026,16 +1050,37 @@ class CompyApp { this.showNotification('Import cancelled', 'info'); return; } else if (importOption === 'replace') { + // Final confirmation to prevent accidental data loss + const confirmed = await this.confirmationManager.show({ + title: 'Replace All Data', + message: `This will delete ${currentState.items.length} existing snippets and reset your profile ("${currentState.profileName || 'Not set'}"). This cannot be undone.\n\nProceed with replace?`, + confirmText: 'Replace All', + cancelText: 'Cancel', + variant: 'danger' + }); + if (!confirmed) { + this.showNotification('Replace cancelled', 'info'); + return; + } shouldClearExisting = true; } // if importOption === 'add', we just continue without clearing } + // Prepare duplicate-detection set (based on existing items when adding) + const buildSig = (i) => `${(i.text || '').trim()}||${(i.desc || '').trim()}||${i.sensitive ? '1' : '0'}`; + const dedupeSet = shouldClearExisting ? new Set() : new Set((currentState.items || []).map(buildSig)); + // Clear existing data if user chose replace if (shouldClearExisting) { this.clearAllData(); } + // Update profile after options are confirmed + if (importedProfileName) { + updateProfile(importedProfileName); + } + // PHASE 3: Process data rows let importCount = 0; let skippedCount = 0; @@ -1064,7 +1109,7 @@ class CompyApp { ['1', 'true'].includes((values[columnMapping.sensitive] || '').toLowerCase()) : false, // PHASE 3C: Parse tags with pipe separator - // Format: "tag1|tag2|tag3" -> ['tag1', 'tag2', 'tag3'] + // Format: \"tag1|tag2|tag3\" -> ['tag1', 'tag2', 'tag3'] tags: columnMapping.tags >= 0 ? (values[columnMapping.tags] || '') .split('|') // Split on pipe separator @@ -1073,12 +1118,12 @@ class CompyApp { : [] }; - // PHASE 3D: Validate and import the item - if (this.addImportedItem(itemData)) { + // PHASE 3D: Validate and import the item (with duplicate skipping in 'Add' mode) + if (this.addImportedItem(itemData, dedupeSet)) { importCount++; } else { skippedCount++; - console.warn(`Skipped invalid item on line ${i + 1}:`, itemData); + console.warn(`Skipped duplicate or invalid item on line ${i + 1}:`, itemData); } } catch (lineError) { @@ -1090,7 +1135,7 @@ class CompyApp { // Provide detailed feedback to user const message = skippedCount > 0 - ? `Imported ${importCount} items (${skippedCount} skipped due to validation errors)` + ? `Imported ${importCount} items (${skippedCount} skipped as duplicates or invalid)` : `Imported ${importCount} items`; this.showNotification(message, importCount > 0 ? 'success' : 'info'); @@ -1114,20 +1159,34 @@ class CompyApp { * @param {Object} itemData - Candidate item * @returns {boolean} True if item was accepted */ - addImportedItem(itemData) { + addImportedItem(itemData, dedupeSet = undefined) { + // Validate first const validation = validateItem(itemData); if (!validation.isValid) { console.warn('Skipping invalid item:', validation.errors); return false; } + // Build a simple signature for duplicate detection based on primary fields + // Duplicate criteria: same text + desc + sensitive flag (tags are ignored for matching) + const text = (itemData.text || '').trim(); + const desc = (itemData.desc || '').trim(); + const sensitiveSig = itemData.sensitive ? '1' : '0'; + const signature = `${text}||${desc}||${sensitiveSig}`; + + if (dedupeSet && dedupeSet.has(signature)) { + // Duplicate of existing or previously imported item + return false; + } + upsertItem({ - text: itemData.text, - desc: itemData.desc, + text, + desc, sensitive: !!itemData.sensitive, tags: Array.isArray(itemData.tags) ? itemData.tags : [] }); + if (dedupeSet) dedupeSet.add(signature); return true; } @@ -1169,125 +1228,108 @@ class CompyApp { */ async showImportOptionsDialog({ existingCount, existingProfile, importingCount, importingProfile }) { return new Promise((resolve) => { + // Remove any previous instance to avoid duplicates + const prior = document.getElementById('importOptionsModal'); + if (prior) prior.remove(); + const modal = document.createElement('div'); modal.className = 'modal'; + modal.id = 'importOptionsModal'; + modal.setAttribute('aria-hidden', 'true'); modal.innerHTML = ` -