From 7d083a024d302b8583e7c6bb71c03b0a29d56b6c Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Fri, 5 Jun 2026 19:19:18 +0530 Subject: [PATCH] feat(renderer): ExportPdfDialog (format, margins, ascii tables) --- .../components/modals/ExportPdfDialog.tsx | 102 ++++++++++++++++++ .../component/modals/ExportPdfDialog.test.tsx | 43 ++++++++ 2 files changed, 145 insertions(+) create mode 100644 src/renderer/components/modals/ExportPdfDialog.tsx create mode 100644 tests/component/modals/ExportPdfDialog.test.tsx diff --git a/src/renderer/components/modals/ExportPdfDialog.tsx b/src/renderer/components/modals/ExportPdfDialog.tsx new file mode 100644 index 0000000..235bc27 --- /dev/null +++ b/src/renderer/components/modals/ExportPdfDialog.tsx @@ -0,0 +1,102 @@ +import { useState } from 'react'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Label } from '@/components/ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { useAppStore } from '@/stores/app-store'; +import { useSettingsStore } from '@/stores/settings-store'; +import { useExportSource } from '@/hooks/use-export-source'; +import { ipc } from '@/lib/ipc'; +import { ExportDialogFooter } from './ExportDialogFooter'; + +const MARGIN_MAP = { + normal: { top: 25, right: 25, bottom: 25, left: 25 }, + narrow: { top: 15, right: 15, bottom: 15, left: 15 }, + wide: { top: 35, right: 35, bottom: 35, left: 35 }, +} as const; + +export function ExportPdfDialog({ sourcePath }: { sourcePath: string }) { + const closeModal = useAppStore((s) => s.closeModal); + const { fontSize, pdfFormat, pdfMargins, pdfEmbedFonts, renderTablesAsAscii } = useSettingsStore(); + const [format, setFormat] = useState<'letter' | 'a4' | 'legal'>(pdfFormat); + const [margins, setMargins] = useState<'normal' | 'narrow' | 'wide'>(pdfMargins); + const [embed, setEmbed] = useState(pdfEmbedFonts); + const [ascii, setAscii] = useState(renderTablesAsAscii); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const source = useExportSource(); + + const handleSubmit = async () => { + if (!source) { + setError('No file open.'); + return; + } + setSubmitting(true); + setError(null); + const result = await ipc.export.pdf({ + inputPath: source.path, + outputPath: source.path.replace(/\.md$/, '.pdf'), + format, + margins: MARGIN_MAP[margins], + embedFonts: embed, + renderTablesAsAscii: ascii, + fontSize, + } as any); + if (!result.data?.ok) { + setError(result.data?.error?.message ?? 'Export failed'); + setSubmitting(false); + } else { + closeModal(); + } + }; + + return ( + !o && closeModal()}> + + + Export to PDF + {sourcePath} + +
+
+ + +
+
+ + +
+ + + {error && ( +
+ {error} +
+ )} +
+ +
+
+ ); +} \ No newline at end of file diff --git a/tests/component/modals/ExportPdfDialog.test.tsx b/tests/component/modals/ExportPdfDialog.test.tsx new file mode 100644 index 0000000..0189d94 --- /dev/null +++ b/tests/component/modals/ExportPdfDialog.test.tsx @@ -0,0 +1,43 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { ExportPdfDialog } from '@/components/modals/ExportPdfDialog'; +import { useFileStore } from '@/stores/file-store'; +import { useEditorStore } from '@/stores/editor-store'; +import { useSettingsStore } from '@/stores/settings-store'; + +describe('ExportPdfDialog', () => { + beforeEach(() => { + localStorage.clear(); + window.electronAPI = { + export: { + pdf: vi.fn().mockResolvedValue({ ok: true, data: { outputPath: '/out.pdf', bytes: 1024, durationMs: 100 } }), + }, + } as any; + useSettingsStore.setState(useSettingsStore.getInitialState()); + useFileStore.setState({ activeTabId: '/test.md', openTabs: [{ id: '/test.md', path: '/test.md', title: 'test.md', dirty: false }] } as any); + useEditorStore.setState({ buffers: new Map([['/test.md', { id: '/test.md', path: '/test.md', content: '# hi', dirty: false }]]) } as any); + }); + + it('renders with default PDF options from settings', () => { + render(); + expect(screen.getByText(/export to pdf/i)).toBeInTheDocument(); + expect(screen.getByRole('combobox', { name: /format/i })).toBeInTheDocument(); + }); + + it('toggles ASCII tables and submits merged options', async () => { + render(); + await userEvent.click(screen.getByRole('checkbox', { name: /ascii/i })); + await userEvent.click(screen.getByRole('button', { name: /^export$/i })); + const call = (window.electronAPI.export.pdf as any).mock.calls[0][0]; + expect(call.renderTablesAsAscii).toBe(true); + expect(call.format).toBe('a4'); + }); + + it('renders an error banner when IPC fails', async () => { + (window.electronAPI.export.pdf as any).mockResolvedValueOnce({ ok: false, error: { code: 'E', message: 'Pandoc not found' } }); + render(); + await userEvent.click(screen.getByRole('button', { name: /^export$/i })); + expect(await screen.findByText(/pandoc not found/i)).toBeInTheDocument(); + }); +}); \ No newline at end of file