fix(email): clear bulk selection on context change (#5229)

This commit is contained in:
RaresKeY
2026-07-11 15:12:12 +02:00
committed by GitHub
parent 801c3a2ff1
commit 732b20776c
2 changed files with 56 additions and 0 deletions
+16
View File
@@ -952,8 +952,20 @@ function _libCachePut(key, value) {
}
}
function _resetBulkSelectionForContextChange({ rerender = false } = {}) {
const hadSelection = !!(state._selectedUids && state._selectedUids.size);
const wasSelectMode = !!state._selectMode;
if (state._selectedUids) state._selectedUids.clear();
state._selectMode = false;
if (hadSelection || wasSelectMode) {
_updateBulkBar();
if (rerender) _renderGrid();
}
}
function _resetEmailListForFreshLoad() {
_exitEmailReaderModeForList();
_resetBulkSelectionForContextChange();
state._libOffset = 0;
state._libEmails = [];
state._libTotal = 0;
@@ -2507,6 +2519,7 @@ function _clearFilterPillSideEffect() {
function _addSearchPill(pill) {
if (!pill) return;
_resetBulkSelectionForContextChange({ rerender: true });
if (!Array.isArray(state._libSearchPills)) state._libSearchPills = [];
// Dedup by email (contact), text (text pill), or filter value.
if (pill.type === 'contact') {
@@ -2541,6 +2554,7 @@ function _searchQueryFromPills() {
function _removeSearchPillAt(idx) {
if (!Array.isArray(state._libSearchPills)) return;
_resetBulkSelectionForContextChange({ rerender: true });
const removed = state._libSearchPills[idx];
state._libSearchPills.splice(idx, 1);
if (removed && removed.type === 'filter') _clearFilterPillSideEffect();
@@ -2718,6 +2732,7 @@ async function _initEmailSearchChipBar() {
// directly.
let _libSearchTypingTimer = null;
input.addEventListener('input', async () => {
_resetBulkSelectionForContextChange({ rerender: true });
state._libSearchDraft = input.value;
await _refreshSuggestions();
if (_libSearchTypingTimer) clearTimeout(_libSearchTypingTimer);
@@ -2853,6 +2868,7 @@ window.addEventListener('click', (e) => {
async function _doSearch() {
_exitEmailReaderModeForList();
_resetBulkSelectionForContextChange({ rerender: true });
const seq = ++_libSearchSeq;
const derived = _deriveSearchScope(state._libSearch);
const q = derived.q;
+40
View File
@@ -12,6 +12,16 @@ def _bulk_action_source() -> str:
return text[start:end]
def _function_source(name: str) -> str:
text = _EMAIL_LIBRARY.read_text(encoding="utf-8")
start = text.index(f"function {name}")
next_function = text.find("\nfunction ", start + 1)
next_async = text.find("\nasync function ", start + 1)
candidates = [idx for idx in (next_function, next_async) if idx != -1]
end = min(candidates) if candidates else len(text)
return text[start:end]
def test_email_bulk_read_unread_calls_provider_write_routes():
"""Bulk read/unread must persist to IMAP/provider, not only mutate UI state.
@@ -34,3 +44,33 @@ def test_email_bulk_read_unread_checks_backend_success_before_syncing_cache():
assert "data?.success === false" in src
assert "throw new Error(data?.error" in src
assert "_libCacheWriteBack()" in src
def test_email_context_changes_clear_bulk_selection_state():
"""IMAP UIDs are folder/account scoped, so stale bulk selections must die.
Folder, account, filter, quick-filter, attachment, and search basis changes
must exit select mode before the next list/search view can run bulk actions.
"""
text = _EMAIL_LIBRARY.read_text(encoding="utf-8")
reset_src = _function_source("_resetBulkSelectionForContextChange")
fresh_src = _function_source("_resetEmailListForFreshLoad")
add_pill_src = _function_source("_addSearchPill")
remove_pill_src = _function_source("_removeSearchPillAt")
search_src = text[text.index("async function _doSearch()"):text.index("// Custom dropdown", text.index("async function _doSearch()"))]
assert "state._selectedUids.clear()" in reset_src
assert "state._selectMode = false" in reset_src
assert "_updateBulkBar()" in reset_src
assert "_resetBulkSelectionForContextChange()" in fresh_src
assert "_resetBulkSelectionForContextChange({ rerender: true })" in add_pill_src
assert "_resetBulkSelectionForContextChange({ rerender: true })" in remove_pill_src
assert "_resetBulkSelectionForContextChange({ rerender: true })" in search_src
assert "state._libFolder = e.target.value;" in text
assert "state._libFilter = e.target.value;" in text
assert "state._libHasAttachments = !state._libHasAttachments;" in text
assert "state._libAccountId = btn.dataset.accId || null;" in text
assert text.count("_loadEmailsFresh();") >= 5
assert "state._libSearchDraft = input.value;" in text