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
@@ -6,6 +6,7 @@ import {
buildCrossFilter,
getCrossUnreadTotal,
fetchCrossViewEmails,
advancedSearchCrossViewEmails,
resolveSourceFolderName,
type UnifiedAccountClient,
} from '@/lib/unified-mailbox';
@@ -203,3 +204,28 @@ describe('fetchCrossViewEmails', () => {
expect(result.errors.get('bad')).toBe('boom');
});
});
describe('advancedSearchCrossViewEmails', () => {
it('ANDs the advanced filter onto the cross-view membership', async () => {
const advancedSearchEmails = vi.fn().mockResolvedValue({ emails: [], total: 0, hasMore: false });
const a = makeAccount({ accountId: 'a', mailboxes: [mb('inbox', 'inbox')] }, { advancedSearchEmails });
await advancedSearchCrossViewEmails([a], 'all', { hasKeyword: '$flagged' }, 50, 0);
const [filter] = advancedSearchEmails.mock.calls[0];
expect(filter).toEqual({
operator: 'AND',
conditions: [{ inMailbox: 'inbox' }, { hasKeyword: '$flagged' }],
});
});
it('uses only the membership filter when the extra filter is empty', async () => {
const advancedSearchEmails = vi.fn().mockResolvedValue({ emails: [], total: 0, hasMore: false });
const a = makeAccount({ accountId: 'a', mailboxes: [mb('inbox', 'inbox')] }, { advancedSearchEmails });
await advancedSearchCrossViewEmails([a], 'all', {}, 50, 0);
const [filter] = advancedSearchEmails.mock.calls[0];
expect(filter).toEqual({ inMailbox: 'inbox' });
});
});
+23
View File
@@ -465,6 +465,29 @@ export async function searchCrossViewEmails(
));
}
/**
* Like `searchCrossViewEmails`, but applies an advanced filter (text + field
* conditions from `buildJMAPFilter`, built WITHOUT an `inMailbox` clause) on top
* of the cross-view membership. `extraFilter` may be empty ({}), in which case
* only the membership filter is used (equivalent to a plain browse).
*/
export async function advancedSearchCrossViewEmails(
accounts: UnifiedAccountClient[],
view: CrossView,
extraFilter: Record<string, unknown>,
limit: number,
position: number,
): Promise<UnifiedFetchResult> {
const hasExtra = Object.keys(extraFilter).length > 0;
return fanOutCrossQuery(accounts, (account, jmapAccountId, ids) => {
const membership = buildCrossFilter(view, ids);
const filter = hasExtra
? { operator: 'AND', conditions: [membership, extraFilter] }
: membership;
return account.client.advancedSearchEmails(filter, jmapAccountId, limit, position);
});
}
/**
* Returns the list of unified roles that exist in at least one account's
* mailboxes.