fix: /local-workspace — chemin du header + clic sur un dossier du sidebar (v7.69.9)
FlowDeck CI / lint (push) Successful in 1m37s
FlowDeck CI / test (push) Failing after 24m19s
FlowDeck CI / docker (push) Skipped

Corrige deux régressions de /local-workspace :

- le chemin du header restait bloqué sur « Home / <workspace> » quel que
  soit le dossier affiché : la route rend désormais breadcrumb_items
  (Home / <workspace> / <dossier> / <sous-dossier>, niveaux cliquables,
  collapse « … » au-delà de 4) et la navigation sans rechargement recalcule
  le chemin via l'event flowdeck:breadcrumb-changed ;
- le clic sur un dossier du sidebar affichait TOUS les composants à la
  fois : Alpine.data('wsInitData') retournait le même objet singleton, le
  2e montage (navigation partielle) levait « Cannot redefine property:
  \ » et initTree abandonnait, laissant tout le contenu au state
  brut. La factory retourne désormais une enveloppe fraîche par montage
  qui délègue à l'état réactif partagé. #lw-config est aussi relu à chaque
  exécution (le 2e montage gardait le folder_id du 1er chargement).

Inclus également le travail en cours de l'arbre : Library (colonnes Last
visited/Source, ordre d'en-tête, favoris à icônes Workspace), Meeting
Notes (bloc, CSS, routes, docs), coloration de code hljs, badges
favori/publié dans l'arbre local-workspace, docs (DATA_MODEL,
architectures) et tests associés.
This commit is contained in:
2026-10-09 16:58:04 -04:00
parent 7468e497c6
commit 0218d8f5e5
44 changed files with 12060 additions and 1729 deletions
+60 -5
View File
@@ -90,6 +90,50 @@ def _local_workspaces_for_user(user: dict | None, workspace_id: int = 0) -> list
def _recent_sidebar_pages(uid: int, limit: int = 8) -> list:
"""Fichiers réellement récents (dernière édition) pour la sidebar.
v7.69.6 : avant, seuls les dépôts Gitea alimentaient la section Recents
(vide sans dépôt). Icônes identiques au Workspace (noms SVG, pas
d'emoji). Notes de réunion exclues → section Meetings.
"""
if not uid:
return []
try:
from app.routers.board import _file_icon
with get_conn() as conn:
rows = conn.execute(
"""SELECT p.id, p.title, p.parent_section, p.content_format
FROM pages p
LEFT JOIN workspaces w ON w.id = p.workspace_id
WHERE p.deleted_at IS NULL AND p.parent_section != 'Trash'
AND (w.owner_id = ? OR p.workspace_id IS NULL)
AND REPLACE(COALESCE(p.content,''), ' ', '')
NOT LIKE '%"type":"meeting"%'
ORDER BY p.updated_at DESC LIMIT ?""",
(uid, limit),
).fetchall()
out = []
for r in rows:
is_folder = r["parent_section"] == "Workspace"
out.append({
"id": f"page/{r['id']}",
"name": r["title"] or "Untitled",
"icon": "folder" if is_folder else _file_icon(
r["title"] or "", r["content_format"]),
"url": f"/pages/{r['id']}",
"active": False,
"indent": 0,
"depth": 0,
"has_children": False,
"children": [],
})
return out
except Exception:
logger.exception("_recent_sidebar_pages")
return []
def _sidebar_data(request: Request, repos: list[dict], include_workspace: bool = True) -> dict:
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
ws = user.get("login", "Bruno") if user else "Bruno"
@@ -108,7 +152,7 @@ def _sidebar_data(request: Request, repos: list[dict], include_workspace: bool =
except Exception:
logger.exception("_sidebar_data")
recent_pages = []
recent_pages = _recent_sidebar_pages(user["id"] if user else 0) if user else []
for repo in repos[:10]:
full_name = repo.get("full_name", "")
recent_pages.append({
@@ -359,21 +403,26 @@ def _require_page_view(request: Request, page_id: int) -> None:
def _build_tree_children(conn, parent_id: int | None, ws_id: int, uid: int | None = None) -> list:
"""Recursively build the tree of children for a node."""
# v7.69.6 : notes de réunion exclues (section Meetings, comme la sidebar).
no_meet = "AND REPLACE(COALESCE(content,''), ' ', '') NOT LIKE '%\"type\":\"meeting\"%'"
if parent_id is None:
rows = conn.execute(
"SELECT id, title, parent_section, content_format, content, page_icon, "
"(is_shared OR share_mode != 'private' OR COALESCE(published,0)) as is_shared, "
"COALESCE(is_published,0) AS pub1, COALESCE(published,0) AS pub2, "
"(is_shared OR share_mode != 'private' OR COALESCE(published,0) OR COALESCE(is_published,0)) as is_shared, "
"created_at, updated_at FROM pages "
"WHERE parent_id IS NULL AND workspace_id=? AND deleted_at IS NULL AND collection_row_id IS NULL "
f"{no_meet}"
"ORDER BY created_at DESC",
(ws_id,),
).fetchall()
else:
rows = conn.execute(
"SELECT id, title, parent_section, content_format, content, page_icon, "
"(is_shared OR share_mode != 'private' OR COALESCE(published,0)) as is_shared, "
"COALESCE(is_published,0) AS pub1, COALESCE(published,0) AS pub2, "
"(is_shared OR share_mode != 'private' OR COALESCE(published,0) OR COALESCE(is_published,0)) as is_shared, "
"created_at, updated_at FROM pages "
"WHERE parent_id=? AND workspace_id=? AND deleted_at IS NULL "
f"WHERE parent_id=? AND workspace_id=? AND deleted_at IS NULL {no_meet} "
"ORDER BY created_at DESC",
(parent_id, ws_id),
).fetchall()
@@ -441,6 +490,7 @@ def _build_tree_children(conn, parent_id: int | None, ws_id: int, uid: int | Non
"author": author,
"tags": tags_map.get(r["id"], []),
"is_shared": bool(r["is_shared"]),
"published": bool(r["pub1"] or r["pub2"]),
"favorited": r["id"] in favorited_ids,
})
return tree
@@ -671,12 +721,17 @@ def _render_blocks_public(blocks: list, titles: dict | None = None) -> str:
)
elif t == "code":
lang = b.get("language", "")
try:
from app.services.export import normalize_code_lang
hljs_lang = normalize_code_lang(lang)
except Exception: # noqa: BLE001 — never break public rendering
hljs_lang = "plaintext"
lang_label = f"<div style='font-size:11px;opacity:.4;margin-bottom:8px;'>{lang}</div>" if lang else ""
html_parts.append(
f'<pre style="background:rgba(255,255,255,.05);padding:16px 20px;border-radius:8px;'
f'overflow-x:auto;font-size:14px;line-height:1.5;margin:12px 0;">'
f'{lang_label}'
f'<code>{c}</code></pre>'
f'<code class="hljs language-{hljs_lang}" data-hljs-lang="{hljs_lang}">{c}</code></pre>'
)
elif t == "divider":
html_parts.append('<hr style="border:none;border-top:1px solid rgba(255,255,255,.1);margin:16px 0;">')