Skip to content

[WIKI-498] [WIKI-567] feat: ability to rearrange columns and rows in table#7624

Merged
sriramveeraghanta merged 7 commits intopreviewfrom
feat/table-entity-rearrrange
Aug 22, 2025
Merged

[WIKI-498] [WIKI-567] feat: ability to rearrange columns and rows in table#7624
sriramveeraghanta merged 7 commits intopreviewfrom
feat/table-entity-rearrrange

Conversation

@aaryan610
Copy link
Member

@aaryan610 aaryan610 commented Aug 22, 2025

Description

This PR brings the ability to rearrange rows and columns in a table in the editor. Along with this, users will now witness a more elegant row and column options dropdown with more options that before, including-

  1. Clear content.
  2. Duplicate.
  3. Better background colors.

Type of Change

  • Feature (non-breaking change which adds functionality)

Media

Screen.Recording.2025-08-22.at.14.06.00.mov

Summary by CodeRabbit

  • New Features

    • Drag handles for table rows and columns with drag-and-drop reordering, live drag previews, duplication, and contextual action menus (insert, duplicate, delete, clear, header toggles).
    • Background color picker for table cells.
    • Clear selected cells command; table header cells now accept block-level content.
    • Ability to hide cell/header content during drag previews.
  • Style

    • Hover-revealed handle buttons, new drag/drop visual markers, and hidden-content styling during previews.
  • Chores

    • Removed legacy table hover highlight and triple-click line-selection behavior.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 22, 2025

Walkthrough

Adds comprehensive table drag-and-drop: new row/column drag-handle UI, plugins, DOM markers, preview utilities, move/duplicate actions, color selector, hideContent cell/header attribute and CSS, helpers for table geometry/selection, clearSelectedCells command, and removes legacy tableControls plugin.

Changes

