feat: move a message from its own window with Message > Move to…

This commit is contained in:
Linus Rath
2026-11-04 05:20:03 +01:00
parent 674845df4e
commit ad970b35ef
2 changed files with 51 additions and 1 deletions
@@ -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<typeof import('@/lib/popout/window-role')>(),
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(<ProEmailView emailId="eaaaaab" client={client as never} onClose={vi.fn()} />);
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();
}
});
});
+14
View File
@@ -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,
/>
</ErrorBoundary>
<ConfirmDialog {...confirmDialogProps} />
{inOwnWindow && (
<MoveToDialog
email={moveDialogOpen ? email : null}
mailboxes={mailboxes}
selectedMailbox=""
onClose={() => setMoveDialogOpen(false)}
onMove={(mailboxId) => void handleMoveToMailbox(mailboxId)}
/>
)}
{preview && (
<FilePreviewModal
name={preview.name}