fix: /local-workspace SVG rendering + ALL emoji icons converted
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

- BUG: x-text→x-html pour _fileIcon() (SVG s'affichait en texte brut)
- _fileIcon(): 11 types de fichiers → SVG (🐍📕🖼️📜🌐🎨🗃️💻⚡📦📄)
- title-card: x-text '📁' → x-html getSvgIcon('folder',24)
- preview empty states (PDF): 📕 → getSvgIcon('file')
- toolbar: 🔍📄📁🗑 → SVG icons
- filter chips: 📁📝📕🖼️📜 → getSvgIcon()
- view menu: 🌳📋📊🏷️📝 → text-only (clean)
- _icons.html: +image, +download
- getSvgIcon JS: +image, +download, +search, +home
- 143 tests passent
This commit is contained in:
2026-07-20 21:39:40 -04:00
parent b835dd6b5e
commit fbd4b836f7
3 changed files with 51 additions and 43 deletions
+4
View File
@@ -48,6 +48,10 @@
<svg width="{{s}}" height="{{s}}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
{%- elif name == 'bell' -%}
<svg width="{{s}}" height="{{s}}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
{%- elif name == 'image' -%}
<svg width="{{s}}" height="{{s}}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
{%- elif name == 'download' -%}
<svg width="{{s}}" height="{{s}}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
{%- elif name == 'chevron-down' -%}
<svg width="{{s}}" height="{{s}}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
{%- elif name == 'inbox' -%}
+5 -1
View File
@@ -605,7 +605,11 @@
folder: '<svg width="'+size+'" height="'+size+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>',
file: '<svg width="'+size+'" height="'+size+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>',
star: '<svg width="'+size+'" height="'+size+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
trash: '<svg width="'+size+'" height="'+size+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>'
trash: '<svg width="'+size+'" height="'+size+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>',
image: '<svg width="'+size+'" height="'+size+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>',
download: '<svg width="'+size+'" height="'+size+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
search: '<svg width="'+size+'" height="'+size+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
home: '<svg width="'+size+'" height="'+size+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>'
};
return svgs[name] || '';
}
+42 -42
View File
@@ -697,7 +697,7 @@ window._wsInitData = (function() {
// PDF preview
if (/\.pdf$/.test(name)) {
this.previewPanelContent = '<div style="text-align:center;padding:24px;color:var(--text-secondary);">'
+ '<span style="font-size:48px;display:block;margin-bottom:8px;">📕</span>'
+ '<span style="font-size:48px;display:block;margin-bottom:8px;">'+getSvgIcon('file',32)+'</span>'
+ '<div style="font-size:14px;margin-bottom:4px;">' + this._escHtml(node.name) + '</div>'
+ '<div style="font-size:12px;color:var(--text-tertiary);margin-bottom:16px;">' + (node.size_display || '') + '</div>'
+ '<a href="/pages/' + node.id + '" class="btn btn-secondary" style="display:inline-block;text-decoration:none;">Open in viewer</a>'
@@ -1394,7 +1394,7 @@ window._wsInitData = (function() {
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>';
var html = '<div style="text-align:center;padding:16px;">'+getSvgIcon('file',48)+'<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...';
@@ -1479,21 +1479,21 @@ window._wsInitData = (function() {
// FlowDeck internal pages (no extension, blocks/markdown format)
if (contentFormat && contentFormat !== 'file') return '<span class="page-icon-svg">'+getSvgIcon('edit',14)+'</span>';
var n = (name || '').toLowerCase();
if (/\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(n)) return '🖼️';
if (/\.pdf$/.test(n)) return '📕';
if (/\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('image',14)+'</span>';
if (/\.pdf$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
if (/\.(md|markdown)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('edit',14)+'</span>';
if (/\.(py)$/.test(n)) return '🐍';
if (/\.(py)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
if (/\.(js|jsx|ts|tsx)$/.test(n)) return '📜';
if (/\.(html?|xml)$/.test(n)) return '🌐';
if (/\.css$/.test(n)) return '🎨';
if (/\.(html?|xml)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
if (/\.css$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
if (/\.json$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
if (/\.sql$/.test(n)) return '🗃️';
if (/\.(sh|bash|zsh)$/.test(n)) return '💻';
if (/\.ps1$/.test(n)) return '⚡';
if (/\.sql$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
if (/\.(sh|bash|zsh)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
if (/\.ps1$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
if (/\.(rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r)$/.test(n)) return '📜';
if (/\.(txt|log)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
if (/\.(zip|tar|gz|rar|7z)$/.test(n)) return '📦';
return '📄';
if (/\.(zip|tar|gz|rar|7z)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
},
// ── Search / Filter ──
@@ -2029,16 +2029,16 @@ window._wsInitData = (function() {
},
_fileTypeLabel(name, contentFormat) {
if (contentFormat && contentFormat !== 'file') return '📝 Page';
if (contentFormat && contentFormat !== 'file') return 'Page';
var n = (name || '').toLowerCase();
if (/\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(n)) return '🖼️ Image';
if (/\.pdf$/.test(n)) return '📕 PDF';
if (/\.(py|js|jsx|ts|tsx|rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r|sh|bash|ps1|sql)$/.test(n)) return '📜 Code';
if (/\.(txt|md|markdown|log)$/.test(n)) return '📄 Text';
if (/\.(zip|tar|gz|rar|7z)$/.test(n)) return '📦 Archive';
if (/\.(mp3|wav|ogg|flac|aac)$/.test(n)) return '🎵 Audio';
if (/\.(mp4|webm|mov|avi)$/.test(n)) return '🎬 Video';
return '📄 File';
if (/\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(n)) return 'Image';
if (/\.pdf$/.test(n)) return 'PDF';
if (/\.(py|js|jsx|ts|tsx|rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r|sh|bash|ps1|sql)$/.test(n)) return 'Code';
if (/\.(txt|md|markdown|log)$/.test(n)) return 'Text';
if (/\.(zip|tar|gz|rar|7z)$/.test(n)) return 'Archive';
if (/\.(mp3|wav|ogg|flac|aac)$/.test(n)) return 'Audio';
if (/\.(mp4|webm|mov|avi)$/.test(n)) return 'Video';
return 'File';
},
selectAllFiltered(ev) {
@@ -2161,15 +2161,15 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
<span class="dropdown-arrow">▾</span>
</button>
<div class="view-dropdown-menu" x-show="viewMenuOpen" x-transition @click="viewMenuOpen=false">
<button class="view-menu-item" :class="{ active: viewMode==='tree' }" @click="viewMode='tree', previewItem=null">🌳 Tree</button>
<button class="view-menu-item" :class="{ active: viewMode==='list' }" @click="viewMode='list', previewItem=null">📋 List</button>
<button class="view-menu-item" :class="{ active: viewMode==='details' }" @click="viewMode='details', previewItem=null">📊 Details</button>
<button class="view-menu-item" :class="{ active: viewMode==='title' }" @click="viewMode='title', previewItem=null">🏷️ Cards</button>
<button class="view-menu-item" :class="{ active: viewMode==='content' }" @click="viewMode='content', previewItem=null">📝 Content</button>
<button class="view-menu-item" :class="{ active: viewMode==='tree' }" @click="viewMode='tree', previewItem=null">Tree</button>
<button class="view-menu-item" :class="{ active: viewMode==='list' }" @click="viewMode='list', previewItem=null">List</button>
<button class="view-menu-item" :class="{ active: viewMode==='details' }" @click="viewMode='details', previewItem=null">Details</button>
<button class="view-menu-item" :class="{ active: viewMode==='title' }" @click="viewMode='title', previewItem=null">Cards</button>
<button class="view-menu-item" :class="{ active: viewMode==='content' }" @click="viewMode='content', previewItem=null">Content</button>
</div>
</div>
<div class="search-bar-inline">
<span class="search-icon">🔍</span>
<span class="search-icon" x-html="getSvgIcon('search',16)"></span>
<input class="search-input" type="text" placeholder="Filter..."
x-model="searchQuery" @input="doFilter" autocomplete="off">
<button class="search-clear" x-show="searchQuery" @click="searchQuery='', doFilter()" title="Clear">×</button>
@@ -2184,14 +2184,14 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
</select>
<button class="btn-icon" @click="expandAll()" title="Expand" x-show="viewMode==='tree'">⊞</button>
<button class="btn-icon" @click="collapseAll()" title="Collapse" x-show="viewMode==='tree'">⊟</button>
<button class="btn-icon btn-create" onclick="window.FlowDeck.createPage()" title="New file">📄</button>
<button class="btn-icon btn-create" onclick="window.FlowDeck.showCreateFolderModal()" title="New folder">📁</button>
<button class="btn-icon btn-create" onclick="window.FlowDeck.createPage()" title="New file" x-html="getSvgIcon('file',16)"></button>
<button class="btn-icon btn-create" onclick="window.FlowDeck.showCreateFolderModal()" title="New folder" x-html="getSvgIcon('folder',16)"></button>
</div>
<!-- Bulk actions bar -->
<div class="bulk-bar" x-show="selectedIdsCount > 0" x-transition>
<span x-text="selectedIdsCount + ' selected'"></span>
<button class="btn btn-danger" @click="bulkDelete()">🗑 Delete</button>
<button class="btn btn-danger" @click="bulkDelete()">Delete</button>
<button class="btn btn-secondary" @click="clearSelection()">✕ Clear</button>
</div>
@@ -2200,11 +2200,11 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
<!-- Compact filter row -->
<div class="filter-row" x-show="tree.length>0 || searchQuery || filterType">
<button class="filter-chip" :class="{ active: filterType === 'folder' }" @click="filterByType('folder')" title="Folders">📁</button>
<button class="filter-chip" :class="{ active: filterType === 'page' }" @click="filterByType('page')" title="Pages">📝</button>
<button class="filter-chip" :class="{ active: filterType === 'pdf' }" @click="filterByType('pdf')" title="PDF">📕</button>
<button class="filter-chip" :class="{ active: filterType === 'image' }" @click="filterByType('image')" title="Images">🖼️</button>
<button class="filter-chip" :class="{ active: filterType === 'code' }" @click="filterByType('code')" title="Code">📜</button>
<button class="filter-chip" :class="{ active: filterType === 'folder' }" @click="filterByType('folder')" title="Folders" x-html="getSvgIcon('folder',14)"></button>
<button class="filter-chip" :class="{ active: filterType === 'page' }" @click="filterByType('page')" title="Pages" x-html="getSvgIcon('file',14)"></button>
<button class="filter-chip" :class="{ active: filterType === 'pdf' }" @click="filterByType('pdf')" title="PDF" x-html="getSvgIcon('file',14)"></button>
<button class="filter-chip" :class="{ active: filterType === 'image' }" @click="filterByType('image')" title="Images" x-html="getSvgIcon('image',14)"></button>
<button class="filter-chip" :class="{ active: filterType === 'code' }" @click="filterByType('code')" title="Code" x-html="getSvgIcon('file',14)"></button>
<button class="filter-chip" :class="{ active: filterType === 'text' }" @click="filterByType('text')" title="Text">📄</button>
<span x-show="filterType || searchQuery" class="filter-clear" @click="filterType='', searchQuery='', doFilter()">✕ clear</span>
</div>
@@ -2247,7 +2247,7 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
<span style="width:16px;flex-shrink:0;"></span>
</template>
<span class="icon" x-text="_fileIcon(node.name, node.is_folder, node.content_format)"></span>
<span class="icon" x-html="_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)"
@@ -2341,7 +2341,7 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
<tr :class="{ selected: !!selectedIds[node.id] }">
<td><input type="checkbox" :checked="!!selectedIds[node.id]" @click.stop="toggleSelect(node, $event)"></td>
<td>
<span class="table-icon" x-text="_fileIcon(node.name, node.is_folder, node.content_format)"></span>
<span class="table-icon" x-html="_fileIcon(node.name, node.is_folder, node.content_format)"></span>
<span class="col-name" @click="node.is_folder ? navigateToFolder(node.id) : openPage(node.id)" x-text="node.name"></span>
</td>
<td class="col-path" x-text="node._path || '—'"></td>
@@ -2389,7 +2389,7 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
<tbody>
<template x-for="node in displayTree" :key="'l'+node.id">
<tr :data-ws-id="node.id" @click="node.is_folder ? navigateToFolder(node.id) : selectForPreview(node)" @mouseenter="showPreview($event, node)" @mouseleave="hidePreview()" style="cursor:pointer">
<td><span x-text="_fileIcon(node.name, node.is_folder, node.content_format)"></span></td>
<td><span x-html="_fileIcon(node.name, node.is_folder, node.content_format)"></span></td>
<td class="col-name" x-text="node.name"></td>
<td style="font-size:12px;color:var(--text-tertiary)" x-text="node.is_folder ? '📁 Folder' : _fileTypeLabel(node.name, node.content_format)"></td>
<td style="font-size:12px;color:var(--text-secondary)" x-text="node.is_folder ? (node.child_count||0)+' items' : (node.size_display||'—')"></td>
@@ -2415,7 +2415,7 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
<template x-for="node in displayTree" :key="'d'+node.id">
<tr :data-ws-id="node.id" @click="node.is_folder ? navigateToFolder(node.id) : selectForPreview(node)" @mouseenter="showPreview($event, node)" @mouseleave="hidePreview()" style="cursor:pointer"
:class="{ 'selected-row': previewItem && previewItem.id === node.id }">
<td><span class="table-icon" x-text="_fileIcon(node.name, node.is_folder, node.content_format)"></span></td>
<td><span class="table-icon" x-html="_fileIcon(node.name, node.is_folder, node.content_format)"></span></td>
<td class="col-name" x-text="node.name"></td>
<td class="col-path" x-text="node._path || '—'"></td>
<td class="col-size" x-text="node.size_display || '—'"></td>
@@ -2439,7 +2439,7 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
<template x-for="node in displayTree" :key="'g'+node.id">
<div class="title-card" :data-ws-id="node.id" @click="node.is_folder ? navigateToFolder(node.id) : selectForPreview(node)" @mouseenter="showPreview($event, node)" @mouseleave="hidePreview()"
:class="{ 'selected-card': previewItem && previewItem.id === node.id }">
<div class="title-card-icon" x-text="node.is_folder ? '📁' : _fileIcon(node.name, node.is_folder, node.content_format)"></div>
<div class="title-card-icon" x-html="node.is_folder ? getSvgIcon('folder',24) : _fileIcon(node.name, node.is_folder, node.content_format)"></div>
<div class="title-card-name" x-text="node.name"></div>
<div class="title-card-meta">
<span x-text="_formatDate(node.updated_at)"></span>
@@ -2455,7 +2455,7 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
<div class="content-item" :data-ws-id="node.id" @click="node.is_folder ? navigateToFolder(node.id) : selectForPreview(node)" @mouseenter="showPreview($event, node)" @mouseleave="hidePreview()"
:class="{ 'selected-row': previewItem && previewItem.id === node.id }">
<div class="content-item-header">
<span x-text="_fileIcon(node.name, node.is_folder, node.content_format)"></span>
<span x-html="_fileIcon(node.name, node.is_folder, node.content_format)"></span>
<span class="col-name" x-text="node.name"></span>
<span style="font-size:11px;color:var(--text-tertiary);margin-left:auto" x-text="_formatDate(node.updated_at)"></span>
</div>
@@ -2608,7 +2608,7 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
<template x-if="previewItem">
<div class="preview-panel" x-show="previewItem" x-transition:enter="preview-enter" x-transition:leave="preview-leave">
<div class="preview-panel-header">
<span x-text="_fileIcon(previewItem.name, previewItem.is_folder, previewItem.content_format)"></span>
<span x-html="_fileIcon(previewItem.name, previewItem.is_folder, previewItem.content_format)"></span>
<span class="preview-panel-title" x-text="previewItem.name"></span>
<button class="preview-close" @click="previewItem=null" title="Close preview">×</button>
</div>