feat(content): v4.6.0 Content Blocks enrichis — Callout, TOC, Math (KaTeX), Toggle, Multi-colonnes
FlowDeck CI / test (push) Failing after 20s
FlowDeck CI / docker (push) Skipped

- Ajout blocs enrichis dans l'éditeur: callout (avec sélecteur d'emoji), table of contents (ancres), math (LaTeX/KaTeX), toggle lists (enfants collapsibles), multi-colonnes
- Intégration KaTeX 0.16.11 self-hosté (JS/CSS/fonts) — aucun CDN externe
- Rendu des nouveaux blocs dans les pages publiques (/p/<slug>): TOC, KaTeX, colonnes, toggle <details>
- Persistance 'children' (colonnes/toggle) + export Markdown étendu
- Sidebar customization par utilisateur (config API + colonne sidebar_config)
- Correctif test_views_calendar: parsing des query params year/month (le défaut ouvrait sur le mois courant)
- Tests: 184 passing
This commit is contained in:
2026-09-03 00:05:33 -04:00
parent d12681720d
commit 814dbe8c2e
77 changed files with 1141 additions and 88 deletions
+112 -4
View File
@@ -619,9 +619,9 @@ def test_views_calendar(client):
r = client.post("/db/api", json={"name": "Cal DB"})
cid = r.json()["id"]
client.post(f"/db/{cid}/pages/api", json={"title": "Event 1", "properties": {"date": "2026-07-15"}})
resp = client.get(f"/db/{cid}/view/calendar")
resp = client.get(f"/db/{cid}/view/calendar?year=2026&month=7")
assert resp.status_code == 200
assert "July" in resp.text or "juillet" in resp.text
assert "July 2026" in resp.text or "juillet 2026" in resp.text
client.delete(f"/db/api/{cid}")
@@ -674,7 +674,7 @@ def test_views_calendar_navigation(client):
cid = r.json()["id"]
resp = client.get(f"/db/{cid}/view/calendar?year=2026&month=8")
assert resp.status_code == 200
assert "August" in resp.text or "août" in resp.text
assert "August 2026" in resp.text or "août 2026" in resp.text
client.delete(f"/db/api/{cid}")
@@ -2757,4 +2757,112 @@ def test_my_tasks_page_render(client):
resp = client.get("/my-tasks?view=all")
assert resp.status_code == 200
assert "Urgent fix" in resp.text
assert "My Tasks DB" in resp.text
assert "My Tasks DB" in resp.text
# ── v4.6.0: Content Blocks Enriched ──
def _make_published_page(client, blocks, title="Enriched Page"):
"""Create a user + published page directly in DB with blocks, return public HTML."""
import uuid
import json as _json
from app.db import get_conn
login = "v460_" + uuid.uuid4().hex[:10]
slug = "v460-" + uuid.uuid4().hex[:8]
with get_conn() as conn:
conn.execute("INSERT INTO users (login, full_name, email) VALUES (?, 'V460', ?)",
(login, login + "@t.com"))
uid = conn.execute("SELECT id FROM users WHERE login=?", (login,)).fetchone()["id"]
cur = conn.execute(
"INSERT INTO pages (workspace, title, content, content_format, is_published, publish_slug) "
"VALUES (?, ?, ?, 'blocks', 1, ?)",
(login, title, _json.dumps(blocks), slug),
)
pid = cur.lastrowid
conn.commit()
resp = client.get(f"/p/{slug}")
with get_conn() as conn:
conn.execute("DELETE FROM users WHERE id=?", (uid,))
conn.execute("DELETE FROM pages WHERE id=?", (pid,))
conn.commit()
return resp
def test_v460_public_table_of_contents(client):
"""Table of contents block renders a list of page headings."""
blocks = [
{"id": "h1", "type": "heading_1", "content": "Overview"},
{"id": "b1", "type": "table_of_contents", "content": ""},
{"id": "h2", "type": "heading_2", "content": "Installation"},
]
resp = _make_published_page(client, blocks)
assert resp.status_code == 200
assert "On this page" in resp.text
assert "Overview" in resp.text
assert "Installation" in resp.text
def test_v460_public_math_block(client):
"""Math block renders a KaTeX-display marker with the LaTeX source."""
blocks = [
{"id": "m1", "type": "math", "content": "E = mc^2"},
]
resp = _make_published_page(client, blocks)
assert resp.status_code == 200
assert 'data-katex="E = mc^2"' in resp.text
def test_v460_public_columns(client):
"""Column blocks render children in a flex row."""
blocks = [
{"id": "cb", "type": "columns",
"children": [
{"id": "c1", "type": "paragraph", "content": "Left cell"},
{"id": "c2", "type": "paragraph", "content": "Right cell"},
]},
]
resp = _make_published_page(client, blocks)
assert resp.status_code == 200
assert "Left cell" in resp.text
assert "Right cell" in resp.text
def test_v460_public_toggle_children(client):
"""Toggle block renders nested children."""
blocks = [
{"id": "tg", "type": "toggle", "content": "Details", "expanded": True,
"children": [{"id": "t1", "type": "bulleted_list", "content": "Nested item"}]},
]
resp = _make_published_page(client, blocks)
assert resp.status_code == 200
assert "Details" in resp.text
assert "Nested item" in resp.text
def test_v460_save_load_blocks_preserves_children(client):
"""Blocks API round-trip preserves children for columns and toggles."""
from app.db import get_conn
import json as _json
r = client.post("/board/api/pages?title=Block RT&section=Private&project=test/test")
pid = r.json()["id"]
client.post(f"/board/api/pages/{pid}/blocks", json={
"title": "Block RT",
"blocks": [
{"id": "cb", "type": "columns", "children": [
{"id": "c1", "type": "paragraph", "content": "A"},
{"id": "c2", "type": "paragraph", "content": "B"},
]},
{"id": "tg", "type": "toggle", "content": "T", "children": [
{"id": "t1", "type": "paragraph", "content": "X"},
]},
{"id": "mt", "type": "math", "content": "x^2"},
],
})
with get_conn() as conn:
row = conn.execute("SELECT content, content_format FROM pages WHERE id=?", (pid,)).fetchone()
assert row["content_format"] == "blocks"
data = _json.loads(row["content"])
types = {b["type"] for b in data}
assert "columns" in types and "toggle" in types and "math" in types
col = next(b for b in data if b["type"] == "columns")
assert len(col["children"]) == 2