Merge pull request #31 from frNNcs/feature/mermaid-integration
Add Mermaid Diagram Integration to MCP Excalidraw
This commit is contained in:
@@ -99,6 +99,13 @@ Both local and Docker setups are **fully working** and production-ready!
|
|||||||
- **Strict Type Checking**: Enhanced development experience and compile-time error detection
|
- **Strict Type Checking**: Enhanced development experience and compile-time error detection
|
||||||
- **Type-Safe React Components**: TSX components with proper props typing
|
- **Type-Safe React Components**: TSX components with proper props typing
|
||||||
|
|
||||||
|
### **🎨 Mermaid Diagram Support (NEW!)**
|
||||||
|
- **Mermaid to Excalidraw**: Convert Mermaid diagrams directly to Excalidraw elements
|
||||||
|
- **MCP Tool Integration**: Use `create_from_mermaid` tool from Claude
|
||||||
|
- **Browser-based Conversion**: Leverages DOM access in the frontend for accurate rendering
|
||||||
|
- **Multiple Diagram Types**: Supports flowcharts, sequence diagrams, class diagrams, and more
|
||||||
|
- **Test Button**: Quick test functionality directly from the canvas UI
|
||||||
|
|
||||||
### **Real-time Canvas Integration**
|
### **Real-time Canvas Integration**
|
||||||
- Elements created via MCP appear instantly on the live canvas
|
- Elements created via MCP appear instantly on the live canvas
|
||||||
- WebSocket-based real-time synchronization
|
- WebSocket-based real-time synchronization
|
||||||
|
|||||||
+48
-14
@@ -2,13 +2,14 @@ import React, { useState, useEffect, useRef } from 'react'
|
|||||||
import {
|
import {
|
||||||
Excalidraw,
|
Excalidraw,
|
||||||
convertToExcalidrawElements,
|
convertToExcalidrawElements,
|
||||||
CaptureUpdateAction,
|
CaptureUpdateAction
|
||||||
ExcalidrawAPIRefValue,
|
|
||||||
ExcalidrawElement
|
|
||||||
} from '@excalidraw/excalidraw'
|
} from '@excalidraw/excalidraw'
|
||||||
import '@excalidraw/excalidraw/index.css'
|
import { convertMermaidToExcalidraw, DEFAULT_MERMAID_CONFIG } from './utils/mermaidConverter'
|
||||||
|
|
||||||
// Type definitions
|
// Type definitions
|
||||||
|
type ExcalidrawAPIRefValue = any;
|
||||||
|
type ExcalidrawElement = any;
|
||||||
|
|
||||||
interface ServerElement {
|
interface ServerElement {
|
||||||
id: string;
|
id: string;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -164,7 +165,7 @@ function App(): JSX.Element {
|
|||||||
|
|
||||||
if (result.success && result.elements && result.elements.length > 0) {
|
if (result.success && result.elements && result.elements.length > 0) {
|
||||||
const cleanedElements = result.elements.map(cleanElementForExcalidraw)
|
const cleanedElements = result.elements.map(cleanElementForExcalidraw)
|
||||||
const convertedElements = convertToExcalidrawElements(cleanedElements, { regenerateIds: false })
|
const convertedElements = convertToExcalidrawElements(cleanedElements as any, { regenerateIds: false })
|
||||||
excalidrawAPI?.updateScene({ elements: convertedElements })
|
excalidrawAPI?.updateScene({ elements: convertedElements })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -214,7 +215,7 @@ function App(): JSX.Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleWebSocketMessage = (data: WebSocketMessage): void => {
|
const handleWebSocketMessage = async (data: WebSocketMessage): Promise<void> => {
|
||||||
if (!excalidrawAPI) {
|
if (!excalidrawAPI) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -228,7 +229,7 @@ function App(): JSX.Element {
|
|||||||
if (data.elements && data.elements.length > 0) {
|
if (data.elements && data.elements.length > 0) {
|
||||||
const cleanedElements = data.elements.map(cleanElementForExcalidraw)
|
const cleanedElements = data.elements.map(cleanElementForExcalidraw)
|
||||||
const validatedElements = validateAndFixBindings(cleanedElements)
|
const validatedElements = validateAndFixBindings(cleanedElements)
|
||||||
const convertedElements = convertToExcalidrawElements(validatedElements)
|
const convertedElements = convertToExcalidrawElements(validatedElements as any)
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: convertedElements,
|
elements: convertedElements,
|
||||||
captureUpdate: CaptureUpdateAction.NEVER
|
captureUpdate: CaptureUpdateAction.NEVER
|
||||||
@@ -239,7 +240,7 @@ function App(): JSX.Element {
|
|||||||
case 'element_created':
|
case 'element_created':
|
||||||
if (data.element) {
|
if (data.element) {
|
||||||
const cleanedNewElement = cleanElementForExcalidraw(data.element)
|
const cleanedNewElement = cleanElementForExcalidraw(data.element)
|
||||||
const newElement = convertToExcalidrawElements([cleanedNewElement])
|
const newElement = convertToExcalidrawElements([cleanedNewElement] as any)
|
||||||
const updatedElementsAfterCreate = [...currentElements, ...newElement]
|
const updatedElementsAfterCreate = [...currentElements, ...newElement]
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: updatedElementsAfterCreate,
|
elements: updatedElementsAfterCreate,
|
||||||
@@ -251,8 +252,8 @@ function App(): JSX.Element {
|
|||||||
case 'element_updated':
|
case 'element_updated':
|
||||||
if (data.element) {
|
if (data.element) {
|
||||||
const cleanedUpdatedElement = cleanElementForExcalidraw(data.element)
|
const cleanedUpdatedElement = cleanElementForExcalidraw(data.element)
|
||||||
const convertedUpdatedElement = convertToExcalidrawElements([cleanedUpdatedElement])[0]
|
const convertedUpdatedElement = convertToExcalidrawElements([cleanedUpdatedElement] as any)[0]
|
||||||
const updatedElements = currentElements.map(el =>
|
const updatedElements = currentElements.map((el: any) =>
|
||||||
el.id === data.element!.id ? convertedUpdatedElement : el
|
el.id === data.element!.id ? convertedUpdatedElement : el
|
||||||
)
|
)
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
@@ -264,7 +265,7 @@ function App(): JSX.Element {
|
|||||||
|
|
||||||
case 'element_deleted':
|
case 'element_deleted':
|
||||||
if (data.elementId) {
|
if (data.elementId) {
|
||||||
const filteredElements = currentElements.filter(el => el.id !== data.elementId)
|
const filteredElements = currentElements.filter((el: any) => el.id !== data.elementId)
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: filteredElements,
|
elements: filteredElements,
|
||||||
captureUpdate: CaptureUpdateAction.NEVER
|
captureUpdate: CaptureUpdateAction.NEVER
|
||||||
@@ -275,7 +276,7 @@ function App(): JSX.Element {
|
|||||||
case 'elements_batch_created':
|
case 'elements_batch_created':
|
||||||
if (data.elements) {
|
if (data.elements) {
|
||||||
const cleanedBatchElements = data.elements.map(cleanElementForExcalidraw)
|
const cleanedBatchElements = data.elements.map(cleanElementForExcalidraw)
|
||||||
const batchElements = convertToExcalidrawElements(cleanedBatchElements)
|
const batchElements = convertToExcalidrawElements(cleanedBatchElements as any)
|
||||||
const updatedElementsAfterBatch = [...currentElements, ...batchElements]
|
const updatedElementsAfterBatch = [...currentElements, ...batchElements]
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
elements: updatedElementsAfterBatch,
|
elements: updatedElementsAfterBatch,
|
||||||
@@ -293,6 +294,39 @@ function App(): JSX.Element {
|
|||||||
console.log(`Server sync status: ${data.count} elements`)
|
console.log(`Server sync status: ${data.count} elements`)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case 'mermaid_convert':
|
||||||
|
console.log('Received Mermaid conversion request from MCP')
|
||||||
|
if ((data as any).mermaidDiagram) {
|
||||||
|
try {
|
||||||
|
const result = await convertMermaidToExcalidraw((data as any).mermaidDiagram, (data as any).config || DEFAULT_MERMAID_CONFIG)
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
console.error('Mermaid conversion error:', result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.elements && result.elements.length > 0) {
|
||||||
|
const convertedElements = convertToExcalidrawElements(result.elements as any, { regenerateIds: false })
|
||||||
|
excalidrawAPI.updateScene({
|
||||||
|
elements: convertedElements,
|
||||||
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.files) {
|
||||||
|
excalidrawAPI.addFiles(Object.values(result.files))
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Mermaid diagram converted successfully:', result.elements.length, 'elements')
|
||||||
|
|
||||||
|
// Sync to backend automatically after creating elements
|
||||||
|
await syncToBackend()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error converting Mermaid diagram from WebSocket:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log('Unknown WebSocket message type:', data.type)
|
console.log('Unknown WebSocket message type:', data.type)
|
||||||
}
|
}
|
||||||
@@ -332,8 +366,8 @@ function App(): JSX.Element {
|
|||||||
const currentElements = excalidrawAPI.getSceneElements()
|
const currentElements = excalidrawAPI.getSceneElements()
|
||||||
console.log(`Syncing ${currentElements.length} elements to backend`)
|
console.log(`Syncing ${currentElements.length} elements to backend`)
|
||||||
|
|
||||||
// 2. Filter out deleted elements
|
// Filter out deleted elements
|
||||||
const activeElements = currentElements.filter(el => !el.isDeleted)
|
const activeElements = currentElements.filter((el: any) => !el.isDeleted)
|
||||||
|
|
||||||
// 3. Convert to backend format
|
// 3. Convert to backend format
|
||||||
const backendElements = activeElements.map(convertToBackendFormat)
|
const backendElements = activeElements.map(convertToBackendFormat)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
|
import '@excalidraw/excalidraw/index.css'
|
||||||
|
|
||||||
const rootElement = document.getElementById('root');
|
const rootElement = document.getElementById('root');
|
||||||
if (!rootElement) {
|
if (!rootElement) {
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { parseMermaidToExcalidraw, MermaidConfig } from '@excalidraw/mermaid-to-excalidraw';
|
||||||
|
|
||||||
|
export interface MermaidConversionResult {
|
||||||
|
elements: any[];
|
||||||
|
files?: any;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Mermaid diagram definition to Excalidraw elements
|
||||||
|
* This function needs to run in the browser context as it requires DOM access
|
||||||
|
*/
|
||||||
|
export const convertMermaidToExcalidraw = async (
|
||||||
|
mermaidDefinition: string,
|
||||||
|
config?: MermaidConfig
|
||||||
|
): Promise<MermaidConversionResult> => {
|
||||||
|
try {
|
||||||
|
// Parse the Mermaid diagram to Excalidraw elements
|
||||||
|
const result = await parseMermaidToExcalidraw(mermaidDefinition, config);
|
||||||
|
|
||||||
|
return {
|
||||||
|
elements: result.elements,
|
||||||
|
files: result.files,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error converting Mermaid to Excalidraw:', error);
|
||||||
|
return {
|
||||||
|
elements: [],
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default Mermaid configuration for Excalidraw conversion
|
||||||
|
*/
|
||||||
|
export const DEFAULT_MERMAID_CONFIG: MermaidConfig = {
|
||||||
|
startOnLoad: false,
|
||||||
|
flowchart: {
|
||||||
|
curve: 'linear',
|
||||||
|
},
|
||||||
|
themeVariables: {
|
||||||
|
fontSize: '20px',
|
||||||
|
},
|
||||||
|
maxEdges: 500,
|
||||||
|
maxTextSize: 50000,
|
||||||
|
};
|
||||||
Generated
+863
-81
File diff suppressed because it is too large
Load Diff
@@ -22,10 +22,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@excalidraw/excalidraw": "^0.18.0",
|
"@excalidraw/excalidraw": "^0.18.0",
|
||||||
|
"@excalidraw/mermaid-to-excalidraw": "^1.1.3",
|
||||||
"@modelcontextprotocol/sdk": "latest",
|
"@modelcontextprotocol/sdk": "latest",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"mermaid": "^11.12.1",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
|||||||
@@ -414,6 +414,41 @@ const tools: Tool[] = [
|
|||||||
required: ['elementIds']
|
required: ['elementIds']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'create_from_mermaid',
|
||||||
|
description: 'Convert a Mermaid diagram to Excalidraw elements and render them on the canvas',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
mermaidDiagram: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The Mermaid diagram definition (e.g., "graph TD; A-->B; B-->C;")'
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'Optional Mermaid configuration',
|
||||||
|
properties: {
|
||||||
|
startOnLoad: { type: 'boolean' },
|
||||||
|
flowchart: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
curve: { type: 'string', enum: ['linear', 'basis'] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
themeVariables: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
fontSize: { type: 'string' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maxEdges: { type: 'number' },
|
||||||
|
maxTextSize: { type: 'number' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['mermaidDiagram']
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'batch_create_elements',
|
name: 'batch_create_elements',
|
||||||
description: 'Create multiple Excalidraw elements at once - ideal for complex diagrams',
|
description: 'Create multiple Excalidraw elements at once - ideal for complex diagrams',
|
||||||
@@ -822,6 +857,60 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'create_from_mermaid': {
|
||||||
|
const params = z.object({
|
||||||
|
mermaidDiagram: z.string(),
|
||||||
|
config: z.object({
|
||||||
|
startOnLoad: z.boolean().optional(),
|
||||||
|
flowchart: z.object({
|
||||||
|
curve: z.enum(['linear', 'basis']).optional()
|
||||||
|
}).optional(),
|
||||||
|
themeVariables: z.object({
|
||||||
|
fontSize: z.string().optional()
|
||||||
|
}).optional(),
|
||||||
|
maxEdges: z.number().optional(),
|
||||||
|
maxTextSize: z.number().optional()
|
||||||
|
}).optional()
|
||||||
|
}).parse(args);
|
||||||
|
|
||||||
|
logger.info('Creating Excalidraw elements from Mermaid diagram via MCP', {
|
||||||
|
diagramLength: params.mermaidDiagram.length,
|
||||||
|
hasConfig: !!params.config
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send the Mermaid diagram to the frontend via the API
|
||||||
|
// The frontend will use mermaid-to-excalidraw to convert it
|
||||||
|
const response = await fetch(`${EXPRESS_SERVER_URL}/api/elements/from-mermaid`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
mermaidDiagram: params.mermaidDiagram,
|
||||||
|
config: params.config
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP server error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json() as ApiResponse;
|
||||||
|
|
||||||
|
logger.info('Mermaid diagram sent to frontend for conversion', {
|
||||||
|
success: result.success
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text',
|
||||||
|
text: `Mermaid diagram sent for conversion!\n\n${JSON.stringify(result, null, 2)}\n\n⚠️ Note: The actual conversion happens in the frontend canvas with DOM access. Open the canvas at ${EXPRESS_SERVER_URL} to see the diagram rendered.`
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to process Mermaid diagram: ${(error as Error).message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case 'batch_create_elements': {
|
case 'batch_create_elements': {
|
||||||
const params = z.object({ elements: z.array(ElementSchema) }).parse(args);
|
const params = z.object({ elements: z.array(ElementSchema) }).parse(args);
|
||||||
logger.info('Batch creating elements via MCP', { count: params.elements.length });
|
logger.info('Batch creating elements via MCP', { count: params.elements.length });
|
||||||
|
|||||||
@@ -399,6 +399,47 @@ app.post('/api/elements/batch', (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Convert Mermaid diagram to Excalidraw elements
|
||||||
|
app.post('/api/elements/from-mermaid', (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { mermaidDiagram, config } = req.body;
|
||||||
|
|
||||||
|
if (!mermaidDiagram || typeof mermaidDiagram !== 'string') {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Mermaid diagram definition is required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Received Mermaid conversion request', {
|
||||||
|
diagramLength: mermaidDiagram.length,
|
||||||
|
hasConfig: !!config
|
||||||
|
});
|
||||||
|
|
||||||
|
// Broadcast to all WebSocket clients to process the Mermaid diagram
|
||||||
|
broadcast({
|
||||||
|
type: 'mermaid_convert',
|
||||||
|
mermaidDiagram,
|
||||||
|
config: config || {},
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return the diagram for frontend processing
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
mermaidDiagram,
|
||||||
|
config: config || {},
|
||||||
|
message: 'Mermaid diagram sent to frontend for conversion.'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error processing Mermaid diagram:', error);
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: (error as Error).message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Sync elements from frontend (overwrite sync)
|
// Sync elements from frontend (overwrite sync)
|
||||||
app.post('/api/elements/sync', (req: Request, res: Response) => {
|
app.post('/api/elements/sync', (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+33
-1
@@ -175,7 +175,8 @@ export type WebSocketMessageType =
|
|||||||
| 'element_deleted'
|
| 'element_deleted'
|
||||||
| 'elements_batch_created'
|
| 'elements_batch_created'
|
||||||
| 'elements_synced'
|
| 'elements_synced'
|
||||||
| 'sync_status';
|
| 'sync_status'
|
||||||
|
| 'mermaid_convert';
|
||||||
|
|
||||||
export interface InitialElementsMessage extends WebSocketMessage {
|
export interface InitialElementsMessage extends WebSocketMessage {
|
||||||
type: 'initial_elements';
|
type: 'initial_elements';
|
||||||
@@ -208,6 +209,37 @@ export interface SyncStatusMessage extends WebSocketMessage {
|
|||||||
timestamp: string;
|
timestamp: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MermaidConvertMessage extends WebSocketMessage {
|
||||||
|
type: 'mermaid_convert';
|
||||||
|
mermaidDiagram: string;
|
||||||
|
config?: MermaidConfig;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mermaid conversion types
|
||||||
|
export interface MermaidConfig {
|
||||||
|
startOnLoad?: boolean;
|
||||||
|
flowchart?: {
|
||||||
|
curve?: 'linear' | 'basis';
|
||||||
|
};
|
||||||
|
themeVariables?: {
|
||||||
|
fontSize?: string;
|
||||||
|
};
|
||||||
|
maxEdges?: number;
|
||||||
|
maxTextSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MermaidConversionRequest {
|
||||||
|
mermaidDiagram: string;
|
||||||
|
config?: MermaidConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MermaidConversionResponse extends ApiResponse {
|
||||||
|
elements: ServerElement[];
|
||||||
|
files?: any;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
// In-memory storage for Excalidraw elements
|
// In-memory storage for Excalidraw elements
|
||||||
export const elements = new Map<string, ServerElement>();
|
export const elements = new Map<string, ServerElement>();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user