From f25d856a2a50c0850a7f3bc4b69bae2ad511733a Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Fri, 5 Jun 2026 19:30:15 +0530 Subject: [PATCH] feat(renderer): ExportBatchDialog (format, concurrency, file list) --- .../components/modals/ExportBatchDialog.tsx | 79 +++++++++++++++++++ .../modals/ExportBatchDialog.test.tsx | 29 +++++++ 2 files changed, 108 insertions(+) create mode 100644 src/renderer/components/modals/ExportBatchDialog.tsx create mode 100644 tests/component/modals/ExportBatchDialog.test.tsx diff --git a/src/renderer/components/modals/ExportBatchDialog.tsx b/src/renderer/components/modals/ExportBatchDialog.tsx new file mode 100644 index 0000000..f740e35 --- /dev/null +++ b/src/renderer/components/modals/ExportBatchDialog.tsx @@ -0,0 +1,79 @@ +import { useState } from 'react'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Label } from '@/components/ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { useAppStore } from '@/stores/app-store'; +import { ipc } from '@/lib/ipc'; +import { ExportDialogFooter } from './ExportDialogFooter'; + +const extFor = (format: 'pdf' | 'docx' | 'html' | 'png') => + format === 'pdf' ? '.pdf' : format === 'docx' ? '.docx' : format === 'png' ? '.png' : '.html'; + +export function ExportBatchDialog({ sourcePaths }: { sourcePaths: string[] }) { + const closeModal = useAppStore((s) => s.closeModal); + const [format, setFormat] = useState<'pdf' | 'docx' | 'html' | 'png'>('pdf'); + const [concurrency, setConcurrency] = useState(4); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const handleSubmit = async () => { + setSubmitting(true); + setError(null); + const items = sourcePaths.map((p) => ({ + inputPath: p, + outputPath: p.replace(/\.md$/, extFor(format)), + })); + const result = await ipc.export.batch(items, { format, concurrency }); + if (!result.ok) { + setError(result.error.message); + setSubmitting(false); + } else { + closeModal(); + } + }; + + return ( + !o && closeModal()}> + + + Batch export + {sourcePaths.length} files + +
+
+ + +
+
+ + +
+
+ {sourcePaths.map((p) =>
{p}
)} +
+ {error && ( +
+ {error} +
+ )} +
+ +
+
+ ); +} diff --git a/tests/component/modals/ExportBatchDialog.test.tsx b/tests/component/modals/ExportBatchDialog.test.tsx new file mode 100644 index 0000000..cb9afdf --- /dev/null +++ b/tests/component/modals/ExportBatchDialog.test.tsx @@ -0,0 +1,29 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { ExportBatchDialog } from '@/components/modals/ExportBatchDialog'; + +describe('ExportBatchDialog', () => { + beforeEach(() => { + localStorage.clear(); + window.electronAPI = { + export: { batch: vi.fn().mockResolvedValue({ ok: true, data: { total: 2, succeeded: 2, failed: 0, results: [] } }) }, + } as any; + }); + + it('renders the file list passed via sourcePaths', () => { + render(); + expect(screen.getByText('/a.md')).toBeInTheDocument(); + expect(screen.getByText('/b.md')).toBeInTheDocument(); + }); + + it('selecting format and concurrency submits correct options', async () => { + render(); + await userEvent.click(screen.getByRole('combobox', { name: /format/i })); + await userEvent.click(screen.getByRole('option', { name: /^pdf$/i })); + await userEvent.click(screen.getByRole('button', { name: /^export$/i })); + const call = (window.electronAPI.export.batch as any).mock.calls[0]; + expect(call[0]).toEqual([{ inputPath: '/a.md', outputPath: expect.any(String) }, { inputPath: '/b.md', outputPath: expect.any(String) }]); + expect(call[1].format).toBe('pdf'); + }); +});