Skip to content

Conversation

YousefED
Copy link
Collaborator

@YousefED YousefED commented Oct 16, 2025

Summary

Clean types and Partial / full block concepts

Rationale

#2089 (review)

Changes

  • Removes a lot of uses of PartialBlock internally, where it's not necessary
  • Cleans up some of the internal types by removing / not exporting types

Impact

Testing

Should mostly be covered by existing unit tests

  • Manual testing still needed

Screenshots/Video

Checklist

  • Code follows the project's coding standards.
  • Unit tests covering the new feature have been added.
  • All existing tests pass.
  • The documentation has been updated to reflect the new feature

Additional Notes

TODO:

  • manual testing
  • fix remaining unit tests
  • get the build of examples to work.

Follow up:

  • clean more of blockToNode (there's logic there that handles partial content / blocks which is not needed anymore)
  • potentially, we could move the definition of Block and Partial lock to Zod as well, and use a transformer to distinguish between input and output types

Copy link

vercel bot commented Oct 16, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
blocknote Error Error Oct 17, 2025 6:30am
blocknote-website Error Error Oct 17, 2025 6:30am

S extends StyleSchema,
>(
tr: Transaction,
// TBD: allow PartialBlock here?
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think we could move it a layer up to the BlockNoteEditor (and only allow partials there, not in the commands. However, for updateBlock I don't think that's feasible, so maybe better to keep it here

Copy link
Contributor

Choose a reason for hiding this comment

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

These don't get run anywhere but from the editor instance, so imo it doesn't make too much a difference anyway

* @returns an {@link OccupancyGrid}
*/
export function getTableCellOccupancyGrid(
block: BlockFromConfigNoChildren<DefaultBlockSchema["table"], any, any>,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Stopped exporting BlockFromConfigNoChildren so we have more consistency in the codebase

Copy link
Contributor

Choose a reason for hiding this comment

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

While we are cleaning up types, I believe that:

BlockFromConfig<DefaultBlockSchema["table"], any, any>

Should only be:

BlockFromConfig<DefaultBlockSchema["table"]>

Most of the time we don't care about the other parameters, and have to remove their defaults with any. This goes for most instances of B, I, S

(props as any)[name] = spec._zod.def.defaultValue;
}
}
// const props = block.props || {};
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

TODO: this code can now be deleted

StyleSpecs,
import {
partialBlockToFullBlock,
type BlockIdentifier,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fyi, some of the changes in type imports were to fix a circular dependency issue that was breaking the build during development

*/
public blocksToHTMLLossy(
blocks: PartialBlock<BSchema, ISchema, SSchema>[] = this.document,
blocks: Block<BSchema, ISchema, SSchema>[] = this.document,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

breaking API change, should be documented in PR / release notes

}

// Hide handles if the table block has been removed.
this.state.block = this.editor.getBlock(this.state.block.id)!;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

TODO: In the old version we were potentially setting this.state.block to undefined. Now, it will keep the old (unavailable block). can this break anything?

Copy link
Contributor

Choose a reason for hiding this comment

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

It isn't clear to me why this was modified, but it certainly is a change in behavior that would need to be validated

*
*/

export type PartialTableCell<
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

moved to separate table file

children: PartialBlockNoDefaults<BSchema, I, S>[];
}>;

export type SpecificPartialBlock<
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

unused, removed

}
: {
type: "tableCell",
// FIXME: content can actually be Partial, we should probably handle this as well
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This comment is to document a finding Matthew earlier found while writing the partialToFull function

"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.addMissingImports": "explicit"
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fyi, this makes sure missing imports are added automatically on save, I kept finding myself to do this manually (or AI-powered)

@YousefED YousefED changed the title refactor types and partial / full refactor: less use of PartialBlocks and type cleanup Oct 16, 2025
S extends StyleSchema,
>(
tr: Transaction,
// TBD: allow PartialBlock here?
Copy link
Contributor

Choose a reason for hiding this comment

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

These don't get run anywhere but from the editor instance, so imo it doesn't make too much a difference anyway

// for this, we do a nodeToBlock on the existing block to get the children.
// it would be cleaner to use a ReplaceAroundStep, but this is a bit simpler and it's quite an edge case
const existingBlock = nodeToBlock(blockInfo.bnBlock.node, pmSchema);
const newFullBlock = partialBlockToFullBlock(schema, block);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const newFullBlock = partialBlockToFullBlock(schema, block);
const newBlock = partialBlockToFullBlock(schema, block);

There are partial blocks & blocks. Full block gives a third name which is not helpful

Comment on lines 657 to 658
// TODO: what happened here?
return isCellEmpty(cell);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this wrong? It is recursing, what you've written is an infinite loop no?

* @returns an {@link OccupancyGrid}
*/
export function getTableCellOccupancyGrid(
block: BlockFromConfigNoChildren<DefaultBlockSchema["table"], any, any>,
Copy link
Contributor

Choose a reason for hiding this comment

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

While we are cleaning up types, I believe that:

BlockFromConfig<DefaultBlockSchema["table"], any, any>

Should only be:

BlockFromConfig<DefaultBlockSchema["table"]>

Most of the time we don't care about the other parameters, and have to remove their defaults with any. This goes for most instances of B, I, S

const domFragment = serializeInlineContentExternalHTML(
editor,
inlineContent as any,
inlineContent as unknown as never,
Copy link
Contributor

Choose a reason for hiding this comment

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

~~ as never? Why so? ~~
nvm, I see the todo

Comment on lines -329 to -334
let id = block.id;

if (id === undefined) {
id = UniqueID.options.generateID();
}

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm actually ok with this function generating ids if the block doesn't have one. I think this makes the caller's life more difficult to be forced to present an ID

Comment on lines +286 to +291
blockHasType(
block,
this.editor,
"table",
defaultBlockSpecs.table.config.propSchema,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we changing this?

Am I right to assume that this would mean that it can now only match the block if it is both a table & conforming to the propSchema? Why does the propSchema matter here? What if someone extends the table to have additional props, wouldn't this break?

}

// Hide handles if the table block has been removed.
this.state.block = this.editor.getBlock(this.state.block.id)!;
Copy link
Contributor

Choose a reason for hiding this comment

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

It isn't clear to me why this was modified, but it certainly is a change in behavior that would need to be validated

Comment on lines +250 to +251
// TODO: I think this should be a CustomBlockNoteSchema,
// but that breaks docxExporter etc
Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, it should be

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.

2 participants