Skip to content

fix xl-multi-column - #2944

Closed
fulcanellee wants to merge 1 commit into
TypeCellOS:mainfrom
fulcanellee:fix/xl-multi-column
Closed

fix xl-multi-column#2944
fulcanellee wants to merge 1 commit into
TypeCellOS:mainfrom
fulcanellee:fix/xl-multi-column

Conversation

@fulcanellee

@fulcanelleefulcanellee commented Aug 5, 2026

Copy link
Copy Markdown

Fix#2943

Summary by CodeRabbit

  • New Features

    • Improved drag-and-drop support for placing blocks beside columns.
    • Blocks dropped onto other blocks are now arranged in a new two-column layout.
    • Column order and surrounding content are preserved during moves.
    • Empty columns are automatically removed after moving content.
  • Bug Fixes

    • Ignored drops where the dragged item and target are the same.
  • Tests

    • Added coverage for column moves, block wrapping, ordering, and empty-column handling.

@vercel

vercelBot commented Aug 5, 2026

Copy link
Copy Markdown

@fulcanellee is attempting to deploy a commit to the TypeCell Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitaiBot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The multi-column drop plugin now delegates to reusable handlers. A shared utility computes column-list children, removes emptied columns, and preserves drop order. Tests cover column moves, block wrapping, insertion order, and source-column handling.

Changes

Multi-column drop behavior

Layer / File(s)Summary
Compute column-list children
packages/xl-multi-column/src/extensions/DropCursor/util/computeColumnListChildren.ts, packages/xl-multi-column/src/extensions/DropCursor/util/computeColumnListChildren.test.ts
The utility removes dragged items, filters empty columns, and inserts a new column beside the target. Tests cover nested blocks, dragged columns, and left/right placement.
Centralize drop handlers
packages/xl-multi-column/src/extensions/DropCursor/dropHandlers.ts, packages/xl-multi-column/src/test/dropCursor/dropHandlers.test.ts
The handlers process column and block drops, ignore self-drops, update documents in transactions, and preserve column order.
Delegate plugin drop processing
packages/xl-multi-column/src/extensions/DropCursor/multiColumnHandleDropPlugin.ts
The plugin calls dropOntoColumn and dropOntoBlock instead of using inline drop logic.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
participant User
participant multiColumnHandleDropPlugin
participant dropOntoColumn
participant computeColumnListChildrenAfterDrop
participant Editor
User->>multiColumnHandleDropPlugin: Drop block beside column or block
multiColumnHandleDropPlugin->>dropOntoColumn: Pass column drop data
dropOntoColumn->>computeColumnListChildrenAfterDrop: Compute updated children
computeColumnListChildrenAfterDrop-->>dropOntoColumn: Return reordered columns
dropOntoColumn->>Editor: Apply transaction
Editor-->>User: Show updated block placement
Loading

Possibly related PRs

  • TypeCellOS/BlockNote#2550: Introduced the multi-column drop-handling logic that this change extracts into handlers and utilities.

Poem

A rabbit drags a block through the columns so neat,
The handler places it beside its new seat.
Empty nests vanish, the order stays bright,
Two blocks wrap together, left or right.
Tests thump their paws: the drop now lands right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Description check⚠️ WarningThe description only references issue #2943 and omits nearly all required template sections and testing details.Add the summary, rationale, changes, impact, testing, checklist, and any relevant screenshots or additional notes.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title identifies the affected package and relates to the drag-and-drop fix, but it does not state the specific defect.
Linked Issues check✅ PassedThe new drop handlers, child computation, and tests address correct block placement and prevent dragged blocks from disappearing for issue #2943.
Out of Scope Changes check✅ PassedAll reported code and test changes directly support the multi-column drag-and-drop fix and issue #2943.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/xl-multi-column/src/test/dropCursor/dropHandlers.test.ts (1)

36-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the resulting structure, not only the absence of a throw.

The fixed bug causes both a console error and a misplaced block. These two tests assert not.toThrow() and a snapshot. A snapshot change is easy to accept without review. Add an explicit assertion for the resulting column order and column contents, in the style of Lines 152-156.

♻️ Example assertion for the left-edge case
 ).not.toThrow();
+ const updated = getEditor().getBlock("column-list-solo")!;+ expect(updated.children.map((c) => c.id)).toEqual([+ expect.any(String),+ "column-sibling",+ ]);+ expect(updated.children[0].children.map((b) => b.id)).toEqual(["solo"]);
expect(getEditor().document).toMatchSnapshot();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/xl-multi-column/src/test/dropCursor/dropHandlers.test.ts` around
lines 36 - 66, Strengthen the left- and right-edge tests around dropOntoColumn
by explicitly asserting the resulting column order and each column’s contents,
following the assertion style used around lines 152-156. Keep the existing
no-throw checks and snapshots, but ensure both cases verify the dragged block is
placed in the correct target position rather than relying on snapshot updates
alone.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/xl-multi-column/src/extensions/DropCursor/dropHandlers.ts`:
- Around line 75-79: In dropOntoBlock, update the first removeAndInsertBlocks
call that removes draggedBlock.id to pass fixColumns: false, preventing column
cleanup before the subsequent operation wraps targetBlock.id. Leave the later
insertion behavior unchanged.
In
`@packages/xl-multi-column/src/extensions/DropCursor/util/computeColumnListChildren.ts`:
- Around line 39-55: Handle a missing target column in the list-building logic
around targetIndex and the return from computeColumnListChildren: when findIndex
returns -1 because removal emptied the target column, use an explicit fallback
insertion position that preserves the intended drop location instead of passing
-1 or 0 to toSpliced. Keep the existing left/right positioning behavior when
targetIndex is found.
---
Nitpick comments:
In `@packages/xl-multi-column/src/test/dropCursor/dropHandlers.test.ts`:
- Around line 36-66: Strengthen the left- and right-edge tests around
dropOntoColumn by explicitly asserting the resulting column order and each
column’s contents, following the assertion style used around lines 152-156. Keep
the existing no-throw checks and snapshots, but ensure both cases verify the
dragged block is placed in the correct target position rather than relying on
snapshot updates alone.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: dd985aef-cc65-47d5-b447-d916ddd207b5

📥 Commits

Reviewing files that changed from the base of the PR and between 036d0f3 and e610449.

⛔ Files ignored due to path filters (1)
  • packages/xl-multi-column/src/test/dropCursor/__snapshots__/dropHandlers.test.ts.snap is excluded by !**/*.snap, !**/__snapshots__/**
📒 Files selected for processing (5)
  • packages/xl-multi-column/src/extensions/DropCursor/dropHandlers.ts
  • packages/xl-multi-column/src/extensions/DropCursor/multiColumnHandleDropPlugin.ts
  • packages/xl-multi-column/src/extensions/DropCursor/util/computeColumnListChildren.test.ts
  • packages/xl-multi-column/src/extensions/DropCursor/util/computeColumnListChildren.ts
  • packages/xl-multi-column/src/test/dropCursor/dropHandlers.test.ts

Comment on lines +75 to +79
editor.transact((tr) => {
if (getNodeById(draggedBlock.id, tr.doc)) {
removeAndInsertBlocks(tr, [draggedBlock.id], []);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash# Inspect removeAndInsertBlocks and the fixColumns option semantics.
fd -t f 'removeAndInsertBlocks*' -E node_modules |while IFS= read -r f;doecho"== $f"; ast-grep outline "$f" --items all
done
rg -n --type=ts -C 6 'fixColumns' -g '!**/node_modules/**'
rg -n --type=ts -C 4 'function (fixColumnList|fixColumns)' -g '!**/node_modules/**'

Repository: TypeCellOS/BlockNote

Length of output: 244


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo"== files matching dropHandlers / multi-column =="
fd -i 'dropHandlers\.ts|multi.*column|column' packages | head -100
echo"== line count for target =="
fd -t f 'dropHandlers\.ts' packages -x sh -c 'echo "--- $1"; wc -l "$1"' sh {}
echo"== exact target excerpt =="
target="$(fd -t f 'dropHandlers\.ts' packages | head -1)"if [ -n"${target:-}" ];then
sed -n '1,140p'"$target"| cat -n
fiecho"== removeAndInsertBlocks source =="
rg -n --type=ts -C 8 'removeAndInsertBlocks|getNodeById|getNodes' packages | head -200

Repository: TypeCellOS/BlockNote

Length of output: 24577


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo"== core replaceBlocks util =="
fd -t f 'replaceBlocks.*|fixColumnList.*|blockManipulation' packages/core/src packages -x sh -c 'echo "--- $1"; wc -l "$1"' sh {} | rg -n 'replaceBlocks|fixColumnList|blockManipulation'||trueecho"== removeAndInsertBlocks source candidates =="
fd -t f . packages/core packages/xl-multi-column | rg 'replaceBlocks|fixColumn|manipulation'| xargs -r sed -n '1,220p'| nl -ba | sed -n '1,360p'echo"== core export/import locations for removeAndInsertBlocks =="
rg -n --type=ts -C 3 'removeAndInsertBlocks|export \{|export \*|blockManipulation' packages/core/src packages/xl-multi-column/src | head -240

Repository: TypeCellOS/BlockNote

Length of output: 3419


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo"== packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts =="
sed -n '1,180p' packages/core/src/api/blockManipulation/commands/replaceBlocks/replaceBlocks.ts | cat -n
echo"== packages/core/src/api/blockManipulation/commands/replaceBlocks/util/fixColumnList.ts =="
sed -n '1,220p' packages/core/src/api/blockManipulation/commands/replaceBlocks/util/fixColumnList.ts | cat -n
echo"== relevant tests mentioning column collapsing removal order =="
rg -n --type=ts -C 8 'fixColumns|last.*column|columnList|removeAndInsertBlocks' packages/core/src/api/blockManipulation/commands/replaceBlocks packages/xl-multi-column/src/test/commands packages/xl-multi-column/src/test/commands/util | sed -n '1,260p'

Repository: TypeCellOS/BlockNote

Length of output: 43034


Disable column collapsing before wrapping the target block.

removeAndInsertBlocks default-enables fixColumns; removing draggedBlock.id can collapse its empty columnList before the second call tries to wrap targetBlock.id. Pass fixColumns: false to the first removal in dropOntoBlock so the target block remains present for the subsequent insert.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/xl-multi-column/src/extensions/DropCursor/dropHandlers.ts` around
lines 75 - 79, In dropOntoBlock, update the first removeAndInsertBlocks call
that removes draggedBlock.id to pass fixColumns: false, preventing column
cleanup before the subsequent operation wraps targetBlock.id. Leave the later
insertion behavior unchanged.

Comment on lines +39 to +55
const targetIndex = withDraggedRemoved.findIndex(
(column) => column.id === targetColumnId,
);

const newColumn = {
type: "column" as const,
children: draggedIsColumn ? draggedBlock.children : [draggedBlock],
props: {},
content: undefined,
id: UniqueID.options.generateID(),
};

return withDraggedRemoved.toSpliced(
position === "left" ? targetIndex : targetIndex + 1,
0,
newColumn,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle targetIndex === -1 when the target column is emptied by the removal.

If the dragged block is the only child of the target column, the filter at Line 37 removes the target column. findIndex then returns -1. toSpliced(-1, 0, ...) inserts before the last element, and toSpliced(0, 0, ...) for "right" inserts at the head. Both place the new column at a wrong index. dropOntoColumn only guards targetColumnId === draggedBlock.id, so a block drag onto its own single-block column reaches this path.

Add an explicit fallback.

🐛 Proposed fallback for a missing target column
 const targetIndex = withDraggedRemoved.findIndex(
(column) => column.id === targetColumnId,
);
const newColumn = {
type: "column" as const,
children: draggedIsColumn ? draggedBlock.children : [draggedBlock],
props: {},
content: undefined,
id: UniqueID.options.generateID(),
};
+ if (targetIndex === -1) {+ // The target column was emptied by the removal, so append the new column.+ return [...withDraggedRemoved, newColumn];+ }+
return withDraggedRemoved.toSpliced(
position === "left" ? targetIndex : targetIndex + 1,
0,
newColumn,
);
}
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
consttargetIndex=withDraggedRemoved.findIndex(
(column)=>column.id===targetColumnId,
);
constnewColumn={
type: "column"asconst,
children: draggedIsColumn ? draggedBlock.children : [draggedBlock],
props: {},
content: undefined,
id: UniqueID.options.generateID(),
};
returnwithDraggedRemoved.toSpliced(
position==="left" ? targetIndex : targetIndex+1,
0,
newColumn,
);
consttargetIndex=withDraggedRemoved.findIndex(
(column)=>column.id===targetColumnId,
);
constnewColumn={
type: "column"asconst,
children: draggedIsColumn ? draggedBlock.children : [draggedBlock],
props: {},
content: undefined,
id: UniqueID.options.generateID(),
};
if(targetIndex===-1){
// The target column was emptied by the removal, so append the new column.
return[...withDraggedRemoved,newColumn];
}
returnwithDraggedRemoved.toSpliced(
position==="left" ? targetIndex : targetIndex+1,
0,
newColumn,
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@packages/xl-multi-column/src/extensions/DropCursor/util/computeColumnListChildren.ts`
around lines 39 - 55, Handle a missing target column in the list-building logic
around targetIndex and the return from computeColumnListChildren: when findIndex
returns -1 because removal emptied the target column, use an explicit fallback
insertion position that preserves the intended drop location instead of passing
-1 or 0 to toSpliced. Keep the existing left/right positioning behavior when
targetIndex is found.

@fulcanellee

Copy link
Copy Markdown
Author

#2885

Sign up for freeto 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.

Error when dragging blocks from @blocknote/xl-multi-column

1 participant

@fulcanellee