Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
72a77ea
fix: add missing ImportantIcon and correct favorite/unfavorite bulk a…
RobinAngele May 7, 2026
e9891ec
feat: add select-all checkbox and lift selection state to parent
RobinAngele May 7, 2026
c575d0f
feat: add filter-based mass selection via search modal
RobinAngele May 7, 2026
de2e6c4
fix: keep select-all checkbox checked after manual deselection
RobinAngele May 8, 2026
4fe9490
fix: context-aware hint — remove 'use filter to refine' when filter i…
RobinAngele May 8, 2026
696869d
fix: show actual selection count when messages are selected
RobinAngele May 8, 2026
4ed40f4
fix: prevent race condition when deselecting via avatar click
RobinAngele May 8, 2026
04a02f4
fix: label shows 'Selecting messages…' during mass loading
RobinAngele May 8, 2026
5a8147b
fix: use $nextTick to reset _localToggleInProgress after watcher flush
RobinAngele May 8, 2026
dbf3962
fix: guard onSelectMultiple against accidental non-shift triggers
RobinAngele May 8, 2026
26a2dc0
fix: robust filter detection using query length instead of string com…
RobinAngele May 8, 2026
6fcc48b
fix: reset endReached when search query changes
RobinAngele May 8, 2026
c78a1cf
fix: use regex to detect real filter instead of length check
RobinAngele May 8, 2026
0b1ab45
fix: prevent false boolean from leaking into search query
RobinAngele May 8, 2026
3369b4f
fix: show Loading… during any envelope load, not just mass-select
RobinAngele May 8, 2026
716bfd2
fix: guard sortedEnvelops watcher against local toggle race
RobinAngele May 8, 2026
0d954d9
fix: only disable checkbox and show spinner during mass-load
RobinAngele May 8, 2026
000dc34
fix: emit select-all-matching synchronously, prevent double sendQuery…
RobinAngele May 8, 2026
ad7af06
fix: delay _closingProgrammatically reset to next tick
RobinAngele May 8, 2026
d87c714
refactor: optimize filter detection, simplify dialog close, merge met…
RobinAngele May 8, 2026
a32fd58
fix: delay select-all-matching emit to let Vue flush searchQuery prop
RobinAngele May 8, 2026
6c4326d
fix: pass searchQuery directly via bus event to avoid Vue prop batching
RobinAngele May 8, 2026
10500bf
fix: guard watcher against overwriting bus handler state
RobinAngele May 8, 2026
f5bb79c
fix: use transient _busHandlerActive flag instead of persistent selec…
RobinAngele May 8, 2026
6c7ced1
fix: restore sendQueryEvent in closeSearchModal for normal search
RobinAngele May 8, 2026
d9999b2
fix: pass query directly to loadEnvelopes to avoid stale prop
RobinAngele May 8, 2026
729f595
feat: Priority inbox mutual exclusion and mass-select robustness
RobinAngele May 10, 2026
2a9c643
fix: import priorityInbox constants instead of hardcoding strings
RobinAngele May 10, 2026
078094f
fix: use @update:model-value on NcCheckboxRadioSwitch
RobinAngele May 10, 2026
38c055c
fix: remove potentially misleading count from select-all banner
RobinAngele May 10, 2026
89f4882
fix: skip 'loaded' label in Priority inbox sections
RobinAngele May 10, 2026
1d923ac
docs: update selectAllLabel JSDoc to reflect isPriorityInbox condition
RobinAngele May 10, 2026
819f23e
fix: address CodeRabbit findings from second review
RobinAngele May 10, 2026
c6c48a5
fix: abort mass-select cleanly on loadMore failure
RobinAngele May 10, 2026
5c43ca3
fix: reset expanded flag in searchQuery watcher
RobinAngele May 10, 2026
994adaf
fix: prevent spurious toast and improve warning visibility
RobinAngele May 11, 2026
acbc3f5
fix: suppress empty state and fix count during mass-select
RobinAngele May 11, 2026
cce8683
fix: suppress toast and restore mutual exclusion after mass-select
RobinAngele May 11, 2026
5daf822
refactor: remove redundant differenceWith cleanup and fix MD fences
RobinAngele May 11, 2026
4a0ba12
fix: exclude favoritesQuery from hasFilter to show correct select-all…
RobinAngele May 11, 2026
81acbde
fix: select-all banner never appeared and hint showed wrong text for …
RobinAngele May 11, 2026
99362fc
fix: suppress scroll hint for manual-pagination sections
RobinAngele May 11, 2026
6f86c43
chore: drop student doc per maintainer request
RobinAngele May 11, 2026
0408e03
fix: typo in log message and clarify unselectAll collapse intent
RobinAngele May 11, 2026
9f111f1
fix(ux): improve clarity of select-all hints and status messages
RobinAngele May 12, 2026
088a749
fix(ux): improve clarity of select-all hints and status messages
RobinAngele May 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/components/Envelope.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
:is-important="isImportant"
@click.exact="onClick"
@click.ctrl.exact.prevent="toggleSelected"
@click.shift.exact.prevent="onSelectMultiple"
@click.shift.exact.prevent="onSelectMultiple($event)"
@delete="onDelete"
@toggle-important="onToggleImportant"
@toggle-seen="onToggleSeen"
Expand Down Expand Up @@ -78,7 +78,7 @@
<template v-else>
<div
@click.stop.exact.prevent="toggleSelected"
@click.shift.exact.prevent="onSelectMultiple">
@click.shift.exact.prevent="onSelectMultiple($event)">
<template v-if="hoveringAvatar || selected">
<CheckIcon
:size="28"
Expand Down Expand Up @@ -1129,7 +1129,10 @@ export default {
}
},

