';
@@ -376,13 +601,9 @@ function libraryPage() {
html += '
' + (item.source_label || 'Private') + '
';
// Edited
html += '
' + self.formatTime(item.updated_at) + '
';
- // Visited
- html += '
' + self.formatTime(item.visited_at) + '
';
html += '
';
}
body.innerHTML = html;
- console.log('[Library] _renderTable: done, body has ' + body.children.length + ' children');
- if (table) console.log('[Library] table visible: offsetHeight=' + table.offsetHeight + ' display=' + table.style.display);
},
_escHtml(s) { var d=document.createElement('div'); d.textContent = s||''; return d.innerHTML; },
@@ -438,6 +659,10 @@ function libraryPage() {
if (!item) return;
this.openPeek(item);
},
+ cancelRename() {
+ this.renamingId = null;
+ this._renderTable();
+ },
_findItem(id) {
for (var i = 0; i < this.flatItems.length; i++) {
if (this.flatItems[i].id === id) return this.flatItems[i];
@@ -445,6 +670,60 @@ function libraryPage() {
return null;
},
+ // ── Row click / context menu ──
+ onRowClick(evt, id) {
+ var item = this._findItem(id);
+ if (!item) return;
+ this.openItem(item);
+ },
+ onRowContextMenu(evt, id) {
+ evt.preventDefault();
+ var item = this._findItem(id);
+ if (!item) return;
+ this.ctxMenuItem = item;
+ this.ctxMenuX = Math.min(evt.clientX, window.innerWidth - 220);
+ this.ctxMenuY = Math.min(evt.clientY, window.innerHeight - 320);
+ },
+
+ // ── Drag & drop (move / reparent) ──
+ onRowDragStart(evt, id) {
+ if (!evt.target.closest('.drag-handle')) { evt.preventDefault(); return; }
+ this._dragId = id;
+ evt.dataTransfer.effectAllowed = 'move';
+ evt.dataTransfer.setData('text/plain', String(id));
+ evt.currentTarget.classList.add('dragging');
+ },
+ onRowDragOver(evt, id) {
+ if (this._dragId === null || this._dragId === id) return;
+ evt.preventDefault();
+ evt.currentTarget.classList.add('drag-over');
+ },
+ onRowDragLeave(evt, id) {
+ evt.currentTarget.classList.remove('drag-over');
+ },
+ async onRowDrop(evt, id) {
+ evt.preventDefault();
+ evt.currentTarget.classList.remove('drag-over');
+ var draggedId = this._dragId;
+ this._dragId = null;
+ if (!draggedId || draggedId === id) return;
+ var target = this._findItem(id);
+ var dragged = this._findItem(draggedId);
+ if (!target || !dragged) return;
+ // Prevent dropping a page onto one of its own (already-loaded) descendants
+ if (this._isDescendant(dragged, id)) {
+ if (window.showToast) window.showToast('Cannot move a page into its own subpage', 'error');
+ return;
+ }
+ await this._movePages([draggedId], id);
+ if (window.showToast) window.showToast('Page moved', 'success');
+ },
+ onRowDragEnd(evt) {
+ this._dragId = null;
+ document.querySelectorAll('.lib-row.dragging').forEach(function(el){ el.classList.remove('dragging'); });
+ document.querySelectorAll('.lib-row.drag-over').forEach(function(el){ el.classList.remove('drag-over'); });
+ },
+
_flatten(nodes, out, depth) {
if (!nodes) return;
for (var i = 0; i < nodes.length; i++) {
@@ -453,10 +732,46 @@ function libraryPage() {
out.push(n);
if (n.has_children && this.expanded[n.id]) {
this._flatten(n.children, out, depth + 1);
+ if (this._canAddChild()) {
+ out.push({ _isAddNew: true, _parentId: n.id, depth: (depth || 0) + 1, id: 'add-' + n.id });
+ }
}
}
},
+ _canAddChild() {
+ return this.tab !== 'favorites' && this.tab !== 'shared' && this.tab !== 'published' && this.tab !== 'repository';
+ },
+
+ _getCsrf() {
+ var m = document.cookie.match(/csrf_token=([^;]+)/);
+ return m ? m[1] : '';
+ },
+
+ _isDescendant(node, targetId) {
+ if (!node || !node.children) return false;
+ for (var i = 0; i < node.children.length; i++) {
+ if (node.children[i].id === targetId) return true;
+ if (this._isDescendant(node.children[i], targetId)) return true;
+ }
+ return false;
+ },
+
+ async _movePages(ids, parentId) {
+ var csrf = this._getCsrf();
+ for (var i = 0; i < ids.length; i++) {
+ try {
+ await fetch('/board/api/pages/' + ids[i] + '/move', {
+ method: 'PUT',
+ headers: {'Content-Type':'application/json', 'X-CSRF-Token': csrf},
+ body: JSON.stringify({ parent_id: parentId || 0 })
+ });
+ } catch(e) {}
+ }
+ this.clearSelection();
+ this.loadTab();
+ },
+
// ── Empty states ──
emptyIcon: '📄',
emptyTitle: 'No items',
@@ -526,7 +841,6 @@ function libraryPage() {
// ── Load ──
async loadTab() {
this.loading = true;
- console.log('[Library] loadTab: ' + this.tab + ' workspaceId=' + this.workspaceId);
try {
var url = '/api/library/' + this.tab;
var sep = '?';
@@ -536,17 +850,20 @@ function libraryPage() {
if (this.tab === 'repository' && this.isGiteaActive) {
url += sep + 'gitea_owner=' + encodeURIComponent(this.giteaOwner) + '&gitea_repo=' + encodeURIComponent(this.giteaRepo);
sep = '&';
+ } else if (this.tab !== 'repository' && this.tab !== 'recents' && this.tab !== 'favorites') {
+ // Hierarchical tree view: show only root-level pages at the top
+ url += sep + 'tree=1';
+ sep = '&';
+ } else if (this.tab !== 'repository' && this.tab !== 'workspace' && this.sourceFilter && this.sourceFilter !== 'all') {
+ url += sep + 'source_type=' + encodeURIComponent(this.sourceFilter); sep = '&';
}
- console.log('[Library] fetching: ' + url);
var r = await fetch(url);
var d = await r.json();
+ // Backend already computes has_children correctly for each item (children
+ // are loaded lazily on expand) — do NOT overwrite it here.
this.items = d.items || [];
- console.log('[Library] loaded ' + this.items.length + ' items');
- this._markTree(this.items, 0);
this._recomputeFlatItems();
this._renderTable();
- console.log('[Library] flatItems after recompute: ' + this.flatItems.length);
- // Force Alpine to re-render (known issue with x-for in 3.14.9)
var self = this;
this.$nextTick(function() { self.loading = false; });
} catch(e) {
@@ -556,29 +873,43 @@ function libraryPage() {
}
},
- _markTree(nodes, depth) {
- if (!nodes) return;
- for (var i = 0; i < nodes.length; i++) {
- nodes[i].depth = depth;
- if (nodes[i].children && nodes[i].children.length > 0) {
- nodes[i].has_children = true;
- this._markTree(nodes[i].children, depth + 1);
- } else {
- nodes[i].has_children = false;
- }
- }
+ doSearch() { this._recomputeFlatItems(); },
+
+ setSourceFilter(v) {
+ this.sourceFilter = v;
+ this.filterOpen = false;
+ this.loadTab();
+ },
+
+ setSort(field) {
+ this.sortBy = field;
+ this.sortOpen = false;
+ this.doSort();
+ },
+
+ toggleColumn(col) {
+ if (col === 'author') this.showAuthor = !this.showAuthor;
+ else if (col === 'source') this.showSource = !this.showSource;
},
- doSearch() { this._recomputeFlatItems(); },
doSort() {
var s = this.sortBy;
if (!s) return;
- var items = this.items.slice();
- if (s === 'name') items.sort(function(a,b){ return (a.title||'').localeCompare(b.title||''); });
- else if (s === 'edited') items.sort(function(a,b){ return (b.updated_at||'').localeCompare(a.updated_at||''); });
- else if (s === 'visited') items.sort(function(a,b){ return (b.visited_at||'').localeCompare(a.visited_at||''); });
- else if (s === 'author') items.sort(function(a,b){ return (a.author||'').localeCompare(b.author||''); });
- this.items = items;
+ var self = this;
+ function sortNodes(nodes) {
+ if (!nodes) return;
+ nodes.sort(function(a,b){
+ if (s === 'name') return (a.title||'').localeCompare(b.title||'');
+ if (s === 'edited') return (b.updated_at||'').localeCompare(a.updated_at||'');
+ if (s === 'author') return (a.author||'').localeCompare(b.author||'');
+ return 0;
+ });
+ for (var i = 0; i < nodes.length; i++) {
+ if (nodes[i].children && nodes[i].children.length) sortNodes(nodes[i].children);
+ }
+ }
+ sortNodes(this.items);
+ this._recomputeFlatItems();
},
// ── Tree expand ──
@@ -657,23 +988,33 @@ function libraryPage() {
var ids = Object.keys(this.selected);
if (!ids.length) return;
if (!confirm('Move ' + ids.length + ' item(s) to Trash?')) return;
+ var csrf = this._getCsrf();
for (var i = 0; i < ids.length; i++) {
- await fetch('/api/pages/' + ids[i] + '/trash', {method:'POST'});
+ await fetch('/board/api/pages/' + ids[i], {method:'DELETE', headers:{'X-CSRF-Token': csrf}});
}
this.clearSelection();
this.loadTab();
if (window.showToast) window.showToast('Moved to trash', 'success');
},
+ async deleteItem(item) {
+ if (!item) return;
+ if (!confirm('Move "' + item.title + '" to Trash?')) return;
+ var csrf = this._getCsrf();
+ await fetch('/board/api/pages/' + item.id, {method:'DELETE', headers:{'X-CSRF-Token': csrf}});
+ this.loadTab();
+ if (window.showToast) window.showToast('Moved to trash', 'success');
+ },
+
removeFromRecents() {
+ // No server-side "visited" tracking exists yet — remove from the
+ // current view only (session-scoped).
this.moreOpen = false;
- var ids = Object.keys(this.selected);
+ var ids = Object.keys(this.selected).map(Number);
if (!ids.length) return;
- ids.forEach(function(id) {
- fetch('/api/pages/' + id + '/visit', {method:'DELETE'}).catch(function(){});
- });
+ this.items = this.items.filter(function(it) { return ids.indexOf(it.id) === -1; });
this.clearSelection();
- if (this.tab === 'recents') this.loadTab();
+ this._recomputeFlatItems();
},
copyLinks() {
@@ -684,9 +1025,86 @@ function libraryPage() {
if (window.showToast) window.showToast('Links copied', 'success');
},
- moveTo() {
+ copyLink(item) {
+ if (!item) return;
+ navigator.clipboard.writeText(window.location.origin + '/pages/' + item.id);
+ if (window.showToast) window.showToast('Link copied', 'success');
+ },
+
+ async toggleFavoriteItem(item) {
+ if (!item) return;
+ var csrf = this._getCsrf();
+ var method = item.favorited ? 'DELETE' : 'POST';
+ var r = await fetch('/board/api/favorites/' + item.id, { method: method, headers: {'X-CSRF-Token': csrf} });
+ if (r.ok) {
+ item.favorited = !item.favorited;
+ this._renderTable();
+ if (window.appState && window.appState.refreshFavorites) window.appState.refreshFavorites();
+ if (this.tab === 'favorites') this.loadTab();
+ }
+ },
+
+ async duplicateItem(item) {
+ if (!item) return;
+ try {
+ var r = await fetch('/board/api/pages/' + item.id);
+ var page = await r.json();
+ var csrf = this._getCsrf();
+ var section = page.parent_section || 'Private';
+ var parentId = page.parent_id || 0;
+ var ws = page.workspace || '';
+ var createUrl = '/board/api/pages?title=' + encodeURIComponent((page.title || 'Untitled') + ' (Copy)')
+ + '§ion=' + encodeURIComponent(section) + '&project=' + encodeURIComponent(ws)
+ + '&parent_id=' + parentId;
+ var cr = await fetch(createUrl, { method: 'POST', headers: {'X-CSRF-Token': csrf} });
+ var created = await cr.json();
+ if (page.content) {
+ var updUrl = '/board/api/pages/' + created.id + '?content=' + encodeURIComponent(page.content)
+ + '&content_format=' + encodeURIComponent(page.content_format || '');
+ await fetch(updUrl, { method: 'PUT', headers: {'X-CSRF-Token': csrf} });
+ }
+ this.loadTab();
+ if (window.showToast) window.showToast('Page duplicated', 'success');
+ } catch(e) {
+ if (window.showToast) window.showToast('Failed to duplicate page', 'error');
+ }
+ },
+
+ // ── Move to (picker modal) ──
+ async openMovePicker(ids) {
this.moreOpen = false;
- if (window.showToast) window.showToast('Move not yet implemented', 'info');
+ this.peekMoreOpen = false;
+ if (!ids || !ids.length) return;
+ this.moveIds = ids;
+ this.moveSearchQuery = '';
+ this.moveModalOpen = true;
+ try {
+ var r = await fetch('/api/library/recents?workspace_id=' + (this.workspaceId || 0));
+ var d = await r.json();
+ var all = (d.items || []).filter(function(it) { return ids.indexOf(it.id) === -1; });
+ this._moveAllCandidates = all;
+ this.moveCandidates = all.slice(0, 30);
+ } catch(e) {
+ this._moveAllCandidates = [];
+ this.moveCandidates = [];
+ }
+ },
+
+ doMoveSearch() {
+ var q = (this.moveSearchQuery || '').toLowerCase().trim();
+ if (!q) { this.moveCandidates = this._moveAllCandidates.slice(0, 30); return; }
+ this.moveCandidates = this._moveAllCandidates.filter(function(it) {
+ return (it.title || '').toLowerCase().indexOf(q) !== -1;
+ }).slice(0, 30);
+ },
+
+ async confirmMove(parentId) {
+ this.moveModalOpen = false;
+ var ids = this.moveIds;
+ this.moveIds = [];
+ if (!ids || !ids.length) return;
+ await this._movePages(ids, parentId);
+ if (window.showToast) window.showToast('Page moved', 'success');
},
// ── Item actions ──
@@ -711,9 +1129,9 @@ function libraryPage() {
this.renamingId = null;
if (!newName || newName === item.title) { this._renderTable(); return; }
try {
- var r = await fetch('/api/pages/' + item.id + '/rename', {
- method:'PUT', headers:{'Content-Type':'application/json'},
- body: JSON.stringify({title: newName})
+ var csrf = this._getCsrf();
+ var r = await fetch('/board/api/pages/' + item.id + '?title=' + encodeURIComponent(newName), {
+ method: 'PUT', headers: {'X-CSRF-Token': csrf}
});
if (r.ok) {
item.title = newName;
@@ -723,18 +1141,33 @@ function libraryPage() {
},
// ── Add new ──
- async addNewPage() {
+ async addNewPage(parentId) {
var name = prompt('Page name:');
if (!name || !name.trim()) return;
- var csrf = document.cookie.match(/csrf_token=([^;]+)/);
+ var csrf = this._getCsrf();
+ var section = 'Private';
try {
- var r = await fetch('/board/api/pages?section=Private', {
- method:'POST',
- headers:{'X-CSRF-Token':csrf?csrf[1]:'', 'Content-Type':'application/json'},
- body: JSON.stringify({title: name.trim()})
- });
+ var url = '/board/api/pages?title=' + encodeURIComponent(name.trim())
+ + '§ion=' + encodeURIComponent(section)
+ + '&parent_id=' + (parentId || 0);
+ var r = await fetch(url, { method: 'POST', headers: {'X-CSRF-Token': csrf} });
if (r.ok) {
var d = await r.json();
+ if (parentId) {
+ // Refresh the parent's children so the new page appears inline
+ var parent = this._findItem(parentId);
+ if (parent) {
+ this.expanded[parentId] = true;
+ try {
+ var cr = await fetch('/api/library/children/' + parentId);
+ var cd = await cr.json();
+ parent.children = cd.items || [];
+ } catch(e) {}
+ this._recomputeFlatItems();
+ this._renderTable();
+ return;
+ }
+ }
window.location.href = '/pages/' + d.id;
}
} catch(e) {
@@ -751,26 +1184,26 @@ function libraryPage() {
this.peekLoading = true;
this.peekFavorited = item.favorited || false;
try {
- var r = await fetch('/api/pages/' + item.id + '/content');
+ var r = await fetch('/board/api/pages/' + item.id);
var d = await r.json();
var content = d.content || '';
// Simple preview
- if (d.format === 'blocks') {
+ if (d.content_format === 'blocks') {
try {
var blocks = typeof content === 'string' ? JSON.parse(content) : content;
var preview = '';
- var lines = (blocks && blocks.blocks) ? blocks.blocks.slice(0, 30) : [];
+ var lines = Array.isArray(blocks) ? blocks.slice(0, 30) : ((blocks && blocks.blocks) ? blocks.blocks.slice(0, 30) : []);
for (var i = 0; i < lines.length; i++) {
var b = lines[i];
var txt = b.content || b.text || (typeof b === 'string' ? b : '');
if (txt && txt.trim()) preview += '
' + this._escHtml(String(txt)) + '
';
}
- this.peekContent = preview || '
(empty page)
';
+ this.peekContent = preview || '
(empty page)
';
} catch(e) {
- this.peekContent = '
' + this._escHtml(String(content).substring(0, 2000)) + '
';
+ this.peekContent = '
' + this._escHtml(String(content).substring(0, 2000)) + '
';
}
} else {
- this.peekContent = '
' + this._escHtml(String(content).substring(0, 2000)) + '
';
+ this.peekContent = content ? '
' + this._escHtml(String(content).substring(0, 2000)) + '
' : '';
}
} catch(e) {
this.peekContent = '';
@@ -783,22 +1216,17 @@ function libraryPage() {
this.peekContent = '';
},
- _escHtml(s) {
- var div = document.createElement('div');
- div.textContent = s;
- return div.innerHTML;
- },
-
async toggleFavoritePeek() {
if (!this.peekItem) return;
var id = this.peekItem.id;
- var csrf = document.cookie.match(/csrf_token=([^;]+)/);
+ var csrf = this._getCsrf();
var method = this.peekFavorited ? 'DELETE' : 'POST';
var r = await fetch('/board/api/favorites/' + id, {
- method: method, headers: {'X-CSRF-Token':csrf?csrf[1]:''}
+ method: method, headers: {'X-CSRF-Token': csrf}
});
if (r.ok) {
this.peekFavorited = !this.peekFavorited;
+ this.peekItem.favorited = this.peekFavorited;
if (window.appState && window.appState.refreshFavorites) window.appState.refreshFavorites();
}
},
@@ -811,19 +1239,22 @@ function libraryPage() {
duplicatePeek() {
this.peekMoreOpen = false;
- if (window.showToast) window.showToast('Duplicate not yet implemented', 'info');
+ if (!this.peekItem) return;
+ this.duplicateItem(this.peekItem);
},
movePeekTo() {
this.peekMoreOpen = false;
- if (window.showToast) window.showToast('Move not yet implemented', 'info');
+ if (!this.peekItem) return;
+ this.openMovePicker([this.peekItem.id]);
},
async deletePeek() {
this.peekMoreOpen = false;
if (!this.peekItem) return;
if (!confirm('Move to Trash?')) return;
- await fetch('/api/pages/' + this.peekItem.id + '/trash', {method:'POST'});
+ var csrf = this._getCsrf();
+ await fetch('/board/api/pages/' + this.peekItem.id, {method:'DELETE', headers:{'X-CSRF-Token': csrf}});
this.closePeek();
this.loadTab();
if (window.showToast) window.showToast('Moved to trash', 'success');
diff --git a/flowdeck.db b/flowdeck.db
index 5a0d357..65b45ae 100644
Binary files a/flowdeck.db and b/flowdeck.db differ
diff --git a/flowdeck_dev.db b/flowdeck_dev.db
new file mode 100644
index 0000000..8ae8600
Binary files /dev/null and b/flowdeck_dev.db differ