From da1fccb38f0ce5f3123c74669baf9bd601d744e7 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Fri, 11 Sep 2026 16:42:46 -0400 Subject: [PATCH] feat(workspace): Notion-style multi-select, select/unselect all, Library-style bulk action bar with move-to modal --- app/templates/library.html | 5 + app/templates/local_workspace.html | 201 ++++++++++++++++++++++++++--- 2 files changed, 188 insertions(+), 18 deletions(-) diff --git a/app/templates/library.html b/app/templates/library.html index 4404311..4e2701b 100644 --- a/app/templates/library.html +++ b/app/templates/library.html @@ -279,6 +279,10 @@
+ @@ -329,6 +333,7 @@
+ Page name
diff --git a/app/templates/local_workspace.html b/app/templates/local_workspace.html index a65c3f6..a1dab46 100644 --- a/app/templates/local_workspace.html +++ b/app/templates/local_workspace.html @@ -59,6 +59,19 @@ .ws-select-bar button:hover{background:var(--bg-hover);color:var(--text-primary);} .ws-select-bar button svg{width:14px;height:14px;} +/* ── Multi-select "…" dropdown (Library-style) ── */ +.ws-select-menu{position:absolute;top:calc(100% + 4px);right:0;min-width:210px;background:var(--bg-modal);border:1px solid var(--border);border-radius:8px;box-shadow:var(--shadow-popover);z-index:300;padding:6px;} +.ws-select-menu .menu-item{padding:6px 8px;font-size:13px;color:var(--text-primary);cursor:pointer;border-radius:4px;display:flex;align-items:center;gap:8px;transition:background 0.1s;} +.ws-select-menu .menu-item:hover{background:var(--bg-hover);} +.ws-select-menu .menu-item svg{width:14px;height:14px;flex-shrink:0;} +.ws-select-menu .menu-item .shortcut{margin-left:auto;font-size:11px;color:var(--text-dim);} +.ws-select-menu .menu-danger{color:var(--danger);} +.ws-select-menu .more-sep{height:1px;background:var(--border);margin:4px 8px;} + +/* ── Move-to modal list items ── */ +.move-modal-item{padding:7px 10px;font-size:13px;color:var(--text-primary);cursor:pointer;border-radius:6px;display:flex;align-items:center;gap:8px;} +.move-modal-item:hover{background:var(--bg-hover);} + /* ── Table (Notion-style rows) ── */ .ws-table{width:100%;} .ws-table-header{display:flex;align-items:center;border-bottom:1px solid var(--border);padding:0 4px;} @@ -534,6 +547,15 @@ window._wsInitData = (function() { selectedIds: {}, selectedIdsCount: 0, lastSelectedId: null, + allSelected: false, + moreOpen: false, + + // Move-to modal + moveModalOpen: false, + moveIds: [], + moveSearchQuery: '', + moveCandidates: [], + _moveAllCandidates: [], // Undo undoVisible: false, @@ -685,6 +707,7 @@ window._wsInitData = (function() { this.flatTree = ft; // filteredTableItems this._computeFilteredTableItems(); + this._syncSelection(); }, // ── Pin/Favorite items ── @@ -702,7 +725,8 @@ window._wsInitData = (function() { // ── Multi-select ── toggleSelect(node, ev) { - if (ev.shiftKey && this.lastSelectedId) { + var e = ev || {}; + if (e.shiftKey && this.lastSelectedId) { // Range select var allIds = this._collectVisibleIds(this.displayTree); var start = allIds.indexOf(this.lastSelectedId); @@ -711,18 +735,19 @@ window._wsInitData = (function() { var from = Math.min(start, end), to = Math.max(start, end); for (var i = from; i <= to; i++) this.selectedIds[allIds[i]] = true; } - } else if (ev.ctrlKey || ev.metaKey) { - // Toggle single + } else if (e.ctrlKey || e.metaKey) { + // Toggle single, keep others if (!!this.selectedIds[node.id]) delete this.selectedIds[node.id]; else this.selectedIds[node.id] = true; } else { - // Replace selection - for (var k in this.selectedIds) delete this.selectedIds[k]; - this.selectedIds[node.id] = true; + // Notion-style: toggle this item, keep the rest of the selection + if (!!this.selectedIds[node.id]) delete this.selectedIds[node.id]; + else this.selectedIds[node.id] = true; } this.lastSelectedId = node.id; // Force reactivity - this.selectedIds = Object.assign({}, this.selectedIds); this.selectedIdsCount = Object.keys(this.selectedIds).length; + this.selectedIds = Object.assign({}, this.selectedIds); + this._syncSelection(); }, _collectVisibleIds(nodes) { var ids = []; @@ -755,16 +780,19 @@ window._wsInitData = (function() { clearSelection() { for (var k in this.selectedIds) delete this.selectedIds[k]; this.selectedIds = {}; - this.selectedIdsCount = 0; + this.moreOpen = false; + this._syncSelection(); }, selectAll(ev) { - if (ev.target.checked) { + // Toggle: select / unselect all currently visible items + if (this.allSelected) { + for (var k in this.selectedIds) delete this.selectedIds[k]; + } else { var all = this._collectVisibleIds(this.displayTree); for (var i=0; i 0 && this.selectedIdsCount >= total; + this._syncCheckboxes(); + }, + _syncCheckboxes() { + var boxes = document.querySelectorAll('input.item-checkbox[data-ws-id]'); + for (var i = 0; i < boxes.length; i++) { + boxes[i].checked = !!this.selectedIds[parseInt(boxes[i].getAttribute('data-ws-id'))]; + } + }, + + // ── Multi-select actions (Library-style select bar) ── + _selectedIds() { + return Object.keys(this.selectedIds).map(Number); + }, + copySelectedLinks() { + this.moreOpen = false; + var ids = Object.keys(this.selectedIds); + if (!ids.length) return; + var links = ids.map(function(id) { return window.location.origin + '/pages/' + id; }); + navigator.clipboard.writeText(links.join('\n')); + if (window.showToast) window.showToast('Links copied', 'success'); + }, + async openMovePicker() { + var ids = this._selectedIds(); + if (!ids.length) return; + this.moreOpen = false; + this.moveIds = ids; + this.moveSearchQuery = ''; + this.moveModalOpen = true; + try { + // Fetch the full workspace tree so every folder is a valid destination + var r = await fetch('/api/local-workspace/tree'); + var d = await r.json(); + var folders = []; + this._collectFolders(d.tree || [], folders, 0); + this._moveAllCandidates = folders; + this.moveCandidates = folders.slice(0, 50); + } catch(e) { + this._moveAllCandidates = []; + this.moveCandidates = []; + } + }, + _collectFolders(nodes, out, depth) { + for (var i = 0; i < nodes.length; i++) { + var n = nodes[i]; + if (n.is_folder) out.push({ id: n.id, name: n.name, depth: depth }); + if (n.children) this._collectFolders(n.children, out, depth + 1); + } + }, + doMoveSearch() { + var q = (this.moveSearchQuery || '').toLowerCase().trim(); + if (!q) { this.moveCandidates = this._moveAllCandidates.slice(0, 50); return; } + this.moveCandidates = this._moveAllCandidates.filter(function(it) { + return (it.name || '').toLowerCase().indexOf(q) !== -1; + }).slice(0, 50); + }, + async confirmBulkMove(parentId) { + this.moveModalOpen = false; + var ids = this.moveIds; + this.moveIds = []; + if (!ids || !ids.length) return; + for (var i = 0; i < ids.length; i++) { + try { + await fetch('/api/local-workspace/items/' + ids[i] + '/move', { + method: 'PUT', + headers: {'Content-Type':'application/json'}, + body: JSON.stringify({ parent_id: parentId || null }) + }); + } catch(e) {} + } + this.clearSelection(); + this._reloadAfterAction(); + if (window.showToast) window.showToast('Item' + (ids.length > 1 ? 's' : '') + ' moved', 'success'); } }; @@ -2470,14 +2583,38 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
- +
- - + + +
+ +
+ + +
+ +
+
@@ -2494,6 +2631,15 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
+ +
+ + + +
+
  • @@ -2615,7 +2761,7 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind - + @@ -2820,6 +2966,25 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind {{ fd_icon('alert-triangle',14) }} You are offline. Changes will sync when connection is restored. + + +
    Name Path Size