Cohort / File(s) Summary
Core meta constants
packages/editor/src/core/constants/meta.ts
Added CORE_EDITOR_META.INTENTIONAL_DELETION and CORE_EDITOR_META.ADD_TO_HISTORY.
Table drag actions
packages/editor/src/core/extensions/table/plugins/drag-handles/actions.ts
New functions: moveSelectedColumns, moveSelectedRows, duplicateRows, duplicateColumns; converts table ↔ 2D cell arrays and emits transactions.
Column drag-handle UI & plugin
packages/editor/src/core/extensions/table/plugins/drag-handles/column/drag-handle.tsx, .../column/dropdown.tsx, .../column/plugin.ts, .../column/utils.ts
Column handle component, options dropdown, per-column plugin rendering, drop-index calculation, DOM metrics, and preview construction utilities.
Row drag-handle UI & plugin
packages/editor/src/core/extensions/table/plugins/drag-handles/row/drag-handle.tsx, .../row/dropdown.tsx, .../row/plugin.ts, .../row/utils.ts
Row handle component, options dropdown, per-row plugin rendering, drop-index calculation, DOM metrics, and preview construction utilities.
Shared drag preview & marker utilities
packages/editor/src/core/extensions/table/plugins/drag-handles/utils.ts, .../drag-handles/marker-utils.ts
Preview table builder, cloneTableCell, updateCellContentVisibility (uses ADD_TO_HISTORY), marker class constants and helpers to create/update/hide DOM markers.
Insert-handlers integration
packages/editor/src/core/extensions/table/plugins/insert-handlers/plugin.ts, .../insert-handlers/utils.ts
Adds per-table drag-marker container lifecycle; TableInfo gains dragMarkerContainerElement?: HTMLElement; view lifecycle and table reconciliation logic.
Table extension helpers
packages/editor/src/core/extensions/table/table/utilities/helpers.ts
New exports: TableNodeLocation type, findTable, haveTableRelatedChanges, getSelectedRect, getSelectedRows/Columns, selection checks, selectColumn/selectRow, widget positions, table pixel metrics.
Table extension updates
packages/editor/src/core/extensions/table/table/table.ts
Registers row/column drag-handle plugins, removes legacy tableControls, adds clearSelectedCells command, adjusts node view wiring.
Cell/header attributes
packages/editor/src/core/extensions/table/table-cell.ts, .../table-header.ts
Added hideContent attribute (default false) and conditional content-hidden class; TableHeader content changed from paragraph+block+.
Color selector
packages/editor/src/core/extensions/table/plugins/drag-handles/color-selector.tsx
New TableDragHandleDropdownColorSelector React component to set/clear table cell background via editor.updateAttributes.
Legacy controls removal
packages/editor/src/core/extensions/table/table/table-controls.ts
Deleted legacy tableControls plugin (hover highlight, triple-click selection, and related state).
Core drag-handle plugin
packages/editor/src/core/plugins/drag-handle.ts
Excludes .table-drag-preview from selectors; exports `getScrollParent(node: HTMLElement
Styles
packages/editor/src/styles/table.css
Added .content-hidden rules, hover-revealed handles, drag/drop marker styles, widget spacing tweak; selectedCell selector excludes content-hidden.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Handle as Column/Row Drag Handle (React)
  participant Plugin as Drag-Handle Plugin
  participant Utils as DnD Utils & Markers
  participant Actions as Table Actions
  participant PM as ProseMirror Transaction
  participant View as EditorView

  User->>Handle: mousedown (start drag)
  Handle->>Plugin: locate table & selection
  Handle->>Utils: create markers + construct preview
  loop mousemove
    Handle->>Utils: measure DOM, compute drop index
    Utils-->>Handle: dropIndex, preview layout
    Handle->>Utils: update drag & drop markers
  end
  User-->>Handle: mouseup (finish)
  alt order changed
    Handle->>Actions: moveSelectedRows/Columns(...)
    Actions->>PM: build transaction
    PM->>View: dispatch(tr)
  else no change
    Handle->>Utils: hide markers/cleanup
  end
Loading
sequenceDiagram
  autonumber
  actor User
  participant Handle as Handle Dropdown
  participant UI as Color Selector
  participant Editor as TipTap Editor

  User->>Handle: open dropdown
  Handle->>UI: render color swatches
  User->>UI: select/erase color
  UI->>Editor: chain().focus().updateAttributes(TABLE_CELL, { background / null })
  Editor-->>User: cell background updated
  UI->>Handle: onSelect/onClose
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

✨feature

Suggested reviewers

  • Palanikannan1437
  • VipinDevelops
  • sriramveeraghanta

Poem

I nibble cells and hop down rows so fleet,
I drag a column, hum a tiny beat,
Markers shimmer, previews lead the way,
I hide and show each cell for playful play,
A rabbit reorders tables—what a treat 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e6f2e52 and b60d893.

📒 Files selected for processing (1)
  • packages/editor/src/core/extensions/table/plugins/drag-handles/marker-utils.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/editor/src/core/extensions/table/plugins/drag-handles/marker-utils.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Build and lint web apps
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/table-entity-rearrrange

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@makeplane
Copy link

makeplane bot commented Aug 22, 2025

Pull Request Linked with Plane Work Items

Comment Automatically Generated by Plane

// states
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
// floating ui
const { refs, floatingStyles, context } = useFloating({
Copy link
Member

Choose a reason for hiding this comment

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

move floating ui logic to different file.

Copy link
Member

@iam-vipin iam-vipin left a comment

Choose a reason for hiding this comment

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

move floating ui changes in new file.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/editor/src/core/extensions/table/plugins/insert-handlers/utils.ts (1)

278-286: Off-by-one in TableRect boundaries (bottom/right) will skip the last row/column

TableRect.right and .bottom are exclusive bounds in prosemirror-tables. Using width - 1 / height - 1 will cause add/remove operations to miss the last row/column. This can lead to malformed tables (e.g., last row not updated on addColumn).

Fix: set bottom to tableMapData.height and right to tableMapData.width in all rect constructions (insert and remove).

   const rect: TableRect = {
     map: tableMapData,
     tableStart: tablePos,
     table: tableNode,
     top: 0,
     left: 0,
-    bottom: tableMapData.height - 1,
-    right: tableMapData.width - 1,
+    bottom: tableMapData.height,
+    right: tableMapData.width,
   };
   const rect = {
     map: tableMapData,
     tableStart: tablePos,
     table: tableNode,
     top: 0,
     left: 0,
-    bottom: tableMapData.height - 1,
-    right: tableMapData.width - 1,
+    bottom: tableMapData.height,
+    right: tableMapData.width,
   };

Also applies to: 350-358, 310-318, 382-390

packages/editor/src/core/plugins/drag-handle.ts (1)

148-175: Animation frame bookkeeping: remove confusing type-cast to null and dead check

  • getScrollParent no longer returns null; the subsequent null-guard is dead.
  • Casting requestAnimationFrame(...) as unknown as null is misleading; it still returns a number at runtime. Keep the numeric id and cancel it normally.
-    const scrollableParent = getScrollParent(dragHandleElement!);
-    if (!scrollableParent) return;
+    const scrollableParent = getScrollParent(dragHandleElement!);
...
-    scrollAnimationFrame = requestAnimationFrame(scroll) as unknown as null;
+    scrollAnimationFrame = requestAnimationFrame(scroll);
🧹 Nitpick comments (38)
packages/editor/src/core/constants/meta.ts (1)

1-5: Document semantics and expected value types for new meta keys

Please add a short JSDoc above CORE_EDITOR_META explaining:

  • Typical value types (e.g., ADD_TO_HISTORY expects boolean, INTENTIONAL_DELETION likely boolean).
  • Which subsystems read them (history plugin, table-drag-handles), and whether they are consumed or just advisory.

This will help prevent accidental misuse in future contributors’ code.

 export enum CORE_EDITOR_META {
   SKIP_FILE_DELETION = "skipFileDeletion",
+  /**
+   * Indicates a user-initiated delete action (not programmatic cleanup).
+   * Value: boolean
+   * Readers: commands/plugins that customize delete behavior or analytics.
+   */
   INTENTIONAL_DELETION = "intentionalDeletion",
+  /**
+   * Controls whether a transaction is added to the undo/redo history.
+   * Value: boolean
+   * Readers: history plugin integrations (e.g. table drag preview).
+   */
   ADD_TO_HISTORY = "addToHistory",
 }
packages/editor/src/core/extensions/table/plugins/insert-handlers/utils.ts (4)

325-340: Potential cell lookup mismatch using TableMap.map offsets

Using tableNode.nodeAt(cellPos) assumes TableMap.map positions are relative to the table node and directly usable with nodeAt. In prosemirror-tables, offsets are relative to the start of the table, but fetching cells can be tricky with rowspans/colspans.

If you observe false negatives in emptiness checks, consider reading with TableMap and tableNode.child traversal or use existing helpers (e.g., from prosemirror-tables) to locate cells by row/column.

Also applies to: 397-412


34-40: UX: very different action thresholds (150px columns vs 40px rows)

Columns require ~150px drag to trigger while rows require 40px. If intentional, ignore. Otherwise consider:

  • Make thresholds configurable via options.
  • Normalize values, or base on button size and device pixel ratio for predictable UX.
-  const ACTION_THRESHOLD = 150; // pixels total distance to trigger action
+  const ACTION_THRESHOLD = 80; // TODO: tweak or inject via options
-  const ACTION_THRESHOLD = 40; // pixels total distance to trigger action
+  const ACTION_THRESHOLD = 80; // TODO: tweak or inject via options

Also applies to: 136-143


112-118: Cleanup: body cursor style is reset but never set

onMouseUp resets document.body.style.cursor = "" but no code sets it during drag. Either set a cursor during drag (col/row-resize) or remove the reset for clarity.

-      document.body.style.cursor = "";
       document.body.style.userSelect = "";

Also applies to: 210-217


219-261: DOM mapping for tables is fragile; prefer view.domAtPos + nearest table or helpers

The manual walk from domAtPos(pos + 1) to find the TABLE element may fail with wrappers or widgets. If available in your helpers, prefer a robust “findTable” that maps PM node positions to DOM tables using view.nodeDOM(pos) and fallback heuristics, or store a WeakMap from tableNode to its DOM in setup.

packages/editor/src/core/extensions/table/table-header.ts (1)

42-45: Hide-content attribute and inline background style: minor hardening

  • hideContent: Looks good and matches CSS .content-hidden.
  • Background style: Always emitting background-color: ${value}; means “none” becomes an inline style. If you want CSS defaults to apply when unset, only render when value !== "none".
-        class: node.attrs.hideContent ? "content-hidden" : "",
-        style: `background-color: ${node.attrs.background};`,
+        class: node.attrs.hideContent ? "content-hidden" : "",
+        style: node.attrs.background !== "none" ? `background-color: ${node.attrs.background};` : undefined,

Also applies to: 60-62

packages/editor/src/core/plugins/drag-handle.ts (2)

29-31: Type the scroll parent cache and return type for clarity

Explicit generics improve readability and IntelliSense, and ensure callers get an Element back.

-const scrollParentCache = new WeakMap();
+const scrollParentCache = new WeakMap<HTMLElement | SVGElement, Element>();
...
-export const getScrollParent = (node: HTMLElement | SVGElement) => {
+export const getScrollParent = (node: HTMLElement | SVGElement): Element => {

Also applies to: 68-86


132-141: Pointer events would simplify cross-input behavior

The plugin uses HTML5 drag events. Pointer events (pointerdown/move/up + setPointerCapture) can reduce edge cases (e.g., lost events, platform inconsistencies) and unify mouse/touch.

Optional follow-up: migrate to pointer events in a separate PR to minimize risk here.

Also applies to: 210-273

packages/editor/src/styles/table.css (3)

58-73: Hover-to-reveal handles: add a mobile-friendly fallback

On touch devices, :hover doesn’t apply, making the controls effectively invisible. Consider forcing opacity: 1 under (hover: none).

@media (hover: none) {
  .table-wrapper table td .table-col-handle-container > button,
  .table-wrapper table th .table-col-handle-container > button,
  .table-wrapper table td .table-row-handle-container > button,
  .table-wrapper table th .table-row-handle-container > button {
    opacity: 1;
  }
}

95-122: Z-index and marker stacking contexts

Markers use z-index: 10. Ensure they appear above table borders/resize handles (z-index: 5) but below floating UI (menus, tooltips). If conflicts arise, centralize z-indices in variables.


209-271: Insert buttons visibility and interaction

The buttons are pointer-events: none until table hover, which can make access harder on touch devices. Consider the same (hover: none) override to ensure they remain usable on mobile.

packages/editor/src/core/extensions/table/table/table.ts (1)

264-268: Avoid runtime casts by widening TableView typing

Casting decorations as Decoration[] is harmless but avoidable. Accepting readonly Decoration[] in TableView aligns with ProseMirror and removes the need for the cast.

Apply this change in table-view.tsx:

-  constructor(
-    node: ProseMirrorNode,
-    cellMinWidth: number,
-    decorations: Decoration[],
-    editor: Editor,
-    getPos: () => number
-  ) {
+  constructor(
+    node: ProseMirrorNode,
+    cellMinWidth: number,
+    decorations: readonly Decoration[],
+    editor: Editor,
+    getPos: () => number
+  ) {

Then you can drop the as Decoration[] here.

packages/editor/src/core/extensions/table/plugins/drag-handles/color-selector.tsx (2)

15-23: Use null/transparent consistently; avoid writing invalid CSS downstream

You’re correctly clearing with null. Ensure the styling layer (TableCell renderHTML) skips emitting CSS when attrs are null (see related comment). If your palette includes a “None” option, prefer null or transparent over "none" for CSS correctness.

If COLORS_LIST includes a “None” item, update it to use transparent or handle it via null and UI-only label. Verify no code paths set background: "none" or textColor: "none".


86-99: A11y: add accessible names for swatches and the clear button

The color buttons lack accessible labels. Screen readers will announce them as “button.” Add aria-label/title, and consider aria-pressed to reflect selection, if tracked.

Apply this diff to both the swatches and clear button:

-              <button
+              <button
                 key={color.key}
                 type="button"
                 className="flex-shrink-0 size-6 rounded border-[0.5px] border-custom-border-400 hover:opacity-60 transition-opacity"
                 style={{
                   backgroundColor: color.backgroundColor,
                 }}
+                aria-label={`Set background color to ${color.key}`}
+                title={`Set background color to ${color.key}`}
                 onClick={() => {
                   handleBackgroundColorChange(editor, color.backgroundColor);
                   onSelect(color.backgroundColor);
                 }}
               />
@@
-            <button
+            <button
               type="button"
               className="flex-shrink-0 size-6 grid place-items-center rounded text-custom-text-300 border-[0.5px] border-custom-border-400 hover:bg-custom-background-80 transition-colors"
+              aria-label="Clear background color"
+              title="Clear background color"
               onClick={() => {
                 handleBackgroundColorChange(editor, null);
                 onSelect(null);
               }}
             >

Also applies to: 100-110

packages/editor/src/core/extensions/table/plugins/insert-handlers/plugin.ts (2)

77-86: Also update on selection/editable changes to keep markers in sync

updateAllTables() only runs when doc changes. Markers may get out of sync when toggling editability or when tables enter/exit the viewport due to selection/scroll without doc changes. Calling updateAllTables() on any view update is cheap and more robust, or at least when selection changes.

Apply one of these:

  • Minimal change:
-        update(view, prevState) {
-          // Update when document changes
-          if (!prevState.doc.eq(view.state.doc)) {
-            updateAllTables();
-          }
-        },
+        update(view, prevState) {
+          if (
+            !prevState.doc.eq(view.state.doc) ||
+            !prevState.selection.eq(view.state.selection) ||
+            view.editable !== (prevState as any).editable // may not exist; triggers update when toggled
+          ) {
+            updateAllTables();
+          }
+        },
  • Or simplest (safe in practice):
-        update(view, prevState) {
-          // Update when document changes
-          if (!prevState.doc.eq(view.state.doc)) {
-            updateAllTables();
-          }
-        },
+        update() {
+          updateAllTables();
+        },

99-107: Minor: mark marker container as inert to assistive tech

Consider adding aria-hidden="true" and role="presentation" to the container to keep it out of the a11y tree.

Apply this diff:

 const createMarkerContainer = (): HTMLElement => {
   const el = document.createElement("div");
   el.classList.add("table-drag-marker-container");
   el.contentEditable = "false";
+  el.setAttribute("aria-hidden", "true");
+  el.setAttribute("role", "presentation");
   el.appendChild(createDropMarker());
   el.appendChild(createColDragMarker());
   el.appendChild(createRowDragMarker());
   return el;
 };
packages/editor/src/core/extensions/table/plugins/drag-handles/row/dropdown.tsx (3)

32-41: Guard duplicate action when no row selection is active

If a user opens the dropdown without a row CellSelection (e.g., caret selection), getSelectedRows(...) may return an empty array. Depending on duplicateRows’s behavior, this could be a silent no-op. Consider falling back to duplicating the anchor row of the current selection or bailing with a toast. Also remove debug logs in getSelectedRows (helpers.ts) to avoid console noise.

Example minimal hardening:

-import { TableMap } from "@tiptap/pm/tables";
+import { TableMap, getSelectedRect } from "@tiptap/pm/tables";
-import { findTable, getSelectedRows } from "@/extensions/table/table/utilities/helpers";
+import { findTable, getSelectedRows, isCellSelection } from "@/extensions/table/table/utilities/helpers";

       const tableMap = TableMap.get(table.node);
       let tr = editor.state.tr;
-      const selectedRows = getSelectedRows(editor.state.selection, tableMap);
+      let selectedRows = getSelectedRows(editor.state.selection, tableMap);
+      if (selectedRows.length === 0 && isCellSelection(editor.state.selection)) {
+        const rect = getSelectedRect(editor.state.selection, tableMap);
+        selectedRows = [rect.top];
+      }
       tr = duplicateRows(table, selectedRows, tr);
       editor.view.dispatch(tr);

65-79: Header row toggle lacks state feedback

The “Header row” control always renders the same icon and text. Consider reflecting the current header-row state (checked/unchecked or toggled icon) for better UX.


83-97: i18n readiness for labels

Labels like “Insert above”, “Duplicate”, “Clear contents”, etc., are hard-coded. If the editor is localized, route these through your i18n layer.

packages/editor/src/core/extensions/table/plugins/drag-handles/row/drag-handle.tsx (3)

71-90: Open-menu click vs. drag start can conflict

You start drag logic on every onMouseDown (select row, attach global listeners) while also using Floating UI’s click interaction to open the menu. A quick click to open the menu will run drag setup unnecessarily. Consider switching to PointerEvents with a small move-threshold before entering “drag mode,” or only attaching listeners after detecting movement beyond a threshold.


124-147: Preview creation: ensure we only hide content when preview is created

updateCellContentVisibility(editor, true) is called within constructRowDragPreview. If pseudoRow is never created (e.g., markers absent), content remains visible, and handleFinish still attempts to restore it. That’s fine functionally, but a small boolean guard like didHideContent can avoid redundant state updates.


155-197: Minor: z-index layering and pointer-events

Backdrop is zIndex 99 and panel is 100; the handle container is zIndex 20. If table toolbars or other overlays use comparable stacking contexts, consider centralizing z-index tokens to avoid overlap issues. Also, set pointer-events: none on the drag preview element to avoid capturing clicks during drag.

packages/editor/src/core/extensions/table/plugins/drag-handles/column/dropdown.tsx (3)

32-41: Guard duplicate action and remove debug logs in helpers

As with rows, ensure duplicateColumns handles the case where no column CellSelection is active. Consider falling back to the anchor column or warning the user. Also, getSelectedColumns currently logs to console—remove these logs for production.


65-79: Header column toggle could reflect current state

Render an active/checked state if the current selection is a header column to improve affordance.


83-97: i18n readiness for labels

Same i18n note as the row dropdown; route button labels through localization.

packages/editor/src/core/extensions/table/plugins/drag-handles/row/plugin.ts (2)

61-62: Consider ignoreSelection on widget decorations

If the handle shouldn’t be part of selection mapping, pass ignoreSelection: true in the widget spec to reduce unnecessary re-mapping churn during selection changes.

Example:

- decorations.push(Decoration.widget(pos, () => dragHandleComponent.element));
+ decorations.push(Decoration.widget(pos, () => dragHandleComponent.element, { ignoreSelection: true }));

27-31: Initial render timing

Handles are only built when haveTableRelatedChanges is true. If the editor mounts with a table already focused and no immediate transaction occurs, handles may not render until the next selection/doc change. If that’s undesirable, consider forcing an initial build when a table is present and there are no previous decorations.

packages/editor/src/core/extensions/table/plugins/drag-handles/utils.ts (2)

6-9: JSDoc return type doesn’t match implementation

The comment claims HTMLTableElement is returned, but the function returns an object with tableElement and tableBodyElement. Update the doc for correctness.

- * @returns {HTMLTableElement} The pseudo table.
+ * @returns {{ tableElement: HTMLTableElement, tableBodyElement: HTMLTableSectionElement }} The pseudo table and its tbody.

14-21: Preview table should ignore pointer events

To avoid the preview intercepting clicks/hover during drag, set pointer-events: none on the preview table.

   const tableElement = document.createElement("table");
   tableElement.classList.add("table-drag-preview");
   tableElement.classList.add("bg-custom-background-100");
   tableElement.style.opacity = "0.9";
+  tableElement.style.pointerEvents = "none";
packages/editor/src/core/extensions/table/plugins/drag-handles/column/utils.ts (1)

21-80: Code duplication with row utilities - consider refactoring.

The calculateColumnDropIndex function has nearly identical logic to calculateRowDropIndex from the row utilities file, just with different axis names (left/width vs top/height). This violates the DRY principle.

Consider creating a generic drop index calculator that can work for both dimensions:

// In a shared utils file
export const calculateDropIndex = (
  index: number,
  items: Array<{ position: number; size: number }>,
  currentPosition: number,
  axis: 'horizontal' | 'vertical'
): number => {
  // Generic implementation that works for both rows and columns
  // ...
};

// Then in column utils
export const calculateColumnDropIndex = (col: number, columns: TableColumn[], left: number): number => {
  const items = columns.map(c => ({ position: c.left, size: c.width }));
  return calculateDropIndex(col, items, left, 'horizontal');
};

// And in row utils
export const calculateRowDropIndex = (row: number, rows: TableRow[], top: number): number => {
  const items = rows.map(r => ({ position: r.top, size: r.height }));
  return calculateDropIndex(row, items, top, 'vertical');
};
packages/editor/src/core/extensions/table/plugins/drag-handles/column/drag-handle.tsx (1)

133-136: Inefficient drop marker position calculation.

The drop marker position calculation uses a ternary operator that could be simplified for better readability and performance.

-        const dropMarkerLeftPx =
-          dropIndex <= col ? columns[dropIndex].left : columns[dropIndex].left + columns[dropIndex].width;
+        const dropColumn = columns[dropIndex];
+        const dropMarkerLeftPx = dropColumn.left + (dropIndex > col ? dropColumn.width : 0);
packages/editor/src/core/extensions/table/plugins/drag-handles/marker-utils.ts (1)

7-14: Consider null safety for querySelector results.

The marker getter functions return HTMLElement | null but the hide functions don't check for null before accessing classList.

Add null checks to the hide functions:

 export const hideDropMarker = (element: HTMLElement): void => {
+  if (!element) return;
   if (!element.classList.contains("hidden")) {
     element.classList.add("hidden");
   }
 };

 export const hideDragMarker = (element: HTMLElement): void => {
+  if (!element) return;
   if (!element.classList.contains("hidden")) {
     element.classList.add("hidden");
   }
 };

Also applies to: 48-58

packages/editor/src/core/extensions/table/table/utilities/helpers.ts (6)

210-213: Use nodeDOM/closest('table') for robust table sizing

domAtPos(table.start) may resolve into a descendant (or a text node), making parentElement unreliable. Prefer nodeDOM at the table’s pos with a safe fallback to closest('table').

-export const getTableHeightPx = (table: TableNodeLocation, editor: Editor): number => {
-  const dom = editor.view.domAtPos(table.start);
-  return dom.node.parentElement?.offsetHeight ?? 0;
-};
+export const getTableHeightPx = (table: TableNodeLocation, editor: Editor): number => {
+  const tableNode = editor.view.nodeDOM(table.pos) as HTMLElement | null;
+  if (tableNode instanceof HTMLElement) return tableNode.offsetHeight ?? 0;
+  const { node } = editor.view.domAtPos(table.start + 1);
+  const el = (node instanceof Element ? node : node.parentElement)?.closest("table") as HTMLElement | null;
+  return el?.offsetHeight ?? 0;
+};

-export const getTableWidthPx = (table: TableNodeLocation, editor: Editor): number => {
-  const dom = editor.view.domAtPos(table.start);
-  return dom.node.parentElement?.offsetWidth ?? 0;
-};
+export const getTableWidthPx = (table: TableNodeLocation, editor: Editor): number => {
+  const tableNode = editor.view.nodeDOM(table.pos) as HTMLElement | null;
+  if (tableNode instanceof HTMLElement) return tableNode.offsetWidth ?? 0;
+  const { node } = editor.view.domAtPos(table.start + 1);
+  const el = (node instanceof Element ? node : node.parentElement)?.closest("table") as HTMLElement | null;
+  return el?.offsetWidth ?? 0;
+};

Also applies to: 221-224


201-203: Clarify parameter naming to avoid confusion with TableMap.map

Minor readability: rename parameters so map.map[index] reads less awkwardly.

-export const getTableCellWidgetDecorationPos = (table: TableNodeLocation, map: TableMap, index: number): number =>
-  table.start + map.map[index] + 1;
+export const getTableCellWidgetDecorationPos = (
+  table: TableNodeLocation,
+  tableMap: TableMap,
+  mapIndex: number
+): number => table.start + tableMap.map[mapIndex] + 1;

169-176: Guard against out-of-range row/column indices

Add bounds checks to prevent invalid access to TableMap.map when index is out of bounds (e.g., stale UI state).

 export const selectColumn = (table: TableNodeLocation, index: number, tr: Transaction): Transaction => {
-  const { map } = TableMap.get(table.node);
+  const { map, width } = TableMap.get(table.node);
+  if (index < 0 || index >= width) return tr;
   const anchorCell = table.start + map[index];
   const $anchor = tr.doc.resolve(anchorCell);
   return tr.setSelection(CellSelection.colSelection($anchor));
 };

 export const selectRow = (table: TableNodeLocation, index: number, tr: Transaction): Transaction => {
-  const { map, width } = TableMap.get(table.node);
+  const { map, width, height } = TableMap.get(table.node);
+  if (index < 0 || index >= height) return tr;
   const anchorCell = table.start + map[index * width];
   const $anchor = tr.doc.resolve(anchorCell);
   return tr.setSelection(CellSelection.rowSelection($anchor));
 };

Also applies to: 185-192


124-131: Micro-optimization: use a Set for membership checks

For large selections, Set lookup avoids repeated includes() scans.

 export const isRectSelected = (rect: Rect, selection: CellSelection): boolean => {
   const map = TableMap.get(selection.$anchorCell.node(-1));
   const cells = map.cellsInRect(rect);
   const selectedCells = map.cellsInRect(getSelectedRect(selection, map));
-
-  return cells.every((cell) => selectedCells.includes(cell));
+  const selected = new Set(selectedCells);
+  return cells.every((cell) => selected.has(cell));
 };

63-71: Prefer tr.selectionSet for selection changes; optional simplification

You can rely on Transaction.selectionSet instead of comparing new/old selections. Slightly cheaper and clearer.

 ): table is TableNodeLocation =>
-  editor.isEditable && table !== undefined && (tr.docChanged || !newState.selection.eq(oldState.selection));
+  editor.isEditable && table !== undefined && (tr.docChanged || tr.selectionSet);

40-44: Reuse NodeWithPos type instead of redefining TableNodeLocation

findParentNode returns a NodeWithPos (pos, start, depth, node). Reusing that type avoids drift and preserves depth when needed.

-import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
+import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
+import type { NodeWithPos } from "@tiptap/core";
 
-export type TableNodeLocation = {
-  pos: number;
-  start: number;
-  node: ProseMirrorNode;
-};
+export type TableNodeLocation = NodeWithPos<ProseMirrorNode>;
 
 export const findTable = (selection: Selection): TableNodeLocation | undefined =>
   findParentNode((node) => node.type.spec.tableRole === "table")(selection);

Also applies to: 51-53

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d0f26f8 and 1669e98.

📒 Files selected for processing (22)
  • packages/editor/src/core/constants/meta.ts (1 hunks)
  • packages/editor/src/core/extensions/table/plugins/drag-handles/actions.ts (1 hunks)
  • packages/editor/src/core/extensions/table/plugins/drag-handles/color-selector.tsx (1 hunks)
  • packages/editor/src/core/extensions/table/plugins/drag-handles/column/drag-handle.tsx (1 hunks)
  • packages/editor/src/core/extensions/table/plugins/drag-handles/column/dropdown.tsx (1 hunks)
  • packages/editor/src/core/extensions/table/plugins/drag-handles/column/plugin.ts (1 hunks)
  • packages/editor/src/core/extensions/table/plugins/drag-handles/column/utils.ts (1 hunks)
  • packages/editor/src/core/extensions/table/plugins/drag-handles/marker-utils.ts (1 hunks)
  • packages/editor/src/core/extensions/table/plugins/drag-handles/row/drag-handle.tsx (1 hunks)
  • packages/editor/src/core/extensions/table/plugins/drag-handles/row/dropdown.tsx (1 hunks)
  • packages/editor/src/core/extensions/table/plugins/drag-handles/row/plugin.ts (1 hunks)
  • packages/editor/src/core/extensions/table/plugins/drag-handles/row/utils.ts (1 hunks)
  • packages/editor/src/core/extensions/table/plugins/drag-handles/utils.ts (1 hunks)
  • packages/editor/src/core/extensions/table/plugins/insert-handlers/plugin.ts (4 hunks)
  • packages/editor/src/core/extensions/table/plugins/insert-handlers/utils.ts (4 hunks)
  • packages/editor/src/core/extensions/table/table-cell.ts (2 hunks)
  • packages/editor/src/core/extensions/table/table-header.ts (3 hunks)
  • packages/editor/src/core/extensions/table/table/table-controls.ts (0 hunks)
  • packages/editor/src/core/extensions/table/table/table.ts (6 hunks)
  • packages/editor/src/core/extensions/table/table/utilities/helpers.ts (2 hunks)
  • packages/editor/src/core/plugins/drag-handle.ts (4 hunks)
  • packages/editor/src/styles/table.css (3 hunks)
💤 Files with no reviewable changes (1)
  • packages/editor/src/core/extensions/table/table/table-controls.ts
🧰 Additional context used
🧬 Code graph analysis (11)
packages/editor/src/core/extensions/table/plugins/drag-handles/column/plugin.ts (2)
packages/editor/src/core/extensions/table/table/utilities/helpers.ts (3)
  • findTable (51-52)
  • haveTableRelatedChanges (63-70)
  • getTableCellWidgetDecorationPos (201-202)
packages/editor/src/core/extensions/table/plugins/drag-handles/row/plugin.ts (1)
  • decorations (68-70)
packages/editor/src/core/extensions/table/plugins/drag-handles/utils.ts (1)
packages/editor/src/index.ts (1)
  • CORE_EXTENSIONS (24-24)
packages/editor/src/core/extensions/table/plugins/drag-handles/row/dropdown.tsx (1)
packages/editor/src/core/extensions/table/table/utilities/helpers.ts (2)
  • findTable (51-52)
  • getSelectedRows (106-116)
packages/editor/src/core/extensions/table/plugins/drag-handles/column/dropdown.tsx (1)
packages/editor/src/core/extensions/table/table/utilities/helpers.ts (2)
  • findTable (51-52)
  • getSelectedColumns (89-98)
packages/editor/src/core/extensions/table/plugins/drag-handles/row/plugin.ts (2)
packages/editor/src/core/extensions/table/table/utilities/helpers.ts (3)
  • findTable (51-52)
  • haveTableRelatedChanges (63-70)
  • getTableCellWidgetDecorationPos (201-202)
packages/editor/src/core/extensions/table/plugins/drag-handles/row/drag-handle.tsx (2)
  • RowDragHandle (47-199)
  • RowDragHandleProps (42-45)
packages/editor/src/core/extensions/table/plugins/drag-handles/row/drag-handle.tsx (5)
packages/editor/src/core/extensions/table/table/utilities/helpers.ts (5)
  • findTable (51-52)
  • selectRow (185-192)
  • getTableHeightPx (210-213)
  • isCellSelection (13-13)
  • getTableWidthPx (221-224)
packages/editor/src/core/extensions/table/plugins/drag-handles/row/utils.ts (3)
  • getTableRowNodesInfo (88-105)
  • calculateRowDropIndex (21-80)
  • constructRowDragPreview (114-143)
packages/editor/src/core/extensions/table/plugins/drag-handles/marker-utils.ts (7)
  • getDropMarker (7-8)
  • getRowDragMarker (51-52)
  • hideDropMarker (10-14)
  • hideDragMarker (54-58)
  • updateRowDropMarker (32-46)
  • DROP_MARKER_THICKNESS (5-5)
  • updateRowDragMarker (79-96)
packages/editor/src/core/extensions/table/plugins/drag-handles/utils.ts (1)
  • updateCellContentVisibility (49-60)
packages/editor/src/core/extensions/table/plugins/drag-handles/row/dropdown.tsx (1)
  • RowOptionsDropdown (62-100)
packages/editor/src/core/extensions/table/table/utilities/helpers.ts (1)
packages/editor/src/core/extensions/table/table/table-view.tsx (3)
  • selectColumn (471-480)
  • selectRow (482-491)
  • dom (274-276)
packages/editor/src/core/extensions/table/plugins/drag-handles/row/utils.ts (2)
packages/editor/src/core/extensions/table/table/utilities/helpers.ts (3)
  • TableNodeLocation (40-44)
  • isCellSelection (13-13)
  • getSelectedRect (78-81)
packages/editor/src/core/extensions/table/plugins/drag-handles/utils.ts (3)
  • constructDragPreviewTable (10-22)
  • cloneTableCell (29-41)
  • updateCellContentVisibility (49-60)
packages/editor/src/core/extensions/table/plugins/drag-handles/column/utils.ts (2)
packages/editor/src/core/extensions/table/table/utilities/helpers.ts (3)
  • TableNodeLocation (40-44)
  • isCellSelection (13-13)
  • getSelectedRect (78-81)
packages/editor/src/core/extensions/table/plugins/drag-handles/utils.ts (3)
  • constructDragPreviewTable (10-22)
  • cloneTableCell (29-41)
  • updateCellContentVisibility (49-60)
packages/editor/src/core/extensions/table/table/table.ts (4)
packages/editor/src/core/extensions/table/table/table-view.tsx (1)
  • TableView (256-492)
packages/editor/src/core/extensions/table/plugins/drag-handles/column/plugin.ts (2)
  • decorations (70-72)
  • TableColumnDragHandlePlugin (21-74)
packages/editor/src/core/extensions/table/plugins/drag-handles/row/plugin.ts (2)
  • decorations (68-70)
  • TableRowDragHandlePlugin (21-72)
packages/editor/src/core/extensions/table/plugins/selection-outline/plugin.ts (1)
  • decorations (55-57)
packages/editor/src/core/extensions/table/plugins/insert-handlers/plugin.ts (1)
packages/editor/src/core/extensions/table/plugins/drag-handles/marker-utils.ts (3)
  • DROP_MARKER_CLASS (1-1)
  • COL_DRAG_MARKER_CLASS (2-2)
  • ROW_DRAG_MARKER_CLASS (3-3)
🪛 ast-grep (0.38.6)
packages/editor/src/core/extensions/table/plugins/drag-handles/marker-utils.ts

[warning] 74-74: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: element.innerHTML = pseudoColumn.outerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)


[warning] 93-93: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: element.innerHTML = pseudoRow.outerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)


[warning] 74-74: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: element.innerHTML = pseudoColumn.outerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 93-93: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: element.innerHTML = pseudoRow.outerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build and lint web apps
  • GitHub Check: Build and lint web apps
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Build and lint web apps
🔇 Additional comments (19)
packages/editor/src/core/constants/meta.ts (1)

3-4: Good addition: explicit meta keys for deletion and history control

Adding INTENTIONAL_DELETION and ADD_TO_HISTORY clarifies transaction semantics and avoids magic strings scattered across the codebase.

packages/editor/src/core/extensions/table/plugins/insert-handlers/utils.ts (1)

274-289: Append behavior of addColumn/addRow supports passing width/height

The ProseMirror/Tiptap utility functions

addColumn(tr: Transaction, rect: TableRect, index: number): Transaction  
addRow(   tr: Transaction, rect: TableRect, index: number): Transaction  

accept a zero-based insertion index and allow values up to rect.map.width (for columns) or rect.map.height (for rows) to append at the end. There is no “after” boolean parameter—the index itself determines where the new column or row goes. (unpkg.com)

As a result, using

const lastColumnIndex = tableMapData.width;
const newTr = addColumn(tr, rect, lastColumnIndex);
// …
const lastRowIndex = tableMapData.height;
const newTr = addRow(tr, rect, lastRowIndex);

correctly appends a column or row. The original off-by-one suggestion can be ignored.

Likely an incorrect or invalid review comment.

packages/editor/src/core/extensions/table/table-header.ts (1)

20-21: Content model broadened to block+: verify interoperability

Moving TABLE_HEADER content from paragraph+ to block+ aligns with Tiptap defaults and enables richer content, but can affect:

  • Paste/HTML parsing rules expecting a paragraph wrapper.
  • Any commands assuming a single paragraph child.

Please verify downstream commands (clearSelectedCells, duplicate, drag-preview) operate correctly with nested blocks.

packages/editor/src/core/plugins/drag-handle.ts (1)

19-20: Exclude drag preview tables from targeting — good call

Filtering with table:not(.table-drag-preview) prevents accidental interactions with preview nodes.

packages/editor/src/styles/table.css (2)

28-57: Good: selection outline suppressed for content-hidden cells

selectedCell:not(.content-hidden) prevents double-highlighting during drag previews.


78-81: Hidden content via visibility: hidden — confirm caret behavior

Using visibility: hidden preserves layout but also hides the caret/selection rendering inside the cell. Verify that keyboard navigation during drag preview doesn’t trap focus or lead to confusing UX.

packages/editor/src/core/extensions/table/table-cell.ts (1)

50-52: ✅ hideContent is ephemeral and safely cleaned up

I’ve verified that:

  • The updateCellContentVisibility helper explicitly sets CORE_EDITOR_META.ADD_TO_HISTORY to false, so toggling hideContent never pollutes the editor’s undo/redo history.
  • Both column and row drag handles call updateCellContentVisibility(editor, true) at drag start and updateCellContentVisibility(editor, false) at drag end, ensuring all cells are reset to hideContent: false once the interaction finishes.
  • The corresponding .content-hidden CSS rules exist in packages/editor/src/styles/table.css, so the UI behavior is scoped to the drag-preview and doesn’t affect persisted visuals.

No further changes are needed here.

packages/editor/src/core/extensions/table/table/table.ts (2)

180-183: clearSelectedCells command integrates cleanly

Importing deleteCellSelection and exposing it as clearSelectedCells is a good, minimal surface for dropdown “Clear contents.” This should be a no-op when there’s no cell selection and return true when applied.

Also applies to: 10-10


279-281: Plugin wiring looks correct and ordering is sensible

Registering column/row drag-handle plugins alongside TableInsertPlugin and before/after columnResizing follows expected execution order. No issues.

packages/editor/src/core/extensions/table/plugins/drag-handles/column/plugin.ts (1)

34-46: Smart staleness check to avoid unnecessary re-renders

Mapping the previous DecorationSet and verifying one widget per expected position is a solid way to skip rebuilds. Nicely done.

packages/editor/src/core/extensions/table/plugins/drag-handles/utils.ts (1)

49-60: History and focus: looks good

Using ADD_TO_HISTORY = false while toggling hideContent avoids polluting undo history. The double updateAttributes for both cell and header is appropriate.

packages/editor/src/core/extensions/table/plugins/drag-handles/row/utils.ts (1)

107-143: LGTM! Well-structured drag preview construction.

The constructRowDragPreview function properly handles cell selection validation, clones cells safely, and manages content visibility. Good use of the shared utility functions.

packages/editor/src/core/extensions/table/plugins/drag-handles/column/utils.ts (1)

115-143: LGTM! Consistent implementation with row preview.

The constructColumnDragPreview function follows the same pattern as the row preview function, properly handling cell selection and content visibility.

packages/editor/src/core/extensions/table/plugins/drag-handles/column/drag-handle.tsx (1)

175-197: LGTM! Well-implemented floating UI integration.

The dropdown implementation properly uses FloatingUI hooks for positioning and interaction management, with appropriate z-index layering for the overlay and dropdown.

packages/editor/src/core/extensions/table/plugins/drag-handles/actions.ts (1)

154-186: LGTM! Clean helper functions for table manipulation.

The tableToCells and tableFromCells helper functions are well-implemented with proper use of visited cell tracking and Fragment creation.

packages/editor/src/core/extensions/table/plugins/drag-handles/marker-utils.ts (1)

1-6: LGTM! Well-organized constants.

Good separation of concerns with clearly named CSS class constants and drop marker thickness configuration.

packages/editor/src/core/extensions/table/table/utilities/helpers.ts (3)

78-81: LGTM: correct rect computation from CellSelection

Using start(-1) with anchor/head cells and TableMap.rectBetween is the right approach.


138-145: LGTM: column selection detection

Rect spanning full height for a single column and isRectSelected are used correctly.


152-160: LGTM: row selection detection

Rect spanning full width for a single row is correctly validated via isRectSelected.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
packages/editor/src/core/extensions/table/plugins/drag-handles/row/drag-handle.tsx (1)

92-114: Always detach listeners; don’t early-return when markers are missing (prevents leaks and stuck drag state).

handleFinish returns early if dropMarker or dragMarker is falsy, skipping removeEventListener cleanup. This can leave global listeners attached and keep cell content hidden if an error path is taken. Detach listeners first, then hide whichever markers exist. This also resolves the prior review comment.

Apply this focused diff:

-      const handleFinish = (): void => {
-        if (!dropMarker || !dragMarker) return;
-        hideDropMarker(dropMarker);
-        hideDragMarker(dragMarker);
-
-        if (isCellSelection(editor.state.selection)) {
-          updateCellContentVisibility(editor, false);
-        }
-
-        if (row !== dropIndex) {
-          let tr = editor.state.tr;
-          const selection = editor.state.selection;
-          if (isCellSelection(selection)) {
-            const table = findTable(selection);
-            if (table) {
-              tr = moveSelectedRows(editor, table, selection, dropIndex, tr);
-            }
-          }
-          editor.view.dispatch(tr);
-        }
-        window.removeEventListener("mouseup", handleFinish);
-        window.removeEventListener("mousemove", handleMove);
-      };
+      const handleFinish = (): void => {
+        // Always detach listeners
+        window.removeEventListener("mouseup", handleFinish);
+        window.removeEventListener("mousemove", handleMove);
+
+        // Hide markers if present
+        if (dropMarker) hideDropMarker(dropMarker);
+        if (dragMarker) hideDragMarker(dragMarker);
+
+        // Restore content visibility if we hid it during preview
+        if (isCellSelection(editor.state.selection)) {
+          updateCellContentVisibility(editor, false);
+        }
+
+        // Commit move if index changed
+        if (row !== dropIndex) {
+          let tr = editor.state.tr;
+          const selection = editor.state.selection;
+          if (isCellSelection(selection)) {
+            const t = findTable(selection);
+            if (t) {
+              tr = moveSelectedRows(editor, t, selection, dropIndex, tr);
+            }
+          }
+          editor.view.dispatch(tr);
+        }
+      };
packages/editor/src/core/extensions/table/plugins/drag-handles/column/drag-handle.tsx (1)

92-114: Ensure cleanup even when markers are missing; avoid early-return before removing listeners.

Same leak risk as the row handle: handleFinish returns early when markers are null, skipping removeEventListener. Detach first, then conditionally hide markers. This is consistent with prior feedback.

Apply this diff:

-      const handleFinish = () => {
-        if (!dropMarker || !dragMarker) return;
-        hideDropMarker(dropMarker);
-        hideDragMarker(dragMarker);
-        if (isCellSelection(editor.state.selection)) {
-          updateCellContentVisibility(editor, false);
-        }
-        if (col !== dropIndex) {
-          let tr = editor.state.tr;
-          const selection = editor.state.selection;
-          if (isCellSelection(selection)) {
-            const table = findTable(selection);
-            if (table) {
-              tr = moveSelectedColumns(editor, table, selection, dropIndex, tr);
-            }
-          }
-          editor.view.dispatch(tr);
-        }
-        window.removeEventListener("mouseup", handleFinish);
-        window.removeEventListener("mousemove", handleMove);
-      };
+      const handleFinish = () => {
+        // Always detach listeners
+        window.removeEventListener("mouseup", handleFinish);
+        window.removeEventListener("mousemove", handleMove);
+
+        // Hide markers if present
+        if (dropMarker) hideDropMarker(dropMarker);
+        if (dragMarker) hideDragMarker(dragMarker);
+
+        if (isCellSelection(editor.state.selection)) {
+          updateCellContentVisibility(editor, false);
+        }
+        if (col !== dropIndex) {
+          let tr = editor.state.tr;
+          const selection = editor.state.selection;
+          if (isCellSelection(selection)) {
+            const t = findTable(selection);
+            if (t) {
+              tr = moveSelectedColumns(editor, t, selection, dropIndex, tr);
+            }
+          }
+          editor.view.dispatch(tr);
+        }
+      };
packages/editor/src/core/extensions/table/plugins/drag-handles/actions.ts (2)

20-51: Optional guard: validate computed columnStart/columnEnd.

selection is typed as CellSelection, so these are normally set. A light guard improves resilience against edge-cases and future refactors.

Apply right after the forEachCell loop:

   });
 
+  if (columnStart === -1 || columnEnd === -1 || columnStart >= columnEnd) {
+    console.warn("Invalid column selection");
+    return tr;
+  }

62-91: Optional guard: validate computed rowStart/rowEnd.

Same rationale as columns.

   });
 
+  if (rowStart === -1 || rowEnd === -1 || rowStart >= rowEnd) {
+    console.warn("Invalid row selection");
+    return tr;
+  }
🧹 Nitpick comments (5)
packages/editor/src/core/extensions/table/plugins/drag-handles/row/drag-handle.tsx (1)

118-146: Defensive try/catch around move handler to guarantee cleanup on runtime errors.

If an exception occurs inside handleMove (e.g., DOM becomes unavailable during drag), listeners remain until mouseup. Consider catching errors and calling handleFinish() to ensure cleanup.

Apply locally inside handleMove:

-      const handleMove = (moveEvent: MouseEvent): void => {
+      const handleMove = (moveEvent: MouseEvent): void => {
+        try {
           if (!dropMarker || !dragMarker) return;
           // ... existing logic ...
           updateRowDragMarker({
             element: dragMarker,
             top: dragMarkerTopPx,
             height: dragMarkerHeightPx,
             pseudoRow,
           });
-      };
+        } catch (err) {
+          console.error("RowDragHandle move error:", err);
+          handleFinish();
+        }
+      };
packages/editor/src/core/extensions/table/plugins/drag-handles/column/drag-handle.tsx (3)

136-140: Drop marker left offset has inconsistent centering vs row handle (-1 px).

left: dropMarkerLeftPx - Math.floor(DROP_MARKER_THICKNESS / 2) - 1 shifts by an extra pixel compared to the row variant (which uses 0.5 thickness without the extra -1). This can cause visible misalignment on 1x/2x displays. Align both to the same centering math.

Apply this minimal fix:

-          left: dropMarkerLeftPx - Math.floor(DROP_MARKER_THICKNESS / 2) - 1,
+          left: dropMarkerLeftPx - DROP_MARKER_THICKNESS / 2,

118-147: Add defensive error handling in move handler to guarantee cleanup on exceptions.

Mirror the row handle suggestion: wrap handleMove body in try/catch and call handleFinish() on error.

-      const handleMove = (moveEvent: MouseEvent) => {
+      const handleMove = (moveEvent: MouseEvent) => {
+        try {
           if (!dropMarker || !dragMarker) return;
           // ... existing logic ...
           updateColDragMarker({
             element: dragMarker,
             left: dragMarkerLeftPx,
             width: dragMarkerWidthPx,
             pseudoColumn,
           });
-      };
+        } catch (err) {
+          console.error("ColumnDragHandle move error:", err);
+          handleFinish();
+        }
+      };

46-69: Consider extracting shared Floating UI + dropdown patterns into a small hook.

Both column and row handles duplicate the same Floating UI setup and click/dismiss/role integrations. A useHandleDropdown() hook would de-duplicate and simplify both components.

Also applies to: 70-156

packages/editor/src/core/extensions/table/plugins/drag-handles/actions.ts (1)

135-162: Duplicate columns: add optional normalization to indices.

Current validation checks bounds but not duplicates/order. Normalizing indices (unique + ascending) avoids double-inserting the same column and makes behavior predictable.

-  // Validate column indices
-  if (columnIndices.some((idx) => idx < 0 || idx >= width)) {
+  // Validate column indices
+  if (!columnIndices.length || columnIndices.some((idx) => idx < 0 || idx >= width)) {
     console.warn("Invalid column indices for duplication");
     return tr;
   }
 
   const mapStart = tr.mapping.maps.length;
 
+  // Normalize once to avoid double-duplication
+  const indices = Array.from(new Set(columnIndices)).sort((a, b) => a - b);
+
   for (let row = 0; row < height; row++) {
-    const lastColumnPos = map[row * width + columnIndices[columnIndices.length - 1]];
+    const lastColumnPos = map[row * width + indices[indices.length - 1]];
     const nextColumnStart = lastColumnPos + (table.node.nodeAt(lastColumnPos)?.nodeSize ?? 0);
     const insertPos = tr.mapping.slice(mapStart).map(table.start + nextColumnStart);
 
-    for (let i = columnIndices.length - 1; i >= 0; i--) {
-      const copiedNode = rows[row][columnIndices[i]];
+    for (let i = indices.length - 1; i >= 0; i--) {
+      const copiedNode = rows[row][indices[i]];
       if (copiedNode !== null) {
         tr.insert(insertPos, copiedNode);
       }
     }
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1669e98 and fe07153.

📒 Files selected for processing (7)
  • packages/editor/src/core/extensions/table/plugins/drag-handles/actions.ts (1 hunks)
  • packages/editor/src/core/extensions/table/plugins/drag-handles/color-selector.tsx (1 hunks)
  • packages/editor/src/core/extensions/table/plugins/drag-handles/column/drag-handle.tsx (1 hunks)
  • packages/editor/src/core/extensions/table/plugins/drag-handles/row/drag-handle.tsx (1 hunks)
  • packages/editor/src/core/extensions/table/plugins/drag-handles/utils.ts (1 hunks)
  • packages/editor/src/core/extensions/table/plugins/insert-handlers/plugin.ts (4 hunks)
  • packages/editor/src/core/extensions/table/table/utilities/helpers.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/editor/src/core/extensions/table/plugins/drag-handles/utils.ts
  • packages/editor/src/core/extensions/table/plugins/insert-handlers/plugin.ts
  • packages/editor/src/core/extensions/table/plugins/drag-handles/color-selector.tsx
  • packages/editor/src/core/extensions/table/table/utilities/helpers.ts
🧰 Additional context used
🧬 Code graph analysis (3)
packages/editor/src/core/extensions/table/plugins/drag-handles/column/drag-handle.tsx (6)
packages/editor/src/core/extensions/table/table/utilities/helpers.ts (5)
  • findTable (51-52)
  • selectColumn (164-171)
  • getTableWidthPx (216-219)
  • isCellSelection (13-13)
  • getTableHeightPx (205-208)
packages/editor/src/core/extensions/table/plugins/drag-handles/column/utils.ts (3)
  • getTableColumnNodesInfo (88-106)
  • calculateColumnDropIndex (21-80)
  • constructColumnDragPreview (115-143)
packages/editor/src/core/extensions/table/plugins/drag-handles/marker-utils.ts (7)
  • getDropMarker (7-8)
  • getColDragMarker (48-49)
  • hideDropMarker (10-14)
  • hideDragMarker (54-58)
  • updateColDropMarker (16-30)
  • DROP_MARKER_THICKNESS (5-5)
  • updateColDragMarker (60-77)
packages/editor/src/core/extensions/table/plugins/drag-handles/utils.ts (1)
  • updateCellContentVisibility (49-60)
packages/editor/src/core/extensions/table/plugins/drag-handles/actions.ts (1)
  • moveSelectedColumns (20-51)
packages/editor/src/core/extensions/table/plugins/drag-handles/column/dropdown.tsx (1)
  • ColumnOptionsDropdown (62-100)
packages/editor/src/core/extensions/table/plugins/drag-handles/actions.ts (1)
packages/editor/src/core/extensions/table/table/utilities/helpers.ts (1)
  • TableNodeLocation (40-44)
packages/editor/src/core/extensions/table/plugins/drag-handles/row/drag-handle.tsx (6)
packages/editor/src/core/extensions/table/table/utilities/helpers.ts (5)
  • findTable (51-52)
  • selectRow (180-187)
  • getTableHeightPx (205-208)
  • isCellSelection (13-13)
  • getTableWidthPx (216-219)
packages/editor/src/core/extensions/table/plugins/drag-handles/row/utils.ts (3)
  • getTableRowNodesInfo (88-105)
  • calculateRowDropIndex (21-80)
  • constructRowDragPreview (114-143)
packages/editor/src/core/extensions/table/plugins/drag-handles/marker-utils.ts (7)
  • getDropMarker (7-8)
  • getRowDragMarker (51-52)
  • hideDropMarker (10-14)
  • hideDragMarker (54-58)
  • updateRowDropMarker (32-46)
  • DROP_MARKER_THICKNESS (5-5)
  • updateRowDragMarker (79-96)
packages/editor/src/core/extensions/table/plugins/drag-handles/utils.ts (1)
  • updateCellContentVisibility (49-60)
packages/editor/src/core/extensions/table/plugins/drag-handles/actions.ts (1)
  • moveSelectedRows (62-91)
packages/editor/src/core/extensions/table/plugins/drag-handles/row/dropdown.tsx (1)
  • RowOptionsDropdown (62-100)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build and lint web apps
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (3)
packages/editor/src/core/extensions/table/plugins/drag-handles/row/drag-handle.tsx (1)

160-201: Floating UI integration and dropdown wiring look good.

The portal/overlay usage, z-index layering, and interaction hooks are set up cleanly and match ColumnHandle behavior.

packages/editor/src/core/extensions/table/plugins/drag-handles/column/drag-handle.tsx (1)

160-201: Nice parity with Row handle; UI/overlay plumbing LGTM.

Consistent Floating UI setup, styling, and dropdown anchor behavior.

packages/editor/src/core/extensions/table/plugins/drag-handles/actions.ts (1)

169-201: Table to/from cells approach is clean and consistent for moves.

The tableToCells/tableFromCells helpers are thoughtfully implemented, handle visited cells, and keep the move operations straightforward.

@sriramveeraghanta sriramveeraghanta merged commit 9ecea15 into preview Aug 22, 2025
6 of 7 checks passed
@sriramveeraghanta sriramveeraghanta deleted the feat/table-entity-rearrrange branch August 22, 2025 19:24
@coderabbitai coderabbitai bot mentioned this pull request Aug 22, 2025
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants