mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 09:19:34 +05:30
refactor(flowchart): standalone window (Cmd+Alt+F) replaces sidebar panel; v4.9.6
Five fix rounds (v4.9.1 → v4.9.5) couldn't make the sidebar flowchart
panel feel right — at 280 px sidebar with canvas + preview cramped to
~175 px each, plus the editor-container hide/show dance the
maximize/restore toggle required, the panel kept presenting as
cramped and unreliable at runtime. Strategy pivot: the flowchart
editor now lives in its own BrowserWindow, matching the ASCII Art
Generator pattern.
- src/flowchart-generator.html — new standalone HTML with header,
toolbar (Insert at Cursor / Reset), canvas host, and preview host.
Stylesheet hrefs are src/-relative (no ../). Forced light surface
via background/color !important rules — mirrors the v4.9.5 CSS fix.
- src/renderer/flowchart-controller.js — pure browser IIFE. Hydrates
from <userData>/flowchart-session.json once on mount, persists on
every mutation with 500 ms debounce. Insert at Cursor wraps the
generated Mermaid source in a fenced ```mermaid block and sends
it through the existing insert-content IPC.
- src/main.js — openFlowchartGenerator() launches the standalone
BrowserWindow (1100×720, contextIsolation:true, nodeIntegration:false).
Tools menu entry 'Flowchart Generator' with Cmd/Ctrl+Alt+F.
- src/preload.js — new window.electronAPI.flowchart.* namespace with
getUserDataPath/readFile/writeFile/insertAtCursor, reusing the
existing thin text-file IPC handlers.
- The four pure modules (flowchart-shapes.js, flowchart-mermaid.js,
flowchart-store.js, flowchart-canvas.js) gained a tiny UMD wrapper
so they work both as CommonJS (legacy sidebar) and as browser
globals (standalone window). No behavioural change to the 73
flowchart unit tests in tests/flowchart-*.test.js.
- src/renderer.js — sidebar registerPanel('flowchart', ...) and the
commandPalette entry are now commented out. Legacy panel file
(src/sidebar/flowchart-panel.js) preserved for rollback.
Tests:
- tests/flowchart-controller.test.js — 10 new tests covering
stylesheet paths (no ../ escape), bootstrap wiring
(getUserDataPath once, reads flowchart-session.json), hydration
from a saved session, Insert-at-Cursor fenced block format,
Reset (with and without confirm), and two regression tests that
src/renderer.js no longer has a live sidebar registration.
All 1122 tests pass; lint + format clean.
Amit Haridas
This commit is contained in:
@@ -257,4 +257,4 @@ Amit Haridas (amit.wh@gmail.com)
|
|||||||
|
|
||||||
## Version
|
## Version
|
||||||
|
|
||||||
v4.9.5
|
v4.9.6
|
||||||
|
|||||||
+16
@@ -1,5 +1,21 @@
|
|||||||
# PanConverter - Updates & Changelog
|
# PanConverter - Updates & Changelog
|
||||||
|
|
||||||
|
## Version 4.9.6 (2026-09-14)
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
- **Flowchart editor is now a standalone window, not a sidebar panel.** Five fix rounds (v4.9.1 → v4.9.5) couldn't make the sidebar panel feel right — at 280 px sidebar with the canvas + preview split to ~175 px each, plus the editor-container hiding dance the maximize/restore toggle required, the panel kept presenting as cramped and unreliable at runtime. Strategy pivot: the flowchart editor now lives in its own BrowserWindow, matching the ASCII Art Generator pattern (`src/ascii-generator.html` + `src/renderer/ascii-controller.js`).
|
||||||
|
- New `src/flowchart-generator.html` — standalone HTML with its own header, toolbar (Insert at Cursor / Reset), canvas host, and preview host (text-only — the canvas on the left IS the visual preview). All stylesheet `href`s are `src/`-relative — no `../` escape (lesson learned from v4.9.2). Forced light surface (`background: #fafafa !important; color: #1f2328 !important`) on the canvas + preview regardless of the project's body theme, mirroring the v4.9.5 CSS fix that traded theme consistency for guaranteed visibility.
|
||||||
|
- New `src/renderer/flowchart-controller.js` — pure browser IIFE. Wires the canvas + preview, hydrates from `<userData>/flowchart-session.json` once on mount, persists on every store mutation with a 500 ms debounce. Insert at Cursor wraps the generated `flowchart TD` source in a fenced ```` ```mermaid ```` block and sends it through the existing `insert-content` IPC channel — same one the renderer.js sidebar panel used. Keyboard shortcuts (Ctrl/Cmd+Z / Ctrl/Cmd+Shift+Z / Delete / Backspace) handled at document level.
|
||||||
|
- New `openFlowchartGenerator()` in `src/main.js` — `BrowserWindow` (1100×720, parent: mainWindow, `contextIsolation: true, nodeIntegration: false`) launched by an `ipcMain.on('open-flowchart-generator')` listener. Tools menu now has a "Flowchart Generator" entry with accelerator `CmdOrCtrl+Alt+F`.
|
||||||
|
- New `window.electronAPI.flowchart.*` namespace in `src/preload.js` — `getUserDataPath` / `readFile` / `writeFile` / `insertAtCursor`. Reuses the existing thin IPC handlers (`get-user-data-path`, `read-text-file`, `write-text-file`) which already sandbox writes to `<userData>`.
|
||||||
|
- The four pure modules (`flowchart-shapes.js`, `flowchart-mermaid.js`, `flowchart-store.js`, `flowchart-canvas.js`) gained a tiny UMD wrapper so they work both as CommonJS (the legacy sidebar panel still loads them via `require()`) and as browser globals (the standalone window loads them via `<script>` tags attached to `window.FlowchartShapes`, etc.). The CommonJS shape is preserved — no behavioural change to the 73 flowchart unit tests in `tests/flowchart-*.test.js`.
|
||||||
|
|
||||||
|
### Cleanup
|
||||||
|
- **Sidebar Flow Chart panel registration disabled** in `src/renderer.js` — the `sidebarManager.registerPanel('flowchart', …)` call and the matching `commandPalette.register('Toggle Sidebar: Flow Chart', …)` entry are now both commented out. The legacy panel implementation (`src/sidebar/flowchart-panel.js`) and its unit tests (`tests/flowchart-panel.test.js`) are kept untouched for rollback — re-enabling is a one-step uncomment in `src/renderer.js`. The unused `flowchartIO` helper that the panel needed was also removed.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
- New `tests/flowchart-controller.test.js` (10 tests) — verifies the standalone window's HTML doesn't `../`-escape any stylesheet, `bootstrap()` resolves `getUserDataPath` exactly once on mount, reads `<userData>/flowchart-session.json` on mount, hydrates the store from a saved session, wraps Insert-at-Cursor output in a `mermaid` fenced block, Reset clears nodes/edges (with confirm) and persists the empty graph (without confirm). Two regression tests assert that `src/renderer.js` no longer contains a live `sidebarManager.registerPanel('flowchart', …)` call or a live `commandPalette.register('Toggle Sidebar: Flow Chart', …)` entry.
|
||||||
|
|
||||||
## Version 4.9.5 (2026-09-14)
|
## Version 4.9.5 (2026-09-14)
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "markdown-converter",
|
"name": "markdown-converter",
|
||||||
"version": "4.9.5",
|
"version": "4.9.6",
|
||||||
"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": {
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Flowchart Generator - MarkdownConverter</title>
|
||||||
|
<!--
|
||||||
|
v4.9.6 — Standalone Flowchart Generator window.
|
||||||
|
Stylesheets are src/-relative (mirrors src/index.html). NO ../ escape —
|
||||||
|
the standalone BrowserWindow's document base is src/, so a ../fonts.css
|
||||||
|
would resolve outside src/ and fail to load.
|
||||||
|
-->
|
||||||
|
<link rel="stylesheet" href="fonts.css" />
|
||||||
|
<link rel="stylesheet" href="styles.css" />
|
||||||
|
<link rel="stylesheet" href="styles-modern.css" />
|
||||||
|
<link rel="stylesheet" href="styles-concreteinfo.css" />
|
||||||
|
<link rel="stylesheet" href="styles-sidebar.css" />
|
||||||
|
<link rel="stylesheet" href="styles-zen.css" />
|
||||||
|
<link rel="stylesheet" href="styles-welcome.css" />
|
||||||
|
<link rel="stylesheet" href="katex.min.css" />
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--fc-bg: #fafafa;
|
||||||
|
--fc-text: #1f2328;
|
||||||
|
--fc-border: #d0d7de;
|
||||||
|
--fc-accent: #e5461f;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', system-ui, sans-serif;
|
||||||
|
background: var(--fc-bg);
|
||||||
|
color: var(--fc-text);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.fc-header {
|
||||||
|
background: linear-gradient(135deg, #464646 0%, #0d0b09 100%);
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-bottom: 3px solid var(--fc-accent);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.fc-header h1 {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.fc-hint {
|
||||||
|
color: #9a9696;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
.fc-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid var(--fc-border);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.fc-btn {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
border: 1px solid var(--fc-border);
|
||||||
|
background: #fff;
|
||||||
|
color: var(--fc-text);
|
||||||
|
}
|
||||||
|
.fc-btn:hover {
|
||||||
|
border-color: var(--fc-accent);
|
||||||
|
}
|
||||||
|
.fc-btn.primary {
|
||||||
|
background: var(--fc-accent);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--fc-accent);
|
||||||
|
}
|
||||||
|
.fc-btn.primary:hover {
|
||||||
|
background: #c93a18;
|
||||||
|
}
|
||||||
|
.fc-status {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6e7681;
|
||||||
|
}
|
||||||
|
.fc-split {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.fc-canvas-host {
|
||||||
|
flex: 7;
|
||||||
|
min-width: 0;
|
||||||
|
position: relative;
|
||||||
|
background: #fafafa;
|
||||||
|
border-right: 1px solid var(--fc-border);
|
||||||
|
}
|
||||||
|
.fc-canvas-host svg.flowchart-canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
.fc-preview-host {
|
||||||
|
flex: 3;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
.fc-preview-source {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 120px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
font-family: var(--mono-font, 'JetBrains Mono', monospace);
|
||||||
|
font-size: 12px;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre;
|
||||||
|
border-bottom: 1px solid var(--fc-border);
|
||||||
|
background: #fff;
|
||||||
|
color: var(--fc-text);
|
||||||
|
}
|
||||||
|
.fc-preview-render {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 120px;
|
||||||
|
padding: 12px;
|
||||||
|
overflow: auto;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
/* v4.9.6 — forced light surfaces inside the standalone window.
|
||||||
|
* Diverges from body theme to guarantee readability, mirroring the
|
||||||
|
* v4.9.5 fix in src/styles-sidebar.css for the (now-legacy) sidebar. */
|
||||||
|
.fc-canvas-host,
|
||||||
|
.fc-preview-host {
|
||||||
|
background: #fafafa !important;
|
||||||
|
color: #1f2328 !important;
|
||||||
|
}
|
||||||
|
.flowchart-node rect,
|
||||||
|
.flowchart-node polygon {
|
||||||
|
fill: #ffffff !important;
|
||||||
|
stroke: #1f2328 !important;
|
||||||
|
stroke-width: 1.25;
|
||||||
|
}
|
||||||
|
.flowchart-node text {
|
||||||
|
fill: #1f2328 !important;
|
||||||
|
}
|
||||||
|
.flowchart-edge {
|
||||||
|
stroke: #1f2328 !important;
|
||||||
|
}
|
||||||
|
.flowchart-node.selected rect,
|
||||||
|
.flowchart-node.selected polygon {
|
||||||
|
fill: #e3f0ff !important;
|
||||||
|
stroke: var(--fc-accent) !important;
|
||||||
|
stroke-width: 3;
|
||||||
|
}
|
||||||
|
.flowchart-edge.selected {
|
||||||
|
stroke: var(--fc-accent) !important;
|
||||||
|
stroke-width: 3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="fc-header">
|
||||||
|
<h1>Flowchart Generator</h1>
|
||||||
|
<span class="fc-hint"
|
||||||
|
>Click empty canvas to add nodes · Alt+drag to connect · Double-click node to edit</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fc-toolbar">
|
||||||
|
<button
|
||||||
|
class="fc-btn primary"
|
||||||
|
id="fc-btn-insert"
|
||||||
|
title="Insert Mermaid block at cursor in main editor"
|
||||||
|
>
|
||||||
|
Insert at Cursor
|
||||||
|
</button>
|
||||||
|
<button class="fc-btn" id="fc-btn-reset" title="Clear all nodes and edges">Reset</button>
|
||||||
|
<span class="fc-status" id="fc-status" aria-live="polite"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fc-split">
|
||||||
|
<div class="fc-canvas-host" id="canvas-host"></div>
|
||||||
|
<div class="fc-preview-host" id="preview-host">
|
||||||
|
<pre class="fc-preview-source" id="preview-source"></pre>
|
||||||
|
<div class="fc-preview-render" id="preview-render"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
v4.9.6 — pure-module load order matters. shapes first, then canvas
|
||||||
|
(which depends on shapes), then store, then mermaid. Controller last
|
||||||
|
and consumes all four via window globals.
|
||||||
|
-->
|
||||||
|
<script src="flowchart/flowchart-shapes.js"></script>
|
||||||
|
<script src="flowchart/flowchart-mermaid.js"></script>
|
||||||
|
<script src="flowchart/flowchart-store.js"></script>
|
||||||
|
<script src="flowchart/flowchart-canvas.js"></script>
|
||||||
|
<script src="renderer/flowchart-controller.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -13,7 +13,12 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { DEFAULT_WIDTH, DEFAULT_HEIGHT, shapeSvg, SHAPE_KINDS } = require('./flowchart-shapes');
|
// v4.9.6 — CommonJS sibling import when running under Node (the renderer.js
|
||||||
|
// sidebar panel still uses require()), browser global fallback when loaded as
|
||||||
|
// a <script> tag in the standalone window (no nodeIntegration).
|
||||||
|
const shapesModule =
|
||||||
|
(typeof window !== 'undefined' && window.FlowchartShapes) || require('./flowchart-shapes');
|
||||||
|
const { DEFAULT_WIDTH, DEFAULT_HEIGHT, shapeSvg, SHAPE_KINDS } = shapesModule;
|
||||||
|
|
||||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||||
|
|
||||||
@@ -322,4 +327,16 @@ function createCanvas(container, store, opts = {}) {
|
|||||||
return { destroy, getSvg: () => svg };
|
return { destroy, getSvg: () => svg };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { createCanvas, SHAPE_KINDS };
|
// v4.9.6 UMD wrapper — same CommonJS export shape + browser global
|
||||||
|
// (window.FlowchartCanvas) so the standalone window's controller can load
|
||||||
|
// this module via <script> tag without nodeIntegration.
|
||||||
|
(function (root, factory) {
|
||||||
|
const exported = factory();
|
||||||
|
if (typeof module === 'object' && module.exports) {
|
||||||
|
module.exports = exported;
|
||||||
|
} else {
|
||||||
|
root.FlowchartCanvas = exported;
|
||||||
|
}
|
||||||
|
})(typeof window !== 'undefined' ? window : globalThis, function () {
|
||||||
|
return { createCanvas, SHAPE_KINDS };
|
||||||
|
});
|
||||||
|
|||||||
@@ -90,4 +90,16 @@ function toMermaid(graph) {
|
|||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { toMermaid, escapeLabel, nodeDeclaration, edgeDeclaration };
|
// v4.9.6 UMD wrapper — same CommonJS export shape + browser global
|
||||||
|
// (window.FlowchartMermaid) so the standalone window's controller can load
|
||||||
|
// this module via <script> tag without nodeIntegration.
|
||||||
|
(function (root, factory) {
|
||||||
|
const exported = factory();
|
||||||
|
if (typeof module === 'object' && module.exports) {
|
||||||
|
module.exports = exported;
|
||||||
|
} else {
|
||||||
|
root.FlowchartMermaid = exported;
|
||||||
|
}
|
||||||
|
})(typeof window !== 'undefined' ? window : globalThis, function () {
|
||||||
|
return { toMermaid, escapeLabel, nodeDeclaration, edgeDeclaration };
|
||||||
|
});
|
||||||
|
|||||||
@@ -55,7 +55,22 @@ function shapeSvg(kind, x, y, width, height) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
// v4.9.6 UMD wrapper — exposes the same surface as a CommonJS module
|
||||||
|
// (used by src/sidebar/flowchart-panel.js via require()) AND as a browser
|
||||||
|
// global (used by src/renderer/flowchart-controller.js via <script> tag).
|
||||||
|
// The standalone BrowserWindow runs with contextIsolation:true +
|
||||||
|
// nodeIntegration:false, so the renderer cannot require() these modules;
|
||||||
|
// loading them as <script> tags in src/flowchart-generator.html attaches
|
||||||
|
// them to window.FlowchartShapes.
|
||||||
|
(function (root, factory) {
|
||||||
|
const exported = factory();
|
||||||
|
if (typeof module === 'object' && module.exports) {
|
||||||
|
module.exports = exported;
|
||||||
|
} else {
|
||||||
|
root.FlowchartShapes = exported;
|
||||||
|
}
|
||||||
|
})(typeof window !== 'undefined' ? window : globalThis, function () {
|
||||||
|
return {
|
||||||
shapeSvg,
|
shapeSvg,
|
||||||
SHAPE_KINDS,
|
SHAPE_KINDS,
|
||||||
DEFAULT_WIDTH,
|
DEFAULT_WIDTH,
|
||||||
@@ -63,3 +78,4 @@ module.exports = {
|
|||||||
LABEL_PADDING_X,
|
LABEL_PADDING_X,
|
||||||
LABEL_PADDING_Y,
|
LABEL_PADDING_Y,
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
|||||||
@@ -258,4 +258,16 @@ function create(io) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { create, NODE_KINDS, EDGE_KINDS };
|
// v4.9.6 UMD wrapper — same CommonJS export shape + browser global
|
||||||
|
// (window.FlowchartStore) so the standalone window's controller can load
|
||||||
|
// this module via <script> tag without nodeIntegration.
|
||||||
|
(function (root, factory) {
|
||||||
|
const exported = factory();
|
||||||
|
if (typeof module === 'object' && module.exports) {
|
||||||
|
module.exports = exported;
|
||||||
|
} else {
|
||||||
|
root.FlowchartStore = exported;
|
||||||
|
}
|
||||||
|
})(typeof window !== 'undefined' ? window : globalThis, function () {
|
||||||
|
return { create, NODE_KINDS, EDGE_KINDS };
|
||||||
|
});
|
||||||
|
|||||||
+45
@@ -1433,6 +1433,12 @@ function createMenu() {
|
|||||||
accelerator: 'CmdOrCtrl+Shift+A',
|
accelerator: 'CmdOrCtrl+Shift+A',
|
||||||
click: () => openAsciiGenerator(),
|
click: () => openAsciiGenerator(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// v4.9.6 — replaces the sidebar Flow Chart panel.
|
||||||
|
label: 'Flowchart Generator',
|
||||||
|
accelerator: 'CmdOrCtrl+Alt+F',
|
||||||
|
click: () => openFlowchartGenerator(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'separator',
|
type: 'separator',
|
||||||
},
|
},
|
||||||
@@ -5485,6 +5491,45 @@ ipcMain.on('select-image-folder', (event, inputId) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Flowchart Generator Window (v4.9.6)
|
||||||
|
// ============================================
|
||||||
|
// Standalone BrowserWindow hosting the flowchart editor. The previous
|
||||||
|
// sidebar-panel implementation (src/sidebar/flowchart-panel.js, Ctrl+Alt+F
|
||||||
|
// through `Toggle Sidebar: Flow Chart`) has been retained as legacy but is
|
||||||
|
// no longer registered with sidebarManager — see src/renderer.js where the
|
||||||
|
// registerPanel('flowchart', …) call is now commented out. The standalone
|
||||||
|
// window mirrors the ASCII Art Generator pattern (src/ascii-generator.html
|
||||||
|
// + src/renderer/ascii-controller.js).
|
||||||
|
let flowchartGeneratorWindow = null;
|
||||||
|
function openFlowchartGenerator() {
|
||||||
|
if (flowchartGeneratorWindow) {
|
||||||
|
flowchartGeneratorWindow.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
flowchartGeneratorWindow = new BrowserWindow({
|
||||||
|
width: 1100,
|
||||||
|
height: 720,
|
||||||
|
parent: mainWindow,
|
||||||
|
modal: false,
|
||||||
|
title: 'Flowchart Generator',
|
||||||
|
icon: path.join(__dirname, '../assets/icon.png'),
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
flowchartGeneratorWindow.loadFile(path.join(__dirname, 'flowchart-generator.html'));
|
||||||
|
flowchartGeneratorWindow.setMenuBarVisibility(false);
|
||||||
|
flowchartGeneratorWindow.on('closed', () => {
|
||||||
|
flowchartGeneratorWindow = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ipcMain.on('open-flowchart-generator', () => {
|
||||||
|
openFlowchartGenerator();
|
||||||
|
});
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// ASCII Art Generator Window
|
// ASCII Art Generator Window
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -98,6 +98,9 @@ const ALLOWED_SEND_CHANNELS = [
|
|||||||
// ASCII generator (separate window)
|
// ASCII generator (separate window)
|
||||||
'open-ascii-generator',
|
'open-ascii-generator',
|
||||||
|
|
||||||
|
// Flowchart generator (standalone window — v4.9.6)
|
||||||
|
'open-flowchart-generator',
|
||||||
|
|
||||||
// ASCII art generator (standalone window — invoke channels)
|
// ASCII art generator (standalone window — invoke channels)
|
||||||
'ascii:generate',
|
'ascii:generate',
|
||||||
'ascii:list-fonts',
|
'ascii:list-fonts',
|
||||||
@@ -513,6 +516,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
// Generator Windows
|
// Generator Windows
|
||||||
generators: {
|
generators: {
|
||||||
openAscii: () => ipcRenderer.send('open-ascii-generator'),
|
openAscii: () => ipcRenderer.send('open-ascii-generator'),
|
||||||
|
openFlowchart: () => ipcRenderer.send('open-flowchart-generator'),
|
||||||
openTable: () => ipcRenderer.send('open-table-generator'),
|
openTable: () => ipcRenderer.send('open-table-generator'),
|
||||||
ascii: {
|
ascii: {
|
||||||
listFonts: () => ipcRenderer.invoke('ascii:list-fonts'),
|
listFonts: () => ipcRenderer.invoke('ascii:list-fonts'),
|
||||||
@@ -524,6 +528,18 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// v4.9.6 — Flowchart Generator standalone window's IO bridge. Reuses the
|
||||||
|
// existing thin text-file IPC handlers (get-user-data-path, read-text-file,
|
||||||
|
// write-text-file) which sandbox writes to <userData>. Insert at cursor
|
||||||
|
// sends the Mermaid-fenced source through the existing 'insert-content'
|
||||||
|
// channel which the renderer.js sidebar panel also uses.
|
||||||
|
flowchart: {
|
||||||
|
getUserDataPath: () => ipcRenderer.invoke('get-user-data-path'),
|
||||||
|
readFile: (p) => ipcRenderer.invoke('read-text-file', p),
|
||||||
|
writeFile: (p, content) => ipcRenderer.invoke('write-text-file', { path: p, content }),
|
||||||
|
insertAtCursor: (text) => ipcRenderer.send('insert-content', text),
|
||||||
|
},
|
||||||
|
|
||||||
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+25
-7
@@ -154,13 +154,10 @@ if (typeof window !== 'undefined' && !window.electronAPI) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lazy-loaded: filesystem helpers used by the flowchart panel for
|
// v4.9.6 — flowchart panel was removed (now a standalone window); the
|
||||||
// <userData>/flowchart-session.json auto-save.
|
// thin IPC helpers it used are now defined inside the standalone window's
|
||||||
const flowchartIO = {
|
// controller (src/renderer/flowchart-controller.js) under
|
||||||
getUserDataPath: () => ipcRenderer.invoke('get-user-data-path'),
|
// window.electronAPI.flowchart.*, exposed via src/preload.js.
|
||||||
readFile: (p) => ipcRenderer.invoke('read-text-file', p),
|
|
||||||
writeFile: (p, content) => ipcRenderer.invoke('write-text-file', { path: p, content }),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use window.ModalManager if already set by script tag, otherwise require it.
|
// Use window.ModalManager if already set by script tag, otherwise require it.
|
||||||
// This prevents "Identifier 'ModalManager' has already been declared" when
|
// This prevents "Identifier 'ModalManager' has already been declared" when
|
||||||
@@ -2414,6 +2411,21 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// v4.9.6 — Flow Chart Editor now lives in a standalone BrowserWindow
|
||||||
|
// (src/flowchart-generator.html + src/renderer/flowchart-controller.js),
|
||||||
|
// launched from Tools → Flowchart Generator (Cmd/Ctrl+Alt+F). The sidebar
|
||||||
|
// panel implementation below is preserved as legacy and the unit tests
|
||||||
|
// (tests/flowchart-panel.test.js, tests/flowchart-canvas.test.js, etc.)
|
||||||
|
// remain green. The standalone-window controller reuses the same pure
|
||||||
|
// modules — see the UMD wrappers at the bottom of flowchart-shapes.js,
|
||||||
|
// flowchart-mermaid.js, flowchart-store.js, and flowchart-canvas.js.
|
||||||
|
//
|
||||||
|
// The `flowchart` sidebar panel is no longer registered with
|
||||||
|
// sidebarManager; the corresponding command-palette entry is also
|
||||||
|
// commented out below. To restore the sidebar experience, uncomment the
|
||||||
|
// block below AND the matching commandPalette.register('Toggle Sidebar:
|
||||||
|
// Flow Chart', …) call.
|
||||||
|
/*
|
||||||
// Flow Chart Editor panel — visual Mermaid flowchart builder.
|
// Flow Chart Editor panel — visual Mermaid flowchart builder.
|
||||||
// Persists to <userData>/flowchart-session.json via injected IPC helpers.
|
// Persists to <userData>/flowchart-session.json via injected IPC helpers.
|
||||||
// Reuses tabManager.insertAtCursor for the "Insert at Cursor" button.
|
// Reuses tabManager.insertAtCursor for the "Insert at Cursor" button.
|
||||||
@@ -2474,6 +2486,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
// Wiki-link navigation: open the target note (creating it on demand after
|
// Wiki-link navigation: open the target note (creating it on demand after
|
||||||
// confirmation). Delegated on document so it works in every tab's preview.
|
// confirmation). Delegated on document so it works in every tab's preview.
|
||||||
@@ -2874,9 +2887,14 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
commandPalette.register('Toggle Sidebar: Outline', 'Ctrl+Shift+O', () =>
|
commandPalette.register('Toggle Sidebar: Outline', 'Ctrl+Shift+O', () =>
|
||||||
sidebarManager.togglePanel('outline')
|
sidebarManager.togglePanel('outline')
|
||||||
);
|
);
|
||||||
|
// v4.9.6 — sidebar Flow Chart panel is no longer registered (see comment
|
||||||
|
// block above); the standalone window owns Cmd/Ctrl+Alt+F via the Tools
|
||||||
|
// menu. Kept the legacy entry commented out for rollback.
|
||||||
|
/*
|
||||||
commandPalette.register('Toggle Sidebar: Flow Chart', 'Ctrl+Alt+F', () =>
|
commandPalette.register('Toggle Sidebar: Flow Chart', 'Ctrl+Alt+F', () =>
|
||||||
sidebarManager.togglePanel('flowchart')
|
sidebarManager.togglePanel('flowchart')
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
commandPalette.register('Print Preview', 'Ctrl+P', () => {
|
commandPalette.register('Print Preview', 'Ctrl+P', () => {
|
||||||
const tab = tabManager.tabs.get(tabManager.activeTabId);
|
const tab = tabManager.tabs.get(tabManager.activeTabId);
|
||||||
const preview = document.getElementById(`preview-${tab.id}`);
|
const preview = document.getElementById(`preview-${tab.id}`);
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
/**
|
||||||
|
* Flowchart Generator — renderer controller for the standalone window.
|
||||||
|
*
|
||||||
|
* Drives the DOM in src/flowchart-generator.html. Boots the graph store,
|
||||||
|
* hydrates from <userData>/flowchart-session.json (reusing the existing
|
||||||
|
* thin text-file IPC handlers), persists on every mutation with a
|
||||||
|
* 500 ms debounce, and wires "Insert at Cursor" to the existing
|
||||||
|
* `insert-content` IPC so the Mermaid source lands in the main editor
|
||||||
|
* wrapped in a fenced code block.
|
||||||
|
*
|
||||||
|
* Pure browser script — no require(), no node. Pure modules
|
||||||
|
* (flowchart-store / flowchart-canvas / flowchart-mermaid / flowchart-shapes)
|
||||||
|
* are loaded via <script> tags in src/flowchart-generator.html and exposed
|
||||||
|
* as window globals (window.FlowchartStore, window.FlowchartCanvas,
|
||||||
|
* window.FlowchartMermaid, window.FlowchartShapes).
|
||||||
|
*
|
||||||
|
* @module flowchart-controller
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const api =
|
||||||
|
window.electronAPI && window.electronAPI.flowchart ? window.electronAPI.flowchart : null;
|
||||||
|
|
||||||
|
const els = {
|
||||||
|
canvasHost: document.getElementById('canvas-host'),
|
||||||
|
previewSource: document.getElementById('preview-source'),
|
||||||
|
previewRender: document.getElementById('preview-render'),
|
||||||
|
btnInsert: document.getElementById('fc-btn-insert'),
|
||||||
|
btnReset: document.getElementById('fc-btn-reset'),
|
||||||
|
status: document.getElementById('fc-status'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Guard rails — these should never be null in a correctly-launched window.
|
||||||
|
// Fail loudly with a visible status message rather than silently no-op'ing
|
||||||
|
// if the HTML or the preload bridge are misconfigured.
|
||||||
|
function fatal(msg) {
|
||||||
|
if (els.status) els.status.textContent = msg;
|
||||||
|
console.error('[flowchart-controller]', msg);
|
||||||
|
}
|
||||||
|
if (!els.canvasHost || !els.previewSource || !els.previewRender) {
|
||||||
|
fatal('Required DOM elements missing — check src/flowchart-generator.html');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!window.FlowchartStore ||
|
||||||
|
!window.FlowchartCanvas ||
|
||||||
|
!window.FlowchartMermaid ||
|
||||||
|
!window.FlowchartShapes
|
||||||
|
) {
|
||||||
|
fatal('Flowchart pure modules not loaded — verify script tags in src/flowchart-generator.html');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!api) {
|
||||||
|
fatal('window.electronAPI.flowchart missing — check src/preload.js');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { create: createStore } = window.FlowchartStore;
|
||||||
|
const { createCanvas } = window.FlowchartCanvas;
|
||||||
|
const { toMermaid } = window.FlowchartMermaid;
|
||||||
|
// FlowchartShapes is intentionally unused here but its presence is
|
||||||
|
// required for createCanvas() to function; the guard above guarantees it.
|
||||||
|
|
||||||
|
const PREVIEW_DEBOUNCE_MS = 250;
|
||||||
|
const PERSIST_DEBOUNCE_MS = 500;
|
||||||
|
const PERSISTENCE_FILENAME = 'flowchart-session.json';
|
||||||
|
|
||||||
|
let _userDataPath = null;
|
||||||
|
let _persistenceFile = null;
|
||||||
|
let _previewTimer = null;
|
||||||
|
let _persistTimer = null;
|
||||||
|
let _store = null;
|
||||||
|
let _canvas = null;
|
||||||
|
|
||||||
|
function setStatus(msg) {
|
||||||
|
if (els.status) els.status.textContent = msg || '';
|
||||||
|
if (msg) setTimeout(() => setStatus(''), 2500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function runPreview() {
|
||||||
|
if (!_store || !els.previewSource || !els.previewRender) return;
|
||||||
|
const graph = _store.getGraph();
|
||||||
|
const source = toMermaid(graph);
|
||||||
|
els.previewSource.textContent = source;
|
||||||
|
// v4.9.6 — text preview only. The SVG canvas on the left is the
|
||||||
|
// visual preview (hand-rolled, no Mermaid runtime needed in this
|
||||||
|
// window's renderer). Mirrors the v4.9.5 fix that forced a light
|
||||||
|
// surface to guarantee visibility.
|
||||||
|
els.previewRender.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function debouncedPreview() {
|
||||||
|
if (_previewTimer) clearTimeout(_previewTimer);
|
||||||
|
_previewTimer = setTimeout(() => {
|
||||||
|
_previewTimer = null;
|
||||||
|
runPreview();
|
||||||
|
}, PREVIEW_DEBOUNCE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function debouncedPersist() {
|
||||||
|
if (_persistTimer) clearTimeout(_persistTimer);
|
||||||
|
_persistTimer = setTimeout(() => {
|
||||||
|
_persistTimer = null;
|
||||||
|
if (!_store || !_persistenceFile) return;
|
||||||
|
api
|
||||||
|
.writeFile(_persistenceFile, _store.serialize())
|
||||||
|
.then(() => setStatus('Saved'))
|
||||||
|
.catch((err) => setStatus(`Save failed: ${err && err.message ? err.message : err}`));
|
||||||
|
}, PERSIST_DEBOUNCE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
// Resolve the userData path ONCE on mount. The persistence path is
|
||||||
|
// interpolated into a string on every read/write; calling the async
|
||||||
|
// api.getUserDataPath() directly would coerce the Promise to
|
||||||
|
// "[object Promise]" and break the userData sandbox check in
|
||||||
|
// main.js:write-text-file.
|
||||||
|
try {
|
||||||
|
_userDataPath = await api.getUserDataPath();
|
||||||
|
} catch (err) {
|
||||||
|
fatal(`Could not resolve userData path: ${err && err.message ? err.message : err}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_persistenceFile = `${_userDataPath}/${PERSISTENCE_FILENAME}`;
|
||||||
|
|
||||||
|
_store = createStore({
|
||||||
|
persistencePath: _persistenceFile,
|
||||||
|
readFile: api.readFile,
|
||||||
|
writeFile: api.writeFile,
|
||||||
|
now: () => Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
_canvas = createCanvas(els.canvasHost, _store, {
|
||||||
|
onEdgeClick: (edgeId) => {
|
||||||
|
const edge = _store.getGraph().edges.find((e) => e.id === edgeId);
|
||||||
|
if (!edge) return;
|
||||||
|
const nextKind = window.prompt('Edge kind (solid, dotted, thick):', edge.kind);
|
||||||
|
if (nextKind && ['solid', 'dotted', 'thick'].includes(nextKind)) {
|
||||||
|
_store.setEdgeKind(edgeId, nextKind);
|
||||||
|
}
|
||||||
|
const nextLabel = window.prompt('Edge label (empty to clear):', edge.label || '');
|
||||||
|
if (nextLabel !== null) {
|
||||||
|
_store.setEdgeLabel(edgeId, nextLabel);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNodeClick: () => {
|
||||||
|
// Canvas already paints the .selected highlight; nothing else
|
||||||
|
// needed here for selection state.
|
||||||
|
},
|
||||||
|
onShapeMenu: (nodeId) => {
|
||||||
|
const next = window.prompt(
|
||||||
|
'New shape (process, decision, terminator, subroutine, document):'
|
||||||
|
);
|
||||||
|
if (next) _store.setNodeKind(nodeId, next);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
_store.subscribe(() => {
|
||||||
|
debouncedPreview();
|
||||||
|
debouncedPersist();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hydrate from disk (defensively — corrupt JSON is caught by the store).
|
||||||
|
try {
|
||||||
|
const json = await api.readFile(_persistenceFile);
|
||||||
|
if (json) _store.deserialize(json);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[flowchart-controller] failed to read session:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (els.btnInsert) {
|
||||||
|
els.btnInsert.addEventListener('click', () => {
|
||||||
|
if (!_store) return;
|
||||||
|
const source = toMermaid(_store.getGraph());
|
||||||
|
const fenced = '```mermaid\n' + source + '\n```';
|
||||||
|
if (api.insertAtCursor) api.insertAtCursor(fenced);
|
||||||
|
setStatus('Inserted');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (els.btnReset) {
|
||||||
|
els.btnReset.addEventListener('click', () => {
|
||||||
|
if (!_store) return;
|
||||||
|
if (!window.confirm('Clear all nodes and edges?')) return;
|
||||||
|
_store.deserialize({ nodes: [], edges: [] });
|
||||||
|
setStatus('Reset');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard shortcuts — Ctrl/Cmd+Z / Ctrl/Cmd+Shift+Z / Delete / Backspace.
|
||||||
|
document.addEventListener('keydown', (ev) => {
|
||||||
|
if (!_store) return;
|
||||||
|
const meta = ev.ctrlKey || ev.metaKey;
|
||||||
|
if (meta && !ev.shiftKey && ev.key.toLowerCase() === 'z') {
|
||||||
|
ev.preventDefault();
|
||||||
|
_store.undo();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (meta && ev.shiftKey && ev.key.toLowerCase() === 'z') {
|
||||||
|
ev.preventDefault();
|
||||||
|
_store.redo();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ev.key === 'Delete' || ev.key === 'Backspace') {
|
||||||
|
// Delete applies to the canvas's currently-selected node/edge.
|
||||||
|
// The canvas doesn't expose selection state directly; we read
|
||||||
|
// it from the DOM .selected class.
|
||||||
|
const selectedNode = els.canvasHost.querySelector('.flowchart-node.selected');
|
||||||
|
if (selectedNode) {
|
||||||
|
const id = selectedNode.getAttribute('data-node-id');
|
||||||
|
if (id) {
|
||||||
|
ev.preventDefault();
|
||||||
|
_store.removeNode(id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selectedEdge = els.canvasHost.querySelector('.flowchart-edge.selected');
|
||||||
|
if (selectedEdge) {
|
||||||
|
const id = selectedEdge.getAttribute('data-edge-id');
|
||||||
|
if (id) {
|
||||||
|
ev.preventDefault();
|
||||||
|
_store.disconnect(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
runPreview();
|
||||||
|
setStatus('Ready');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose a minimal handle for tests (mirrors ascii-controller.js pattern).
|
||||||
|
window.FlowchartController = {
|
||||||
|
bootstrap,
|
||||||
|
get store() {
|
||||||
|
return _store;
|
||||||
|
},
|
||||||
|
get canvas() {
|
||||||
|
return _canvas;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', bootstrap);
|
||||||
|
} else {
|
||||||
|
bootstrap();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
/**
|
||||||
|
* @jest-environment jsdom
|
||||||
|
*
|
||||||
|
* Tests the standalone Flowchart Generator window's renderer controller
|
||||||
|
* (src/renderer/flowchart-controller.js). Loads the real HTML and the
|
||||||
|
* pure-module + controller scripts into jsdom, mocks
|
||||||
|
* window.electronAPI.flowchart, and asserts:
|
||||||
|
* - The stylesheet links are src/-relative (no `..` escape).
|
||||||
|
* - bootstrap() resolves getUserDataPath exactly once and reads
|
||||||
|
* <userData>/flowchart-session.json.
|
||||||
|
* - "Insert at Cursor" wraps the Mermaid source in a fenced code block.
|
||||||
|
* - Reset clears nodes/edges and persists the empty graph.
|
||||||
|
* - The sidebar Flow Chart panel registration is commented out of
|
||||||
|
* src/renderer.js (regression: ensure the legacy sidebar doesn't
|
||||||
|
* re-appear).
|
||||||
|
*/
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const HTML_PATH = path.join(__dirname, '..', 'src', 'flowchart-generator.html');
|
||||||
|
const CONTROLLER_PATH = path.join(__dirname, '..', 'src', 'renderer', 'flowchart-controller.js');
|
||||||
|
const RENDERER_PATH = path.join(__dirname, '..', 'src', 'renderer.js');
|
||||||
|
|
||||||
|
async function mount(apiMockOverrides = {}) {
|
||||||
|
const html = fs.readFileSync(HTML_PATH, 'utf-8');
|
||||||
|
// Strip <head>/<body> wrappers — jsdom already provides those.
|
||||||
|
const bodyMatch = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
||||||
|
document.body.innerHTML = bodyMatch ? bodyMatch[1] : html;
|
||||||
|
|
||||||
|
const apiMock = {
|
||||||
|
getUserDataPath: jest.fn(async () => '/userdata'),
|
||||||
|
readFile: jest.fn(async () => null),
|
||||||
|
writeFile: jest.fn(async () => undefined),
|
||||||
|
insertAtCursor: jest.fn(),
|
||||||
|
...apiMockOverrides,
|
||||||
|
};
|
||||||
|
window.electronAPI = { flowchart: apiMock };
|
||||||
|
|
||||||
|
// Load the pure-module UMD scripts so the controller sees them as
|
||||||
|
// window globals. Each is a self-contained IIFE; running them in the
|
||||||
|
// current realm is enough — they don't need the DOM. The content of each
|
||||||
|
// file is hardcoded under src/ (no user input) — same pattern used by
|
||||||
|
// tests/ascii-controller.test.js.
|
||||||
|
const scriptPaths = [
|
||||||
|
'flowchart/flowchart-shapes.js',
|
||||||
|
'flowchart/flowchart-mermaid.js',
|
||||||
|
'flowchart/flowchart-store.js',
|
||||||
|
'flowchart/flowchart-canvas.js',
|
||||||
|
];
|
||||||
|
for (const rel of scriptPaths) {
|
||||||
|
const src = fs.readFileSync(path.join(__dirname, '..', 'src', rel), 'utf-8');
|
||||||
|
// eslint-disable-next-line no-new-func
|
||||||
|
new Function('window', 'document', src)(window, document);
|
||||||
|
}
|
||||||
|
// Then run the controller itself. The controller's IIFE auto-calls
|
||||||
|
// bootstrap() at the end because document.readyState is 'complete' in
|
||||||
|
// jsdom (not 'loading'). Wait for the bootstrap chain — which awaits
|
||||||
|
// api.getUserDataPath() and api.readFile() — to settle, so callers see a
|
||||||
|
// fully-initialized store and don't need to call bootstrap() themselves.
|
||||||
|
const ctrlSrc = fs.readFileSync(CONTROLLER_PATH, 'utf-8');
|
||||||
|
// eslint-disable-next-line no-new-func
|
||||||
|
new Function('window', 'document', ctrlSrc)(window, document);
|
||||||
|
// Flush microtasks until the bootstrap's awaits resolve.
|
||||||
|
for (let i = 0; i < 5; i += 1) {
|
||||||
|
await Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { apiMock };
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
jest.useRealTimers();
|
||||||
|
delete window.electronAPI;
|
||||||
|
delete window.FlowchartController;
|
||||||
|
delete window.FlowchartShapes;
|
||||||
|
delete window.FlowchartMermaid;
|
||||||
|
delete window.FlowchartStore;
|
||||||
|
delete window.FlowchartCanvas;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('flowchart-generator.html — stylesheet links', () => {
|
||||||
|
test('all stylesheet hrefs are src/-relative (no "../" escape)', () => {
|
||||||
|
const html = fs.readFileSync(HTML_PATH, 'utf-8');
|
||||||
|
const linkRe = /<link[^>]*rel=["']stylesheet["'][^>]*href=["']([^"']+)["'][^>]*>/gi;
|
||||||
|
const hrefs = [];
|
||||||
|
let match;
|
||||||
|
while ((match = linkRe.exec(html)) !== null) hrefs.push(match[1]);
|
||||||
|
expect(hrefs.length).toBeGreaterThan(0);
|
||||||
|
for (const href of hrefs) {
|
||||||
|
expect(href.startsWith('../')).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('links fonts.css as the first stylesheet', () => {
|
||||||
|
const html = fs.readFileSync(HTML_PATH, 'utf-8');
|
||||||
|
expect(html).toMatch(/<link[^>]*href=["']fonts\.css["']/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('flowchart-controller: bootstrap', () => {
|
||||||
|
test('resolves getUserDataPath exactly once on mount', async () => {
|
||||||
|
const { apiMock } = await mount();
|
||||||
|
expect(apiMock.getUserDataPath).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reads <userData>/flowchart-session.json on mount', async () => {
|
||||||
|
const { apiMock } = await mount();
|
||||||
|
expect(apiMock.readFile).toHaveBeenCalledWith('/userdata/flowchart-session.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hydrates the store from a saved session', async () => {
|
||||||
|
const saved = JSON.stringify({
|
||||||
|
nodes: [{ id: 'n1', kind: 'process', x: 10, y: 10, label: 'Loaded' }],
|
||||||
|
edges: [],
|
||||||
|
});
|
||||||
|
await mount({ readFile: jest.fn(async () => saved) });
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
const store = window.FlowchartController.store;
|
||||||
|
expect(store.getGraph().nodes).toHaveLength(1);
|
||||||
|
expect(store.getGraph().nodes[0].label).toBe('Loaded');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('flowchart-controller: insert at cursor', () => {
|
||||||
|
test('wraps the Mermaid source in a fenced code block', async () => {
|
||||||
|
const { apiMock } = await mount();
|
||||||
|
const store = window.FlowchartController.store;
|
||||||
|
store.addNode({ kind: 'process', x: 0, y: 0, label: 'Start' });
|
||||||
|
store.addNode({ kind: 'process', x: 200, y: 0, label: 'End' });
|
||||||
|
store.connect(store.getGraph().nodes[0].id, store.getGraph().nodes[1].id, 'solid');
|
||||||
|
|
||||||
|
document.getElementById('fc-btn-insert').click();
|
||||||
|
|
||||||
|
expect(apiMock.insertAtCursor).toHaveBeenCalledTimes(1);
|
||||||
|
const fenced = apiMock.insertAtCursor.mock.calls[0][0];
|
||||||
|
expect(fenced.startsWith('```mermaid\n')).toBe(true);
|
||||||
|
expect(fenced.endsWith('\n```')).toBe(true);
|
||||||
|
expect(fenced).toContain('flowchart TD');
|
||||||
|
expect(fenced).toContain('A[Start]');
|
||||||
|
expect(fenced).toContain('B[End]');
|
||||||
|
expect(fenced).toContain('A --> B');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('flowchart-controller: reset', () => {
|
||||||
|
test('clears all nodes and edges after confirm', async () => {
|
||||||
|
const { apiMock } = await mount();
|
||||||
|
const store = window.FlowchartController.store;
|
||||||
|
store.addNode({ kind: 'process', x: 0, y: 0, label: 'One' });
|
||||||
|
jest.advanceTimersByTime(600);
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(apiMock.writeFile).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Mock confirm() to return true and click Reset.
|
||||||
|
window.confirm = jest.fn(() => true);
|
||||||
|
document.getElementById('fc-btn-reset').click();
|
||||||
|
|
||||||
|
expect(store.getGraph().nodes).toHaveLength(0);
|
||||||
|
expect(store.getGraph().edges).toHaveLength(0);
|
||||||
|
|
||||||
|
// Reset mutation should trigger another persisted write within 500 ms.
|
||||||
|
apiMock.writeFile.mockClear();
|
||||||
|
jest.advanceTimersByTime(600);
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(apiMock.writeFile).toHaveBeenCalled();
|
||||||
|
const lastCall = apiMock.writeFile.mock.calls[apiMock.writeFile.mock.calls.length - 1];
|
||||||
|
expect(lastCall[0]).toBe('/userdata/flowchart-session.json');
|
||||||
|
expect(JSON.parse(lastCall[1]).nodes).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does nothing when the user cancels the confirm', async () => {
|
||||||
|
await mount();
|
||||||
|
const store = window.FlowchartController.store;
|
||||||
|
store.addNode({ kind: 'process', x: 0, y: 0, label: 'Keep me' });
|
||||||
|
window.confirm = jest.fn(() => false);
|
||||||
|
|
||||||
|
document.getElementById('fc-btn-reset').click();
|
||||||
|
|
||||||
|
expect(store.getGraph().nodes).toHaveLength(1);
|
||||||
|
expect(store.getGraph().nodes[0].label).toBe('Keep me');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('flowchart-controller: renderer.js sidebar registration is disabled', () => {
|
||||||
|
test('src/renderer.js no longer registers the flowchart sidebar panel', () => {
|
||||||
|
const src = fs.readFileSync(RENDERER_PATH, 'utf-8');
|
||||||
|
// The active code path should NOT call sidebarManager.registerPanel('flowchart', ...).
|
||||||
|
// Strip /* ... */ block comments (legacy sidebar code is preserved as a
|
||||||
|
// rollback comment block) AND // line comments so the regex doesn't match
|
||||||
|
// the comment block describing the legacy registration.
|
||||||
|
const blockStripped = src.replace(/\/\*[\s\S]*?\*\//g, '');
|
||||||
|
const stripped = blockStripped
|
||||||
|
.split('\n')
|
||||||
|
.filter((line) => !/^\s*\*/.test(line) && !/^\s*\/\//.test(line))
|
||||||
|
.join('\n');
|
||||||
|
expect(stripped).not.toMatch(/sidebarManager\.registerPanel\(\s*['"]flowchart['"]/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the command-palette entry for "Toggle Sidebar: Flow Chart" is disabled', () => {
|
||||||
|
const src = fs.readFileSync(RENDERER_PATH, 'utf-8');
|
||||||
|
// Strip /* ... */ blocks (the legacy registration is preserved inside
|
||||||
|
// such a block for rollback); only the live, uncommented registration
|
||||||
|
// would match.
|
||||||
|
const blockStripped = src.replace(/\/\*[\s\S]*?\*\//g, '');
|
||||||
|
const liveMatch = blockStripped.match(
|
||||||
|
/^commandPalette\.register\(\s*['"]Toggle Sidebar: Flow Chart['"]/m
|
||||||
|
);
|
||||||
|
expect(liveMatch).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user