From 334a937507151774c7e27d1f14c97dd5c63518cf Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Mon, 14 Sep 2026 11:02:34 -0400 Subject: [PATCH] fix(templates): persist workspace context so template pages actually save MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pages created via the page-template picker (weekly report, project doc, meeting notes, ...) previously got workspace_id=NULL and workspace=login, so they never appeared in the active local/Gitea workspace tree — they looked like they 'didn't save' even though the row existed. Frontend: _useTemplate now sends the active workspace context (workspace_id or workspace key), mirroring _createPlainPage. Backend: use_page_template resolves/validates the workspace and persists both workspace and workspace_id on the new page. --- app/routers/board.py | 44 ++++++++++++++++++++++---- app/templates/base.html | 7 +++- tests/test_v511_v512_wiki_templates.py | 33 +++++++++++++++++++ 3 files changed, 76 insertions(+), 8 deletions(-) diff --git a/app/routers/board.py b/app/routers/board.py index f654e79..cadce6c 100644 --- a/app/routers/board.py +++ b/app/routers/board.py @@ -248,20 +248,50 @@ async def use_page_template(request: Request, template_id: int): blocks_json = row["blocks_json"] name = row["name"] title = title or row["name"] + # Resolve the target workspace so the new page actually shows up in the + # active local workspace (bugfix: template pages previously got + # workspace_id = NULL and never appeared in the sidebar/tree). + uid = (user or {}).get("id") + ws_id_raw = body.get("workspace_id") + ws_id = None + if ws_id_raw is not None: + try: + ws_id = int(ws_id_raw) + except (TypeError, ValueError): + ws_id = None ws_key = user.get("login", "Bruno") if user else "Bruno" + if ws_id is not None: + with get_conn() as conn: + ws_row = conn.execute( + "SELECT id, name, owner_id FROM workspaces WHERE id=?", (ws_id,) + ).fetchone() + if ws_row and (uid is None or ws_row["owner_id"] == uid): + ws_key = ws_row["name"] or ws_key + else: + ws_id = None + else: + body_ws = (body.get("workspace") or "").strip() + if body_ws: + ws_key = body_ws try: json.loads(blocks_json) except (json.JSONDecodeError, TypeError): raise HTTPException(500, "Template content corrupted") from None with get_conn() as conn: - next_order = conn.execute( - "SELECT COALESCE(MAX(sort_order), -1) + 1 FROM pages WHERE workspace=? AND parent_id IS NULL", - (ws_key,), - ).fetchone()[0] + if ws_id is not None: + next_order = conn.execute( + "SELECT COALESCE(MAX(sort_order), -1) + 1 FROM pages WHERE workspace_id=? AND parent_id IS NULL", + (ws_id,), + ).fetchone()[0] + else: + next_order = conn.execute( + "SELECT COALESCE(MAX(sort_order), -1) + 1 FROM pages WHERE workspace=? AND parent_id IS NULL", + (ws_key,), + ).fetchone()[0] cur = conn.execute( - """INSERT INTO pages (workspace, title, content, content_format, parent_section, sort_order) - VALUES (?,?,?,?, 'Private', ?)""", - (ws_key, title or name, blocks_json, "blocks", next_order), + """INSERT INTO pages (workspace, workspace_id, title, content, content_format, parent_section, sort_order) + VALUES (?,?,?,?,?, 'Private', ?)""", + (ws_key, ws_id, title or name, blocks_json, "blocks", next_order), ) conn.commit() page_id = cur.lastrowid diff --git a/app/templates/base.html b/app/templates/base.html index 4e9c6b1..0dd6953 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -981,7 +981,12 @@ var parts = use.split(':'); var body = {}; var url; - if (parts[0] === 'key') { url = '/board/api/page-templates/0/use'; body = {key: parts.slice(1).join(':')}; } + // Pass the current workspace context so the created page is persisted + // in the active local/Gitea workspace (mirrors _createPlainPage()). + var ctx = self._getContext(); + if (ctx.wsId > 0) { body.workspace_id = ctx.wsId; } + else if (ctx.workspaceKey) { body.workspace = ctx.workspaceKey; } + if (parts[0] === 'key') { body.key = parts.slice(1).join(':'); url = '/board/api/page-templates/0/use'; } else { url = '/board/api/page-templates/' + parts[1] + '/use'; } fetch(url, { method: 'POST', diff --git a/tests/test_v511_v512_wiki_templates.py b/tests/test_v511_v512_wiki_templates.py index 86e02ca..b168425 100644 --- a/tests/test_v511_v512_wiki_templates.py +++ b/tests/test_v511_v512_wiki_templates.py @@ -283,6 +283,39 @@ def test_use_builtin_template_creates_page(client): cookies={"flowdeck_session": s}, headers={"X-CSRF-Token": c}).status_code == 404 +def test_use_template_persists_workspace_id(client): + s, c, uid = _login(client) + # Create a local workspace owned by this user + r = client.post("/api/workspaces", json={"name": "Tpl WS"}, + cookies={"flowdeck_session": s}, headers={"X-CSRF-Token": c}) + assert r.status_code == 200 + ws_id = r.json()["id"] + client.post(f"/api/workspaces/{ws_id}/select", cookies={"flowdeck_session": s}) + # built-in template use with workspace context + r2 = client.post("/board/api/page-templates/0/use", json={"key": "meeting_notes", "workspace_id": ws_id}, + cookies={"flowdeck_session": s}, headers={"X-CSRF-Token": c}) + assert r2.status_code == 200 + pid = r2.json()["id"] + page = client.get(f"/board/api/pages/{pid}").json() + assert page["workspace_id"] == ws_id + assert page["workspace"] == "Tpl WS" + # user template use with workspace context + r3 = client.post("/board/api/page-templates", + json={"name": "Tpl WS template", "blocks": _blocks_b("ws body")}, + cookies={"flowdeck_session": s}, headers={"X-CSRF-Token": c}) + tid = r3.json()["id"] + r4 = client.post(f"/board/api/page-templates/{tid}/use", json={"workspace_id": ws_id}, + cookies={"flowdeck_session": s}, headers={"X-CSRF-Token": c}) + assert r4.status_code == 200 + page2 = client.get(f"/board/api/pages/{r4.json()['id']}").json() + assert page2["workspace_id"] == ws_id + assert page2["workspace"] == "Tpl WS" + # routing: template-created page must appear in the workspace tree + tree = client.get("/api/local-workspace/tree", cookies={"flowdeck_session": s}).json() + ids = [n["id"] for n in tree["tree"]] + assert pid in ids + + def test_user_template_roundtrip(client): s, c, uid = _login(client) pid = _mk_page(client, s, "My Weekly", _blocks_b("body text"))