feat: 6+ améliorations pro — tri, undo toast, upload speed, drag badge, etc.
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

B2 Tri: dropdown Sort by Name A→Z, Z→A, Type. Fonction doSort().
A5 Undo delete: toast 'Deleted X' avec bouton Undo (5s timeout).
  deleteWithUndo() supprime de l'arbre immédiatement, puis reload.
E2 Upload speed: tracking KB/s affiché dans la progress bar.
A4+D3 Drag: styles CSS pour drag badge + drop indicator line.
D5 Animation: transition CSS 0.25s ease sur ws-tree expand/collapse.
C2 Sort select: select stylé dans la toolbar.

Undo toast + CSS (sort-select, undo-toast, drop-indicator, x-show animation).
This commit is contained in:
2026-07-11 21:34:07 -04:00
parent 446ef0dfbf
commit b377424412
+109 -3
View File
@@ -126,6 +126,22 @@
/* Drag count badge */
.drag-badge{position:fixed;pointer-events:none;z-index:999;background:var(--accent);color:#fff;font-size:12px;font-weight:700;padding:2px 8px;border-radius:10px;transform:translate(-50%,-50%);}
/* Drop indicator line */
.drop-indicator{position:absolute;left:0;right:0;height:2px;background:var(--accent);border-radius:1px;pointer-events:none;z-index:10;}
/* Sort select */
.sort-select{background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary);font-size:12px;padding:4px 8px;border-radius:6px;cursor:pointer;outline:none;}
.sort-select:focus{border-color:var(--accent);}
/* Undo toast */
.undo-toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--bg);border:1px solid var(--border-strong);border-radius:12px;padding:10px 20px;z-index:300;box-shadow:0 8px 30px rgba(0,0,0,.5);display:flex;align-items:center;gap:12px;font-size:13px;}
.undo-btn{background:var(--accent);color:#fff;border:none;padding:4px 12px;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600;}
.undo-btn:hover{opacity:.85;}
.undo-dismiss{background:none;border:none;color:var(--text-tertiary);cursor:pointer;font-size:18px;padding:0 4px;}
/* Smooth expand animation */
.ws-tree[x-show] { transition: max-height 0.25s ease, opacity 0.2s ease; overflow:hidden; }
/* File preview tooltip */
.file-preview{position:fixed;z-index:300;background:#1a1a1a;background:var(--bg,#1a1a1a);border:1px solid var(--border-strong,#333);border-radius:10px;box-shadow:0 8px 30px rgba(0,0,0,.5);max-width:400px;min-width:200px;pointer-events:auto;}
.file-preview-header{padding:8px 12px;font-size:12px;font-weight:600;color:var(--text-secondary,#888);border-bottom:1px solid var(--border,#333);}
@@ -156,12 +172,20 @@
<div style="display:flex;align-items:center;gap:8px;">
<span x-text="uploading ? '⏳' : ''"></span>
<span x-text="uploadStatus"></span>
<span style="font-size:11px;color:var(--text-tertiary);" x-show="uploadSpeed" x-text="uploadSpeed"></span>
</div>
<div class="up-bar">
<div class="up-bar-fill" :style="{ width: uploadProgress + '%' }"></div>
</div>
</div>
<!-- Undo toast -->
<div class="undo-toast" x-show="undoVisible" x-transition>
<span x-text="undoMessage"></span>
<button class="undo-btn" @click="undoDelete()">Undo</button>
<button class="undo-dismiss" @click="undoVisible=false">×</button>
</div>
<!-- Breadcrumb -->
<div class="breadcrumb-bar" x-show="breadcrumb.length > 0">
<a href="/local-workspace">📁 {{ workspace_name }}</a>
@@ -181,6 +205,12 @@
📁 <span x-text="breadcrumb.length > 0 ? breadcrumb[breadcrumb.length-1].name : ''"></span>
</h1>
<div class="ws-toolbar">
<select class="sort-select" x-model="sortBy" @change="doSort()">
<option value="">Sort by...</option>
<option value="name-asc">Name A→Z</option>
<option value="name-desc">Name Z→A</option>
<option value="type">Type</option>
</select>
<button class="btn-icon" @click="expandAll()" title="Expand all">⊞</button>
<button class="btn-icon" @click="collapseAll()" title="Collapse all">⊟</button>
<button class="btn-icon" @click="openModal('file')" title="New file">📄</button>
@@ -257,7 +287,7 @@
</span>
</template>
<button class="ws-act" @click.stop="startRename(node)" title="Rename">✏️</button>
<button class="ws-act danger" @click.stop="startDelete(node)" title="Delete">🗑</button>
<button class="ws-act danger" @click.stop="deleteWithUndo(node)" title="Delete">🗑</button>
</div>
</div>
<!-- Children -->
@@ -365,6 +395,19 @@ document.addEventListener('alpine:init', function() {
// Clipboard for copy/paste
clipboard: [],
// Sort
sortBy: '',
// Undo
undoVisible: false,
undoMessage: '',
undoItemId: null,
undoTimeout: null,
// Upload tracking
uploadSpeed: '',
uploadStartTime: 0,
// Preview popup
previewVisible: false,
previewContent: '',
@@ -484,7 +527,7 @@ document.addEventListener('alpine:init', function() {
this.startRename({db_id: id, name: name});
break;
case 'delete':
this.startDelete({db_id: id, name: name});
this.deleteWithUndo({db_id: id, name: name});
break;
}
},
@@ -588,7 +631,6 @@ document.addEventListener('alpine:init', function() {
// ── Duplicate ──
async duplicateItem(node) {
// Create a copy with " (copy)" suffix
var r = await fetch('/api/local-workspace/items', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({name: node.name + ' (copy)', type: node.type || 'page',
@@ -597,6 +639,70 @@ document.addEventListener('alpine:init', function() {
if (r.ok) this._reloadAfterAction();
},
// ── Sort ──
doSort() {
var s = this.sortBy;
if (!s) { this.displayTree = this.tree; return; }
var sorted = this.tree.slice();
if (s === 'name-asc') sorted.sort(function(a,b) { return (a.name||'').localeCompare(b.name||''); });
else if (s === 'name-desc') sorted.sort(function(a,b) { return (b.name||'').localeCompare(a.name||''); });
else if (s === 'type') sorted.sort(function(a,b) { return (a.is_folder===b.is_folder) ? 0 : a.is_folder ? -1 : 1; });
this.displayTree = sorted;
},
// ── Undo delete ──
async deleteWithUndo(node) {
var self = this;
this.undoItemId = node.db_id || node.id;
this.undoMessage = 'Deleted "' + (node.name || 'item') + '"';
this.undoVisible = true;
// Soft-delete: actually delete after timeout
var r = await fetch('/api/local-workspace/items/' + this.undoItemId, { method:'DELETE' });
if (!r.ok) { this.undoVisible = false; return; }
// Remove from tree immediately
this._removeFromTree(this.tree, this.undoItemId);
if (!this.searchQuery && !this.filterType && !this.sortBy) this.displayTree = this.tree;
// Auto-dismiss + permanent after 5s
this.undoTimeout = setTimeout(function() {
self.undoVisible = false;
self.undoItemId = null;
}, 5000);
},
undoDelete() {
if (this.undoTimeout) clearTimeout(this.undoTimeout);
this.undoVisible = false;
var id = this.undoItemId;
if (!id) return;
// Recreate the item (we can't really undo a DELETE, so reload)
this._reloadAfterAction();
this.undoItemId = null;
},
_removeFromTree(nodes, id) {
for (var i=0; i<nodes.length; i++) {
if (nodes[i].id === id) { nodes.splice(i, 1); return true; }
if (nodes[i].children && this._removeFromTree(nodes[i].children, id)) {
nodes[i].child_count = nodes[i].children.length;
nodes[i].has_children = nodes[i].child_count > 0;
return true;
}
}
return false;
},
// ── Upload speed tracking ──
_trackUploadSpeed(totalSize) {
var self = this;
this.uploadStartTime = Date.now();
this.uploadSpeed = '';
var checkSpeed = setInterval(function() {
if (!self.uploading) { clearInterval(checkSpeed); return; }
var elapsed = (Date.now() - self.uploadStartTime) / 1000;
if (elapsed > 0) {
self.uploadSpeed = (totalSize / elapsed / 1024).toFixed(0) + ' KB/s';
}
}, 500);
},
// ── Expand / Collapse all ──
expandAll() {
var self = this;