feat: menu contextuel Déplacer/Dupliquer sidebar + réparation drag & drop interne #201 #BUG-117
CI / lint (push) Successful in 3m13s
CI / security (push) Successful in 1m59s
CI / test (push) Successful in 5m16s
CI / build (push) Successful in 1m52s
CI / e2e (push) Successful in 18m2s

This commit is contained in:
2026-10-15 10:02:48 -04:00
parent ed9029d760
commit 411667fd88
19 changed files with 314 additions and 20 deletions
+74
View File
@@ -1826,9 +1826,13 @@ export const ContextMenuManager = {
this._addSeparator();
this._addItem('brain', '🧠 BooksLM', () => { import('./bookslm.js').then(m => m.default.open(this._targetVault, this._targetPath)); }, false);
this._addSeparator();
this._addItem('move', t('common.move'), () => this._moveItem(), isReadonly);
this._addItem('copy', t('common.duplicate'), () => this._duplicateItem(), isReadonly);
this._addItem('edit', 'Renommer', () => this._renameItem(), isReadonly);
this._addItem('trash-2', 'Supprimer', () => this._deleteDirectory(), isReadonly);
} else if (type === 'file') {
this._addItem('move', t('common.move'), () => this._moveItem(), isReadonly);
this._addItem('copy', t('common.duplicate'), () => this._duplicateItem(), isReadonly);
this._addItem('edit', 'Renommer', () => this._renameItem(), isReadonly);
this._addItem('trash-2', 'Supprimer', () => this._deleteFile(), isReadonly);
this._addSeparator();
@@ -1918,6 +1922,76 @@ export const ContextMenuManager = {
FileOperations.confirmDeleteFile(this._targetVault, this._targetPath);
},
// #BUG-117 — Déplacer / Dupliquer via le modal harmonisé promptTargetFolder
// (réutilisé de upload.js, opts pour titre/bouton). Le backend ne supporte
// que le même vault ; choix d'un autre vault = toast d'erreur.
async _pickDestination(title) {
const vault = this._targetVault;
const name = this._targetPath.split('/').pop();
const parent = this._targetPath.includes('/') ? this._targetPath.slice(0, this._targetPath.lastIndexOf('/')) : '';
const { promptTargetFolder } = await import('./upload.js');
// Destination pré-sélectionnée = emplacement actuel (sinon '' = racine,
// ce qui dupliquait/mouvait à la racine par défaut).
const picked = await promptTargetFolder(vault, parent, [], {
title: title + ' — ' + name,
summaryText: t('ui.move_summary', { name: name }),
confirmLabel: title,
});
if (!picked) return null;
if (picked.vault !== vault) {
showToast(t('toast.move_cross_vault'), 'error');
return null;
}
return picked;
},
async _moveItem() {
const vault = this._targetVault;
const path = this._targetPath;
const type = this._targetType;
let picked = null;
try {
picked = await this._pickDestination(t('common.move'));
if (!picked) return;
const result = await api(`/api/move/${encodeURIComponent(vault)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ source_path: path, destination_dir: picked.targetDir }),
});
await refreshSidebarTreePreservingState();
const nextPath = result.new_path;
if (state.currentVault === vault && state.currentPath && (state.currentPath === path || (type === 'directory' && state.currentPath.startsWith(path + '/')))) {
if (type === 'file') {
await openFile(vault, nextPath);
} else {
state.currentPath = nextPath + state.currentPath.slice(path.length);
syncActiveFileTreeItem();
}
}
showToast(picked.targetDir ? t('toast.moved', { dir: picked.targetDir }) : t('toast.move_root'), 'success');
} catch (err) {
showToast(err.message || t('common.error'), 'error');
}
},
async _duplicateItem() {
const vault = this._targetVault;
const path = this._targetPath;
try {
const picked = await this._pickDestination(t('common.duplicate'));
if (!picked) return;
const result = await api(`/api/duplicate/${encodeURIComponent(vault)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ source_path: path, destination_dir: picked.targetDir }),
});
await refreshSidebarTreePreservingState();
showToast(t('toast.duplicated', { name: result.new_path.split('/').pop() }), 'success');
} catch (err) {
showToast(err.message || t('common.error'), 'error');
}
},
_copyPath(path) {
// Try modern clipboard API first, fall back to execCommand for non-secure contexts
if (navigator.clipboard && navigator.clipboard.writeText) {