fix: side peek des bases repasse en vanilla JS + largeur 1100px standard (v7.49.0)
FlowDeck CI / test (push) Failing after 3h13m58s
FlowDeck CI / lint (push) Successful in 2m12s
FlowDeck CI / docker (push) Skipped

- Panneau peek: les bindings Alpine (x-data absent du conteneur) rendaient
  loovverture et le redimensionnement inoperants -> cblage direct sur le document.
- Helper unique window.fdWirePeekResize (app.js): pointer capture, 300px-90vw,
  clic=fermer, largeur persiste fd_peek_width partagee entre les 4 peeks.
- database-table-container margin:0 (tableau colle a gauche, marge Library).
- .lib-container remonte dans app.css (trash etait pleine largeur), .db-index 1100px.
- ObsiGate verifie sans code: creation .xlsx OK (openpyxl, #186).
This commit is contained in:
2026-10-05 22:47:36 -04:00
parent 894004a1f5
commit 1d1cdbd618
63 changed files with 9473 additions and 393 deletions
+117 -34
View File
@@ -840,28 +840,73 @@ def test_my_tasks_page(client):
def test_my_tasks_cross_db(client):
"""My Tasks API returns JSON."""
r = client.post("/db/api", json={"name": "My Project"})
"""My Tasks API returns JSON from every *connected* database.
Depuis v7.47.0, une base n'alimente My Tasks qu'après conversion
explicite (``POST /db/{id}/task-db/api``, mapping des 3 colonnes) : la
page n'est plus scopée par ``flowdeck_workspace``.
"""
import time
from app.db import get_conn
col_name = f"My Project {int(time.time() * 1000)}"
with get_conn() as conn:
ws_row = conn.execute(
"SELECT id FROM workspaces WHERE owner_id=(SELECT id FROM users WHERE login='tester') LIMIT 1"
).fetchone()
ws_id = ws_row["id"] if ws_row else conn.execute(
"INSERT INTO workspaces (name, owner_id) VALUES (?, "
"(SELECT id FROM users WHERE login='tester'))", ("My Tasks Cross DB WS",)
).lastrowid
conn.commit()
r = client.post("/db/api", json={"name": col_name, "workspace_id": ws_id})
cid = r.json()["id"]
client.post(f"/db/{cid}/pages/api", json={"title": "Task 1", "properties": {"Status": "Todo"}})
client.post(f"/db/{cid}/pages/api", json={"title": "Task 2", "properties": {"Status": "In Progress"}})
# Sans conversion : la base n'émet rien.
resp = client.get("/my-tasks/api")
assert resp.status_code == 200
assert cid not in [t["collection_id"] for t in resp.json()["tasks"]]
conv = client.post(f"/db/{cid}/task-db/api", json={})
assert conv.status_code == 200, conv.text
assert conv.json()["configured"] is True
for title in ("Task 1", "Task 2"):
client.post(f"/db/{cid}/pages/api", json={"title": title})
resp = client.get("/my-tasks/api")
assert resp.status_code == 200
data = resp.json()
assert "tasks" in data
assert data["total"] >= 2
titles = [t["title"] for t in data["tasks"]]
assert "Task 1" in titles and "Task 2" in titles
assert col_name in [s["name"] for s in data["sources"]]
# Le périmètre est transverse : pas de workspace_id dans le payload.
assert "workspace_id" not in data
client.delete(f"/db/api/{cid}")
def test_my_tasks_view_today(client):
resp = client.get("/my-tasks?view=today")
@pytest.mark.parametrize("view", ["table", "board", "calendar"])
def test_my_tasks_views(client, view):
"""Les trois vues (tableau / kanban / calendrier) sont acceptées.
Sans base connectée, le serveur rend son état vide (le conteneur client
n'est chargé que lorsqu'il y a des tâches à afficher) : on vérifie donc le
code HTTP ici, et le contenu via l'API.
"""
resp = client.get("/my-tasks", params={"view": view})
assert resp.status_code == 200
assert "My Tasks" in resp.text
assert client.get("/my-tasks/api",
params={"view": view}).status_code == 200
def test_my_tasks_view_overdue(client):
resp = client.get("/my-tasks?view=overdue")
@pytest.mark.parametrize("due", ["all", "today", "overdue", "week"])
def test_my_tasks_due_filters(client, due):
resp = client.get("/my-tasks/api", params={"due": due})
assert resp.status_code == 200
assert resp.json()["filters"]["due"] == due
# ── v2.1.0: Workspace, Comments, Favorites, CSV ──
@@ -1716,18 +1761,33 @@ def test_oauth_login_local_page(client):
assert "Login" in resp.text or "login" in resp.text.lower()
def test_oauth_login_gitea_redirect(client):
"""GET /auth/login?provider=gitea — redirects to Gitea OAuth (configured in test env)."""
def test_oauth_login_gitea_redirect(client, monkeypatch):
"""GET /auth/login?provider=gitea — redirects to Gitea OAuth when configured.
v7.46.0 : les valeurs de substitution livrées dans app/config.py
(``test-id`` / ``test-secret``) ne comptent plus comme « configuré » — sinon
/auth/login redirigeait vers Gitea avec un client_id invalide. Le test
configure donc de VRAIS identifiants.
"""
from app.config import settings
monkeypatch.setattr(settings, "gitea_oauth_client_id", "gitea-real-id")
monkeypatch.setattr(settings, "gitea_oauth_client_secret", "gitea-real-secret")
resp = client.get("/auth/login?provider=gitea", follow_redirects=False)
# Gitea OAuth IS configured in test env → redirect to Gitea
# Gitea OAuth configuré → redirect to Gitea
assert resp.status_code == 302
assert "login/oauth" in resp.headers.get("location", "").lower()
assert "client_id=gitea-real-id" in resp.headers["location"]
def test_oauth_login_with_link_mode(client):
def test_oauth_login_with_link_mode(client, monkeypatch):
"""GET /auth/login?provider=gitea&mode=link — link mode redirects to Gitea OAuth."""
from app.config import settings
monkeypatch.setattr(settings, "gitea_oauth_client_id", "gitea-real-id")
monkeypatch.setattr(settings, "gitea_oauth_client_secret", "gitea-real-secret")
resp = client.get("/auth/login?provider=gitea&mode=link", follow_redirects=False)
# Gitea OAuth IS configured → redirect to Gitea with link mode set in session
# Gitea OAuth configuré → redirect to Gitea with link mode set in session
assert resp.status_code == 302
assert "login/oauth" in resp.headers.get("location", "").lower()
@@ -1786,8 +1846,12 @@ def test_get_redirect_uri_env_override_wins(monkeypatch):
monkeypatch.undo()
def test_oauth_login_redirect_uri_dynamic(client):
def test_oauth_login_redirect_uri_dynamic(client, monkeypatch):
"""The authorize URL carries the request-derived redirect_uri (encoded)."""
from app.config import settings
monkeypatch.setattr(settings, "gitea_oauth_client_id", "gitea-real-id")
monkeypatch.setattr(settings, "gitea_oauth_client_secret", "gitea-real-secret")
resp = client.get(
"/auth/login?provider=gitea",
headers={"X-Forwarded-Proto": "https", "X-Forwarded-Host": "flowdeck.dracodev.net"},
@@ -2802,28 +2866,47 @@ def test_sprint_burndown(client):
def test_my_tasks_page_render(client):
"""My Tasks page renders cross-database aggregation."""
# Create a task collection
resp = client.post("/db/api", json={"name": "My Tasks DB"})
coll_id = resp.json()["id"]
"""My Tasks page renders the three views and its sources.
# Toggle to task mode
client.put(f"/db/{coll_id}/toggle-task/api")
Le tableau de bord est rendu côté client depuis ``/my-tasks/api`` : le HTML
transporte la configuration (``data-config``), pas les tâches.
"""
import time
# Add pages
client.post(f"/db/{coll_id}/pages/api", json={
"title": "Urgent fix",
"properties": {"Status": "Todo"},
})
client.post(f"/db/{coll_id}/pages/api", json={
"title": "Deploy",
"properties": {"Status": "Done"},
})
from app.db import get_conn
name = f"My Tasks DB {int(time.time() * 1000)}"
with get_conn() as conn:
ws_row = conn.execute(
"SELECT id FROM workspaces WHERE owner_id=(SELECT id FROM users WHERE login='tester') LIMIT 1"
).fetchone()
ws_id = ws_row["id"] if ws_row else conn.execute(
"INSERT INTO workspaces (name, owner_id) VALUES (?, "
"(SELECT id FROM users WHERE login='tester'))", ("Test Workspace",)
).lastrowid
conn.commit()
resp = client.get("/my-tasks?view=all")
coll_id = client.post("/db/api",
json={"name": name, "workspace_id": ws_id}).json()["id"]
# `toggle-task` reste rétrocompatible : il réalise la conversion complète.
resp = client.put(f"/db/{coll_id}/toggle-task/api")
assert resp.status_code == 200
assert "Urgent fix" in resp.text
assert "My Tasks DB" in resp.text
assert resp.json()["is_task"] is True
client.post(f"/db/{coll_id}/pages/api", json={"title": "Urgent fix"})
client.post(f"/db/{coll_id}/pages/api", json={"title": "Deploy"})
resp = client.get("/my-tasks")
assert resp.status_code == 200
assert 'id="my-tasks-app"' in resp.text
assert "/static/js/my_tasks.js" in resp.text
# Les titres sont bien exposés par l'API alimentée par la page.
titles = [t["title"] for t in client.get("/my-tasks/api").json()["tasks"]]
assert "Urgent fix" in titles
assert name in [s["name"] for s in client.get("/my-tasks/api").json()["sources"]]
client.delete(f"/db/api/{coll_id}")
# ── v4.6.0: Content Blocks Enriched ──