Files
flowdeck/static/js/library.js
T
bruno 8d0d69e7b8
FlowDeck CI / test (push) Failing after 3h9m51s
FlowDeck CI / lint (push) Successful in 1m54s
FlowDeck CI / docker (push) Skipped
fix: A27 lint terminé — eslint 0/0 (285 warnings nettoyés) (v7.25.0)
3 familles, 13 fichiers (+153/−167) :

1. no-empty ×70 = TOUS des `catch (x) {}` vides → `catch { /* volontaire */ }`
   (binding optionnel ES2019 + commentaire : passe no-empty ET
   no-unused-vars, zéro changement de comportement).

2. no-unused-vars ×171 :
   - bindings de catch inutilisés retirés (e/err/ex/e2/e3)
   - 24 lignes mortes déterministes, chaque suppression validée par assert
     sur le texte exact (`var self = this` ×8, `var lang`, `var acc`,
     `var today`, `var path/restored/files/resolved/items/clickEl`,
     `uid()`/`propName()` sans un seul appel, `.then` + `resolved++`
     compteurs jamais lus)
   - `/* exported */` sur les 10 fonctions appelées depuis les attributs
     HTML des templates (vérifiées par grep : 1 template chacune) :
     setActiveTab/kanbanBoard/filterSystem/sortSystem/newIssueForm/
     showNewIssue, importWizard, libraryPage, workspacesPage, settingsInit

3. no-undef ×44 = vrais globaux déclarés dans eslint.config.mjs
   (getSvgIcon = script inline de base.html, TextDecoder = API navigateur,
   Prism = CDN) + 2 vrais correctifs :
   - settings.js : `typeof toast === 'function'` = guard TOUJOURS faux
     (pas de toast global) → les toasts timezone/SAML ne s'affichaient
     jamais → `window.showToast` (2 sites)
   - local_workspace.js : `_wsInitData = window._wsInitData`
     (auto-affectation sans effet, global implicite) supprimé

eslint static/js : **0 erreur / 0 warning** (285 → 0) · node --check vert
sur tous les fichiers · suite **1089/1089** · ruff OK · docs à jour
2026-10-01 21:30:37 -04:00

1040 lines
39 KiB
JavaScript

