feat: sidebar compact + menu contextuel (clic droit/long press) + boutons Expand/Collapse All
FlowDeck CI / test (push) Failing after 5s
FlowDeck CI / docker (push) Has been skipped

Sidebar workspace:
- Arbre plus compact: padding réduit (4+12*depth au lieu de 8+16),
  gap 4px, min-height 24px, font 13px, chevron 12px
- Boutons d'action (📄📁✏️🗑) remplacés par menu contextuel
  au clic droit (@contextmenu.prevent) et long-press mobile (500ms)
- Menu: New File, New Folder (si dossier), Rename, Delete
- Nom de l'item affiché en bas du menu

Page workspace:
- Boutons ⊞ (Expand All) et ⊟ (Collapse All) dans la toolbar
- expandAll(): parcourt l'arbre, set expanded[id]=true, toggle DOM
- collapseAll(): vide expanded, masque tous les <ul>
This commit is contained in:
2026-07-11 13:37:04 -04:00
parent d97f79f737
commit 08d2bac793
3 changed files with 136 additions and 14 deletions
+94 -9
View File
@@ -116,14 +116,18 @@
@dragover.prevent="dragOver($event, {{ page.db_id }}, {{ 'true' if page.is_folder else 'false' }})"
@dragleave="dragLeave($event)"
@drop.prevent="drop($event, {{ page.db_id }})"
style="padding-left:{{ 8 + depth * 16 }}px;">
style="padding-left:{{ 4 + depth * 12 }}px;"
@contextmenu.prevent="showWsContext($event, {{ page.db_id }}, '{{ page.name | e }}', {{ 'true' if page.is_folder else 'false' }})"
@touchstart="wsTouchStart($event)"
@touchend="wsTouchEnd($event, {{ page.db_id }}, '{{ page.name | e }}', {{ 'true' if page.is_folder else 'false' }})"
@touchmove="wsTouchMove($event)">
{% if page.is_folder %}
<button class="tree-chevron"
@click.stop="toggleTreeFolder({{ page.db_id }})"
:class="{ open: expandedFolders[{{ page.db_id }}] }"
title="Toggle folder">▶</button>
{% else %}
<span style="width:16px;flex-shrink:0;"></span>
<span style="width:12px;flex-shrink:0;"></span>
{% endif %}
<span class="page-icon">{{ page.icon }}</span>
{% if page.is_folder %}
@@ -134,13 +138,6 @@
{% else %}
<a href="/pages/{{ page.db_id }}" class="page-name" draggable="false">{{ page.name }}</a>
{% endif %}
<div class="sidebar-item-actions">
{% if page.is_folder %}
<button class="ws-act" @click.stop="newPageInFolder({{ page.db_id }})" title="New file in folder">📄</button>
<button class="ws-act" @click.stop="newFolderInFolder({{ page.db_id }})" title="New sub-folder">📁</button>
{% endif %}
<button class="ws-act danger" @click.stop="deleteWorkspacePage({{ page.db_id }})" title="Delete">×</button>
</div>
</li>
{% if page.children %}
<li x-show="expandedFolders[{{ page.db_id }}]" x-transition style="list-style:none;margin:0;padding:0;">
@@ -446,6 +443,36 @@
</div>
</div>
<!-- ═══════════ WORKSPACE TREE CONTEXT MENU ═══════════ -->
<div class="context-menu" id="ws-context-menu"
:class="{ visible: wsCtx.visible }"
:style="'left:' + wsCtx.x + 'px; top:' + wsCtx.y + 'px'"
@click.away="wsCtx.visible = false">
<template x-if="wsCtx.isFolder">
<div class="context-menu-section">
<div class="context-menu-item" @click="wsCtxAction('new-file')">
<span class="menu-icon">📄</span> New File
</div>
<div class="context-menu-item" @click="wsCtxAction('new-folder')">
<span class="menu-icon">📁</span> New Folder
</div>
</div>
</template>
<div class="context-menu-section">
<div class="context-menu-item" @click="wsCtxAction('rename')">
<span class="menu-icon">✏️</span> Rename
</div>
<div class="context-menu-item" @click="wsCtxAction('delete')">
<span class="menu-icon">🗑</span> Delete
</div>
</div>
<div class="context-menu-section">
<div class="context-menu-item text-dim" style="font-size:11px;cursor:default;">
<span x-text="wsCtx.name"></span>
</div>
</div>
</div>
<!-- ═══════════ TOOLTIP ═══════════ -->
<div class="tooltip" id="tooltip" x-ref="tooltip"></div>
@@ -492,6 +519,64 @@
contextActions: {},
showNewPageMenu: false,
// ── Workspace tree context menu ──
wsCtx: { visible: false, x: 0, y: 0, id: null, name: '', isFolder: false },
wsTouchTimer: null,
showWsContext(ev, id, name, isFolder) {
this.wsCtx = { visible: true, x: ev.clientX, y: ev.clientY, id: id, name: name, isFolder: isFolder };
},
hideWsContext() { this.wsCtx.visible = false; },
wsTouchStart(ev) {
var self = this;
this.wsTouchTimer = setTimeout(function() {
self.wsTouchTimer = null;
var el = ev.target.closest('.ws-tree-item');
if (el) {
var id = parseInt(el.getAttribute('data-id'));
var name = el.getAttribute('data-name') || '';
var isFolder = el.getAttribute('data-is-folder') === 'true';
var touch = ev.touches[0];
self.wsCtx = { visible: true, x: touch.clientX, y: touch.clientY, id: id, name: name, isFolder: isFolder };
}
}, 500);
},
wsTouchEnd(ev, id, name, isFolder) {
if (this.wsTouchTimer) { clearTimeout(this.wsTouchTimer); this.wsTouchTimer = null; return; }
// Already opened by touchstart; prevent Alpine @touchend from also firing the context menu
ev.preventDefault();
},
wsTouchMove(ev) {
if (this.wsTouchTimer) { clearTimeout(this.wsTouchTimer); this.wsTouchTimer = null; }
},
wsCtxAction(action) {
var id = this.wsCtx.id;
var name = this.wsCtx.name;
this.wsCtx.visible = false;
if (!id) return;
switch(action) {
case 'new-file':
this.newPageInFolder(id);
break;
case 'new-folder':
this.newFolderInFolder(id);
break;
case 'rename':
var newName = prompt('Rename:', name);
if (newName && newName.trim()) {
fetch('/api/local-workspace/items/' + id, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
body: JSON.stringify({ name: newName.trim() })
}).then(function(r) { if (r.ok) window.location.reload(); });
}
break;
case 'delete':
if (!confirm('Delete "' + name + '"?')) break;
this.deleteWorkspacePage(id);
break;
}
},
// ── Favorites tracking ──
favoriteIds: new Set([{% for p in favorite_pages %}'{{ p.id }}'{% if not loop.last %},{% endif %}{% endfor %}]),
+37
View File
@@ -158,6 +158,8 @@
📁 <span x-text="breadcrumb.length > 0 ? breadcrumb[breadcrumb.length-1].name : ''"></span>
</h1>
<div class="ws-toolbar">
<button class="btn-icon" @click="expandAll()" title="Expand all">⊞</button>
<button class="btn-icon" @click="collapseAll()" title="Collapse all">⊟</button>
<button class="btn-icon" @click="openModal('file')" title="New file">📄</button>
<button class="btn-icon" @click="openModal('folder')" title="New folder">📁</button>
</div>
@@ -448,6 +450,41 @@ document.addEventListener('alpine:init', function() {
window.location = '/local-workspace?folder=' + id;
},
// ── Expand / Collapse all ──
expandAll() {
var self = this;
this._walkTree(this.tree, function(n) {
if (n.is_folder && n.children && n.children.length > 0) {
self.expanded[n.id] = true;
}
});
this.expanded = Object.assign({}, this.expanded);
// Toggle static HTML for renderChildren
this._toggleAllUl(this.$el, true);
},
collapseAll() {
this.expanded = {};
this._toggleAllUl(this.$el, false);
},
_walkTree(nodes, fn) {
for (var i=0; i<nodes.length; i++) {
fn(nodes[i]);
if (nodes[i].children) this._walkTree(nodes[i].children, fn);
}
},
_toggleAllUl(el, show) {
var uls = el.querySelectorAll('ul.ws-tree');
for (var i=0; i<uls.length; i++) {
uls[i].style.display = show ? '' : 'none';
}
// Also toggle chevrons
var chevs = el.querySelectorAll('.ws-chevron');
for (var j=0; j<chevs.length; j++) {
if (show) chevs[j].classList.add('open');
else chevs[j].classList.remove('open');
}
},
// ── Search / Filter ──
doFilter() {
var q = (this.searchQuery || '').toLowerCase().trim();
+5 -5
View File
@@ -321,15 +321,15 @@ input, select, textarea { font-family: inherit; color: inherit; }
position: relative;
display: flex;
align-items: center;
gap: var(--space-sm);
padding: 3px 6px;
padding-left: 8px;
gap: 4px;
padding: 2px 4px;
padding-left: 4px;
border-radius: var(--radius-sm);
font-size: 14px;
font-size: 13px;
color: var(--text-primary);
cursor: pointer;
transition: background var(--transition);
min-height: 28px;
min-height: 24px;
}
.sidebar-item:hover {