mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 18:10:18 +05:30
fix(ipc): wire preload bridge so menu Open file actually loads in editor + preview
Two related defects were breaking the 'open file' flow: 1. The BrowserWindow webPreferences had no preload path AND contextIsolation: false, so contextBridge.exposeInMainWorld threw on load and window.electronAPI was never set. Result: every renderer-side ipc.* call returned CHANNEL_MISSING and silently no-op'd. Toolbar Open/Save, the file menu, every dialog — all broken. 2. The 'file.opened' IPC bridge in register-menu-commands.ts dispatched to a 'file.opened' command that was never registered as a handler. Even if the preload had worked, the main menu's File -> Open would have been a no-op once it reached the command store. Fixes: - window/index.js: add preload path; set contextIsolation: true (required for contextBridge) and nodeIntegration: false (renderer no longer needs direct Node access; everything goes through the whitelisted preload bridge). - file-store.ts: add openFileFromMain(path, content) that opens a tab + editor buffer from content the main process already read (skips the renderer's ipc.file.read since main has the bytes). Preserves existing buffer content if the file is already open. - register-menu-commands.ts: register 'file.opened' to call openFileFromMain. Drop the dead 'command.palette' bridge (no consumer, no UI, no handler). - tests: two new cases for openFileFromMain — verifies it does not call ipc.file.read and does not clobber user edits on re-open. Verified end-to-end via the run-desktop driver: triggering file-opened from the main process with a test .md now shows the content in BOTH the CodeMirror editor and the rendered preview pane (plus the Outline panel picks up the H1). 497 tests pass.
This commit is contained in:
@@ -16,8 +16,15 @@ function createMainWindow() {
|
|||||||
y: bounds.y,
|
y: bounds.y,
|
||||||
show: true,
|
show: true,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true,
|
// The preload script exposes `window.electronAPI` — the only IPC
|
||||||
contextIsolation: false,
|
// bridge the renderer uses. Without this, every renderer call returns
|
||||||
|
// CHANNEL_MISSING and file/folder/save all silently no-op.
|
||||||
|
preload: path.join(__dirname, '../../preload.js'),
|
||||||
|
// contextIsolation MUST be true for the preload's contextBridge
|
||||||
|
// exposeInMainWorld call to succeed. Without it, the preload throws
|
||||||
|
// on load and the renderer never gets the IPC bridge.
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
spellcheck: true
|
spellcheck: true
|
||||||
},
|
},
|
||||||
icon: path.join(__dirname, '../../../assets/icon.png')
|
icon: path.join(__dirname, '../../../assets/icon.png')
|
||||||
|
|||||||
@@ -102,6 +102,10 @@ export function registerMenuCommands(): void {
|
|||||||
});
|
});
|
||||||
register('view.toggleSidebar', () => useAppStore.getState().toggleSidebar());
|
register('view.toggleSidebar', () => useAppStore.getState().toggleSidebar());
|
||||||
register('view.togglePreview', () => useAppStore.getState().togglePreview());
|
register('view.togglePreview', () => useAppStore.getState().togglePreview());
|
||||||
|
register('file.opened', (payload?: { path?: string; content?: string }) => {
|
||||||
|
if (!payload?.path) return;
|
||||||
|
void useFileStore.getState().openFileFromMain(payload.path, payload.content ?? '');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRegisterMenuCommands(): void {
|
export function useRegisterMenuCommands(): void {
|
||||||
@@ -118,7 +122,6 @@ export function useRegisterMenuCommands(): void {
|
|||||||
export function useBridgeNativeMenu(): void {
|
export function useBridgeNativeMenu(): void {
|
||||||
useMenuAction('file-save', 'file.save');
|
useMenuAction('file-save', 'file.save');
|
||||||
useMenuAction('toggle-preview', 'view.togglePreview');
|
useMenuAction('toggle-preview', 'view.togglePreview');
|
||||||
useMenuAction('toggle-command-palette', 'command.palette');
|
|
||||||
useMenuAction('toggle-sidebar-panel', 'view.sidebarPanel', (panel) => panel as string);
|
useMenuAction('toggle-sidebar-panel', 'view.sidebarPanel', (panel) => panel as string);
|
||||||
useMenuAction('toggle-bottom-panel', 'view.bottomPanel');
|
useMenuAction('toggle-bottom-panel', 'view.bottomPanel');
|
||||||
useMenuAction('toggle-find', 'find.toggle');
|
useMenuAction('toggle-find', 'find.toggle');
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ interface FileState {
|
|||||||
loadChildren: (dirPath: string) => Promise<void>;
|
loadChildren: (dirPath: string) => Promise<void>;
|
||||||
toggleExpanded: (dirPath: string) => void;
|
toggleExpanded: (dirPath: string) => void;
|
||||||
openFile: (filePath: string) => Promise<void>;
|
openFile: (filePath: string) => Promise<void>;
|
||||||
|
openFileFromMain: (filePath: string, content: string) => Promise<void>;
|
||||||
closeTab: (id: string) => void;
|
closeTab: (id: string) => void;
|
||||||
setActiveTab: (id: string) => void;
|
setActiveTab: (id: string) => void;
|
||||||
markTabClean: (id: string) => void;
|
markTabClean: (id: string) => void;
|
||||||
@@ -165,6 +166,25 @@ export const useFileStore = create<FileState>()(
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Used when the main process has already read the file (e.g. file-opened
|
||||||
|
// IPC from File → Open menu). Skips the renderer-side IPC read since main
|
||||||
|
// already has the content. Preserves the editor's current buffer content
|
||||||
|
// if the file is already open.
|
||||||
|
openFileFromMain: async (filePath, content) => {
|
||||||
|
const existing = useFileStore.getState().openTabs.find((t) => t.id === filePath);
|
||||||
|
if (existing) {
|
||||||
|
set((s) => { s.activeTabId = filePath; });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = filePath.split('/').pop() ?? filePath;
|
||||||
|
useEditorStore.getState().openBuffer(filePath, filePath, content);
|
||||||
|
set((s) => {
|
||||||
|
s.openTabs.push({ id: filePath, path: filePath, title, dirty: false });
|
||||||
|
s.activeTabId = filePath;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
closeTab: (id) => {
|
closeTab: (id) => {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
const idx = s.openTabs.findIndex((t) => t.id === id);
|
const idx = s.openTabs.findIndex((t) => t.id === id);
|
||||||
|
|||||||
@@ -232,6 +232,37 @@ describe('useFileStore', () => {
|
|||||||
expect(useEditorStore.getState().buffers.size).toBe(0);
|
expect(useEditorStore.getState().buffers.size).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- openFileFromMain ---
|
||||||
|
|
||||||
|
it('openFileFromMain does NOT call ipc.file.read (main already sent content)', async () => {
|
||||||
|
await useFileStore.getState().openFileFromMain('/root/from-menu.md', '# from menu');
|
||||||
|
|
||||||
|
expect(fakeRead).not.toHaveBeenCalled();
|
||||||
|
const buf = useEditorStore.getState().buffers.get('/root/from-menu.md');
|
||||||
|
expect(buf).not.toBeUndefined();
|
||||||
|
expect(buf!.content).toBe('# from menu');
|
||||||
|
const tabs = useFileStore.getState().openTabs;
|
||||||
|
expect(tabs).toHaveLength(1);
|
||||||
|
expect(tabs[0].path).toBe('/root/from-menu.md');
|
||||||
|
expect(tabs[0].title).toBe('from-menu.md');
|
||||||
|
expect(useFileStore.getState().activeTabId).toBe('/root/from-menu.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('openFileFromMain on an already-open file does not overwrite the editor buffer', async () => {
|
||||||
|
// Open once via the menu path
|
||||||
|
await useFileStore.getState().openFileFromMain('/root/x.md', '# menu version');
|
||||||
|
// User then types something in the editor (simulated via updateContent)
|
||||||
|
useEditorStore.getState().updateContent('/root/x.md', '# user edited');
|
||||||
|
|
||||||
|
// Menu sends the same file again (e.g. user picks it from Recent Files)
|
||||||
|
await useFileStore.getState().openFileFromMain('/root/x.md', '# menu version');
|
||||||
|
|
||||||
|
// Buffer keeps the user's edits — the menu re-open must not clobber them
|
||||||
|
expect(useEditorStore.getState().buffers.get('/root/x.md')!.content).toBe('# user edited');
|
||||||
|
expect(useFileStore.getState().openTabs).toHaveLength(1);
|
||||||
|
expect(useFileStore.getState().activeTabId).toBe('/root/x.md');
|
||||||
|
});
|
||||||
|
|
||||||
// --- closeTab ---
|
// --- closeTab ---
|
||||||
|
|
||||||
it('closeTab removes the tab and updates activeTabId to the previous neighbor', async () => {
|
it('closeTab removes the tab and updates activeTabId to the previous neighbor', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user