feat(ui): handle errors on folder creation or folder/file renaming

- protect file_management_service::rename_file with validate_storage_name
 - remove specific rename modal and use the generic modal class (less duplicate)
 - handle errors on modal action: do not close the modal on error and display this error
 - hide "Go to parent folder" contextMenu if section is files and folder is the same as current one
This commit is contained in:
Edouard Vanbelle
2026-05-09 00:06:30 +02:00
parent 52f4a865a9
commit 7697f3d35b
9 changed files with 160 additions and 174 deletions
+17 -112
View File
@@ -11,6 +11,7 @@ import { showConfirmDialog, ui } from '../../app/ui.js';
import { getCsrfHeaders } from '../../core/csrf.js';
import { escapeHtml } from '../../core/formatters.js';
import { i18n } from '../../core/i18n.js';
import { Modal } from '../../core/modal.js';
import { favorites } from '../library/favorites.js';
import { musicView } from '../library/music.js';
import { fileSharing } from '../sharing/fileSharing.js';
@@ -69,7 +70,8 @@ const contextMenus = {
const option = document.getElementById('open-parent-folder-option');
if (!option) return;
const folderId = app?.contextMenuTargetFile?.folder_id;
option.classList.toggle('hidden', !folderId);
const alreadyViewing = folderId && folderId === app?.currentPath;
option.classList.toggle('hidden', !folderId || alreadyViewing);
},
syncAddToPlaylistOption() {
@@ -120,11 +122,14 @@ const contextMenus = {
ui.closeContextMenu();
});
document.getElementById('rename-folder-option').addEventListener('click', () => {
if (app.contextMenuTargetFolder) {
this.showRenameDialog(app.contextMenuTargetFolder);
}
document.getElementById('rename-folder-option').addEventListener('click', async () => {
const folder = app.contextMenuTargetFolder;
ui.closeContextMenu();
if (!folder) return;
const newName = await Modal.promptRename(folder.name, true, async (name) => {
await fileOps.renameFolder(folder.id, name);
});
if (newName) loadFiles();
});
document.getElementById('move-folder-option').addEventListener('click', () => {
@@ -239,11 +244,14 @@ const contextMenus = {
ui.closeFileContextMenu();
});
document.getElementById('rename-file-option').addEventListener('click', () => {
if (app.contextMenuTargetFile) {
this.showRenameFileDialog(app.contextMenuTargetFile);
}
document.getElementById('rename-file-option').addEventListener('click', async () => {
const file = app.contextMenuTargetFile;
ui.closeFileContextMenu();
if (!file) return;
const newName = await Modal.promptRename(file.name, false, async (name) => {
await fileOps.renameFile(file.id, name);
});
if (newName) loadFiles();
});
document.getElementById('move-file-option').addEventListener('click', () => {
@@ -281,23 +289,6 @@ const contextMenus = {
}
});
// Rename dialog events
const renameCancelBtn = document.getElementById('rename-cancel-btn');
const renameConfirmBtn = document.getElementById('rename-confirm-btn');
const renameInput = document.getElementById('rename-input');
renameCancelBtn.addEventListener('click', this.closeRenameDialog);
renameConfirmBtn.addEventListener('click', () => contextMenus.renameItem());
// Rename on Enter key
renameInput.addEventListener('keyup', (e) => {
if (e.key === 'Enter') {
contextMenus.renameItem();
} else if (e.key === 'Escape') {
this.closeRenameDialog();
}
});
// Move dialog events
const moveCancelBtn = document.getElementById('move-cancel-btn');
const moveConfirmBtn = document.getElementById('move-confirm-btn');
@@ -386,55 +377,6 @@ const contextMenus = {
});
},
/**
* Show rename dialog for a folder
* @param {Object} folder - Folder object
*/
showRenameDialog(folder) {
const renameInput = document.getElementById('rename-input');
const renameDialog = document.getElementById('rename-dialog');
app.renameMode = 'folder';
// Store the folder reference so it survives context menu cleanup
app.renameTarget = folder;
renameInput.value = folder.name;
// Update header text
const headerSpan = renameDialog.querySelector('.rename-dialog-header span');
if (headerSpan) headerSpan.textContent = i18n.t('dialogs.rename_folder');
renameDialog?.classList.remove('hidden');
renameInput.focus();
renameInput.select();
},
/**
* Show rename dialog for a file
* @param {Object} file - File object
*/
showRenameFileDialog(file) {
const renameInput = document.getElementById('rename-input');
const renameDialog = document.getElementById('rename-dialog');
app.renameMode = 'file';
// Store the file reference so it survives context menu cleanup
app.renameTarget = file;
renameInput.value = file.name;
// Update header text
const headerSpan = renameDialog.querySelector('.rename-dialog-header span');
if (headerSpan) headerSpan.textContent = i18n.t('dialogs.rename_file');
renameDialog?.classList.remove('hidden');
renameInput.focus();
renameInput.select();
},
/**
* Close rename dialog
*/
closeRenameDialog() {
document.getElementById('rename-dialog')?.classList.add('hidden');
app.contextMenuTargetFolder = null;
app.renameTarget = null;
},
/**
* Show move dialog for a file or folder
* @param {Object} item - File or folder object
@@ -508,43 +450,6 @@ const contextMenus = {
app.contextMenuTargetFolder = null;
},
/**
* Rename the selected folder or file
*/
async renameItem() {
const newName = document.getElementById('rename-input').value.trim();
if (!newName) {
alert(i18n.t('errors.empty_name'));
return;
}
// Use renameTarget which was saved before the context menu was closed
const target = app.renameTarget;
if (!target) {
console.error('No rename target available');
return;
}
if (app.renameMode === 'file') {
const success = await fileOps.renameFile(target.id, newName);
if (success) {
contextMenus.closeRenameDialog();
loadFiles();
}
} else if (app.renameMode === 'folder') {
const success = await fileOps.renameFolder(target.id, newName);
if (success) {
contextMenus.closeRenameDialog();
loadFiles();
}
}
},
// Keep backward compat
renameFolder() {
return contextMenus.renameItem();
},
/**
* Load folders for the move dialog with navigation support
* Shows subfolders of the specified parent folder and allows navigation
+41 -25
View File
@@ -685,6 +685,10 @@ const fileOps = {
}
},
/**
* Create a new folder
* @param {string} name - Folder name
*/
/**
* Create a new folder
* @param {string} name - Folder name
@@ -718,13 +722,20 @@ const fileOps = {
ui.showNotification('Folder created', `"${name}" created successfully`);
} else {
const errorData = await response.text();
console.error('Create folder error:', errorData);
ui.showNotification('Error', 'Error creating the folder');
const errorText = await response.text();
console.error('Create folder error:', errorText);
let errorMessage = 'Unknown error';
try {
const errorData = JSON.parse(errorText);
errorMessage = errorData.error || response.statusText;
} catch (_e) {
errorMessage = errorText || response.statusText;
}
throw new Error(errorMessage);
}
} catch (error) {
console.error('Error creating folder:', error);
ui.showNotification('Error', 'Error creating the folder');
throw error;
}
},
@@ -978,6 +989,12 @@ const fileOps = {
* @param {string} newName - New file name
* @returns {Promise<boolean>} - Success status
*/
/**
* Rename a file
* @param {string} fileId - File ID
* @param {string} newName - New file name
* @returns {Promise<string|null>} - null on success, error message string on failure
*/
async renameFile(fileId, newName) {
try {
console.log(`Renaming file ${fileId} to "${newName}"`);
@@ -995,24 +1012,22 @@ const fileOps = {
if (response.ok) {
ui.showNotification(i18n.t('notifications.file_renamed'), i18n.t('notifications.file_renamed_to', { name: newName }));
return true;
} else {
const errorText = await response.text();
console.error('Error response:', errorText);
let errorMessage = 'Unknown error';
try {
const errorData = JSON.parse(errorText);
errorMessage = errorData.error || response.statusText;
} catch (_e) {
errorMessage = errorText || response.statusText;
throw new Error(errorData.error || response.statusText);
} catch (parseError) {
if (parseError instanceof SyntaxError) {
throw new Error(errorText || response.statusText);
}
throw parseError;
}
ui.showNotification('Error', `Error renaming the file: ${errorMessage}`);
return false;
}
} catch (error) {
console.error('Error renaming file:', error);
ui.showNotification('Error', 'Error renaming the file');
return false;
throw error;
}
},
@@ -1022,6 +1037,12 @@ const fileOps = {
* @param {string} newName - New folder name
* @returns {Promise<boolean>} - Success status
*/
/**
* Rename a folder
* @param {string} folderId - Folder ID
* @param {string} newName - New folder name
* @returns {Promise<string|null>} - null on success, error message string on failure
*/
async renameFolder(folderId, newName) {
try {
console.log(`Renaming folder ${folderId} to "${newName}"`);
@@ -1039,28 +1060,23 @@ const fileOps = {
if (response.ok) {
ui.showNotification('Folder renamed', `Folder renamed to "${newName}"`);
return true;
} else {
const errorText = await response.text();
console.error('Error response:', errorText);
let errorMessage = 'Unknown error';
try {
// Try to parse as JSON
const errorData = JSON.parse(errorText);
errorMessage = errorData.error || response.statusText;
} catch (_e) {
// If not JSON, use text as is
errorMessage = errorText || response.statusText;
throw new Error(errorData.error || response.statusText);
} catch (parseError) {
if (parseError instanceof SyntaxError) {
throw new Error(errorText || response.statusText);
}
throw parseError;
}
ui.showNotification('Error', `Error renaming the folder: ${errorMessage}`);
return false;
}
} catch (error) {
console.error('Error renaming folder:', error);
ui.showNotification('Error', 'Error renaming the folder');
return false;
throw error;
}
},