feat: add more node types in filter panel (#519)

* fix(web): enable Cypher queries when connected to backend server

Route queries through HTTP API in backend mode instead of checking local WASM database.

Made-with: Cursor

* chore: add Maven/Gradle wrapper files to default ignore list

Add build wrapper scripts and directories to hardcoded ignore lists:
- Directories: .mvn, .gradle, gradle
- Files: mvnw, mvnw.cmd, gradlew, gradlew.bat

These are build infrastructure files, not source code.

Made-with: Cursor

* ci: re-trigger CI (Windows flaky timeout)

Made-with: Cursor

* feat: add more node types in filter panel

* feat: add more node types in filter panel

* revert additional changes

* test(web): add unit tests for filter panel node types

- FILTERABLE_LABELS: verify new types (Enum, Type, Decorator, Variable)
  have colors, sizes, and no duplicates
- Filter panel icons: verify every filterable label has an icon mapped
  and all icons are exported from lucide-icons
- Color legend: verify new types are included, ordered correctly, and
  are a subset of FILTERABLE_LABELS

Made-with: Cursor
This commit is contained in:
Chirag Nighut 2026-03-26 12:20:40 +05:30 • committed by GitHub
parent 961f004c4b
commit b398e58e53
5 changed files with 132 additions and 3 deletions

View file

@ -14,6 +14,9 @@ import {
Variable,
Hash,
Target,
List,
AtSign,
Type,
} from '@/lib/lucide-icons';
import { useAppState } from '../hooks/useAppState';
import { FILTERABLE_LABELS, NODE_COLORS, ALL_EDGE_TYPES, EDGE_INFO, type EdgeType } from '../lib/constants';
@ -187,7 +190,11 @@ const getNodeTypeIcon = (label: NodeLabel) => {
case 'Function': return Braces;
case 'Method': return Braces;
case 'Interface': return Hash;
case 'Enum': return List;
case 'Type': return Type;
case 'Decorator': return AtSign;
case 'Import': return FileCode;
case 'Variable': return Variable;
default: return Variable;
}
};
@ -501,7 +508,7 @@ export const FileTreePanel = ({ onFocusNode }: FileTreePanelProps) => {
Color Legend
</h3>
<div className="grid grid-cols-2 gap-2">
{(['Folder', 'File', 'Class', 'Function', 'Interface', 'Method'] as NodeLabel[]).map(label => (
{(['Folder', 'File', 'Class', 'Interface', 'Enum', 'Type', 'Function', 'Method', 'Variable', 'Decorator'] as NodeLabel[]).map(label => (
<div key={label} className="flex items-center gap-1.5">
<div
className="w-2.5 h-2.5 rounded-full"

View file

@ -112,15 +112,18 @@ export const DEFAULT_VISIBLE_LABELS: NodeLabel[] = [
'Type',
];
// All filterable labels
// All filterable labels (in display order)
export const FILTERABLE_LABELS: NodeLabel[] = [
'Folder',
'File',
'Class',
'Interface',
'Enum',
'Type',
'Function',
'Method',
'Variable',
'Interface',
'Decorator',
'Import',
];

View file

@ -10,6 +10,7 @@ export {
AlertCircle,
AlertTriangle,
ArrowRight,
AtSign,
Brain,
Box,
Braces,
@ -39,6 +40,7 @@ export {
Layers,
Lightbulb,
LightbulbOff,
List,
Loader2,
Maximize2,
MousePointerClick,
@ -63,6 +65,7 @@ export {
Target,
Terminal,
Trash2,
Type,
Upload,
User,
Variable,

View file

@ -59,6 +59,41 @@ describe('DEFAULT_VISIBLE_LABELS', () => {
});
});
describe('FILTERABLE_LABELS', () => {
it('includes all newly added node types', () => {
expect(FILTERABLE_LABELS).toContain('Enum');
expect(FILTERABLE_LABELS).toContain('Type');
expect(FILTERABLE_LABELS).toContain('Decorator');
expect(FILTERABLE_LABELS).toContain('Variable');
});
it('every filterable label has a defined color in NODE_COLORS', () => {
for (const label of FILTERABLE_LABELS) {
expect(NODE_COLORS).toHaveProperty(label);
expect(NODE_COLORS[label as keyof typeof NODE_COLORS]).toMatch(/^#[0-9a-f]{6}$/i);
}
});
it('every filterable label has a defined size in NODE_SIZES', () => {
for (const label of FILTERABLE_LABELS) {
expect(NODE_SIZES).toHaveProperty(label);
expect(NODE_SIZES[label as keyof typeof NODE_SIZES]).toBeGreaterThan(0);
}
});
it('has no duplicate entries', () => {
const unique = new Set(FILTERABLE_LABELS);
expect(unique.size).toBe(FILTERABLE_LABELS.length);
});
it('is a subset of DEFAULT_VISIBLE_LABELS plus togglable labels', () => {
const allKnown = new Set(Object.keys(NODE_COLORS));
for (const label of FILTERABLE_LABELS) {
expect(allKnown.has(label)).toBe(true);
}
});
});
describe('edge types', () => {
it('ALL_EDGE_TYPES contains all EDGE_INFO keys', () => {
const edgeInfoKeys = Object.keys(EDGE_INFO).sort();

View file

@ -0,0 +1,81 @@
import { describe, expect, it } from 'vitest';
import { FILTERABLE_LABELS, NODE_COLORS } from '../../src/lib/constants';
import type { NodeLabel } from '../../src/core/graph/types';
import * as lucideIcons from '../../src/lib/lucide-icons';
const LEGEND_LABELS: NodeLabel[] = [
'Folder', 'File', 'Class', 'Interface', 'Enum', 'Type',
'Function', 'Method', 'Variable', 'Decorator',
];
const ICON_MAP: Record<string, string> = {
Folder: 'Folder',
File: 'FileCode',
Class: 'Box',
Function: 'Braces',
Method: 'Braces',
Interface: 'Hash',
Enum: 'List',
Type: 'Type',
Decorator: 'AtSign',
Import: 'FileCode',
Variable: 'Variable',
};
describe('filter panel icon mappings', () => {
it('every filterable label has a mapped icon', () => {
for (const label of FILTERABLE_LABELS) {
expect(ICON_MAP).toHaveProperty(label);
}
});
it('every mapped icon is exported from lucide-icons', () => {
const exportedNames = new Set(Object.keys(lucideIcons));
const requiredIcons = new Set(Object.values(ICON_MAP));
for (const iconName of requiredIcons) {
expect(exportedNames.has(iconName), `${iconName} should be exported from lucide-icons`).toBe(true);
}
});
it('newly added node types have distinct icons', () => {
expect(ICON_MAP.Enum).toBe('List');
expect(ICON_MAP.Type).toBe('Type');
expect(ICON_MAP.Decorator).toBe('AtSign');
});
});
describe('color legend', () => {
it('includes all newly added node types', () => {
expect(LEGEND_LABELS).toContain('Enum');
expect(LEGEND_LABELS).toContain('Type');
expect(LEGEND_LABELS).toContain('Decorator');
expect(LEGEND_LABELS).toContain('Variable');
});
it('every legend label has a color defined', () => {
for (const label of LEGEND_LABELS) {
expect(NODE_COLORS).toHaveProperty(label);
expect(NODE_COLORS[label]).toMatch(/^#[0-9a-f]{6}$/i);
}
});
it('legend labels match the order used in FileTreePanel', () => {
const expected: NodeLabel[] = [
'Folder', 'File', 'Class', 'Interface', 'Enum', 'Type',
'Function', 'Method', 'Variable', 'Decorator',
];
expect(LEGEND_LABELS).toEqual(expected);
});
it('legend labels are a subset of filterable labels plus Import', () => {
const filterableSet = new Set(FILTERABLE_LABELS);
for (const label of LEGEND_LABELS) {
expect(filterableSet.has(label), `${label} should be in FILTERABLE_LABELS`).toBe(true);
}
});
it('has no duplicate entries', () => {
const unique = new Set(LEGEND_LABELS);
expect(unique.size).toBe(LEGEND_LABELS.length);
});
});