Conversation
Signed-off-by: Cristian <cristianvargasvalencia@gmail.com>
Signed-off-by: Cristian <cristianvargasvalencia@gmail.com>
Signed-off-by: Cristian <cristianvargasvalencia@gmail.com>
Signed-off-by: Cristian <cristianvargasvalencia@gmail.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes post-move behavior in the Mail app by ensuring that when the currently opened message is moved (via action menu, thread view, or drag-and-drop), the UI automatically navigates to an adjacent message instead of leaving the detail pane empty or jumping to the top.
Changes:
- Added shared “select adjacent envelope” navigation logic in
Mailbox.vue, reused for delete, move, and drag-and-drop move scenarios. - Rewired move events across list, thread, and modal components so navigation can happen before the moved envelope disappears from the list.
- Added a new unit test suite for
Mailbox.vuebehavior and fixed a locale-dependent test in grouped-envelope date grouping.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/components/Mailbox.vue | Adds move/drag-drop handlers and shared adjacent-selection navigation logic. |
| src/components/MoveModal.vue | Emits moved envelope IDs before executing the move to enable correct list-index navigation. |
| src/components/Envelope.vue | Forwards move events with envelope IDs; emits move for quick-move actions. |
| src/components/EnvelopeList.vue | Forwards @move events upward (mirrors existing delete forwarding pattern). |
| src/components/ThreadEnvelope.vue | Forwards move events with envelope IDs. |
| src/components/Thread.vue | Emits move upward for Mailbox-level navigation instead of routing to mailbox root. |
| src/components/MailboxThread.vue | Bridges thread move events onto the shared bus. |
| src/components/NavigationMailbox.vue | Removes drag-and-drop post-move navigation to mailbox root. |
| src/tests/unit/components/Mailbox.vue.spec.js | Adds unit tests for adjacent-navigation behavior on delete/move/drag-drop. |
| src/tests/unit/util/groupedEnvelopes.spec.js | Fixes locale-dependent assertion for month label grouping. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| mounted() { | ||
| dragEventBus.on('drag-start', this.onDragStart) | ||
| dragEventBus.on('drag-end', this.onDragEnd) | ||
| dragEventBus.on('envelopes-moved', this.onEnvelopesMoved) | ||
| }, |
There was a problem hiding this comment.
With the envelopes-moved handler removed here, dragEventBus.emit('envelopes-moved', ...) (still emitted by the droppable-mailbox directive) currently has no listeners in the app. Either remove that emit as dead code or re-home any remaining side effects that still need to run after the move finishes.
| const idx = findIndex(propEq(id, 'databaseId'), this.envelopes) | ||
| if (idx === -1) { | ||
| logger.debug('envelope to delete does not exist in envelope list') | ||
| logger.debug('envelope does not exist in envelope list') | ||
| return |
There was a problem hiding this comment.
navigateToAdjacentEnvelope() uses findIndex(propEq(id, 'databaseId'), this.envelopes). propEq is strict-equality, so this will fail if route/thread IDs are numbers but envelope.databaseId values (or the passed id) are strings (or vice versa), preventing any navigation after delete/move in real routes (Thread.vue parses $route.params.threadId as int). Normalize IDs before comparing (e.g., compare String(databaseId) values, or parse both to numbers consistently).
There was a problem hiding this comment.
same issue, read the other comments below
| if (id !== this.$route.params.threadId) { | ||
| logger.debug('other message open, not jumping to the next/previous message') | ||
| return |
There was a problem hiding this comment.
This check is type-sensitive: id is often a number (env.databaseId), while this.$route.params.threadId is typically a string from the URL. That makes the guard incorrectly treat the current message as “different”, so it won’t navigate after delete/move. Convert both sides to the same type before comparing.
There was a problem hiding this comment.
Read the comment below. Same issue.
| // Find the nearest envelope that is not being moved, preferring | ||
| // the one above (idx - 1) unless we're at the top of the list | ||
| let next | ||
| if (idx === 0) { | ||
| next = this.envelopes.slice(idx + 1).find((env) => !excludeIds.includes(env.databaseId)) | ||
| } else { | ||
| next = this.envelopes.slice(0, idx).reverse().find((env) => !excludeIds.includes(env.databaseId)) | ||
| || this.envelopes.slice(idx + 1).find((env) => !excludeIds.includes(env.databaseId)) |
There was a problem hiding this comment.
excludeIds.includes(env.databaseId) is also type-sensitive and becomes O(n*k) for bulk moves. Consider normalizing excludeIds (e.g., String(id)) and using a Set for membership checks so moved envelopes are reliably skipped and the lookup stays O(1).
| // Find the nearest envelope that is not being moved, preferring | |
| // the one above (idx - 1) unless we're at the top of the list | |
| let next | |
| if (idx === 0) { | |
| next = this.envelopes.slice(idx + 1).find((env) => !excludeIds.includes(env.databaseId)) | |
| } else { | |
| next = this.envelopes.slice(0, idx).reverse().find((env) => !excludeIds.includes(env.databaseId)) | |
| || this.envelopes.slice(idx + 1).find((env) => !excludeIds.includes(env.databaseId)) | |
| // Normalize excludeIds to strings and use a Set for O(1), type-robust membership checks | |
| const excludeIdSet = new Set(excludeIds.map(String)) | |
| // Find the nearest envelope that is not being moved, preferring | |
| // the one above (idx - 1) unless we're at the top of the list | |
| let next | |
| if (idx === 0) { | |
| next = this.envelopes.slice(idx + 1).find((env) => !excludeIdSet.has(String(env.databaseId))) | |
| } else { | |
| next = this.envelopes.slice(0, idx).reverse().find((env) => !excludeIdSet.has(String(env.databaseId))) | |
| || this.envelopes.slice(idx + 1).find((env) => !excludeIdSet.has(String(env.databaseId))) |
There was a problem hiding this comment.
Slightly more complexity for practically very low gains. I don't think moving thousands of emails is a common occurrence. I can do it, but should be ok to keep it as is.
| const currentThreadId = this.$route.params.threadId | ||
| const movedId = ids.find((id) => id === currentThreadId) | ||
| if (movedId !== undefined) { | ||
| this.navigateToAdjacentEnvelope(movedId, ids) |
There was a problem hiding this comment.
onMove() compares ids to this.$route.params.threadId with strict equality, but ids are frequently numbers while the route param is a string. As a result, move navigation won’t trigger in common cases (e.g., thread view emits numeric ids). Normalize types (String/Number) for the membership check and when passing excludeIds to navigateToAdjacentEnvelope().
| const currentThreadId = this.$route.params.threadId | |
| const movedId = ids.find((id) => id === currentThreadId) | |
| if (movedId !== undefined) { | |
| this.navigateToAdjacentEnvelope(movedId, ids) | |
| const currentThreadIdRaw = this.$route.params.threadId | |
| if (currentThreadIdRaw === undefined || currentThreadIdRaw === null) { | |
| return | |
| } | |
| const idsAreNumbers = ids.some((id) => typeof id === 'number') | |
| const currentThreadId = idsAreNumbers ? Number(currentThreadIdRaw) : String(currentThreadIdRaw) | |
| const movedId = ids.find((id) => id === currentThreadId) | |
| if (movedId !== undefined) { | |
| const normalizedExcludeIds = idsAreNumbers | |
| ? ids.map((id) => Number(id)) | |
| : ids.map((id) => String(id)) | |
| this.navigateToAdjacentEnvelope(movedId, normalizedExcludeIds) |
There was a problem hiding this comment.
The delete path already used this approach https://github.com/nextcloud/mail/blob/main/src/components/Mailbox.vue#L556
If this is going to be changed maybe it should be in it's own issue? It works in practice as-is, and I don't want to touch more than what I need to fix the specific issue the PR is about.
| const envelopes = [ | ||
| { databaseId: 'A', mailboxId: 1 }, | ||
| { databaseId: 'B', mailboxId: 1 }, | ||
| { databaseId: 'C', mailboxId: 1 }, | ||
| { databaseId: 'D', mailboxId: 1 }, | ||
| { databaseId: 'E', mailboxId: 1 }, | ||
| ] |
There was a problem hiding this comment.
These tests use non-numeric databaseId values ('A'..'E') and threadId route params that match by string. In production, databaseId/threadId are typically numeric (e.g., Thread.vue parses $route.params.threadId with parseInt), so the current tests won’t catch the real-world type mismatch that can block navigation. Consider switching the fixtures to numeric IDs and include at least one case where route params are strings but envelope IDs are numbers.
Co-authored-by: Daniel <mail@danielkesselberg.de> Signed-off-by: Cristian Vargas <cristianvargasvalencia@gmail.com>
Signed-off-by: Cristian <cristianvargasvalencia@gmail.com>
|
I fixed the one that made the most sense...the rest are questionable. |
|
Thank you for your contribution. Small head-up about the commit history. 083bd5e is a refactoring. This PR is generally a bug fix. After review approval commits should be squashed into a single See also https://github.com/nextcloud/mail/blob/main/.github/CONTRIBUTING.md#commit-messages. |
Fixes #7548
After moving a message (via action menu, thread view, or drag-and-drop), the app now auto-selects the adjacent message in the list instead of leaving the right pane empty or jumping to the topmost message.
Changes
Core logic (Mailbox.vue)
below if at the top), skipping any that are also being moved
Event wiring
index lookup)
Tests
I am not sure about how to test the Threads in the UI. I manually tested the drag n drop, the menu at the right of each envelope, and the bulk move.