Skip to content
Open
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@
"react-dom": "^19.0.0",
"react-router-dom": "^7.15.1",
"sigma": "^3.0.0-beta.27",
"zod": "^3.22.4"
"zod": "^3.22.4",
"zustand": "^5.0.14"
},
"devDependencies": {
"@eslint/js": "9.39.4",
Expand Down
27 changes: 27 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions src/components/SyncToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react';
import { RefreshCw } from 'lucide-react';
import { useGraphSyncStore } from '../store/graph-sync-store';

interface SyncToggleProps {
tooltip?: string;
}

const SyncToggle: React.FC<SyncToggleProps> = ({
tooltip = "Sync mind map with knowledge graph"
}) => {
const { syncEnabled, setSyncEnabled } = useGraphSyncStore();

return (
<button
onClick={() => setSyncEnabled(!syncEnabled)}
className={`filter-chip ${syncEnabled ? 'active' : ''}`}
aria-pressed={syncEnabled}
title={tooltip}
aria-label={tooltip}
style={{
display: 'flex',
alignItems: 'center',
gap: 'var(--space-2)',
position: 'relative'
}}
>
<RefreshCw size={14} className={syncEnabled ? 'animate-spin-slow' : ''} />
{syncEnabled ? 'Sync On' : 'Sync Off'}
{syncEnabled && (
<span style={{
position: 'absolute',
top: '2px',
right: '2px',
width: '6px',
height: '6px',
background: 'var(--status-success)',
borderRadius: '50%',
boxShadow: '0 0 4px var(--status-success)'
}} />
)}
</button>
);
};

export default SyncToggle;
87 changes: 87 additions & 0 deletions src/features/__tests__/sync-integration.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, act } from '@testing-library/react';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'render' is defined but never used


Unused variables are generally considered a code smell and should be avoided.

import React from 'react';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'React' is defined but never used


Unused variables are generally considered a code smell and should be avoided.

import { useGraphSyncStore } from '../../store/graph-sync-store';
import { setupMindMapSyncListeners } from '../mindmap/sync-adapter';
import { setupGraphSyncListeners } from '../graph/sync-adapter';
import Graph from 'graphology';

// Mock MindElixir as it's hard to render in Vitest/Happy-dom without a real canvas
const mockMindMap = {
bus: {
addListener: vi.fn(),
},
findEle: vi.fn(),
addChild: vi.fn(),
updateNodeStyle: vi.fn(),
refresh: vi.fn(),
removeNodes: vi.fn(),
};

