feat(unified-mailbox): enable search in the unified views

Enable text AND advanced search in all Unified Mailbox views (the per-role
mailboxes and the folder-selected All mail / Unread / Starred cross views). The
search input was hard-disabled for every unified view; the store fan-out already
supported text search.

- page.tsx: the search text input and the advanced-filter toggle are enabled for
  all unified views (only the scheduled view stays disabled). Clear-search also
  restores a cross view (not just per-role).
- Advanced filters now apply in cross views too: new advancedSearchCrossViewEmails
  ANDs the advanced filter (text + field conditions from buildJMAPFilter, built
  without an inMailbox clause) onto the cross-view membership. Per-role unified
  views keep using advancedSearchUnifiedEmails. Both honor the filter on the first
  page, on load-more, and on the folder-switch re-run. Fixes: an active Starred
  filter not applying after switching into a cross view, and the Unread filter in
  the Unread view returning nothing.
- Search persistence on folder switch: an active search is kept and re-run in the
  target view, preserving advanced filters. handleMailboxSelect picks
  advancedSearch when filters are set (normal, per-role unified, and cross views,
  after setting the unified state), text searchEmails when only a query is set,
  and browses otherwise. The scheduled view is the only view that resets the
  search on enter (unavailable there; setScheduledView clears searchQuery +
  searchFilters).

Account scope is intentionally left unrestricted in search (it already fanned out
across all accounts); the per-view folder selection still applies via
crossIncludedMailboxIds.
This commit is contained in:
Stefan Hildebrandt
2026-07-11 21:14:39 +02:00
parent 7c221c4a4a
commit dc72122ed8
5 changed files with 102 additions and 15 deletions
+35 -10
View File
@@ -1838,7 +1838,18 @@ export default function Home() {
}
const populated = await buildPopulatedUnifiedAccounts();
await fetchUnifiedEmailsAction(populated, role);
// Keep an active search across the switch and re-run it in this view
// (mirrors normal mailboxes), preserving advanced filters; otherwise browse.
if (client && (!isFilterEmpty(searchFilters) || searchQuery)) {
useEmailStore.setState({ isUnifiedView: true, unifiedRole: role, crossView: null });
if (!isFilterEmpty(searchFilters)) {
await advancedSearch(client);
} else {
await searchEmails(client, searchQuery);
}
} else {
await fetchUnifiedEmailsAction(populated, role);
}
refreshUnifiedCounts(populated);
return;
}
@@ -1860,7 +1871,18 @@ export default function Home() {
}
const populated = await buildPopulatedUnifiedAccounts();
await fetchCrossViewAction(populated, view);
// Keep an active search across the switch and re-run it in this view
// (mirrors normal mailboxes), preserving advanced filters; otherwise browse.
if (client && (!isFilterEmpty(searchFilters) || searchQuery)) {
useEmailStore.setState({ isUnifiedView: true, crossView: view, unifiedRole: null });
if (!isFilterEmpty(searchFilters)) {
await advancedSearch(client);
} else {
await searchEmails(client, searchQuery);
}
} else {
await fetchCrossViewAction(populated, view);
}
refreshCrossCounts(populated);
return;
}
@@ -2206,13 +2228,16 @@ export default function Home() {
setSearchQuery("");
clearSearchFilters();
if (!client) return;
// In unified view the active "mailbox" is a virtual role, so refresh via
// the unified fan-out instead of fetchEmails.
// In unified view the active "mailbox" is a virtual role or cross view, so
// refresh via the unified fan-out instead of fetchEmails.
if (isUnifiedView) {
const populated = await buildPopulatedUnifiedAccounts();
const role = useEmailStore.getState().unifiedRole;
const cross = useEmailStore.getState().crossView;
if (role) {
const populated = await buildPopulatedUnifiedAccounts();
await fetchUnifiedEmailsAction(populated, role);
} else if (cross) {
await fetchCrossViewAction(populated, cross);
}
return;
}
@@ -2848,8 +2873,8 @@ export default function Home() {
className={cn("ps-9 h-9", searchQuery && "pe-8")}
data-search-input
data-tour="search-input"
disabled={isUnifiedView || isScheduledView}
title={isUnifiedView ? t("unified_mailbox.search_unavailable") : isScheduledView ? t('email_viewer.scheduled_actions_only') : undefined}
disabled={isScheduledView}
title={isScheduledView ? t('email_viewer.scheduled_actions_only') : undefined}
/>
{searchQuery && (
<button
@@ -2865,15 +2890,15 @@ export default function Home() {
<button
type="button"
onClick={toggleAdvancedSearch}
disabled={isUnifiedView || isScheduledView}
disabled={isScheduledView}
className={cn(
"relative flex-shrink-0 p-2 rounded-md transition-colors",
(isUnifiedView || isScheduledView) && "opacity-50 cursor-not-allowed",
isScheduledView && "opacity-50 cursor-not-allowed",
isAdvancedSearchOpen || activeFilterCount(searchFilters) > 0
? "bg-primary/10 text-primary"
: "text-muted-foreground hover:text-foreground hover:bg-muted"
)}
title={isUnifiedView ? t("unified_mailbox.search_unavailable") : isScheduledView ? t('email_viewer.scheduled_actions_only') : t("advanced_search.toggle_filters")}
title={isScheduledView ? t('email_viewer.scheduled_actions_only') : t("advanced_search.toggle_filters")}
>
<Filter className="w-4 h-4" />
{!isAdvancedSearchOpen && activeFilterCount(searchFilters) > 0 && (