fix: offer every folder as a tree in the search folder picker instead of the first twelve

This commit is contained in:
Linus Rath
2026-10-31 04:55:30 +01:00
parent 56e4a1e051
commit e5c9433a0e
3 changed files with 61 additions and 6 deletions
@@ -0,0 +1,30 @@
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import { searchScopeFolders } from '../search-scope-folders';
import type { Mailbox } from '../../api/types';
// The search panel's Folder picker was a flat list cut at twelve folders, so
// deeper or later folders couldn't be searched on their own (webmail
// lib/search-scope-folders.ts lists them all as a tree).
const box = (id: string, parentId: string | null = null, role: string | null = null) =>
({ id, name: id, parentId, role, sortOrder: 0, totalEmails: 0, unreadEmails: 0, isShared: false } as unknown as Mailbox);
describe('search folder picker', () => {
it('lists every folder, each subfolder after its parent with its depth, without the open one', () => {
const boxes = [box('inbox', null, 'inbox'), ...Array.from({ length: 15 }, (_, i) => box(`f${String(i).padStart(2, '0')}`)), box('child', 'f03')];
const list = searchScopeFolders(boxes, 'inbox');
expect(list).toHaveLength(16);
const ids = list.map((m) => m.id);
expect(ids.indexOf('child')).toBe(ids.indexOf('f03') + 1);
expect(list.find((m) => m.id === 'child')?.depth).toBe(1);
expect(ids).not.toContain('inbox');
});
it('is what the panel shows, uncut', () => {
const list = fs.readFileSync(path.resolve(__dirname, '../../screens/EmailListScreen.tsx'), 'utf8');
expect(list).not.toMatch(/\.slice\(0, 12\)/);
expect(list).toMatch(/searchScopeFolders\(scopedMailboxes, currentMailboxId\)\.map/);
});
});
+22
View File
@@ -0,0 +1,22 @@
import type { Mailbox } from '../api/types';
import { buildMailboxTree, flattenVisible, type MailboxNode } from './mailbox-tree';
/**
* The folders the search panel's Folder picker offers: every folder (it was
* cut at twelve), in sidebar order with each subfolder right after its parent
* and its `depth` for indenting (webmail lib/search-scope-folders.ts). The
* open folder is left out: "This folder" picks it. A shared account's
* grouping row is no folder and is left out too.
*/
export function searchScopeFolders(mailboxes: Mailbox[], currentMailboxId: string | null): MailboxNode[] {
const tree = buildMailboxTree(mailboxes);
const all = new Set<string>();
const collect = (nodes: MailboxNode[]) => {
for (const n of nodes) {
all.add(n.id);
collect(n.children);
}
};
collect(tree);
return flattenVisible(tree, all).filter((n) => !n.isAccountNode && n.id !== currentMailboxId);
}
+9 -6
View File
@@ -64,6 +64,7 @@ import { isPermanentDelete, confirmPermanentDelete } from '../lib/delete-confirm
import { confirmMailRemoval } from '../lib/mail-removal-confirm'; import { confirmMailRemoval } from '../lib/mail-removal-confirm';
import { moveSheetCurrentId } from '../lib/move-targets'; import { moveSheetCurrentId } from '../lib/move-targets';
import { listViewKey } from '../lib/list-view-key'; import { listViewKey } from '../lib/list-view-key';
import { searchScopeFolders } from '../lib/search-scope-folders';
import { draftContextFromEmail, isDraftEmail } from '../lib/draft-context'; import { draftContextFromEmail, isDraftEmail } from '../lib/draft-context';
import { getFullEmail, emptyMailbox as apiEmptyMailbox } from '../api/email'; import { getFullEmail, emptyMailbox as apiEmptyMailbox } from '../api/email';
import type { RootStackParamList } from '../navigation/types'; import type { RootStackParamList } from '../navigation/types';
@@ -1854,17 +1855,18 @@ export default function EmailListScreen({
active={folderScope === 'current'} active={folderScope === 'current'}
onPress={() => setFolderScope('current')} onPress={() => setFolderScope('current')}
/> />
{scopedMailboxes </View>
.filter((m) => m.id !== currentMailboxId) {/* Every folder, as a tree (it was a flat list cut at 12). */}
.slice(0, 12) <View style={styles.scopeFolderList}>
.map((m) => ( {searchScopeFolders(scopedMailboxes, currentMailboxId).map((m) => (
<View key={m.id} style={{ marginLeft: m.depth * spacing.lg }}>
<ScopeChip <ScopeChip
key={m.id}
label={localizeMailboxName(m.role, m.name, t)} label={localizeMailboxName(m.role, m.name, t)}
active={folderScope === m.id} active={folderScope === m.id}
onPress={() => setFolderScope(m.id)} onPress={() => setFolderScope(m.id)}
/> />
))} </View>
))}
</View> </View>
</View> </View>
@@ -2427,6 +2429,7 @@ function makeStyles(c: ThemePalette) {
gap: spacing.sm, gap: spacing.sm,
marginTop: 4, marginTop: 4,
}, },
scopeFolderList: { alignItems: 'flex-start', gap: spacing.xs, marginTop: spacing.sm },
triToggle: { triToggle: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',