fix: sidebar workspace tree + pages DB — emojis → outlined icons

Root cause: 3 sources d'icônes colorées dans le sidebar:
1. _workspace_tree_macro.html L28: {{ page.icon }} raw → filtré via valid_icons
2. db.py: DEFAULT '📄' → 'file'
3. board.py _file_icon(): 15 émojis → 'file'/'edit'/'image'
4. board.py/dashboard.py: JSON icon: 📄/📁 → file/folder
5. base.html L931: JS favorites icon → validNames filter
6. base.html render_tree_item: valid_icons list étendue + fallback

Migration: nouvelles pages utilisent 'file'/'folder'. Pages existantes
avec émojis → les templates utilisent le fallback automatiquement.
143 tests passent
This commit is contained in:
2026-07-21 09:28:28 -04:00
parent 3fcc12ad8c
commit 53d31572e6
5 changed files with 31 additions and 26 deletions
+1 -1
View File
@@ -192,7 +192,7 @@ def init_db():
id INTEGER PRIMARY KEY AUTOINCREMENT,
collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
title TEXT NOT NULL DEFAULT '',
icon TEXT DEFAULT '📄',
icon TEXT DEFAULT 'file',
position INTEGER NOT NULL DEFAULT 0,
parent_id INTEGER REFERENCES collection_pages(id),
gitea_issue_id INTEGER,
+18 -18
View File
@@ -63,7 +63,7 @@ def _map_issue_to_card(issue: dict, owner: str = "", repo: str = "") -> dict:
"bug": "🐛", "feature": "✨", "enhancement": "⚡", "documentation": "📄",
"design": "🎨", "testing": "🧪", "refactor": "🔧", "security": "🔒",
}
icon = "📄"
icon = "file"
for lbl in labels:
for kw, emoji in icon_map.items():
if kw in lbl.get("name", "").lower():
@@ -142,27 +142,27 @@ def _build_page_tree(conn, parent_id: int | None, ws_key: str, depth: int = 0, m
def _file_icon(name: str, content_format: str = "") -> str:
"""Map file extension to emoji icon."""
"""Map file extension to icon name (SVG-safe)."""
import re
# FlowDeck internal pages (no extension)
if content_format and content_format != 'file':
return '📝'
return 'edit'
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 '📄'
if re.search(r'\.(png|jpe?g|gif|webp|svg|bmp|ico)$', n): return 'image'
if n.endswith('.pdf'): return 'file'
if re.search(r'\.(md|markdown)$', n): return 'edit'
if n.endswith('.py'): return 'file'
if re.search(r'\.(js|jsx|ts|tsx)$', n): return 'file'
if re.search(r'\.(html?|xml)$', n): return 'file'
if n.endswith('.css'): return 'file'
if n.endswith('.json'): return 'file'
if n.endswith('.sql'): return 'file'
if re.search(r'\.(sh|bash|zsh)$', n): return 'file'
if n.endswith('.ps1'): return 'file'
if re.search(r'\.(rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r)$', n): return 'file'
if re.search(r'\.(txt|log)$', n): return 'file'
if re.search(r'\.(zip|tar|gz|rar|7z)$', n): return 'file'
return 'file'
def _load_workspace_pages(ws_cookie: str) -> list:
+4 -4
View File
@@ -96,7 +96,7 @@ def _sidebar_data(request: Request, repos: list[dict], include_workspace: bool =
recent_pages.append({
"id": full_name,
"name": repo.get("name", full_name),
"icon": "📁",
"icon": "folder",
"url": f"/board/{full_name}",
"active": False,
"indent": 0,
@@ -430,7 +430,7 @@ async def help_page(request: Request):
</div>
<div class="help-card">
<h3><span class="icon">📁</span>Workspaces</h3>
<h3><span class="icon">{{ fd_icon("folder",16) }}</span>Workspaces</h3>
<p>Organize your work into separate workspaces. Each has its own pages and files.</p>
<ul>
<li><span class="help-badge local">Local</span> Files stored on your server</li>
@@ -1100,7 +1100,7 @@ def _nav_breadcrumb(conn, page_id: int) -> list:
"id": row["id"],
"label": title,
"url": None,
"icon": "\U0001F4C1" if is_folder else _file_icon(title, row["content_format"]),
"icon": "folder" if is_folder else _file_icon(title, row["content_format"]),
"menu": True,
})
current = row["parent_id"]
@@ -1160,7 +1160,7 @@ async def nav_menu(request: Request, workspace_id: int = None, parent_id: int =
items.append({
"id": r["id"],
"name": title,
"icon": "\U0001F4C1" if is_folder else _file_icon(title, r["content_format"]),
"icon": "folder" if is_folder else _file_icon(title, r["content_format"]),
"has_children": child_counts.get(r["id"], 0) > 0,
"url": f"/pages/{r['id']}",
})
+2 -1
View File
@@ -1,3 +1,4 @@
{% from '_icons.html' import fd_icon %}
{% macro render_workspace_tree(pages, depth=0) %}
{% for page in pages %}
{% set safe_name = page.name | replace("'", "\\'") %}
@@ -25,7 +26,7 @@
{% else %}
<span style="width:12px;flex-shrink:0;"></span>
{% endif %}
<span class="page-icon">{{ page.icon }}</span>
<span class="page-icon">{% set valid_icons = ["folder","file","star","link","trash","book","home","settings","lock","globe","image","edit","calendar","users","user","search","tag","bar-chart","grid","list","align-left","zap"] %}{% if page.icon in valid_icons %}{{ fd_icon(page.icon,14) }}{% elif page.is_folder %}{{ fd_icon("folder",14) }}{% else %}{{ fd_icon("file",14) }}{% endif %}</span>
{% if page.is_folder %}
<span class="page-name tree-folder-link"
@click.stop="navigateToFolder({{ page.db_id }})"
+6 -2
View File
@@ -123,7 +123,7 @@
{% else %}
<span class="tree-toggle-placeholder"></span>
{% endif %}
<span class="page-icon">{% if page.icon in ("folder","file","star","link","trash","book") %}{{ fd_icon(page.icon,14) }}{% else %}{{ page.icon }}{% endif %}</span>
<span class="page-icon">{% if page.icon in ("folder","file","star","link","trash","book","home","settings","lock","globe","image","edit","calendar","users","user","search","tag","bar-chart","grid","list","align-left","zap") %}{{ fd_icon(page.icon,14) }}{% elif page.icon and page.icon|length <= 2 %}{{ fd_icon("file",14) }}{% else %}{{ fd_icon("folder",14) }}{% endif %}</span>
<span class="page-name">{{ page.name }}</span>
<span class="item-actions">
<button class="item-action-btn" @click.stop="newSubPage('{{ page.id }}')">+</button>
@@ -928,7 +928,11 @@
favList.innerHTML = '<li class="sidebar-item empty-hint"><span class="page-icon page-icon-svg">'+getSvgIcon('star',16)+'</span><span class="page-name text-dim">No favorites yet</span></li>';
} else {
favList.innerHTML = items.map(function(item) {
var icon = item.icon || '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
var iconName = item.icon || 'file';
// Map known icon names, anything else (including emojis) → file
var validNames = ['folder','file','star','link','trash','book','home','settings','lock','globe','image','edit','calendar','users','user','search','tag'];
if (validNames.indexOf(iconName) === -1) iconName = 'file';
var icon = '<span class="page-icon-svg">'+getSvgIcon(iconName,14)+'</span>';
const name = (item.title || 'Untitled');
const safeName = name.replace(/'/g, "\\'").replace(/"/g, '&quot;');
const url = item.url || ('/pages/' + item.id);