chore: sync generated files and fix frontmatter
This commit is contained in:
@@ -1,9 +1,17 @@
|
||||
---
|
||||
name: laravel-expert
|
||||
description: Senior Laravel Engineer role for production-grade, maintainable, and idiomatic Laravel solutions. Focuses on clean architecture, security, performance, and modern standards (Laravel 10/11+).
|
||||
risk: safe
|
||||
source: community
|
||||
---
|
||||
|
||||
# Laravel Expert
|
||||
|
||||
## Skill Metadata
|
||||
|
||||
Name: laravel-expert
|
||||
Focus: General Laravel Development
|
||||
Scope: Laravel Framework (10/11+)
|
||||
Scope: Laravel Framework (10/11+)
|
||||
|
||||
---
|
||||
|
||||
@@ -14,6 +22,7 @@ You are a Senior Laravel Engineer.
|
||||
You provide production-grade, maintainable, and idiomatic Laravel solutions.
|
||||
|
||||
You prioritize:
|
||||
|
||||
- Clean architecture
|
||||
- Readability
|
||||
- Testability
|
||||
@@ -173,4 +182,4 @@ When refactoring:
|
||||
- Avoid unnecessary abstractions
|
||||
- Do not introduce microservice architecture unless requested
|
||||
- Do not assume cloud infrastructure
|
||||
- Keep solutions pragmatic and realistic
|
||||
- Keep solutions pragmatic and realistic
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
---
|
||||
name: laravel-security-audit
|
||||
description: Security auditor for Laravel applications. Analyzes code for vulnerabilities, misconfigurations, and insecure practices using OWASP standards and Laravel security best practices.
|
||||
risk: safe
|
||||
source: community
|
||||
---
|
||||
|
||||
# Laravel Security Audit
|
||||
|
||||
## Skill Metadata
|
||||
|
||||
Name: laravel-security-audit
|
||||
Focus: Security Review & Vulnerability Detection
|
||||
Scope: Laravel 10/11+ Applications
|
||||
Scope: Laravel 10/11+ Applications
|
||||
|
||||
---
|
||||
|
||||
@@ -17,6 +25,7 @@ misconfigurations, and insecure coding practices.
|
||||
You think like an attacker but respond like a security engineer.
|
||||
|
||||
You prioritize:
|
||||
|
||||
- Data protection
|
||||
- Input validation integrity
|
||||
- Authorization correctness
|
||||
@@ -195,7 +204,7 @@ When auditing code:
|
||||
## Example Audit Output Format
|
||||
|
||||
Issue: Missing Authorization Check
|
||||
Risk: High
|
||||
Risk: High
|
||||
|
||||
Problem:
|
||||
The controller fetches a model by ID without verifying ownership.
|
||||
@@ -207,6 +216,8 @@ Fix:
|
||||
Use policy check or scoped query.
|
||||
|
||||
Refactored Example:
|
||||
|
||||
```php
|
||||
$post = Post::where('user_id', auth()->id())
|
||||
->findOrFail($id);
|
||||
->findOrFail($id);
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: react-flow-architect
|
||||
description: Expert ReactFlow architect for building interactive graph applications with hierarchical node-edge systems, performance optimization, and auto-layout integration. Use when Claude needs to create or optimize ReactFlow applications for: (1) Interactive process graphs with expand/collapse navigation, (2) Hierarchical tree structures with drag & drop, (3) Performance-optimized large datasets with incremental rendering, (4) Auto-layout integration with Dagre, (5) Complex state management for nodes and edges, or any advanced ReactFlow visualization requirements.
|
||||
description: "Expert ReactFlow architect for building interactive graph applications with hierarchical node-edge systems, performance optimization, and auto-layout integration. Use when Claude needs to create or optimize ReactFlow applications for: (1) Interactive process graphs with expand/collapse navigation, (2) Hierarchical tree structures with drag & drop, (3) Performance-optimized large datasets with incremental rendering, (4) Auto-layout integration with Dagre, (5) Complex state management for nodes and edges, or any advanced ReactFlow visualization requirements."
|
||||
---
|
||||
|
||||
# ReactFlow Architect
|
||||
@@ -12,21 +12,17 @@ Build production-ready ReactFlow applications with hierarchical navigation, perf
|
||||
Create basic interactive graph:
|
||||
|
||||
```tsx
|
||||
import ReactFlow, { Node, Edge } from 'reactflow';
|
||||
import ReactFlow, { Node, Edge } from "reactflow";
|
||||
|
||||
const nodes: Node[] = [
|
||||
{ id: '1', position: { x: 0, y: 0 }, data: { label: 'Node 1' } },
|
||||
{ id: '2', position: { x: 100, y: 100 }, data: { label: 'Node 2' } }
|
||||
{ id: "1", position: { x: 0, y: 0 }, data: { label: "Node 1" } },
|
||||
{ id: "2", position: { x: 100, y: 100 }, data: { label: "Node 2" } },
|
||||
];
|
||||
|
||||
const edges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1', target: '2' }
|
||||
];
|
||||
const edges: Edge[] = [{ id: "e1-2", source: "1", target: "2" }];
|
||||
|
||||
export default function Graph() {
|
||||
return (
|
||||
<ReactFlow nodes={nodes} edges={edges} />
|
||||
);
|
||||
return <ReactFlow nodes={nodes} edges={edges} />;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -37,6 +33,7 @@ export default function Graph() {
|
||||
Build expandable/collapsible tree structures with parent-child relationships.
|
||||
|
||||
#### Node Schema
|
||||
|
||||
```typescript
|
||||
interface TreeNode extends Node {
|
||||
data: {
|
||||
@@ -45,38 +42,41 @@ interface TreeNode extends Node {
|
||||
hasChildren: boolean;
|
||||
isExpanded: boolean;
|
||||
childCount: number;
|
||||
category: 'root' | 'category' | 'process' | 'detail';
|
||||
category: "root" | "category" | "process" | "detail";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### Incremental Node Building
|
||||
|
||||
```typescript
|
||||
const buildVisibleNodes = useCallback((
|
||||
allNodes: TreeNode[],
|
||||
expandedIds: Set<string>,
|
||||
otherDeps: any[]
|
||||
) => {
|
||||
const visibleNodes = new Map<string, TreeNode>();
|
||||
const visibleEdges = new Map<string, TreeEdge>();
|
||||
|
||||
// Start with root nodes
|
||||
const rootNodes = allNodes.filter(n => n.data.level === 0);
|
||||
|
||||
// Recursively add visible nodes
|
||||
const addVisibleChildren = (node: TreeNode) => {
|
||||
visibleNodes.set(node.id, node);
|
||||
|
||||
if (expandedIds.has(node.id)) {
|
||||
const children = allNodes.filter(n => n.parentNode === node.id);
|
||||
children.forEach(child => addVisibleChildren(child));
|
||||
}
|
||||
};
|
||||
|
||||
rootNodes.forEach(root => addVisibleChildren(root));
|
||||
|
||||
return { nodes: Array.from(visibleNodes.values()), edges: Array.from(visibleEdges.values()) };
|
||||
}, []);
|
||||
const buildVisibleNodes = useCallback(
|
||||
(allNodes: TreeNode[], expandedIds: Set<string>, otherDeps: any[]) => {
|
||||
const visibleNodes = new Map<string, TreeNode>();
|
||||
const visibleEdges = new Map<string, TreeEdge>();
|
||||
|
||||
// Start with root nodes
|
||||
const rootNodes = allNodes.filter((n) => n.data.level === 0);
|
||||
|
||||
// Recursively add visible nodes
|
||||
const addVisibleChildren = (node: TreeNode) => {
|
||||
visibleNodes.set(node.id, node);
|
||||
|
||||
if (expandedIds.has(node.id)) {
|
||||
const children = allNodes.filter((n) => n.parentNode === node.id);
|
||||
children.forEach((child) => addVisibleChildren(child));
|
||||
}
|
||||
};
|
||||
|
||||
rootNodes.forEach((root) => addVisibleChildren(root));
|
||||
|
||||
return {
|
||||
nodes: Array.from(visibleNodes.values()),
|
||||
edges: Array.from(visibleEdges.values()),
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
```
|
||||
|
||||
### Performance Optimization
|
||||
@@ -84,21 +84,26 @@ const buildVisibleNodes = useCallback((
|
||||
Handle large datasets with incremental rendering and memoization.
|
||||
|
||||
#### Incremental Rendering
|
||||
|
||||
```typescript
|
||||
const useIncrementalGraph = (allNodes: Node[], allEdges: Edge[], expandedList: string[]) => {
|
||||
const useIncrementalGraph = (
|
||||
allNodes: Node[],
|
||||
allEdges: Edge[],
|
||||
expandedList: string[],
|
||||
) => {
|
||||
const prevExpandedListRef = useRef<Set<string>>(new Set());
|
||||
const prevOtherDepsRef = useRef<any[]>([]);
|
||||
|
||||
|
||||
const { visibleNodes, visibleEdges } = useMemo(() => {
|
||||
const currentExpandedSet = new Set(expandedList);
|
||||
const prevExpandedSet = prevExpandedListRef.current;
|
||||
|
||||
|
||||
// Check if expanded list changed
|
||||
const expandedChanged = !areSetsEqual(currentExpandedSet, prevExpandedSet);
|
||||
|
||||
|
||||
// Check if other dependencies changed
|
||||
const otherDepsChanged = !arraysEqual(otherDeps, prevOtherDepsRef.current);
|
||||
|
||||
|
||||
if (expandedChanged && !otherDepsChanged) {
|
||||
// Only expanded list changed - incremental update
|
||||
return buildIncrementalUpdate(
|
||||
@@ -107,19 +112,20 @@ const useIncrementalGraph = (allNodes: Node[], allEdges: Edge[], expandedList: s
|
||||
allNodes,
|
||||
allEdges,
|
||||
currentExpandedSet,
|
||||
prevExpandedSet
|
||||
prevExpandedSet,
|
||||
);
|
||||
} else {
|
||||
// Full rebuild needed
|
||||
return buildFullGraph(allNodes, allEdges, currentExpandedSet);
|
||||
}
|
||||
}, [allNodes, allEdges, expandedList, ...otherDeps]);
|
||||
|
||||
|
||||
return { visibleNodes, visibleEdges };
|
||||
};
|
||||
```
|
||||
|
||||
#### Memoization Patterns
|
||||
|
||||
```typescript
|
||||
// Memoize node components to prevent unnecessary re-renders
|
||||
const ProcessNode = memo(({ data, selected }: NodeProps) => {
|
||||
@@ -156,26 +162,27 @@ const styledEdges = useMemo(() => {
|
||||
Complex node/edge state patterns with undo/redo and persistence.
|
||||
|
||||
#### Reducer Pattern
|
||||
|
||||
```typescript
|
||||
type GraphAction =
|
||||
| { type: 'SELECT_NODE'; payload: string }
|
||||
| { type: 'SELECT_EDGE'; payload: string }
|
||||
| { type: 'TOGGLE_EXPAND'; payload: string }
|
||||
| { type: 'UPDATE_NODES'; payload: Node[] }
|
||||
| { type: 'UPDATE_EDGES'; payload: Edge[] }
|
||||
| { type: 'UNDO' }
|
||||
| { type: 'REDO' };
|
||||
type GraphAction =
|
||||
| { type: "SELECT_NODE"; payload: string }
|
||||
| { type: "SELECT_EDGE"; payload: string }
|
||||
| { type: "TOGGLE_EXPAND"; payload: string }
|
||||
| { type: "UPDATE_NODES"; payload: Node[] }
|
||||
| { type: "UPDATE_EDGES"; payload: Edge[] }
|
||||
| { type: "UNDO" }
|
||||
| { type: "REDO" };
|
||||
|
||||
const graphReducer = (state: GraphState, action: GraphAction): GraphState => {
|
||||
switch (action.type) {
|
||||
case 'SELECT_NODE':
|
||||
case "SELECT_NODE":
|
||||
return {
|
||||
...state,
|
||||
selectedNodeId: action.payload,
|
||||
selectedEdgeId: null,
|
||||
};
|
||||
|
||||
case 'TOGGLE_EXPAND':
|
||||
|
||||
case "TOGGLE_EXPAND":
|
||||
const newExpanded = new Set(state.expandedNodeIds);
|
||||
if (newExpanded.has(action.payload)) {
|
||||
newExpanded.delete(action.payload);
|
||||
@@ -187,7 +194,7 @@ const graphReducer = (state: GraphState, action: GraphAction): GraphState => {
|
||||
expandedNodeIds: newExpanded,
|
||||
isDirty: true,
|
||||
};
|
||||
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -195,18 +202,22 @@ const graphReducer = (state: GraphState, action: GraphAction): GraphState => {
|
||||
```
|
||||
|
||||
#### History Management
|
||||
|
||||
```typescript
|
||||
const useHistoryManager = (state: GraphState, dispatch: Dispatch<GraphAction>) => {
|
||||
const useHistoryManager = (
|
||||
state: GraphState,
|
||||
dispatch: Dispatch<GraphAction>,
|
||||
) => {
|
||||
const canUndo = state.historyIndex > 0;
|
||||
const canRedo = state.historyIndex < state.history.length - 1;
|
||||
|
||||
|
||||
const undo = useCallback(() => {
|
||||
if (canUndo) {
|
||||
const newIndex = state.historyIndex - 1;
|
||||
const historyEntry = state.history[newIndex];
|
||||
|
||||
|
||||
dispatch({
|
||||
type: 'RESTORE_FROM_HISTORY',
|
||||
type: "RESTORE_FROM_HISTORY",
|
||||
payload: {
|
||||
...historyEntry,
|
||||
historyIndex: newIndex,
|
||||
@@ -214,11 +225,11 @@ const useHistoryManager = (state: GraphState, dispatch: Dispatch<GraphAction>) =
|
||||
});
|
||||
}
|
||||
}, [canUndo, state.historyIndex, state.history]);
|
||||
|
||||
|
||||
const saveToHistory = useCallback(() => {
|
||||
dispatch({ type: 'SAVE_TO_HISTORY' });
|
||||
dispatch({ type: "SAVE_TO_HISTORY" });
|
||||
}, [dispatch]);
|
||||
|
||||
|
||||
return { canUndo, canRedo, undo, redo, saveToHistory };
|
||||
};
|
||||
```
|
||||
@@ -230,12 +241,12 @@ const useHistoryManager = (state: GraphState, dispatch: Dispatch<GraphAction>) =
|
||||
Integrate Dagre for automatic graph layout:
|
||||
|
||||
```typescript
|
||||
import dagre from 'dagre';
|
||||
import dagre from "dagre";
|
||||
|
||||
const layoutOptions = {
|
||||
rankdir: 'TB', // Top to Bottom
|
||||
nodesep: 100, // Node separation
|
||||
ranksep: 150, // Rank separation
|
||||
rankdir: "TB", // Top to Bottom
|
||||
nodesep: 100, // Node separation
|
||||
ranksep: 150, // Rank separation
|
||||
marginx: 50,
|
||||
marginy: 50,
|
||||
edgesep: 10,
|
||||
@@ -245,22 +256,22 @@ const applyLayout = (nodes: Node[], edges: Edge[]) => {
|
||||
const g = new dagre.graphlib.Graph();
|
||||
g.setGraph(layoutOptions);
|
||||
g.setDefaultEdgeLabel(() => ({}));
|
||||
|
||||
|
||||
// Add nodes to graph
|
||||
nodes.forEach(node => {
|
||||
nodes.forEach((node) => {
|
||||
g.setNode(node.id, { width: 200, height: 100 });
|
||||
});
|
||||
|
||||
|
||||
// Add edges to graph
|
||||
edges.forEach(edge => {
|
||||
edges.forEach((edge) => {
|
||||
g.setEdge(edge.source, edge.target);
|
||||
});
|
||||
|
||||
|
||||
// Calculate layout
|
||||
dagre.layout(g);
|
||||
|
||||
|
||||
// Apply positions
|
||||
return nodes.map(node => ({
|
||||
return nodes.map((node) => ({
|
||||
...node,
|
||||
position: {
|
||||
x: g.node(node.id).x - 100,
|
||||
@@ -270,10 +281,7 @@ const applyLayout = (nodes: Node[], edges: Edge[]) => {
|
||||
};
|
||||
|
||||
// Debounce layout calculations
|
||||
const debouncedLayout = useMemo(
|
||||
() => debounce(applyLayout, 150),
|
||||
[]
|
||||
);
|
||||
const debouncedLayout = useMemo(() => debounce(applyLayout, 150), []);
|
||||
```
|
||||
|
||||
### Focus Mode
|
||||
@@ -281,34 +289,38 @@ const debouncedLayout = useMemo(
|
||||
Isolate selected nodes and their direct connections:
|
||||
|
||||
```typescript
|
||||
const useFocusMode = (selectedNodeId: string, allNodes: Node[], allEdges: Edge[]) => {
|
||||
const useFocusMode = (
|
||||
selectedNodeId: string,
|
||||
allNodes: Node[],
|
||||
allEdges: Edge[],
|
||||
) => {
|
||||
return useMemo(() => {
|
||||
if (!selectedNodeId) return { nodes: allNodes, edges: allEdges };
|
||||
|
||||
|
||||
// Get direct connections
|
||||
const connectedNodeIds = new Set([selectedNodeId]);
|
||||
const focusedEdges: Edge[] = [];
|
||||
|
||||
allEdges.forEach(edge => {
|
||||
|
||||
allEdges.forEach((edge) => {
|
||||
if (edge.source === selectedNodeId || edge.target === selectedNodeId) {
|
||||
focusedEdges.push(edge);
|
||||
connectedNodeIds.add(edge.source);
|
||||
connectedNodeIds.add(edge.target);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Get connected nodes
|
||||
const focusedNodes = allNodes.filter(n => connectedNodeIds.has(n.id));
|
||||
|
||||
const focusedNodes = allNodes.filter((n) => connectedNodeIds.has(n.id));
|
||||
|
||||
return { nodes: focusedNodes, edges: focusedEdges };
|
||||
}, [selectedNodeId, allNodes, allEdges]);
|
||||
};
|
||||
|
||||
// Smooth transitions for focus mode
|
||||
const focusModeStyles = {
|
||||
transition: 'all 0.3s ease-in-out',
|
||||
transition: "all 0.3s ease-in-out",
|
||||
opacity: isInFocus ? 1 : 0.3,
|
||||
filter: isInFocus ? 'none' : 'blur(2px)',
|
||||
filter: isInFocus ? "none" : "blur(2px)",
|
||||
};
|
||||
```
|
||||
|
||||
@@ -317,27 +329,25 @@ const focusModeStyles = {
|
||||
Search and navigate to specific nodes:
|
||||
|
||||
```typescript
|
||||
const searchNodes = useCallback((
|
||||
nodes: Node[],
|
||||
query: string
|
||||
) => {
|
||||
const searchNodes = useCallback((nodes: Node[], query: string) => {
|
||||
if (!query.trim()) return [];
|
||||
|
||||
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return nodes.filter(node =>
|
||||
node.data.label.toLowerCase().includes(lowerQuery) ||
|
||||
node.data.description?.toLowerCase().includes(lowerQuery)
|
||||
return nodes.filter(
|
||||
(node) =>
|
||||
node.data.label.toLowerCase().includes(lowerQuery) ||
|
||||
node.data.description?.toLowerCase().includes(lowerQuery),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const navigateToSearchResult = (nodeId: string) => {
|
||||
// Expand parent nodes
|
||||
const nodePath = calculateBreadcrumbPath(nodeId, allNodes);
|
||||
const parentIds = nodePath.slice(0, -1).map(n => n.id);
|
||||
|
||||
setExpandedIds(prev => new Set([...prev, ...parentIds]));
|
||||
const parentIds = nodePath.slice(0, -1).map((n) => n.id);
|
||||
|
||||
setExpandedIds((prev) => new Set([...prev, ...parentIds]));
|
||||
setSelectedNodeId(nodeId);
|
||||
|
||||
|
||||
// Fit view to node
|
||||
fitView({ nodes: [{ id: nodeId }], duration: 800 });
|
||||
};
|
||||
@@ -359,16 +369,16 @@ class GraphAnalyzer {
|
||||
edgeCount: this.countEdges(content),
|
||||
renderTime: this.estimateRenderTime(content),
|
||||
memoryUsage: this.estimateMemoryUsage(content),
|
||||
complexity: this.calculateComplexity(content)
|
||||
complexity: this.calculateComplexity(content),
|
||||
},
|
||||
issues: [],
|
||||
optimizations: [],
|
||||
patterns: this.detectPatterns(content)
|
||||
patterns: this.detectPatterns(content),
|
||||
};
|
||||
|
||||
// Detect performance issues
|
||||
this.detectPerformanceIssues(analysis);
|
||||
|
||||
|
||||
// Suggest optimizations
|
||||
this.suggestOptimizations(analysis);
|
||||
|
||||
@@ -378,14 +388,14 @@ class GraphAnalyzer {
|
||||
countNodes(content) {
|
||||
const nodePatterns = [
|
||||
/nodes:\s*\[.*?\]/gs,
|
||||
/const\s+\w+\s*=\s*\[.*?id:.*?position:/gs
|
||||
/const\s+\w+\s*=\s*\[.*?id:.*?position:/gs,
|
||||
];
|
||||
|
||||
let totalCount = 0;
|
||||
nodePatterns.forEach(pattern => {
|
||||
nodePatterns.forEach((pattern) => {
|
||||
const matches = content.match(pattern);
|
||||
if (matches) {
|
||||
matches.forEach(match => {
|
||||
matches.forEach((match) => {
|
||||
const nodeMatches = match.match(/id:\s*['"`][^'"`]+['"`]/g);
|
||||
if (nodeMatches) {
|
||||
totalCount += nodeMatches.length;
|
||||
@@ -400,12 +410,12 @@ class GraphAnalyzer {
|
||||
estimateRenderTime(content) {
|
||||
const nodeCount = this.countNodes(content);
|
||||
const edgeCount = this.countEdges(content);
|
||||
|
||||
|
||||
// Base render time estimation (ms)
|
||||
const baseTime = 5;
|
||||
const nodeTime = nodeCount * 0.1;
|
||||
const edgeTime = edgeCount * 0.05;
|
||||
|
||||
|
||||
return baseTime + nodeTime + edgeTime;
|
||||
}
|
||||
|
||||
@@ -414,19 +424,19 @@ class GraphAnalyzer {
|
||||
|
||||
if (metrics.nodeCount > 500) {
|
||||
analysis.issues.push({
|
||||
type: 'HIGH_NODE_COUNT',
|
||||
severity: 'high',
|
||||
type: "HIGH_NODE_COUNT",
|
||||
severity: "high",
|
||||
message: `Too many nodes (${metrics.nodeCount}). Consider virtualization.`,
|
||||
suggestion: 'Implement virtualization or reduce visible nodes'
|
||||
suggestion: "Implement virtualization or reduce visible nodes",
|
||||
});
|
||||
}
|
||||
|
||||
if (metrics.renderTime > 16) {
|
||||
analysis.issues.push({
|
||||
type: 'SLOW_RENDER',
|
||||
severity: 'high',
|
||||
type: "SLOW_RENDER",
|
||||
severity: "high",
|
||||
message: `Render time (${metrics.renderTime.toFixed(2)}ms) exceeds 60fps.`,
|
||||
suggestion: 'Optimize with memoization and incremental rendering'
|
||||
suggestion: "Optimize with memoization and incremental rendering",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -447,8 +457,9 @@ class GraphAnalyzer {
|
||||
|
||||
```typescript
|
||||
// Use Map for O(1) lookups instead of array.find
|
||||
const nodesById = useMemo(() =>
|
||||
new Map(allNodes.map(n => [n.id, n])), [allNodes]
|
||||
const nodesById = useMemo(
|
||||
() => new Map(allNodes.map((n) => [n.id, n])),
|
||||
[allNodes],
|
||||
);
|
||||
|
||||
// Cache layout results
|
||||
@@ -552,14 +563,14 @@ export default function InteractiveGraph() {
|
||||
const debouncedLayout = useMemo(
|
||||
() => debounce((nodes: Node[], edges: Edge[]) => {
|
||||
const cacheKey = generateLayoutCacheKey(nodes, edges);
|
||||
|
||||
|
||||
if (layoutCacheRef.current.has(cacheKey)) {
|
||||
return layoutCacheRef.current.get(cacheKey)!;
|
||||
}
|
||||
|
||||
const layouted = applyDagreLayout(nodes, edges);
|
||||
layoutCacheRef.current.set(cacheKey, layouted);
|
||||
|
||||
|
||||
return layouted;
|
||||
}, 150),
|
||||
[]
|
||||
|
||||
Reference in New Issue
Block a user