fix(library+workspace): arborescence dans Workspace tab, icônes drag+multi-select visibles, inline rename au clic
- 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:
@@ -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,'"');
|
||||
@@ -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()"
|
||||
|
||||
Reference in New Issue
Block a user