Files
markdown-converter/src/renderer/components/modals/ExportHtmlDialog.tsx
T
amitwh 21a00a53a4 fix: resolve 12 critical runtime failures, security issue, and dead code
CRITICAL fixes:
- GitStatusPanel: use ipc.file.gitStage/gitCommit instead of non-existent top-level methods
- HeaderFooterDialog: use send/on channels instead of non-existent headerFooter namespace
- CodeMirrorEditor: use invoke('save-pasted-image') instead of non-existent savePastedImage
- ipc.ts: map to correct preload namespaces (git.status, git.stage, git.commit)
- Add pick-folder/pick-file to ALLOWED_SEND_CHANNELS whitelist
- Add git convenience namespace to preload (git.status/stage/commit/log/diff)

HIGH fixes:
- FindReplaceBar: replace broken match count stub with regex-based counter
- PDF export window: disable nodeIntegration, enable contextIsolation + sandbox
- Git handler: accept rootPath argument from renderer
- Add git-diff IPC handler + GitOperations.diff()

MEDIUM fixes:
- Remove 4 dead functions: checkPandocAvailable, safeExecFile, runPandoc, openPDFFile, exportWithPandocPDF
- Remove stale ODT header/footer console.log stub
- Rewrite electron.d.ts to match actual preload API shape
2026-06-13 19:52:51 +05:30

198 lines
6.6 KiB
TypeScript

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 { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { useAppStore } from '@/stores/app-store';
import { useSettingsStore } from '@/stores/settings-store';
import { useExportSource } from '@/hooks/use-export-source';
import { generateHtml } from '@/lib/html-export';
import { ipc } from '@/lib/ipc';
import { toast } from '@/lib/toast';
import { ExportDialogFooter } from './ExportDialogFooter';
export function ExportHtmlDialog({ sourcePath }: { sourcePath: string }) {
const closeModal = useAppStore((s) => s.closeModal);
const { htmlHighlightStyle, renderTablesAsAscii } = useSettingsStore();
const source = useExportSource();
const [standalone, setStandalone] = useState(true);
const [highlight, setHighlight] = useState(htmlHighlightStyle);
const [ascii, setAscii] = useState(renderTablesAsAscii);
const [selfContained, setSelfContained] = useState(false);
const [toc, setToc] = useState(false);
const [tocDepth, setTocDepth] = useState(3);
const [numberSections, setNumberSections] = useState(false);
const [cssPath, setCssPath] = useState('');
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async () => {
if (!source) {
setError('No file open.');
return;
}
setSubmitting(true);
setError(null);
try {
const html = generateHtml({
source: source.source,
title: source.title,
standalone,
highlightStyle: highlight,
renderTablesAsAscii: ascii,
});
const options = {
standalone,
highlightStyle: highlight,
selfContained,
toc,
tocDepth,
numberSections,
css: cssPath || undefined,
};
await window.electronAPI?.export?.withOptions?.('html', options);
const saveResult = await ipc.app.showSaveDialog?.({
title: 'Save as HTML',
defaultPath: source.path.replace(/\.md$/, '.html'),
});
if (!saveResult?.ok || !saveResult.data) {
setSubmitting(false);
return;
}
const buffer = new TextEncoder().encode(html);
const writeResult = await ipc.file.writeBuffer({ path: saveResult.data, buffer });
if (!writeResult.ok) {
setError(writeResult.error.message);
setSubmitting(false);
return;
}
toast.success(`Exported ${source.title} to ${saveResult.data}`);
closeModal();
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
toast.error(`Export failed: ${msg}`);
setError(msg);
setSubmitting(false);
}
};
return (
<Dialog open onOpenChange={(o) => !o && closeModal()}>
<DialogContent className="max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Export to HTML</DialogTitle>
<DialogDescription>{sourcePath}</DialogDescription>
</DialogHeader>
<div className="space-y-3 text-sm">
<label className="flex items-center gap-2">
<Checkbox
checked={standalone}
onCheckedChange={(c) => setStandalone(!!c)}
aria-label="Standalone"
/>
Standalone document (with inline CSS)
</label>
<div>
<Label htmlFor="html-highlight">Syntax highlight style</Label>
<Select value={highlight} onValueChange={(v) => setHighlight(v as any)}>
<SelectTrigger id="html-highlight" aria-label="Highlight style">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="github">GitHub</SelectItem>
<SelectItem value="monokai">Monokai</SelectItem>
<SelectItem value="nord">Nord</SelectItem>
<SelectItem value="none">None</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-3">
<Switch
checked={selfContained}
onCheckedChange={setSelfContained}
id="html-self-contained"
/>
<Label htmlFor="html-self-contained">Self-contained (embed all CSS inline)</Label>
</div>
<label className="flex items-center gap-2">
<Checkbox
checked={ascii}
onCheckedChange={(c) => setAscii(!!c)}
aria-label="ASCII tables"
/>
Render tables as ASCII
</label>
<div className="flex items-center gap-3">
<Switch checked={toc} onCheckedChange={setToc} id="html-toc" />
<Label htmlFor="html-toc">Table of Contents</Label>
</div>
{toc && (
<div className="pl-9">
<Label htmlFor="html-toc-depth">TOC Depth</Label>
<Input
id="html-toc-depth"
type="number"
min={1}
max={6}
value={tocDepth}
onChange={(e) => setTocDepth(Math.min(6, Math.max(1, Number(e.target.value))))}
className="w-20"
aria-label="TOC depth"
/>
</div>
)}
<div className="flex items-center gap-3">
<Switch
checked={numberSections}
onCheckedChange={setNumberSections}
id="html-number-sections"
/>
<Label htmlFor="html-number-sections">Number sections</Label>
</div>
<div>
<Label htmlFor="html-css">Custom CSS file</Label>
<Input
id="html-css"
value={cssPath}
onChange={(e) => setCssPath(e.target.value)}
placeholder="/path/to/custom.css"
aria-label="Custom CSS file path"
/>
</div>
{error && (
<div
role="alert"
className="rounded border border-destructive/40 bg-destructive/5 p-2 text-xs text-destructive"
>
{error}
</div>
)}
</div>
<ExportDialogFooter
onCancel={closeModal}
onSubmit={handleSubmit}
submitting={submitting}
submitLabel="Export"
/>
</DialogContent>
</Dialog>
);
}