fix(editor): repair block identity for template pages (duplicate/reordered lines)
FlowDeck CI / lint (push) Successful in 52s
FlowDeck CI / test (push) Successful in 5m46s
FlowDeck CI / docker (push) Successful in 47s

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:
2026-09-14 11:45:44 -04:00
parent 334a937507
commit 4038e9bdad
6 changed files with 242 additions and 13 deletions
+17 -8
View File
@@ -74,6 +74,7 @@ window.__fdRT = (function () {
const blk = op.block || {};
const id = blk.id || ('rb' + Date.now().toString(36));
blk.id = id;
if (typeof ensureBlockIds === 'function') ensureBlockIds([blk]);
let idx = op.index != null ? op.index : blocks.length;
idx = Math.max(0, Math.min(idx, blocks.length));
return blocks.slice(0, idx).concat([blk]).concat(blocks.slice(idx));
@@ -268,8 +269,13 @@ window.__fdRT = (function () {
/* ── application des changements distants ── */
function applySync(blocks, title) {
if (!E || !E.blocks) return;
// v5.13.1: guarantee ids before comparing/merging. Server rooms may still
// carry legacy id-less blocks; without this they collide on
// data-bid="undefined" and the merge below duplicated every line.
blocks = (blocks || []).slice();
if (typeof ensureBlockIds === 'function') ensureBlockIds(blocks);
const curIds = JSON.stringify((E.blocks || []).map(b => b.id));
const srvIds = JSON.stringify((blocks || []).map(b => b.id));
const srvIds = JSON.stringify(blocks.map(b => b.id));
if (curIds === srvIds) {
base = clone(E.blocks);
E.dirty = false;
@@ -280,14 +286,17 @@ window.__fdRT = (function () {
(E.blocks || []).forEach(b => { curById[b.id] = b; });
let out;
try {
out = (blocks || []).map(b => {
const cb = curById[b.id];
if (cb && b.id === fid) return cb; // garde la frappe locale en cours
return b;
});
if (!blocks.length) {
// serveur vide : ne pas effacer le contenu local (page neuve).
out = (E.blocks || []).slice();
} else {
out = blocks.map(b => {
const cb = curById[b.id];
if (cb && b.id === fid) return cb; // garde la frappe locale en cours
return b;
});
}
} catch (e) { return; }
const seen = {}; (blocks || []).forEach(b => { seen[b.id] = 1; });
(E.blocks || []).forEach(b => { if (!seen[b.id]) out.push(b); });
E.blocks = out;
const tEl = document.getElementById('_titleEl');
if (title && title !== E.pageTitle && document.activeElement !== tEl) {