feat(electron): implement inline metadata editing with save options modal
- Remove Edit button from header, add pencil icons next to editable fields - Add inline editing for Version, Author, and Comment metadata fields - Implement save options modal with overwrite/save-as-new functionality - Add file dialog integration for save-as-new option - Fix backend MANIFEST_FILENAME error by using correct string literals - Add CSS styling for editable fields and save options modal - Clean up debug logging code BREAKING CHANGE: Edit button removed from UI in favor of inline editing
This commit is contained in:
@@ -15,11 +15,16 @@ const openBundleBtn = document.getElementById('open-bundle-btn');
|
||||
const createBundleBtn = document.getElementById('create-bundle-btn');
|
||||
|
||||
// Bundle action buttons
|
||||
const editBundleBtn = document.getElementById('edit-bundle-btn');
|
||||
const signBundleBtn = document.getElementById('sign-bundle-btn');
|
||||
const validateBundleBtn = document.getElementById('validate-bundle-btn');
|
||||
const fillBundleBtn = document.getElementById('fill-bundle-btn');
|
||||
const clearBundleBtn = document.getElementById('clear-bundle-btn');
|
||||
const saveMetadataBtn = document.getElementById('save-metadata-btn');
|
||||
|
||||
// Save options modal elements
|
||||
const saveOptionsModal = document.getElementById('save-options-modal');
|
||||
const overwriteBundleBtn = document.getElementById('overwrite-bundle-btn');
|
||||
const saveAsNewBtn = document.getElementById('save-as-new-btn');
|
||||
|
||||
// Bundle display
|
||||
const bundleTitle = document.getElementById('bundle-title');
|
||||
@@ -52,6 +57,8 @@ const fillProgressView = document.getElementById('fill-progress-view');
|
||||
const fillProgressList = document.getElementById('fill-progress-list');
|
||||
|
||||
let currentBundlePath = null;
|
||||
let hasUnsavedChanges = false;
|
||||
let originalMetadata = {};
|
||||
|
||||
// --- INITIALIZATION ---
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
@@ -93,6 +100,9 @@ function setupEventListeners() {
|
||||
signBundleBtn.addEventListener('click', handleSignBundle);
|
||||
validateBundleBtn.addEventListener('click', handleValidateBundle);
|
||||
clearBundleBtn.addEventListener('click', handleClearBundle);
|
||||
saveMetadataBtn.addEventListener('click', showSaveOptionsModal);
|
||||
overwriteBundleBtn.addEventListener('click', () => handleSaveMetadata(false));
|
||||
saveAsNewBtn.addEventListener('click', () => handleSaveMetadata(true));
|
||||
fillBundleBtn.addEventListener('click', async () => {
|
||||
if (!currentBundlePath) {
|
||||
showModal('Error', 'No bundle is currently open.');
|
||||
@@ -415,6 +425,15 @@ function displayBundleInfo(report) {
|
||||
|
||||
const { manifest, signature, validation, plugins } = report;
|
||||
|
||||
// Store original metadata for comparison
|
||||
originalMetadata = {
|
||||
bundleVersion: manifest.bundleVersion || '',
|
||||
bundleAuthor: manifest.bundleAuthor || '',
|
||||
bundleComment: manifest.bundleComment || ''
|
||||
};
|
||||
hasUnsavedChanges = false;
|
||||
updateSaveButtonVisibility();
|
||||
|
||||
// Set bundle title
|
||||
bundleTitle.textContent = manifest.bundleName || 'Untitled Bundle';
|
||||
|
||||
@@ -443,16 +462,19 @@ function displayBundleInfo(report) {
|
||||
<div class="card">
|
||||
<div class="card-header"><h3>Manifest Details</h3></div>
|
||||
<div class="card-content">
|
||||
<p><strong>Version:</strong> ${manifest.bundleVersion || 'N/A'}</p>
|
||||
<p><strong>Author:</strong> ${manifest.bundleAuthor || 'N/A'}</p>
|
||||
${createEditableField('Version', 'bundleVersion', manifest.bundleVersion || 'N/A')}
|
||||
${createEditableField('Author', 'bundleAuthor', manifest.bundleAuthor || 'N/A')}
|
||||
<p><strong>Bundled On:</strong> ${manifest.bundledOn || 'N/A'}</p>
|
||||
<p><strong>Comment:</strong> ${manifest.bundleComment || 'N/A'}</p>
|
||||
${createEditableField('Comment', 'bundleComment', manifest.bundleComment || 'N/A')}
|
||||
${manifest.bundleAuthorKeyFingerprint ? `<p><strong>Author Key:</strong> ${manifest.bundleAuthorKeyFingerprint}</p>` : ''}
|
||||
${manifest.bundleSignature ? `<p><strong>Signature:</strong> <span class="signature">${manifest.bundleSignature}</span></p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add event listeners for edit functionality
|
||||
setupEditableFieldListeners();
|
||||
|
||||
// --- Plugins Tab ---
|
||||
pluginsList.innerHTML = '';
|
||||
if (plugins && Object.keys(plugins).length > 0) {
|
||||
@@ -493,3 +515,230 @@ function displayBundleInfo(report) {
|
||||
// Reset to overview tab by default
|
||||
switchTab('overview-tab');
|
||||
}
|
||||
|
||||
// Helper function to create editable fields with pencil icons
|
||||
function createEditableField(label, fieldName, value) {
|
||||
const displayValue = value === 'N/A' ? '' : value;
|
||||
return `
|
||||
<p class="editable-field">
|
||||
<strong>${label}:</strong>
|
||||
<span class="field-display" data-field="${fieldName}">${value}</span>
|
||||
<span class="field-edit hidden" data-field="${fieldName}">
|
||||
<input type="text" class="field-input" data-field="${fieldName}" value="${displayValue}">
|
||||
</span>
|
||||
<button class="edit-icon" data-field="${fieldName}" title="Edit ${label}">✏️</button>
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
|
||||
// Setup event listeners for editable fields
|
||||
function setupEditableFieldListeners() {
|
||||
const editIcons = document.querySelectorAll('.edit-icon');
|
||||
const fieldInputs = document.querySelectorAll('.field-input');
|
||||
|
||||
editIcons.forEach(icon => {
|
||||
icon.addEventListener('click', (e) => {
|
||||
const fieldName = e.target.dataset.field;
|
||||
toggleFieldEdit(fieldName, true);
|
||||
});
|
||||
});
|
||||
|
||||
fieldInputs.forEach(input => {
|
||||
input.addEventListener('blur', (e) => {
|
||||
const fieldName = e.target.dataset.field;
|
||||
saveFieldEdit(fieldName);
|
||||
});
|
||||
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const fieldName = e.target.dataset.field;
|
||||
saveFieldEdit(fieldName);
|
||||
} else if (e.key === 'Escape') {
|
||||
const fieldName = e.target.dataset.field;
|
||||
cancelFieldEdit(fieldName);
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
checkForChanges();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle between display and edit mode for a field
|
||||
function toggleFieldEdit(fieldName, editMode) {
|
||||
const displaySpan = document.querySelector(`.field-display[data-field="${fieldName}"]`);
|
||||
const editSpan = document.querySelector(`.field-edit[data-field="${fieldName}"]`);
|
||||
const input = document.querySelector(`.field-input[data-field="${fieldName}"]`);
|
||||
const icon = document.querySelector(`.edit-icon[data-field="${fieldName}"]`);
|
||||
|
||||
if (editMode) {
|
||||
displaySpan.classList.add('hidden');
|
||||
editSpan.classList.remove('hidden');
|
||||
icon.textContent = '✅';
|
||||
icon.title = 'Save';
|
||||
input.focus();
|
||||
input.select();
|
||||
} else {
|
||||
displaySpan.classList.remove('hidden');
|
||||
editSpan.classList.add('hidden');
|
||||
icon.textContent = '✏️';
|
||||
icon.title = `Edit ${fieldName}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Save field edit and update display
|
||||
function saveFieldEdit(fieldName) {
|
||||
const input = document.querySelector(`.field-input[data-field="${fieldName}"]`);
|
||||
const displaySpan = document.querySelector(`.field-display[data-field="${fieldName}"]`);
|
||||
|
||||
const newValue = input.value.trim();
|
||||
const displayValue = newValue || 'N/A';
|
||||
|
||||
displaySpan.textContent = displayValue;
|
||||
toggleFieldEdit(fieldName, false);
|
||||
checkForChanges();
|
||||
}
|
||||
|
||||
// Cancel field edit and restore original value
|
||||
function cancelFieldEdit(fieldName) {
|
||||
const input = document.querySelector(`.field-input[data-field="${fieldName}"]`);
|
||||
const originalValue = originalMetadata[fieldName] || '';
|
||||
|
||||
input.value = originalValue;
|
||||
toggleFieldEdit(fieldName, false);
|
||||
}
|
||||
|
||||
// Check if any fields have been modified
|
||||
function checkForChanges() {
|
||||
const inputs = document.querySelectorAll('.field-input');
|
||||
let hasChanges = false;
|
||||
|
||||
inputs.forEach(input => {
|
||||
const fieldName = input.dataset.field;
|
||||
const currentValue = input.value.trim();
|
||||
const originalValue = originalMetadata[fieldName] || '';
|
||||
|
||||
if (currentValue !== originalValue) {
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
|
||||
hasUnsavedChanges = hasChanges;
|
||||
updateSaveButtonVisibility();
|
||||
}
|
||||
|
||||
// Show/hide save button based on changes
|
||||
function updateSaveButtonVisibility() {
|
||||
if (hasUnsavedChanges) {
|
||||
saveMetadataBtn.classList.remove('hidden');
|
||||
} else {
|
||||
saveMetadataBtn.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Show save options modal
|
||||
function showSaveOptionsModal() {
|
||||
if (!currentBundlePath || !hasUnsavedChanges) return;
|
||||
saveOptionsModal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Hide save options modal
|
||||
function hideSaveOptionsModal() {
|
||||
saveOptionsModal.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Handle save metadata with option for save as new
|
||||
async function handleSaveMetadata(saveAsNew = false) {
|
||||
if (!currentBundlePath || !hasUnsavedChanges) return;
|
||||
|
||||
// Hide the modal first
|
||||
hideSaveOptionsModal();
|
||||
|
||||
const inputs = document.querySelectorAll('.field-input');
|
||||
const updatedMetadata = {};
|
||||
|
||||
inputs.forEach(input => {
|
||||
const fieldName = input.dataset.field;
|
||||
const value = input.value.trim();
|
||||
if (value !== originalMetadata[fieldName]) {
|
||||
// Convert camelCase to snake_case for backend
|
||||
const backendFieldName = fieldName.replace(/([A-Z])/g, '_$1').toLowerCase();
|
||||
updatedMetadata[backendFieldName] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(updatedMetadata).length === 0) {
|
||||
hasUnsavedChanges = false;
|
||||
updateSaveButtonVisibility();
|
||||
return;
|
||||
}
|
||||
|
||||
let targetPath = currentBundlePath;
|
||||
|
||||
if (saveAsNew) {
|
||||
// Show file save dialog for new bundle
|
||||
const result = await ipcRenderer.invoke('show-save-dialog', {
|
||||
defaultPath: currentBundlePath.replace('.fbundle', '_edited.fbundle'),
|
||||
filters: [{ name: 'Bundle Files', extensions: ['fbundle'] }]
|
||||
});
|
||||
|
||||
if (result.canceled) {
|
||||
return; // User canceled the save dialog
|
||||
}
|
||||
|
||||
targetPath = result.filePath;
|
||||
|
||||
// Copy the original bundle to the new location first
|
||||
try {
|
||||
await ipcRenderer.invoke('copy-file', {
|
||||
source: currentBundlePath,
|
||||
destination: targetPath
|
||||
});
|
||||
} catch (error) {
|
||||
showModal('Copy Error', `Failed to copy bundle: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
showSpinner();
|
||||
const result = await ipcRenderer.invoke('edit-bundle', {
|
||||
bundlePath: targetPath,
|
||||
updatedManifest: updatedMetadata
|
||||
});
|
||||
hideSpinner();
|
||||
|
||||
if (result.success) {
|
||||
// Update original metadata to reflect saved changes
|
||||
Object.keys(updatedMetadata).forEach(backendKey => {
|
||||
const frontendKey = backendKey.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase());
|
||||
originalMetadata[frontendKey] = updatedMetadata[backendKey];
|
||||
});
|
||||
|
||||
hasUnsavedChanges = false;
|
||||
updateSaveButtonVisibility();
|
||||
|
||||
if (saveAsNew) {
|
||||
showModal('Success', `New bundle created successfully at: ${targetPath}`);
|
||||
// Open the new bundle
|
||||
currentBundlePath = targetPath;
|
||||
await reloadCurrentBundle();
|
||||
} else {
|
||||
showModal('Success', 'Metadata updated successfully!');
|
||||
// Reload current bundle to reflect changes
|
||||
await reloadCurrentBundle();
|
||||
}
|
||||
} else {
|
||||
showModal('Save Error', `Failed to save metadata: ${result.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to reload current bundle
|
||||
async function reloadCurrentBundle() {
|
||||
if (!currentBundlePath) return;
|
||||
|
||||
const result = await ipcRenderer.invoke('open-bundle', currentBundlePath);
|
||||
if (result.success) {
|
||||
displayBundleInfo(result.report);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user