Repository navigation
Add vendor migrations to blocks - #6401
Merged
Merged
Conversation
A block's version is a single counter, so only one party can advance it. A block that receives migrations from the library providing it as well as from the application using it therefore has to share the counter, which breaks as soon as both sides add a migration. Migrations shipped with a block are declared with vendorVersion and vendorMigrations and count independently in $$vendorVersion. They run before the block's own migrations, which keep using version, migrations and $$version unchanged. The DraftJS migration of createTipTapRichTextBlock becomes such a vendor migration, so it no longer occupies version 1 of the block. Content saved with the previous numbering is split into the two counters when it is loaded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ByXXwcnSeVYzQfLujMknXH
The migrations of ExternalLinkBlock, YouTubeVideoBlock and DamVideoBlock belong to Dextinity, not to the application using them, so they count in $$vendorVersion from now on and leave the block's own version free. Block instances saved before the move know only $$version, which carries both chains. legacyVendorVersions states how many of those versions belong to the vendor chain, so the counter is split up when such an instance is loaded and no migration runs twice. The DraftJS migration of createTipTapRichTextBlock uses the same mechanism instead of its own helper. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ByXXwcnSeVYzQfLujMknXH
Vendor migrations lived in the same migrate object the application fills, so a library adding its migrations had to merge them into the application's object, where either side can overwrite the other's. migrateVendor is a sibling of migrate instead, with its own version, migrations and legacyVersions. Neither side sees the other's option, so the block's own migrations can't drop the ones shipped with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ByXXwcnSeVYzQfLujMknXH
An application can extend a block it doesn't own by extending its data and input classes. The extending block declared no vendor migrations, so it never ran them - while the version stamp it inherited from the extended block claimed it had, leaving content marked as migrated that never was. The vendor migrations therefore travel with the BlockData class: a block extending it runs them before its own migrations, and its own migrations count from 1. The data and input classes of the blocks that ship vendor migrations are exported, so an application can extend them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ByXXwcnSeVYzQfLujMknXH
…ng them A block extending another block needs the classes that block is built from. Exporting them puts two names into the package's API for every block that may be extended, although the block itself is the thing applications already have. A block therefore carries them as blockDataClass and blockInputClass. The classes stay unexported, and the blocks shipped with vendor migrations are extended through their block. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ByXXwcnSeVYzQfLujMknXH
Reaching a block's classes through the block put two more properties on every block, for a case the blocks shipped with Dextinity don't have to serve. A block extending another block's data still inherits its vendor migrations, which is what keeps both chains correct. Which blocks expose the classes to extend is left to the library providing them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ByXXwcnSeVYzQfLujMknXH
nsams
commented
Sep 18, 2026
Migrations of a block with migrateFromDraftJs are numbered like any other block's now, so the block's page has nothing to add to the migrations page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ByXXwcnSeVYzQfLujMknXH
nsams
marked this pull request as ready for review
September 18, 2026 09:58
Member
Author
Applications write the migrate options inline and never name their types, so exporting them only grows the package's API. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ByXXwcnSeVYzQfLujMknXH
Contributor
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ByXXwcnSeVYzQfLujMknXH
…ject Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ByXXwcnSeVYzQfLujMknXH
A block extending a block with vendor migrations replaced the inherited chain when it declared vendor migrations of its own. Both count in $$vendorVersion, so only one of them can be expressed there, and the block ran the wrong one while stamping the counter as if it had run the other. Supporting both would mean a counter per library. The vendor migrations of a block come from one library instead, and a second chain is refused where it is declared. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ByXXwcnSeVYzQfLujMknXH
Member
Author
|
@greptileai review |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ByXXwcnSeVYzQfLujMknXH
VPS-Obi
reviewed
Sep 21, 2026
VPS-Obi
previously approved these changes
Sep 21, 2026
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ByXXwcnSeVYzQfLujMknXH
VPS-Obi
self-requested a review
September 21, 2026 09:02
VPS-Obi
approved these changes
Sep 21, 2026
VPS-Obi
pushed a commit
that referenced
this pull request
Sep 23, 2026
…e textBlocks (#6382) The text block type select could only offer a fixed paragraph entry plus every enabled heading level, in a fixed order. A design that wants two entries for the same tag — a display headline above a regular heading 1 — or a different order could not be configured. `textBlocks` configures the entries explicitly and decouples a text block from the tag it is stored as: ```tsx createTipTapRichTextBlock({ textBlocks: [ { name: "paragraph", label: "Paragraph", tag: "p" }, { name: "display", label: "Display", tag: "h1" }, { name: "heading-1", label: "Heading 1", tag: "h1" }, { name: "heading-2", label: "Heading 2", tag: "h2" }, ], defaultTextBlock: "paragraph", }); ``` Two entries on the same tag only stay apart if the content records which one the editor picked, so paragraphs and headings are stored as a single `textBlock` node that names its entry. The tag is not stored at all — it follows from the configuration. The name is therefore the only thing content carries, and a node's type, its level and its text block can no longer drift apart. The site renders by that name instead of by heading level: ```tsx const nodeMapping: Record<string, TipTapNodeHandler> = { textBlock: ({ node, children }) => <Typography variant={textBlockToVariant[node.attrs?.textBlock]}>{children}</Typography>, }; ``` Content written before this change — `paragraph` and `heading` nodes carrying a `level` — is converted when the block is loaded, by a vendor migration (#6401). A project needs no migration of its own. Its own migrations run after Dextinity's and therefore see the converted nodes, so one written against `{ type: "heading", attrs: { level } }` has to match `{ type: "textBlock", attrs: { textBlock } }` instead. Because every text block is the same node now, a list item's content expression can no longer keep a heading out of it. The editor takes the content out of the list when a heading is chosen there, turns a heading back into a paragraph when it is wrapped in a list, and the API rejects a heading text block inside a list item. First of four stacked pull requests: #6383 moves the styles into the text blocks, #6384 adds `defaultStyle`, #6385 splits the block's Storybook stories by topic. ## Example `createTipTapRichTextBlock.test.ts`/`.test.tsx` cover the configuration, the stored format and what a list item accepts, `buildTextBlockNodeMigration.test.ts` the conversion. In Storybook: - [Heading Levels](https://tiptap-text-blocks--69df3371c46abe69b5199825.chromatic.com/?path=/story/blocks-tiptaprichtextblock--heading-levels) - [Heading Only](https://tiptap-text-blocks--69df3371c46abe69b5199825.chromatic.com/?path=/story/blocks-tiptaprichtextblock--heading-only) - [Heading Only With Text Block Styles](https://tiptap-text-blocks--69df3371c46abe69b5199825.chromatic.com/?path=/story/blocks-tiptaprichtextblock--heading-only-with-text-block-styles) - [List Text Block](https://tiptap-text-blocks--69df3371c46abe69b5199825.chromatic.com/?path=/story/blocks-tiptaprichtextblock--list-text-block) Session: https://claude.ai/code/session_f3c6ad37-c68b-4187-85b6-2abe5aa95c35 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An alternative to #6390, solving the same problem with a smaller API.
A block's
versionis currently a single counter, so only one party can advance it. That works while an application owns all of a block's migrations, but not when the library providing the block ships migrations as well.Examples where this is a problem:
createTipTapRichTextBlockwithmigrateFromDraftJs(currently we workaround the limitation by letting the vendor use version 1 and application migrations must start at 2)Proposed solution
Add a second
migrateVendor, a sibling of themigrateoption the application fills. They count from 1 in$$vendorVersion, independently of the block'sversion, and run before the block's own migrations.migratestays exactly as it is, and because the two options are separate, a library never merges its migrations into the application's object and neither side can overwrite the other's.example (in real world this would be split in library and project):
naming
I decided to name the new counter "vendor", because it is a migration from a 3rd party vendor. Other option would be "library". A single vendor counter is enough, any other package providing blocks should also use vendor migrations.
Further information
Differences to #6390:
scopesrecord inside the application's.migratekeeps its shape, and$$versionits meaning — only$$vendorVersionis added next to it.Note: extending eg. Youtube block is currently not possible because Data and Input classes are not exported.
Session: https://claude.ai/code/session_01ByXXwcnSeVYzQfLujMknXH