onSelectMultiple() {
onSelectMultiple(event) {
if (!event.shiftKey) {
return
}
this.$emit('select-multiple')
},

Expand Down
95 changes: 69 additions & 26 deletions src/components/EnvelopeList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@
v-if="isAtLeastOneSelectedFavorite"
variant="tertiary"
:title="n('mail', 'Unfavorite {number}', 'Unfavorite {number}', selection.length, { number: selection.length })"
@click.prevent="favoriteAll">
@click.prevent="unfavoriteAll">
<IconUnFavorite :size="20" />
</NcButton>

<NcButton
v-if="isAtLeastOneSelectedUnFavorite"
variant="tertiary"
:title="n('mail', 'Favorite {number}', 'Favorite {number}', selection.length, { number: selection.length })"
@click.prevent="unFavoriteAll">
@click.prevent="favoriteAll">
<IconFavorite :size="20" />
</NcButton>

Expand Down Expand Up @@ -167,11 +167,12 @@
import { showError } from '@nextcloud/dialogs'
import { NcActionButton as ActionButton, NcActions as Actions, NcButton, NcDialog } from '@nextcloud/vue'
import { mapStores } from 'pinia'
import { differenceWith } from 'ramda'

import AlertOctagonIcon from 'vue-material-design-icons/AlertOctagonOutline.vue'
import IconSelect from 'vue-material-design-icons/CloseThick.vue'
import EmailRead from 'vue-material-design-icons/EmailOpenOutline.vue'
import EmailUnread from 'vue-material-design-icons/EmailOutline.vue'
import ImportantIcon from 'vue-material-design-icons/LabelVariant.vue'
import ImportantOutlineIcon from 'vue-material-design-icons/LabelVariantOutline.vue'
import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue'
import AddIcon from 'vue-material-design-icons/Plus.vue'
Expand Down Expand Up @@ -204,6 +205,7 @@ export default {
ActionButton,
Envelope,
IconDelete,
ImportantIcon,
ImportantOutlineIcon,
IconFavorite,
IconSelect,
Expand Down Expand Up @@ -263,11 +265,20 @@ export default {
type: Boolean,
default: false,
},

selection: {
type: Array,
default: () => [],
},

flatIndex: {
type: Number,
default: 0,
},
},

data() {
return {
selection: [],
showMoveModal: false,
showTagModal: false,
lastToggledIndex: undefined,
Expand Down Expand Up @@ -359,14 +370,37 @@ export default {
},

watch: {
sortedEnvelops(newVal, oldVal) {
// Unselect vanished envelopes
const newIds = newVal.map((env) => env.databaseId)
this.selection = this.selection.filter((id) => newIds.includes(id))
differenceWith((a, b) => a.databaseId === b.databaseId, oldVal, newVal)
.forEach((env) => {
env.flags.selected = false
selection: {
handler(newSelection) {
// Skip sync during local toggle to avoid race condition
// where the watcher overwrites flags.selected set by
// a local click before emitLocalSelection reads it.
if (this._localToggleInProgress) {
return
}
// Sync flags.selected with the global selection prop.
// This ensures checkboxes stay correct when another
// EnvelopeList instance changes the selection (e.g. shift-click
// across groups, or Select All / Unselect All).
const selectionSet = new Set(newSelection)
this.sortedEnvelops.forEach((env) => {
env.flags.selected = selectionSet.has(env.databaseId)
})
},
immediate: true,
},

sortedEnvelops(newVal, oldVal) {
// Skip if a local toggle is in progress to avoid race conditions
if (this._localToggleInProgress) {
return
}
// 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)
}
},
},

Expand Down Expand Up @@ -451,23 +485,21 @@ export default {
this.unselectAll()
},

favoriteAll() {
const favFlag = !this.isAtLeastOneSelectedUnFavorite
unfavoriteAll() {
this.selectedEnvelopes.forEach((envelope) => {
this.mainStore.markEnvelopeFavoriteOrUnfavorite({
envelope,
favFlag,
favFlag: false,
})
})
this.unselectAll()
},

unFavoriteAll() {
const favFlag = !this.isAtLeastOneSelectedFavorite
favoriteAll() {
this.selectedEnvelopes.forEach((envelope) => {
this.mainStore.markEnvelopeFavoriteOrUnfavorite({
envelope,
favFlag,
favFlag: true,
})
})
this.unselectAll()
Expand Down Expand Up @@ -537,16 +569,27 @@ export default {
const alreadySelected = this.selection.includes(envelope.databaseId)
if (selected && !alreadySelected) {
envelope.flags.selected = true
this.selection.push(envelope.databaseId)
} else if (!selected && alreadySelected) {
envelope.flags.selected = false
this.selection.splice(this.selection.indexOf(envelope.databaseId), 1)
}
},

emitLocalSelection() {
const localIds = this.sortedEnvelops
.filter((env) => env.flags.selected)
.map((env) => env.databaseId)
this.$emit('update:selection', localIds, this.envelopes)
},

onEnvelopeSelectToggle(envelope, index, selected) {
this.lastToggledIndex = index
this._localToggleInProgress = true
this.setEnvelopeSelected(envelope, selected)
this.emitLocalSelection()
// Reset after next tick — Vue batches watchers at end of tick
this.$nextTick(() => {
this._localToggleInProgress = false
})
},

onEnvelopeSelectMultiple(envelope, index) {
Expand All @@ -557,20 +600,20 @@ export default {
return
}

const start = Math.min(lastToggledIndex, index)
const end = Math.max(lastToggledIndex, index)
const selected = this.selection.includes(envelope.databaseId)
for (let i = start; i <= end; i++) {
this.setEnvelopeSelected(this.sortedEnvelops[i], !selected)
}
// Convert to global flat indices and delegate to the parent
const globalFrom = this.flatIndex + lastToggledIndex
const globalTo = this.flatIndex + index
// If the clicked envelope is already selected, deselect the range
const deselect = this.selection.includes(envelope.databaseId)
this.$emit('select-range', globalFrom, globalTo, deselect)
this.lastToggledIndex = index
},

unselectAll() {
this.sortedEnvelops.forEach((env) => {
env.flags.selected = false
})
this.selection = []
this.$emit('update:selection', [], this.envelopes)
},

onOpenMoveModal() {
Expand Down
Loading