Skip to content

fix(file-tree): support dragging files from Finder into vault folders#121

Open
spamsch wants to merge 8 commits into
jsgrrchg:mainfrom
spamsch:worktree-fix+finder-drag-to-file-tree
Open

fix(file-tree): support dragging files from Finder into vault folders#121
spamsch wants to merge 8 commits into
jsgrrchg:mainfrom
spamsch:worktree-fix+finder-drag-to-file-tree

Conversation

@spamsch
Copy link
Copy Markdown
Contributor

@spamsch spamsch commented May 20, 2026

Closes #120

Summary

  • copy_external_file_to_vault (Rust): new Tauri command that takes an absolute source path + a vault-relative target folder, copies the file using the existing prepare_binary_file_target dedup logic, and emits a vault change event so the file tree refreshes.
  • EXTERNAL_FILE_TREE_DRAG_EVENT (dragEvents.ts): lightweight custom DOM event that carries a hover phase and folder path — lets MultiPaneWorkspace talk to FileTree without prop-drilling.
  • MultiPaneWorkspace.tsx: on Tauri "enter"/"over", queries data-folder-path at the cursor and emits hover events; on "drop", if the cursor is over a file tree folder the files are copied there instead of being opened in an editor pane.
  • FileTree.tsx: listens for the event and sets dragOverPath, activating the existing accent-tinted folder highlight (same visual as internal drag-and-drop).

Test plan

  • Drag a file from Finder and hover over a folder in the file tree — folder highlights with the accent outline
  • Drop the file — it appears in that folder, tree refreshes
  • Drag to the root (blank area below all rows) — root container highlights, file lands at the vault root
  • Drop multiple files at once — all are copied
  • Drop a file whose name already exists in the target — deduplicated with a timestamp suffix (existing behaviour from prepare_binary_file_target)
  • Drag from Finder onto an editor pane — still opens the file as a tab, unaffected

Tauri's onDragDropEvent only handled drops onto editor panes; drops over
the file tree were silently discarded.

- Add copy_external_file_to_vault backend command: copies an external
  file (absolute path) into a vault-relative folder using the existing
  dedup logic, then emits a vault change event
- Add EXTERNAL_FILE_TREE_DRAG_EVENT so MultiPaneWorkspace can broadcast
  hover state to the file tree without prop-drilling
- Extend onDragDropEvent: on enter/over emit folder hover events; on
  drop, if the cursor is over a data-folder-path element, copy files
  into that folder instead of opening them in an editor pane
- FileTree listens for the event and sets dragOverPath, activating the
  existing accent-tinted folder highlight
@spamsch
Copy link
Copy Markdown
Contributor Author

spamsch commented May 20, 2026

Code Review (Claude Opus)

Verdict: Solid fix. The core flow is right, the backend command mirrors the existing save_vault_binary_file shape, and the event-bus wiring matches the FILE_TREE_NOTE_DRAG_EVENT convention. A handful of real problems worth addressing before merge.


Issues to fix

1. Silent failure on copy — no user feedback
copyExternalFilesToVaultFolder fires Promise.all with no .catch. If prepare_binary_file_target rejects (colon in macOS filename, dotfile failing leaf validation, ignored path) or fs::copy fails (permissions, full disk), the user sees nothing — which from their perspective looks identical to the original bug.

Promise.allSettled instead of Promise.all so one bad file doesn't drop the rest; surface failures via a toast + logError.

2. op_id not threaded through — diverges from save_vault_binary_file
The sibling command reads opId/op_id and passes it into build_vault_note_change. Here it's hardcoded None. If the frontend uses op_id to dedup its own echo of vault changes, external-drop copies will look like remote changes. Either thread an opId through vaultInvoke, or document why it's intentionally absent.

3. Race with internal mouse-drag — shared dragOverPath with no coordination
Both the Tauri drag and the internal mouse drag write to the same dragOverPath state with no coordination. If a Tauri cancel/leave event fires mid-internal-drag (window blur, OS cancellation), it clears dragOverPath while dragOverPathRef.current still holds the folder — and onUp then commits the move to that path despite the visual being cleared. The reverse race (external over painting the wrong folder during internal drag) can also occur.