describe('Sync Integration', () => {
beforeEach(() => {
useGraphSyncStore.getState().clearEvents();
useGraphSyncStore.getState().setSyncEnabled(true);
vi.clearAllMocks();
});

it('should sync node addition from mind map to graph', () => {
const graph = new Graph();
setupGraphSyncListeners(graph);

// Simulate setupMindMapSyncListeners
let mindMapListener: (op: any) => void = () => {};

Check warning on line 33 in src/features/__tests__/sync-integration.test.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/features/__tests__/sync-integration.test.tsx#L33

Unexpected any. Specify a different type.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected any. Specify a different type


The any type can sometimes leak into your codebase. TypeScript compiler skips the type checking of the any typed variables, so it creates a potential safety hole, and source of bugs in your codebase. We recommend using unknown or never type variable.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected empty arrow function


Having empty functions hurts readability, and is considered a code-smell. There's almost always a way to avoid using them. If you must use one, consider adding a comment to inform the reader of its purpose.

mockMindMap.bus.addListener.mockImplementation((name, cb) => {
if (name === 'operation') mindMapListener = cb;
});
setupMindMapSyncListeners(mockMindMap as any);

Check warning on line 37 in src/features/__tests__/sync-integration.test.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/features/__tests__/sync-integration.test.tsx#L37

Unexpected any. Specify a different type.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected any. Specify a different type


The any type can sometimes leak into your codebase. TypeScript compiler skips the type checking of the any typed variables, so it creates a potential safety hole, and source of bugs in your codebase. We recommend using unknown or never type variable.


// 1. Add node in mind map
act(() => {
mindMapListener({
name: 'addChild',
obj: { id: 'test-node', topic: 'Test Node' }
});
});

// Verify event emitted
expect(useGraphSyncStore.getState().pendingEvents).toHaveLength(1);
expect(useGraphSyncStore.getState().pendingEvents[0].type).toBe('node:add');

// 2. Consume in graph (simulating the effect in GraphView)
const events = useGraphSyncStore.getState().consumeEvents('graph');
expect(events).toHaveLength(1);

act(() => {
const payload: any = events[0].payload;

Check warning on line 56 in src/features/__tests__/sync-integration.test.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/features/__tests__/sync-integration.test.tsx#L56

Unexpected any. Specify a different type.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected any. Specify a different type


The any type can sometimes leak into your codebase. TypeScript compiler skips the type checking of the any typed variables, so it creates a potential safety hole, and source of bugs in your codebase. We recommend using unknown or never type variable.

graph.addNode(payload.id, { label: payload.label });
});

expect(graph.hasNode('test-node')).toBe(true);
expect(graph.getNodeAttribute('test-node', 'label')).toBe('Test Node');
});

it('should sync node update from graph to mind map', () => {
const graph = new Graph();
setupGraphSyncListeners(graph);

// 1. Add node in graph
act(() => {
graph.addNode('node-1', { label: 'Initial' });
});

// Should have emitted node:add
expect(useGraphSyncStore.getState().pendingEvents).toHaveLength(1);
useGraphSyncStore.getState().consumeEvents('mindmap'); // Clear

// 2. Update node in graph
act(() => {
graph.setNodeAttribute('node-1', 'label', 'Updated');
});

const events = useGraphSyncStore.getState().pendingEvents;
expect(events.some(e => e.type === 'node:update')).toBe(true);
const updateEvent = events.find(e => e.type === 'node:update');
expect((updateEvent?.payload as any).label).toBe('Updated');

Check warning on line 85 in src/features/__tests__/sync-integration.test.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/features/__tests__/sync-integration.test.tsx#L85

Unexpected any. Specify a different type.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected any. Specify a different type


The any type can sometimes leak into your codebase. TypeScript compiler skips the type checking of the any typed variables, so it creates a potential safety hole, and source of bugs in your codebase. We recommend using unknown or never type variable.

});
});
2 changes: 2 additions & 0 deletions src/features/graph/GraphControls.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useState, useRef, useEffect } from 'react';
import { Focus, Camera, Clock, X, FolderOpen, GitCompare, RotateCcw, Loader2, Layout, LayoutDashboard, Download, CircleDot } from 'lucide-react';
import SyncToggle from '../../components/SyncToggle';
import { useFocusTrap } from '../../hooks/useFocusTrap';
import { useEscapeKey } from '../../hooks/useEscapeKey';
import { useMediaQuery } from '../../hooks/useMediaQuery';
Expand Down Expand Up @@ -215,6 +216,7 @@ const GraphControls: React.FC<GraphControlsProps> = ({
<RotateCcw size={16} /> Exit Snapshot
</button>
)}
{!snapshotMode && <SyncToggle />}
{hasSelection && !isMobile && (
<div className="selection-info">
Selected: <strong>{selectedName}</strong>
Expand Down
65 changes: 65 additions & 0 deletions src/features/graph/GraphView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import { logger } from '../../lib/logger';
import { perf } from '../../lib/perf';
import { applyCircularLayout, applyForceLayout, applyHierarchicalLayout } from '../../lib/graph-layout';
import { useGraphSyncStore } from '../../store/graph-sync-store';
import { setupGraphSyncListeners } from './sync-adapter';
import type { SharedNode, SharedEdge } from '../../store/graph-sync-types';
import { useGraphKeyboardNavigation } from './GraphKeyboardNav';
import { useGraphTouchGestures } from './GraphTouchHandler';
import { useGraphSnapshotManager } from './GraphSnapshotManager';
Expand Down Expand Up @@ -298,6 +301,12 @@
};
}, [effectiveData, selectedNode, focusMode, snapshotMode, setFocusMode, setSelectedNode, layout, layoutSettings, links, graphSize]);

