fix: arborescence dédoublée + dossiers fermés visibles + preview pages FlowDeck
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.
This commit is contained in:
@@ -554,6 +554,21 @@ async def local_workspace_tree(request: Request, folder: int = None):
|
|||||||
return {"tree": roots, "breadcrumb": [], "current_folder": 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")
|
@router.get("/api/local-workspace/breadcrumb")
|
||||||
async def local_workspace_breadcrumb(request: Request, folder: int):
|
async def local_workspace_breadcrumb(request: Request, folder: int):
|
||||||
"""Return breadcrumb trail for a folder."""
|
"""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."""
|
"""Recursively build the tree of children for a node."""
|
||||||
if parent_id is None:
|
if parent_id is None:
|
||||||
rows = conn.execute(
|
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",
|
"WHERE parent_id IS NULL AND workspace_id=? ORDER BY created_at DESC",
|
||||||
(ws_id,),
|
(ws_id,),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
else:
|
else:
|
||||||
rows = conn.execute(
|
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",
|
"WHERE parent_id=? AND workspace_id=? ORDER BY created_at DESC",
|
||||||
(parent_id, ws_id),
|
(parent_id, ws_id),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
@@ -586,6 +601,7 @@ def _build_tree_children(conn, parent_id: int | None, ws_id: int) -> list:
|
|||||||
"name": r["title"] or "Untitled",
|
"name": r["title"] or "Untitled",
|
||||||
"type": "folder" if is_folder else "page",
|
"type": "folder" if is_folder else "page",
|
||||||
"is_folder": is_folder,
|
"is_folder": is_folder,
|
||||||
|
"content_format": r["content_format"] if not is_folder else None,
|
||||||
"children": children,
|
"children": children,
|
||||||
"has_children": len(children) > 0,
|
"has_children": len(children) > 0,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -326,25 +326,25 @@ document.addEventListener('alpine:init', function() {
|
|||||||
}
|
}
|
||||||
var r = await fetch(url);
|
var r = await fetch(url);
|
||||||
var d = await r.json();
|
var d = await r.json();
|
||||||
this.tree = this.addDepths(d.tree || [], 0);
|
// Keep tree hierarchical, only roots in displayTree
|
||||||
this.displayTree = this.tree;
|
var roots = d.tree || [];
|
||||||
|
this._markTree(roots, 0);
|
||||||
|
this.tree = roots;
|
||||||
|
this.displayTree = roots;
|
||||||
this.breadcrumb = d.breadcrumb || [];
|
this.breadcrumb = d.breadcrumb || [];
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
} catch(e) { console.error('Tree load error:', e); this.loaded = true; }
|
} catch(e) { console.error('Tree load error:', e); this.loaded = true; }
|
||||||
},
|
},
|
||||||
|
|
||||||
addDepths(nodes, depth) {
|
_markTree(nodes, depth) {
|
||||||
var result = [];
|
|
||||||
for (var i=0; i<nodes.length; i++) {
|
for (var i=0; i<nodes.length; i++) {
|
||||||
var n = nodes[i];
|
var n = nodes[i];
|
||||||
n.depth = depth;
|
n.depth = depth;
|
||||||
n.db_id = n.id; // Ensure db_id for consistency with renderChildren
|
n.db_id = n.id;
|
||||||
result.push(n);
|
|
||||||
if (n.children && n.children.length > 0) {
|
if (n.children && n.children.length > 0) {
|
||||||
result = result.concat(this.addDepths(n.children, depth+1));
|
this._markTree(n.children, depth + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
renderChildren(children, depth) {
|
renderChildren(children, depth) {
|
||||||
@@ -402,19 +402,21 @@ document.addEventListener('alpine:init', function() {
|
|||||||
doFilter() {
|
doFilter() {
|
||||||
var q = (this.searchQuery || '').toLowerCase().trim();
|
var q = (this.searchQuery || '').toLowerCase().trim();
|
||||||
if (!q) { this.displayTree = this.tree; return; }
|
if (!q) { this.displayTree = this.tree; return; }
|
||||||
var self = this;
|
this.displayTree = this._filterTree(this.tree, q);
|
||||||
this.displayTree = this.tree.filter(function(n) {
|
|
||||||
return self._itemMatches(n, q);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
_itemMatches(node, query) {
|
_filterTree(nodes, query) {
|
||||||
if ((node.name || '').toLowerCase().indexOf(query) !== -1) return true;
|
var result = [];
|
||||||
if (node.children && node.children.length > 0) {
|
for (var i=0; i<nodes.length; i++) {
|
||||||
for (var i = 0; i < node.children.length; i++) {
|
var n = nodes[i];
|
||||||
if (this._itemMatches(node.children[i], query)) return true;
|
var filteredChildren = n.children ? this._filterTree(n.children, query) : [];
|
||||||
|
var selfMatches = (n.name || '').toLowerCase().indexOf(query) !== -1;
|
||||||
|
if (selfMatches || filteredChildren.length > 0) {
|
||||||
|
var clone = Object.assign({}, n);
|
||||||
|
clone.children = filteredChildren;
|
||||||
|
result.push(clone);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Preview on hover ──
|
// ── Preview on hover ──
|
||||||
@@ -426,20 +428,34 @@ document.addEventListener('alpine:init', function() {
|
|||||||
this.previewVisible = true;
|
this.previewVisible = true;
|
||||||
var self = this;
|
var self = this;
|
||||||
var wsId = {{ workspace_id }} || 1;
|
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 imgExts = ['.png','.jpg','.jpeg','.gif','.webp','.svg','.bmp','.ico'];
|
||||||
var isImg = false;
|
var isImg = false;
|
||||||
for (var i=0; i<imgExts.length; i++) {
|
for (var i=0; i<imgExts.length; i++) {
|
||||||
if ((node.name||'').toLowerCase().endsWith(imgExts[i])) { isImg = true; break; }
|
if ((node.name||'').toLowerCase().endsWith(imgExts[i])) { isImg = true; break; }
|
||||||
}
|
}
|
||||||
if (isImg) {
|
|
||||||
this.previewContent = '<img src="'+url+'" style="max-width:380px;max-height:180px;display:block;">';
|
if (node.content_format === 'file') {
|
||||||
|
// Uploaded file: serve from disk
|
||||||
|
var url = '/api/files/' + wsId + '/' + encodeURIComponent(node.name);
|
||||||
|
if (isImg) {
|
||||||
|
this.previewContent = '<img src="'+url+'" style="max-width:380px;max-height:180px;display:block;">';
|
||||||
|
} 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 {
|
} else {
|
||||||
|
// FlowDeck page: fetch content via API
|
||||||
this.previewContent = 'Loading...';
|
this.previewContent = 'Loading...';
|
||||||
fetch(url)
|
fetch('/api/local-workspace/page-content/' + node.id)
|
||||||
.then(function(r) { return r.text(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(t) { self.previewContent = t.substring(0, 500); })
|
.then(function(d) {
|
||||||
|
self.previewContent = (d.content || '').substring(0, 500);
|
||||||
|
})
|
||||||
.catch(function() { self.previewContent = '(preview not available)'; });
|
.catch(function() { self.previewContent = '(preview not available)'; });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user