feat: implement inline rename for files and directories with state preservation

- Replace placeholder inline rename with full implementation
- Add input validation for file/directory names with character restrictions
- Handle file extensions separately during rename operations
- Update current path when renaming open files or parent directories
- Replace refreshVaultTree calls with refreshSidebarTreePreservingState to maintain tree expansion state
- Add keyboard shortcuts (Enter to submit, Escape to cancel)
This commit is contained in:
Bruno Charest 2026-03-30 15:53:42 -04:00
parent 32c1bad1a1
commit 628a664c59

View File

@ -5721,7 +5721,7 @@
showToast(`Dossier "${name}" créé`, 'success'); showToast(`Dossier "${name}" créé`, 'success');
this._closeModal(overlay); this._closeModal(overlay);
await refreshVaultTree(); await refreshSidebarTreePreservingState();
} catch (err) { } catch (err) {
showToast(err.message || 'Erreur lors de la création', 'error'); showToast(err.message || 'Erreur lors de la création', 'error');
createBtn.disabled = false; createBtn.disabled = false;
@ -5831,7 +5831,7 @@
showToast(`Fichier "${name}" créé`, 'success'); showToast(`Fichier "${name}" créé`, 'success');
this._closeModal(overlay); this._closeModal(overlay);
await refreshVaultTree(); await refreshSidebarTreePreservingState();
openFile(vault, path); openFile(vault, path);
} catch (err) { } catch (err) {
showToast(err.message || 'Erreur lors de la création', 'error'); showToast(err.message || 'Erreur lors de la création', 'error');
@ -5850,7 +5850,114 @@
}, },
async startInlineRename(vault, path, type) { async startInlineRename(vault, path, type) {
showToast('Fonctionnalité de renommage inline en cours de développement', 'info'); const item = document.querySelector(`.tree-item[data-vault="${CSS.escape(vault)}"][data-path="${CSS.escape(path)}"]`);
if (!item) {
showToast('Élément introuvable dans larborescence', 'error');
return;
}
const textNode = Array.from(item.childNodes).find((node) => node.nodeType === Node.TEXT_NODE && node.textContent.trim());
if (!textNode) {
showToast('Impossible de renommer cet élément', 'error');
return;
}
const originalText = textNode.textContent;
const trimmedOriginal = originalText.trim();
const currentName = path.split('/').pop() || trimmedOriginal;
const baseName = type === 'file' ? currentName.replace(/(\.[^./\\]+)$/i, '') : currentName;
const extension = type === 'file' ? (currentName.match(/(\.[^./\\]+)$/i)?.[1] || '') : '';
const input = document.createElement('input');
input.type = 'text';
input.className = 'sidebar-item-input';
input.value = baseName;
textNode.textContent = ' ';
const badge = item.querySelector('.badge-small');
if (badge) {
item.insertBefore(input, badge);
} else {
item.appendChild(input);
}
const restore = () => {
input.remove();
textNode.textContent = originalText;
};
const validateName = (name) => {
if (!name.trim()) return 'Le nom ne peut pas être vide';
if (/[/\\:*?"<>|]/.test(name)) return 'Caractères interdits: / \\ : * ? " < > |';
return null;
};
const submit = async () => {
const name = input.value.trim();
const error = validateName(name);
if (error) {
showToast(error, 'error');
input.focus();
input.select();
return;
}
const newName = `${name}${extension}`;
if (newName === currentName) {
restore();
return;
}
input.disabled = true;
try {
const endpoint = type === 'directory' ? `/api/directory/${encodeURIComponent(vault)}` : `/api/file/${encodeURIComponent(vault)}`;
const result = await api(endpoint, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path, new_name: newName }),
});
const nextPath = result.new_path;
await refreshSidebarTreePreservingState();
if (type === 'file' && currentVault === vault && currentPath === path) {
await openFile(vault, nextPath);
} else if (type === 'directory' && currentVault === vault && currentPath && (currentPath === path || currentPath.startsWith(`${path}/`))) {
const suffix = currentPath === path ? '' : currentPath.slice(path.length);
currentPath = `${nextPath}${suffix}`;
await focusPathInSidebar(vault, currentPath, { alignToTop: false });
}
showToast(type === 'directory' ? 'Dossier renommé' : 'Fichier renommé', 'success');
} catch (err) {
input.disabled = false;
showToast(err.message || 'Erreur lors du renommage', 'error');
input.focus();
input.select();
return;
}
};
input.addEventListener('click', (e) => e.stopPropagation());
input.addEventListener('keydown', async (e) => {
e.stopPropagation();
if (e.key === 'Enter') {
e.preventDefault();
await submit();
}
if (e.key === 'Escape') {
e.preventDefault();
restore();
}
});
input.addEventListener('blur', async () => {
if (!input.disabled) {
await submit();
}
});
input.focus();
input.setSelectionRange(0, input.value.length);
}, },
confirmDeleteDirectory(vault, path) { confirmDeleteDirectory(vault, path) {
@ -5900,7 +6007,7 @@
showToast(`Dossier supprimé (${result.deleted_count} fichiers)`, 'success'); showToast(`Dossier supprimé (${result.deleted_count} fichiers)`, 'success');
this._closeModal(overlay); this._closeModal(overlay);
await refreshVaultTree(); await refreshSidebarTreePreservingState();
if (currentVault === vault && currentPath && currentPath.startsWith(path)) { if (currentVault === vault && currentPath && currentPath.startsWith(path)) {
showWelcome(); showWelcome();
@ -5962,7 +6069,7 @@
showToast('Fichier supprimé', 'success'); showToast('Fichier supprimé', 'success');
this._closeModal(overlay); this._closeModal(overlay);
await refreshVaultTree(); await refreshSidebarTreePreservingState();
if (currentVault === vault && currentPath === path) { if (currentVault === vault && currentPath === path) {
showWelcome(); showWelcome();