useEffect(() => {
if (!graphRef.current) return;
const cleanup = setupGraphSyncListeners(graphRef.current);
return cleanup;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arrow function expected no return value


Any code paths that do not have explicit returns will return undefined. It is recommended to replace any implicit dead-ends that return undefined with a return null statement.

}, []);

useEffect(() => {
if (!selectedNode) {
setSelectedEntityClaims([]);
Expand Down Expand Up @@ -330,6 +339,62 @@
void sigma.getCamera().animatedReset({ duration: 400 });
}, [layout, effectiveData.entities, links]);

// Sync effect to consume events
useEffect(() => {
const unsubscribe = useGraphSyncStore.subscribe((state) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function has a cyclomatic complexity of 6 with "medium" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

if (!state.syncEnabled || !graphRef.current || state.pendingEvents.length === 0) return;

const events = useGraphSyncStore.getState().consumeEvents('graph');
if (events.length === 0) return;

const graph = graphRef.current;
events.forEach(event => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function has a cyclomatic complexity of 14 with "medium" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

if (event.type === 'node:add') {
const payload = event.payload as SharedNode;
if (!graph.hasNode(payload.id)) {
graph.addNode(payload.id, {
label: payload.label,
size: 10,
color: '#2563eb',
x: Math.random(),

Check failure on line 359 in src/features/graph/GraphView.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/features/graph/GraphView.tsx#L359

This rule identifies use of cryptographically weak random number generators.
y: Math.random()

Check failure on line 360 in src/features/graph/GraphView.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/features/graph/GraphView.tsx#L360

This rule identifies use of cryptographically weak random number generators.
});
}
} else if (event.type === 'node:update') {
const payload = event.payload as SharedNode;
if (graph.hasNode(payload.id)) {
const currentLabel = graph.getNodeAttribute(payload.id, 'label');
if (currentLabel !== payload.label) {
console.warn(`[Sync Conflict] Graph node ${payload.id} has different label. Local: ${currentLabel}, Incoming: ${payload.label}. Applying last-writer wins.`);
graph.mergeNodeAttributes(payload.id, { label: payload.label });
}
}
} else if (event.type === 'node:remove') {
const payload = event.payload as SharedNode;
if (graph.hasNode(payload.id)) {
graph.dropNode(payload.id);
}
} else if (event.type === 'edge:add') {
const payload = event.payload as SharedEdge;
if (graph.hasNode(payload.from) && graph.hasNode(payload.to) && !graph.hasEdge(payload.id)) {
graph.addEdgeWithKey(payload.id, payload.from, payload.to, {
label: payload.label,
size: 2,
color: '#94a3b8'
});
}
} else if (event.type === 'edge:remove') {
const payload = event.payload as SharedEdge;
if (graph.hasEdge(payload.id)) {
graph.dropEdge(payload.id);
}
}
});
if (sigmaInstance.current) sigmaInstance.current.refresh();
});
return () => unsubscribe();
}, []);

