fix: bouton delete cassé + preview PDF cliquable + icônes distinctes FlowDeck vs upload
FlowDeck CI / test (push) Failing after 8s
FlowDeck CI / docker (push) Has been skipped

1. Delete: ev.target pouvait être un noeud texte (emoji 🗑)
   → Text.closest() n'existe pas → handleTreeAction échouait.
   Fix: fallback sur ev.target.parentElement.closest().

2. Preview PDF: ajout lien 'Open viewer →' vers /pages/{id}
   (le viewer standalone rend le PDF dans une iframe).

3. Icônes distinctes:
   - Pages FlowDeck (content_format='blocks'/'markdown') → 📝
   - Fichiers uploadés → icône basée sur l'extension
   - _fileIcon() accepte contentFormat, propage depuis l'API tree
   - Appliqué dans sidebar (board.py) + page workspace (x-for + renderChildren)
This commit is contained in:
2026-07-11 14:00:57 -04:00
parent fc6e30aebb
commit 8497a36da9
2 changed files with 16 additions and 10 deletions
+8 -5
View File
@@ -140,9 +140,12 @@ def _build_page_tree(conn, parent_id: int | None, ws_key: str, depth: int = 0, m
return items
def _file_icon(name: str) -> str:
def _file_icon(name: str, content_format: str = "") -> str:
"""Map file extension to emoji icon."""
import re
# FlowDeck internal pages (no extension)
if content_format and content_format != 'file':
return '📝'
n = name.lower()
if re.search(r'\.(png|jpe?g|gif|webp|svg|bmp|ico)$', n): return '🖼️'
if n.endswith('.pdf'): return '📕'
@@ -169,7 +172,7 @@ def _load_workspace_pages(ws_cookie: str) -> list:
ws_id = int(ws_cookie)
with get_conn() as conn:
rows = conn.execute(
"SELECT id, title, parent_section FROM pages WHERE workspace_id=? AND parent_id IS NULL ORDER BY created_at DESC",
"SELECT id, title, parent_section, content_format FROM pages WHERE workspace_id=? AND parent_id IS NULL ORDER BY created_at DESC",
(ws_id,),
).fetchall()
items = []
@@ -179,7 +182,7 @@ def _load_workspace_pages(ws_cookie: str) -> list:
items.append({
"db_id": r["id"], "name": title,
"id": f"page/{r['id']}",
"icon": "📁" if is_folder else _file_icon(title),
"icon": "📁" if is_folder else _file_icon(title, r["content_format"]),
"is_folder": is_folder,
"children": _load_children(r["id"]),
})
@@ -192,7 +195,7 @@ def _load_children(parent_id: int) -> list:
"""Recursively load children of a page."""
with get_conn() as conn:
rows = conn.execute(
"SELECT id, title, parent_section FROM pages WHERE parent_id=? ORDER BY created_at",
"SELECT id, title, parent_section, content_format FROM pages WHERE parent_id=? ORDER BY created_at",
(parent_id,),
).fetchall()
children = []
@@ -202,7 +205,7 @@ def _load_children(parent_id: int) -> list:
children.append({
"db_id": r["id"], "name": title,
"id": f"page/{r['id']}",
"icon": "📁" if is_folder else _file_icon(title),
"icon": "📁" if is_folder else _file_icon(title, r["content_format"]),
"is_folder": is_folder,
"children": _load_children(r["id"]),
})
+8 -5
View File
@@ -207,7 +207,7 @@
<span style="width:16px;flex-shrink:0;"></span>
</template>
<span class="icon" x-text="_fileIcon(node.name, node.is_folder)"></span>
<span class="icon" x-text="_fileIcon(node.name, node.is_folder, node.content_format)"></span>
<template x-if="node.is_folder">
<span class="name folder" @click.stop="navigateToFolder(node.id)" x-text="node.name"></span>
@@ -376,7 +376,7 @@ document.addEventListener('alpine:init', function() {
for (var i=0; i<children.length; i++) {
var c = children[i];
var hasKids = c.children && c.children.length > 0;
var icon = this._fileIcon(c.name, c.is_folder);
var icon = this._fileIcon(c.name, c.is_folder, c.content_format);
var escName = this.esc(c.name);
html += '<li><div class="ws-tree-item" style="padding-left:'+(depth*20)+'px" draggable="true"'
+ ' data-folder-id="'+(c.is_folder?c.id:'')+'"'
@@ -421,7 +421,8 @@ document.addEventListener('alpine:init', function() {
// ── Event delegation for renderChildren action buttons ──
handleTreeAction(ev) {
var btn = ev.target.closest('[data-ws-action]');
var btn = (ev.target.closest && ev.target.closest('[data-ws-action]'))
|| (ev.target.parentElement && ev.target.parentElement.closest('[data-ws-action]'));
if (!btn) return;
ev.stopPropagation();
var action = btn.getAttribute('data-ws-action');
@@ -516,8 +517,10 @@ document.addEventListener('alpine:init', function() {
},
// ── File type icon ──
_fileIcon(name, isFolder) {
_fileIcon(name, isFolder, contentFormat) {
if (isFolder) return '📁';
// FlowDeck internal pages (no extension, blocks/markdown format)
if (contentFormat && contentFormat !== 'file') return '📝';
var n = (name || '').toLowerCase();
if (/\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(n)) return '🖼️';
if (/\.pdf$/.test(n)) return '📕';
@@ -598,7 +601,7 @@ document.addEventListener('alpine:init', function() {
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></div>';
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)