mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 09:19:34 +05:30
feat(sidebar): register flowchart panel + rail button + thin fs IPC
- Add data-panel=flowchart rail button in src/index.html after daily-notes
- Append .flowchart-* panel CSS selectors to src/styles-sidebar.css
- Add flowchartIO IPC bridge and registerPanel('flowchart') in src/renderer.js
- Add get-user-data-path / read-text-file / write-text-file IPC in src/main.js
- Path validation: sandbox inside app.getPath('userData')
- read-text-file returns null on ENOENT (file doesn't exist)
Amit Haridas
This commit is contained in:
@@ -2483,6 +2483,32 @@
|
|||||||
<line x1="3" y1="10" x2="21" y2="10" />
|
<line x1="3" y1="10" x2="21" y2="10" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- Flow Chart Editor: visual Mermaid flowchart builder -->
|
||||||
|
<button
|
||||||
|
class="sidebar-icon"
|
||||||
|
data-panel="flowchart"
|
||||||
|
title="Flow Chart Editor (Ctrl+Alt+F)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect x="3" y="3" width="6" height="4" rx="1" />
|
||||||
|
<rect x="15" y="3" width="6" height="4" rx="1" />
|
||||||
|
<polygon points="9,17 12,13 15,17" />
|
||||||
|
<line x1="6" y1="7" x2="6" y2="11" />
|
||||||
|
<line x1="18" y1="7" x2="18" y2="11" />
|
||||||
|
<line x1="6" y1="11" x2="9" y2="15" />
|
||||||
|
<line x1="18" y1="11" x2="15" y2="15" />
|
||||||
|
<line x1="12" y1="17" x2="12" y2="21" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-panel" id="sidebar-panel">
|
<div class="sidebar-panel" id="sidebar-panel">
|
||||||
<div class="sidebar-panel-header">
|
<div class="sidebar-panel-header">
|
||||||
|
|||||||
+23
@@ -6111,6 +6111,29 @@ ipcMain.handle('read-file', async (event, filePath) => {
|
|||||||
}
|
}
|
||||||
return fs.readFileSync(validation.resolved, 'utf-8');
|
return fs.readFileSync(validation.resolved, 'utf-8');
|
||||||
});
|
});
|
||||||
|
ipcMain.handle('get-user-data-path', () => app.getPath('userData'));
|
||||||
|
|
||||||
|
ipcMain.handle('read-text-file', async (_event, filePath) => {
|
||||||
|
// Reuse the same path validation as the existing read-file handler.
|
||||||
|
const safe = path.resolve(filePath);
|
||||||
|
if (!safe.startsWith(path.resolve(app.getPath('userData')))) {
|
||||||
|
throw new Error('read-text-file: path outside userData is not allowed');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await fs.promises.readFile(safe, 'utf-8');
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'ENOENT') return null;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('write-text-file', async (_event, { path: filePath, content }) => {
|
||||||
|
const safe = path.resolve(filePath);
|
||||||
|
if (!safe.startsWith(path.resolve(app.getPath('userData')))) {
|
||||||
|
throw new Error('write-text-file: path outside userData is not allowed');
|
||||||
|
}
|
||||||
|
await fs.promises.writeFile(safe, content, 'utf-8');
|
||||||
|
});
|
||||||
ipcMain.handle('write-file', async (event, payload) => {
|
ipcMain.handle('write-file', async (event, payload) => {
|
||||||
const validation = resolveWritablePath(payload?.path);
|
const validation = resolveWritablePath(payload?.path);
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
|
|||||||
@@ -154,6 +154,14 @@ if (typeof window !== 'undefined' && !window.electronAPI) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lazy-loaded: filesystem helpers used by the flowchart panel for
|
||||||
|
// <userData>/flowchart-session.json auto-save.
|
||||||
|
const flowchartIO = {
|
||||||
|
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 }),
|
||||||
|
};
|
||||||
|
|
||||||
// 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
|
||||||
// both the script tag in index.html and CommonJS require() declare it.
|
// both the script tag in index.html and CommonJS require() declare it.
|
||||||
@@ -2406,6 +2414,40 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Flow Chart Editor panel — visual Mermaid flowchart builder.
|
||||||
|
// Persists to <userData>/flowchart-session.json via injected IPC helpers.
|
||||||
|
// Reuses tabManager.insertAtCursor for the "Insert at Cursor" button.
|
||||||
|
const { renderFlowChartPanel } = require('./sidebar/flowchart-panel');
|
||||||
|
// Reuse the existing Mermaid render path used by the preview pane
|
||||||
|
// (src/renderer.js:1106-1142). Lazily loads mermaid on first use.
|
||||||
|
const renderFlowChartMermaid = (source, targetEl) => {
|
||||||
|
targetEl.innerHTML = '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'mermaid';
|
||||||
|
div.textContent = source;
|
||||||
|
targetEl.appendChild(div);
|
||||||
|
if (!window.mermaid) {
|
||||||
|
const mermaidModule = require('mermaid');
|
||||||
|
window.mermaid = mermaidModule.default || mermaidModule;
|
||||||
|
}
|
||||||
|
const theme = document.body.className.includes('theme-dark') ? 'dark' : 'default';
|
||||||
|
window.mermaid.initialize({ startOnLoad: false, theme, securityLevel: 'loose' });
|
||||||
|
window.mermaid
|
||||||
|
.run({ nodes: [div] })
|
||||||
|
.catch((err) => console.warn('flowchart preview render failed:', err));
|
||||||
|
};
|
||||||
|
sidebarManager.registerPanel('flowchart', {
|
||||||
|
title: 'Flow Chart',
|
||||||
|
render: (container) =>
|
||||||
|
renderFlowChartPanel(container, {
|
||||||
|
getUserDataPath: flowchartIO.getUserDataPath,
|
||||||
|
readFile: flowchartIO.readFile,
|
||||||
|
writeFile: flowchartIO.writeFile,
|
||||||
|
insertAtCursor: (text) => tabManager.insertAtCursor(text),
|
||||||
|
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
|
||||||
// confirmation). Delegated on document so it works in every tab's preview.
|
// confirmation). Delegated on document so it works in every tab's preview.
|
||||||
document.addEventListener('click', (event) => {
|
document.addEventListener('click', (event) => {
|
||||||
|
|||||||
@@ -1023,3 +1023,91 @@ body[class*='dark'] .preview-content a.wiki-link {
|
|||||||
color: var(--text-secondary, inherit);
|
color: var(--text-secondary, inherit);
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === Flow Chart Editor panel === */
|
||||||
|
.flowchart-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.flowchart-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-bottom: 1px solid var(--border-color, #444);
|
||||||
|
}
|
||||||
|
.flowchart-insert-btn {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border: 1px solid var(--accent, #4a9eff);
|
||||||
|
background: var(--accent, #4a9eff);
|
||||||
|
color: var(--accent-fg, #fff);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.flowchart-status {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted, #888);
|
||||||
|
}
|
||||||
|
.flowchart-split {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.flowchart-canvas-host {
|
||||||
|
flex: 7;
|
||||||
|
min-width: 0;
|
||||||
|
position: relative;
|
||||||
|
background: var(--bg-secondary, #1e1e1e);
|
||||||
|
}
|
||||||
|
.flowchart-canvas-host svg.flowchart-canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
.flowchart-preview-host {
|
||||||
|
flex: 3;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-left: 1px solid var(--border-color, #444);
|
||||||
|
}
|
||||||
|
.flowchart-preview-source {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px;
|
||||||
|
font-family: var(--mono-font, monospace);
|
||||||
|
font-size: 11px;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre;
|
||||||
|
border-bottom: 1px solid var(--border-color, #444);
|
||||||
|
}
|
||||||
|
.flowchart-preview-render {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.flowchart-node {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
.flowchart-node.selected rect,
|
||||||
|
.flowchart-node.selected polygon {
|
||||||
|
stroke: var(--accent, #4a9eff);
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
.flowchart-edge.selected {
|
||||||
|
stroke: var(--accent, #4a9eff);
|
||||||
|
}
|
||||||
|
.flowchart-label-input {
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid var(--accent, #4a9eff);
|
||||||
|
background: var(--bg-primary, #fff);
|
||||||
|
color: inherit;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user