useEffect(() => {
return () => {
sigmaInstance.current?.kill();
Expand Down
69 changes: 69 additions & 0 deletions src/features/graph/sync-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type Graph from 'graphology';
import { useGraphSyncStore } from '../../store/graph-sync-store';
import { SharedNode, SharedEdge } from '../../store/graph-sync-types';

export function setupGraphSyncListeners(
graph: Graph
) {
const store = useGraphSyncStore.getState();

const onNodeAdded = ({ key, attributes }: any) => {

Check warning on line 10 in src/features/graph/sync-adapter.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/features/graph/sync-adapter.ts#L10

Unexpected any. Specify a different type.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected any. Specify a different type


The any type can sometimes leak into your codebase. TypeScript compiler skips the type checking of the any typed variables, so it creates a potential safety hole, and source of bugs in your codebase. We recommend using unknown or never type variable.

if (!useGraphSyncStore.getState().syncEnabled) return;
store.emitEvent({
type: 'node:add',
source: 'graph',
payload: { id: key, label: attributes.label } as SharedNode,
});
};

const onNodeAttributesUpdated = ({ key, attributes }: any) => {

Check warning on line 19 in src/features/graph/sync-adapter.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/features/graph/sync-adapter.ts#L19

Unexpected any. Specify a different type.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected any. Specify a different type


The any type can sometimes leak into your codebase. TypeScript compiler skips the type checking of the any typed variables, so it creates a potential safety hole, and source of bugs in your codebase. We recommend using unknown or never type variable.

if (!useGraphSyncStore.getState().syncEnabled) return;
store.emitEvent({
type: 'node:update',
source: 'graph',
payload: { id: key, label: attributes.label } as SharedNode,
});
};

const onNodeDropped = ({ key }: any) => {

Check warning on line 28 in src/features/graph/sync-adapter.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/features/graph/sync-adapter.ts#L28

Unexpected any. Specify a different type.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected any. Specify a different type


The any type can sometimes leak into your codebase. TypeScript compiler skips the type checking of the any typed variables, so it creates a potential safety hole, and source of bugs in your codebase. We recommend using unknown or never type variable.

if (!useGraphSyncStore.getState().syncEnabled) return;
store.emitEvent({
type: 'node:remove',
source: 'graph',
payload: { id: key, label: '' } as SharedNode,
});
};

const onEdgeAdded = ({ key, source, target, attributes }: any) => {

Check warning on line 37 in src/features/graph/sync-adapter.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/features/graph/sync-adapter.ts#L37

Unexpected any. Specify a different type.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected any. Specify a different type


The any type can sometimes leak into your codebase. TypeScript compiler skips the type checking of the any typed variables, so it creates a potential safety hole, and source of bugs in your codebase. We recommend using unknown or never type variable.

if (!useGraphSyncStore.getState().syncEnabled) return;
store.emitEvent({
type: 'edge:add',
source: 'graph',
payload: { id: key, from: source, to: target, label: attributes.label } as SharedEdge,
});
};

const onEdgeDropped = ({ key }: any) => {

Check warning on line 46 in src/features/graph/sync-adapter.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/features/graph/sync-adapter.ts#L46

Unexpected any. Specify a different type.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected any. Specify a different type


The any type can sometimes leak into your codebase. TypeScript compiler skips the type checking of the any typed variables, so it creates a potential safety hole, and source of bugs in your codebase. We recommend using unknown or never type variable.

if (!useGraphSyncStore.getState().syncEnabled) return;
// We don't have from/to here, but id should be enough for removal if shared
store.emitEvent({
type: 'edge:remove',
source: 'graph',
payload: { id: key, from: '', to: '' } as SharedEdge,
});
};

graph.on('nodeAdded', onNodeAdded);
graph.on('nodeAttributesUpdated', onNodeAttributesUpdated);
graph.on('nodeDropped', onNodeDropped);
graph.on('edgeAdded', onEdgeAdded);
graph.on('edgeDropped', onEdgeDropped);

return () => {
graph.off('nodeAdded', onNodeAdded);
graph.off('nodeAttributesUpdated', onNodeAttributesUpdated);
graph.off('nodeDropped', onNodeDropped);
graph.off('edgeAdded', onEdgeAdded);
graph.off('edgeDropped', onEdgeDropped);
};
}
Comment on lines +5 to +69

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Loading
Loading