feat: icônes par type de fichier + filtre par type + fix preview PDF
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

1. Icônes de fichier contextuelles:
   - _fileIcon() JS et _file_icon() Python: mappe l'extension vers emoji
   - 📕 PDF, 🖼️ images, 🐍 Python, 📜 JS/TS, 🌐 HTML, 🎨 CSS,
     📋 JSON, 🗃️ SQL, 💻 Shell, ⚡ PS1, 📝 MD, 📦 archives...
   - Appliqué dans sidebar (board.py) + page workspace (x-for + renderChildren)

2. Preview PDF: ne fetch plus le contenu binaire (caractères bizarres),
   affiche 📕 'PDF Document' dans le popup hover

3. Filtre par type:
   - Chips 📝 Pages | 📕 PDF | 🖼️ Img | 📜 Code | 📄 Text
   - _filterTree() accepte un paramètre fileType
   - Désélection par second clic sur le même chip
This commit is contained in:
2026-07-11 13:49:12 -04:00
parent a8f9832535
commit fc6e30aebb
2 changed files with 96 additions and 14 deletions
+31 -6
View File
@@ -140,6 +140,27 @@ def _build_page_tree(conn, parent_id: int | None, ws_key: str, depth: int = 0, m
return items
def _file_icon(name: str) -> str:
"""Map file extension to emoji icon."""
import re
n = name.lower()
if re.search(r'\.(png|jpe?g|gif|webp|svg|bmp|ico)$', n): return '🖼️'
if n.endswith('.pdf'): return '📕'
if re.search(r'\.(md|markdown)$', n): return '📝'
if n.endswith('.py'): return '🐍'
if re.search(r'\.(js|jsx|ts|tsx)$', n): return '📜'
if re.search(r'\.(html?|xml)$', n): return '🌐'
if n.endswith('.css'): return '🎨'
if n.endswith('.json'): return '📋'
if n.endswith('.sql'): return '🗃️'
if re.search(r'\.(sh|bash|zsh)$', n): return '💻'
if n.endswith('.ps1'): return '⚡'
if re.search(r'\.(rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r)$', n): return '📜'
if re.search(r'\.(txt|log)$', n): return '📄'
if re.search(r'\.(zip|tar|gz|rar|7z)$', n): return '📦'
return '📄'
def _load_workspace_pages(ws_cookie: str) -> list:
"""Load top-level pages with children for the active workspace."""
if not ws_cookie:
@@ -153,11 +174,13 @@ def _load_workspace_pages(ws_cookie: str) -> list:
).fetchall()
items = []
for r in rows:
is_folder = r["parent_section"] == "Workspace"
title = r["title"] or "Untitled"
items.append({
"db_id": r["id"], "name": r["title"] or "Untitled",
"db_id": r["id"], "name": title,
"id": f"page/{r['id']}",
"icon": "📁" if r["parent_section"] == "Workspace" else "📄",
"is_folder": r["parent_section"] == "Workspace",
"icon": "📁" if is_folder else _file_icon(title),
"is_folder": is_folder,
"children": _load_children(r["id"]),
})
return items
@@ -174,11 +197,13 @@ def _load_children(parent_id: int) -> list:
).fetchall()
children = []
for r in rows:
is_folder = r["parent_section"] == "Workspace"
title = r["title"] or "Untitled"
children.append({
"db_id": r["id"], "name": r["title"] or "Untitled",
"db_id": r["id"], "name": title,
"id": f"page/{r['id']}",
"icon": "📁" if r["parent_section"] == "Workspace" else "📄",
"is_folder": r["parent_section"] == "Workspace",
"icon": "📁" if is_folder else _file_icon(title),
"is_folder": is_folder,
"children": _load_children(r["id"]),
})
return children
+65 -8
View File
@@ -107,6 +107,12 @@
.search-clear{background:none;border:none;color:var(--text-tertiary);cursor:pointer;font-size:16px;padding:2px 4px;border-radius:3px;}
.search-clear:hover{color:var(--text);background:var(--bg-hover);}
/* Filter chips */
.filter-chips{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap;}
.filter-chip{padding:4px 10px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:6px;font-size:12px;color:var(--text-secondary);cursor:pointer;transition:all 150ms;}
.filter-chip:hover{background:var(--bg-hover);color:var(--text);}
.filter-chip.active{background:var(--accent);color:#fff;border-color:var(--accent);}
/* 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);}
@@ -174,6 +180,15 @@
<button class="search-clear" x-show="searchQuery" @click="searchQuery='';doFilter()" title="Clear">×</button>
</div>
<!-- Filter chips -->
<div class="filter-chips" x-show="tree.length>0">
<button class="filter-chip" :class="{ active: filterType === 'page' }" @click="filterByType('page')" title="FlowDeck pages">📝 Pages</button>
<button class="filter-chip" :class="{ active: filterType === 'pdf' }" @click="filterByType('pdf')" title="PDF">📕 PDF</button>
<button class="filter-chip" :class="{ active: filterType === 'image' }" @click="filterByType('image')" title="Images">🖼️ Img</button>
<button class="filter-chip" :class="{ active: filterType === 'code' }" @click="filterByType('code')" title="Code">📜 Code</button>
<button class="filter-chip" :class="{ active: filterType === 'text' }" @click="filterByType('text')" title="Text">📄 Text</button>
</div>
<ul class="ws-tree" x-show="displayTree.length>0" @click="handleTreeAction($event)">
<template x-for="node in displayTree" :key="node.id">
<li>
@@ -192,7 +207,7 @@
<span style="width:16px;flex-shrink:0;"></span>
</template>
<span class="icon" x-text="node.is_folder ? '📁' : '📄'"></span>
<span class="icon" x-text="_fileIcon(node.name, node.is_folder)"></span>
<template x-if="node.is_folder">
<span class="name folder" @click.stop="navigateToFolder(node.id)" x-text="node.name"></span>
@@ -306,6 +321,7 @@ document.addEventListener('alpine:init', function() {
})(),
currentFolder: {{ current_folder_id }},
searchQuery: '',
filterType: '',
// Preview popup
previewVisible: false,
@@ -360,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 = c.is_folder ? '📁' : '📄';
var icon = this._fileIcon(c.name, c.is_folder);
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:'')+'"'
@@ -499,19 +515,58 @@ document.addEventListener('alpine:init', function() {
}
},
// ── File type icon ──
_fileIcon(name, isFolder) {
if (isFolder) return '📁';
var n = (name || '').toLowerCase();
if (/\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(n)) return '🖼️';
if (/\.pdf$/.test(n)) return '📕';
if (/\.(md|markdown)$/.test(n)) return '📝';
if (/\.(py)$/.test(n)) return '🐍';
if (/\.(js|jsx|ts|tsx)$/.test(n)) return '📜';
if (/\.(html?|xml)$/.test(n)) return '🌐';
if (/\.css$/.test(n)) return '🎨';
if (/\.json$/.test(n)) return '📋';
if (/\.sql$/.test(n)) return '🗃️';
if (/\.(sh|bash|zsh)$/.test(n)) return '💻';
if (/\.ps1$/.test(n)) return '⚡';
if (/\.(rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r)$/.test(n)) return '📜';
if (/\.(txt|log)$/.test(n)) return '📄';
if (/\.(zip|tar|gz|rar|7z)$/.test(n)) return '📦';
return '📄';
},
// ── Search / Filter ──
doFilter() {
var q = (this.searchQuery || '').toLowerCase().trim();
if (!q) { this.displayTree = this.tree; return; }
this.displayTree = this._filterTree(this.tree, q);
var ft = this.filterType || '';
if (!q && !ft) { this.displayTree = this.tree; return; }
var self = this;
this.displayTree = this._filterTree(this.tree, q, ft);
},
_filterTree(nodes, query) {
filterByType(type) {
this.filterType = (this.filterType === type) ? '' : type;
this.doFilter();
},
_filterTree(nodes, query, fileType) {
var result = [];
for (var i=0; i<nodes.length; i++) {
var n = nodes[i];
var filteredChildren = n.children ? this._filterTree(n.children, query) : [];
var selfMatches = (n.name || '').toLowerCase().indexOf(query) !== -1;
if (selfMatches || filteredChildren.length > 0) {
var filteredChildren = n.children ? this._filterTree(n.children, query, fileType) : [];
var selfMatches = (n.name || '').toLowerCase().indexOf(query || '') !== -1;
// File type filter
var typeMatch = true;
if (fileType && !n.is_folder) {
var nm = (n.name || '').toLowerCase();
switch(fileType) {
case 'pdf': typeMatch = /\.pdf$/.test(nm); break;
case 'image': typeMatch = /\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(nm); break;
case 'code': typeMatch = /\.(py|js|jsx|ts|tsx|rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r|sh|bash|ps1|sql)$/.test(nm); break;
case 'text': typeMatch = /\.(txt|md|markdown|log)$/.test(nm); break;
case 'page': typeMatch = !/\.(pdf|png|jpe?g|gif|webp|svg|bmp|ico|py|js|jsx|ts|tsx|rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r|sh|bash|ps1|sql|txt|md|markdown|log)$/.test(nm); break;
}
}
if ((selfMatches || filteredChildren.length > 0) && typeMatch) {
var clone = Object.assign({}, n);
clone.children = filteredChildren;
result.push(clone);
@@ -542,6 +597,8 @@ document.addEventListener('alpine:init', function() {
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></div>';
} else {
this.previewContent = 'Loading...';
fetch(url)