Skip to content

fix(editor): preserve tasks in cross-note drags#693

Open
maccman wants to merge 2 commits into
nextfrom
codex/fix-cross-day-task-drag
Open

fix(editor): preserve tasks in cross-note drags#693
maccman wants to merge 2 commits into
nextfrom
codex/fix-cross-day-task-drag

Conversation

@maccman

@maccman maccman commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Problem

Each day in the daily stream owns a separate Meowdown/ProseMirror editor. ProseKit's six-dot block handle exports rendered block HTML, but not the data-pm-slice envelope that a different editor needs to recognize it as native content. Dropping a round task such as + [ ] Task onto another day therefore ran the task DOM through Meowdown's foreign-HTML converter and produced a literal \[ ] paragraph followed by detached task text. The saved note no longer contained or indexed a task.

This change preserves the existing drag semantics: reordering inside one editor is still a move, while a drop between editor views is still a copy. It fixes the reported content corruption without adding cross-note source deletion.

Before -> After

Flow Before After
Drag a round task to another daily note Checkbox becomes \[ ]; task text becomes a separate paragraph Exact task structure and + marker survive
Drag a block within one note Reorders through ProseMirror's source slice Unchanged
Drag files or other non-handle content Existing handlers own the event Unchanged

Changes

  1. Add BlockDragClipboard inside the editor's ProseKit context. Its bubbling dragstart listener runs after ProseKit has selected the block, reuses ProseMirror's transformed slice metadata, and writes lossless HTML plus Markdown text/plain to the transfer.
  2. Serialize the slice through the full Meowdown schema instead of the flat-list clipboard serializer. This retains Reflect-specific list attributes such as the round + marker; the normal native-list serializer would normalize it to a square - [ ] checklist.
  3. Clear the source view's stale drag slice on dragend. The handle lives outside the contenteditable, so ProseMirror's own cleanup does not see cross-editor or canceled handle drags.
  4. Add focused coverage using the real Meowdown schema. The tests reparse the transferred HTML, prove that + [ ] Task survives exactly, isolate unrelated drags, and cover delayed mount plus listener cleanup.

Verification

  • pnpm --filter @reflect/desktop test --run src/editor/block-drag-clipboard.test.tsx src/editor/note-editor.test.tsx — 38 tests passed.
  • pnpm check — typecheck and lint passed. The existing max-lines warnings in graph-provider.tsx and indexer.ts remain unchanged.

Risk / Rollout

No migration or rollout step is required. The listener is scoped to ProseKit's block-handle custom element, and the regression covers the main isolation boundary. Cross-editor drops intentionally remain copies; implementing true cross-note moves would require destination acknowledgement before safely deleting the source.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds block-handle drag clipboard serialization that preserves ProseMirror slice metadata, integrates it into NoteEditor, and adds tests for handled, unrelated, and completed drags.

Changes

Block drag clipboard flow

Layer / File(s) Summary
Handle drag serialization
apps/desktop/src/editor/block-drag-clipboard.tsx, apps/desktop/package.json
Adds BlockDragClipboard, which listens for block-handle drag events, serializes slices into HTML and plain text, preserves data-pm-slice, and clears drag state on completion.
Editor integration and validation
apps/desktop/src/editor/note-editor.tsx, apps/desktop/src/editor/block-drag-clipboard.test.tsx
Renders the clipboard component in NoteEditor and tests handled drag serialization, unrelated drag exclusion, and drag cleanup.

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

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: preserving task markers during cross-note editor drags.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-cross-day-task-drag

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

@maccman maccman requested a review from ocavue July 9, 2026 23:01

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
apps/desktop/src/editor/block-drag-clipboard.test.tsx (1)

63-140: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider covering mount-retry and cleanup paths.

Tests exercise the handled/unrelated dragstart and handled dragend cases well, but the RAF-based mount-retry (editor.mounted === false) and listener cleanup on unmount aren't exercised, nor is a dragend on a non-handle target (to confirm dragging is left untouched).

🤖 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 `@apps/desktop/src/editor/block-drag-clipboard.test.tsx` around lines 63 - 140,
Extend the BlockDragClipboard tests to cover the missing lifecycle paths: verify
that when editor.mounted is false, the dragstart handler retries via
requestAnimationFrame and processes the handle drag after mounting; verify
unmount removes the dragstart/dragend listeners so subsequent events have no
effect; and verify dragend from a non-handle target leaves editor.view.dragging
unchanged. Use the existing renderBridge setup and mock RAF behavior as needed,
restoring mocks during cleanup.
apps/desktop/src/editor/block-drag-clipboard.tsx (1)

45-52: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer a runtime type guard over the assertion.

event as DragEvent bypasses type-checking; since wrapper.addEventListener is typed generically (Element, not HTMLElement), an instanceof DragEvent check would satisfy the same narrowing without an assertion.

♻️ Proposed refactor
       const handleDragStart = (event: Event): void => {
-        const dragEvent = event as DragEvent
-        if (dragEvent.dataTransfer === null) {
+        if (!(event instanceof DragEvent) || event.dataTransfer === null) {
           return
         }
+        const dragEvent = event
         if (!isBlockHandleEvent(event)) {
           return
         }

As per coding guidelines, "Avoid type assertions unless they are genuinely necessary."

🤖 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 `@apps/desktop/src/editor/block-drag-clipboard.tsx` around lines 45 - 52, In
handleDragStart, replace the event as DragEvent assertion with an instanceof
DragEvent runtime check before accessing dataTransfer, returning early for
non-drag events; retain the existing null check and isBlockHandleEvent
validation.

Source: Coding guidelines

🤖 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.

Nitpick comments:
In `@apps/desktop/src/editor/block-drag-clipboard.test.tsx`:
- Around line 63-140: Extend the BlockDragClipboard tests to cover the missing
lifecycle paths: verify that when editor.mounted is false, the dragstart handler
retries via requestAnimationFrame and processes the handle drag after mounting;
verify unmount removes the dragstart/dragend listeners so subsequent events have
no effect; and verify dragend from a non-handle target leaves
editor.view.dragging unchanged. Use the existing renderBridge setup and mock RAF
behavior as needed, restoring mocks during cleanup.

In `@apps/desktop/src/editor/block-drag-clipboard.tsx`:
- Around line 45-52: In handleDragStart, replace the event as DragEvent
assertion with an instanceof DragEvent runtime check before accessing
dataTransfer, returning early for non-drag events; retain the existing null
check and isBlockHandleEvent validation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 365683a2-4466-40ac-8be2-a4659cc76e44

📥 Commits

Reviewing files that changed from the base of the PR and between 912406c and 29dd0e8.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • apps/desktop/package.json
  • apps/desktop/src/editor/block-drag-clipboard.test.tsx
  • apps/desktop/src/editor/block-drag-clipboard.tsx
  • apps/desktop/src/editor/note-editor.tsx

@ocavue ocavue 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.

Let me re-implement this in the meowdown side.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants