This issue has been created
There is 1 comment.
 
 
XWiki Platform / cid:jira-generated-image-avatar-c8ae0008-6d12-4d83-a3e5-7e41fdb7106e XWIKI-25521 Open

The BlockNote editor content is not reloaded after a save with automatic merge

 
View issue   ยท   Add comment
 

Issue created

 
cid:jira-generated-image-avatar-bb451557-b783-4c9e-addb-fc1e3dca5727 Marius Dumitru Florea created this issue on 22/Oct/26 18:52
 
Summary: The BlockNote editor content is not reloaded after a save with automatic merge
Issue Type: cid:jira-generated-image-avatar-c8ae0008-6d12-4d83-a3e5-7e41fdb7106e Bug
Affects Versions: 18.8.0
Assignee: Unassigned
Components: BlockNote
Created: 22/Oct/26 18:52
Priority: cid:jira-generated-image-static-major-859896c2-f585-4768-80a2-b6c4ed5d72b3 Major
Reporter: Marius Dumitru Florea
Description:

Steps to reproduce

  1. Make BlockNote the default WYSIWYG editor.
  2. Create a page whose content has 3 paragraphs: "one", "two" and "three".
  3. Edit the page with BlockNote (standalone edit mode).
  4. In a second browser tab, edit the same page outside the realtime session (e.g. with the Wiki editor), change "three" to "three third" and save.
  5. Back in the first tab, change "one" to "one first" and save (Save & Continue or Ctrl+S).

Expected results

The save is done with automatic merge ("Saved with merge") and the editor shows the merged content: "one first", "two", "three third". The other users editing the page in the same realtime session see the merged content as well.

Actual results

The save is done with merge, but BlockNote keeps showing "one first", "two", "three", i.e. the change saved from the second tab is not displayed. Since the edit form is updated with the new version number, the next save overwrites the merged changes without a merge conflict, so the change made outside the realtime session is lost.

CKEditor (realtime WYSIWYG editor) behaves correctly in this case, see RealtimeWYSIWYGEditorIT#reloadEditorsSilentMergeConflictManualSave.

 
 

1 comment

 
cid:jira-generated-image-avatar-bb451557-b783-4c9e-addb-fc1e3dca5727 Marius Dumitru Florea on 22/Oct/26 18:52
 

Findings

  • When a save is done with automatic merge, actionButtons.js calls AjaxSaveAndContinue.reloadEditor(). It does the same when the user chooses "Reload" in the merge conflict modal.
  • BlockNote joins the realtime collaboration session even when it can't connect to the collaboration server (XWIKI-24387), so its auto-saver, and thus XWikiFormSaveTarget (autosave-xwiki), is always installed. XWikiFormSaveTarget replaces reloadEditor with document.reload() (which only refreshes the version metadata) and triggers xwiki:actions:reload on the edit form.
  • The realtime CKEditor listens to xwiki:actions:reload and resets its content (wysiwygEditor.js _resetContent()). BlockNote has no such listener, so its content stays unmerged.
  • With CKEditor:
    • standalone edit mode with realtime: the page is not reloaded, the content is reset by the realtime editor;
    • standalone edit mode after leaving the realtime session: the save target is disposed so the whole page is reloaded;
    • in-place edit mode: the xwiki:actions:reload event bubbles up to .xcontent.form where the in-place editing code catches it (shouldReload) and reloads the edit mode (view, then edit again), so the editor instance is recreated.
  • For BlockNote, recreating the editor is not enough: once the Yjs session is initialized (initialContentLoaded in BlockNoteViewWrapper.tsx) the initial content is ignored, so the recreated editor shows the stale shared content again.
  • Only reloading the BlockNote content is not enough either: other form fields can be affected by the merge too (e.g. the title, or a TextArea object property edited with BlockNote). Updating each field separately would require custom code for every picker / editor.

Proposed fix

Reload the edit form (the whole page in standalone edit mode; in-place edit mode already reloads the edit mode), then merge the reloaded (merged) values into the collaboration session, without overwriting the remote changes made while the page was reloading (as long as they are not affected by the merge). The 3-way merge is done with Yjs only, in the collaboration layer:

  • S = Y.encodeStateAsUpdate(doc) captured right before the form is submitted. It covers all the form fields edited in realtime (one shared type per field name).
  • M = the merged values the reloaded form fields start with.
  • F = the live session state after the reload, i.e. S plus the remote changes made in the meantime.
  • For each realtime field: restore S in a temporary Y.Doc, let the field write its value M into its shared type there, then apply Y.encodeStateAsUpdate(temp, stateVectorOfS) to the live doc. The CRDT merges this delta with F, so the remote changes survive where the merge didn't touch.

Steps:

  1. collaboration-api: add Collaboration.initializeField(name, write). The field widget only provides a write(doc) callback that writes its value into its shared type, minimally (BlockNote's blocksToYXmlFragment does, through y-prosemirror's updateYFragment). The collaboration layer decides whether to call it on the live doc (first user joining, with a flag per field instead of the single initialContentLoaded flag), on the temporary doc restored from S (reload after merge), or not at all (normal join).
  2. autosave-xwiki (XWikiFormSaveTarget): new options, with defaults keeping the current behaviour for CKEditor:
    • resetContentOnReload: false wraps reloadEditor instead of replacing it;
    • onBeforeSubmit (to capture S);
    • onReload(expectedVersion), where the version is the merged save's newVersion, or the latestVersion from the 409 response when the user chooses "Reload" in the merge conflict modal;
    • isSaveBlocked.
  3. collaboration-xwiki (formCollaboration.ts):
    • on reload, store the expected version and S in the session storage, keyed by document reference and locale;
    • set a pendingMerge/<field> entry in the shared configuration map for each field found in S, which blocks the saves of the other collaborators while the page reloads, with an expiration of about 30s as a safety net;
    • on join, consume the stored entry only if its version matches the loaded document version and the live doc contains S;
    • decorate initializeField to apply the delta, with an origin that doesn't mark the saver as dirty, then release that field's lock.
  4. BlockNote: replace the first-joiner initialization in BlockNoteViewWrapper.tsx with collaboration.initializeField(name, doc => blocksToYXmlFragment(editor, content, doc.getXmlFragment(name))).

Tests:

  • Unit tests for the Yjs merge, the lock and the new save target options.
  • Port RealtimeWYSIWYGEditorIT#reloadEditorsSilentMergeConflictManualSave to the BlockNote CollaborationIT, for both standalone and in-place edit modes. The change outside the realtime session is made through REST, since BlockNote has no "leave collaboration" action.
  • Port the "Reload" choice from the merge conflict modal (reloadEditorsMergeConflictManualSave).