diff --git a/components/settings/__tests__/all-mail-folder-names.test.tsx b/components/settings/__tests__/all-mail-folder-names.test.tsx new file mode 100644 index 000000000..9a4de72c6 --- /dev/null +++ b/components/settings/__tests__/all-mail-folder-names.test.tsx @@ -0,0 +1,63 @@ +import { render, screen } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { Mailbox } from '@/lib/jmap/types'; +import { useAccountStore } from '@/stores/account-store'; +import { useEmailStore } from '@/stores/email-store'; +import { usePolicyStore } from '@/stores/policy-store'; +import { useSettingsStore } from '@/stores/settings-store'; +import { MultipleAccountsSection } from '../layout-settings'; + +// settings-9 (usability pass 2026-11-04): the folders that feed All mail are +// named as in the sidebar, not by the server ("Sent Items", "Junk Mail"). + +vi.mock('../settings-section', async (importOriginal) => ({ + ...(await importOriginal()), + SettingsSection: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +const mailbox = (id: string, name: string, role?: string) => + ({ id, name, role, sortOrder: 0, totalEmails: 0, unreadEmails: 0, isShared: false, myRights: {} } as unknown as Mailbox); + +const initial = { + account: useAccountStore.getState(), + email: useEmailStore.getState(), + policy: usePolicyStore.getState(), + settings: useSettingsStore.getState(), +}; + +beforeEach(() => { + useAccountStore.setState({ + accounts: [ + { id: 'a', email: 'a@example.org', displayName: 'A', isConnected: true }, + { id: 'b', email: 'b@example.org', displayName: 'B', isConnected: true }, + ], + activeAccountId: 'a', + } as never); + useEmailStore.setState({ + mailboxes: [mailbox('sent', 'Sent Items', 'sent'), mailbox('junk', 'Junk Mail', 'junk'), mailbox('work', 'Work')], + } as never); + usePolicyStore.setState({ + isSettingLocked: () => false, + isSettingHidden: () => false, + isFeatureEnabled: () => true, + } as never); + useSettingsStore.setState({ enableUnifiedMailbox: true, enableCrossAllView: true, allMailFolderIds: {} }); +}); + +afterEach(() => { + useAccountStore.setState(initial.account, true); + useEmailStore.setState(initial.email, true); + usePolicyStore.setState(initial.policy, true); + useSettingsStore.setState(initial.settings, true); +}); + +describe('the All mail folder list', () => { + it('names role folders as the sidebar does', () => { + render(); + expect(screen.getByText('mailboxes.sent')).toBeInTheDocument(); + expect(screen.getByText('mailboxes.spam')).toBeInTheDocument(); + expect(screen.getByText('Work')).toBeInTheDocument(); + expect(screen.queryByText('Sent Items')).toBeNull(); + expect(screen.queryByText('Junk Mail')).toBeNull(); + }); +}); diff --git a/components/settings/__tests__/folder-settings-bulk-move.test.tsx b/components/settings/__tests__/folder-settings-bulk-move.test.tsx index b90ce5d83..a4a83b401 100644 --- a/components/settings/__tests__/folder-settings-bulk-move.test.tsx +++ b/components/settings/__tests__/folder-settings-bulk-move.test.tsx @@ -84,7 +84,8 @@ describe('FolderSettings bulk move (#1173)', () => { fireEvent.click(checkboxFor('A3'), { shiftKey: true }); expect(['A1', 'A2', 'A3'].map(isTicked)).toEqual([true, true, true]); - expect(['Inbox', 'A4', 'Old mailbox'].map(isTicked)).toEqual([false, false, false]); + // The Inbox row carries the sidebar's name (mailboxes.inbox under the key-echo mock). + expect(['mailboxes.inbox', 'A4', 'Old mailbox'].map(isTicked)).toEqual([false, false, false]); expect(screen.getByText('selected_count')).toBeTruthy(); }); diff --git a/components/settings/__tests__/folder-settings-role-label.test.tsx b/components/settings/__tests__/folder-settings-role-label.test.tsx index 5931912b2..4880cf8c5 100644 --- a/components/settings/__tests__/folder-settings-role-label.test.tsx +++ b/components/settings/__tests__/folder-settings-role-label.test.tsx @@ -5,8 +5,9 @@ import { useEmailStore } from '@/stores/email-store'; import { useAuthStore } from '@/stores/auth-store'; import type { Mailbox } from '@/lib/jmap/types'; -// #495: Stalwart's Scheduled mailbox carries role "scheduled"; the folder list -// renders `role_` and the key must exist (see translations.test.ts). +// Folders with a role are named as in the sidebar (settings-9, 2026-11-04): +// the server's "Sent Items" or "Junk Mail" never shows, and no second chip +// repeats the role next to the name. vi.mock('../settings-section', async (importOriginal) => ({ ...(await importOriginal()), @@ -39,21 +40,51 @@ function makeMailbox(overrides: Partial): Mailbox { } as Mailbox; } -describe('FolderSettings role label for the Scheduled mailbox (#495)', () => { +describe('FolderSettings names role folders as the sidebar does', () => { beforeEach(() => { useAuthStore.setState({ client: null } as never); useEmailStore.setState({ mailboxes: [ makeMailbox({ id: 'inbox', name: 'Inbox', role: 'inbox' }), - makeMailbox({ id: 'sched', name: 'Scheduled', role: 'scheduled' as never }), + makeMailbox({ id: 'sent', name: 'Sent Items', role: 'sent', sortOrder: 1 }), + makeMailbox({ id: 'junk', name: 'Junk Mail', role: 'junk', sortOrder: 2 }), + makeMailbox({ id: 'trash', name: 'Deleted Items', role: 'trash', sortOrder: 3 }), + makeMailbox({ id: 'sched', name: 'Scheduled', role: 'scheduled' as never, sortOrder: 4 }), + makeMailbox({ id: 'work', name: 'Work', sortOrder: 5 }), ], fetchMailboxes: vi.fn(), } as never); }); - it('renders the role badge through the role_scheduled key', () => { + it('shows the sidebar name for role folders, never the server name', () => { render(); - // The test i18n mock echoes the key, so the badge shows the key itself. - expect(screen.getByText('role_scheduled')).toBeInTheDocument(); + // The test i18n mock echoes the key: the sidebar's mailboxes. keys. + const rowName = (text: string) => screen.getAllByText(text).find(el => el.tagName === 'SPAN'); + expect(rowName('mailboxes.sent')).toBeTruthy(); + expect(rowName('mailboxes.spam')).toBeTruthy(); + expect(rowName('mailboxes.trash')).toBeTruthy(); + for (const serverName of ['Sent Items', 'Junk Mail', 'Deleted Items']) { + expect(screen.queryByText(serverName)).toBeNull(); + expect(screen.queryByTitle(serverName)).toBeNull(); + } + // Folders the sidebar has no name for keep their own. + expect(rowName('Work')).toBeTruthy(); + expect(rowName('Scheduled')).toBeTruthy(); + }); + + it('draws no role chip next to the name', () => { + render(); + // role_ is only the label of its row in Standard folder roles. + for (const role of ['inbox', 'sent', 'junk', 'trash']) { + expect(screen.getAllByText(`role_${role}`)).toHaveLength(1); + } + expect(screen.queryByText('role_scheduled')).toBeNull(); + }); + + it('offers the sidebar names in the move-to and role pickers', () => { + render(); + const options = Array.from(document.querySelectorAll('option')).map(o => o.textContent); + expect(options).not.toContain('Sent Items'); + expect(options).not.toContain('Junk Mail'); }); }); diff --git a/components/settings/folder-settings.tsx b/components/settings/folder-settings.tsx index 19aa2a4d5..5ec6b278c 100644 --- a/components/settings/folder-settings.tsx +++ b/components/settings/folder-settings.tsx @@ -40,6 +40,7 @@ import { } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { isFolderCountMode } from '@/lib/folder-counts'; +import { localizeMailboxName } from '@/lib/mailbox-label'; // Must match the per-level indentation used in the row styles below: dragging // a folder this many pixels right/left targets one nesting level deeper/higher. @@ -164,6 +165,11 @@ function SortableFolderRow({ id, title, children }: { id: string; title: string; export function FolderSettings() { const t = useTranslations('settings.folders'); + const tSidebar = useTranslations('sidebar'); + // Folders with a role read as in the sidebar (Sent, Spam, Trash), never + // by the server's name ("Sent Items", "Junk Mail"). + const folderName = (mb: { role?: string | null; name: string }): string => + localizeMailboxName(mb.role, mb.name, (k) => tSidebar(`mailboxes.${k}`)); const { client } = useAuthStore(); const { mailboxes, fetchMailboxes, createMailbox, renameMailbox, deleteMailbox, setMailboxRole, reorderMailboxes, moveMailbox, moveMailboxes } = useEmailStore(); @@ -304,15 +310,15 @@ export function FolderSettings() { }; moveRoots.forEach(blockSubtree); - const getFolderPath = (mb: { parentId?: string; name: string }): string => { - const parts = [mb.name]; + const getFolderPath = (mb: { parentId?: string; name: string; role?: string | null }): string => { + const parts = [folderName(mb)]; const seen = new Set(); let parentId = mb.parentId; while (parentId && !seen.has(parentId)) { seen.add(parentId); const parent = ownMailboxes.find(p => p.id === parentId); if (!parent) break; - parts.unshift(parent.name); + parts.unshift(folderName(parent)); parentId = parent.parentId; } return parts.join(' / '); @@ -515,7 +521,8 @@ export function FolderSettings() { const renderCreateInline = (parentId: string | null, depth: number) => { if (!isCreating || creatingParentId !== parentId) return null; - const parentName = parentId ? ownMailboxes.find(m => m.id === parentId)?.name : null; + const parent = parentId ? ownMailboxes.find(m => m.id === parentId) : undefined; + const parentName = parent ? folderName(parent) : null; return (
{ if (e.shiftKey) e.preventDefault(); }} @@ -658,7 +665,7 @@ export function FolderSettings() { {/* Expand/collapse toggle for folders with children */} {hasChildren ? ( toggleExpanded(mb.id)} className="h-5 w-5 text-muted-foreground flex-shrink-0" @@ -707,12 +714,7 @@ export function FolderSettings() { /> )}
- {mb.name} - {mb.role && ( - - {t(`role_${mb.role}`)} - - )} + {folderName(mb)} {mb.unreadEmails > 0 && ( {mb.unreadEmails} @@ -722,7 +724,7 @@ export function FolderSettings() { {/* Phone: the counter and the row actions don't fit next to the name, so they sit behind a ⋮ (right-click opens it too). */} openContextMenuFrom(e.currentTarget, mb, 'end')} > @@ -732,7 +734,7 @@ export function FolderSettings() {