Files
flowdeck/static/js/local_workspace.js
T
bruno 45917c194d
FlowDeck CI / lint (push) Successful in 1m55s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 37m14s
fix: A27 phase 2b — +3 801 L extraits (recette config JSON) (v7.23.0)
4 blocs interpolés extraits avec la recette de la 2a (config JSON inline +
JS statique, substitutions sur le CORPS du bloc) :
- local_workspace.html → local_workspace.js (2 031 L, lw-config :
  current_folder_id, workspace_id)
- settings.html → settings.js (1 093 L, st-config : avatar, user
  full_name/login/email, is_admin (bool), auth_method — 2 routes rendent ce
  template, expressions « or "" » préservées pour les valeurs Undefined)
- _page_editor_realtime.html → page_editor_realtime.js (531 L, rt-config :
  SELF id/login/full_name/color)
- board.html → board.js (146 L, bd-config : owner/repo/initial_view)

BONUS sécurité : les valeurs passent par |tojson (échappement JSON explicite)
au lieu d'être interpolées dans des strings JS. Tags : config JSON (nonce
conservé) + <script src> ?v={{ asset_version }} ; loaders JSON.parse en tête
(try/catch → {}). Correctif sur le loader (accolade try en trop, caught par
node --check avant tout commit).

Cumul A27 : 10 560 L extraites (13 904 → 3 344 restantes, -76 %).
Reste structurel : base 1 338 ({% block %}/{% for %}) + database_table 1 323
(if/else) + 279 warnings eslint (12 fichiers, 0 erreur).

suite **1089/1089** · ruff OK · node --check ×4 vert · docs à jour
2026-10-01 20:37:07 -04:00

2033 lines
80 KiB
JavaScript

