fix(templates): persist workspace context so template pages actually save
FlowDeck CI / lint (push) Successful in 51s
FlowDeck CI / test (push) Successful in 5m46s
FlowDeck CI / docker (push) Successful in 46s

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.
This commit is contained in:
2026-09-14 11:03:21 -04:00
parent c7d4fd901f
commit 334a937507
3 changed files with 76 additions and 8 deletions
+37 -7
View File
@@ -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
+6 -1
View File
@@ -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',
+33
View File
@@ -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"))