From 722c7103de8f1e97690ff0685a5b3881c5e4e0ad Mon Sep 17 00:00:00 2001 From: bruno Date: Sat, 11 Jul 2026 10:08:33 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20arborescence=20d=C3=A9doubl=C3=A9e=20+?= =?UTF-8?q?=20dossiers=20ferm=C3=A9s=20visibles=20+=20preview=20pages=20Fl?= =?UTF-8?q?owDeck?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Tree affichage: displayTree contient maintenant les racines uniquement (plus de flatten). _markTree ajoute depth/db_id sans aplatir, et renderChildren gère la récursion. Les enfants n'apparaissent plus quand le dossier est fermé, et plus de dédoublement à l'ouverture. 2. Filtre: _filterTree travaille sur l'arbre hiérarchique, clone les nodes pour préserver la structure. 3. Preview pages FlowDeck: nouvelle API GET /api/local-workspace/page-content/{id}. showPreview détecte content_format='file' → /api/files/, sinon → API page-content. Plus de 'file not found' sur les fichiers créés dans FlowDeck. 4. API tree: ajout content_format dans la réponse pour distinguer fichiers uploadés vs pages FlowDeck. --- app/routers/dashboard.py | 20 ++++++++- app/templates/local_workspace.html | 66 +++++++++++++++++++----------- 2 files changed, 59 insertions(+), 27 deletions(-) diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index 90a2f2b..048ecb5 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -554,6 +554,21 @@ async def local_workspace_tree(request: Request, folder: int = None): return {"tree": roots, "breadcrumb": [], "current_folder": None} +@router.get("/api/local-workspace/page-content/{page_id:int}") +async def get_page_content(page_id: int): + """Return the raw content of a page (for preview).""" + with get_conn() as conn: + row = conn.execute( + "SELECT content, content_format FROM pages WHERE id=?", (page_id,) + ).fetchone() + if not row: + return JSONResponse({"error": "Not found"}, status_code=404) + fmt = row["content_format"] + if fmt == "file": + return JSONResponse({"content": "(uploaded file)", "format": fmt}) + return {"content": row["content"] or "", "format": fmt} + + @router.get("/api/local-workspace/breadcrumb") async def local_workspace_breadcrumb(request: Request, folder: int): """Return breadcrumb trail for a folder.""" @@ -566,13 +581,13 @@ def _build_tree_children(conn, parent_id: int | None, ws_id: int) -> list: """Recursively build the tree of children for a node.""" if parent_id is None: rows = conn.execute( - "SELECT id, title, parent_section FROM pages " + "SELECT id, title, parent_section, content_format FROM pages " "WHERE parent_id IS NULL AND workspace_id=? ORDER BY created_at DESC", (ws_id,), ).fetchall() else: rows = conn.execute( - "SELECT id, title, parent_section FROM pages " + "SELECT id, title, parent_section, content_format FROM pages " "WHERE parent_id=? AND workspace_id=? ORDER BY created_at DESC", (parent_id, ws_id), ).fetchall() @@ -586,6 +601,7 @@ def _build_tree_children(conn, parent_id: int | None, ws_id: int) -> list: "name": r["title"] or "Untitled", "type": "folder" if is_folder else "page", "is_folder": is_folder, + "content_format": r["content_format"] if not is_folder else None, "children": children, "has_children": len(children) > 0, }) diff --git a/app/templates/local_workspace.html b/app/templates/local_workspace.html index f2561bb..23c0dd1 100644 --- a/app/templates/local_workspace.html +++ b/app/templates/local_workspace.html @@ -326,25 +326,25 @@ document.addEventListener('alpine:init', function() { } var r = await fetch(url); var d = await r.json(); - this.tree = this.addDepths(d.tree || [], 0); - this.displayTree = this.tree; + // 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; } catch(e) { console.error('Tree load error:', e); this.loaded = true; } }, - addDepths(nodes, depth) { - var result = []; + _markTree(nodes, depth) { for (var i=0; i 0) { - result = result.concat(this.addDepths(n.children, depth+1)); + this._markTree(n.children, depth + 1); } } - return result; }, renderChildren(children, depth) { @@ -402,19 +402,21 @@ document.addEventListener('alpine:init', function() { doFilter() { var q = (this.searchQuery || '').toLowerCase().trim(); if (!q) { this.displayTree = this.tree; return; } - var self = this; - this.displayTree = this.tree.filter(function(n) { - return self._itemMatches(n, q); - }); + this.displayTree = this._filterTree(this.tree, q); }, - _itemMatches(node, query) { - if ((node.name || '').toLowerCase().indexOf(query) !== -1) return true; - if (node.children && node.children.length > 0) { - for (var i = 0; i < node.children.length; i++) { - if (this._itemMatches(node.children[i], query)) return true; + _filterTree(nodes, query) { + var result = []; + for (var i=0; i 0) { + var clone = Object.assign({}, n); + clone.children = filteredChildren; + result.push(clone); } } - return false; + return result; }, // ── Preview on hover ── @@ -426,20 +428,34 @@ document.addEventListener('alpine:init', function() { this.previewVisible = true; var self = this; var wsId = {{ workspace_id }} || 1; - var url = '/api/files/' + wsId + '/' + encodeURIComponent(node.name); - // Check if image + + // Image preview var imgExts = ['.png','.jpg','.jpeg','.gif','.webp','.svg','.bmp','.ico']; var isImg = false; for (var i=0; i'; + + if (node.content_format === 'file') { + // Uploaded file: serve from disk + var url = '/api/files/' + wsId + '/' + encodeURIComponent(node.name); + if (isImg) { + this.previewContent = ''; + } else { + this.previewContent = 'Loading...'; + fetch(url) + .then(function(r) { return r.text(); }) + .then(function(t) { self.previewContent = t.substring(0, 500); }) + .catch(function() { self.previewContent = '(preview not available)'; }); + } } else { + // FlowDeck page: fetch content via API this.previewContent = 'Loading...'; - fetch(url) - .then(function(r) { return r.text(); }) - .then(function(t) { self.previewContent = t.substring(0, 500); }) + fetch('/api/local-workspace/page-content/' + node.id) + .then(function(r) { return r.json(); }) + .then(function(d) { + self.previewContent = (d.content || '').substring(0, 500); + }) .catch(function() { self.previewContent = '(preview not available)'; }); } },