/** * Configuration diff viewer. * * Shows what changed in configuration before saving. */ const ConfigDiffViewer = { /** * Original configuration state (before changes). */ originalConfigs: new Map(), /** * Store original configuration for a plugin. * * @param {string} pluginId - Plugin identifier * @param {Object} config - Original configuration */ storeOriginal(pluginId, config) { this.originalConfigs.set(pluginId, JSON.parse(JSON.stringify(config))); }, /** * Get original configuration for a plugin. * * @param {string} pluginId - Plugin identifier * @returns {Object|null} Original configuration */ getOriginal(pluginId) { return this.originalConfigs.get(pluginId) || null; }, /** * Clear stored original configuration. * * @param {string} pluginId - Plugin identifier */ clearOriginal(pluginId) { this.originalConfigs.delete(pluginId); }, /** * Compare two configuration objects and return differences. * * @param {Object} oldConfig - Old configuration * @param {Object} newConfig - New configuration * @returns {Object} Differences object with added, removed, and changed keys */ compare(oldConfig, newConfig) { const differences = { added: {}, removed: {}, changed: {}, unchanged: {} }; // Get all keys from both configs const allKeys = new Set([ ...Object.keys(oldConfig || {}), ...Object.keys(newConfig || {}) ]); for (const key of allKeys) { const oldValue = oldConfig?.[key]; const newValue = newConfig?.[key]; if (!(key in (oldConfig || {}))) { // Key was added differences.added[key] = newValue; } else if (!(key in (newConfig || {}))) { // Key was removed differences.removed[key] = oldValue; } else if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) { // Key was changed differences.changed[key] = { old: oldValue, new: newValue }; } else { // Key unchanged differences.unchanged[key] = oldValue; } } return differences; }, /** * Check if there are any differences. * * @param {Object} differences - Differences object from compare() * @returns {boolean} True if there are changes */ hasChanges(differences) { return Object.keys(differences.added).length > 0 || Object.keys(differences.removed).length > 0 || Object.keys(differences.changed).length > 0; }, /** * Format differences for display. * * @param {Object} differences - Differences object * @returns {string} HTML formatted diff */ formatDiff(differences) { const parts = []; // Added keys if (Object.keys(differences.added).length > 0) { parts.push(`

Added

${Object.entries(differences.added).map(([key, value]) => `
${this.escapeHtml(key)} =
${this.escapeHtml(JSON.stringify(value, null, 2))}
`).join('')}
`); } // Removed keys if (Object.keys(differences.removed).length > 0) { parts.push(`

Removed

${Object.entries(differences.removed).map(([key, value]) => `
${this.escapeHtml(key)} =
${this.escapeHtml(JSON.stringify(value, null, 2))}
`).join('')}
`); } // Changed keys if (Object.keys(differences.changed).length > 0) { parts.push(`

Changed

${Object.entries(differences.changed).map(([key, change]) => `
${this.escapeHtml(key)}
Old Value:
${this.escapeHtml(JSON.stringify(change.old, null, 2))}
New Value:
${this.escapeHtml(JSON.stringify(change.new, null, 2))}
`).join('')}
`); } if (parts.length === 0) { return '
No changes detected
'; } return parts.join(''); }, /** * Show diff modal before saving. * * @param {string} pluginId - Plugin identifier * @param {Object} newConfig - New configuration * @returns {Promise} Promise resolving to true if user confirms, false if cancelled */ async showDiffModal(pluginId, newConfig) { return new Promise((resolve) => { const original = this.getOriginal(pluginId); if (!original) { // No original to compare, proceed without diff resolve(true); return; } const differences = this.compare(original, newConfig); if (!this.hasChanges(differences)) { // No changes, proceed without showing modal resolve(true); return; } // Create modal const modalContainer = document.createElement('div'); modalContainer.id = 'config-diff-modal-container'; modalContainer.className = 'fixed inset-0 z-50 overflow-y-auto'; modalContainer.innerHTML = `

Review Configuration Changes

${this.formatDiff(differences)}
`; document.body.appendChild(modalContainer); // Store resolve function globally (hack for onclick handlers) window.__configDiffResolve = resolve; // Attach event listeners const confirmBtn = modalContainer.querySelector('#config-diff-confirm-btn'); const cancelBtn = modalContainer.querySelector('#config-diff-cancel-btn'); confirmBtn.addEventListener('click', () => { modalContainer.remove(); delete window.__configDiffResolve; resolve(true); }); cancelBtn.addEventListener('click', () => { modalContainer.remove(); delete window.__configDiffResolve; resolve(false); }); }); }, /** * Escape HTML to prevent XSS. */ escapeHtml(text) { if (typeof text !== 'string') { text = String(text); } const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } }; // Export if (typeof module !== 'undefined' && module.exports) { module.exports = ConfigDiffViewer; } else { window.ConfigDiffViewer = ConfigDiffViewer; }