diff --git a/components/settings/__tests__/folder-settings-role-order.test.tsx b/components/settings/__tests__/folder-settings-role-order.test.tsx new file mode 100644 index 000000000..348c9b23e --- /dev/null +++ b/components/settings/__tests__/folder-settings-role-order.test.tsx @@ -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()), + SettingsSection: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +function makeMailbox(overrides: Partial): 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(); + + 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']); + }); +}); diff --git a/components/settings/folder-settings.tsx b/components/settings/folder-settings.tsx index 27c0cd679..7934cc777 100644 --- a/components/settings/folder-settings.tsx +++ b/components/settings/folder-settings.tsx @@ -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 */} {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(); - 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)})`