fix: wire orphaned IPC channels, fix data-loss bug, print preview events, sidebar navigation

- Fix file.clearRecent sending IPC to main process instead of clearing openTabs
- Wire show-batch-converter, show-document-compare, open-header-footer-dialog IPC channels
- Add print preview event listeners (mc:print-preview, mc:print-preview-styled) in App.tsx
- Fix sidebar hidden bridge buttons toggling sidebar closed after scroll
- Update sidebar menu labels to match actual sections (Files, Outline, Git)
- Fix ipc.print to be an object with show/doPrint methods (was bare function)
- Update callers: ExportPdfDialog, pdf-export, PrintPreview
- Add 7 regression tests for all fixes
- Clean 2 obsolete snapshots
- Build Linux packages (deb, AppImage, snap) — 549 tests passing
This commit is contained in:
2026-06-11 07:15:33 +05:30
parent e25a5e1d75
commit cf6b6817b9
11 changed files with 179 additions and 24 deletions
+3 -4
View File
@@ -226,10 +226,9 @@ function viewItems(mainWindow) {
{
label: 'Sidebar',
submenu: [
{ label: 'File Explorer', click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'explorer') },
{ label: 'Git', click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'git') },
{ label: 'Snippets', click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'snippets') },
{ label: 'Templates', click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'templates') }
{ label: 'Files', click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'explorer') },
{ label: 'Outline', click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'snippets') },
{ label: 'Git', click: () => mainWindow.webContents.send('toggle-sidebar-panel', 'git') }
]
},
{
+7 -1
View File
@@ -17,7 +17,13 @@ function App() {
useEffect(() => {
const handler = () => setPrintOpen(true);
window.addEventListener('mc:print', handler);
return () => window.removeEventListener('mc:print', handler);
window.addEventListener('mc:print-preview', handler);
window.addEventListener('mc:print-preview-styled', handler);
return () => {
window.removeEventListener('mc:print', handler);
window.removeEventListener('mc:print-preview', handler);
window.removeEventListener('mc:print-preview-styled', handler);
};
}, []);
return (
@@ -51,7 +51,7 @@ export function ExportPdfDialog({ sourcePath }: { sourcePath: string }) {
const m = MARGIN_MAP[margins];
const pageCss = `@page { size: ${fmt.width} ${fmt.height}; margin: ${m.top}mm ${m.right}mm ${m.bottom}mm ${m.left}mm; }`;
const finalHtml = html.replace('</style>', `${pageCss}</style>`);
const result = await ipc.print({ html: finalHtml, withStyles: embed });
const result = await ipc.print.show({ html: finalHtml, withStyles: embed });
if (!result.ok) {
const msg = result.error?.message ?? 'PDF export failed';
toast.error(`Export failed: ${msg}`);
+4 -6
View File
@@ -3,14 +3,12 @@ import { Button } from '@/components/ui/button';
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/collapsible';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useFileStore } from '@/stores/file-store';
import { useCommandStore } from '@/stores/command-store';
import { FileTree } from './FileTree';
import { Outline } from './Outline';
import { GitStatusPanel } from './GitStatusPanel';
export function Sidebar() {
const tree = useFileStore((s) => s.tree);
const dispatch = useCommandStore((s) => s.dispatch);
function scrollToSection(label: string) {
const el = document.querySelector(`[data-sidebar-section="${label}"]`);
@@ -86,10 +84,10 @@ export function Sidebar() {
matching section into view. The hidden elements expose a hook for
Playwright tests and the menu handler. */}
<div className="sr-only" aria-hidden="true">
<button data-testid="sidebar-jump-explorer" onClick={() => { scrollToSection('Files'); dispatch('view.toggleSidebar'); }}>jump-explorer</button>
<button data-testid="sidebar-jump-git" onClick={() => { scrollToSection('Git'); dispatch('view.toggleSidebar'); }}>jump-git</button>
<button data-testid="sidebar-jump-snippets" onClick={() => { scrollToSection('Outline'); dispatch('view.toggleSidebar'); }}>jump-snippets</button>
<button data-testid="sidebar-jump-templates" onClick={() => { scrollToSection('Files'); dispatch('view.toggleSidebar'); }}>jump-templates</button>
<button data-testid="sidebar-jump-explorer" onClick={() => scrollToSection('Files')}>jump-explorer</button>
<button data-testid="sidebar-jump-git" onClick={() => scrollToSection('Git')}>jump-git</button>
<button data-testid="sidebar-jump-snippets" onClick={() => scrollToSection('Outline')}>jump-snippets</button>
<button data-testid="sidebar-jump-templates" onClick={() => scrollToSection('Files')}>jump-templates</button>
</div>
</div>
);
@@ -225,8 +225,35 @@ export function registerMenuCommands(): void {
window.dispatchEvent(new CustomEvent('mc:print-preview-styled'));
},
// Batch converter — PDF batch reuses ExportBatchDialog; others show coming-soon toast.
'batch.showConverter': (type?: string) => {
if (!type) return;
if (type === 'pdf') {
const paths = useFileStore.getState().openTabs.map((t) => t.path);
if (paths.length > 0) {
useAppStore.getState().openModal('export-batch', { sourcePaths: paths });
} else {
toast.info('Open files first to batch-export as PDF');
}
return;
}
toast.info(
`${type.charAt(0).toUpperCase() + type.slice(1)} batch conversion — coming soon!`,
);
},
// Document compare — no modal yet, acknowledge with toast.
'tools.documentCompare': () => {
toast.info('Document compare — coming soon!');
},
// Header & footer settings — point users to Settings → Editor.
'settings.headerFooter': () => {
useAppStore.getState().openModal('settings');
},
'file.clearRecent': () => {
useFileStore.setState({ openTabs: [] });
window.electronAPI?.send?.('clear-recent-files');
},
// File → New — creates an unsaved buffer with a default name.
@@ -324,4 +351,7 @@ export function useBridgeNativeMenu(): void {
useMenuAction('file-opened', 'file.opened', (payload) => payload);
useMenuAction('clear-recent-files', 'file.clearRecent');
useMenuAction('file-new', 'file.new');
useMenuAction('show-batch-converter', 'batch.showConverter', (type) => type as string);
useMenuAction('show-document-compare', 'tools.documentCompare');
useMenuAction('open-header-footer-dialog', 'settings.headerFooter');
}
+6 -2
View File
@@ -83,8 +83,12 @@ export const ipc = {
writeBuffer: (args: { path: string; buffer: Uint8Array }): Promise<IpcResult<void | ChannelMissing>> =>
safeCall('file', 'writeBuffer', args),
},
print: (args: { html: string }): Promise<IpcResult<void | ChannelMissing>> =>
safeCall('print', 'show', args),
print: {
show: (args: { html: string }): Promise<IpcResult<void | ChannelMissing>> =>
safeCall('print', 'show', args),
doPrint: (args: { withStyles?: boolean }): Promise<IpcResult<void | ChannelMissing>> =>
safeCall('print', 'doPrint', args),
},
export: {
pdf: (opts: PdfOptions): Promise<IpcResult<ExportResult | ChannelMissing>> =>
safeCall('export', 'pdf', opts),
+1 -1
View File
@@ -41,7 +41,7 @@ export async function generatePdf(options: PdfExportOptions): Promise<void> {
// Hand the rendered HTML to the main process for native print-to-PDF.
// This avoids the print dialog and works headlessly.
const result = await ipc.print({ html: finalHtml });
const result = await ipc.print.show({ html: finalHtml });
if (!result.ok) {
toast.error(`PDF export failed: ${result.error?.message ?? 'unknown error'}`);
} else {