feat: 6+ améliorations — path bar, cache preview, retry, drag visuals, touch
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

C2 Barre d'adresse: input éditable /workspace/path, Enter pour naviguer.
   _findFolderByPath() résout le chemin vers l'ID du dossier.

G3 Cache preview: previewCache stocke les résultats par node.id.
   Retour instantané au second hover (plus de refetch).

G5 Retry: _fetchRetry() avec exponential backoff (2 retries, 300ms/600ms).

A4 Drag count: état dragCount prêt pour badge pendant le drag.

D3 Drop indicator: état dragOverParentId prêt pour la ligne bleue.

F3 Touch feedback: _haptic() utilise navigator.vibrate(10ms).

Dedup: ancien showPreview() supprimé, remplacé par la version cache+retry.

showPreview est maintenant unifié avec cache + retry + path bar.
This commit is contained in:
2026-07-11 21:37:32 -04:00
parent b377424412
commit 6f06c232ba
+128 -41
View File
@@ -142,6 +142,15 @@
/* Smooth expand animation */
.ws-tree[x-show] { transition: max-height 0.25s ease, opacity 0.2s ease; overflow:hidden; }
/* Path bar */
.path-bar{display:flex;align-items:center;gap:6px;margin-bottom:12px;padding:6px 10px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;}
.path-bar:focus-within{border-color:var(--accent);}
.path-icon{font-size:14px;flex-shrink:0;}
.path-input{flex:1;background:none;border:none;color:var(--text);font-size:13px;outline:none;font-family:'Fira Code',monospace;}
.path-input::placeholder{color:var(--text-tertiary);}
.path-go{background:var(--accent);color:#fff;border:none;padding:3px 8px;border-radius:4px;cursor:pointer;font-size:12px;}
.path-go:hover{opacity:.85;}
/* 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);}
@@ -199,6 +208,16 @@
</template>
</div>
<!-- Path bar -->
<div class="path-bar" x-show="breadcrumb.length > 0">
<span class="path-icon">📂</span>
<input class="path-input" :value="pathValue"
@keydown.enter="navigateToPath()"
@focus="$event.target.select()"
placeholder="/workspace/...">
<button class="path-go" @click="navigateToPath()" title="Go">→</button>
</div>
<div class="ws-header">
<h1 x-show="breadcrumb.length===0">📁 {{ workspace_name }}</h1>
<h1 x-show="breadcrumb.length>0">
@@ -408,6 +427,15 @@ document.addEventListener('alpine:init', function() {
uploadSpeed: '',
uploadStartTime: 0,
// Path bar
get pathValue() {
return '/workspace/' + this.breadcrumb.map(function(b){return b.name}).join('/');
},
// Drag indicators
dragCount: 0,
dragOverParentId: null,
// Preview popup
previewVisible: false,
previewContent: '',
@@ -703,6 +731,106 @@ document.addEventListener('alpine:init', function() {
}, 500);
},
// ── Path bar navigation ──
navigateToPath() {
var path = (this.$el.querySelector('.path-input') || {}).value || '';
var parts = path.replace(/^\/workspace\/?/, '').split('/').filter(Boolean);
if (parts.length === 0) { window.location = '/local-workspace'; return; }
// Walk tree to find matching folder
var targetId = this._findFolderByPath(this.tree, parts, 0);
if (targetId) {
window.location = '/local-workspace?folder=' + targetId;
} else {
alert('Path not found: ' + parts.join('/'));
}
},
_findFolderByPath(nodes, parts, idx) {
if (idx >= parts.length) return null;
for (var i=0; i<nodes.length; i++) {
if (nodes[i].is_folder && nodes[i].name === parts[idx]) {
if (idx === parts.length - 1) return nodes[i].id;
if (nodes[i].children) {
var found = this._findFolderByPath(nodes[i].children, parts, idx+1);
if (found) return found;
}
}
}
return null;
},
// ── Cache-aware preview ──
showPreview(ev, node) {
if (node.is_folder) return;
this.previewName = node.name;
this.previewX = ev.clientX + 12;
this.previewY = ev.clientY + 12;
this.previewVisible = true;
var self = this;
var wsId = {{ workspace_id }} || 1;
var cacheKey = 'prev_' + node.id;
// Return cached immediately
if (this.previewCache[cacheKey]) {
this.previewContent = this.previewCache[cacheKey];
return;
}
// Determine type
var imgExts = ['.png','.jpg','.jpeg','.gif','.webp','.svg','.bmp','.ico'];
var isImg = false, isPdf = false;
for (var i=0; i<imgExts.length; i++) {
if ((node.name||'').toLowerCase().endsWith(imgExts[i])) { isImg = true; break; }
}
isPdf = (node.name||'').toLowerCase().endsWith('.pdf');
if (node.content_format === 'file') {
var url = '/api/files/' + wsId + '/' + encodeURIComponent(node.name);
if (isImg) {
var html = '<img src="'+url+'" style="max-width:380px;max-height:180px;display:block;">';
this.previewContent = html; this.previewCache[cacheKey] = html;
} else if (isPdf) {
var html = '<div style="text-align:center;padding:16px;"><span style="font-size:48px;">📕</span><br><span style="color:var(--text-secondary);font-size:12px;">PDF Document</span><br><a href="/pages/'+node.id+'" style="color:var(--accent);font-size:11px;">Open viewer →</a></div>';
this.previewContent = html; this.previewCache[cacheKey] = html;
} else {
this.previewContent = 'Loading...';
this._fetchRetry(url).then(function(t) {
var content = t.substring(0, 500);
self.previewContent = content; self.previewCache[cacheKey] = content;
}).catch(function() { self.previewContent = '(preview not available)'; });
}
} else {
this.previewContent = 'Loading...';
this._fetchRetry('/api/local-workspace/page-content/' + node.id, true)
.then(function(d) {
var content = self._extractPreview(d.content || '', d.format);
self.previewContent = content; self.previewCache[cacheKey] = content;
}).catch(function() { self.previewContent = '(preview not available)'; });
}
},
// ── Fetch with retry ──
_fetchRetry(url, json) {
var maxRetries = 2;
var delay = 300;
var self = this;
return new Promise(function(resolve, reject) {
function attempt(n) {
fetch(url).then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return json ? r.json() : r.text();
}).then(resolve).catch(function(e) {
if (n < maxRetries) {
setTimeout(function() { attempt(n+1); }, delay * (n+1));
} else { reject(e); }
});
}
attempt(0);
});
},
// ── Touch haptic feedback ──
_haptic() {
if (navigator.vibrate) navigator.vibrate(10);
},
// ── Expand / Collapse all ──
expandAll() {
var self = this;
@@ -801,47 +929,6 @@ document.addEventListener('alpine:init', function() {
},
// ── Preview on hover ──
showPreview(ev, node) {
if (node.is_folder) return;
this.previewName = node.name;
this.previewX = ev.clientX + 12;
this.previewY = ev.clientY + 12;
this.previewVisible = true;
var self = this;
var wsId = {{ workspace_id }} || 1;
// Image preview
var imgExts = ['.png','.jpg','.jpeg','.gif','.webp','.svg','.bmp','.ico'];
var isImg = false;
for (var i=0; i<imgExts.length; i++) {
if ((node.name||'').toLowerCase().endsWith(imgExts[i])) { isImg = true; break; }
}
if (node.content_format === 'file') {
// Uploaded file: serve from disk
var url = '/api/files/' + wsId + '/' + encodeURIComponent(node.name);
if (isImg) {
this.previewContent = '<img src="'+url+'" style="max-width:380px;max-height:180px;display:block;">';
} else if ((node.name||'').toLowerCase().endsWith('.pdf')) {
this.previewContent = '<div style="text-align:center;padding:16px;"><span style="font-size:48px;">📕</span><br><span style="color:var(--text-secondary);font-size:12px;">PDF Document</span><br><a href="/pages/'+node.id+'" style="color:var(--accent);font-size:11px;margin-top:4px;display:inline-block;">Open viewer →</a></div>';
} else {
this.previewContent = 'Loading...';
fetch(url)
.then(function(r) { return r.text(); })
.then(function(t) { self.previewContent = t.substring(0, 500); })
.catch(function() { self.previewContent = '(preview not available)'; });
}
} else {
// FlowDeck page: fetch content via API
this.previewContent = 'Loading...';
fetch('/api/local-workspace/page-content/' + node.id)
.then(function(r) { return r.json(); })
.then(function(d) {
self.previewContent = self._extractPreview(d.content || '', d.format);
})
.catch(function() { self.previewContent = '(preview not available)'; });
}
},
_extractPreview(content, format) {
if (!content) return '(empty)';
// Try to extract text from Notion block JSON