mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 18:10:18 +05:30
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
This commit is contained in:
@@ -41,4 +41,14 @@ async function log(dir, maxCount = 20) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { getStatus, stage, commit, log };
|
async function diff(dir, filePath) {
|
||||||
|
try {
|
||||||
|
const git = getGitInstance(dir);
|
||||||
|
const args = filePath ? ['--', filePath] : [];
|
||||||
|
return await git.diff(args);
|
||||||
|
} catch (err) {
|
||||||
|
return { error: err.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getStatus, stage, commit, log, diff };
|
||||||
|
|||||||
+25
-16
@@ -4,33 +4,42 @@ const { ipcMain } = require('electron');
|
|||||||
const GitOperations = require('../GitOperations');
|
const GitOperations = require('../GitOperations');
|
||||||
|
|
||||||
function register(currentFileRef) {
|
function register(currentFileRef) {
|
||||||
ipcMain.handle('git-status', async () => {
|
ipcMain.handle('git-status', async (_event, rootPath) => {
|
||||||
const dir = currentFileRef.current
|
const dir =
|
||||||
? require('path').dirname(currentFileRef.current)
|
rootPath ||
|
||||||
: process.cwd();
|
(currentFileRef.current ? require('path').dirname(currentFileRef.current) : process.cwd());
|
||||||
return GitOperations.getStatus(dir);
|
return GitOperations.getStatus(dir);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('git-stage', async (_event, { files }) => {
|
ipcMain.handle('git-stage', async (_event, { rootPath, files }) => {
|
||||||
const dir = currentFileRef.current
|
const dir =
|
||||||
? require('path').dirname(currentFileRef.current)
|
rootPath ||
|
||||||
: process.cwd();
|
(currentFileRef.current ? require('path').dirname(currentFileRef.current) : process.cwd());
|
||||||
return GitOperations.stage(dir, files);
|
return GitOperations.stage(dir, files);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('git-commit', async (_event, { message }) => {
|
ipcMain.handle('git-commit', async (_event, { rootPath, message }) => {
|
||||||
const dir = currentFileRef.current
|
const dir =
|
||||||
? require('path').dirname(currentFileRef.current)
|
rootPath ||
|
||||||
: process.cwd();
|
(currentFileRef.current ? require('path').dirname(currentFileRef.current) : process.cwd());
|
||||||
return GitOperations.commit(dir, message);
|
return GitOperations.commit(dir, message);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('git-log', async () => {
|
ipcMain.handle('git-log', async (_event, rootPath) => {
|
||||||
const dir = currentFileRef.current
|
const dir =
|
||||||
? require('path').dirname(currentFileRef.current)
|
rootPath ||
|
||||||
: process.cwd();
|
(currentFileRef.current ? require('path').dirname(currentFileRef.current) : process.cwd());
|
||||||
return GitOperations.log(dir);
|
return GitOperations.log(dir);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('git-diff', async (_event, filePath) => {
|
||||||
|
const dir = filePath
|
||||||
|
? require('path').dirname(filePath)
|
||||||
|
: currentFileRef.current
|
||||||
|
? require('path').dirname(currentFileRef.current)
|
||||||
|
: process.cwd();
|
||||||
|
return GitOperations.diff(dir, filePath);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { register };
|
module.exports = { register };
|
||||||
|
|||||||
+5
-95
@@ -57,41 +57,6 @@ function getFFmpegPath() {
|
|||||||
return process.platform === 'win32' ? 'ffmpeg' : 'ffmpeg';
|
return process.platform === 'win32' ? 'ffmpeg' : 'ffmpeg';
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
function checkPandocAvailable() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const pandocPath = getPandocPath();
|
|
||||||
execFile(pandocPath, ['--version'], (error) => {
|
|
||||||
resolve(!error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Safe command execution using execFile (no shell injection risk)
|
|
||||||
* @param {string} command - The command to execute
|
|
||||||
* @param {string[]} args - Array of arguments
|
|
||||||
* @param {object} options - Options for execFile
|
|
||||||
* @returns {Promise<{stdout: string, stderr: string}>}
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
function safeExecFile(command, args, options = {}) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
execFile(
|
|
||||||
command,
|
|
||||||
args,
|
|
||||||
{ maxBuffer: 10 * 1024 * 1024, ...options },
|
|
||||||
(error, stdout, stderr) => {
|
|
||||||
if (error) {
|
|
||||||
reject({ error, stdout, stderr });
|
|
||||||
} else {
|
|
||||||
resolve({ stdout, stderr });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// File size validation
|
// File size validation
|
||||||
const MAX_FILE_SIZE_MB = 50;
|
const MAX_FILE_SIZE_MB = 50;
|
||||||
const MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024;
|
const MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024;
|
||||||
@@ -134,17 +99,6 @@ function convertDataToMarkdown(content, format) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Run Pandoc command safely with execFile
|
|
||||||
* @param {string[]} args - Pandoc arguments array
|
|
||||||
* @param {Function} callback - Callback function (error, stdout, stderr)
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
function runPandoc(args, callback) {
|
|
||||||
const pandocPath = getPandocPath();
|
|
||||||
execFile(pandocPath, args, { maxBuffer: 10 * 1024 * 1024 }, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a Pandoc command string safely using execFile
|
* Run a Pandoc command string safely using execFile
|
||||||
* Parses the command string and uses execFile to prevent shell injection
|
* Parses the command string and uses execFile to prevent shell injection
|
||||||
@@ -593,23 +547,6 @@ function showDependenciesDialog() {
|
|||||||
depsWindow.setMenuBarVisibility(false);
|
depsWindow.setMenuBarVisibility(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
function openPDFFile() {
|
|
||||||
const files = dialog.showOpenDialogSync(mainWindow, {
|
|
||||||
properties: ['openFile'],
|
|
||||||
filters: [{ name: 'PDF Files', extensions: ['pdf'] }],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (files && files[0]) {
|
|
||||||
const stats = fs.statSync(files[0]);
|
|
||||||
if (stats.size > MAX_FILE_SIZE) {
|
|
||||||
dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mainWindow.webContents.send('open-pdf-viewer', files[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openFile() {
|
function openFile() {
|
||||||
const files = dialog.showOpenDialogSync(mainWindow, {
|
const files = dialog.showOpenDialogSync(mainWindow, {
|
||||||
properties: ['openFile'],
|
properties: ['openFile'],
|
||||||
@@ -1958,42 +1895,14 @@ function exportWithPandoc(pandocCmd, outputFile, format) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add headers/footers to ODT if enabled
|
// ODT header/footer is not supported by Pandoc's ODT output
|
||||||
if (format === 'odt' && headerFooterSettings.enabled) {
|
// Headers/footers are applied only for DOCX format
|
||||||
// ODT format is similar to DOCX in structure, we could implement this
|
|
||||||
console.log('ODT header/footer support not yet implemented');
|
|
||||||
}
|
|
||||||
|
|
||||||
showExportSuccess(outputFile);
|
showExportSuccess(outputFile);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to export PDF with pandoc (with fallbacks) - uses runPandocCmd for safety
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
function exportWithPandocPDF(pandocCmd, outputFile) {
|
|
||||||
runPandocCmd(pandocCmd, (error, _stdout, _stderr) => {
|
|
||||||
if (error) {
|
|
||||||
console.log('XeLaTeX failed, trying PDFLaTeX...');
|
|
||||||
// Fallback to pdflatex
|
|
||||||
const fallbackCmd = pandocCmd.replace('--pdf-engine=xelatex', '--pdf-engine=pdflatex');
|
|
||||||
runPandocCmd(fallbackCmd, (fallbackError, _fallbackStdout, _fallbackStderr) => {
|
|
||||||
if (fallbackError) {
|
|
||||||
console.log('PDFLaTeX failed, trying Electron PDF...');
|
|
||||||
// Final fallback to Electron PDF
|
|
||||||
exportToPDFElectron(outputFile);
|
|
||||||
} else {
|
|
||||||
console.log('Successfully exported PDF with PDFLaTeX');
|
|
||||||
showExportSuccess(outputFile);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('Successfully exported PDF with XeLaTeX');
|
|
||||||
showExportSuccess(outputFile);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export to HTML using marked (no pandoc required)
|
// Export to HTML using marked (no pandoc required)
|
||||||
function exportToHTML(outputFile) {
|
function exportToHTML(outputFile) {
|
||||||
try {
|
try {
|
||||||
@@ -2219,8 +2128,9 @@ function exportToPDFElectron(outputFile) {
|
|||||||
const pdfWindow = new BrowserWindow({
|
const pdfWindow = new BrowserWindow({
|
||||||
show: false,
|
show: false,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true,
|
nodeIntegration: false,
|
||||||
contextIsolation: false,
|
contextIsolation: true,
|
||||||
|
sandbox: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ const ALLOWED_SEND_CHANNELS = [
|
|||||||
'is-directory',
|
'is-directory',
|
||||||
'copy-path',
|
'copy-path',
|
||||||
'move-path',
|
'move-path',
|
||||||
|
'pick-folder',
|
||||||
|
'pick-file',
|
||||||
|
|
||||||
// Git
|
// Git
|
||||||
'git-status',
|
'git-status',
|
||||||
@@ -460,6 +462,15 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
|
|
||||||
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
||||||
|
|
||||||
|
// Git Operations
|
||||||
|
git: {
|
||||||
|
status: (rootPath) => ipcRenderer.invoke('git-status', rootPath),
|
||||||
|
stage: (args) => ipcRenderer.invoke('git-stage', args),
|
||||||
|
commit: (args) => ipcRenderer.invoke('git-commit', args),
|
||||||
|
log: (rootPath) => ipcRenderer.invoke('git-log', rootPath),
|
||||||
|
diff: (filePath) => ipcRenderer.invoke('git-diff', filePath),
|
||||||
|
},
|
||||||
|
|
||||||
app: {
|
app: {
|
||||||
quit: () => ipcRenderer.send('app:quit'),
|
quit: () => ipcRenderer.send('app:quit'),
|
||||||
openExternal: (url) => ipcRenderer.send('app:open-external', url),
|
openExternal: (url) => ipcRenderer.send('app:open-external', url),
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ async function handleImageFile(file: File): Promise<void> {
|
|||||||
const base64 = (reader.result as string).split(',')[1];
|
const base64 = (reader.result as string).split(',')[1];
|
||||||
if (!base64) return;
|
if (!base64) return;
|
||||||
const ext = guessExt(file.type);
|
const ext = guessExt(file.type);
|
||||||
const result = await window.electronAPI.savePastedImage({ base64, ext });
|
const result = await window.electronAPI.invoke('save-pasted-image', { base64, ext });
|
||||||
if (result) {
|
if (result) {
|
||||||
insertSnippet(``);
|
insertSnippet(``);
|
||||||
toast.success('Image pasted');
|
toast.success('Image pasted');
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
getSearchQuery,
|
getSearchQuery,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
} from '@codemirror/search';
|
} from '@codemirror/search';
|
||||||
|
import type { EditorView } from '@codemirror/view';
|
||||||
import { getActiveView } from '@/lib/editor-commands';
|
import { getActiveView } from '@/lib/editor-commands';
|
||||||
import { useAppStore } from '@/stores/app-store';
|
import { useAppStore } from '@/stores/app-store';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@@ -24,14 +25,17 @@ export function FindReplaceBar() {
|
|||||||
const [useRegex, setUseRegex] = useState(false);
|
const [useRegex, setUseRegex] = useState(false);
|
||||||
const [matchInfo, setMatchInfo] = useState<{ current: number; total: number } | null>(null);
|
const [matchInfo, setMatchInfo] = useState<{ current: number; total: number } | null>(null);
|
||||||
|
|
||||||
const executeCommand = useCallback((fn: (view: any) => any) => {
|
const executeCommand = useCallback(
|
||||||
const view = getActiveView();
|
(fn: (view: EditorView) => boolean | void) => {
|
||||||
if (!view) return false;
|
const view = getActiveView();
|
||||||
const result = fn(view);
|
if (!view) return false;
|
||||||
updateMatchCount();
|
const result = fn(view);
|
||||||
view.focus();
|
updateMatchCount();
|
||||||
return result;
|
view.focus();
|
||||||
}, []);
|
return result;
|
||||||
|
},
|
||||||
|
[updateMatchCount]
|
||||||
|
);
|
||||||
|
|
||||||
const updateMatchCount = useCallback(() => {
|
const updateMatchCount = useCallback(() => {
|
||||||
const view = getActiveView();
|
const view = getActiveView();
|
||||||
@@ -40,22 +44,34 @@ export function FindReplaceBar() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const query = getSearchQuery(view.state);
|
const query = getSearchQuery(view.state);
|
||||||
if (!query) {
|
if (!query || !query.search) {
|
||||||
setMatchInfo(null);
|
setMatchInfo(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const searchState = view.state.field(
|
|
||||||
// @ts-expect-error - internal field accessor
|
|
||||||
query.spec.create() &&
|
|
||||||
(() => {
|
|
||||||
for (const facet of view.state.field) return null;
|
|
||||||
})(),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
// @ts-expect-error - search state inspection
|
const docText = view.state.doc.toString();
|
||||||
const panel = view.state.facet?.(searchPanelFacet)?.[0];
|
const searchStr = query.regexp
|
||||||
setMatchInfo(null);
|
? query.search
|
||||||
|
: query.search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
const flags = `g${query.caseSensitive ? '' : 'i'}`;
|
||||||
|
const regex = new RegExp(searchStr, flags);
|
||||||
|
const matches = docText.match(regex);
|
||||||
|
if (!matches) {
|
||||||
|
setMatchInfo({ current: 0, total: 0 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selectionHead = view.state.selection.main.head;
|
||||||
|
let currentMatch = 0;
|
||||||
|
const matchPositions: number[] = [];
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(docText)) !== null) {
|
||||||
|
matchPositions.push(match.index);
|
||||||
|
if (match.index <= selectionHead && selectionHead <= match.index + match[0].length) {
|
||||||
|
currentMatch = matchPositions.length;
|
||||||
|
}
|
||||||
|
if (matchPositions.length > 10000) break;
|
||||||
|
}
|
||||||
|
setMatchInfo({ current: currentMatch || 1, total: matchPositions.length });
|
||||||
} catch {
|
} catch {
|
||||||
setMatchInfo(null);
|
setMatchInfo(null);
|
||||||
}
|
}
|
||||||
@@ -210,6 +226,12 @@ export function FindReplaceBar() {
|
|||||||
<Regex className="h-3.5 w-3.5" />
|
<Regex className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{matchInfo && matchInfo.total > 0 && (
|
||||||
|
<span className="min-w-[4rem] text-center text-[10px] text-muted-foreground">
|
||||||
|
{matchInfo.current}/{matchInfo.total}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
ref={replaceRef}
|
ref={replaceRef}
|
||||||
placeholder="Replace..."
|
placeholder="Replace..."
|
||||||
|
|||||||
@@ -251,7 +251,11 @@ export function BatchMediaConverterDialog() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Switch checked={includeSubfolders} onCheckedChange={setIncludeSubfolders} id="batch-subfolders" />
|
<Switch
|
||||||
|
checked={includeSubfolders}
|
||||||
|
onCheckedChange={setIncludeSubfolders}
|
||||||
|
id="batch-subfolders"
|
||||||
|
/>
|
||||||
<Label htmlFor="batch-subfolders">Include subdirectories</Label>
|
<Label htmlFor="batch-subfolders">Include subdirectories</Label>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -147,7 +147,11 @@ export function ExportDocxDialog({ sourcePath }: { sourcePath: string }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Switch checked={numberSections} onCheckedChange={setNumberSections} id="docx-number-sections" />
|
<Switch
|
||||||
|
checked={numberSections}
|
||||||
|
onCheckedChange={setNumberSections}
|
||||||
|
id="docx-number-sections"
|
||||||
|
/>
|
||||||
<Label htmlFor="docx-number-sections">Number sections</Label>
|
<Label htmlFor="docx-number-sections">Number sections</Label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -124,7 +124,11 @@ export function ExportHtmlDialog({ sourcePath }: { sourcePath: string }) {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Switch checked={selfContained} onCheckedChange={setSelfContained} id="html-self-contained" />
|
<Switch
|
||||||
|
checked={selfContained}
|
||||||
|
onCheckedChange={setSelfContained}
|
||||||
|
id="html-self-contained"
|
||||||
|
/>
|
||||||
<Label htmlFor="html-self-contained">Self-contained (embed all CSS inline)</Label>
|
<Label htmlFor="html-self-contained">Self-contained (embed all CSS inline)</Label>
|
||||||
</div>
|
</div>
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2">
|
||||||
@@ -155,7 +159,11 @@ export function ExportHtmlDialog({ sourcePath }: { sourcePath: string }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Switch checked={numberSections} onCheckedChange={setNumberSections} id="html-number-sections" />
|
<Switch
|
||||||
|
checked={numberSections}
|
||||||
|
onCheckedChange={setNumberSections}
|
||||||
|
id="html-number-sections"
|
||||||
|
/>
|
||||||
<Label htmlFor="html-number-sections">Number sections</Label>
|
<Label htmlFor="html-number-sections">Number sections</Label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -194,12 +194,19 @@ export function ExportPdfDialog({ sourcePath }: { sourcePath: string }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Switch checked={numberSections} onCheckedChange={setNumberSections} id="pdf-number-sections" />
|
<Switch
|
||||||
|
checked={numberSections}
|
||||||
|
onCheckedChange={setNumberSections}
|
||||||
|
id="pdf-number-sections"
|
||||||
|
/>
|
||||||
<Label htmlFor="pdf-number-sections">Number sections</Label>
|
<Label htmlFor="pdf-number-sections">Number sections</Label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="pdf-page-geometry">Page Geometry</Label>
|
<Label htmlFor="pdf-page-geometry">Page Geometry</Label>
|
||||||
<Select value={pageGeometry} onValueChange={(v) => setPageGeometry(v as typeof pageGeometry)}>
|
<Select
|
||||||
|
value={pageGeometry}
|
||||||
|
onValueChange={(v) => setPageGeometry(v as typeof pageGeometry)}
|
||||||
|
>
|
||||||
<SelectTrigger id="pdf-page-geometry" aria-label="Page geometry">
|
<SelectTrigger id="pdf-page-geometry" aria-label="Page geometry">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|||||||
@@ -71,10 +71,15 @@ export function HeaderFooterDialog() {
|
|||||||
let mounted = true;
|
let mounted = true;
|
||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
const result = await window.electronAPI?.headerFooter?.getSettings?.();
|
window.electronAPI?.send('get-header-footer-settings');
|
||||||
if (mounted && result && typeof result === 'object') {
|
const unsub = window.electronAPI?.on('header-footer-settings-data', (data: unknown) => {
|
||||||
setSettings({ ...DEFAULTS, ...result });
|
if (mounted && data && typeof data === 'object') {
|
||||||
}
|
setSettings({ ...DEFAULTS, ...(data as Partial<HeaderFooterSettings>) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
unsub?.();
|
||||||
|
};
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setLoading(false);
|
if (mounted) setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -97,14 +102,19 @@ export function HeaderFooterDialog() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleBrowseLogo = useCallback(async () => {
|
const handleBrowseLogo = useCallback(async () => {
|
||||||
const result = await window.electronAPI?.headerFooter?.browseLogo?.();
|
window.electronAPI?.send('browse-header-footer-logo');
|
||||||
if (typeof result === 'string') {
|
const unsub = window.electronAPI?.on('header-footer-logo-selected', (data: unknown) => {
|
||||||
setSettings((prev) => ({
|
if (typeof data === 'string') {
|
||||||
...prev,
|
setSettings((prev) => ({
|
||||||
logoPath: result,
|
...prev,
|
||||||
logoPosition: prev.logoPosition === 'none' ? 'left' : prev.logoPosition,
|
logoPath: data,
|
||||||
}));
|
logoPosition: prev.logoPosition === 'none' ? 'left' : prev.logoPosition,
|
||||||
}
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
unsub?.();
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleClearLogo = useCallback(() => {
|
const handleClearLogo = useCallback(() => {
|
||||||
@@ -113,16 +123,18 @@ export function HeaderFooterDialog() {
|
|||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
window.electronAPI?.send('save-header-footer-settings', settings);
|
||||||
await window.electronAPI?.headerFooter?.saveSettings?.(settings);
|
const unsub = window.electronAPI?.on('header-footer-logo-saved', () => {
|
||||||
toast.success('Header & footer settings saved');
|
toast.success('Header & footer settings saved');
|
||||||
closeModal();
|
closeModal();
|
||||||
} catch (err) {
|
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
|
||||||
toast.error(`Failed to save: ${msg}`);
|
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
unsub?.();
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
setSaving(false);
|
||||||
|
toast.success('Header & footer settings saved');
|
||||||
|
closeModal();
|
||||||
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fieldRows: Array<{ key: FieldKey; label: string; enabled: boolean }> = [
|
const fieldRows: Array<{ key: FieldKey; label: string; enabled: boolean }> = [
|
||||||
|
|||||||
@@ -91,31 +91,29 @@ export function GitStatusPanel() {
|
|||||||
const stageFiles = async (files: string[]) => {
|
const stageFiles = async (files: string[]) => {
|
||||||
if (!rootPath || files.length === 0) return;
|
if (!rootPath || files.length === 0) return;
|
||||||
setStaging(true);
|
setStaging(true);
|
||||||
try {
|
const result = await ipc.file.gitStage({ rootPath, files });
|
||||||
await window.electronAPI.gitStage(files);
|
setStaging(false);
|
||||||
|
if (result.ok) {
|
||||||
toast.success(`Staged ${files.length} file${files.length === 1 ? '' : 's'}`);
|
toast.success(`Staged ${files.length} file${files.length === 1 ? '' : 's'}`);
|
||||||
setSelected(new Set());
|
setSelected(new Set());
|
||||||
window.dispatchEvent(new CustomEvent('mc:git-refresh'));
|
window.dispatchEvent(new CustomEvent('mc:git-refresh'));
|
||||||
} catch {
|
} else {
|
||||||
toast.error('Failed to stage files');
|
toast.error(`Failed to stage: ${result.error.message}`);
|
||||||
} finally {
|
|
||||||
setStaging(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const commit = async () => {
|
const commit = async () => {
|
||||||
if (!rootPath || !commitMsg.trim()) return;
|
if (!rootPath || !commitMsg.trim()) return;
|
||||||
setCommitting(true);
|
setCommitting(true);
|
||||||
try {
|
const result = await ipc.file.gitCommit({ rootPath, message: commitMsg.trim() });
|
||||||
await window.electronAPI.gitCommit(commitMsg.trim());
|
setCommitting(false);
|
||||||
|
if (result.ok) {
|
||||||
toast.success('Changes committed');
|
toast.success('Changes committed');
|
||||||
setCommitMsg('');
|
setCommitMsg('');
|
||||||
setSelected(new Set());
|
setSelected(new Set());
|
||||||
window.dispatchEvent(new CustomEvent('mc:git-refresh'));
|
window.dispatchEvent(new CustomEvent('mc:git-refresh'));
|
||||||
} catch {
|
} else {
|
||||||
toast.error('Failed to commit changes');
|
toast.error(`Failed to commit: ${result.error.message}`);
|
||||||
} finally {
|
|
||||||
setCommitting(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+26
-22
@@ -58,7 +58,7 @@ function safeCall<T extends (...args: any[]) => Promise<any>>(
|
|||||||
|
|
||||||
export const ipc = {
|
export const ipc = {
|
||||||
file: {
|
file: {
|
||||||
open: (): Promise<IpcResult<FileResult | ChannelMissing>> => safeCall('file', 'open'),
|
open: (): Promise<IpcResult<FileResult | ChannelMissing>> => safeCall('file', 'pickFile'),
|
||||||
read: (path: string): Promise<IpcResult<string | ChannelMissing>> =>
|
read: (path: string): Promise<IpcResult<string | ChannelMissing>> =>
|
||||||
safeCall('file', 'read', path),
|
safeCall('file', 'read', path),
|
||||||
write: (path: string, content: string): Promise<IpcResult<void | ChannelMissing>> =>
|
write: (path: string, content: string): Promise<IpcResult<void | ChannelMissing>> =>
|
||||||
@@ -70,10 +70,10 @@ export const ipc = {
|
|||||||
pickFile: (): Promise<IpcResult<string | null | ChannelMissing>> =>
|
pickFile: (): Promise<IpcResult<string | null | ChannelMissing>> =>
|
||||||
safeCall('file', 'pickFile'),
|
safeCall('file', 'pickFile'),
|
||||||
onChange: (cb: (path: string) => void): (() => void) => {
|
onChange: (cb: (path: string) => void): (() => void) => {
|
||||||
if (typeof window === 'undefined' || !window.electronAPI?.file?.onChange) {
|
if (typeof window === 'undefined' || !window.electronAPI?.on) {
|
||||||
return () => {};
|
return () => {};
|
||||||
}
|
}
|
||||||
return window.electronAPI.file.onChange(cb);
|
return window.electronAPI.on('list-directory', cb as (...a: unknown[]) => void);
|
||||||
},
|
},
|
||||||
search: (args: {
|
search: (args: {
|
||||||
rootPath: string;
|
rootPath: string;
|
||||||
@@ -82,7 +82,7 @@ export const ipc = {
|
|||||||
caseSensitive: boolean;
|
caseSensitive: boolean;
|
||||||
}): Promise<
|
}): Promise<
|
||||||
IpcResult<Array<{ filePath: string; line: number; content: string }> | ChannelMissing>
|
IpcResult<Array<{ filePath: string; line: number; content: string }> | ChannelMissing>
|
||||||
> => safeCall('file', 'search', args),
|
> => safeCall('file', 'pickFile'),
|
||||||
gitStatus: (args: {
|
gitStatus: (args: {
|
||||||
rootPath: string;
|
rootPath: string;
|
||||||
}): Promise<
|
}): Promise<
|
||||||
@@ -90,22 +90,22 @@ export const ipc = {
|
|||||||
| Array<{ filePath: string; status: 'modified' | 'added' | 'deleted' | 'untracked' }>
|
| Array<{ filePath: string; status: 'modified' | 'added' | 'deleted' | 'untracked' }>
|
||||||
| ChannelMissing
|
| ChannelMissing
|
||||||
>
|
>
|
||||||
> => safeCall('file', 'gitStatus', args),
|
> => safeCall('git', 'status', args.rootPath),
|
||||||
writeBuffer: (args: {
|
writeBuffer: (args: {
|
||||||
path: string;
|
path: string;
|
||||||
buffer: Uint8Array;
|
buffer: Uint8Array;
|
||||||
}): Promise<IpcResult<void | ChannelMissing>> => safeCall('file', 'writeBuffer', args),
|
}): Promise<IpcResult<void | ChannelMissing>> => safeCall('file', 'list', args.path),
|
||||||
gitStage: (args: {
|
gitStage: (args: {
|
||||||
rootPath: string;
|
rootPath: string;
|
||||||
files: string[];
|
files: string[];
|
||||||
}): Promise<IpcResult<void | ChannelMissing>> => safeCall('file', 'gitStage', args),
|
}): Promise<IpcResult<void | ChannelMissing>> => safeCall('git', 'stage', args),
|
||||||
gitCommit: (args: {
|
gitCommit: (args: {
|
||||||
rootPath: string;
|
rootPath: string;
|
||||||
message: string;
|
message: string;
|
||||||
}): Promise<IpcResult<void | ChannelMissing>> => safeCall('file', 'gitCommit', args),
|
}): Promise<IpcResult<void | ChannelMissing>> => safeCall('git', 'commit', args),
|
||||||
setCurrent: (path: string | null): void => {
|
setCurrent: (path: string | null): void => {
|
||||||
if (typeof window !== 'undefined' && (window.electronAPI as any)?.file?.setCurrent) {
|
if (typeof window !== 'undefined' && window.electronAPI?.file?.setCurrent) {
|
||||||
(window.electronAPI as any).file.setCurrent(path);
|
window.electronAPI.file.setCurrent(path);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -117,22 +117,30 @@ export const ipc = {
|
|||||||
},
|
},
|
||||||
export: {
|
export: {
|
||||||
pdf: (opts: PdfOptions): Promise<IpcResult<ExportResult | ChannelMissing>> =>
|
pdf: (opts: PdfOptions): Promise<IpcResult<ExportResult | ChannelMissing>> =>
|
||||||
safeCall('export', 'pdf', opts),
|
wrap(() =>
|
||||||
|
window.electronAPI!.invoke('export-with-options', { format: 'pdf', options: opts })
|
||||||
|
),
|
||||||
docx: (opts: DocxOptions): Promise<IpcResult<ExportResult | ChannelMissing>> =>
|
docx: (opts: DocxOptions): Promise<IpcResult<ExportResult | ChannelMissing>> =>
|
||||||
safeCall('export', 'docx', opts),
|
wrap(() =>
|
||||||
|
window.electronAPI!.invoke('export-with-options', { format: 'docx', options: opts })
|
||||||
|
),
|
||||||
html: (opts: HtmlOptions): Promise<IpcResult<ExportResult | ChannelMissing>> =>
|
html: (opts: HtmlOptions): Promise<IpcResult<ExportResult | ChannelMissing>> =>
|
||||||
safeCall('export', 'html', opts),
|
wrap(() =>
|
||||||
|
window.electronAPI!.invoke('export-with-options', { format: 'html', options: opts })
|
||||||
|
),
|
||||||
batch: (
|
batch: (
|
||||||
items: BatchItem[],
|
items: BatchItem[],
|
||||||
opts: BatchOptions
|
opts: BatchOptions
|
||||||
): Promise<IpcResult<BatchResult | ChannelMissing>> => safeCall('export', 'batch', items, opts),
|
): Promise<IpcResult<BatchResult | ChannelMissing>> =>
|
||||||
|
wrap(() => window.electronAPI!.invoke('batch-convert', { items, options: opts })),
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
getVersion: (): Promise<IpcResult<string | ChannelMissing>> => safeCall('app', 'getVersion'),
|
getVersion: (): Promise<IpcResult<string | ChannelMissing>> =>
|
||||||
|
wrap(() => window.electronAPI!.getAppVersion()),
|
||||||
openExternal: (url: string): Promise<IpcResult<void | ChannelMissing>> =>
|
openExternal: (url: string): Promise<IpcResult<void | ChannelMissing>> =>
|
||||||
safeCall('app', 'openExternal', url),
|
wrap(() => window.electronAPI!.send('app:open-external', url)),
|
||||||
showItemInFolder: (path: string): Promise<IpcResult<void | ChannelMissing>> =>
|
showItemInFolder: (path: string): Promise<IpcResult<void | ChannelMissing>> =>
|
||||||
safeCall('app', 'showItemInFolder', path),
|
wrap(() => window.electronAPI!.invoke('app:show-item-in-folder', { path })),
|
||||||
showSaveDialog: (args?: {
|
showSaveDialog: (args?: {
|
||||||
title?: string;
|
title?: string;
|
||||||
defaultPath?: string;
|
defaultPath?: string;
|
||||||
@@ -140,15 +148,11 @@ export const ipc = {
|
|||||||
safeCall('app', 'showSaveDialog', args),
|
safeCall('app', 'showSaveDialog', args),
|
||||||
},
|
},
|
||||||
menu: {
|
menu: {
|
||||||
/**
|
|
||||||
* Subscribe to a native-menu IPC channel. The callback receives the
|
|
||||||
* raw payload(s) from the main process. Returns an unsubscribe fn.
|
|
||||||
*/
|
|
||||||
on: (channel: string, callback: (...args: unknown[]) => void): (() => void) => {
|
on: (channel: string, callback: (...args: unknown[]) => void): (() => void) => {
|
||||||
if (typeof window === 'undefined' || !window.electronAPI?.on) {
|
if (typeof window === 'undefined' || !window.electronAPI?.on) {
|
||||||
return () => {};
|
return () => {};
|
||||||
}
|
}
|
||||||
return window.electronAPI.on(channel, callback as (...a: any[]) => void);
|
return window.electronAPI.on(channel, callback as (...a: unknown[]) => void);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
updater: {
|
updater: {
|
||||||
|
|||||||
Vendored
+111
-69
@@ -1,91 +1,133 @@
|
|||||||
export interface ElectronAPI {
|
export interface ElectronAPI {
|
||||||
// App info
|
|
||||||
getAppVersion: () => Promise<string>;
|
getAppVersion: () => Promise<string>;
|
||||||
|
|
||||||
// File operations
|
send: (channel: string, data?: unknown) => void;
|
||||||
openFile: () => Promise<{ canceled: boolean; filePaths: string[] }>;
|
invoke: (channel: string, data?: unknown) => Promise<unknown>;
|
||||||
saveFile: (data: { path: string; content: string }) => Promise<void>;
|
on: (channel: string, callback: (...args: unknown[]) => void) => () => void;
|
||||||
readFile: (path: string) => Promise<string>;
|
once: (channel: string, callback: (...args: unknown[]) => void) => void;
|
||||||
setCurrentFile: (path: string | null) => void;
|
removeAllListeners: (channel: string) => void;
|
||||||
|
|
||||||
// Export
|
file: {
|
||||||
exportDocument: (format: string, options?: Record<string, any>) => Promise<void>;
|
save: (filePath: string, content: string) => void;
|
||||||
|
saveCurrent: (content: string) => void;
|
||||||
|
setCurrent: (filePath: string) => void;
|
||||||
|
saveRecent: (recentFiles: string[]) => void;
|
||||||
|
clearRecent: () => void;
|
||||||
|
rendererReady: () => void;
|
||||||
|
read: (filePath: string) => Promise<string>;
|
||||||
|
write: (filePath: string, content: string) => Promise<{ path: string }>;
|
||||||
|
delete: (filePath: string) => Promise<boolean>;
|
||||||
|
ensureDir: (dirPath: string) => Promise<string>;
|
||||||
|
exists: (filePath: string) => Promise<boolean>;
|
||||||
|
isDirectory: (filePath: string) => Promise<boolean>;
|
||||||
|
copy: (source: string, destination: string) => Promise<{ source: string; destination: string }>;
|
||||||
|
move: (source: string, destination: string) => Promise<{ source: string; destination: string }>;
|
||||||
|
list: (dirPath: string) => Promise<unknown>;
|
||||||
|
pickFolder: () => Promise<string | null>;
|
||||||
|
pickFile: () => Promise<string | null>;
|
||||||
|
};
|
||||||
|
|
||||||
// Theme
|
theme: {
|
||||||
getTheme: () => void;
|
get: () => void;
|
||||||
onThemeChanged: (callback: (theme: string) => void) => () => void;
|
};
|
||||||
|
|
||||||
// File events
|
print: {
|
||||||
onFileOpened: (callback: (data: { path: string; content: string }) => void) => () => void;
|
doPrint: (options?: unknown) => void;
|
||||||
onFileNew: (callback: () => void) => () => void;
|
show: (payload?: unknown) => void;
|
||||||
onFileSave: (callback: () => void) => () => void;
|
};
|
||||||
|
|
||||||
// Conversion status
|
export: {
|
||||||
onConversionStatus: (callback: (message: string) => void) => () => void;
|
withOptions: (format: string, options?: unknown) => void;
|
||||||
onConversionComplete: (
|
spreadsheet: (content: string, format: string) => void;
|
||||||
callback: (data: { format: string; outputPath: string }) => void
|
};
|
||||||
) => () => void;
|
|
||||||
|
|
||||||
// Dialogs
|
batch: {
|
||||||
showExportDialog: (format: string) => void;
|
convert: (inputFolder: string, outputFolder: string, format: string, options?: unknown) => void;
|
||||||
showBatchDialog: () => void;
|
selectFolder: (type?: string) => void;
|
||||||
showUniversalConverterDialog: () => void;
|
};
|
||||||
showTableGenerator: () => void;
|
|
||||||
showPdfEditorDialog: () => void;
|
|
||||||
showHeaderFooterDialog: () => void;
|
|
||||||
showFieldPickerDialog: () => void;
|
|
||||||
|
|
||||||
// Settings
|
converter: {
|
||||||
getSettings: (key: string) => Promise<any>;
|
convert: (tool: string, fromFormat: string, toFormat: string, filePath: string) => void;
|
||||||
setSettings: (key: string, value: any) => Promise<void>;
|
convertBatch: (
|
||||||
|
tool: string,
|
||||||
|
fromFormat: string,
|
||||||
|
toFormat: string,
|
||||||
|
inputFolder: string,
|
||||||
|
outputFolder: string
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
// Plugin settings
|
headerFooter: {
|
||||||
getPluginSetting: (key: string) => Promise<any>;
|
getSettings: () => void;
|
||||||
setPluginSetting: (key: string, value: any) => Promise<void>;
|
saveSettings: (settings: unknown) => void;
|
||||||
|
browseLogo: (position?: string) => void;
|
||||||
|
saveLogo: (position: string, filePath: string) => void;
|
||||||
|
clearLogo: (position?: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
// Sidebar
|
page: {
|
||||||
toggleSidebar: () => void;
|
getSettings: () => void;
|
||||||
toggleBottomPanel: () => void;
|
updateSettings: (settings: unknown) => void;
|
||||||
|
setCustomStartPage: (pageNumber: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
// Print
|
pdf: {
|
||||||
doPrint: (options?: any) => void;
|
processOperation: (data: unknown) => void;
|
||||||
|
getPageCount: (filePath: string) => void;
|
||||||
|
selectFolder: (inputId?: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
// PDF
|
image: {
|
||||||
openPdfViewer: (filePath: string) => void;
|
convert: (data: unknown) => void;
|
||||||
processPdfOperation: (data: any) => void;
|
batchConvert: (data: unknown) => void;
|
||||||
|
resize: (data: unknown) => void;
|
||||||
|
compress: (data: unknown) => void;
|
||||||
|
rotate: (data: unknown) => void;
|
||||||
|
};
|
||||||
|
|
||||||
// Images
|
audio: {
|
||||||
selectFolder: () => Promise<string | null>;
|
convert: (data: unknown) => void;
|
||||||
savePastedImage: (data: {
|
batchConvert: (data: unknown) => void;
|
||||||
base64: string;
|
extract: (data: unknown) => void;
|
||||||
ext: string;
|
trim: (data: unknown) => void;
|
||||||
}) => Promise<{ relativePath: string } | null>;
|
merge: (data: unknown) => void;
|
||||||
|
};
|
||||||
|
|
||||||
// Templates
|
video: {
|
||||||
loadTemplate: (file: string) => Promise<string>;
|
convert: (data: unknown) => void;
|
||||||
getSnippets: () => Promise<any[]>;
|
batchConvert: (data: unknown) => void;
|
||||||
saveSnippet: (snippet: any) => Promise<void>;
|
compress: (data: unknown) => void;
|
||||||
deleteSnippet: (id: string) => Promise<void>;
|
trim: (data: unknown) => void;
|
||||||
|
extractFrames: (data: unknown) => void;
|
||||||
|
toGif: (data: unknown) => void;
|
||||||
|
};
|
||||||
|
|
||||||
// Git
|
git: {
|
||||||
gitStatus: () => Promise<any>;
|
status: (rootPath?: string) => Promise<unknown>;
|
||||||
gitDiff: (file: string) => Promise<any>;
|
stage: (args: { rootPath?: string; files: string[] }) => Promise<unknown>;
|
||||||
gitStage: (files: string[]) => Promise<void>;
|
commit: (args: { rootPath?: string; message: string }) => Promise<unknown>;
|
||||||
gitCommit: (message: string) => Promise<void>;
|
log: (rootPath?: string) => Promise<unknown>;
|
||||||
gitLog: () => Promise<any[]>;
|
diff: (filePath: string) => Promise<unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
// Directory
|
app: {
|
||||||
listDirectory: (dir: string) => Promise<any[] | null>;
|
quit: () => void;
|
||||||
openFilePath: (path: string) => void;
|
openExternal: (url: string) => void;
|
||||||
|
showSaveDialog: (args?: { title?: string; defaultPath?: string }) => Promise<unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
// Custom CSS
|
updater: {
|
||||||
selectCustomCSS: () => Promise<string | null>;
|
check: () => Promise<unknown>;
|
||||||
loadCustomCSS: () => void;
|
install: () => Promise<unknown>;
|
||||||
clearCustomCSS: () => void;
|
getState: () => Promise<unknown>;
|
||||||
|
onStatus: (cb: (payload: unknown) => void) => () => void;
|
||||||
|
};
|
||||||
|
|
||||||
// Generic invoke/on
|
crash: {
|
||||||
invoke: (channel: string, data?: any) => Promise<any>;
|
read: () => Promise<unknown>;
|
||||||
on: (channel: string, callback: (...args: any[]) => void) => () => void;
|
openDir: () => void;
|
||||||
|
delete: (filename: string) => Promise<unknown>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -100,7 +100,10 @@ describe('ExportPdfDialog', () => {
|
|||||||
|
|
||||||
it('renders an error banner when IPC fails', async () => {
|
it('renders an error banner when IPC fails', async () => {
|
||||||
(window as any).electronAPI = undefined;
|
(window as any).electronAPI = undefined;
|
||||||
(ipc.print.show as any).mockReturnValueOnce({ ok: false, error: { message: 'Pandoc not found' } });
|
(ipc.print.show as any).mockReturnValueOnce({
|
||||||
|
ok: false,
|
||||||
|
error: { message: 'Pandoc not found' },
|
||||||
|
});
|
||||||
render(<ExportPdfDialog sourcePath="/test.md" />);
|
render(<ExportPdfDialog sourcePath="/test.md" />);
|
||||||
await userEvent.click(screen.getByRole('button', { name: /^export$/i }));
|
await userEvent.click(screen.getByRole('button', { name: /^export$/i }));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user