Skip to content

fix: preserve block identity during type conversion#325

Merged
appflowy merged 3 commits into
mainfrom
fix/shared_block
Jun 21, 2026
Merged

fix: preserve block identity during type conversion#325
appflowy merged 3 commits into
mainfrom
fix/shared_block

Conversation

@appflowy

@appflowy appflowy commented May 8, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR aligns the web editor with the desktop shared-block fix by preserving block identity during safe text block type conversions.

Key changes:

  • Update turnToBlock so paragraph/list/heading/quote/todo-style conversions update the existing Yjs block type/data in place when the target block can safely keep the same child shape.
  • Keep child-incompatible conversions, such as nested list to heading, on the replacement path so children are flattened below the converted block.
  • Preserve the existing block id and text id during safe in-place conversions.
  • Apply same-id Yjs type/data changes back to Slate so remote type updates render without forcing a destructive reload path.
  • Add regression coverage for the user-reported child-flattening and undo/redo cases from the historical desktop fix context.

Related GitHub Issues

Root Cause

The old conversion path modeled type changes as delete old block plus insert new block. That preserved the visual document shape, but changed block identity and regenerated related refs. Under high edit churn, especially markdown shortcuts and list/toggle conversions, persisted Yjs updates could later contain a child item whose parent was not a shared collection or tombstone, producing the Type: 8 open failure.

Compatibility

Writer Reader Result
Old desktop/web client New client Works for normal sync/open. Existing old documents can be opened, but ids already lost by old delete+insert conversions cannot be reconstructed.
New desktop/web client New client Supported. Type conversions preserve block identity and sync as same-id type/data updates.
New client Old desktop/web client Compatibility risk. Old clients may not understand or render same-id type updates correctly, so mixed-version collaboration is not fully backward-compatible.
New client opens an old-client-generated document New client Supported. Stored block trees open/render/edit normally; historical identity loss remains historical.

Desktop Alignment

Companion desktop PR: AppFlowy-IO/AppFlowy-Premium#979

Desktop uses the same architecture end-to-end through Flutter editor operations, the transaction adapter, Rust update_type, collab in-place type updates, and remote diff support.

Test Coverage

  • Childless paragraph -> heading conversion updates the same Yjs block/text entries in place, then undo/redo restores only type/data without changing ids.
  • Nested todo_list -> numbered_list conversion keeps the parent block id, child block ids, text ids, and child parent links through undo/redo because both block types can keep the same child shape.
  • Nested numbered_list -> heading conversion flattens children into sibling blocks under the page because heading blocks cannot contain children; undo restores the original nested numbered list and redo returns to the flattened heading shape.
  • Remote same-id Yjs type/data updates apply to the Slate node found by block id, not by a stale root path.
  • Remote type/data updates for a block id that is not rendered in Slate are ignored, leaving visible siblings unchanged.

Verification

  • pnpm jest src/application/slate-yjs/__tests__/yjs-utils.test.ts src/application/slate-yjs/__tests__/applyToSlate.test.ts --runInBand --no-coverage
  • pnpm run type-check
  • pnpm exec eslint --quiet --ext .ts src/application/slate-yjs/__tests__/applyToSlate.test.ts src/application/slate-yjs/__tests__/yjs-utils.test.ts src/application/slate-yjs/utils/yjs.ts --ignore-path .eslintignore.web
  • pnpm exec prettier --check src/application/slate-yjs/__tests__/applyToSlate.test.ts src/application/slate-yjs/__tests__/yjs-utils.test.ts src/application/slate-yjs/utils/yjs.ts

Summary by Sourcery

Preserve Yjs block identity during safe text block type conversions and ensure remote block type/data updates apply correctly to matching Slate nodes.

Bug Fixes:

  • Fix safe text block conversions to update existing Yjs blocks in place, keeping block and text ids stable and avoiding corruption from delete+insert conversions.
  • Ensure remote block type/data updates are applied to Slate nodes by block id and ignored when the block is not rendered, preventing stale-path mutations.

Enhancements:

  • Add guardrails to only convert blocks in place when their child structure is compatible, flattening incompatible nested structures instead.
  • Improve mapping between Yjs blocks and Slate elements so type and data changes are reflected without destructive reloads.

