local-workspace: replace Alpine preview panel with vanilla JS DOM manipulation
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

Alpine 3.14.9 x-if and x-show are fundamentally broken for reactive
visibility toggling of the preview panel. Complete rewrite:

- Replace all Alpine directives in preview panel with static HTML + IDs
- selectForPreview(): vanilla JS to populate fields and show panel
- closePreviewPanel(): vanilla JS to hide panel
- _loadPreviewContentVanilla(): async content loader using innerHTML
- Open button wired via onclick to navigate to /pages/{id}
- No Alpine reactivity dependencies - panel just works
This commit is contained in:
2026-07-22 09:09:17 -04:00
parent 45eefa0c11
commit 31db48a40d
+71 -46
View File
@@ -796,40 +796,72 @@ window._wsInitData = (function() {
return null;
},
// ── Preview panel ──
// ── Preview panel (vanilla JS — Alpine x-if/x-show broken in 3.14.9) ──
selectForPreview(node) {
this.previewItem = node;
this.previewPanelContent = '<div style="text-align:center;padding:40px;color:var(--text-tertiary)">Loading...</div>';
this._loadPreviewContent(node);
var panel = document.getElementById('ws-preview-panel');
if (!panel) return;
// Populate panel
document.getElementById('ws-preview-icon').innerHTML = this._fileIcon(node.name, node.is_folder, node.content_format);
document.getElementById('ws-preview-title').textContent = node.name;
document.getElementById('ws-preview-type').textContent = node.is_folder ? 'Folder' : this._fileTypeLabel(node.name, node.content_format);
document.getElementById('ws-preview-size').textContent = node.size_display || '-';
document.getElementById('ws-preview-date').textContent = this._formatDate(node.updated_at);
// Tags
var tagDiv = document.getElementById('ws-preview-tags');
tagDiv.innerHTML = '';
if (node.tags && node.tags.length > 0) {
tagDiv.style.display = 'flex';
for (var i = 0; i < node.tags.length; i++) {
var t = node.tags[i];
tagDiv.innerHTML += '<span class=\"tag-chip\" style=\"background:' + this._escHtml(t.color) + '\">' + this._escHtml(t.name) + '</span>';
}
} else {
tagDiv.style.display = 'none';
}
// Open button
var openBtn = document.getElementById('ws-preview-open-btn');
openBtn.onclick = function() { window.location = '/pages/' + node.id; };
// Body
document.getElementById('ws-preview-body').innerHTML = '<div style=\"text-align:center;padding:40px;color:var(--text-tertiary)\">Loading...</div>';
// Show panel
panel.style.display = 'flex';
// Load content async
this._loadPreviewContentVanilla(node);
},
async _loadPreviewContent(node) {
closePreviewPanel() {
var panel = document.getElementById('ws-preview-panel');
if (panel) panel.style.display = 'none';
this.previewItem = null;
},
async _loadPreviewContentVanilla(node) {
var body = document.getElementById('ws-preview-body');
if (!body) return;
if (node.is_folder) {
this.previewPanelContent = '<div style="text-align:center;padding:40px;color:var(--text-secondary)">'
+ '<span class="empty-state-icon">'+getSvgIcon('folder',32)+'</span>'
+ '<div style="font-size:14px">' + (node.child_count||0) + ' item(s)</div>'
+ '<a href="/local-workspace?folder=' + node.id + '" class="btn btn-secondary" style="display:inline-block;text-decoration:none;margin-top:16px">Open folder</a>'
body.innerHTML = '<div style=\"text-align:center;padding:40px;color:var(--text-secondary)\">'
+ '<span>' + getSvgIcon('folder',32) + '</span>'
+ '<div style=\"font-size:14px\">' + (node.child_count||0) + ' item(s)</div>'
+ '<a href=\"/local-workspace?folder=' + node.id + '\" class=\"btn btn-secondary\" style=\"display:inline-block;text-decoration:none;margin-top:16px\">Open folder</a>'
+ '</div>';
return;
}
try {
var name = (node.name || '').toLowerCase();
// Image preview
if (/\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(name)) {
var wsId = 1;
var imgUrl = '/api/files/' + wsId + '/' + encodeURIComponent(node.name);
this.previewPanelContent = '<div style="text-align:center;padding:12px;">'
+ '<img src="' + imgUrl + '" style="max-width:100%;max-height:50vh;border-radius:8px;" onerror="this.style.display=\'none\'">'
body.innerHTML = '<div style=\"text-align:center;padding:12px;\">'
+ '<img src=\"' + imgUrl + '\" style=\"max-width:100%;max-height:50vh;border-radius:8px;\">'
+ '</div>';
return;
}
// 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;">'+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>'
body.innerHTML = '<div style=\"text-align:center;padding:24px;color:var(--text-secondary);\">'
+ '<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>'
+ '</div>';
return;
}
@@ -841,14 +873,13 @@ window._wsInitData = (function() {
try {
var meta = JSON.parse(content);
if (meta.file_path && /\.(png|jpe?g|gif|webp|svg|bmp|ico)$/i.test(meta.file_path)) {
var wsId2 = 1;
var imgUrl2 = '/api/files/' + wsId2 + '/' + encodeURIComponent(meta.file_path);
this.previewPanelContent = '<img src="' + imgUrl2 + '" style="max-width:100%;max-height:50vh;border-radius:8px;">';
var imgUrl2 = '/api/files/' + 1 + '/' + encodeURIComponent(meta.file_path);
body.innerHTML = '<img src=\"' + imgUrl2 + '\" style=\"max-width:100%;max-height:50vh;border-radius:8px;\">';
return;
}
} catch(e) {}
preview = '<div style="text-align:center;padding:20px;color:var(--text-secondary)">'
+ '<span class="empty-state-icon">'+getSvgIcon('file',32)+'</span>'
preview = '<div style=\"text-align:center;padding:20px;color:var(--text-secondary)\">'
+ '<span>' + getSvgIcon('file',32) + '</span>'
+ (node.size_display || '') + ' — open to view</div>';
} else if (format === 'blocks') {
try {
@@ -856,16 +887,16 @@ window._wsInitData = (function() {
if (blocks && blocks.blocks) {
preview = blocks.blocks.slice(0, 30).map(function(b) {
var txt = b.content || b.text || (typeof b === 'string' ? b : '');
return '<div style="margin-bottom:6px;line-height:1.5">' + this._escHtml(txt) + '</div>';
return '<div style=\"margin-bottom:6px;line-height:1.5\">' + this._escHtml(txt) + '</div>';
}, this).join('');
}
} catch(e) { preview = '<pre>' + this._escHtml(String(content).substring(0, 2000)) + '</pre>'; }
} else {
preview = '<pre>' + this._escHtml(String(content).substring(0, 2000)) + '</pre>';
}
this.previewPanelContent = preview || '<div style="color:var(--text-tertiary);text-align:center;padding:20px">(empty)</div>';
body.innerHTML = preview || '<div style=\"color:var(--text-tertiary);text-align:center;padding:20px\">(empty)</div>';
} catch(e) {
this.previewPanelContent = '<div style="text-align:center;padding:20px;color:var(--text-tertiary)">Preview not available</div>';
body.innerHTML = '<div style=\"text-align:center;padding:20px;color:var(--text-tertiary)\">Preview not available</div>';
}
},
@@ -2800,33 +2831,27 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
<div class="file-preview-body" x-html="previewContent"></div>
</div>
<!-- Preview Panel — right side panel for file preview -->
<div class="preview-panel" x-show="previewItem" x-transition:enter="preview-enter" x-transition:leave="preview-leave" x-cloak>
<template x-if="previewItem">
<div style="display:flex;flex-direction:column;height:100%;">
<!-- Preview Panel — right side panel (vanilla JS, no Alpine) -->
<div id="ws-preview-panel" class="preview-panel" style="display:none;">
<div class="preview-panel-header">
<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>
<span id="ws-preview-icon"></span>
<span class="preview-panel-title" id="ws-preview-title"></span>
<button class="preview-close" onclick="window._wsData.closePreviewPanel()" title="Close preview">&times;</button>
</div>
<div class="preview-panel-meta">
<span x-text="previewItem.is_folder ? 'Folder' : _fileTypeLabel(previewItem.name, previewItem.content_format)"></span>
<span x-text="previewItem.size_display || '—'"></span>
<span x-text="_formatDate(previewItem.updated_at)"></span>
<span id="ws-preview-type"></span>
<span id="ws-preview-size"></span>
<span id="ws-preview-date"></span>
</div>
<div class="preview-panel-tags" x-show="(previewItem.tags||[]).length > 0">
<template x-for="t in (previewItem.tags||[])" :key="'ptag'+t.id">
<span class="tag-chip" :style="{background: t.color}"><span x-text="t.name"></span></span>
</template>
<div class="preview-panel-tags" id="ws-preview-tags" style="display:none;">
</div>
<div class="preview-panel-body" id="ws-preview-body">
<div style="text-align:center;padding:40px;color:var(--text-tertiary)">Select a file to preview</div>
</div>
<div class="preview-panel-body" x-html="previewPanelContent || '<div style=text-align:center;padding:40px;color:var(--text-tertiary)>Loading preview...</div>'"></div>
<div class="preview-panel-actions">
<button class="btn btn-secondary" @click="openPage(previewItem.id)">{{ fd_icon("file",14) }} Open</button>
<button class="btn btn-secondary" @click="startRename(previewItem)">{{ fd_icon("edit",14) }} Rename</button>
<button class="btn btn-danger" @click="deleteWithUndo(previewItem)">{{ fd_icon("trash",14) }} Delete</button>
<button class="btn btn-secondary" id="ws-preview-open-btn">{{ fd_icon("file",14) }} Open</button>
<button class="btn btn-secondary" onclick="window._wsData.closePreviewPanel()">{{ fd_icon("edit",14) }} Close</button>
</div>
</div>
</template>
</div>
</div><!-- /ws-split -->