diff --git a/components/settings/__tests__/folder-settings-row-menu.test.tsx b/components/settings/__tests__/folder-settings-row-menu.test.tsx new file mode 100644 index 000000000..1eb297320 --- /dev/null +++ b/components/settings/__tests__/folder-settings-row-menu.test.tsx @@ -0,0 +1,73 @@ +import { fireEvent, 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 { useSettingsStore } from '@/stores/settings-store'; +import type { Mailbox } from '@/lib/jmap/types'; + +// On the phone the counter select and the row's icon buttons left no room +// for the folder name. They sit behind a ⋮ there, which right-click opens +// too, and the name keeps its full text in a tooltip. + +vi.mock('../settings-section', async (importOriginal) => ({ + ...(await importOriginal()), + SettingsSection: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +const rights = { + mayReadItems: true, mayAddItems: true, mayRemoveItems: true, maySetSeen: true, maySetKeywords: true, + mayCreateChild: true, mayRename: true, mayDelete: true, maySubmit: true, +}; + +function makeMailbox(overrides: Partial): Mailbox { + return { + id: 'mb', name: 'Folder', sortOrder: 0, totalEmails: 0, unreadEmails: 0, totalThreads: 0, unreadThreads: 0, + myRights: rights, isSubscribed: true, isShared: false, ...overrides, + } as Mailbox; +} + +describe('FolderSettings row actions menu', () => { + beforeEach(() => { + useAuthStore.setState({ client: null } as never); + useSettingsStore.setState({ folderCountModes: {} } as never); + useEmailStore.setState({ + mailboxes: [makeMailbox({ id: 'projects', name: 'Projects with a long name' })], + fetchMailboxes: vi.fn(), + } as never); + }); + + it('opens the row actions from a named ⋮ and sets the counter from it', () => { + render(); + expect(screen.getByTitle('Projects with a long name')).toBeTruthy(); + + fireEvent.click(screen.getByRole('button', { name: 'actions' })); + const menu = screen.getByRole('menu'); + expect(within(menu).getByRole('menuitem', { name: 'create_subfolder' })).toBeTruthy(); + expect(within(menu).getByRole('menuitem', { name: 'rename' })).toBeTruthy(); + expect(within(menu).getByRole('menuitem', { name: 'delete' })).toBeTruthy(); + + fireEvent.click(within(menu).getByRole('menuitem', { name: 'counter' })); + fireEvent.click(screen.getByRole('menuitem', { name: 'count_mode_none' })); + expect(useSettingsStore.getState().folderCountModes).toEqual({ projects: 'none' }); + }); + + it('opens the same menu on right-click and starts renaming from it', () => { + render(); + fireEvent.contextMenu(screen.getByTitle('Projects with a long name'), { clientX: 20, clientY: 20 }); + fireEvent.click(screen.getByRole('menuitem', { name: 'rename' })); + expect((screen.getByDisplayValue('Projects with a long name') as HTMLInputElement).tagName).toBe('INPUT'); + }); + + it('names the expand toggle of a folder with subfolders', () => { + useEmailStore.setState({ + mailboxes: [ + makeMailbox({ id: 'p', name: 'Parent' }), + makeMailbox({ id: 'c', name: 'Child', parentId: 'p' }), + ], + } as never); + render(); + const toggle = screen.getByRole('button', { name: 'expand' }); + expect(toggle.getAttribute('aria-expanded')).toBe('false'); + }); +}); diff --git a/components/settings/folder-settings.tsx b/components/settings/folder-settings.tsx index 728d5a2b1..cfe298e36 100644 --- a/components/settings/folder-settings.tsx +++ b/components/settings/folder-settings.tsx @@ -14,8 +14,12 @@ import { Star, Heart, Bookmark, Tag, Flag, Briefcase, Users, Bell, Zap, Globe, Lock, Eye, MessageSquare, Mail, AlertTriangle, NotebookPen, CalendarClock, BellOff, CheckSquare, Square, + MoreVertical, Palette, ListOrdered, type AppIcon, } from '@/components/icons'; +import { IconButton } from '@/components/ui/icon-button'; +import { ContextMenu, ContextMenuItem, ContextMenuSeparator, ContextMenuSubMenu } from '@/components/ui/context-menu'; +import { useContextMenu } from '@/hooks/use-context-menu'; import { cn, buildMailboxTree, flattenMailboxTree, type MailboxNode } from '@/lib/utils'; import { jmapErrorType } from '@/lib/jmap/jmap-error'; import { @@ -201,6 +205,9 @@ export function FolderSettings() { const [moveTarget, setMoveTarget] = useState(''); const [isMoving, setIsMoving] = useState(false); + // The row's actions: the ⋮ on the phone and right-click everywhere. + const { contextMenu, openContextMenu, openContextMenuFrom, closeContextMenu, menuRef } = useContextMenu(); + const ownMailboxes = mailboxes.filter(mb => !mb.isShared); const folderTree = buildMailboxTree(ownMailboxes); @@ -502,7 +509,7 @@ export function FolderSettings() { return (
@@ -563,7 +570,7 @@ export function FolderSettings() { if (editingId === mb.id) { return (
-
+
-
+

{t('confirm_delete', { name: mb.name })} @@ -628,10 +635,14 @@ export function FolderSettings() {

{ + if ((e.target as HTMLElement).closest('input, select')) return; + openContextMenu(e, mb); + }} >
{mb.myRights?.mayRename ? ( @@ -659,15 +670,17 @@ export function FolderSettings() { )} {/* Expand/collapse toggle for folders with children */} {hasChildren ? ( - + ) : ( )} @@ -707,7 +720,7 @@ export function FolderSettings() { /> )}
- {mb.name} + {mb.name} {mb.role && ( {t(`role_${mb.role}`)} @@ -719,48 +732,49 @@ 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')} + > + + +