Files
flowdeck/tests/test_v696_sidebar_library_publish.py
T
bruno 0218d8f5e5
FlowDeck CI / lint (push) Successful in 1m37s
FlowDeck CI / test (push) Failing after 24m19s
FlowDeck CI / docker (push) Skipped
fix: /local-workspace — chemin du header + clic sur un dossier du sidebar (v7.69.9)
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.
2026-10-09 16:58:04 -04:00

241 lines
10 KiB
Python

"""FlowDeck v7.69.6 — Library AI Meeting Notes, Recents sidebar, Publish unifié.
- ``GET /api/library/meetings`` : notes de réunion groupées par date côté
client ; exclues des onglets recents/workspace/private.
- Sidebar Recents : fichiers réellement récents (``_recent_sidebar_pages``),
icônes workspace, notes de réunion exclues.
- Publish : ``is_published`` + ``published`` maintenus ensemble — la page
publiée apparaît dans la sidebar Published, la Library et le dialogue.
"""
from __future__ import annotations
import json
import secrets
from conftest import anon_csrf # noqa: F401
from app.db import get_conn
def _login(client):
from app.auth.session import SessionManager
login = f"v696_{secrets.token_hex(4)}"
with get_conn() as conn:
conn.execute(
"INSERT INTO users (login, full_name, email, is_admin) VALUES (?, 'V696', ?, 0)",
(login, f"{login}@test.com"),
)
uid = conn.execute("SELECT id FROM users WHERE login=?", (login,)).fetchone()["id"]
conn.commit()
return SessionManager.create_session({"id": uid, "login": login}), uid
def _cookies(session):
return {"flowdeck_session": session}
def _mkworkspace(uid, name="WS696"):
with get_conn() as conn:
cur = conn.execute("INSERT INTO workspaces (name, owner_id) VALUES (?, ?)",
(name, uid))
conn.commit()
return cur.lastrowid
def _mkpage(ws_id, title, with_meeting=False, updated="2026-10-09 12:00:00"):
content = json.dumps([{"id": "b1",
"type": "meeting" if with_meeting else "paragraph",
"content": ""}])
with get_conn() as conn:
cur = conn.execute(
"INSERT INTO pages (workspace, workspace_id, title, content, content_format,"
" parent_section, updated_at) VALUES ('t', ?, ?, ?, 'blocks', 'Private', ?)",
(ws_id, title, content, updated))
conn.commit()
return cur.lastrowid
# ── Library meetings ─────────────────────────────────────────────────────
def test_library_meetings_tab_lists_only_meeting_notes(client):
session, uid = _login(client)
ws = _mkworkspace(uid)
mp = _mkpage(ws, "Sprint Review", True)
_mkpage(ws, "Plain notes", False)
r = client.get("/api/library/meetings", cookies=_cookies(session))
assert r.status_code == 200, r.text
items = r.json()["items"]
assert [i["id"] for i in items] == [mp]
assert items[0]["is_meeting"] is True
assert items[0]["url"] == f"/pages/{mp}"
def test_library_browsing_tabs_hide_meeting_notes(client):
session, uid = _login(client)
ws = _mkworkspace(uid)
_mkpage(ws, "Sprint Review", True, updated="2026-10-09 12:00:00")
plain = _mkpage(ws, "Plain notes", False, updated="2026-10-09 13:00:00")
for tab in ("recents", "workspace", "private"):
r = client.get(f"/api/library/{tab}", cookies=_cookies(session))
assert r.status_code == 200, r.text
ids = [i["id"] for i in r.json()["items"]]
assert plain in ids
assert not any(i.get("is_meeting") for i in r.json()["items"])
assert len([i for i in ids if isinstance(i, int)]) == len(ids)
# ── Sidebar recents ──────────────────────────────────────────────────────
def test_recent_sidebar_pages_excludes_meetings_and_uses_workspace_icons(client):
from app.routers.dashboard._common import _recent_sidebar_pages
session, uid = _login(client)
ws = _mkworkspace(uid)
_mkpage(ws, "Sprint Review", True)
plain = _mkpage(ws, "Plain notes", False)
recents = _recent_sidebar_pages(uid)
ids = [e["id"] for e in recents]
assert f"page/{plain}" in ids
assert not any("Sprint" in e["name"] for e in recents)
icons = {e["id"]: e["icon"] for e in recents}
assert icons[f"page/{plain}"] == "edit" # même convention que le Workspace
assert all(e["url"].startswith("/pages/") for e in recents)
# ── Publish unifié ───────────────────────────────────────────────────────
def test_publish_sets_both_flags_and_lists(client):
session, uid = _login(client)
ws = _mkworkspace(uid)
pid = _mkpage(ws, "Launch notes", False)
r = client.post(f"/api/pages/{pid}/publish", cookies=_cookies(session))
assert r.status_code == 200, r.text
assert r.json()["is_published"] is True
with get_conn() as conn:
row = conn.execute("SELECT is_published, published, publish_slug FROM pages"
" WHERE id=?", (pid,)).fetchone()
assert row["is_published"] == 1 and row["published"] == 1
assert row["publish_slug"]
# Library Published + sidebar Published voient la page.
r = client.get("/api/library/published", cookies=_cookies(session))
assert pid in [i["id"] for i in r.json()["items"]]
from app.routers.board._common import _load_shared_sidebar_pages
_made, _received, published, _shared = _load_shared_sidebar_pages(uid)
assert any(p["db_id"] == pid for p in published)
# Le dialogue (page_data) reflète l'état.
r = client.get(f"/pages/{pid}", cookies=_cookies(session))
assert r.status_code == 200
assert '"published": true' in r.text
# Unpublish nettoie les deux drapeaux.
r = client.delete(f"/api/pages/{pid}/publish", cookies=_cookies(session))
assert r.json()["is_published"] is False
with get_conn() as conn:
row = conn.execute("SELECT is_published, published FROM pages WHERE id=?",
(pid,)).fetchone()
assert row["is_published"] == 0 and row["published"] == 0
r = client.get("/api/library/published", cookies=_cookies(session))
assert pid not in [i["id"] for i in r.json()["items"]]
def test_share_dialog_publish_path_sets_both_flags_and_slug(client):
session, uid = _login(client)
ws = _mkworkspace(uid)
pid = _mkpage(ws, "Dialog publish", False)
r = client.post(f"/board/api/share/{pid}",
json={"mode": "private", "published": True},
cookies=_cookies(session))
assert r.status_code == 200, r.text
with get_conn() as conn:
row = conn.execute("SELECT is_published, published, publish_slug FROM pages"
" WHERE id=?", (pid,)).fetchone()
assert row["is_published"] == 1 and row["published"] == 1
assert row["publish_slug"]
r = client.get("/api/library/published", cookies=_cookies(session))
assert pid in [i["id"] for i in r.json()["items"]]
def test_customize_panel_above_overlay():
from pathlib import Path
css = (Path(__file__).resolve().parent.parent
/ "static" / "css" / "app.css").read_text(encoding="utf-8")
panel = css.split(".sidebar-customize-panel")[1].split("}")[0]
assert "z-index: 999" in panel
def test_library_column_selectors_beat_row_rules():
"""Show columns : les règles de visibilité doivent battre
``.lib-table-header .th`` et ``.lib-row .row-cell`` (0,2,0) — avant,
seul ``:not(.show-editedtime)`` gagnait et seule Last edited time
s'affichait/masquait (vérifié en direct au navigateur)."""
from pathlib import Path
html = (Path(__file__).resolve().parent.parent
/ "app" / "templates" / "library.html").read_text(encoding="utf-8")
for cls in ("lib-col-author", "lib-col-editedby",
"lib-col-source", "lib-col-visited"):
assert f".lib-table div.{cls}" in html, cls
# ── v7.69.6b : provenance, badges, favoris ─────────────────────────────────
def _save_blocks(client, session, pid, title="T"):
r = client.post(f"/board/api/pages/{pid}/blocks",
json={"title": title,
"blocks": [{"id": "b1", "type": "paragraph",
"content": "hello"}]},
cookies=_cookies(session))
assert r.status_code == 200, r.text
def test_library_provenance_columns(client):
session, uid = _login(client)
ws = _mkworkspace(uid)
pid = _mkpage(ws, "Prov notes", False)
_save_blocks(client, session, pid, "Prov notes")
r = client.get("/api/library/recents", cookies=_cookies(session))
assert r.status_code == 200, r.text
items = [i for i in r.json()["items"] if i["id"] == pid]
assert len(items) == 1
it = items[0]
assert it["author"], "Created by must resolve (versions, sinon owner)"
assert it["edited_by"], "Last edited by must resolve"
assert it["source_icon"] in ("file", "link", "external-link")
assert it["visited_at"] == "", "not visited yet"
# Lecture → visite tracée.
assert client.get(f"/pages/{pid}", cookies=_cookies(session)).status_code == 200
r = client.get("/api/library/recents", cookies=_cookies(session))
it = [i for i in r.json()["items"] if i["id"] == pid][0]
assert it["visited_at"], "Last visited time must be tracked on view"
def test_favorites_use_workspace_icons(client):
session, uid = _login(client)
ws = _mkworkspace(uid)
pid = _mkpage(ws, "Fav notes", False)
with get_conn() as conn:
conn.execute("INSERT INTO favorites (user_id, page_id, position) VALUES (?,?,0)",
(uid, pid))
conn.commit()
r = client.get("/api/library/favorites", cookies=_cookies(session))
assert r.status_code == 200, r.text
items = [i for i in r.json()["items"] if i["id"] == pid]
assert len(items) == 1
assert items[0]["icon"] in ("folder", "file", "edit", "image")
def test_local_workspace_tree_status_flags(client):
from app.routers.dashboard._common import _build_tree_children
session, uid = _login(client)
ws = _mkworkspace(uid)
pid = _mkpage(ws, "Flagged notes", False)
with get_conn() as conn:
conn.execute("INSERT INTO favorites (user_id, page_id, position) VALUES (?,?,0)",
(uid, pid))
conn.execute("UPDATE pages SET is_published=1, published=1, publish_slug='x',"
" share_mode='anyone' WHERE id=?", (pid,))
conn.commit()
tree = _build_tree_children(conn, None, ws, uid)
nodes = [n for n in tree if n["id"] == pid]
assert len(nodes) == 1
assert nodes[0]["favorited"] is True
assert nodes[0]["published"] is True
assert nodes[0]["is_shared"] is True