/* exported libraryPage -- appeles depuis les attributs HTML des templates */
function libraryPage() {
return {
tab: 'recents',
workspaceId: 0,
isGiteaActive: false,
giteaOwner: '',
giteaRepo: '',
items: [],
loading: false,
searchOpen: false,
searchQuery: '',
// Selection
selected: {},
selectedCount: 0,
allSelected: false,
// Tree
expanded: {},
renamingId: null,
renameValue: '',
// Toolbar dropdowns
filterOpen: false,
sortOpen: false,
viewOpen: false,
sourceFilter: 'all',
shareDir: 'all',
showAuthor: false,
showSource: false,
// Tags
workspaceTags: [],
tagFilter: '',
// Sort
sortBy: '',
// More menu
moreOpen: false,
// Move-to modal
moveModalOpen: false,
moveIds: [],
moveSearchQuery: '',
// No workspace guard
noWorkspace: false,
moveCandidates: [],
_moveAllCandidates: [],
// Drag & drop
_dragId: null,
// Side peek
peekItem: null,
peekContent: '',
peekLoading: false,
peekFavorited: false,
peekMoreOpen: false,
// ── Computed flat list ──
flatItems: [],
_renderTable() {
var self = this;
var body = document.getElementById('lib-table-body');
var table = document.getElementById('lib-table');
var emptyDiv = document.getElementById('lib-empty');
var loadingDiv = document.getElementById('lib-loading');
if (!body) return;
// Show/hide table vs empty state
if (this.flatItems.length === 0) {
if (table) table.style.display = 'none';
if (emptyDiv) emptyDiv.style.display = '';
return;
}
if (table) table.style.display = '';
if (emptyDiv) emptyDiv.style.display = 'none';
if (loadingDiv) loadingDiv.style.display = 'none';
var html = '';
for (var i = 0; i < this.flatItems.length; i++) {
var item = this.flatItems[i];
// ── Synthetic "+ Add new" placeholder row for an expanded parent ──
if (item._isAddNew) {
var pad = 30 + (item.depth || 0) * 20;
html += '<div class="add-new-under" style="padding-left:' + pad + 'px" onclick="window.FlowDeck.createPage()">';
html += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> Add new</div>';
continue;
}
var selClass = this.selected[item.id] ? ' selected' : '';
var indentPx = 4 + (item.depth || 0) * 20;
var checkClass = this.selected[item.id] ? ' checked' : '';
var chevOpen = this.expanded[item.id] ? ' open' : '';
var isRenaming = this.renamingId === item.id;
html += '<div class="lib-row' + selClass + '" data-lib-id="' + item.id + '" draggable="true" '
+ 'ondragstart="window._libData.onRowDragStart(event,' + item.id + ')" '
+ 'ondragover="window._libData.onRowDragOver(event,' + item.id + ')" '
+ 'ondragleave="window._libData.onRowDragLeave(event,' + item.id + ')" '
+ 'ondrop="window._libData.onRowDrop(event,' + item.id + ')" '
+ 'ondragend="window._libData.onRowDragEnd(event)" '
+ 'oncontextmenu="window._libData.onRowContextMenu(event,' + item.id + ')" '
+ 'onclick="window._libData.onRowClick(event,' + item.id + ')">';
html += '<div class="row-cell lib-col-name" style="padding-left:' + indentPx + 'px"><div class="row-name">';
// Drag handle
html += '<div class="drag-handle hover-only" title="Drag to move">';
html += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="6" r="1.5" fill="currentColor" stroke="none"/><circle cx="15" cy="6" r="1.5" fill="currentColor" stroke="none"/><circle cx="9" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="15" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="9" cy="18" r="1.5" fill="currentColor" stroke="none"/><circle cx="15" cy="18" r="1.5" fill="currentColor" stroke="none"/></svg></div>';
// Checkbox
html += '<div class="lib-checkbox hover-only' + checkClass + '" onclick="event.stopPropagation();window._libData.toggleItemById(' + item.id + ')">';
if (this.selected[item.id]) html += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg>';
html += '</div>';
// Chevron or spacer
if (item.has_children) {
html += '<button class="tree-chevron' + chevOpen + '" onclick="event.stopPropagation();window._libData.toggleExpandById(' + item.id + ')">';
html += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="9 6 15 12 9 18"/></svg></button>';
} else {
html += '<span style="width:18px;flex-shrink:0;"></span>';
}
// Icon — monochrome, comme local-workspace (emoji custom si défini)
var isFolder = item.is_folder || item.has_children;
html += '<span class="row-icon">' + self._renderIcon(item) + '</span>';
// Title or rename input
if (isRenaming) {
html += '<input id="rename-inp-' + item.id + '" value="' + self._escAttr(self.renameValue) + '" onkeydown="if(event.key===\'Enter\')window._libData.commitRenameById(' + item.id + ');if(event.key===\'Escape\')window._libData.cancelRename();" onblur="window._libData.commitRenameById(' + item.id + ')" style="flex:1;min-width:80px;font-size:13px;padding:2px 4px;background:var(--bg-primary);border:1px solid var(--accent);border-radius:4px;color:var(--text-primary);outline:none;" onclick="event.stopPropagation()">';
} else {
html += '<span class="row-title" title="' + self._escAttr(item.title) + '">' + self._escHtml(item.title) + '</span>';
}
// Tags
if (item.tags && item.tags.length) {
html += '<span class="row-tags">';
for (var ti = 0; ti < item.tags.length; ti++) {
var tg = item.tags[ti];
html += '<span class="tag-chip" style="background:' + self._escAttr(tg.color || '#787774') + '" title="' + self._escAttr(tg.name) + '">' + self._escHtml(tg.name) + '</span>';
}
html += '</span>';
}
// OPEN button (side peek) — hidden for folders
if (!isFolder) {
html += '<button class="btn-open hover-only" onclick="event.stopPropagation();window._libData.openPeekById(' + item.id + ')" title="Open in side peek">';
html += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="15" y1="3" x2="15" y2="21"/></svg> Open</button>';
}
html += '</div></div>';
// Author
html += '<div class="row-cell lib-col-author"><div class="row-author">';
html += '<div class="author-avatar">' + (item.author_initial || '?') + '</div>';
html += '<span>' + (item.author || '\u2014') + '</span></div></div>';
// Source
html += '<div class="row-cell lib-col-source"><span class="row-source">' + (item.source_label || 'Private') + '</span></div>';
// Edited
html += '<div class="row-cell lib-col-edited"><span class="row-time">' + self.formatTime(item.updated_at) + '</span></div>';
html += '</div>';
}
body.innerHTML = html;
},
_escHtml(s) { var d=document.createElement('div'); d.textContent = s||''; return d.innerHTML; },
_escAttr(s) { return String(s||'').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); },
_renderIcon(item) {
if (!item) return getSvgIcon('file', 14);
// Custom emoji (image URL), icon name, or unicode emoji
if (item.page_icon) return '<span style="font-size:14px;line-height:1;display:inline-flex;align-items:center;">' + (window.fdIconHtml ? window.fdIconHtml(item.page_icon, 14) : this._escHtml(item.page_icon)) + '</span>';
if (item.is_folder || item.has_children) return getSvgIcon('folder', 14);
if (item.content_format && item.content_format !== 'file') return getSvgIcon('edit', 14);
var n = (item.title || '').toLowerCase();
if (/\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(n)) return getSvgIcon('image', 14);
return getSvgIcon('file', 14);
},
_recomputeFlatItems() {
var result = [];
this._flatten(this.items, result, 0);
if (this.searchQuery.trim()) {
var q = this.searchQuery.toLowerCase();
result = result.filter(function(r) {
return (r.title || '').toLowerCase().indexOf(q) !== -1;
});
}
if (this.tagFilter) {
var tf = this.tagFilter;
result = result.filter(function(r) {
return (r.tags || []).some(function(t) { return t.name === tf; });
});
}
// Use splice for Alpine 3.14.9 reactivity (array reference reassign fails)
this.flatItems.splice(0, this.flatItems.length, ...result);
this.selectedCount = Object.keys(this.selected).length;
this.allSelected = this.flatItems.length > 0 && this.selectedCount === this.flatItems.length;
this._renderTable();
},
// ── Global dispatch (bypasses Alpine x-for reactivity issues) ──
toggleItemById(id) {
var item = this._findItem(id);
if (!item) return;
this.toggleItem(item);
},
toggleExpandById(id) {
var item = this._findItem(id);
if (!item) return;
this.toggleExpand(item);
},
startRenameById(id) {
var item = this._findItem(id);
if (!item) return;
this.startRename(item);
},
commitRenameById(id) {
var item = this._findItem(id);
if (!item) { this.renamingId = null; this._renderTable(); return; }
// Read value from the input
var inp = document.getElementById('rename-inp-' + id);
if (inp) this.renameValue = inp.value;
this.commitRename(item);
},
openItemById(id) {
var item = this._findItem(id);
if (!item) return;
this.openItem(item);
},
openPeekById(id) {
var item = this._findItem(id);
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];
}
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;
if (!(window.Alpine && Alpine.store('fdCtx'))) return;
/* MENU UNIFIÉ (partial _ctx_menu.html + store fdCtx) — handlers library
complète l'union. Open folder/tags non prévus côté library :
fdCtx les cache automatiquement via handlers absents. */
var self = this;
var handlers = {
open: function (n) { self.openItem(n); },
openTab: function (n) { self.openInNewTab(n); },
peek: function (n) { self.openPeek(n); },
rename: function (n) { self.startRename(n); },
setIcon: function (icon) { self.setItemIcon(item, icon); },
duplicate: function (n) { self.duplicateItem(n); },
link: function (n) { self.copyLink(n); },
download: function (n) { self.downloadItem(n); },
copyContent: function (n) { self.copyItemContent(n); },
move: function (n) { self.openMovePicker([n.id]); },
fav: function (n) { self.toggleFavoriteItem(n); },
delete: function (n) { self.deleteItem(n); },
tagExisting: function (t) { self.ctxAddExistingTag(t); },
tagAdd: function (name, color) { self.ctxAddNewTag(name, color); },
tagRemove: function (tagId) { self.ctxRemoveTag(tagId); }
};
if (this.tab === 'recents') handlers.recent = function (n) { self.removeItemFromRecents(n); };
var store = Alpine.store('fdCtx');
store.setAvail(self._ctxAvailTags(item));
store.openMenu(evt, item, 'library', handlers);
},
downloadItem(node) {
if (!node || node.is_folder) return;
var url = '/api/pages/' + node.id + '/download';
var a = document.createElement('a');
a.href = url;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
if (window.showToast) window.showToast('Download started', 'success');
},
async copyItemContent(node) {
if (!node || node.is_folder) return;
try {
var r = await fetch('/api/pages/' + node.id + '/file-content');
var d = await r.json();
if (d.ok && d.content !== undefined) {
await navigator.clipboard.writeText(d.content);
if (window.showToast) window.showToast('Content copied to clipboard', 'success');
} else {
if (window.showToast) window.showToast('Not a text file', 'error');
}
} catch {
if (window.showToast) window.showToast('Copy failed', 'error');
}
},
// ── 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++) {
var n = nodes[i];
n.depth = depth || 0;
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] : '';
},
_syncSidebar() {
if (window.appState && window.appState.refreshSidebarTree) window.appState.refreshSidebarTree();
if (window.appState && window.appState.refreshFavorites) window.appState.refreshFavorites();
},
_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 { /* volontaire */ }
}
this.clearSelection();
this.loadTab();
this._syncSidebar();
},
// ── Empty states ──
emptyIcon: 'file',
emptyTitle: 'No items',
emptyText: '',
_recomputeEmpty() {
var icons = {recents:'clock',favorites:'star',shared:'users',published:'globe',private:'lock',workspace:'folder',repository:'folder'};
this.emptyIcon = icons[this.tab] || 'file';
var titles = {recents:'No recent pages',favorites:'No favorites',shared:'No shared pages',
published:'No published pages',private:'No private pages',workspace:'No workspace pages',
repository:'Empty repository'};
this.emptyTitle = titles[this.tab] || 'No items';
var texts = {recents:'Pages you\'ve recently viewed or edited will appear here.',
favorites:'Right-click a page in the sidebar and select "Add to Favorites".',
shared:'Shared pages from your team will appear here.',
published:'Pages published to the web will appear here.',
private:'Create a new page with the + button.',
workspace:'Pages inside workspaces will appear here.',
repository:'Repository files and pages will appear here.'};
this.emptyText = texts[this.tab] || '';
},
// ── Init ──
init() {
window._libData = this; // expose for onclick handlers
console.log('[Library] init start');
var wsEl = document.getElementById('lib-workspace-id');
this.workspaceId = parseInt(wsEl ? wsEl.textContent : '0') || 0;
var giteaEl = document.getElementById('lib-gitea-active');
this.isGiteaActive = (giteaEl ? giteaEl.textContent : '0') === '1';
this.giteaOwner = (document.getElementById('lib-gitea-owner') || {}).textContent || '';
this.giteaRepo = (document.getElementById('lib-gitea-repo') || {}).textContent || '';
// Detect no workspace: no local wsId AND no Gitea active
if (!this.workspaceId && !this.isGiteaActive) {
this.noWorkspace = true;
this.loading = false; // don't show loading spinner
return;
}
console.log('[Library] workspaceId=' + this.workspaceId + ' isGitea=' + this.isGiteaActive);
var params = new URLSearchParams(window.location.search);
var t = params.get('tab');
if (t && ['recents','favorites','shared','published','private','workspace','repository'].indexOf(t) !== -1) {
this.tab = t;
}
this._recomputeEmpty();
this.loadWorkspaceTags();
console.log('[Library] loading tab: ' + this.tab);
this.loadTab();
},
switchTab(t) {
if (this.tab === t) return;
if (t === 'repository' && !this.isGiteaActive) return;
this.tab = t;
this.shareDir = 'all';
this.tagFilter = '';
this.items = [];
this.selected = {};
this.selectedCount = 0;
this.allSelected = false;
this.expanded = {};
this.searchQuery = '';
this.searchOpen = false;
this._recomputeEmpty();
this.updateUrl();
this.loadTab();
},
updateUrl() {
var p = new URLSearchParams();
if (this.tab !== 'recents') p.set('tab', this.tab);
var qs = p.toString();
window.history.replaceState({}, '', qs ? '/library?' + qs : '/library');
},
// ── Load ──
async loadTab() {
this.loading = true;
try {
var url = '/api/library/' + this.tab;
var sep = '?';
if (this.workspaceId && (this.tab === 'recents' || this.tab === 'workspace')) {
url += sep + 'workspace_id=' + this.workspaceId; sep = '&';
}
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 = '&';
}
if (this.tab === 'shared' && this.shareDir && this.shareDir !== 'all') {
url += sep + 'dir=' + encodeURIComponent(this.shareDir); sep = '&';
}
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 || [];
this._recomputeFlatItems();
this._renderTable();
var self = this;
this.$nextTick(function() { self.loading = false; });
} catch(e) {
console.error('[Library] load error:', e);
this.items = [];
this.loading = false;
}
},
doSearch() { this._recomputeFlatItems(); },
setSourceFilter(v) {
this.sourceFilter = v;
this.filterOpen = false;
this.loadTab();
},
setShareDir(v) {
if (this.shareDir === v) return;
this.shareDir = v;
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;
},
doSort() {
var s = this.sortBy;
if (!s) return;
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 ──
async toggleExpand(item) {
if (this.expanded[item.id]) {
// Collapse
delete this.expanded[item.id];
this.expanded = Object.assign({}, this.expanded);
this._recomputeFlatItems();
this._renderTable();
return;
}
// Expand — load children if not loaded yet
this.expanded[item.id] = true;
this.expanded = Object.assign({}, this.expanded);
if (!item.children || !item.children.length) {
try {
var r = await fetch('/api/library/children/' + item.id);
var d = await r.json();
item.children = d.items || [];
// Mark the tree for depth calculation
this._markChildren(item.children, (item.depth || 0) + 1);
} catch {
item.children = [];
}
}
this._recomputeFlatItems();
this._renderTable();
},
_markChildren(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) {
this._markChildren(nodes[i].children, depth + 1);
}
}
},
// ── Selection ──
toggleItem(item) {
if (this.selected[item.id]) {
delete this.selected[item.id];
} else {
this.selected[item.id] = true;
}
this.selected = Object.assign({}, this.selected);
this._recomputeFlatItems();
this._renderTable();
},
toggleSelectAll() {
if (this.allSelected) {
this.selected = {};
} else {
var s = {};
for (var i = 0; i < this.flatItems.length; i++) {
s[this.flatItems[i].id] = true;
}
this.selected = s;
}
this._recomputeFlatItems();
this._renderTable();
},
clearSelection() {
this.selected = {};
this.moreOpen = false;
this._recomputeFlatItems();
this._renderTable();
},
// ── Multi-select actions ──
async deleteSelected() {
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('/board/api/pages/' + ids[i], {method:'DELETE', headers:{'X-CSRF-Token': csrf}});
}
this.clearSelection();
this.loadTab();
this._syncSidebar();
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();
this._syncSidebar();
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).map(Number);
if (!ids.length) return;
this.items = this.items.filter(function(it) { return ids.indexOf(it.id) === -1; });
this.clearSelection();
this._recomputeFlatItems();
},
copyLinks() {
this.moreOpen = false;
var ids = Object.keys(this.selected);
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');
},
copyLink(item) {
if (!item) return;
navigator.clipboard.writeText(window.location.origin + '/pages/' + item.id);
if (window.showToast) window.showToast('Link copied', 'success');
},
openInNewTab(item) {
if (!item || !item.id) return;
window.open('/pages/' + item.id, '_blank');
},
removeItemFromRecents(item) {
if (!item) return;
var id = item.id;
this.items = this.items.filter(function(it) { return it.id !== id; });
this._recomputeFlatItems();
if (window.showToast) window.showToast('Removed from Recents', 'success');
},
async setItemIcon(item, icon) {
if (!item || !item.id) return;
icon = (icon || '').trim();
try {
var r = await fetch('/board/api/pages/' + item.id + '/icon', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRF-Token': this._getCsrf()},
body: JSON.stringify({icon: icon})
});
if (!r.ok) throw new Error('icon update failed');
item.page_icon = icon;
this._renderTable();
if (window.showToast) window.showToast('Icon updated', 'success');
} catch {
if (window.showToast) window.showToast('Failed to update icon', 'error');
}
},
// ── Tags ──
async loadWorkspaceTags() {
try {
var r = await fetch('/api/settings/tags/all');
var d = await r.json();
this.workspaceTags = d.tags || [];
} catch { this.workspaceTags = []; }
},
toggleTagFilter(tagName) {
this.tagFilter = (this.tagFilter === tagName) ? '' : tagName;
this._recomputeFlatItems();
},
_ctxAvailTags(item) {
return (this.workspaceTags || []).filter(function(t) {
return !((item && item.tags) || []).some(function(nt) { return nt.id === t.id; });
});
},
_ctxRefreshAvail() {
var store = window.Alpine && Alpine.store('fdCtx');
if (store && store.node) store.setAvail(this._ctxAvailTags(store.node));
},
ctxAddExistingTag(tag) {
this.ctxAddNewTag(tag.name, tag.color);
},
ctxRemoveTag(tagId) {
var store = window.Alpine && Alpine.store('fdCtx');
var item = store && store.node;
if (!item) return;
var self = this;
fetch('/api/local-workspace/items/' + item.id + '/tags/' + tagId, {headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||''}, method: 'DELETE' })
.then(function(r) {
if (!r.ok) return;
item.tags = (item.tags || []).filter(function(t) { return t.id !== tagId; });
self.loadWorkspaceTags();
self._ctxRefreshAvail();
self._recomputeFlatItems();
});
},
async ctxAddNewTag(tagName, color) {
tagName = (tagName || '').trim();
if (!tagName) return;
var store = window.Alpine && Alpine.store('fdCtx');
var item = store && store.node;
if (!item) return;
color = color || (store && store.newTagColor) || '#787774';
try {
var r = await fetch('/api/local-workspace/items/' + item.id + '/tags', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||''},
body: JSON.stringify({name: tagName, color: color})
});
if (r.ok) {
var d = await r.json();
var cur = item.tags || [];
if (!cur.some(function(t) { return t.id === d.tag.id; })) item.tags = cur.concat([d.tag]);
await this.loadWorkspaceTags();
this._ctxRefreshAvail();
this._recomputeFlatItems();
}
} catch { /* volontaire */ }
},
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)')
+ '&section=' + 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();
this._syncSidebar();
if (window.showToast) window.showToast('Page duplicated', 'success');
} catch {
if (window.showToast) window.showToast('Failed to duplicate page', 'error');
}
},
// ── Move to (picker modal) ──
async openMovePicker(ids) {
this.moreOpen = false;
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 {
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 ──
openItem(item) {
if (!item || !item.id) return;
if (window.fdNavigate) window.fdNavigate('/pages/' + item.id);
else window.location.href = '/pages/' + item.id;
},
startRename(item) {
this.renamingId = item.id;
this.renameValue = item.title;
this._renderTable();
// Focus the rename input
setTimeout(function() {
var inp = document.getElementById('rename-inp-' + item.id);
if (inp) { inp.focus(); inp.select(); }
}, 50);
},
async commitRename(item) {
var newName = (this.renameValue || '').trim();
this.renamingId = null;
if (!newName || newName === item.title) { this._renderTable(); return; }
try {
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;
}
} catch { /* volontaire */ }
this._renderTable();
this._syncSidebar();
},
// ── Add new ──
async addNewPage(parentId) {
var name = prompt('Page name:');
if (!name || !name.trim()) return;
var csrf = this._getCsrf();
var section = 'Private';
try {
var url = '/board/api/pages?title=' + encodeURIComponent(name.trim())
+ '&section=' + 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 { /* volontaire */ }
this._recomputeFlatItems();
this._renderTable();
return;
}
}
if (window.fdNavigate) window.fdNavigate('/pages/' + d.id);
else window.location.href = '/pages/' + d.id;
}
} catch {
if (window.showToast) window.showToast('Failed to create page', 'error');
}
},
newPage() { this.addNewPage(); },
// ── Side peek ──
async openPeek(item) {
this.peekItem = item;
this.peekFavorited = item.favorited || false;
},
closePeek() {
this.peekItem = null;
},
async toggleFavoritePeek() {
if (!this.peekItem) return;
var id = this.peekItem.id;
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}
});
if (r.ok) {
this.peekFavorited = !this.peekFavorited;
this.peekItem.favorited = this.peekFavorited;
if (window.appState && window.appState.refreshFavorites) window.appState.refreshFavorites();
}
},
copyPeekLink() {
if (!this.peekItem) return;
navigator.clipboard.writeText(window.location.origin + '/pages/' + this.peekItem.id);
if (window.showToast) window.showToast('Link copied', 'success');
},
duplicatePeek() {
this.peekMoreOpen = false;
if (!this.peekItem) return;
this.duplicateItem(this.peekItem);
},
movePeekTo() {
this.peekMoreOpen = false;
if (!this.peekItem) return;
this.openMovePicker([this.peekItem.id]);
},
async deletePeek() {
this.peekMoreOpen = false;
if (!this.peekItem) return;
if (!confirm('Move to Trash?')) return;
var csrf = this._getCsrf();
await fetch('/board/api/pages/' + this.peekItem.id, {method:'DELETE', headers:{'X-CSRF-Token': csrf}});
this.closePeek();
this.loadTab();
this._syncSidebar();
if (window.showToast) window.showToast('Moved to trash', 'success');
},
// ── Time formatting ──
formatTime(d) {
if (!d) return '—';
try {
var dt = new Date(d);
if (isNaN(dt.getTime())) return d;
var now = new Date();
var diff = now - dt;
var mins = Math.floor(diff/60000);
if (mins < 1) return 'Just now';
if (mins < 60) return mins + 'm ago';
var hours = Math.floor(mins/60);
if (hours < 24) return hours + 'h ago';
var days = Math.floor(hours/24);
if (days < 7) return days + 'd ago';
if (days < 30) return Math.floor(days/7) + 'w ago';
return dt.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'});
} catch { return d; }
}
};
}
// Init peek resize handle
(function() {
var panel = document.querySelector('.peek-overlay');
var handle = document.getElementById('lib-peek-resize-handle');
if (!panel || !handle) return;
var startX, startWidth, dragging = false;
handle.addEventListener('pointerdown', function(e) {
e.preventDefault();
handle.setPointerCapture(e.pointerId);
startX = e.clientX;
startWidth = panel.offsetWidth;
dragging = true;
document.body.style.userSelect = 'none';
});
handle.addEventListener('pointermove', function(e) {
if (!dragging) return;
var deltaX = startX - e.clientX;
var newWidth = Math.max(300, Math.min(window.innerWidth * 0.9, startWidth + deltaX));
panel.style.width = newWidth + 'px';
});
handle.addEventListener('pointerup', function(e) {
if (!dragging) return;
document.body.style.userSelect = '';
var moved = Math.abs(e.clientX - startX);
if (moved < 3) {
var el = document.querySelector('.peek-overlay');
if (el && el.__x) { el.__x.$data.peekItem = null; }
}
dragging = false;
try { localStorage.setItem('fd_peek_width', panel.style.width); } catch { /* volontaire */ }
handle.releasePointerCapture(e.pointerId);
});
try {
var saved = localStorage.getItem('fd_peek_width');
if (saved) panel.style.width = saved;
} catch { /* volontaire */ }
})();