Skip to content

fix xl-multi-column - #2944

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

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

Conversation

@fulcanellee

@fulcanellee fulcanellee 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

vercel Bot 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

coderabbitai Bot 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 name Status Explanation Resolution
Description check ⚠️ Warning The 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 name Status Explanation
Title check ✅ Passed The title identifies the affected package and relates to the drag-and-drop fix, but it does not state the specific defect.
Linked Issues check ✅ Passed The 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 ✅ Passed All reported code and test changes directly support the multi-column drag-and-drop fix and issue #2943.
Docstring Coverage ✅ Passed No 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.

@coderabbitai coderabbitai Bot 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; do
  echo "== $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/bash
set -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
fi

echo "== removeAndInsertBlocks source =="
rg -n --type=ts -C 8 'removeAndInsertBlocks|getNodeById|getNodes' packages | head -200

Repository: TypeCellOS/BlockNote

Length of output: 24577


🏁 Script executed:

#!/bin/bash
set -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' || true

echo "== 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/bash
set -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
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,
);
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,
);
🤖 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.

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.

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

1 participant