Fix: guard the external event handler with an early return if dragStateRef.current is set.

4. Payload cast is duplicated
event.payload is cast twice with slightly different shapes at lines ~380 and ~417. Narrow once at the top of the handler:

const payload = event.payload as { position?: { x: number; y: number }; paths?: string[] };

5. data-folder-path="" contract is implicit
The empty string on the root container is load-bearing for both the highlight logic and the backend's relative_dir. If someone changes it to "/" or "__root__", files silently land in a wrong subfolder. A comment on the element (// must be "" — consumed by copy_external_file_to_vault as root relative_dir) would make this explicit.

6. Multiple files → multiple vault refreshes
Promise.all over N files triggers N parallel refresh_vault_state + emit_vault_change calls. For a batch Finder drop this is wasteful. Better as a single Tauri call that accepts Vec<String> source paths and does one refresh at the end.


Worth noting

7. Deduplication is silent
prepare_binary_file_target silently renames report.pdfreport-1716200000000.pdf on collision. For internal operations that's fine; for a Finder drag the user expects their filename to land as-is. A toast ("Renamed to avoid conflict") or a confirm dialog would close the loop.

8. Note rows also carry data-folder-path
At FileTree.tsx:1344, note rows have data-folder-path={getParentPath(note.id)} for the internal drag system. Hovering a note during a Finder drag highlights its parent folder — which is correct behavior, but it's accidental. A comment in resolveFileTreeFolderAtPoint documenting this would prevent someone from "fixing" the note-row attribute and breaking this.


What's done well

  • Event-bus decoupling matches the project's existing conventions (FILE_TREE_NOTE_DRAG_EVENT, AGENT_SIDEBAR_DRAG_EVENT). Correct placement in dragEvents.ts.
  • Backend reuses prepare_binary_file_target — path traversal, ignored-path checks, leaf-name validation, dedup, and parent-dir creation all come for free. No security holes.
  • is_file() guard gives a clear early error for symlinks, directories, and disappeared files.
  • File-tree drop and editor-pane drop are mutually exclusive (fileTreeFolder !== null short-circuits first). Precedence is correct.
  • refresh_vault_state + emit_vault_change means the tree live-updates without a reload.

Recommend: address #1 (error reporting) and #3 (internal-drag guard) before merge. Everything else is nice-to-have.

- Switch Promise.all to Promise.allSettled in copyExternalFilesToVaultFolder
  so one bad file doesn't silently drop the rest; log each failure via logError
- Guard the EXTERNAL_FILE_TREE_DRAG_EVENT handler in FileTree with an early
  return when dragStateRef.current is set, preventing a Tauri cancel/leave
  event from clearing dragOverPath mid-internal-drag
- Mock document.elementsFromPoint in MultiPaneWorkspace tests (jsdom does
  not implement it); returning [] makes resolveFileTreeFolderAtPoint return
  null so existing editor-pane drop tests are unaffected
@spamsch
Copy link
Copy Markdown
Contributor Author

spamsch commented May 20, 2026

Addressed the must-fix items from the review (f73a1b8):

  • Silent failurePromise.allSettled so one bad file doesn't abort the batch; each rejected copy is logged via logError("file-tree", ...)
  • Internal-drag race → external event handler in FileTree now bails out immediately if dragStateRef.current is set, so a Tauri cancel/leave event can't clear dragOverPath while an in-app mouse drag is active
  • Test → mocked document.elementsFromPoint in beforeEach (jsdom doesn't implement it); returns [] so resolveFileTreeFolderAtPoint yields null and the existing editor-pane drop tests are unaffected — all 13 pass

@jsgrrchg jsgrrchg marked this pull request as ready for review May 21, 2026 16:35
@jsgrrchg
Copy link
Copy Markdown
Owner

LGTM

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.

File tree: dragging files from Finder into a folder does nothing

2 participants