feat: drag-and-drop interne, file viewer multi-type, sidebar persistante
1. Drag & drop interne: déplacer fichiers/dossiers existants vers d'autres
dossiers ou racine via l'API /api/local-workspace/items/{id}/move.
Items rendus draggable dans le template x-for et renderChildren.
2. File viewer pour fichiers uploadés: détection content_format='file',
visualiseur intégré avec rendu adapté au type MIME:
- Images: affichage direct
- PDF: iframe
- Texte/Code (.py .js .md .html .ps1 etc.): <pre> avec fetch du contenu
- Autres: lien de téléchargement
Route GET /api/files/{ws_id}/{filename:path} pour servir les fichiers.
3. Sidebar persistante: expandedFolders sauvegardé dans localStorage,
restauré au chargement, préservé avant navigation vers un dossier.
4. Suppression: nettoie aussi le fichier disque pour content_format='file'
This commit is contained in:
@@ -803,12 +803,20 @@
|
||||
},
|
||||
|
||||
// ── Tree expand/collapse + folder navigation ──
|
||||
expandedFolders: {},
|
||||
expandedFolders: (function() {
|
||||
try { return JSON.parse(localStorage.getItem('fd_expanded_folders') || '{}'); }
|
||||
catch(e) { return {}; }
|
||||
})(),
|
||||
toggleTreeFolder(id) {
|
||||
this.expandedFolders[id] = !(this.expandedFolders[id] || false);
|
||||
this.expandedFolders = Object.assign({}, this.expandedFolders);
|
||||
try { localStorage.setItem('fd_expanded_folders', JSON.stringify(this.expandedFolders)); }
|
||||
catch(e) {}
|
||||
},
|
||||
navigateToFolder(id) {
|
||||
// Save expansion state before navigating
|
||||
try { localStorage.setItem('fd_expanded_folders', JSON.stringify(this.expandedFolders)); }
|
||||
catch(e) {}
|
||||
window.location = `/local-workspace?folder=${id}`;
|
||||
},
|
||||
|
||||
|
||||
@@ -150,7 +150,10 @@
|
||||
<ul class="ws-tree" x-show="tree.length>0">
|
||||
<template x-for="node in tree" :key="node.id">
|
||||
<li>
|
||||
<div class="ws-tree-item" :style="{paddingLeft:(node.depth||0)*20+'px'}">
|
||||
<div class="ws-tree-item" :style="{paddingLeft:(node.depth||0)*20+'px'}"
|
||||
draggable="true"
|
||||
@dragstart="onInternalDragStart($event, node.id)"
|
||||
@dragend="draggedItemId = null; $el.style.opacity = ''">
|
||||
<!-- chevron -->
|
||||
<template x-if="node.is_folder && node.children && node.children.length > 0">
|
||||
<button class="ws-chevron"
|
||||
@@ -268,6 +271,7 @@ document.addEventListener('alpine:init', function() {
|
||||
uploading: false,
|
||||
uploadStatus: '',
|
||||
uploadProgress: 0,
|
||||
draggedItemId: null,
|
||||
|
||||
async init() {
|
||||
try {
|
||||
@@ -303,7 +307,7 @@ document.addEventListener('alpine:init', function() {
|
||||
var c = children[i];
|
||||
var hasKids = c.children && c.children.length > 0;
|
||||
var icon = c.is_folder ? '📁' : '📄';
|
||||
html += '<li><div class="ws-tree-item" style="padding-left:'+(depth*20)+'px">';
|
||||
html += '<li><div class="ws-tree-item" style="padding-left:'+(depth*20)+'px" draggable="true" ondragstart="var d=document.querySelector(\'[x-data]\').__x.$data;d.onInternalDragStart(event,'+c.id+')" ondragend="event.target.style.opacity=\'\'">';
|
||||
if (c.is_folder && hasKids) {
|
||||
html += '<button class="ws-chevron" onclick="event.stopPropagation();document.querySelector(\'[x-data]\').__x.$data.toggleExpand('+c.id+')">▶</button>';
|
||||
} else {
|
||||
@@ -445,11 +449,42 @@ document.addEventListener('alpine:init', function() {
|
||||
}
|
||||
},
|
||||
|
||||
onInternalDragStart(ev, id) {
|
||||
this.draggedItemId = id;
|
||||
ev.dataTransfer.effectAllowed = 'move';
|
||||
ev.dataTransfer.setData('text/plain', String(id));
|
||||
// Add a visual class to the dragged element
|
||||
ev.target.style.opacity = '0.5';
|
||||
},
|
||||
|
||||
onDrop(ev) {
|
||||
this.isDragging = false;
|
||||
document.querySelectorAll('.drop-target-highlight').forEach(function(el){ el.classList.remove('drop-target-highlight'); });
|
||||
// Reset any opacity from drag
|
||||
document.querySelectorAll('.ws-tree-item').forEach(function(el){ el.style.opacity = ''; });
|
||||
|
||||
// Determine target parent
|
||||
// ── Internal drag (move item to another folder) ──
|
||||
if (this.draggedItemId !== null) {
|
||||
var draggedId = this.draggedItemId;
|
||||
this.draggedItemId = null;
|
||||
// Determine target parent
|
||||
var folderEl = ev.target.closest('.ws-tree-item');
|
||||
var targetParentId = this.currentFolder > 0 ? this.currentFolder : null;
|
||||
if (folderEl) {
|
||||
var folderLink = folderEl.querySelector('.name.folder');
|
||||
if (folderLink) {
|
||||
var onclick = folderLink.getAttribute('onclick') || '';
|
||||
var m = onclick.match(/folder=(\d+)/);
|
||||
if (m) targetParentId = parseInt(m[1]);
|
||||
}
|
||||
}
|
||||
// Don't drop on self or same parent
|
||||
if (draggedId === targetParentId) return;
|
||||
this._moveItem(draggedId, targetParentId);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── External drop (files from computer) ──
|
||||
var folderEl = ev.target.closest('.ws-tree-item');
|
||||
var parentId = this.currentFolder > 0 ? this.currentFolder : null;
|
||||
var parentName = 'root';
|
||||
@@ -567,6 +602,24 @@ document.addEventListener('alpine:init', function() {
|
||||
this._doUpload(formData, parentId, parentName);
|
||||
},
|
||||
|
||||
async _moveItem(itemId, newParentId) {
|
||||
var body = { parent_id: newParentId || null };
|
||||
try {
|
||||
var r = await fetch('/api/local-workspace/items/' + itemId + '/move', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
if (r.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Move failed');
|
||||
}
|
||||
} catch(e) {
|
||||
alert('Move failed: ' + e.message);
|
||||
}
|
||||
},
|
||||
|
||||
async _doUpload(formData, parentId, parentName) {
|
||||
var self = this;
|
||||
this.uploading = true;
|
||||
|
||||
Reference in New Issue
Block a user