Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions AUDIT_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

This log tracks all significant changes, updates, and versions in the PaperCache project.

## 2026-06-27 (Graph View Bugfix)
**Change:** fix(graph): prevent `e.graphData is not a function` crash on unmount and replace setInterval

**Details/Why:**
1. When navigating to a note from Graph View (`onNodeClick`), the unmounting sequence destroyed internal `react-force-graph-3d` methods on `fgRef.current` before `GraphView`'s effect cleanup ran. Calling `fg.graphData()` threw a TypeError (`e.graphData is not a function`). Added defensive `typeof fg.graphData === 'function'` verification before invocation and introduced `graphDataRef` as a safe fallback cache for node positions.
2. Replaced the active `setInterval` loop in `GraphView.tsx` with a chained `setTimeout` pattern to comply with project timer guidelines (`No setInterval in renderer or main process`).

**Files changed:** `src/GraphView.tsx`, `CHANGELOG.md`, `AUDIT_LOG.md`.

---

## 2026-06-27 (v0.5.5 Release & Smart Onboarding)
**Change:** chore(release): bump version to 0.5.5; implement smart onboarding and release notes routing

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Settings Bug Report & About Menu**: Added a "Submit a Bug Report" button under System settings linking directly to the GitHub issue creation form. Added a dedicated "About" section displaying the app logo, current version number, update checker, Ko-fi support link, and a thank you message.

### Fixed
- **Graph View crash on navigation**: Fixed `e.graphData is not a function` TypeError when navigating to a note from the Graph View by adding defensive checks before invoking ref methods on component unmount and falling back to a ref cache. Also replaced `setInterval` with a chained `setTimeout` loop.
- **Windows Onboarding File Linking & Generation**: Fixed a bug on Windows where backslashes in generated note IDs caused internal `/file` links in `Welcome.md` to fail and create duplicate empty notes. Normalized note ID generation across Rust and TypeScript to consistently use forward slashes on all platforms, and ensured onboarding template files regenerate correctly on application updates.
- **Window position/size now persists across restarts**: The window-state plugin's `on_window_ready` fires before the macOS display server is ready, causing `available_monitors()` to return empty and the saved position to be silently discarded. Fixed by deferring window-state restoration via a background thread + `run_on_main_thread` 300ms after `setup()` completes, bypassing the plugin's monitor-intersection check with a direct file read. Both the tray "Quit" and Settings "Quit" buttons now explicitly save window state before exit.
- **Launch at Startup now registers as a proper Login Item**: Changed `MacosLauncher` from `LaunchAgent` to `AppleScript`, which registers PaperCache in System Settings > General > Login Items instead of creating a hidden `launchd` plist. Users can now see and manage the autostart entry directly from System Settings.
Expand Down
45 changes: 31 additions & 14 deletions src/GraphView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,15 @@ export default function GraphView({
const fgRef = useRef<ForceGraphMethods<GraphNode, GraphLink> | undefined>(undefined)

const draggedNodesRef = useRef<Set<string>>(new Set())
const graphDataRef = useRef<{ nodes: GraphNode[]; links: GraphLink[] }>({ nodes: [], links: [] })

useEffect(() => {
let raf: number
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let ctrls: any = null
const setup = () => {
const fg = fgRef.current
if (!fg) {
if (!fg || typeof fg.controls !== 'function') {
raf = requestAnimationFrame(setup)
return
}
Expand All @@ -89,8 +90,12 @@ export default function GraphView({
ctrls.zoomSpeed = 6
ctrls.panSpeed = 0.15
ctrls.update()
fg.cameraPosition({ x: 0, y: 0, z: 500 })
setTimeout(() => fg.zoomToFit(400, 50), 300)
if (typeof fg.cameraPosition === 'function') {
fg.cameraPosition({ x: 0, y: 0, z: 500 })
}
setTimeout(() => {
if (fg && typeof fg.zoomToFit === 'function') fg.zoomToFit(400, 50)
}, 300)
}
raf = requestAnimationFrame(setup)
return () => cancelAnimationFrame(raf)
Expand All @@ -101,8 +106,7 @@ export default function GraphView({
// (avoids the react-hooks/exhaustive-deps stale-ref warning)
const fg = fgRef.current
return () => {
if (!fg) return
const data = fg.graphData()
const data = fg && typeof fg.graphData === 'function' ? fg.graphData() : graphDataRef.current
if (!data || !data.nodes) return
data.nodes.forEach((node: GraphNode) => {
if (node.x != null && node.y != null) {
Expand Down Expand Up @@ -151,16 +155,23 @@ export default function GraphView({
return { nodes, links }
}, [notes])

useEffect(() => {
graphDataRef.current = graphData
}, [graphData])

useEffect(() => {
let attempts = 0
const id = setInterval(() => {
let timeoutId: number | null = null

const attemptForceSetup = () => {
const fg = fgRef.current
if (!fg) {
if (!fg || typeof fg.d3Force !== 'function') {
attempts++
if (attempts > 20) clearInterval(id)
if (attempts <= 20) {
timeoutId = window.setTimeout(attemptForceSetup, 50)
}
return
}
clearInterval(id)

const folders = Array.from(new Set(graphData.nodes.map((n) => n.folder).filter(Boolean)))
const centroids = buildFolderCentroids(folders)
Expand All @@ -187,9 +198,15 @@ export default function GraphView({
)
fg.d3Force('charge')?.strength(-120)
fg.d3Force('collision', d3.forceCollide<GraphNode>(22))
fg.d3ReheatSimulation()
}, 50)
return () => clearInterval(id)
if (typeof fg.d3ReheatSimulation === 'function') {
fg.d3ReheatSimulation()
}
}

timeoutId = window.setTimeout(attemptForceSetup, 50)
return () => {
if (timeoutId !== null) clearTimeout(timeoutId)
}
}, [graphData])

const handleNodeClick = useCallback(
Expand All @@ -212,8 +229,8 @@ export default function GraphView({

const focusOnNode = useCallback((nodeId: string) => {
const fg = fgRef.current
if (!fg) return
const node = fg.graphData().nodes.find((n: GraphNode) => n.id === nodeId)
if (!fg || typeof fg.graphData !== 'function' || typeof fg.cameraPosition !== 'function') return
const node = fg.graphData()?.nodes?.find((n: GraphNode) => n.id === nodeId)
if (!node || node.x == null || node.y == null) return
fg.cameraPosition({ x: node.x, y: node.y, z: 120 }, { x: node.x, y: node.y, z: 0 }, 400)
}, [])
Expand Down
Loading