fix(flowchart): pre-await getUserDataPath to fix persistence; bump v4.9.0 → v4.9.1

Amit Haridas
This commit is contained in:
2026-09-14 22:24:27 +05:30
parent 58aa4da00f
commit 8c708b7abb
6 changed files with 51 additions and 7 deletions
+1 -1
View File
@@ -257,4 +257,4 @@ Amit Haridas (amit.wh@gmail.com)
## Version ## Version
v4.9.0 v4.9.1
+5
View File
@@ -1,5 +1,10 @@
# PanConverter - Updates & Changelog # PanConverter - Updates & Changelog
## Version 4.9.1 (2026-09-14)
### Fixes
- **Flowchart Panel save failed**: `getUserDataPath()` IPC was not awaited in `src/renderer.js:2439-2449`, so the persistence path was computed as `"[object Promise]/flowchart-session.json"` and rejected by the `write-text-file` userData sandbox. Pre-resolved the path on panel register and cached it; persistence (read and write) now works correctly.
## Version 4.9.0 (2026-09-14) ## Version 4.9.0 (2026-09-14)
### New: Visual Flow Chart Editor (Sidebar panel → "Flow Chart") ### New: Visual Flow Chart Editor (Sidebar panel → "Flow Chart")
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "markdown-converter", "name": "markdown-converter",
"version": "4.9.0", "version": "4.9.1",
"description": "Professional Markdown editor and universal file converter with PDF editing, batch processing, and syntax highlighting", "description": "Professional Markdown editor and universal file converter with PDF editing, batch processing, and syntax highlighting",
"main": "src/main.js", "main": "src/main.js",
"scripts": { "scripts": {
+12 -4
View File
@@ -2436,16 +2436,24 @@ document.addEventListener('DOMContentLoaded', async () => {
.run({ nodes: [div] }) .run({ nodes: [div] })
.catch((err) => console.warn('flowchart preview render failed:', err)); .catch((err) => console.warn('flowchart preview render failed:', err));
}; };
// Pre-resolve the userData path on first panel render and cache it. The
// flowchart panel's getUserDataPath() must be sync (it is interpolated into
// the persistence path on mount); calling flowchartIO.getUserDataPath()
// directly would coerce the IPC Promise to "[object Promise]" and break
// every write/read against <userData>/flowchart-session.json.
let cachedUserDataPath = null;
sidebarManager.registerPanel('flowchart', { sidebarManager.registerPanel('flowchart', {
title: 'Flow Chart', title: 'Flow Chart',
render: (container) => render: async (container) => {
renderFlowChartPanel(container, { if (!cachedUserDataPath) cachedUserDataPath = await flowchartIO.getUserDataPath();
getUserDataPath: flowchartIO.getUserDataPath, return renderFlowChartPanel(container, {
getUserDataPath: () => cachedUserDataPath,
readFile: flowchartIO.readFile, readFile: flowchartIO.readFile,
writeFile: flowchartIO.writeFile, writeFile: flowchartIO.writeFile,
insertAtCursor: (text) => tabManager.insertAtCursor(text), insertAtCursor: (text) => tabManager.insertAtCursor(text),
renderMermaid: renderFlowChartMermaid, renderMermaid: renderFlowChartMermaid,
}), });
},
}); });
// Wiki-link navigation: open the target note (creating it on demand after // Wiki-link navigation: open the target note (creating it on demand after
+1 -1
View File
@@ -11,7 +11,7 @@
* *
* @param {HTMLElement} container Mount point inside the sidebar panel * @param {HTMLElement} container Mount point inside the sidebar panel
* @param {object} deps * @param {object} deps
* @param {() => string} deps.getUserDataPath Absolute userData directory * @param {() => string} deps.getUserDataPath Absolute userData directory (renderer pre-resolves and caches this on first mount — must be synchronous and return a real string, not a Promise)
* @param {(path:string) => Promise<string|null>} deps.readFile * @param {(path:string) => Promise<string|null>} deps.readFile
* @param {(path:string, content:string) => Promise<void>} deps.writeFile * @param {(path:string, content:string) => Promise<void>} deps.writeFile
* @param {(text:string) => void} deps.insertAtCursor * @param {(text:string) => void} deps.insertAtCursor
+31
View File
@@ -67,6 +67,37 @@ describe('flowchart-panel: persistence', () => {
const readFile = jest.fn().mockResolvedValue('{not-json'); const readFile = jest.fn().mockResolvedValue('{not-json');
expect(() => mount({ readFile })).not.toThrow(); expect(() => mount({ readFile })).not.toThrow();
}); });
// Regression: src/renderer.js used to pass flowchartIO.getUserDataPath
// (which returns ipcRenderer.invoke's Promise) directly into the panel.
// The panel then coerced that Promise to "[object Promise]" in the
// template literal, producing a persistence path that fell outside the
// userData sandbox and was rejected by write-text-file. The fix is to
// await the path on the renderer side and cache it, then hand the panel
// a sync getter. This test mirrors that pattern: simulate the renderer's
// pre-resolve + cache, and assert the panel's persistence path is a
// real resolved string on mount.
test('handles an async getUserDataPath via renderer-side pre-resolve + cache', async () => {
const asyncGetUserDataPath = jest.fn().mockResolvedValue('/abs/path/to/userdata');
// Pre-resolve exactly the way src/renderer.js now does.
let cachedUserDataPath = null;
if (!cachedUserDataPath) cachedUserDataPath = await asyncGetUserDataPath();
const syncGetUserDataPath = () => cachedUserDataPath;
const writeFile = jest.fn().mockResolvedValue(undefined);
const { api } = mount({ getUserDataPath: syncGetUserDataPath, writeFile });
// read on mount must use the resolved (real) path, not "[object Promise]".
await Promise.resolve();
expect(asyncGetUserDataPath).toHaveBeenCalledTimes(1);
// mutate -> debounced write -> assert writeFile is called with the real path.
api.getStore().addNode({ kind: 'process', x: 0, y: 0, label: 'async-cache' });
jest.advanceTimersByTime(500);
await Promise.resolve();
expect(writeFile).toHaveBeenCalledTimes(1);
expect(writeFile.mock.calls[0][0]).toBe('/abs/path/to/userdata/flowchart-session.json');
});
}); });
describe('flowchart-panel: live preview', () => { describe('flowchart-panel: live preview', () => {