Tests:

  • Add Yjs utility tests covering in-place paragraph-to-heading conversion with stable ids and undo/redo behavior.
  • Add tests for nested list conversions that preserve child ids when compatible and flatten children for heading conversions.
  • Add Slate/Yjs integration tests validating remote block type updates by id and ignoring updates for non-rendered blocks.

@sourcery-ai

sourcery-ai Bot commented May 8, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR updates the Yjs block type conversion and Slate sync pipeline so that compatible text block type changes happen in-place (preserving block/text IDs and children), while incompatible conversions continue to replace/flatten blocks, and remote same-id type/data updates are applied to matching Slate nodes with new tests covering these behaviors.

Sequence diagram for applying same-id Yjs block type/data updates to Slate

sequenceDiagram
  participant YjsDoc
  participant translateYEvents
  participant applyUpdateBlockYEvent
  participant SlateEditor as editor

  YjsDoc->>translateYEvents: YMapEvent
  translateYEvents->>applyUpdateBlockYEvent: applyUpdateBlockYEvent
  applyUpdateBlockYEvent->>applyUpdateBlockYEvent: block.get block_type
  applyUpdateBlockYEvent->>applyUpdateBlockYEvent: block.get block_data
  applyUpdateBlockYEvent->>applyUpdateBlockYEvent: dataStringTOJson
  applyUpdateBlockYEvent->>applyUpdateBlockYEvent: findSlateEntryByBlockId
  applyUpdateBlockYEvent->>applyUpdateBlockYEvent: isEqual oldData, newData
  alt type or data changed
    applyUpdateBlockYEvent->>SlateEditor: apply { type: set_node, path, newProperties, properties }
  else no change
    applyUpdateBlockYEvent-->>translateYEvents: []
  end
Loading

File-Level Changes

Change Details Files
Convert compatible text block type changes in-place in Yjs while preserving IDs and children.
  • Introduce helpers to fetch document data, create text blocks with parents, and get required text from Yjs structures for tests.
  • Add ensureTextForBlock to guarantee a text entry exists for a block and canTurnToBlockInPlace to decide when a conversion can reuse the existing block.
  • Update turnToBlock to detect child-compatible targets and mutate the existing block type/data and text metadata instead of creating a new block, returning the original block id.
src/application/slate-yjs/__tests__/yjs-utils.test.ts
src/application/slate-yjs/utils/yjs.ts
Keep incompatible conversions (e.g., nested list to heading) on the replacement/flattening path and verify undo/redo behavior.
  • Add tests that converting nested todo_list to numbered_list preserves parent/child block IDs, text IDs, and parent links through undo/redo.
  • Add tests that converting nested numbered_list to heading flattens children into siblings under the page, and that undo/redo fully restores/ reapplies the nested structure.
  • Ensure the replacement path deletes old parent/child blocks and re-parents flattened list items under the page for non-container targets like headings.
src/application/slate-yjs/__tests__/yjs-utils.test.ts
src/application/slate-yjs/utils/yjs.ts
Apply remote same-id type/data updates from Yjs to the correct Slate node based on block id and ignore non-rendered blocks.
  • Change applyUpdateBlockYEvent to read block type as well as data from Yjs and compute minimal newProperties/properties (type, data) for Slate set_node operations.
  • Skip applying updates when type/data are unchanged to avoid unnecessary operations and logging.
  • Log richer debug information including old/new types and data keys, and treat missing Slate nodes as a non-fatal condition.
  • Add tests to ensure remote type updates target the correct block id regardless of root path and are ignored (with a console error) when the block id is not rendered in Slate.
src/application/slate-yjs/utils/applyToSlate.ts
src/application/slate-yjs/__tests__/applyToSlate.test.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@appflowy appflowy changed the title fix: shared block type fix: preserve block identity during type conversion May 8, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@appflowy appflowy merged commit fae8913 into main Jun 21, 2026
10 of 13 checks passed
@appflowy appflowy deleted the fix/shared_block branch June 21, 2026 06:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant