fix: sort folders in the role assignment dropdown #984

This commit is contained in:
Linus Rath
2026-09-07 22:34:47 +02:00
parent c633ce1ff4
commit f4d2fd950c
2 changed files with 79 additions and 3 deletions
@@ -0,0 +1,72 @@
import { render, screen, within } from '@testing-library/react';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { FolderSettings } from '../folder-settings';
import { useEmailStore } from '@/stores/email-store';
import { useAuthStore } from '@/stores/auth-store';
import type { Mailbox } from '@/lib/jmap/types';
// #984: the role assignment dropdowns listed folders in raw server order.
// They must follow the sidebar order the folder tree produces.
vi.mock('../settings-section', async (importOriginal) => ({
...(await importOriginal<typeof import('../settings-section')>()),
SettingsSection: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
function makeMailbox(overrides: Partial<Mailbox>): Mailbox {
return {
id: 'mb',
name: 'Folder',
sortOrder: 0,
totalEmails: 0,
unreadEmails: 0,
totalThreads: 0,
unreadThreads: 0,
myRights: {
mayReadItems: true,
mayAddItems: true,
mayRemoveItems: true,
maySetSeen: true,
maySetKeywords: true,
mayCreateChild: true,
mayRename: true,
mayDelete: true,
maySubmit: true,
},
isSubscribed: true,
isShared: false,
...overrides,
} as Mailbox;
}
describe('FolderSettings role dropdown order (#984)', () => {
beforeEach(() => {
useAuthStore.setState({ client: null } as never);
useEmailStore.setState({
// Deliberately unsorted: a child before its parent, a plain folder
// before the Inbox, and names out of alphabetical order.
mailboxes: [
makeMailbox({ id: 'zeta', name: 'Zeta' }),
makeMailbox({ id: 'proj-b', name: 'Beta', parentId: 'projects' }),
makeMailbox({ id: 'inbox', name: 'Inbox', role: 'inbox' }),
makeMailbox({ id: 'projects', name: 'Projects' }),
makeMailbox({ id: 'proj-a', name: 'Alpha', parentId: 'projects' }),
makeMailbox({ id: 'alpha', name: 'Alpha' }),
],
fetchMailboxes: vi.fn(),
} as never);
});
it('lists options in folder-tree order, children under their parent', () => {
render(<FolderSettings />);
const selects = screen.getAllByRole('combobox');
expect(selects.length).toBeGreaterThan(0);
const values = within(selects[0] as HTMLElement)
.getAllByRole('option')
.map((o) => (o as HTMLOptionElement).value)
.filter((v) => v !== '');
expect(values).toEqual(['inbox', 'alpha', 'projects', 'proj-a', 'proj-b', 'zeta']);
});
});
+7 -3
View File
@@ -16,7 +16,7 @@ import {
AlertTriangle, NotebookPen, CalendarClock, BellOff,
type LucideIcon,
} from 'lucide-react';
import { cn, buildMailboxTree, type MailboxNode } from '@/lib/utils';
import { cn, buildMailboxTree, flattenMailboxTree, type MailboxNode } from '@/lib/utils';
import {
flattenVisibleTree, removeSubtree, getProjection, getDropPlan,
type FlatFolder,
@@ -670,9 +670,13 @@ export function FolderSettings() {
{/* Standard Folder Roles - advanced section */}
<SettingsSection title={t('standard_roles')} description={t('standard_roles_description')}>
{STANDARD_ROLES.map((role) => {
// Offer the folders in sidebar order (the built tree: role priority,
// then name, children under their parent) rather than raw server
// order, so the dropdown reads like the folder list. (#984)
const orderedMailboxes = flattenMailboxTree(folderTree);
// Disambiguate duplicate folder names by appending parent path
const nameCounts = new Map<string, number>();
ownMailboxes.forEach(mb => nameCounts.set(mb.name, (nameCounts.get(mb.name) || 0) + 1));
orderedMailboxes.forEach(mb => nameCounts.set(mb.name, (nameCounts.get(mb.name) || 0) + 1));
const getParentPath = (mb: { parentId?: string; name: string }) => {
if (!mb.parentId) return '';
const parent = ownMailboxes.find(p => p.id === mb.parentId);
@@ -686,7 +690,7 @@ export function FolderSettings() {
onChange={(value) => handleRoleChange(role, value)}
options={[
{ value: '', label: t('role_none') },
...ownMailboxes.map(mb => ({
...orderedMailboxes.map(mb => ({
value: mb.id,
label: (nameCounts.get(mb.name) || 0) > 1
? `${getParentPath(mb)}${mb.name} (${mb.id.slice(-6)})`