feat: add filter-based mass selection via search modal#12901
feat: add filter-based mass selection via search modal#12901RobinAngele wants to merge 35 commits intonextcloud:mainfrom
Conversation
…ctions - Add missing ImportantIcon import and component registration in EnvelopeList so the 'Mark as important' icon renders during bulk selection - Fix favorite/unfavorite bulk action logic: rename methods to favoriteAll/unfavoriteAll and use explicit favFlag values (true/false) instead of inverted computed checks that failed when all selected messages shared the same state AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
Add a 'Select all X messages' checkbox above the envelope list using NcCheckboxRadioSwitch from @nextcloud/vue, allowing users to select all visible messages at once. Lift envelope selection state from individual EnvelopeList instances up to the Mailbox parent component. This enables: - Cross-group shift-click range selection via flat envelope indexing - Consistent selection state across grouped envelope lists - Global select-all / unselect-all from the parent level Also pass the selection array as a prop to EnvelopeList and add proper event handling for update:selection and select-range. Closes: nextcloud#4285 Refs: nextcloud#7880, nextcloud#6070, nextcloud#7276 AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
Add a 'Select all matching' button to the search parameters dialog and enable selecting all messages matching a filter across all pages. Search modal: - New 'Select all matching' button in SearchMessages.vue dialog - Emits 'select-all-matching' event via the MailboxThread bus Mass loading: - Mailbox.vue onBusSelectAllMatching forces a fresh load, then iterates loadMore() until all pages are fetched - Spinner + 'Selecting messages…' shown during loading - Checkbox disabled while loading Context-aware labels: - 'Select N loaded messages' when more pages exist - 'Select N matching messages' when a filter is active - 'Select all N messages' when all pages are loaded - Hint row: 'Scroll down to include more messages, use filter to refine, or click an avatar circle to select one at a time' Depends on: nextcloud#12900 (select-all checkbox feature) Closes: nextcloud#4285, nextcloud#7880 Refs: nextcloud#6070, nextcloud#7276, nextcloud#11526 AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
Use selectMode (any selection) instead of allSelected (must match all visible) for the checkbox checked state. This way the checkbox stays checked when the user manually deselects individual messages via avatar circles after using Select All. AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
…s active When a filter is already applied, the hint now shows: 'Scroll down to include more messages or click an avatar circle to select one at a time' — removing the redundant filter advice. AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
When in selectMode, the checkbox label now shows '{N} selected'
with the real count instead of the loaded count. This way when
the user deselects individual messages, the number updates
immediately from 20 to 19, etc.
AI-assisted: Cline (Claude)
Signed-off-by: RobinAngele <robin@robin4consulting.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR lifts selection into Mailbox, adds a select-all UI and cross-page “select all matching” flow, converts EnvelopeList to prop-driven selection with flatIndex for global ranges, delegates range selection to Mailbox, updates favorite/unfavorite bulk actions, wires SearchMessages and MailboxThread to emit selection events, refactors envelope shift-click handling, and adds documentation and UI styles. Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/Mailbox.vue (1)
317-335:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReset pagination and bulk-selection state when the source list changes.
These watchers clear
selection, but they leaveendReached/expandedbehind and only partially reset the select-all-matching flags. If the previous view had already reached the end, the next mailbox/filter/sort starts in a stale “fully loaded” state, which can hide further pages and prevent the new result set from using the intended bulk-select flow.Suggested reset
mailbox() { this.selection = [] + this.selectAllMatching = false + this.loadingAllMatching = false + this.endReached = false + this.expanded = false this.loadEnvelopes() .then(() => { logger.debug(`syncing mailbox ${this.mailbox.databaseId} (${this.query}) after folder change`) this.sync(false) }) }, searchQuery() { this.selection = [] this.selectAllMatching = false this.loadingAllMatching = false + this.endReached = false + this.expanded = false this.loadEnvelopes() }, sortOrder() { this.selection = [] + this.selectAllMatching = false + this.loadingAllMatching = false + this.endReached = false + this.expanded = false this.loadEnvelopes() },🤖 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 `@src/components/Mailbox.vue` around lines 317 - 335, The watchers mailbox(), searchQuery(), and sortOrder() currently clear selection but leave pagination and bulk-selection state stale; update each watcher to also reset endReached (set to false), clear expanded state (reset the expanded map/array), and set selectAllMatching and loadingAllMatching to false before calling loadEnvelopes() (and before sync in mailbox()), so the new mailbox/filter/sort starts with fresh pagination and bulk-selection state; reference the mailbox(), searchQuery(), and sortOrder() watcher handlers and the component properties endReached, expanded, selectAllMatching, loadingAllMatching, selection, loadEnvelopes(), and sync().
🤖 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.
Inline comments:
In `@src/components/MailboxThread.vue`:
- Around line 607-609: The onSelectAllMatching handler currently emits a generic
'select-all-matching' on the shared mitt bus causing every Mailbox subscriber to
react; change it to emit a scoped payload (e.g., include mailboxId or query) or
target the specific Mailbox directly. Update the onSelectAllMatching method to
call this.bus.emit('select-all-matching', { mailboxId: this.mailboxId, query:
this.currentQuery }) (or invoke a direct method on the intended Mailbox
instance) and update Mailbox subscribers to check the payload (e.g., mailboxId
or query) before starting load/select so only the intended mailbox responds.
Ensure the event name remains 'select-all-matching' but listeners in Mailbox use
the identifying field to ignore unrelated events.
In `@src/components/SearchMessages.vue`:
- Around line 578-584: The selectAllMatching() method currently calls
sendQueryEvent() and emits 'select-all-matching' inside a single
this.$nextTick(), which can let Mailbox receive a stale
MailboxThread.searchQuery prop; change the flow in selectAllMatching to call
this.sendQueryEvent() inside the first this.$nextTick(), then call a second
this.$nextTick() before emitting this.$emit('select-all-matching') so
Mailbox.onBusSelectAllMatching() receives the updated prop value and avoids
corrupting the cache key logic.
---
Outside diff comments:
In `@src/components/Mailbox.vue`:
- Around line 317-335: The watchers mailbox(), searchQuery(), and sortOrder()
currently clear selection but leave pagination and bulk-selection state stale;
update each watcher to also reset endReached (set to false), clear expanded
state (reset the expanded map/array), and set selectAllMatching and
loadingAllMatching to false before calling loadEnvelopes() (and before sync in
mailbox()), so the new mailbox/filter/sort starts with fresh pagination and
bulk-selection state; reference the mailbox(), searchQuery(), and sortOrder()
watcher handlers and the component properties endReached, expanded,
selectAllMatching, loadingAllMatching, selection, loadEnvelopes(), and sync().
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 495a2697-7608-4c97-89cd-216e17ceac3f
📒 Files selected for processing (4)
src/components/EnvelopeList.vuesrc/components/Mailbox.vuesrc/components/MailboxThread.vuesrc/components/SearchMessages.vue
Added _localToggleInProgress guard that prevents the selection watcher from overwriting flags.selected during a local click toggle. This fixes a bug where clicking one avatar to deselect would sometimes remove multiple messages at once. AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
When loadingAllMatching is true, selectAllLabel now returns 'Selecting messages…' instead of the contextual label (matching/loaded/all). This prevents showing stale filter labels while loading is in progress. AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/components/Mailbox.vue (2)
830-846: ⚖️ Poor tradeoffConsider adding a cancellation mechanism for long-running operations.
The
while (!this.endReached)loop could run for a very long time on large mailboxes, especially over slow connections. If the user changes the filter, navigates away, or the component unmounts during loading, the loop continues until completion.Consider:
- Adding an abort flag checked in the loop
- Implementing a page limit (e.g., max 50 pages)
- Checking component mount state before state updates
♻️ Example cancellation pattern
async selectAllMatchingAction() { this.loadingAllMatching = true this.selectAllMatching = true + this._selectAllAborted = false try { // Load remaining pages until all envelopes are fetched - while (!this.endReached) { + while (!this.endReached && !this._selectAllAborted) { await this.loadMore() } + if (this._selectAllAborted) { + return + } // Now select all loaded envelopes this.selection = this.flatEnvelopeList.map((e) => e.databaseId) } catch (error) { logger.error('Failed to load all matching envelopes', { error }) } finally { this.loadingAllMatching = false } },Then set
this._selectAllAborted = trueinunselectAll()and watchers that reset state.🤖 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 `@src/components/Mailbox.vue` around lines 830 - 846, The selectAllMatchingAction can run indefinitely; add a cancellable pattern: introduce a boolean flag (e.g. this._selectAllAborted) and a maxPage cap (e.g. const MAX_SELECT_PAGES = 50) and modify selectAllMatchingAction to check both the abort flag and page cap inside the while (!this.endReached) loop before each await this.loadMore() and break if triggered; ensure you check the abort flag before assigning this.selection or calling this.flatEnvelopeList, and clear loadingAllMatching on abort; set this._selectAllAborted = true from unselectAll(), relevant filter-change watchers, and the component beforeDestroy/unmounted hook so the in-flight loop exits and avoids updating state after unmount.
36-48: 💤 Low valueDead code: loading icon inside the banner will never render.
The banner's
v-ifcondition includes!selectAllMatching, butloadingAllMatchingandselectAllMatchingare always set together (both inselectAllMatchingActionandonBusSelectAllMatching). WhenloadingAllMatchingis true,selectAllMatchingis also true, so the banner won't render and theNcLoadingIconon line 39 is unreachable.Consider removing the loading icon from the banner or adjusting the banner's visibility condition if loading state should be shown here.
♻️ Suggested fix
<div v-if="allSelected && !selectAllMatching && flatEnvelopeList.length < totalEnvelopeCount" class="select-all-banner"> - <NcLoadingIcon v-if="loadingAllMatching" :size="16" /> <span>{{ n('mail',🤖 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 `@src/components/Mailbox.vue` around lines 36 - 48, The banner's v-if (allSelected && !selectAllMatching && flatEnvelopeList.length < totalEnvelopeCount) prevents loadingAllMatching from ever being visible because selectAllMatching is set alongside loadingAllMatching in selectAllMatchingAction and onBusSelectAllMatching; either remove the unreachable NcLoadingIcon or change the banner condition to allow the loading state — e.g. update the v-if to include loadingAllMatching (allSelected && (loadingAllMatching || !selectAllMatching) && flatEnvelopeList.length < totalEnvelopeCount) so NcLoadingIcon can render while select-all is in progress, or simply delete the NcLoadingIcon element if you prefer not to show loading there.
🤖 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.
Inline comments:
In `@src/components/Mailbox.vue`:
- Around line 21-28: The checkbox is listening to the wrong event: change the
event handler on the NcCheckboxRadioSwitch from `@update`:checked to
`@update`:model-value so toggles actually invoke your handlers; update the
component invocation that references selectMode and the methods selectAll() /
unselectAll() to use `@update`:model-value="selectMode ? unselectAll() :
selectAll()" so the v-model update triggers those functions.
---
Nitpick comments:
In `@src/components/Mailbox.vue`:
- Around line 830-846: The selectAllMatchingAction can run indefinitely; add a
cancellable pattern: introduce a boolean flag (e.g. this._selectAllAborted) and
a maxPage cap (e.g. const MAX_SELECT_PAGES = 50) and modify
selectAllMatchingAction to check both the abort flag and page cap inside the
while (!this.endReached) loop before each await this.loadMore() and break if
triggered; ensure you check the abort flag before assigning this.selection or
calling this.flatEnvelopeList, and clear loadingAllMatching on abort; set
this._selectAllAborted = true from unselectAll(), relevant filter-change
watchers, and the component beforeDestroy/unmounted hook so the in-flight loop
exits and avoids updating state after unmount.
- Around line 36-48: The banner's v-if (allSelected && !selectAllMatching &&
flatEnvelopeList.length < totalEnvelopeCount) prevents loadingAllMatching from
ever being visible because selectAllMatching is set alongside loadingAllMatching
in selectAllMatchingAction and onBusSelectAllMatching; either remove the
unreachable NcLoadingIcon or change the banner condition to allow the loading
state — e.g. update the v-if to include loadingAllMatching (allSelected &&
(loadingAllMatching || !selectAllMatching) && flatEnvelopeList.length <
totalEnvelopeCount) so NcLoadingIcon can render while select-all is in progress,
or simply delete the NcLoadingIcon element if you prefer not to show loading
there.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: e480ccd4-a6cf-4ccc-9a40-65b3f878b3f0
📒 Files selected for processing (2)
src/components/EnvelopeList.vuesrc/components/Mailbox.vue
Vue 2 batches watchers at end of tick. The previous synchronous reset meant the watcher would fire after _localToggleInProgress was already false, re-introducing the race condition. Now reset via $nextTick so the guard remains active during the watcher's flush. AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/components/EnvelopeList.vue (1)
393-404: 💤 Low valueConsider removing redundant cleanup code.
The new cleanup logic (lines 395-399) correctly removes vanished envelope IDs from selection and emits the cleaned array. The older
differenceWithcleanup (lines 400-403) setsflags.selected = falseon envelopes that are no longer insortedEnvelops, which has no practical effect since those envelopes are already removed from the rendered list. This code could be removed for clarity.♻️ Suggested cleanup
sortedEnvelops(newVal, oldVal) { // Unselect vanished envelopes by emitting cleaned selection const newIds = new Set(newVal.map((env) => env.databaseId)) const cleanedSelection = this.selection.filter((id) => newIds.has(id)) if (cleanedSelection.length !== this.selection.length) { this.$emit('update:selection', cleanedSelection, this.envelopes) } - differenceWith((a, b) => a.databaseId === b.databaseId, oldVal, newVal) - .forEach((env) => { - env.flags.selected = false - }) },🤖 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 `@src/components/EnvelopeList.vue` around lines 393 - 404, Remove the redundant cleanup in the sortedEnvelops watcher: the initial block that builds newIds, computes cleanedSelection from this.selection and emits update:selection already cleans vanished IDs, so delete the subsequent differenceWith(...).forEach(env => env.flags.selected = false) code path (referenced as differenceWith in the sortedEnvelops watcher which manipulates env.flags.selected) to avoid dead/unused state changes while keeping the selection cleanup and this.$emit('update:selection', cleanedSelection, this.envelopes) intact.
🤖 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 `@src/components/EnvelopeList.vue`:
- Around line 393-404: Remove the redundant cleanup in the sortedEnvelops
watcher: the initial block that builds newIds, computes cleanedSelection from
this.selection and emits update:selection already cleans vanished IDs, so delete
the subsequent differenceWith(...).forEach(env => env.flags.selected = false)
code path (referenced as differenceWith in the sortedEnvelops watcher which
manipulates env.flags.selected) to avoid dead/unused state changes while keeping
the selection cleanup and this.$emit('update:selection', cleanedSelection,
this.envelopes) intact.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 32a554d8-9c2d-4cd5-bfdc-7a6234f76eed
📒 Files selected for processing (1)
src/components/EnvelopeList.vue
Added event.shiftKey check in onSelectMultiple to prevent range selection from firing on plain clicks. Also passes $event from template to the handler. AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/Envelope.vue (1)
1-4:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUpdate SPDX header to the required repository format.
Line 2 and Line 3 do not match the mandatory SPDX strings for Vue files in this repository (year/text and identifier key).
🔧 Proposed fix
<!-- - - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors - - SPDX-License-Identifier: AGPL-3.0-or-later + - SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + - SPDX-Identifier: AGPL-3.0-or-later -->As per coding guidelines "
{lib,src,tests}/**/*.{php,js,vue}: Every file must include an SPDX license header" and "SPDX license header format must be:SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributorsandSPDX-Identifier: AGPL-3.0-or-later".🤖 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 `@src/components/Envelope.vue` around lines 1 - 4, Update the SPDX header in Envelope.vue to the repository-mandated format by replacing the existing two header lines with the exact required strings: "SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors" and "SPDX-Identifier: AGPL-3.0-or-later"; locate the existing header comment at the top of src/components/Envelope.vue and swap those two lines so the file matches the project's SPDX header rules.
🤖 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.
Outside diff comments:
In `@src/components/Envelope.vue`:
- Around line 1-4: Update the SPDX header in Envelope.vue to the
repository-mandated format by replacing the existing two header lines with the
exact required strings: "SPDX-FileCopyrightText: 2026 Nextcloud GmbH and
Nextcloud contributors" and "SPDX-Identifier: AGPL-3.0-or-later"; locate the
existing header comment at the top of src/components/Envelope.vue and swap those
two lines so the file matches the project's SPDX header rules.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: f1de2789-e604-4606-890e-aa1cb989082a
📒 Files selected for processing (1)
src/components/Envelope.vue
…parison Changed hasFilter check from !== 'match:allof' to .length > 12. This handles cases where closeSearchModal emits a stale query with residual whitespace or parameters. The default match:allof query is exactly 12 chars; anything longer has a real filter. AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
d96ac21 to
c5a8cfd
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/Mailbox.vue (1)
323-341:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReset pagination and mass-selection flags when the result set changes.
These watchers clear
selection, but they leaveendReached/expandedbehind, andmailbox/sortOrderalso leave the bulk-selection flags behind. After fully loading one folder or filter, the next one can start withendReached = true, which hides pagination and makes the new select-all flow stop early.Suggested fix
watch: { mailbox() { - this.selection = [] + this.unselectAll() + this.endReached = false + this.expanded = false this.loadEnvelopes() .then(() => { logger.debug(`syncing mailbox ${this.mailbox.databaseId} (${this.query}) after folder change`) this.sync(false) }) }, searchQuery() { - this.selection = [] - this.selectAllMatching = false - this.loadingAllMatching = false + this.unselectAll() + this.endReached = false + this.expanded = false this.loadEnvelopes() }, sortOrder() { - this.selection = [] + this.unselectAll() + this.endReached = false + this.expanded = false this.loadEnvelopes() }, },🤖 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 `@src/components/Mailbox.vue` around lines 323 - 341, The mailbox(), searchQuery(), and sortOrder() watchers currently only clear selection but must also reset pagination and mass-selection flags to avoid carrying state into the next result set; update each watcher (mailbox(), searchQuery(), sortOrder()) to set endReached = false and expanded = false and also clear selectAllMatching = false and loadingAllMatching = false before calling loadEnvelopes()/sync(), ensuring any pagination cursor or offset state used by loadEnvelopes() is effectively reset for the new folder/filter.
🤖 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.
Inline comments:
In `@src/components/Mailbox.vue`:
- Around line 37-47: The banner/button currently uses totalEnvelopeCount as if
it were the full match total even when endReached is false; update the template
logic around the banner (the v-if using allSelected, selectAllMatching and
flatEnvelopeList.length < totalEnvelopeCount) and the copy/CTA (the span,
NcButton and their use of totalEnvelopeCount) so that exact "all matching"
counts are only shown when endReached === true—otherwise show a non-exact CTA
like "Select all visible messages" or "Select all matching messages" (no exact
number) and, if desired, show a progressive "Select all {visible} on this page"
with a secondary loader state (loadingAllMatching) that then resolves to the
exact count when endReached becomes true; apply the same change to the second
identical block that uses the same variables and selectAllMatchingAction.
- Around line 831-846: The selectAllMatchingAction loop can spin indefinitely
when loadMore() throws because endReached never flips; update
selectAllMatchingAction so that if loadMore() rejects you break the while loop
(or set endReached=true) before rethrowing or logging. Specifically, inside
selectAllMatchingAction around the while (!this.endReached) { await
this.loadMore() } call handle errors from loadMore() (or wrap each await
this.loadMore() in its own try/catch) and on error log via logger.error and then
break the loop so the method can proceed to set this.selection from
this.flatEnvelopeList and finish (preserve the existing finally that clears
this.loadingAllMatching and leave this.selectAllMatching true/false behavior
unchanged).
---
Outside diff comments:
In `@src/components/Mailbox.vue`:
- Around line 323-341: The mailbox(), searchQuery(), and sortOrder() watchers
currently only clear selection but must also reset pagination and mass-selection
flags to avoid carrying state into the next result set; update each watcher
(mailbox(), searchQuery(), sortOrder()) to set endReached = false and expanded =
false and also clear selectAllMatching = false and loadingAllMatching = false
before calling loadEnvelopes()/sync(), ensuring any pagination cursor or offset
state used by loadEnvelopes() is effectively reset for the new folder/filter.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 7a525e38-36d4-4e8e-8869-8d36f335f18c
📒 Files selected for processing (1)
src/components/Mailbox.vue
|
Sorry if the review rabbit caused any confusion. We enabled the integration but the config to turn auto reviews off was missing. |
After a filter loads all pages, endReached stays true. When the filter is cleared, loadEnvelopes() is called but doesn't reset endReached, causing the label to skip the 'loaded' state and show the default 'Select all N messages' instead. Now the searchQuery watcher resets endReached before reloading. AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
The previous .length > 12 check could fail if the default match:allof query had extra whitespace or encoding artifacts. Now uses /^match:allof\s*$/ regex for robust detection. AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
mentionsMe is a boolean. After resetFilter(), it's false. filterData returned the raw boolean, and searchQuery builder added 'mentions:false' to the query because it only skipped empty strings and null, not false. Now returns empty string when mentionsMe is false. AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
Previously only loadingAllMatching prevented stale labels. Now loadingEnvelopes (normal filter/mailbox change) also shows 'Loading…' and disables the checkbox, preventing stale counts from appearing during async loads. AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
The sortedEnvelops watcher could fire during a local toggle and clear flags.selected on active envelopes via differenceWith. Added _localToggleInProgress guard to prevent this. AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
Normal envelope loading now only changes the label to 'Loading…' without disabling the checkbox or showing the spinner row. This prevents the stuck spinner when rapid filter changes cause overlapping loadEnvelopes calls. AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
…hods - hasFilter as computed (single regex, reused by label + hint) - closeSearchModal no longer emits (only explicit button clicks) Removes _closingProgrammatically guard entirely - Merged selectAllMatchingAction into onBusSelectAllMatching AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
search-changed updates the searchQuery prop synchronously but Vue batches prop updates. select-all-matching was firing before the batch flushed, causing onBusSelectAllMatching to use the old query. Now wrapped in $nextTick so the new prop value is available. AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
Instead of reading this.searchQuery prop (which might be stale due to Vue batching), the query string is now passed through the entire event chain: SearchMessages -> MailboxThread bus -> Mailbox. This guarantees the correct query on first click. AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
search-changed watcher was resetting loadingAllMatching and selectAllMatching after onBusSelectAllMatching set them, causing the spinner and mass selection to abort. Now the watcher checks this.selectAllMatching and skips reset when the bus handler is managing the state. AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
…tAllMatching selectAllMatching stays true after mass selection completes, blocking all future watcher calls. _busHandlerActive is only true during the bus handler's execution and cleared in finally. AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
Removing sendQueryEvent from closeSearchModal broke the normal Search button - filters were never applied. Restored with the _busHandlerActive guard still protecting against watcher overwrite. AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
Vue batches prop updates. loadEnvelopes reads this.searchQuery which may not be updated yet when onBusSelectAllMatching runs. Now accepts an optional queryOverride parameter for the bus handler to pass the correct query directly. AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
|
Tested in production — all scenarios work. The two-event design handles filter changes and mass loading reliably. Ready for review when you have time. |
- Add isPriorityInbox prop; hide select-all banner in Priority inbox (each section only knows its own count, not the inbox total) - Add section-selected bus event: selecting in one Priority inbox section clears siblings — cross-section bulk actions are not possible since each section has an independent toolbar - Remove section-selected emit from finally block to prevent concurrent mass-selects from fighting each other - Add !loadingAllMatching guard in loadEnvelopes() to suppress LoadingSkeleton during mass-select (keeps spinner visible) - Remove dead loadingEnvelopes branch from selectAllLabel computed - Merge onUpdateSelection into single array assignment (removes intermediate empty reactive state) - Add select-all-limit-warning CSS with warning colour - Set searchQuery before bus emit in onSelectAllMatching to ensure Priority inbox sections read the correct parent query after $nextTick AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
hasFilter compared against 'is:pi-important' and 'is:pi-other' as literal strings. If those constants ever change in priorityInbox.js, hasFilter would silently break. Import and use the exported constants. AI-assisted: Cline (Claude) Signed-off-by: RobinAngele <robin@robin4consulting.com>
NcCheckboxRadioSwitch emits update:model-value (Vue 3 v-model contract), not update:checked. The previous binding silently did nothing when the checkbox was toggled. Signed-off-by: RobinAngele <robin@robin4consulting.com>
totalEnvelopeCount returns envelopes.length when endReached is false, which is the store-cached count and may be lower than the actual total on the server. The banner button was showing this partial count as if it were the full match total (e.g. "Select all 40 messages" when there are actually 200). Remove the count from the banner copy and button to avoid the mismatch. Signed-off-by: RobinAngele <robin@robin4consulting.com>
The Other section uses paginate='scroll' (no explicit paginate prop), so endReached starts false on first load and the label shows 'Select 20 loaded messages'. Since Priority inbox sections have no banner and mass-select is only available via the search modal, the 'loaded' qualifier is confusing — users have no obvious path to load more before selecting. Skip the !endReached branch for isPriorityInbox and fall through to 'Select all N messages'. Signed-off-by: RobinAngele <robin@robin4consulting.com>
Signed-off-by: RobinAngele <robin@robin4consulting.com>
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 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.
Inline comments:
In `@Nextcloud_Mail_Select_All_and_Mass_Selection.md`:
- Around line 168-177: Update the documented checkbox examples to match the
merged Mailbox.vue API: replace the deprecated `@update`:checked usage with
`@update`:model-value and show the new behavior where the handler toggles
selectMode to call selectAll() or unselectAll(); specifically reference the
NcCheckboxRadioSwitch component and the selectMode/selectAll/unselectAll
functions so readers understand to wire the checkbox's `@update`:model-value to
toggle selectMode which in turn invokes selectAll() or unselectAll().
- Around line 46-50: Add language identifiers to all unlabeled fenced code
blocks (e.g., the diagram block that starts with "Mailbox └── EnvelopeList (owns
`selection[]` locally)"), using text for ASCII diagrams and select vue, js, or
http for the other code snippets as appropriate; update each fence to start with
```text, ```vue, ```js or ```http so markdownlint MD040 warnings are cleared
across the file (apply the same change to the other unlabeled blocks mentioned
in the review).
In `@src/components/Mailbox.vue`:
- Around line 831-837: The onUpdateSelection merge leaves IDs that no longer
exist in the current visible mailbox, so update the merge to first build a Set
of current visible envelope IDs (e.g., from the component's visible/envelopes
array) and then filter the resulting selection against that set; specifically,
in onUpdateSelection replace the final assignment so it removes any id not
present in visibleIds after merging the filtered old selection and
childSelection, ensuring this.selection contains only IDs currently visible.
- Around line 244-249: flatEnvelopeList currently flattens from the original
source arrays (this.groupEnvelopes / this.envelopesToShow), which is out of sync
with the rendered sort order and causes shift-click range selection mismatches;
change flatEnvelopeList to return the flattened list derived from the
rendered/sorted source (use this.sortedEnvelops or the existing computed that
the UI uses for rendering) instead of the unsorted arrays so selections match
rendered rows — i.e. when a sorted/ordered computed like sortedEnvelops exists
return that flattened list (or map sorted entries to their envelope objects when
grouped), otherwise fall back to this.envelopesToShow.
- Around line 882-912: The mass-select flow in onBusSelectAllMatching sets
loadingAllMatching to true which causes the selection watcher to skip emitting
'section-selected', and it never gets emitted after completion; fix by, at the
end of onBusSelectAllMatching (after the finally sets this.loadingAllMatching =
false), explicitly emit the section-clearing event the watcher would have
emitted (i.e. emit the 'section-selected' event with the appropriate payload to
clear sibling section selection) so the Priority Inbox state is updated;
reference onBusSelectAllMatching, loadingAllMatching, the selection watcher, and
the 'section-selected' event when locating where to add the emit.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 821c2645-d20d-47bb-a8ca-d516cb257b79
📒 Files selected for processing (5)
Nextcloud_Mail_Select_All_and_Mass_Selection.mdsrc/components/EnvelopeList.vuesrc/components/Mailbox.vuesrc/components/MailboxThread.vuesrc/components/SearchMessages.vue
🚧 Files skipped from review as they are similar to previous changes (2)
- src/components/MailboxThread.vue
- src/components/SearchMessages.vue
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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.
Inline comments:
In `@src/components/Mailbox.vue`:
- Around line 903-913: The mass-select loop currently breaks on a failed
loadMore(effectiveQuery) but still proceeds to set this.selectAllMatching and
this.selection from this.flatEnvelopeList, which can silently produce a partial
selection and exceed MAX_SELECT_MESSAGES; change the flow so that if
loadMore(...) returns false you immediately clear/disable select-all mode (set
this.selectAllMatching = false), do not populate this.selection from partial
results, and set this.selectionLimitReached = true; additionally, after the
fetch loop always enforce a strict cap by truncating any assignment to
this.selection (e.g., use the first MAX_SELECT_MESSAGES of
this.flatEnvelopeList) and log the cap, and apply the same guard in the later
block that sets selection (the block around lines 916-919) so both places
consistently abort select-all on failed load and enforce the hard
MAX_SELECT_MESSAGES cap.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 3db28979-8b9d-4a6a-a7bd-2896b8c76045
📒 Files selected for processing (2)
Nextcloud_Mail_Select_All_and_Mass_Selection.mdsrc/components/Mailbox.vue
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
- flatEnvelopeList now mirrors EnvelopeList sortedEnvelops order so shift-click range selection is correct when sortOrder === 'oldest' - onUpdateSelection prunes phantom IDs via a visibleIds filter so the select-all bar can't stay checked after an envelope disappears - onBusSelectAllMatching emits section-selected after mass-select completes so Priority inbox siblings are cleared (the selection watcher skips this emit while loadingAllMatching is true) - loadMore infinite-loop finding: already fixed (returns false on error, loop already breaks on false) — no change needed - Markdown doc: label ASCII-diagram fences as text, update stale @update:checked examples to @update:model-value Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: RobinAngele <git@robin4consulting.com>
When loadMore() returns false (a page fetch error), the loop now sets a loadFailed flag and aborts without populating this.selection from the partial results. This prevents a silent partial selection being shown as if it were complete. Also enforce MAX_SELECT_MESSAGES as a hard cap on the final slice so the selection never exceeds the limit regardless of the loop exit path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: RobinAngele <git@robin4consulting.com>
9f79425 to
af4769f
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
7fad4f3 to
9d1ac5f
Compare
The mailbox() and sortOrder() watchers already reset expanded, but searchQuery() was missing it. After a filter change the component could start in a stale "expanded" state, causing the cached page to be shown instead of triggering a fresh fetch. Signed-off-by: RobinAngele <robin@robin4consulting.com>
9d1ac5f to
ccc59be
Compare
Depends on: #12900
Closes #4285
Closes #7880
Refs #6070
Refs #7276
Refs #11526
Summary
Adds "Select all matching" as a primary button in the search dialog, allowing users to load every message matching the current filter across all pages and select them for bulk actions — completing the feature requested in #4285 and #7880.
Built on #12900 which centralised selection state in
Mailbox.Also adds full Priority inbox support: each section (Favorites, Follow-up, Important, Other) gets its own select-all flow with mutual exclusion — selecting in one section automatically clears any selection in a sibling section, since each section has an independent action toolbar and cross-section operations are not possible.
Screenshots
Search modal — new "Select all matching" primary button
Mass-select result — N selected with action toolbar
Priority inbox mutual exclusion — section cleared toast
Architecture
Component interaction
State flows down via props, changes flow up via Vue events, and cross-instance coordination uses the shared mitt bus.
Key design decisions
The regular folder flow (left side) is straightforward — one
Mailbox, one bus listener, a linear page fetch. The Priority inbox (right side) is where the complexity lives: 4Mailboxinstances share one bus with no direct references between them, each computing a different filtered query. The decisions below address the non-obvious coordination problems this creates.1. Capturing the settled query before loading
Priority inbox sections inject a query suffix one Vue render tick after the parent's
searchQueryprop changes. Starting mass-select immediately would use a stale query and corrupt the cache key. The fix is to raise a lock flag (loadingAllMatching) before yielding — which simultaneously blocks thesearchQuerywatcher from triggering a competing reload — then read the settled, section-filtered query after the tick.2. Mutual exclusion between sections
Each section has its own independent action toolbar, so a selection spanning two sections can't be acted upon in a single bulk operation. When a section's selection goes from empty to non-empty (user-initiated), it broadcasts a
section-selectedevent on the shared bus. Any sibling holding a selection clears itself with an info toast.3. Concurrent mass-selects must not interfere
The search modal triggers mass-select on all 4 sections simultaneously — each independently loads its own filtered pages. Mutual exclusion must not fire on completion: each finishing section would otherwise clear the others' just-loaded selections. So
section-selectedis emitted only on user-initiated transitions, never at the end of a programmatic mass-load.4. OOM protection: 500-message cap
A unified mailbox fans out to N IMAP accounts per page fetch. Without a ceiling, large multi-account selections exhaust client memory. The loop halts at 500 and shows a visible orange warning rather than silently truncating.
5. Keeping the spinner visible during mass-select
Mass-select forces a cache invalidation to guarantee a fresh fetch. Without a guard, this triggers
LoadingSkeleton, hiding the spinner mid-operation. A!loadingAllMatchingcondition inloadEnvelopes()suppresses the skeleton so the spinner stays visible throughout.Changes
SearchMessages.vueNcDialogbutton; "Search" demoted to tertiaryselectAllMatching()setsmatch = 'allof', closes the modal, then emitsselect-all-matchingwith the full computed query string — intentionally nosendQueryEvent()to avoid a concurrent reloadcloseSearchModal()has an idempotency guard — safe to call multiple timesfilterData.mentions: coercesfalseto''somentions:falseis never appended to the IMAP query stringMailboxThread.vue@select-all-matching="onSelectAllMatching"onSearchMessagesonSelectAllMatching(query)setsthis.searchQuery = querybeforethis.bus.emit('select-all-matching')so each Priority inbox section reads the correct parent query when it captureseffectiveQueryafter its own$nextTickMailbox.vueisPriorityInboxprop — controls banner visibility and mutual exclusion behaviourloadingAllMatching/selectAllMatching/selectionLimitReacheddata flagsonBusSelectAllMatching()— full mass-select lifecycle: raises flag, yields tick, clears cache, fetches all pages, caps at 500, selects allloadEnvelopes()—!loadingAllMatchingguard suppressesLoadingSkeletonduring mass-selectselectionwatcher — emitssection-selectedon 0 → non-zero transition, guarded by!loadingAllMatchingonBusSectionSelected(activeQuery)— clears sibling section selection with info toast; self-guard viaactiveQuery !== this.searchQueryselectAllMatchingAction()— in-page banner entry point (callsonBusSelectAllMatching()directly, no bus round-trip)selectAllLabelcomputed — context-aware label:loadingAllMatchingselectModehasFilter!endReached && count > 0select-all-banner— prompts to load all matching when all visible are selected and more exist; hidden for Priority inboxselect-all-limit-warning— shown in warning colour when 500-cap is hitonUpdateSelection()— merged into single array assignment (removes intermediate empty reactive state)unselectAll()— also clearsloadingAllMatchingandexpandedbus.on/off('section-selected', ...)symmetric increated/unmountedpriorityImportantQuery/priorityOtherQueryconstants instead of hardcoding strings inhasFilterEnvelopeList.vue_localToggleInProgressguard on bothselectionandsortedEnvelopswatchers — prevents watchers from overwritingflags.selectedduring a local click; reset via$nextTickto survive Vue's watcher flushHow to test
Known limitations
selectionarray and fire individual API calls. A future improvement would be a server-side endpoint accepting a filter query and performing the operation without enumerating IDs client-side