Reparenting a folder meant the Folders settings page, or nothing at all. The tree already accepted messages dropped onto a folder, so folders now travel the same way: drag one onto another to nest it, or onto the Folders heading to bring it back to the top level.
Behaviour
The heading reads "Drop here for the top level" while a folder is in flight — an unlabelled strip of heading is not a discoverable target.
The dragged row fades, the row under the pointer is outlined, and only rows that would actually accept the drop light up.
After a move the destination expands, so the folder is visible where it landed rather than hidden inside a closed parent.
The move goes through updateMailbox, so the filter rules pointing at that folder follow it — the reconciliation from #37/#39 applies to drags for free.
Four drops are refused
A folder onto itself; into its own subtree; onto the parent it already has; and any folder the server gave a role, which isn't draggable at all. The subtree case is the one that matters, since it would orphan the branch — and it checks the whole subtree, not just immediate children.
One wrinkle worth recording: whether a drop is legal has to be known during dragover, when dataTransfer.getData is blocked for security. So the tree remembers what is being dragged rather than asking the drag.
Verified
The guards are pure and live in lib/folderMove.ts with 9 tests — role folders, whole-subtree detection, self-drop, drop-on-current-parent, root in both directions, and a missing target.
Driven in the real app against the mock:
draggable rows are exactly Newsletters and Work; Inbox, Archive, Drafts, Junk Mail, Sent and Trash all report draggable=false
dragging Newsletters onto Work nests it, and Work expands to show it
dragging it onto the Folders heading returns it to the top level
dropping Work onto itself: not a target, no highlight
Regression-checked the message drop, which shares the handler I changed: dragging a message onto a folder still carries application/x-ihasmail-emails and still highlights the folder, behaving identically with and without this change. (My synthetic drop does not complete the move in either case — the list count is unchanged on main too — so that is a limit of the harness, not of the feature.)
npm run typecheck, npm test — 222 web + 88 server passing.
Reparenting a folder meant the Folders settings page, or nothing at all. The tree already accepted messages dropped onto a folder, so folders now travel the same way: **drag one onto another to nest it, or onto the Folders heading to bring it back to the top level.**
## Behaviour
- The heading reads **"Drop here for the top level"** while a folder is in flight — an unlabelled strip of heading is not a discoverable target.
- The dragged row fades, the row under the pointer is outlined, and only rows that would actually accept the drop light up.
- After a move the destination expands, so the folder is visible where it landed rather than hidden inside a closed parent.
- The move goes through `updateMailbox`, so the filter rules pointing at that folder follow it — the reconciliation from #37/#39 applies to drags for free.
## Four drops are refused
A folder onto itself; into its own subtree; onto the parent it already has; and any folder the server gave a role, which isn't draggable at all. The subtree case is the one that matters, since it would orphan the branch — and it checks the **whole** subtree, not just immediate children.
One wrinkle worth recording: whether a drop is legal has to be known during `dragover`, when `dataTransfer.getData` is blocked for security. So the tree remembers what is being dragged rather than asking the drag.
## Verified
The guards are pure and live in `lib/folderMove.ts` with 9 tests — role folders, whole-subtree detection, self-drop, drop-on-current-parent, root in both directions, and a missing target.
Driven in the real app against the mock:
- draggable rows are exactly `Newsletters` and `Work`; `Inbox`, `Archive`, `Drafts`, `Junk Mail`, `Sent` and `Trash` all report `draggable=false`
- dragging `Newsletters` onto `Work` nests it, and `Work` expands to show it
- dragging it onto the Folders heading returns it to the top level
- dropping `Work` onto itself: not a target, no highlight
**Regression-checked the message drop**, which shares the handler I changed: dragging a message onto a folder still carries `application/x-ihasmail-emails` and still highlights the folder, behaving identically with and without this change. (My synthetic drop does not complete the move in either case — the list count is unchanged on main too — so that is a limit of the harness, not of the feature.)
`npm run typecheck`, `npm test` — 222 web + 88 server passing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
**Merged** 2026-08-25 as coffey-labs/ihasmail@145abef3fe02
<sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Reparenting a folder meant the Folders settings page, or nothing at all. The tree already accepted messages dropped onto a folder, so folders now travel the same way: drag one onto another to nest it, or onto the Folders heading to bring it back to the top level.
Behaviour
updateMailbox, so the filter rules pointing at that folder follow it — the reconciliation from #37/#39 applies to drags for free.Four drops are refused
A folder onto itself; into its own subtree; onto the parent it already has; and any folder the server gave a role, which isn't draggable at all. The subtree case is the one that matters, since it would orphan the branch — and it checks the whole subtree, not just immediate children.
One wrinkle worth recording: whether a drop is legal has to be known during
dragover, whendataTransfer.getDatais blocked for security. So the tree remembers what is being dragged rather than asking the drag.Verified
The guards are pure and live in
lib/folderMove.tswith 9 tests — role folders, whole-subtree detection, self-drop, drop-on-current-parent, root in both directions, and a missing target.Driven in the real app against the mock:
NewslettersandWork;Inbox,Archive,Drafts,Junk Mail,SentandTrashall reportdraggable=falseNewslettersontoWorknests it, andWorkexpands to show itWorkonto itself: not a target, no highlightRegression-checked the message drop, which shares the handler I changed: dragging a message onto a folder still carries
application/x-ihasmail-emailsand still highlights the folder, behaving identically with and without this change. (My synthetic drop does not complete the move in either case — the list count is unchanged on main too — so that is a limit of the harness, not of the feature.)npm run typecheck,npm test— 222 web + 88 server passing.🤖 Generated with Claude Code
Merged 2026-08-25 as coffey-labs/ihasmail@145abef3fe
Rebuilt from: git history, session transcript.