const LW=(()=>{try{const el=document.getElementById('lw-config');return el?JSON.parse(el.textContent):{}}catch(e){return {}}})();
console.log('FLOWDECK v2.4.6 — IIFE running, about to set _wsInitData');
window._wsInitData = (function() {
return {
tree:[],
displayTree:[],
breadcrumb: [],
loaded:false,
showCreate:false,
showRename:false,
showDelete:false,
showAll:false,
createType:'file',
newName:'',
parentFolder:null,
target:null,
expanded: (function() {
try { return JSON.parse(localStorage.getItem('fd_ws_expanded') || '{}'); }
catch(e) { return {}; }
})(),
currentFolder: LW.current_folder_id,
searchQuery: '',
filterType: '',
filterOpen: false,
editingPath: false,
folderStack: [],
// Inline rename
renamingId: null,
renamingName: '',
renameInputRef: null,
// Preview cache
previewCache: {},
// Clipboard for copy/paste
clipboard: [],
// Sort
sortBy: '',
viewMode: localStorage.getItem('fd_default_view') || 'tree',
viewMenuOpen: false,
sortOpen: false,
searchOpen: false,
viewIcons: {tree:'',list:'',details:'',title:'',content:''},
viewLabels: {tree:'Tree',list:'List',details:'Details',title:'Cards',content:'Content'},
// Context menu — state lives in the shared fdCtx Alpine store (_ctx_menu.html)
// Tags
workspaceTags: [],
tagFilter: '',
tagInputId: null,
tagsVersion: 0,
// Preview panel
previewItem: null,
previewPanelContent: '',
// Multi-select
selectedIds: {},
selectedIdsCount: 0,
lastSelectedId: null,
allSelected: false,
moreOpen: false,
// Move-to modal
moveModalOpen: false,
moveIds: [],
moveSearchQuery: '',
moveCandidates: [],
_moveAllCandidates: [],
// Undo
undoVisible: false,
undoMessage: '',
undoItemId: null,
undoTimeout: null,
// Upload tracking
uploadSpeed: '',
uploadStartTime: 0,
// Path bar
pathValue: '/workspace/',
async navigateToRoot() {
this.folderStack = [];
this.navIndex = 0;
this._saveExpandedState();
await this._loadFolder(0);
},
async navigateToBreadcrumb(idx) {
this.folderStack = this.folderStack.slice(0, idx + 1);
this.navIndex = idx;
this._saveExpandedState();
var entry = this.folderStack[idx];
await this._loadFolder(entry ? entry.id : 0);
},
startPathEdit() {
this.editingPath = true;
var self = this;
this.$nextTick(function() {
var inp = self.$refs.pathEdit;
if (inp) { inp.focus(); inp.select(); }
});
},
// Drag indicators
dragCount: 0,
dragOverParentId: null,
// Online/offline
online: navigator.onLine,
pendingSyncCount: 0,
pinnedIds: (function() {
try { return JSON.parse(localStorage.getItem('fd_ws_pinned') || '[]'); }
catch(e) { return []; }
})(),
// Navigation history
navHistory: [],
navIndex: -1,
canGoBack: false,
canGoForward: false,
// Preview popup
previewVisible: false,
previewContent: '',
previewName: '',
previewX: 0,
previewY: 0,
// Drag & drop state
isDragging: false,
dropTarget: null,
dropTargetName: '',
uploading: false,
uploadStatus: '',
uploadProgress: 0,
draggedItemId: null,
async init() {
try {
// Expose globally for event handlers (BEFORE await)
window._wsData = this;
var url = '/api/local-workspace/tree';
if (this.currentFolder && this.currentFolder > 0) {
url += '?folder=' + this.currentFolder;
}
var r = await fetch(url);
var d = await r.json();
// Keep tree hierarchical, only roots in displayTree
var roots = d.tree || [];
this._markTree(roots, 0);
this.tree = roots;
this.displayTree = roots;
this.breadcrumb = d.breadcrumb || [];
this.loaded = true;
// Initialize folder stack from breadcrumb
this.folderStack = (this.breadcrumb || []).map(function(b) { return {id: b.id, name: b.name}; });
this.navIndex = this.folderStack.length > 0 ? this.folderStack.length - 1 : 0;
// Expose globally for renderChildren onclick handlers
window._wsData = this;
// Listen for online/offline
var self = this;
window.addEventListener('online', function() { self.online = true; });
window.addEventListener('offline', function() { self.online = false; });
// v6.0.0 PWA: reflect offline queue state from the client module
if (window.FlowOffline && window.FlowOffline.onChange) {
window.FlowOffline.onChange(function(s) {
self.online = s.online;
self.pendingSyncCount = s.pendingSyncCount;
});
}
// Side-peek editor posts renames here so the tree/peek title stay in sync.
window.addEventListener('message', function(ev) {
if (ev.origin !== window.location.origin) return;
var m = ev.data;
if (!m || m.type !== 'fd-page-renamed' || m.id == null) return;
var node = self._findNodeById(self.tree, m.id);
if (node) node.name = m.title || 'Untitled';
self.displayTree = self.displayTree.slice();
var t = document.getElementById('ws-preview-title');
if (t && self.previewItem && self.previewItem.id === m.id) t.textContent = m.title || 'Untitled';
var side = document.querySelector('.sidebar-item.ws-tree-item[data-id="' + m.id + '"] .page-name');
if (side) side.textContent = m.title || 'Untitled';
});
// Load workspace tags
this.loadWorkspaceTags();
} catch(e) { console.error('Tree load error:', e); this.loaded = true; }
this._recompute();
this._initResizeHandle();
},
_initResizeHandle() {
var panel = document.getElementById('ws-preview-panel');
var handle = document.getElementById('ws-preview-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) {
window._wsData.closePreviewPanel();
}
dragging = false;
try { localStorage.setItem('fd_peek_width', panel.style.width); } catch(ex) {}
handle.releasePointerCapture(e.pointerId);
});
try {
var saved = localStorage.getItem('fd_peek_width');
if (saved) panel.style.width = saved;
} catch(ex) {}
},
_recompute() {
// Recompute all former getters as plain properties
this.canGoBack = this.navIndex > 0;
this.canGoForward = this.navIndex < this.folderStack.length - 1;
this.pathValue = '/workspace/' + this.folderStack.filter(function(b){return b.id > 0}).map(function(b){return b.name}).join('/');
// flatTree
var ft = [];
this._flattenTree(this.tree, ft);
this.flatTree = ft;
// filteredTableItems
this._computeFilteredTableItems();
this._syncSelection();
},
// ── Pin/Favorite items ──
togglePin(id) {
var idx = this.pinnedIds.indexOf(id);
if (idx === -1) this.pinnedIds.push(id);
else this.pinnedIds.splice(idx, 1);
try { localStorage.setItem('fd_ws_pinned', JSON.stringify(this.pinnedIds)); }
catch(e) {}
this._haptic();
},
isPinned(id) {
return this.pinnedIds.indexOf(id) !== -1;
},
// ── Multi-select ──
toggleSelect(node, ev) {
var e = ev || {};
if (e.shiftKey && this.lastSelectedId) {
// Range select
var allIds = this._collectVisibleIds(this.displayTree);
var start = allIds.indexOf(this.lastSelectedId);
var end = allIds.indexOf(node.id);
if (start !== -1 && end !== -1) {
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 (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 {
// 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._syncSelection();
},
_collectVisibleIds(nodes) {
var ids = [];
for (var i=0; i<nodes.length; i++) {
ids.push(nodes[i].id);
if (nodes[i].children) ids = ids.concat(this._collectVisibleIds(nodes[i].children));
}
return ids;
},
async bulkDelete() {
var ids = Object.keys(this.selectedIds);
if (ids.length === 0) return;
if (!confirm('Delete ' + ids.length + ' selected items?')) return;
var self = this;
// Soft-delete all selected items
for (var i=0; i<ids.length; i++) {
await fetch('/api/local-workspace/items/' + ids[i], {headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||''}, method: 'DELETE' });
}
this.clearSelection();
this._reloadAfterAction();
// Show undo banner for bulk delete
this.undoItemId = ids.join(',');
this.undoMessage = 'Deleted ' + ids.length + ' item(s)';
this.undoVisible = true;
this.undoTimeout = setTimeout(function() {
self.undoVisible = false;
self.undoItemId = null;
}, 5000);
},
clearSelection() {
for (var k in this.selectedIds) delete this.selectedIds[k];
this.selectedIds = {};
this.moreOpen = false;
this._syncSelection();
},
selectAll(ev) {
// 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<all.length; i++) this.selectedIds[all[i]] = true;
}
this.selectedIds = Object.assign({}, this.selectedIds);
this._syncSelection();
},
// ── Table view data ──
flatTree: [],
_flattenTree(nodes, out) {
for (var i=0; i<nodes.length; i++) {
out.push(nodes[i]);
if (nodes[i].children) this._flattenTree(nodes[i].children, out);
}
},
// ── Filtered table items (for enhanced filter/search table) ──
filteredTableItems: [],
_computeFilteredTableItems() {
var self = this;
var q = (this.searchQuery || '').toLowerCase().trim();
var ft = this.filterType || '';
var tf = this.tagFilter || '';
// If no filter active, return empty (show regular views)
if (!q && !ft && !tf) { this.filteredTableItems = []; return; }
// Flatten entire tree
var all = [];
this._flattenTree(this.tree, all);
// Build path for each item
for (var i=0; i<all.length; i++) {
all[i]._path = self._getItemPath(all[i].id);
}
// Filter
this.filteredTableItems = all.filter(function(n) {
var nameMatch = !q || (n.name || '').toLowerCase().indexOf(q) !== -1;
var typeMatch = true;
if (ft) {
switch(ft) {
case 'folder': typeMatch = n.is_folder; break;
case 'pdf': typeMatch = /\.pdf$/.test((n.name||'').toLowerCase()); break;
case 'image': typeMatch = /\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test((n.name||'').toLowerCase()); break;
case 'code': typeMatch = /\.(py|js|jsx|ts|tsx|rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r|sh|bash|ps1|sql)$/.test((n.name||'').toLowerCase()); break;
case 'text': typeMatch = /\.(txt|md|markdown|log)$/.test((n.name||'').toLowerCase()); break;
case 'page': typeMatch = !n.is_folder && !/\.(pdf|png|jpe?g|gif|webp|svg|bmp|ico|py|js|jsx|ts|tsx|rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r|sh|bash|ps1|sql|txt|md|markdown|log)$/.test((n.name||'').toLowerCase()); break;
}
}
var tagMatch = true;
if (tf) {
tagMatch = (n.tags || []).some(function(t) { return t.name === tf; });
}
return nameMatch && typeMatch && tagMatch;
});
},
_getItemPath(id) {
// Build folder path by walking up parent chain via tree
var path = [];
var found = this._findInTree(this.tree, id);
if (!found) return '—';
return '/' + found.path.reverse().join('/');
},
_findInTree(nodes, targetId, currentPath) {
if (!currentPath) currentPath = [];
for (var i=0; i<nodes.length; i++) {
var n = nodes[i];
if (n.id === targetId) {
return {node: n, path: currentPath.concat([n.name])};
}
if (n.children && n.children.length > 0) {
var r = this._findInTree(n.children, targetId, currentPath.concat([n.name]));
if (r) return r;
}
}
return null;
},
// ── Preview panel (loads editor in iframe via /pages/{id}?embed=1) ──
selectForPreview(node) {
var panel = document.getElementById('ws-preview-panel');
if (!panel) return;
this.previewItem = node;
document.getElementById('ws-preview-icon').innerHTML = this._fileIcon(node.name, node.is_folder, node.content_format, node.page_icon);
document.getElementById('ws-preview-title').textContent = node.name;
var openBtn = document.getElementById('ws-preview-open-btn');
openBtn.onclick = function() { if (window.fdNavigate) window.fdNavigate('/pages/' + node.id); else window.location = '/pages/' + node.id; };
var favBtn = document.getElementById('ws-preview-fav-btn');
favBtn.onclick = function() { window.showToast('Favorite toggled', 'success'); };
var copyBtn = document.getElementById('ws-preview-copy-btn');
copyBtn.onclick = function() {
var url = window.location.origin + '/pages/' + node.id;
navigator.clipboard.writeText(url).then(function() {
window.showToast('Link copied', 'success');
}).catch(function() {
window.showToast('Failed to copy', 'error');
});
};
// Load editor in iframe
var iframe = document.getElementById('ws-preview-iframe');
if (iframe) iframe.src = '/pages/' + node.id + '?embed=1';
// Show panel
panel.style.display = 'flex';
requestAnimationFrame(function() {
requestAnimationFrame(function() {
panel.style.transform = 'translateX(0)';
});
});
},
closePreviewPanel() {
var panel = document.getElementById('ws-preview-panel');
if (!panel) return;
panel.style.transform = 'translateX(100%)';
var self = this;
setTimeout(function() {
panel.style.display = 'none';
self.previewItem = null;
}, 200);
},
async _loadPreviewContentVanilla(node) {
var body = document.getElementById('ws-preview-body');
if (!body) return;
if (node.is_folder) {
body.innerHTML = '<div style="text-align:center;padding:40px;color:var(--text-secondary)">'
+ '<span>' + getSvgIcon('folder',32) + '</span>'
+ '<div style="font-size:14px">' + (node.child_count||0) + ' item(s)</div>'
+ '<a href="/local-workspace?folder=' + node.id + '" class="btn btn-secondary" style="display:inline-block;text-decoration:none;margin-top:16px">Open folder</a>'
+ '</div>';
return;
}
try {
var name = (node.name || '').toLowerCase();
// Images: try direct file URL
if (/\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(name)) {
body.innerHTML = '<div style="text-align:center;padding:12px;">'
+ '<img src="/api/files/1/' + encodeURIComponent(node.name) + '" style="max-width:100%;max-height:50vh;border-radius:8px;">'
+ '</div>';
return;
}
// PDF: show icon + open link
if (/\.pdf$/.test(name)) {
body.innerHTML = '<div style="text-align:center;padding:24px;color:var(--text-secondary);">'
+ '<span style="font-size:48px;display:block;margin-bottom:8px;">' + getSvgIcon('file',32) + '</span>'
+ '<div style="font-size:14px;margin-bottom:4px;">' + this._escHtml(node.name) + '</div>'
+ '<div style="font-size:12px;color:var(--text-tertiary);margin-bottom:16px;">' + (node.size_display || '') + '</div>'
+ '<a href="/pages/' + node.id + '" class="btn btn-secondary" style="display:inline-block;text-decoration:none;">Open in viewer</a>'
+ '</div>';
return;
}
// Fetch content from API
var r = await this._fetchRetry('/api/local-workspace/page-content/' + node.id, true);
var content = r.content || '';
var format = r.format || '';
var preview = '';
if (format === 'file') {
// Uploaded file — show metadata
preview = '<div style="text-align:center;padding:30px;color:var(--text-secondary)">'
+ '<div style="font-size:48px;margin-bottom:12px;">' + getSvgIcon('file',32) + '</div>'
+ '<div style="font-size:15px;font-weight:600;margin-bottom:4px;">' + this._escHtml(node.name) + '</div>'
+ '<div style="font-size:13px;color:var(--text-tertiary);margin-bottom:16px;">' + (node.size_display || '') + '</div>'
+ '<a href="/pages/' + node.id + '" class="btn btn-secondary" style="display:inline-block;text-decoration:none;">Open file</a>'
+ '</div>';
} else if (format === 'blocks') {
// FlowDeck internal page — blocks can be array or {blocks:[...]}
try {
var blocks = typeof content === 'string' ? JSON.parse(content) : content;
var blockList = Array.isArray(blocks) ? blocks : (blocks && blocks.blocks ? blocks.blocks : []);
if (blockList.length > 0) {
preview = blockList.slice(0, 50).map(function(b) {
var txt = b.content || b.text || (typeof b === 'string' ? b : '');
var tag = 'div';
var style = 'margin-bottom:6px;line-height:1.5;';
if (b.type && b.type.indexOf('heading') === 0) {
var lvl = parseInt(b.type.split('_')[1]) || 1;
tag = 'h' + Math.min(lvl + 2, 6);
style += 'font-weight:600;';
}
if (b.type === 'code' || b.type === 'code_block') {
tag = 'pre';
style += 'background:var(--bg-tertiary);padding:10px 14px;border-radius:6px;font-family:monospace;font-size:12px;white-space:pre-wrap;';
}
return '<' + tag + ' style="' + style + '">' + (txt ? this._escHtml(txt) : '<br>') + '</' + tag + '>';
}, this).join('');
} else {
preview = '<div style="text-align:center;padding:30px;color:var(--text-tertiary)">Empty document</div>';
}
} catch(e) {
preview = '<pre style="background:var(--bg-tertiary);padding:14px;border-radius:6px;overflow:auto;font-size:12px;font-family:monospace;white-space:pre-wrap;">' + this._escHtml(String(content).substring(0, 3000)) + '</pre>';
}
} else if (format === 'markdown') {
// Markdown — render as preformatted for now
preview = '<div style="white-space:pre-wrap;line-height:1.6;font-size:13px;">' + this._escHtml(String(content).substring(0, 5000)) + '</div>';
} else {
// Unknown format — show as text
var display = String(content || '').trim();
if (display) {
preview = '<pre style="background:var(--bg-tertiary);padding:14px;border-radius:6px;overflow:auto;font-size:12px;font-family:monospace;white-space:pre-wrap;">' + this._escHtml(display.substring(0, 5000)) + '</pre>';
} else {
preview = '<div style="text-align:center;padding:30px;color:var(--text-tertiary)">No content</div>';
}
}
body.innerHTML = preview || '<div style="color:var(--text-tertiary);text-align:center;padding:20px">(empty)</div>';
} catch(e) {
body.innerHTML = '<div style="text-align:center;padding:20px;color:var(--text-tertiary)">Preview not available</div>';
}
},
_escHtml(s) {
return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
},
// ── Content snippet for content view ──
contentSnippetMap: {},
// ── Context menu (menu unifié partagé via le store Alpine fdCtx) ──
onContextMenu(ev) {
var el = ev.target.closest('[data-ws-id]');
if (!el) return;
var id = parseInt(el.getAttribute('data-ws-id'));
if (!id) return;
var node = this._findNodeById(this.displayTree, id) || this._findNodeById(this.tree, id);
if (!node) return;
var store = window.Alpine && Alpine.store('fdCtx');
if (!store) return;
var self = this;
if (!this.workspaceTags || this.workspaceTags.length === 0) this.loadWorkspaceTags();
store.setAvail(this._ctxAvailTags(node));
var handlers = {
open: function (n) { self.openPage(n.id); },
openTab: function (n) { window.open('/pages/' + n.id, '_blank'); },
peek: function (n) { self.openSidePeek(n.id); },
folder: function (n) { if (n.is_folder) self.navigateToFolder(n.id); },
rename: function (n) { self.startRename(n); },
setIcon: function (icon) { self.setItemIcon(node, icon); },
duplicate: function (n) { self.duplicateItem(n); },
link: function (n) { self.copyItemLink(n); },
download: function (n) { self.downloadItem(n); },
copyContent: function (n) { self.copyItemContent(n); },
move: function (n) { self._ctxMove(n); },
fav: function (n) { self.toggleFavorite(n); },
delete: function (n) { self.deleteWithUndo(n); },
tagExisting: function (t) { self.ctxAddExistingTag(t); },
tagAdd: function (name, color) { self.ctxAddNewTag(name, color); },
tagRemove: function (tagId) { self.ctxRemoveTag(tagId); }
};
store.openMenu(ev, node, 'workspace', 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 (e) {
if (window.showToast) window.showToast('Copy failed', 'error');
}
},
_ctxAvailTags(node) {
return (this.workspaceTags || []).filter(function(t) {
return !((node && node.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));
},
copyItemLink(node) {
if (!node) return;
navigator.clipboard.writeText(window.location.origin + '/pages/' + node.id);
if (window.showToast) window.showToast('Link copied', 'success');
},
async toggleFavorite(node) {
if (!node) return;
try {
var method = node.favorited ? 'DELETE' : 'POST';
var r = await fetch('/board/api/favorites/' + node.id, { method: method });
if (r.ok) {
node.favorited = !node.favorited;
if (window.appState && window.appState.refreshFavorites) window.appState.refreshFavorites();
if (window.showToast) window.showToast(node.favorited ? 'Added to Favorites' : 'Removed from Favorites', 'success');
}
} catch(e) {}
},
async setItemIcon(node, icon) {
if (!node || !node.id) return;
icon = (icon || '').trim();
try {
var r = await fetch('/board/api/pages/' + node.id + '/icon', {
method: 'POST',
headers: {'Content-Type': 'application/json',
'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/) || [])[1] || ''},
body: JSON.stringify({icon: icon})
});
if (!r.ok) throw new Error('icon update failed');
node.page_icon = icon;
this.displayTree = [...this.tree];
this._reloadAfterAction();
if (window.showToast) window.showToast('Icon updated', 'success');
} catch(e) {
if (window.showToast) window.showToast('Failed to update icon', 'error');
}
},
_ctxMove(node) {
if (!node) return;
this.selectedIds = {};
this.selectedIds[node.id] = true;
this.openMovePicker();
},
// ── Long-press (mobile context menu) ──
_touchStartX: 0,
_touchStartY: 0,
_touchStartTime: 0,
_touchMoved: false,
_touchTarget: null,
onTouchStart(ev) {
if (!ev.touches || ev.touches.length !== 1) return;
this._touchStartX = ev.touches[0].clientX;
this._touchStartY = ev.touches[0].clientY;
this._touchStartTime = Date.now();
this._touchMoved = false;
this._touchTarget = ev.target;
},
onTouchMove(ev) {
if (!ev.touches || ev.touches.length !== 1) return;
var dx = Math.abs(ev.touches[0].clientX - this._touchStartX);
var dy = Math.abs(ev.touches[0].clientY - this._touchStartY);
if (dx > 10 || dy > 10) this._touchMoved = true;
},
onTouchEnd(ev) {
if (this._touchMoved) return;
var elapsed = Date.now() - this._touchStartTime;
if (elapsed < 600) return;
// Long press detected — find target element
var el = this._touchTarget;
if (!el) return;
var idEl = el.closest('[data-ws-id]');
if (!idEl) return;
// Prevent default context menu if it hasn't fired yet
ev.preventDefault();
var fakeEv = {
clientX: this._touchStartX,
clientY: this._touchStartY,
target: idEl,
preventDefault: function(){}
};
this.onContextMenu(fakeEv);
},
ctxAddExistingTag(tag) {
this.ctxAddNewTag(tag.name, tag.color);
},
ctxRemoveTag(tagId) {
var store = window.Alpine && Alpine.store('fdCtx');
var node = store && store.node;
if (!node) return;
this.removeTag(node.id, tagId);
node.tags = (node.tags || []).filter(function(t) { return t.id !== tagId; });
this._ctxRefreshAvail();
},
async ctxAddNewTag(tagName, color) {
tagName = (tagName || '').trim();
if (!tagName) return;
var store = window.Alpine && Alpine.store('fdCtx');
var node = store && store.node;
if (!node) return;
color = color || (store && store.newTagColor) || '#787774';
try {
var r = await fetch('/api/local-workspace/items/' + node.id + '/tags', {
method: 'POST', headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'', 'Content-Type':'application/json'},
body: JSON.stringify({name: tagName, color: color})
});
if (r.ok) {
var d = await r.json();
var cur = node.tags || [];
if (!cur.some(function(t) { return t.id === d.tag.id; })) node.tags = cur.concat([d.tag]);
this._updateNodeTags(this.tree, node.id, d.tag, 'add');
this.displayTree = [...this.tree];
this.tagsVersion++;
this._bumpTagCount(d.tag, 1);
await this.loadWorkspaceTags();
this._computeFilteredTableItems();
this._ctxRefreshAvail();
}
} catch(e) {}
},
_markTree(nodes, depth) {
for (var i=0; i<nodes.length; i++) {
var n = nodes[i];
n.depth = depth;
n.db_id = n.id;
n._path = '/' + n.name;
if (n.children && n.children.length > 0) {
this._markTree(n.children, depth + 1);
}
}
},
renderChildren(children, depth) {
if (!children || children.length === 0) return '';
var html = '';
for (var i=0; i<children.length; i++) {
var c = children[i];
var hasKids = c.children && c.children.length > 0;
var icon = this._fileIcon(c.name, c.is_folder, c.content_format, c.page_icon);
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="'+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="'+escId+'" onclick="event.stopPropagation();var d=window._wsData;if(d)d.toggleExpand('+escId+',this)">\u25b6</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._wsData.navigateToFolder('+escId+')">'+c.name+countBadge+'</span>';
} else {
// Click to rename, double-click to open
html += '<span class="name file" onclick="event.stopPropagation();window._wsData.startRenderChildRename('+escId+',\''+escName+'\')"'
+ ' ondblclick="window.fdNavigate?window.fdNavigate(\'/pages/'+escId+'\'):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()">'
+ (c.is_shared ? '<span class="ws-shared-badge" title="Partag\u00e9" onclick="event.stopPropagation()">👥</span>' : '')
+ '<span id="rc-name-'+escId+'">'+c.name+'</span>'
+ '<input id="rc-input-'+escId+'" class="inline-rename-input" style="display:none"'
+ ' onkeydown="if(event.key===\'Enter\')window._wsData.commitRenderChildRename('+escId+')"'
+ ' onblur="window._wsData.commitRenderChildRename('+escId+')"'
+ ' onclick="event.stopPropagation()">'
+ '<a href="#" class="btn-open hover-only"'
+ ' onclick="event.stopPropagation();event.preventDefault();var d=window._wsData;if(d)d.openSidePeek('+escId+')"'
+ ' title="Open in side preview">↢ Open</a>'
+ '</span>';
}
// Open button for files — to the right of title (like Library)
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="'+escId+'" data-ws-name="'+escName+'" title="New file">'+getSvgIcon('file',14)+'</button>';
html += '<button class="ws-act" data-ws-action="new-folder" data-ws-id="'+escId+'" data-ws-name="'+escName+'" title="New folder">'+getSvgIcon('folder',14)+'</button>';
html += '<a class="ws-act" href="/local-workspace" title="Open workspace page" style="text-decoration:none;display:inline-flex;align-items:center;">'+getSvgIcon('external-link',14)+'</a>';
html += '</span>';
}
html += '<button class="ws-act" data-ws-action="rename" data-ws-id="'+escId+'" data-ws-name="'+escName+'" title="Rename">'+getSvgIcon('edit',14)+'</button>';
html += '<button class="ws-act danger" data-ws-action="delete" data-ws-id="'+escId+'" data-ws-name="'+escName+'" title="Delete">'+getSvgIcon('trash',14)+'</button>';
// Tags
if (c.tags && c.tags.length > 0) {
html += '<span style="display:flex;gap:2px;margin-left:4px;">';
for (var ti=0; ti<c.tags.length; ti++) {
var t = c.tags[ti];
var tc = this.esc(t.color||'#787774');
var tn = this.esc(t.name);
html += '<span class="tag-chip" style="background:'+tc+'">'+tn+'</span>';
}
html += '</span>';
}
html += '</div></div>';
if (hasKids) {
html += '<ul class="ws-tree" style="display:none">'+this.renderChildren(c.children, depth+1)+'</ul>';
}
html += '</li>';
}
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;
var w = this._fitNameWidth(currentName);
var avail = input.parentElement ? input.parentElement.clientWidth : 0;
if (avail > 60) input.style.width = Math.min(w, avail) + 'px'; else input.style.width = w + 'px';
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: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'', '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,'&quot;');
},
_fitNameWidth(text) {
var off = document.createElement('span');
off.style.cssText = 'position:absolute;visibility:hidden;white-space:pre;font-size:13px;';
off.textContent = (text || '').toString();
document.body.appendChild(off);
var w = off.offsetWidth;
document.body.removeChild(off);
return Math.max(w + 16, 60);
},
// ── Event delegation for renderChildren action buttons ──
handleTreeAction(ev) {
var btn = (ev.target.closest && ev.target.closest('[data-ws-action]'))
|| (ev.target.parentElement && ev.target.parentElement.closest('[data-ws-action]'));
if (!btn) return;
ev.stopPropagation();
var action = btn.getAttribute('data-ws-action');
var id = parseInt(btn.getAttribute('data-ws-id'));
var name = btn.getAttribute('data-ws-name') || '';
if (!id) return;
switch(action) {
case 'new-file':
window.FlowDeck.createPage(id);
break;
case 'new-folder':
window.FlowDeck.showCreateFolderModal(id);
break;
case 'rename':
this.startRename({db_id: id, name: name});
break;
case 'delete':
this.deleteWithUndo({db_id: id, name: name});
break;
}
},
toggleExpand(id, btn) {
this.expanded[id] = !(this.expanded[id] || false);
this.expanded = Object.assign({}, this.expanded);
this._saveExpandedState();
// For renderChildren static HTML: toggle chevron + sibling <ul>
if (btn) {
btn.classList.toggle('open', this.expanded[id]);
// Navigate: btn → .ws-tree-item → <li> → <ul.ws-tree>
var itemDiv = btn.closest('.ws-tree-item');
if (itemDiv && itemDiv.parentElement) {
var children = itemDiv.parentElement.children;
for (var i = 0; i < children.length; i++) {
if (children[i].tagName === 'UL' && children[i].classList.contains('ws-tree')) {
children[i].style.display = this.expanded[id] ? '' : 'none';
break;
}
}
}
}
var self = this;
this.$nextTick(function() { self._syncCheckboxes(); });
},
async navigateToFolder(id) {
// Find the folder name for the stack
var folderNode = this._findNodeById(this.tree, id);
var folderName = folderNode ? folderNode.name : 'Folder';
// Push to folder stack for breadcrumb
if (this.navIndex < this.folderStack.length - 1) {
this.folderStack = this.folderStack.slice(0, this.navIndex + 1);
}
this.folderStack.push({id: id, name: folderName});
this.navIndex = this.folderStack.length - 1;
// Fetch children of this folder
await this._loadFolder(id);
},
async goToParent() {
if (this.folderStack.length <= 1) { await this._loadFolder(0); return; }
this.folderStack.pop();
this.navIndex = this.folderStack.length - 1;
var parentId = this.folderStack.length > 0 ? this.folderStack[this.folderStack.length - 1].id : 0;
await this._loadFolder(parentId);
},
async _loadFolder(folderId) {
var url = '/api/local-workspace/tree';
if (folderId && folderId > 0) url += '?folder=' + folderId;
try {
var r = await fetch(url);
var d = await r.json();
var roots = d.tree || [];
this._markTree(roots, 0);
this.tree = roots;
this.displayTree = [...roots];
this.showAll = false;
this.breadcrumb = d.breadcrumb || [];
this.currentFolder = folderId || 0;
this._saveExpandedState();
this.previewItem = null;
this._recompute();
var self = this;
this.$nextTick(function() { self._syncCheckboxes(); });
} catch(e) { console.error('Load folder error:', e); }
},
_findNodeById(nodes, id) {
for (var i=0; i<nodes.length; i++) {
if (nodes[i].id === id) return nodes[i];
if (nodes[i].children) {
var found = this._findNodeById(nodes[i].children, id);
if (found) return found;
}
}
return null;
},
async goBack() {
if (this.navIndex <= 0) return;
this.navIndex--;
var entry = this.folderStack[this.navIndex];
this._saveExpandedState();
await this._loadFolder(entry ? entry.id : 0);
},
async goForward() {
if (this.navIndex >= this.folderStack.length - 1) return;
this.navIndex++;
var entry = this.folderStack[this.navIndex];
this._saveExpandedState();
await this._loadFolder(entry ? entry.id : 0);
},
_saveExpandedState() {
try { localStorage.setItem('fd_ws_expanded', JSON.stringify(this.expanded)); }
catch(e) {}
},
_reloadAfterAction() {
this._saveExpandedState();
this._loadFolder(this.currentFolder);
// Directly refresh sidebar tree
if (window.appState && window.appState.refreshSidebarTree) {
window.appState.refreshSidebarTree();
}
},
// ── Inline Rename ──
startInlineRename(node, ev) {
this.renamingId = node.id;
this.renamingName = node.name;
var self = this;
this.$nextTick(function() {
var inp = self.$el.querySelector('.inline-rename-input:not([style*="display: none"])');
if (inp) {
var w = self._fitNameWidth(self.renamingName);
var avail = inp.parentElement ? inp.parentElement.clientWidth : 0;
if (avail > 60) inp.style.width = Math.min(w, avail) + 'px'; else inp.style.width = w + 'px';
inp.focus(); inp.select();
}
});
},
async commitRename(node) {
var n = this.renamingName.trim();
this.renamingId = null;
if (!n || n === node.name) return;
var r = await fetch('/api/local-workspace/items/' + node.id, {
method: 'PUT', headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'', 'Content-Type':'application/json'},
body: JSON.stringify({name: n})
});
if (r.ok) {
node.name = n; node.db_name = n;
this._saveExpandedState();
if (window.appState && window.appState.refreshSidebarTree) window.appState.refreshSidebarTree();
if (window.FlowDeck && window.FlowDeck.syncPageTitle) window.FlowDeck.syncPageTitle(node.id, n);
var self = this;
if (this.folderStack && this.folderStack.length) {
this.folderStack.forEach(function(f) { if (String(f.id) === String(node.id)) f.name = n; });
}
if (this.breadcrumb && this.breadcrumb.length) {
this.breadcrumb.forEach(function(b) { if (String(b.id) === String(node.id)) b.name = n; });
}
if (this.previewItem && String(this.previewItem.id) === String(node.id)) {
this.previewItem.name = n;
var pt = document.getElementById('ws-preview-title');
if (pt) pt.textContent = n;
}
}
},
cancelRename() { this.renamingId = null; },
// ── Keyboard shortcuts ──
onKeydown(ev) {
var tag = ev.target && ev.target.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA') return; // Don't hijack typing
if (this.renamingId) return; // Don't intercept during rename
if (ev.key === 'Delete' && !ev.ctrlKey && !ev.metaKey) {
ev.preventDefault();
// Delete selected item — implement when multi-select exists
}
if (ev.key === 'F2' && !ev.ctrlKey) {
ev.preventDefault();
// Find hovered or first file item and rename
var hovered = document.querySelector('.ws-tree-item:hover');
if (hovered) {
var id = parseInt(hovered.getAttribute('data-ws-id') || hovered.getAttribute('data-folder-id'));
var name = (hovered.querySelector('.name') || {}).textContent || '';
if (id) this.startInlineRename({id: id, name: name}, ev);
}
}
if (ev.ctrlKey && ev.key === 'c') {
// Copy selected items to clipboard
ev.preventDefault();
this.clipboard = []; // Will implement with multi-select
}
if (ev.ctrlKey && ev.key === 'v' && this.clipboard.length > 0) {
ev.preventDefault();
// Paste items — move them to current folder
var self = this;
var targetId = this.currentFolder > 0 ? this.currentFolder : null;
this.clipboard.forEach(function(id) {
fetch('/api/local-workspace/items/' + id + '/move', {
method: 'PUT', headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'', 'Content-Type':'application/json'},
body: JSON.stringify({parent_id: targetId})
}).then(function(r) { if (r.ok) self._reloadAfterAction(); });
});
this.clipboard = [];
}
},
// ── Duplicate ──
async duplicateItem(node) {
var r = await fetch('/api/local-workspace/items', {
method: 'POST', headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'', 'Content-Type':'application/json'},
body: JSON.stringify({name: node.name + ' (copy)', type: node.type || 'page',
parent_id: this.currentFolder > 0 ? this.currentFolder : null})
});
if (r.ok) this._reloadAfterAction();
},
// ── Sort ──
doSort() {
var s = this.sortBy;
if (!s) { this.displayTree = [...this.tree]; this.showAll = false; return; }
var sorted = this.tree.slice();
if (s === 'name-asc') sorted.sort(function(a,b) { return (a.name||'').localeCompare(b.name||''); });
else if (s === 'name-desc') sorted.sort(function(a,b) { return (b.name||'').localeCompare(a.name||''); });
else if (s === 'date-asc') sorted.sort(function(a,b) { return (a.updated_at||'').localeCompare(b.updated_at||''); });
else if (s === 'date-desc') sorted.sort(function(a,b) { return (b.updated_at||'').localeCompare(a.updated_at||''); });
else if (s === 'type') sorted.sort(function(a,b) { return (a.is_folder===b.is_folder) ? 0 : a.is_folder ? -1 : 1; });
this.displayTree = sorted;
this._recompute();
},
// ── Undo delete ──
async deleteWithUndo(node) {
var self = this;
this.undoItemId = node.db_id || node.id;
this.undoMessage = 'Deleted "' + (node.name || 'item') + '"';
this.undoVisible = true;
// Delete via API
var r = await fetch('/api/local-workspace/items/' + this.undoItemId, {headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||''}, method:'DELETE' });
if (!r.ok) { this.undoVisible = false; return; }
// Reload from API for reliability (sidebar refresh handled by _reloadAfterAction)
this._reloadAfterAction();
// Auto-dismiss undo banner after 5s
this.undoTimeout = setTimeout(function() {
self.undoVisible = false;
self.undoItemId = null;
}, 5000);
},
undoDelete() {
if (this.undoTimeout) clearTimeout(this.undoTimeout);
this.undoVisible = false;
var ids = (this.undoItemId || '').toString().split(',');
this.undoItemId = null;
if (!ids.length || !ids[0]) return;
// Restore via API (soft-delete undo) — single or bulk
var self = this;
var restored = 0;
var total = ids.length;
function restoreOne(i) {
if (i >= total) {
self._reloadAfterAction();
return;
}
fetch('/api/local-workspace/items/' + ids[i] + '/restore', {headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||''}, method: 'POST' })
.then(function(r) { if (r.ok) restored++; })
.finally(function() { restoreOne(i + 1); });
}
restoreOne(0);
},
_removeFromTree(nodes, id) {
for (var i=0; i<nodes.length; i++) {
if (nodes[i].id === id) { nodes.splice(i, 1); return true; }
if (nodes[i].children && this._removeFromTree(nodes[i].children, id)) {
nodes[i].child_count = nodes[i].children.length;
nodes[i].has_children = nodes[i].child_count > 0;
return true;
}
}
return false;
},
// ── Upload speed tracking ──
_trackUploadSpeed(totalSize) {
var self = this;
this.uploadStartTime = Date.now();
this.uploadSpeed = '';
var checkSpeed = setInterval(function() {
if (!self.uploading) { clearInterval(checkSpeed); return; }
var elapsed = (Date.now() - self.uploadStartTime) / 1000;
if (elapsed > 0) {
self.uploadSpeed = (totalSize / elapsed / 1024).toFixed(0) + ' KB/s';
}
}, 500);
},
// ── Path bar navigation ──
navigateToPath() {
var path = (this.$el.querySelector('.path-input') || {}).value || '';
var parts = path.replace(/^\/workspace\/?/, '').split('/').filter(Boolean);
if (parts.length === 0) { if (window.fdNavigate) window.fdNavigate('/local-workspace'); else window.location = '/local-workspace'; return; }
// Walk tree to find matching folder
var targetId = this._findFolderByPath(this.tree, parts, 0);
if (targetId) {
if (window.fdNavigate) window.fdNavigate('/local-workspace?folder=' + targetId);
else window.location = '/local-workspace?folder=' + targetId;
} else {
window.showToast('Path not found: ' + parts.join('/'), 'error');
}
},
_findFolderByPath(nodes, parts, idx) {
if (idx >= parts.length) return null;
for (var i=0; i<nodes.length; i++) {
if (nodes[i].is_folder && nodes[i].name === parts[idx]) {
if (idx === parts.length - 1) return nodes[i].id;
if (nodes[i].children) {
var found = this._findFolderByPath(nodes[i].children, parts, idx+1);
if (found) return found;
}
}
}
return null;
},
// ── Cache-aware preview ──
showPreview(ev, node) {
if (node.is_folder) return;
this.previewName = node.name;
this.previewX = ev.clientX + 12;
this.previewY = ev.clientY + 12;
this.previewVisible = true;
var self = this;
var wsId = LW.workspace_id || 1;
var cacheKey = 'prev_' + node.id;
// Return cached immediately
if (this.previewCache[cacheKey]) {
this.previewContent = this.previewCache[cacheKey];
return;
}
// Determine type
var imgExts = ['.png','.jpg','.jpeg','.gif','.webp','.svg','.bmp','.ico'];
var isImg = false, isPdf = false;
for (var i=0; i<imgExts.length; i++) {
if ((node.name||'').toLowerCase().endsWith(imgExts[i])) { isImg = true; break; }
}
isPdf = (node.name||'').toLowerCase().endsWith('.pdf');
if (node.content_format === 'file') {
var url = '/api/files/' + wsId + '/' + encodeURIComponent(node.name);
if (isImg) {
var html = '<img src="'+url+'" style="max-width:380px;max-height:180px;display:block;">';
this.previewContent = html; this.previewCache[cacheKey] = html;
} else if (isPdf) {
var html = '<div style="text-align:center;padding:16px;">'+getSvgIcon('file',48)+'<br><span style="color:var(--text-secondary);font-size:12px;">PDF Document</span><br><a href="/pages/'+node.id+'" style="color:var(--accent);font-size:11px;">Open viewer →</a></div>';
this.previewContent = html; this.previewCache[cacheKey] = html;
} else {
this.previewContent = 'Loading...';
this._fetchRetry(url).then(function(t) {
var content = t.substring(0, 500);
self.previewContent = content; self.previewCache[cacheKey] = content;
}).catch(function() { self.previewContent = '(preview not available)'; });
}
} else {
this.previewContent = 'Loading...';
this._fetchRetry('/api/local-workspace/page-content/' + node.id, true)
.then(function(d) {
var content = self._extractPreview(d.content || '', d.format);
self.previewContent = content; self.previewCache[cacheKey] = content;
}).catch(function() { self.previewContent = '(preview not available)'; });
}
},
// ── Fetch with retry ──
_fetchRetry(url, json) {
var maxRetries = 2;
var delay = 300;
var self = this;
return new Promise(function(resolve, reject) {
function attempt(n) {
fetch(url).then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return json ? r.json() : r.text();
}).then(resolve).catch(function(e) {
if (n < maxRetries) {
setTimeout(function() { attempt(n+1); }, delay * (n+1));
} else { reject(e); }
});
}
attempt(0);
});
},
// ── Touch haptic feedback ──
_haptic() {
if (navigator.vibrate) navigator.vibrate(10);
},
// ── 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');
}
},
// ── File type icon ──
_fileIcon(name, isFolder, contentFormat, customIcon) {
if (customIcon) {
var html = window.fdIconHtml ? window.fdIconHtml(customIcon, 14) : this.esc(customIcon);
return '<span class="page-icon-svg" style="font-size:14px;line-height:1;display:inline-flex;align-items:center;">'+html+'</span>';
}
if (isFolder) return '<span class="page-icon-svg">'+getSvgIcon('folder',14)+'</span>';
// FlowDeck internal pages (no extension, blocks/markdown format)
if (contentFormat && contentFormat !== 'file') return '<span class="page-icon-svg">'+getSvgIcon('edit',14)+'</span>';
var n = (name || '').toLowerCase();
if (/\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('image',14)+'</span>';
if (/\.pdf$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
if (/\.(md|markdown)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('edit',14)+'</span>';
if (/\.(py)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
if (/\.(js|jsx|ts|tsx)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
if (/\.(html?|xml)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
if (/\.css$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
if (/\.json$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
if (/\.sql$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
if (/\.(sh|bash|zsh)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
if (/\.ps1$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
if (/\.(rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
if (/\.(txt|log)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
if (/\.(zip|tar|gz|rar|7z)$/.test(n)) return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
return '<span class="page-icon-svg">'+getSvgIcon('file',14)+'</span>';
},
// ── Search / Filter ──
doFilter() {
var q = (this.searchQuery || '').toLowerCase().trim();
var ft = this.filterType || '';
if (!q && !ft && !this.tagFilter) { this.displayTree = [...this.tree]; this.showAll = false; this._computeFilteredTableItems(); return; }
// Apply all filters (search + type + tag) to tree view
this.displayTree = this._filterTree([...this.tree], q, ft);
if (this.tagFilter) {
this.displayTree = this._filterTreeByTag(this.displayTree, this.tagFilter);
}
this._computeFilteredTableItems();
},
_filterTreeByTag(nodes, tagName) {
var result = [];
for (var i=0; i<nodes.length; i++) {
var n = nodes[i];
var hasTag = (n.tags || []).some(function(t) { return t.name === tagName; });
var filteredChildren = n.children ? this._filterTreeByTag(n.children, tagName) : [];
if (hasTag || filteredChildren.length > 0) {
var clone = Object.assign({}, n);
clone.children = filteredChildren;
result.push(clone);
}
}
return result;
},
filterByType(type) {
this.filterType = (this.filterType === type) ? '' : type;
this.doFilter();
},
_filterTree(nodes, query, fileType) {
var result = [];
for (var i=0; i<nodes.length; i++) {
var n = nodes[i];
var filteredChildren = n.children ? this._filterTree(n.children, query, fileType) : [];
var selfMatches = (n.name || '').toLowerCase().indexOf(query || '') !== -1;
// File type filter
var typeMatch = true;
if (fileType && !n.is_folder) {
var nm = (n.name || '').toLowerCase();
switch(fileType) {
case 'folder': typeMatch = n.is_folder; break;
case 'pdf': typeMatch = /\.pdf$/.test(nm); break;
case 'image': typeMatch = /\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(nm); break;
case 'code': typeMatch = /\.(py|js|jsx|ts|tsx|rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r|sh|bash|ps1|sql)$/.test(nm); break;
case 'text': typeMatch = /\.(txt|md|markdown|log)$/.test(nm); break;
case 'page': typeMatch = !/\.(pdf|png|jpe?g|gif|webp|svg|bmp|ico|py|js|jsx|ts|tsx|rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r|sh|bash|ps1|sql|txt|md|markdown|log)$/.test(nm); break;
}
}
if ((selfMatches || filteredChildren.length > 0) && typeMatch) {
var clone = Object.assign({}, n);
clone.children = filteredChildren;
result.push(clone);
}
}
return result;
},
// ── Preview on hover ──
_extractPreview(content, format) {
if (!content) return '(empty)';
// Try to extract text from Notion block JSON
if (format === 'blocks' || format === 'markdown') {
try {
var blocks = typeof content === 'string' ? JSON.parse(content) : content;
if (blocks && blocks.blocks && Array.isArray(blocks.blocks)) {
var lines = [];
for (var i=0; i<blocks.blocks.length && lines.length<20; i++) {
var b = blocks.blocks[i];
if (b.content) lines.push(b.content);
else if (b.text) lines.push(b.text);
else if (typeof b === 'string') lines.push(b);
}
return lines.join('\n').substring(0, 500) || '(empty)';
}
} catch(e) {}
}
// Fallback: raw text
return String(content).substring(0, 500);
},
hidePreview() {
this.previewVisible = false;
},
openModal(type, parent) {
this.createType = type;
this.parentFolder = parent||null;
this.newName = '';
this.showCreate = true;
var self = this;
this.$nextTick(function() {
var el = self.$refs.cinp;
if (el) setTimeout(function() { el.focus(); }, 100);
});
},
async doCreate() {
var n = this.newName.trim();
if (!n) return;
var body = { name:n, type:this.createType };
if (this.parentFolder) body.parent_id = this.parentFolder.db_id;
var r = await fetch('/api/local-workspace/items', {
method:'POST',
headers:{'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'', 'Content-Type':'application/json'},
body:JSON.stringify(body)
});
if (r.ok) {
var d = await r.json();
// Save parent ref BEFORE clearing so we can insert into tree
var parent = this.parentFolder;
this.showCreate = false;
this.parentFolder = null;
this.newName = '';
// Insert new node into tree without reload
var isFolder = this.createType === 'folder';
var newNode = {
id: d.id, db_id: d.id, name: d.name || n,
is_folder: isFolder, type: isFolder ? 'folder' : 'page',
content_format: isFolder ? null : 'blocks',
children: [], has_children: false, depth: 0
};
if (parent && parent.db_id) {
// Insert as child of parent folder
this._insertIntoParent(this.tree, parent.db_id, newNode);
// Auto-expand parent so the new item is visible
this.expanded[parent.db_id] = true;
this.expanded = Object.assign({}, this.expanded);
} else {
// Insert at root
this.tree.unshift(newNode);
}
// Force new reference so Alpine x-for re-renders
if (!this.searchQuery && !this.filterType) {
this.displayTree = [...this.tree];
} else if (this.sortBy) {
// Re-sort with the new item
this.doSort();
} else {
// Re-apply filter with updated tree
this.doFilter();
}
this._saveExpandedState();
// Directly refresh sidebar tree
if (window.appState && window.appState.refreshSidebarTree) {
window.appState.refreshSidebarTree();
}
}
},
_insertIntoParent(nodes, parentId, newNode) {
for (var i=0; i<nodes.length; i++) {
if (nodes[i].id === parentId) {
if (!nodes[i].children) nodes[i].children = [];
nodes[i].children.unshift(newNode);
nodes[i].has_children = true;
newNode.depth = (nodes[i].depth || 0) + 1;
return true;
}
if (nodes[i].children && this._insertIntoParent(nodes[i].children, parentId, newNode)) {
return true;
}
}
return false;
},
openPage(id) { if (window.fdNavigate) window.fdNavigate('/pages/' + id); else window.location = '/pages/' + id; },
openSidePeek(id) {
var node = this._findNodeById(this.tree, id) || this._findNodeById(this.displayTree, id);
if (node) this.selectForPreview(node);
},
startRename(node) {
this.target = node;
this.newName = node.name.trim();
this.showRename = true;
var self = this;
this.$nextTick(function() {
var el = self.$refs.rinp;
if (el) setTimeout(function() { el.focus(); el.select(); }, 100);
});
},
async doRename() {
var n = this.newName.trim();
if (!n||!this.target) return;
var r = await fetch('/api/local-workspace/items/'+this.target.db_id, {
method:'PUT',
headers:{'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'', 'Content-Type':'application/json'},
body:JSON.stringify({name:n})
});
if (r.ok) { this._reloadAfterAction(); }
},
startDelete(node) {
this.target = node;
this.showDelete = true;
},
async doDelete() {
if (!this.target) return;
var r = await fetch('/api/local-workspace/items/'+this.target.db_id, {headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||''}, method:'DELETE' });
if (r.ok) { this._reloadAfterAction(); }
},
// ── Drag & Drop ──
onDragOver(ev) {
this.isDragging = true;
// Detect if hovering over a folder item
var folderEl = ev.target.closest('.ws-tree-item');
var parentFolder = folderEl ? folderEl.querySelector('.name.folder') : null;
// Clear previous highlight
document.querySelectorAll('.drop-target-highlight').forEach(function(el){ el.classList.remove('drop-target-highlight'); });
if (parentFolder) {
var name = parentFolder.textContent.trim();
this.dropTargetName = name;
// Find parent node id from the onclick attribute or data
var li = parentFolder.closest('.ws-tree-item');
if (li) {
var clickEl = li.querySelector('.name.folder');
// Try to extract ID from onclick or the onclick handler
// Use data attributes if available, otherwise fall back
}
folderEl.classList.add('drop-target-highlight');
} else {
this.dropTargetName = 'root';
}
},
onDragLeave(ev) {
// Only reset if leaving the layout entirely
if (!ev.currentTarget.contains(ev.relatedTarget)) {
this.isDragging = false;
this.dropTargetName = '';
document.querySelectorAll('.drop-target-highlight').forEach(function(el){ el.classList.remove('drop-target-highlight'); });
}
},
onInternalDragStart(ev, id) {
this.draggedItemId = id;
ev.dataTransfer.effectAllowed = 'move';
ev.dataTransfer.setData('text/plain', String(id));
// Add a visual class to the dragged element
ev.target.style.opacity = '0.5';
},
onDrop(ev) {
this.isDragging = false;
document.querySelectorAll('.drop-target-highlight').forEach(function(el){ el.classList.remove('drop-target-highlight'); });
// Reset any opacity from drag
document.querySelectorAll('.ws-tree-item').forEach(function(el){ el.style.opacity = ''; });
// ── Internal drag (move item to another folder) ──
if (this.draggedItemId !== null) {
var draggedId = this.draggedItemId;
this.draggedItemId = null;
// Determine target parent
var folderEl = ev.target.closest('.ws-tree-item');
var targetParentId = this.currentFolder > 0 ? this.currentFolder : null;
if (folderEl) {
var folderLink = folderEl.querySelector('.name.folder');
if (folderLink) {
var fid = folderEl.getAttribute('data-folder-id');
if (fid) targetParentId = parseInt(fid);
}
}
// Don't drop on self or same parent
if (draggedId === targetParentId) return;
this._moveItem(draggedId, targetParentId);
return;
}
// ── External drop (files from computer) ──
var folderEl = ev.target.closest('.ws-tree-item');
var parentId = this.currentFolder > 0 ? this.currentFolder : null;
var parentName = 'root';
if (folderEl) {
var folderLink = folderEl.querySelector('.name.folder');
if (folderLink) {
parentName = folderLink.textContent.trim();
// Use data-folder-id attribute (works for both x-for and renderChildren)
var fid = folderEl.getAttribute('data-folder-id');
if (fid) parentId = parseInt(fid);
}
}
var items = ev.dataTransfer.items;
if (!items) return;
var self = this;
var dtFiles = ev.dataTransfer.files;
// Check if any item is a directory (WebKit only)
var hasDirectory = false;
for (var i = 0; i < items.length; i++) {
var entry = items[i].webkitGetAsEntry ? items[i].webkitGetAsEntry() : null;
if (entry && entry.isDirectory) { hasDirectory = true; break; }
}
if (hasDirectory) {
// Walk directory tree and upload structure
this._processDirectoryDrop(items, parentId, parentName);
} else if (dtFiles.length > 0) {
// Simple file upload
this._uploadFiles(dtFiles, parentId, parentName);
}
},
_processDirectoryDrop(items, parentId, parentName) {
var self = this;
var allItems = []; // {path: string, type: 'file'|'folder', entry: FileSystemEntry}
var pending = items.length;
function walkEntry(entry, basePath) {
var path = basePath ? basePath + '/' + entry.name : entry.name;
if (entry.isFile) {
allItems.push({path: path, type: 'file', entry: entry});
pending--;
if (pending === 0) doUpload();
} else if (entry.isDirectory) {
allItems.push({path: path, type: 'folder', entry: entry});
var reader = entry.createReader();
reader.readEntries(function(entries) {
pending += entries.length;
for (var i = 0; i < entries.length; i++) {
walkEntry(entries[i], path);
}
pending--;
if (pending === 0) doUpload();
});
}
}
for (var i = 0; i < items.length; i++) {
var entry = items[i].webkitGetAsEntry ? items[i].webkitGetAsEntry() : null;
if (entry) walkEntry(entry, '');
else pending--;
}
if (pending === 0) doUpload();
function doUpload() {
// Build structure
var structure = [];
var files = [];
for (var i = 0; i < allItems.length; i++) {
structure.push({path: allItems[i].path, type: allItems[i].type});
}
// Resolve file entries to actual File objects
var fileEntries = [];
for (var i = 0; i < allItems.length; i++) {
if (allItems[i].type === 'file') fileEntries.push(allItems[i]);
}
var resolved = 0;
var formData = new FormData();
formData.append('structure', JSON.stringify(structure));
if (parentId) formData.append('parent_id', parentId);
function addFile(idx) {
if (idx >= fileEntries.length) {
// All resolved, upload
self._doUpload(formData, parentId, parentName);
return;
}
var fe = fileEntries[idx];
fe.entry.file(function(file) {
formData.append('files', file, fe.path);
resolved++;
addFile(idx + 1);
}, function() {
resolved++;
addFile(idx + 1);
});
}
addFile(0);
}
},
_uploadFiles(fileList, parentId, parentName) {
var formData = new FormData();
for (var i = 0; i < fileList.length; i++) {
formData.append('files', fileList[i]);
}
if (parentId) formData.append('parent_id', parentId);
this._doUpload(formData, parentId, parentName);
},
async _moveItem(itemId, newParentId) {
var body = { parent_id: newParentId || null };
try {
var r = await fetch('/api/local-workspace/items/' + itemId + '/move', {
method: 'PUT',
headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'', 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (r.ok) {
this._reloadAfterAction();
} else {
window.showToast('Move failed', 'error');
}
} catch(e) {
window.showToast('Move failed: ' + e.message, 'error');
}
},
async _doUpload(formData, parentId, parentName) {
var self = this;
this.uploading = true;
this.uploadStatus = 'Uploading to ' + parentName + '...';
this.uploadProgress = 10;
var folderHandled = formData.has('structure');
var url = folderHandled ? '/api/local-workspace/upload-folder' : '/api/local-workspace/upload';
try {
var r = await fetch(url, { method: 'POST', body: formData });
this.uploadProgress = 80;
var d = await r.json();
if (r.ok) {
this.uploadStatus = '✓ ' + d.items.length + ' item(s) uploaded';
this.uploadProgress = 100;
var items = d.items || [];
setTimeout(function() {
self.uploading = false;
self._reloadAfterAction();
}, 800);
} else {
this.uploadStatus = '✗ ' + (d.error || 'Upload failed');
this.uploadProgress = 0;
setTimeout(function() { self.uploading = false; }, 2000);
}
} catch(e) {
this.uploadStatus = '✗ Network error';
this.uploadProgress = 0;
setTimeout(function() { self.uploading = false; }, 2000);
}
},
// ── Tag management ──
async loadWorkspaceTags() {
try {
var r = await fetch('/api/local-workspace/tags', { cache: 'no-store' });
var d = await r.json();
this.workspaceTags = d.tags || [];
} catch(e) { this.workspaceTags = []; }
},
// Mise à jour OPTIMISTE du compteur d'un tag dans la barre de tags :
// rafraîchit immédiatement la pastille (delta +1/-1) sans attendre le
// re-fetch (qui viendra ensuite confirmer/surécrire avec les valeurs serveur).
_bumpTagCount(tag, delta) {
if (!tag) return;
var tags = (this.workspaceTags || []).slice();
var i = -1;
for (var k = 0; k < tags.length; k++) {
if (String(tags[k].id) === String(tag.id)) { i = k; break; }
}
if (i >= 0) {
tags[i] = Object.assign({}, tags[i], { count: Math.max(0, (tags[i].count || 0) + delta) });
} else if (delta > 0) {
tags.push({ id: tag.id, name: tag.name || '', color: tag.color || '#787774', count: delta });
}
// Réassigne un nouveau tableau → réactivité Alpine
this.workspaceTags = tags;
},
toggleTagFilter(tagName) {
this.tagFilter = (this.tagFilter === tagName) ? '' : tagName;
this.filterType = '';
this.searchQuery = '';
this.doFilter();
},
startTagInput(node) {
this.tagInputId = node.id;
var self = this;
this.$nextTick(function() {
var inp = self.$el.querySelector('.tag-input:not([style*="display: none"])');
if (inp) inp.focus();
});
},
async addTag(itemId, tagName) {
tagName = (tagName || '').trim();
if (!tagName) { this.tagInputId = null; return; }
try {
var r = await fetch('/api/local-workspace/items/' + itemId + '/tags', {
method: 'POST',
headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'', 'Content-Type': 'application/json'},
body: JSON.stringify({name: tagName})
});
if (r.ok) {
var d = await r.json();
// Update node tags in tree
this._updateNodeTags(this.tree, itemId, d.tag, 'add');
this.displayTree = [...this.tree];
this.tagsVersion++;
this._bumpTagCount(d.tag, 1);
await this.loadWorkspaceTags();
this._computeFilteredTableItems();
}
} catch(e) {}
this.tagInputId = null;
},
async removeTag(itemId, tagId) {
try {
var r = await fetch('/api/local-workspace/items/' + itemId + '/tags/' + tagId, {headers: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||''},
method: 'DELETE'
});
if (r.ok) {
this._updateNodeTags(this.tree, itemId, null, 'remove', tagId);
this.displayTree = [...this.tree];
this.tagsVersion++;
this._bumpTagCount({ id: tagId }, -1);
await this.loadWorkspaceTags();
this._computeFilteredTableItems();
}
} catch(e) {}
},
_updateNodeTags(nodes, itemId, tag, action, removeTagId) {
for (var i=0; i<nodes.length; i++) {
if (nodes[i].id === itemId) {
if (action === 'add') {
// Réassigne un nouveau tableau → déclenche la réactivité Alpine
var cur = nodes[i].tags || [];
if (!cur.some(function(t) { return t.id === tag.id; })) {
nodes[i].tags = cur.concat([tag]);
}
} else if (action === 'remove') {
nodes[i].tags = (nodes[i].tags || []).filter(function(t) { return t.id !== removeTagId; });
}
return true;
}
if (nodes[i].children && this._updateNodeTags(nodes[i].children, itemId, tag, action, removeTagId)) {
return true;
}
}
return false;
},
// ── Helpers ──
_formatDate(dt) {
if (!dt) return '—';
var d = new Date(dt.replace(' ', 'T') + 'Z');
if (isNaN(d.getTime())) return '—';
var now = new Date();
var diffMs = now - d;
var diffMin = Math.floor(diffMs / 60000);
var diffHrs = Math.floor(diffMs / 3600000);
var diffDays = Math.floor(diffMs / 86400000);
if (diffMin < 1) return 'Just now';
if (diffMin < 60) return diffMin + 'm ago';
if (diffHrs < 24) return diffHrs + 'h ago';
if (diffDays < 7) return diffDays + 'd ago';
return d.toLocaleDateString('en-CA');
},
_fileTypeLabel(name, contentFormat) {
if (contentFormat && contentFormat !== 'file') return 'Page';
var n = (name || '').toLowerCase();
if (/\.(png|jpe?g|gif|webp|svg|bmp|ico)$/.test(n)) return 'Image';
if (/\.pdf$/.test(n)) return 'PDF';
if (/\.(py|js|jsx|ts|tsx|rs|go|java|rb|php|c|cpp|h|swift|kt|scala|r|sh|bash|ps1|sql)$/.test(n)) return 'Code';
if (/\.(txt|md|markdown|log)$/.test(n)) return 'Text';
if (/\.(zip|tar|gz|rar|7z)$/.test(n)) return 'Archive';
if (/\.(mp3|wav|ogg|flac|aac)$/.test(n)) return 'Audio';
if (/\.(mp4|webm|mov|avi)$/.test(n)) return 'Video';
return 'File';
},
selectAllFiltered(ev) {
if (ev.target.checked) {
for (var i=0; i<this.filteredTableItems.length; i++) {
this.selectedIds[this.filteredTableItems[i].id] = true;
}
} else {
for (var k in this.selectedIds) delete this.selectedIds[k];
}
this.selectedIds = Object.assign({}, this.selectedIds);
this._syncSelection();
},
// ── Selection sync (keeps checkboxes & counters in sync) ──
_syncSelection() {
this.selectedIdsCount = Object.keys(this.selectedIds).length;
var total = this._collectVisibleIds(this.displayTree).length;
this.allSelected = total > 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: {'X-CSRF-Token': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'', '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');
}
};
})();
// Injecter toutes les props comme globales pour Alpine (résout les 36 erreurs de scope)
var _wsKeys = Object.keys(window._wsInitData);
for (var _i = 0; _i < _wsKeys.length; _i++) {
try { window[_wsKeys[_i]] = window._wsInitData[_wsKeys[_i]]; } catch(e) {}
}
_wsInitData = window._wsInitData;
console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(window._wsInitData).length);