fix(dragdrop): amélioration sélection répertoire cible, déplacement arborescence et ouverture auto IA (#89)
CI / lint (push) Successful in 1m17s
CI / security (push) Successful in 51s
CI / test (push) Successful in 3m11s
CI / build (push) Successful in 48s
CI / e2e (push) Successful in 11m2s

This commit is contained in:
2026-09-14 15:55:41 -04:00
parent 1db3e0ad2f
commit ba7b49c35b
5 changed files with 144 additions and 22 deletions
+100 -3
View File
@@ -181,14 +181,22 @@ export async function uploadBatch(vault, targetDir, items, options = {}) {
export async function handleVaultDropUpload(vault, targetDir, items) {
if (!items || !items.length) return;
// If target directory is not explicitly passed or if dropping on vault root,
// allow user to choose or confirm the target folder
let chosenTargetDir = targetDir || '';
if (targetDir === undefined || targetDir === null) {
const picked = await promptTargetFolder(vault, chosenTargetDir, items);
if (picked === null) return; // User cancelled
chosenTargetDir = picked;
}
const fileCount = items.filter((x) => !x.isDir).length;
const dirCount = items.filter((x) => x.isDir).length;
const destName = targetDir ? `${vault}/${targetDir}` : vault;
const destName = chosenTargetDir ? `${vault}/${chosenTargetDir}` : vault;
showToast(`${t('upload.uploading', { count: fileCount }) || `Uploading ${fileCount} file(s) to ${destName}...`}`, 'info');
try {
const res = await uploadBatch(vault, targetDir, items);
const res = await uploadBatch(vault, chosenTargetDir, items);
if (res && res.success) {
showToast(t('upload.success', { count: res.uploaded.length }) || `Imported ${res.uploaded.length} file(s) successfully`, 'success');
} else if (res && res.uploaded && res.uploaded.length > 0) {
@@ -203,3 +211,92 @@ export async function handleVaultDropUpload(vault, targetDir, items) {
showToast(`${t('upload.error') || 'Upload failed'}: ${err.message || err}`, 'error');
}
}
/**
* Display a folder picker modal for choosing the upload destination folder.
*
* @param {string} vault - Vault name
* @param {string} currentDir - Initial folder path
* @param {Array} items - Items to upload
* @returns {Promise<string|null>} Selected folder path (empty for root) or null if cancelled.
*/
export async function promptTargetFolder(vault, currentDir = '', items = []) {
let folders = [];
try {
const res = await api(`/api/vault/${encodeURIComponent(vault)}/paths?limit=5000`);
if (res && Array.isArray(res.paths)) {
folders = res.paths
.filter((p) => p.type === 'directory')
.map((p) => p.path)
.sort();
}
} catch (e) {
console.warn('Could not fetch vault directory paths:', e);
}
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.className = 'obsigate-modal-overlay active';
overlay.style.zIndex = '100000';
const fileCount = items.filter((x) => !x.isDir).length;
const dirCount = items.filter((x) => x.isDir).length;
let optionsHtml = `<option value="">/ (${t('upload.vault_root') || 'Racine du vault'})</option>`;
for (const folder of folders) {
const selected = folder === currentDir ? 'selected' : '';
optionsHtml += `<option value="${escapeHtml(folder)}" ${selected}>📁 ${escapeHtml(folder)}</option>`;
}
overlay.innerHTML = `
<div class="obsigate-modal" style="max-width: 480px;">
<div class="modal-header">
<h3>📁 ${t('upload.choose_target_folder') || 'Choisir le dossier de destination'}</h3>
<button class="modal-close" type="button" aria-label="Fermer">×</button>
</div>
<div class="modal-body" style="padding: 16px;">
<p style="margin-bottom: 12px; color: var(--text-muted);">
${t('upload.import_summary', { files: fileCount, dirs: dirCount }) || `Éléments à importer : ${fileCount} fichier(s)${dirCount ? `, ${dirCount} dossier(s)` : ''}`}
</p>
<label style="display: block; font-weight: 500; margin-bottom: 6px;">
${t('upload.target_directory') || 'Dossier de destination dans'} <strong>${escapeHtml(vault)}</strong> :
</label>
<select id="obsigate-upload-folder-select" class="form-select" style="width: 100%; padding: 8px 12px; margin-bottom: 16px; border-radius: 6px; background: var(--bg-secondary); color: var(--text-normal); border: 1px solid var(--border-color);">
${optionsHtml}
</select>
<div style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px;">
<button type="button" class="btn btn-secondary modal-cancel-btn">${t('button.cancel') || 'Annuler'}</button>
<button type="button" class="btn btn-primary modal-confirm-btn">${t('upload.import_button') || 'Importer'}</button>
</div>
</div>
</div>
`;
document.body.appendChild(overlay);
const close = (val) => {
overlay.remove();
resolve(val);
};
overlay.querySelector('.modal-close').addEventListener('click', () => close(null));
overlay.querySelector('.modal-cancel-btn').addEventListener('click', () => close(null));
overlay.addEventListener('click', (e) => {
if (e.target === overlay) close(null);
});
overlay.querySelector('.modal-confirm-btn').addEventListener('click', () => {
const select = overlay.querySelector('#obsigate-upload-folder-select');
close(select ? select.value : '');
});
});
}
function escapeHtml(str) {
return String(str || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}