feat: Add multi-canvas system with project canvas support

- Add project_canvas and canvas_components tables for persistent canvas storage
- Add ProjectCanvas store with full CRUD operations
- Add ProjectCanvasPage for rendering saved canvas with components
- Add ProjectsPage for managing canvas list (create, clone, delete)
- Add HomePage that loads default canvas or falls back to dynamic canvas
- Add toolbar support for displaying canvas as pages with custom icons
- Add component usage validation to prevent deletion of components in use
- Add MCP tools for canvas management (list, create, update, delete, clone)
- Update router with /canvas/:id and /projects routes
- Update Toolbar to show dynamic canvas pages from database
This commit is contained in:
2026-02-13 06:32:46 -06:00
parent 2e64dceb1e
commit 8a017db777
13 changed files with 2016 additions and 13 deletions

View File

@@ -146,9 +146,13 @@ export const componentsApi = {
return res.json()
},
async delete(id: string): Promise<{ success: boolean }> {
async delete(id: string): Promise<{ success: boolean; error?: string; usedBy?: { id: string; name: string }[] }> {
const res = await fetch(`${API_URL}/api/components/${id}`, { method: 'DELETE' })
return res.json()
const data = await res.json()
if (!res.ok) {
return { success: false, error: data.error || data.message, usedBy: data.usedBy }
}
return data
},
async deleteAll(): Promise<{ success: boolean }> {

View File

@@ -19,8 +19,13 @@ import {
setRouter,
GLOBAL_TOOLS
} from './tools/globalTools'
import {
registerProjectCanvasTools,
unregisterProjectCanvasTools,
PROJECT_CANVAS_TOOLS
} from './tools/projectCanvasTools'
type PageName = 'canvas' | 'components' | 'themes'
type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas'
interface PageToolSet {
register: () => void
@@ -29,10 +34,23 @@ interface PageToolSet {
}
const pageTools: Record<PageName, PageToolSet> = {
home: {
register: () => {
registerCanvasTools()
registerComponentTools()
registerProjectCanvasTools()
},
unregister: () => {
unregisterCanvasTools()
unregisterComponentTools()
unregisterProjectCanvasTools()
},
toolNames: [...CANVAS_TOOLS, ...COMPONENT_TOOLS, ...PROJECT_CANVAS_TOOLS]
},
canvas: {
register: () => {
registerCanvasTools()
registerComponentTools() // Canvas también puede guardar/cargar
registerComponentTools()
},
unregister: () => {
unregisterCanvasTools()
@@ -40,6 +58,24 @@ const pageTools: Record<PageName, PageToolSet> = {
},
toolNames: [...CANVAS_TOOLS, ...COMPONENT_TOOLS]
},
'project-canvas': {
register: () => {
registerCanvasTools()
registerComponentTools()
registerProjectCanvasTools()
},
unregister: () => {
unregisterCanvasTools()
unregisterComponentTools()
unregisterProjectCanvasTools()
},
toolNames: [...CANVAS_TOOLS, ...COMPONENT_TOOLS, ...PROJECT_CANVAS_TOOLS]
},
projects: {
register: registerProjectCanvasTools,
unregister: unregisterProjectCanvasTools,
toolNames: PROJECT_CANVAS_TOOLS
},
components: {
register: registerComponentTools,
unregister: unregisterComponentTools,

View File

@@ -0,0 +1,246 @@
import { registerTool, unregisterTools } from '../webmcp'
import { useProjectCanvasStore } from '../../stores/projectCanvas'
export const PROJECT_CANVAS_TOOLS = [
'list_canvases',
'create_canvas',
'get_canvas',
'update_canvas',
'delete_canvas',
'clone_canvas',
'add_component_to_canvas',
'remove_component_from_canvas',
'get_canvas_components'
]
export function registerProjectCanvasTools() {
const store = useProjectCanvasStore()
// list_canvases
registerTool(
'list_canvases',
'Lista todos los canvas disponibles (proyectos, sistema)',
{
type: 'object',
properties: {
type: {
type: 'string',
enum: ['all', 'project', 'system'],
description: 'Filtrar por tipo de canvas'
}
}
},
async (args: { type?: string }) => {
await store.fetchCanvases()
let canvases = store.canvases
if (args.type === 'project') {
canvases = store.projectCanvases
} else if (args.type === 'system') {
canvases = store.systemCanvases
}
return JSON.stringify(canvases.map(c => ({
id: c.id,
name: c.name,
type: c.type,
description: c.description,
is_system: c.is_system
})), null, 2)
}
)
// create_canvas
registerTool(
'create_canvas',
'Crea un nuevo project canvas',
{
type: 'object',
properties: {
name: { type: 'string', description: 'Nombre del canvas' },
description: { type: 'string', description: 'Descripcion del canvas' },
theme_id: { type: 'string', description: 'ID del tema a usar (opcional)' },
config: {
type: 'object',
description: 'Configuracion del canvas (layout, settings, permissions)'
}
},
required: ['name']
},
async (args: { name: string; description?: string; theme_id?: string; config?: object }) => {
const id = await store.createCanvas({
name: args.name,
description: args.description,
theme_id: args.theme_id,
config: args.config as any,
type: 'project'
})
if (id) {
return `Canvas "${args.name}" creado con ID: ${id}`
}
return `Error al crear canvas: ${store.error}`
}
)
// get_canvas
registerTool(
'get_canvas',
'Obtiene los detalles de un canvas por ID',
{
type: 'object',
properties: {
id: { type: 'string', description: 'ID del canvas' }
},
required: ['id']
},
async (args: { id: string }) => {
const canvas = await store.fetchCanvasById(args.id)
if (!canvas) {
return `Canvas con ID "${args.id}" no encontrado`
}
return JSON.stringify(canvas, null, 2)
}
)
// update_canvas
registerTool(
'update_canvas',
'Actualiza un canvas existente',
{
type: 'object',
properties: {
id: { type: 'string', description: 'ID del canvas a actualizar' },
name: { type: 'string', description: 'Nuevo nombre' },
description: { type: 'string', description: 'Nueva descripcion' },
theme_id: { type: 'string', description: 'Nuevo tema' },
config: { type: 'object', description: 'Nueva configuracion' }
},
required: ['id']
},
async (args: { id: string; name?: string; description?: string; theme_id?: string; config?: object }) => {
const { id, ...data } = args
const success = await store.updateCanvas(id, data as any)
if (success) {
return `Canvas "${id}" actualizado`
}
return `Error al actualizar canvas: ${store.error}`
}
)
// delete_canvas
registerTool(
'delete_canvas',
'Elimina un canvas (no se pueden eliminar canvas del sistema)',
{
type: 'object',
properties: {
id: { type: 'string', description: 'ID del canvas a eliminar' }
},
required: ['id']
},
async (args: { id: string }) => {
const success = await store.deleteCanvas(args.id)
if (success) {
return `Canvas "${args.id}" eliminado`
}
return `Error al eliminar canvas: ${store.error}`
}
)
// clone_canvas
registerTool(
'clone_canvas',
'Clona un canvas existente (incluyendo sus componentes)',
{
type: 'object',
properties: {
id: { type: 'string', description: 'ID del canvas a clonar' },
name: { type: 'string', description: 'Nombre para el nuevo canvas (opcional)' }
},
required: ['id']
},
async (args: { id: string; name?: string }) => {
const newId = await store.cloneCanvas(args.id, args.name)
if (newId) {
return `Canvas clonado con nuevo ID: ${newId}`
}
return `Error al clonar canvas: ${store.error}`
}
)
// add_component_to_canvas
registerTool(
'add_component_to_canvas',
'Agrega un componente guardado a un canvas',
{
type: 'object',
properties: {
canvas_id: { type: 'string', description: 'ID del canvas' },
component_id: { type: 'string', description: 'ID del componente a agregar' },
props: { type: 'object', description: 'Props para el componente en este canvas' },
position: { type: 'number', description: 'Posicion del componente (orden de renderizado)' }
},
required: ['canvas_id', 'component_id']
},
async (args: { canvas_id: string; component_id: string; props?: object; position?: number }) => {
const success = await store.addComponentToCanvas(
args.canvas_id,
args.component_id,
args.props as Record<string, any>,
args.position
)
if (success) {
return `Componente "${args.component_id}" agregado al canvas "${args.canvas_id}"`
}
return 'Error al agregar componente al canvas'
}
)
// remove_component_from_canvas
registerTool(
'remove_component_from_canvas',
'Remueve un componente de un canvas',
{
type: 'object',
properties: {
canvas_id: { type: 'string', description: 'ID del canvas' },
component_id: { type: 'string', description: 'ID del componente a remover' }
},
required: ['canvas_id', 'component_id']
},
async (args: { canvas_id: string; component_id: string }) => {
const success = await store.removeComponentFromCanvas(args.canvas_id, args.component_id)
if (success) {
return `Componente "${args.component_id}" removido del canvas "${args.canvas_id}"`
}
return 'Error al remover componente del canvas'
}
)
// get_canvas_components
registerTool(
'get_canvas_components',
'Obtiene los componentes de un canvas con sus definiciones',
{
type: 'object',
properties: {
canvas_id: { type: 'string', description: 'ID del canvas' }
},
required: ['canvas_id']
},
async (args: { canvas_id: string }) => {
await store.fetchCanvasComponents(args.canvas_id)
return JSON.stringify(store.activeCanvasComponents, null, 2)
}
)
}
export function unregisterProjectCanvasTools() {
unregisterTools(PROJECT_CANVAS_TOOLS)
}