fix(dragdrop): modale de sélection de voûte et sous-répertoire alignée avec le thème ObsiGate (#89)
This commit is contained in:
+106
-40
@@ -181,22 +181,25 @@ 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 chosenVault = vault;
|
||||
let chosenTargetDir = targetDir || '';
|
||||
|
||||
// If target directory is not explicitly passed or if dropping from global overlay,
|
||||
// allow user to choose or confirm the target vault and folder
|
||||
if (targetDir === undefined || targetDir === null) {
|
||||
const picked = await promptTargetFolder(vault, chosenTargetDir, items);
|
||||
if (picked === null) return; // User cancelled
|
||||
chosenTargetDir = picked;
|
||||
chosenVault = picked.vault || chosenVault;
|
||||
chosenTargetDir = picked.targetDir || '';
|
||||
}
|
||||
|
||||
const fileCount = items.filter((x) => !x.isDir).length;
|
||||
const destName = chosenTargetDir ? `${vault}/${chosenTargetDir}` : vault;
|
||||
const destName = chosenTargetDir ? `${chosenVault}/${chosenTargetDir}` : chosenVault;
|
||||
|
||||
showToast(`${t('upload.uploading', { count: fileCount }) || `Uploading ${fileCount} file(s) to ${destName}...`}`, 'info');
|
||||
|
||||
try {
|
||||
const res = await uploadBatch(vault, chosenTargetDir, items);
|
||||
const res = await uploadBatch(chosenVault, 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) {
|
||||
@@ -213,27 +216,57 @@ export async function handleVaultDropUpload(vault, targetDir, items) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a folder picker modal for choosing the upload destination folder.
|
||||
* Display a folder picker modal for choosing the upload destination vault and folder.
|
||||
*
|
||||
* @param {string} vault - Vault name
|
||||
* @param {string} currentDir - Initial folder path
|
||||
* @param {string} initialVault - Default vault name
|
||||
* @param {string} initialDir - Initial folder path
|
||||
* @param {Array} items - Items to upload
|
||||
* @returns {Promise<string|null>} Selected folder path (empty for root) or null if cancelled.
|
||||
* @returns {Promise<{ vault: string, targetDir: string }|null>} Selected target or null if cancelled.
|
||||
*/
|
||||
export async function promptTargetFolder(vault, currentDir = '', items = []) {
|
||||
let folders = [];
|
||||
export async function promptTargetFolder(initialVault, initialDir = '', items = []) {
|
||||
let vaultsList = [];
|
||||
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();
|
||||
const vRes = await api('/api/vaults');
|
||||
if (vRes && Array.isArray(vRes.vaults)) {
|
||||
vaultsList = vRes.vaults;
|
||||
} else if (Array.isArray(vRes)) {
|
||||
vaultsList = vRes;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not fetch vault directory paths:', e);
|
||||
console.warn('Could not fetch vaults list:', e);
|
||||
}
|
||||
|
||||
// Fallback if vaults list API returns empty or fails
|
||||
if (!vaultsList.length && initialVault) {
|
||||
vaultsList = [{ name: initialVault }];
|
||||
}
|
||||
|
||||
let selectedVault = initialVault || (vaultsList[0]?.name) || '';
|
||||
|
||||
// Cache folder list per vault
|
||||
const vaultFoldersMap = {};
|
||||
|
||||
async function fetchFoldersForVault(v) {
|
||||
if (vaultFoldersMap[v]) return vaultFoldersMap[v];
|
||||
try {
|
||||
const res = await api(`/api/vault/${encodeURIComponent(v)}/paths?limit=5000`);
|
||||
if (res && Array.isArray(res.paths)) {
|
||||
vaultFoldersMap[v] = res.paths
|
||||
.filter((p) => p.type === 'directory')
|
||||
.map((p) => p.path)
|
||||
.sort();
|
||||
} else {
|
||||
vaultFoldersMap[v] = [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Could not fetch directories for vault ${v}:`, e);
|
||||
vaultFoldersMap[v] = [];
|
||||
}
|
||||
return vaultFoldersMap[v];
|
||||
}
|
||||
|
||||
await fetchFoldersForVault(selectedVault);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'obsigate-modal-overlay active';
|
||||
@@ -242,38 +275,70 @@ export async function promptTargetFolder(vault, currentDir = '', items = []) {
|
||||
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>`;
|
||||
function renderVaultOptions() {
|
||||
return vaultsList.map((v) => {
|
||||
const vName = typeof v === 'string' ? v : v.name;
|
||||
const sel = vName === selectedVault ? 'selected' : '';
|
||||
return `<option value="${escapeHtml(vName)}" ${sel}>🗄️ ${escapeHtml(vName)}</option>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderFolderOptions(folders, curDir) {
|
||||
let html = `<option value="">${escapeHtml(t('upload.vault_root') || '/ (Racine de la voute)')}</option>`;
|
||||
for (const folder of folders) {
|
||||
const sel = folder === curDir ? 'selected' : '';
|
||||
html += `<option value="${escapeHtml(folder)}" ${sel}>📁 ${escapeHtml(folder)}</option>`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
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 class="obsigate-modal" style="min-width: 440px; max-width: 520px;">
|
||||
<div class="obsigate-modal-header">
|
||||
<span class="obsigate-modal-title">📁 ${escapeHtml(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="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)` : ''}`}
|
||||
<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)` : ''}`)}
|
||||
</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 class="modal-form-group">
|
||||
<label style="display: block; font-weight: 600; font-size: 0.85rem; margin-bottom: 6px; color: var(--text-primary);">
|
||||
${escapeHtml(t('upload.target_vault') || 'Voute cible')}
|
||||
</label>
|
||||
<select id="obsigate-upload-vault-select" class="modal-select modal-input">
|
||||
${renderVaultOptions()}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="modal-form-group" style="margin-top: 14px;">
|
||||
<label style="display: block; font-weight: 600; font-size: 0.85rem; margin-bottom: 6px; color: var(--text-primary);">
|
||||
${escapeHtml(t('upload.target_directory') || 'Sous-répertoire cible')}
|
||||
</label>
|
||||
<select id="obsigate-upload-folder-select" class="modal-select modal-input">
|
||||
${renderFolderOptions(vaultFoldersMap[selectedVault] || [], initialDir)}
|
||||
</select>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const vaultSelect = overlay.querySelector('#obsigate-upload-vault-select');
|
||||
const folderSelect = overlay.querySelector('#obsigate-upload-folder-select');
|
||||
|
||||
vaultSelect.addEventListener('change', async () => {
|
||||
selectedVault = vaultSelect.value;
|
||||
const fList = await fetchFoldersForVault(selectedVault);
|
||||
folderSelect.innerHTML = renderFolderOptions(fList, '');
|
||||
});
|
||||
|
||||
const close = (val) => {
|
||||
overlay.remove();
|
||||
resolve(val);
|
||||
@@ -286,8 +351,9 @@ export async function promptTargetFolder(vault, currentDir = '', items = []) {
|
||||
});
|
||||
|
||||
overlay.querySelector('.modal-confirm-btn').addEventListener('click', () => {
|
||||
const select = overlay.querySelector('#obsigate-upload-folder-select');
|
||||
close(select ? select.value : '');
|
||||
const v = vaultSelect.value;
|
||||
const d = folderSelect.value;
|
||||
close({ vault: v, targetDir: d });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -641,9 +641,10 @@
|
||||
"upload.success": "Successfully imported {count} file(s)",
|
||||
"upload.partial": "Imported {count} file(s) ({errors} errors)",
|
||||
"upload.error": "Upload failed",
|
||||
"upload.choose_target_folder": "Choose destination folder",
|
||||
"upload.vault_root": "Vault root",
|
||||
"upload.target_directory": "Destination directory in",
|
||||
"upload.choose_target_folder": "Import Destination",
|
||||
"upload.target_vault": "Target Vault",
|
||||
"upload.vault_root": "/ (Vault root)",
|
||||
"upload.target_directory": "Target subdirectory",
|
||||
"upload.import_summary": "Items to import: {files} file(s), {dirs} folder(s)",
|
||||
"upload.import_button": "Import",
|
||||
"editor.cancel": "Cancel",
|
||||
|
||||
@@ -641,9 +641,10 @@
|
||||
"upload.success": "{count} fichier(s) importé(s) avec succès",
|
||||
"upload.partial": "{count} fichier(s) importé(s) ({errors} erreurs)",
|
||||
"upload.error": "Échec de l'importation",
|
||||
"upload.choose_target_folder": "Choisir le dossier de destination",
|
||||
"upload.vault_root": "Racine du vault",
|
||||
"upload.target_directory": "Dossier de destination dans",
|
||||
"upload.choose_target_folder": "Destination de l'importation",
|
||||
"upload.target_vault": "Voute cible",
|
||||
"upload.vault_root": "/ (Racine de la voute)",
|
||||
"upload.target_directory": "Sous-répertoire cible",
|
||||
"upload.import_summary": "Éléments à importer : {files} fichier(s), {dirs} dossier(s)",
|
||||
"upload.import_button": "Importer",
|
||||
"editor.cancel": "Annuler",
|
||||
|
||||
Reference in New Issue
Block a user