feat(dragdrop): drag & drop complet de fichiers/dossiers & intégration IA (#89)
CI / lint (push) Successful in 1m20s
CI / security (push) Successful in 50s
CI / test (push) Successful in 2m42s
CI / build (push) Successful in 51s
CI / e2e (push) Successful in 10m29s

This commit is contained in:
2026-09-14 15:31:12 -04:00
parent 150b57d538
commit 1db3e0ad2f
13 changed files with 990 additions and 10 deletions
+144 -6
View File
@@ -9,8 +9,10 @@
*/
import { api } from './auth.js';
import { t } from './i18n.js';
import { showToast } from './ui.js';
import { refreshSidebarTreePreservingState } from './sidebar.js';
import { readDataTransferEntries, handleVaultDropUpload } from './upload.js';
// ---------------------------------------------------------------------------
// State
@@ -180,14 +182,21 @@ function onDragStart(e) {
}
function onDragOver(e) {
if (!dragState) return;
const isExternalFiles = e.dataTransfer && Array.from(e.dataTransfer.types || []).includes('Files');
if (!dragState && !isExternalFiles) return;
e.preventDefault(); // Required to allow drop
e.dataTransfer.dropEffect = 'move';
if (isExternalFiles) {
e.dataTransfer.dropEffect = 'copy';
} else {
e.dataTransfer.dropEffect = 'move';
}
clearAllHighlights();
const target = getDropTarget(e.target);
if (!target || isInvalidDrop(dragState, target)) {
if (!target || (!isExternalFiles && isInvalidDrop(dragState, target))) {
e.dataTransfer.dropEffect = 'none';
return;
}
@@ -224,17 +233,33 @@ async function onDrop(e) {
e.preventDefault();
e.stopPropagation();
if (!dragState) return;
const isExternalFiles = e.dataTransfer && (
(e.dataTransfer.files && e.dataTransfer.files.length > 0) ||
(e.dataTransfer.items && e.dataTransfer.items.length > 0) ||
Array.from(e.dataTransfer.types || []).includes('Files')
);
if (!dragState && !isExternalFiles) return;
const target = getDropTarget(e.target);
if (!target || isInvalidDrop(dragState, target)) {
if (!target || (!isExternalFiles && isInvalidDrop(dragState, target))) {
onDragEnd();
return;
}
const { vault, path, type } = dragState;
const { vault: targetVault, targetDir } = target;
if (isExternalFiles) {
onDragEnd();
const items = await readDataTransferEntries(e.dataTransfer.items || e.dataTransfer.files);
if (items.length > 0) {
await handleVaultDropUpload(targetVault, targetDir, items);
}
return;
}
const { vault, path, type } = dragState;
// Don't move if source and target are the same vault
if (vault !== targetVault) {
showToast(t('dragdrop.cross_vault_unsupported'), 'error');
@@ -312,6 +337,119 @@ export function initDragDrop() {
makeAllDraggable();
});
observer.observe(container, { childList: true, subtree: true });
// Initialize global drop overlay
initGlobalDropOverlay();
}
/**
* Initialize a global dropzone overlay across the entire window
* when dragging files/folders from the OS.
*/
export function initGlobalDropOverlay() {
if (document.getElementById('obsigate-global-drop-overlay')) return;
const overlay = document.createElement('div');
overlay.id = 'obsigate-global-drop-overlay';
overlay.className = 'obsigate-drop-overlay';
overlay.innerHTML = `
<div class="obsigate-drop-backdrop"></div>
<div class="obsigate-drop-modal">
<h2 class="obsigate-drop-title">${t('dragdrop.overlay_title') || 'Déposer des fichiers ou dossiers'}</h2>
<div class="obsigate-drop-zones">
<div class="obsigate-drop-zone obsigate-drop-vault" data-zone="vault">
<div class="obsigate-drop-zone-icon">📁</div>
<div class="obsigate-drop-zone-title">${t('dragdrop.overlay_vault_zone') || 'Importer dans le Vault'}</div>
<div class="obsigate-drop-zone-desc" id="obsigate-drop-vault-target"></div>
</div>
<div class="obsigate-drop-zone obsigate-drop-ai" data-zone="ai">
<div class="obsigate-drop-zone-icon">🧠</div>
<div class="obsigate-drop-zone-title">${t('dragdrop.overlay_ai_zone') || "Analyser avec l'Assistant IA"}</div>
<div class="obsigate-drop-zone-desc">Envoyer au chat BooksLM</div>
</div>
</div>
</div>
`;
document.body.appendChild(overlay);
let dragCounter = 0;
window.addEventListener('dragenter', (e) => {
if (dragState) return; // Don't show global overlay for internal tree reordering
const types = Array.from(e.dataTransfer?.types || []);
if (!types.includes('Files')) return;
dragCounter++;
if (dragCounter === 1) {
// Update target vault description
const activeVault = window.state?.activeVault || (document.querySelector('.tree-item.vault-item')?.dataset?.vault) || '';
const vaultDesc = overlay.querySelector('#obsigate-drop-vault-target');
if (vaultDesc) {
vaultDesc.textContent = activeVault ? `Vault : ${activeVault}` : 'Vault par défaut';
}
overlay.classList.add('active');
}
});
window.addEventListener('dragleave', (e) => {
if (dragState) return;
const types = Array.from(e.dataTransfer?.types || []);
if (!types.includes('Files')) return;
dragCounter--;
if (dragCounter <= 0) {
dragCounter = 0;
overlay.classList.remove('active');
}
});
window.addEventListener('dragover', (e) => {
const types = Array.from(e.dataTransfer?.types || []);
if (!types.includes('Files')) return;
e.preventDefault();
});
const zones = overlay.querySelectorAll('.obsigate-drop-zone');
zones.forEach((zone) => {
zone.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'copy';
zone.classList.add('hover');
});
zone.addEventListener('dragleave', () => {
zone.classList.remove('hover');
});
zone.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation();
dragCounter = 0;
overlay.classList.remove('active');
zone.classList.remove('hover');
const zoneType = zone.dataset.zone;
const items = await readDataTransferEntries(e.dataTransfer.items || e.dataTransfer.files);
if (!items.length) return;
if (zoneType === 'vault') {
const activeVault = window.state?.activeVault || (document.querySelector('.tree-item.vault-item')?.dataset?.vault);
if (activeVault) {
await handleVaultDropUpload(activeVault, '', items);
} else {
showToast('Aucun vault sélectionné', 'error');
}
} else if (zoneType === 'ai') {
// Dispatch or pass to BooksLM assistant
if (window.BooksLMInstance) {
window.BooksLMInstance.handleExternalDroppedItems(items);
} else {
showToast("Assistant IA non initialisé", 'warning');
}
}
});
});
}
/**