feat: menu contextuel Déplacer/Dupliquer sidebar + réparation drag & drop interne #201 #BUG-117
This commit is contained in:
Vendored
+7
-2
@@ -239,9 +239,14 @@ async function onDrop(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const isExternalFiles = e.dataTransfer && (
|
||||
// BUG-117: 'items.length > 0' comptait aussi les formats STRING (tree:vault/path/
|
||||
// type) ajoutés par setData au dragstart interne → tout déplacement interne était
|
||||
// classé à tort « upload externe » et avalé silencieusement. Seuls les items de
|
||||
// kind 'file' (ou le type 'Files', ou files non vide) désignent un drop externe,
|
||||
// et un dragState actif (drag interne) prime toujours.
|
||||
const isExternalFiles = !dragState && e.dataTransfer && (
|
||||
(e.dataTransfer.files && e.dataTransfer.files.length > 0) ||
|
||||
(e.dataTransfer.items && e.dataTransfer.items.length > 0) ||
|
||||
Array.from(e.dataTransfer.items || []).some((it) => it.kind === 'file') ||
|
||||
Array.from(e.dataTransfer.types || []).includes('Files')
|
||||
);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -232,7 +232,7 @@ export async function handleVaultDropUpload(vault, targetDir, items) {
|
||||
* @param {Array} items - Items to upload
|
||||
* @returns {Promise<{ vault: string, targetDir: string }|null>} Selected target or null if cancelled.
|
||||
*/
|
||||
export async function promptTargetFolder(initialVault, initialDir = '', items = []) {
|
||||
export async function promptTargetFolder(initialVault, initialDir = '', items = [], opts = {}) {
|
||||
let vaultsList = [];
|
||||
try {
|
||||
const vRes = await api('/api/vaults');
|
||||
@@ -316,12 +316,12 @@ export async function promptTargetFolder(initialVault, initialDir = '', items =
|
||||
overlay.innerHTML = `
|
||||
<div class="obsigate-modal" style="min-width: 460px; max-width: 560px;">
|
||||
<div class="obsigate-modal-header">
|
||||
<span class="obsigate-modal-title">📁 ${escapeHtml(t('upload.choose_target_folder') || "Destination de l'importation")}</span>
|
||||
<span class="obsigate-modal-title">📁 ${escapeHtml(opts.title || t('upload.choose_target_folder') || "Destination de l'importation")}</span>
|
||||
<button class="modal-close modal-btn" type="button" aria-label="Fermer" style="padding: 4px 8px; line-height: 1;">×</button>
|
||||
</div>
|
||||
<div class="obsigate-modal-body">
|
||||
<p style="margin: 0 0 16px 0; color: var(--text-muted); font-size: 0.9rem;">
|
||||
${escapeHtml(t('upload.import_summary', { files: fileCount, dirs: dirCount }) || `Éléments à importer : ${fileCount} fichier(s)${dirCount ? `, ${dirCount} dossier(s)` : ''}`)}
|
||||
${opts.summaryText ? escapeHtml(opts.summaryText) : escapeHtml(t('upload.import_summary', { files: fileCount, dirs: dirCount }) || `Éléments à importer : ${fileCount} fichier(s)${dirCount ? `, ${dirCount} dossier(s)` : ''}`)}
|
||||
</p>
|
||||
|
||||
<div class="modal-form-group">
|
||||
@@ -352,7 +352,7 @@ export async function promptTargetFolder(initialVault, initialDir = '', items =
|
||||
</div>
|
||||
<div class="obsigate-modal-footer">
|
||||
<button type="button" class="modal-btn modal-cancel-btn">${escapeHtml(t('button.cancel') || 'Annuler')}</button>
|
||||
<button type="button" class="modal-btn primary modal-confirm-btn">${escapeHtml(t('upload.import_button') || 'Importer')}</button>
|
||||
<button type="button" class="modal-btn primary modal-confirm-btn">${escapeHtml(opts.confirmLabel || t('upload.import_button') || 'Importer')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -358,6 +358,7 @@
|
||||
"common.error": "Error",
|
||||
"common.loading": "Loading...",
|
||||
"common.move": "Move",
|
||||
"common.duplicate": "Duplicate",
|
||||
"common.never": "Never",
|
||||
"common.next": "Next",
|
||||
"common.no": "No",
|
||||
@@ -1834,6 +1835,11 @@
|
||||
"toast.no_open_file": "No file open",
|
||||
"toast.not_found_tree": "Item not found in tree",
|
||||
"toast.path_copied": "Path copied: {path}",
|
||||
"ui.move_summary": "Item to move: {name}",
|
||||
"toast.moved": "Moved to: {dir}",
|
||||
"toast.move_root": "Moved to vault root",
|
||||
"toast.duplicated": "Duplicated: {name}",
|
||||
"toast.move_cross_vault": "Cross-vault move/duplicate not supported",
|
||||
"toast.reindex_done": "Reindexing complete",
|
||||
"toast.reindex_started": "Reindexing...",
|
||||
"toast.rename_error": "Rename error",
|
||||
|
||||
@@ -358,6 +358,7 @@
|
||||
"common.error": "Erreur",
|
||||
"common.loading": "Chargement...",
|
||||
"common.move": "Déplacer",
|
||||
"common.duplicate": "Dupliquer",
|
||||
"common.never": "Jamais",
|
||||
"common.next": "Suivant",
|
||||
"common.no": "Non",
|
||||
@@ -1834,6 +1835,11 @@
|
||||
"toast.no_open_file": "Aucun fichier ouvert",
|
||||
"toast.not_found_tree": "Élément introuvable dans l'arborescence",
|
||||
"toast.path_copied": "Chemin copié : {path}",
|
||||
"ui.move_summary": "Élément à déplacer : {name}",
|
||||
"toast.moved": "Déplacé vers : {dir}",
|
||||
"toast.move_root": "Déplacé vers la racine de la voute",
|
||||
"toast.duplicated": "Dupliqué : {name}",
|
||||
"toast.move_cross_vault": "Déplacement/duplication entre voutes non supporté",
|
||||
"toast.reindex_done": "Réindexation terminée",
|
||||
"toast.reindex_started": "Réindexation en cours...",
|
||||
"toast.rename_error": "Erreur lors du renommage",
|
||||
|
||||
Reference in New Issue
Block a user