fix(editor): repair block identity for template pages (duplicate/reordered lines)
Template-created pages (weekly report, project doc, meeting notes, to-do list, ...) were persisted without block ids. The editor assigned ids client-side, but the realtime room loaded the raw id-less content and sent it back on 'sync'; applySync then merged id-less server blocks with local blocks, producing data-bid='undefined' collisions and duplicated/shuffled lines as soon as the user edited. Editing an empty page was unaffected because the server state was empty. Fixes: - board.use_page_template: materialize unique block ids (recursively) when instantiating built-in or user templates. - realtime_server: unique block_id() (uuid) + ensure_block_ids() on room load and on insert ops. - editor: recursive ensureBlockIds() in init; gtTok() now restores [[fddate:...]] tokens (date chips survived as labels before). - realtime client: applySync() normalizes ids, no longer appends unknown local blocks (duplication), and keeps local content when server is empty. Tests: pytest (templates + realtime) and Playwright e2e covering to-do list, weekly report date chip, Enter ordering and legacy id-less repair.
This commit is contained in:
@@ -25,8 +25,26 @@ def color_for(uid: int) -> str:
|
||||
|
||||
|
||||
def block_id() -> str:
|
||||
import time
|
||||
return f"b{int(time.time()*1000)}"
|
||||
import uuid
|
||||
return "b" + uuid.uuid4().hex[:12]
|
||||
|
||||
|
||||
def ensure_block_ids(blocks: list[dict]) -> list[dict]:
|
||||
"""Assign unique ids to blocks missing one, recursively.
|
||||
|
||||
Template-created pages may have been persisted without block ids; without
|
||||
them the room state is not addressable by ops and the editor ends up with
|
||||
``data-bid="undefined"`` blocks (duplicated / reordered lines).
|
||||
"""
|
||||
if not isinstance(blocks, list):
|
||||
return blocks
|
||||
for b in blocks:
|
||||
if isinstance(b, dict):
|
||||
if not b.get("id"):
|
||||
b["id"] = block_id()
|
||||
if isinstance(b.get("children"), list):
|
||||
ensure_block_ids(b["children"])
|
||||
return blocks
|
||||
|
||||
|
||||
def apply_op(blocks: list[dict], op: dict) -> list[dict]:
|
||||
@@ -37,6 +55,7 @@ def apply_op(blocks: list[dict], op: dict) -> list[dict]:
|
||||
if not blk.get("id"):
|
||||
blk = dict(blk)
|
||||
blk["id"] = block_id()
|
||||
ensure_block_ids([blk])
|
||||
idx = op.get("index")
|
||||
if not isinstance(idx, int):
|
||||
idx = len(blocks)
|
||||
@@ -126,7 +145,7 @@ class RealtimeManager:
|
||||
room.title = row["title"] or ""
|
||||
if (row["content_format"] or "") == "blocks" and row["content"]:
|
||||
try:
|
||||
room.blocks = json.loads(row["content"])
|
||||
room.blocks = ensure_block_ids(json.loads(row["content"]))
|
||||
except Exception:
|
||||
room.blocks = []
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user