diff --git a/components/pro/__tests__/pro-email-edit-draft.test.tsx b/components/pro/__tests__/pro-email-edit-draft.test.tsx index 9c453ee6f..0edefdb22 100644 --- a/components/pro/__tests__/pro-email-edit-draft.test.tsx +++ b/components/pro/__tests__/pro-email-edit-draft.test.tsx @@ -1,4 +1,4 @@ -import { render, waitFor } from '@testing-library/react'; +import { act, render, waitFor } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import React from 'react'; import { ProTabFocusContext } from '@/hooks/use-pane-context'; @@ -26,6 +26,20 @@ vi.mock('@/components/error', () => ({ })); vi.mock('next-intl', () => ({ useTranslations: () => (key: string) => key })); + +// A message window: the Message menu's Move to… opens the folder list there. +const popout = vi.hoisted(() => ({ value: false })); +vi.mock('@/lib/popout/window-role', async (importOriginal) => ({ + ...await importOriginal(), + isPopoutWindow: () => popout.value, +})); +const moveDialog = vi.hoisted(() => ({ current: null as { email: unknown } | null })); +vi.mock('@/components/email/move-to-dialog', () => ({ + MoveToDialog: (props: { email: unknown }) => { + moveDialog.current = props; + return null; + }, +})); vi.mock('@/stores/toast-store', () => ({ toast: { error: () => {}, success: () => {} } })); const multiAccount = vi.hoisted(() => ({ value: { enabled: false, groups: [] as unknown[], allIdentities: [] as Array<{ id: string; email: string; name: string }> }, @@ -79,6 +93,7 @@ vi.mock('@/stores/pro-tab-store', () => ({ })); import { ProEmailView } from '../pro-email-tab-body'; +import { resetDesktopCommands, runDesktopCommand } from '@/lib/desktop/commands'; const DRAFT = { id: 'eaaaaab', @@ -307,3 +322,24 @@ describe('Pro email view - keyboard', () => { expect(openComposeTab).not.toHaveBeenCalled(); }); }); + +describe('Message > Move to… in a message window', () => { + it('opens the folder list for the message on screen', async () => { + popout.value = true; + window.bulwarkDesktop = { version: 1, platform: 'win32', menu: { setState: vi.fn() } } as never; + try { + const client = { getEmail: vi.fn().mockResolvedValue(DRAFT), getUsername: () => 'userb@example.org', getAccountId: () => 'e' }; + render(); + await waitFor(() => expect(viewerProps.current?.email).toBeTruthy()); + expect(moveDialog.current?.email ?? null).toBeNull(); + act(() => { + expect(runDesktopCommand('message.move')).toBe(true); + }); + await waitFor(() => expect(moveDialog.current?.email).toMatchObject({ id: 'eaaaaab' })); + } finally { + popout.value = false; + window.bulwarkDesktop = undefined; + resetDesktopCommands(); + } + }); +}); diff --git a/components/pro/pro-email-tab-body.tsx b/components/pro/pro-email-tab-body.tsx index 569b13b0a..8e854bfcc 100644 --- a/components/pro/pro-email-tab-body.tsx +++ b/components/pro/pro-email-tab-body.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useState, useMemo, useRef } from "react"; import { useTranslations } from "next-intl"; import { EmailViewer } from "@/components/email/email-viewer"; +import { MoveToDialog } from "@/components/email/move-to-dialog"; import { ErrorBoundary, EmailViewerErrorFallback } from "@/components/error"; import { useAuthStore } from "@/stores/auth-store"; import { useEmailStore, resolveEmailBlobContext, type EmailBlobContext, type EmailWriteSource } from "@/stores/email-store"; @@ -472,6 +473,8 @@ export function ProEmailView({ emailId, client: clientOverride, clientAccountId, // In a pop-out this message is all there is, so the menu bar's Message menu means it. const inOwnWindow = typeof window !== 'undefined' && isPopoutWindow(); const seen = !!email?.keywords?.$seen; + // Message > Move to… in a pop-out: the same folder list the command palette opens. + const [moveDialogOpen, setMoveDialogOpen] = useState(false); useDesktopCommands({ 'message.reply': () => handleReply(), 'message.replyAll': handleReplyAll, @@ -481,6 +484,7 @@ export function ProEmailView({ emailId, client: clientOverride, clientAccountId, 'message.star': () => void handleToggleStar(), 'message.archive': archiveActionEnabled ? () => void handleArchive() : undefined, 'message.delete': () => void handleDelete(), + 'message.move': () => { if (email) setMoveDialogOpen(true); }, }, { 'message.reply': !!email, 'message.replyAll': !!email, @@ -490,6 +494,7 @@ export function ProEmailView({ emailId, client: clientOverride, clientAccountId, 'message.star': !!email, 'message.archive': !!email && archiveActionEnabled, 'message.delete': !!email, + 'message.move': !!email, }, inOwnWindow); const handleMoveToMailbox = useCallback(async (mailboxId: string) => { @@ -642,6 +647,15 @@ export function ProEmailView({ emailId, client: clientOverride, clientAccountId, /> + {inOwnWindow && ( + setMoveDialogOpen(false)} + onMove={(mailboxId) => void handleMoveToMailbox(mailboxId)} + /> + )} {preview && (