fix(library+workspace): arborescence dans Workspace tab, icônes drag+multi-select visibles, inline rename au clic
FlowDeck CI / test (push) Failing after 13s
FlowDeck CI / docker (push) Has been skipped

- Library: ajout /api/library/children/{id} pour charger les enfants à la demande
- Library: drag handle (.drag-handle) et checkbox (.lib-checkbox) toujours visibles
- Library: SortableJS init pour drag-and-drop réordonner les rows
- Library: toggleExpand() async avec chargement des enfants depuis l'API
- Local workspace: drag handle 6-dots + checkbox toujours visible dans les tree items
- Local workspace: renderChildren() inclut checkbox + drag handle + inline rename
- Local workspace: clic sur nom de fichier = inline rename (plus navigation directe)
- Local workspace: bouton Open pour ouvrir le fichier (double-clic aussi)
- CSS: .item-checkbox toujours visible (plus opacity:0)
- CSS: .drag-handle avec opacité .45 par défaut, 1.0 au hover
This commit is contained in:
2026-07-19 12:53:09 -04:00
parent 6a2d56a7bd
commit 33933352a8
3 changed files with 183 additions and 27 deletions
+87 -17
View File
@@ -79,8 +79,7 @@
.ws-more-item:hover{opacity:1;background:var(--bg-tertiary);}
/* Multi-select */
.item-checkbox{width:14px;height:14px;flex-shrink:0;cursor:pointer;accent-color:var(--accent);opacity:0;transition:opacity 150ms;}
.ws-tree-item:hover .item-checkbox,.ws-tree-item.selected .item-checkbox{opacity:1;}
.item-checkbox{width:15px;height:15px;flex-shrink:0;cursor:pointer;accent-color:var(--accent);margin:0 4px;}
.ws-tree-item.selected{background:rgba(35,131,226,.1);}
/* Bulk bar */
@@ -118,6 +117,10 @@
/* Drag & drop */
.ws-layout.drop-active { outline: 2px dashed var(--accent); outline-offset: -4px; border-radius: 8px; }
/* Drag handle — 6 dots */
.drag-handle{display:flex;flex-wrap:wrap;width:16px;height:16px;cursor:grab;align-content:space-between;justify-content:space-between;flex-shrink:0;opacity:0.45;margin:0 4px;}
.ws-tree-item:hover .drag-handle{opacity:1;}
.drag-handle .dot{width:3px;height:3px;background:var(--text-tertiary);border-radius:50%;}
.drop-overlay{display:none;position:fixed;inset:0;background:rgba(35,131,226,.08);z-index:100;pointer-events:none;align-items:center;justify-content:center;}
.drop-overlay.active{display:flex;}
.drop-overlay .drop-hint{background:var(--bg);border:2px dashed var(--accent);border-radius:16px;padding:40px 60px;text-align:center;color:var(--text);font-size:16px;}
@@ -942,34 +945,60 @@ window._wsInitData = (function() {
var hasKids = c.children && c.children.length > 0;
var icon = this._fileIcon(c.name, c.is_folder, c.content_format);
var escName = this.esc(c.name);
var escId = c.id;
html += '<li><div class="ws-tree-item" style="padding-left:'+(depth*20)+'px" draggable="true"'
+ ' data-folder-id="'+(c.is_folder?c.id:'')+'"'
+ ' data-ws-id="'+c.id+'"'
+ ' ondragstart="var d=window._wsData;if(d)d.onInternalDragStart(event,'+c.id+')"'
+ ' ondragend="event.target.style.opacity=\'\'">';
+ ' data-ws-id="'+escId+'"'
+ ' ondragstart="var d=window._wsData;if(d)d.onInternalDragStart(event,'+escId+')"'
+ ' ondragend="event.target.style.opacity=\\'\\'">';
// Checkbox
html += '<input type="checkbox" class="item-checkbox"'
+ ' data-ws-id="'+escId+'"'
+ ' onclick="event.stopPropagation();var d=window._wsData;if(d)d.toggleSelect({id:'+escId+',name:\\''+escName+'\\'},event)"'
+ ' title="Select item">';
// Drag handle
html += '<div class="drag-handle" title="Drag to move">'
+ '<span class="dot"></span><span class="dot"></span>'
+ '<span class="dot"></span><span class="dot"></span>'
+ '<span class="dot"></span><span class="dot"></span></div>';
if (c.is_folder && hasKids) {
html += '<button class="ws-chevron ws-expand-btn" data-ws-id="'+c.id+'" onclick="event.stopPropagation();var d=window._wsData;if(d)d.toggleExpand('+c.id+',this)">▶</button>';
html += '<button class="ws-chevron ws-expand-btn" data-ws-id="'+escId+'" onclick="event.stopPropagation();var d=window._wsData;if(d)d.toggleExpand('+escId+',this)">▶</button>';
} else {
html += '<span style="width:16px;flex-shrink:0;"></span>';
}
html += '<span class="icon">'+icon+'</span>';
if (c.is_folder) {
var countBadge = (c.child_count > 0) ? ' <span class="child-count">('+c.child_count+')</span>' : '';
html += '<span class="name folder" onclick="window.location=\'/local-workspace?folder='+c.id+'\'">'+c.name+countBadge+'</span>';
html += '<span class="name folder" onclick="window._wsData.navigateToFolder('+escId+')">'+c.name+countBadge+'</span>';
} else {
html += '<span class="name file" onclick="window.location=\'/pages/'+c.id+'\'"'
+ ' onmouseenter="var d=window._wsData;if(d)d.showPreview(event,{id:'+c.id+',name:\''+escName+'\',is_folder:false,content_format:\''+(c.content_format||'')+'\'})"'
+ ' onmouseleave="var d=window._wsData;if(d)d.hidePreview()">'+c.name+'</span>';
// Click to rename, double-click or button to open
html += '<span class="name file" onclick="event.stopPropagation();window._wsData.startRenderChildRename('+escId+',\''+escName+'\')"'
+ ' ondblclick="window.location=\\'/pages/'+escId+'\\'"'
+ ' title="Click to rename, double-click to open"'
+ ' onmouseenter="var d=window._wsData;if(d)d.showPreview(event,{id:'+escId+',name:\\''+escName+'\\',is_folder:false,content_format:\\''+(c.content_format||'')+'\\'})"'
+ ' onmouseleave="var d=window._wsData;if(d)d.hidePreview()">'
+ '<span id="rc-name-'+escId+'">'+c.name+'</span>'
+ '<input id="rc-input-'+escId+'" class="inline-rename-input" style="display:none;width:100%"'
+ ' onkeydown="if(event.key===\\'Enter\\')window._wsData.commitRenderChildRename('+escId+')"'
+ ' onblur="window._wsData.commitRenderChildRename('+escId+')"'
+ ' onclick="event.stopPropagation()">'
+ '</span>';
}
html += '<div class="actions">';
if (c.is_folder) {
html += '<span style="display:flex;gap:2px">';
html += '<button class="ws-act" data-ws-action="new-file" data-ws-id="'+c.id+'" data-ws-name="'+escName+'">📄</button>';
html += '<button class="ws-act" data-ws-action="new-folder" data-ws-id="'+c.id+'" data-ws-name="'+escName+'">📁</button>';
html += '<button class="ws-act" data-ws-action="new-file" data-ws-id="'+escId+'" data-ws-name="'+escName+'">📄</button>';
html += '<button class="ws-act" data-ws-action="new-folder" data-ws-id="'+escId+'" data-ws-name="'+escName+'">📁</button>';
html += '</span>';
}
html += '<button class="ws-act" data-ws-action="rename" data-ws-id="'+c.id+'" data-ws-name="'+escName+'">✏️</button>';
html += '<button class="ws-act danger" data-ws-action="delete" data-ws-id="'+c.id+'" data-ws-name="'+escName+'">🗑</button>';
html += '<button class="ws-act" data-ws-action="rename" data-ws-id="'+escId+'" data-ws-name="'+escName+'">✏️</button>';
html += '<button class="ws-act danger" data-ws-action="delete" data-ws-id="'+escId+'" data-ws-name="'+escName+'">🗑</button>';
// Open button for files
if (!c.is_folder) {
html += '<a href="/pages/'+escId+'" class="btn-open" style="display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border:1px solid var(--border);border-radius:4px;font-size:12px;color:var(--text-secondary);background:var(--bg-tertiary);cursor:pointer;text-decoration:none;margin-left:4px;opacity:0;"'
+ ' onmouseover="this.style.opacity=\\'1\\'" onmouseout="this.style.opacity=\\'0\\'"'
+ ' onclick="event.stopPropagation();return true">⤢ Open</a>';
}
// Tags
if (c.tags && c.tags.length > 0) {
html += '<span style="display:flex;gap:2px;margin-left:4px;">';
@@ -990,6 +1019,41 @@ window._wsInitData = (function() {
return html;
},
// Inline rename for renderChildren (static HTML)
startRenderChildRename(id, currentName) {
var nameSpan = document.getElementById('rc-name-' + id);
var input = document.getElementById('rc-input-' + id);
if (!nameSpan || !input) return;
nameSpan.style.display = 'none';
input.style.display = '';
input.value = currentName;
input.focus();
input.select();
},
async commitRenderChildRename(id) {
var nameSpan = document.getElementById('rc-name-' + id);
var input = document.getElementById('rc-input-' + id);
if (!input) return;
var newName = (input.value || '').trim();
input.style.display = 'none';
if (nameSpan) nameSpan.style.display = '';
if (!newName) return;
try {
var r = await fetch('/api/local-workspace/items/' + id, {
method: 'PUT', headers: {'Content-Type':'application/json'},
body: JSON.stringify({name: newName})
});
if (r.ok) {
if (nameSpan) nameSpan.textContent = newName;
// Update tree node
var node = this._findNodeById(this.tree, id);
if (node) { node.name = newName; }
this._reloadAfterAction();
}
} catch(e) {}
},
esc(s) {
if (!s) return '';
return s.replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/"/g,'&quot;');
@@ -2166,6 +2230,12 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
:checked="!!selectedIds[node.id]"
@click.stop="toggleSelect(node, $event)"
title="Select item">
<!-- drag handle -->
<div class="drag-handle" style="margin-right:2px;" title="Drag to move">
<span class="dot"></span><span class="dot"></span>
<span class="dot"></span><span class="dot"></span>
<span class="dot"></span><span class="dot"></span>
</div>
<!-- chevron -->
<template x-if="node.is_folder && node.children && node.children.length > 0">
<button class="ws-chevron"
@@ -2190,10 +2260,10 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
</span>
</template>
<template x-if="!node.is_folder">
<span class="name file" @click.stop="openPage(node.id)"
@dblclick.stop="startInlineRename(node, $event)"
<span class="name file" @click.stop="startInlineRename(node, $event)"
@mouseenter="showPreview($event, node)"
@mouseleave="hidePreview()">
@mouseleave="hidePreview()"
title="Click to rename, double-click to open">
<span x-show="renamingId !== node.id" x-text="node.name"></span>
<input x-show="renamingId === node.id" class="inline-rename-input" x-model="renamingName"
@keydown.enter="commitRename(node)" @keydown.escape="cancelRename()"