diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 129fad6..3afb748 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -10,6 +10,8 @@ import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './servi const route = useRoute() const router = useRouter() +type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' + onMounted(async () => { // Initialize WebMCP connection await initWebMCP() @@ -19,13 +21,13 @@ onMounted(async () => { // Initialize tools for current page (handles refresh) const currentPage = (route.name as string) || 'canvas' - initToolsOnRefresh(currentPage as 'canvas' | 'components' | 'themes') + initToolsOnRefresh(currentPage as PageName) }) // Watch for route changes and update tools watch(() => route.name, (newPage) => { if (newPage) { - activatePageTools(newPage as 'canvas' | 'components' | 'themes') + activatePageTools(newPage as PageName) } }) diff --git a/frontend/src/components/Toolbar.vue b/frontend/src/components/Toolbar.vue index 4a87a1e..a8dc22b 100644 --- a/frontend/src/components/Toolbar.vue +++ b/frontend/src/components/Toolbar.vue @@ -1,9 +1,12 @@ + + diff --git a/frontend/src/pages/ProjectCanvasPage.vue b/frontend/src/pages/ProjectCanvasPage.vue new file mode 100644 index 0000000..10d719a --- /dev/null +++ b/frontend/src/pages/ProjectCanvasPage.vue @@ -0,0 +1,314 @@ + + + + + diff --git a/frontend/src/pages/ProjectsPage.vue b/frontend/src/pages/ProjectsPage.vue new file mode 100644 index 0000000..d494a26 --- /dev/null +++ b/frontend/src/pages/ProjectsPage.vue @@ -0,0 +1,504 @@ + + + + + diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index b186eb7..3c4c960 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -5,9 +5,25 @@ const router = createRouter({ routes: [ { path: '/', + name: 'home', + component: () => import('../pages/HomePage.vue') + }, + { + path: '/dynamic-canvas', name: 'canvas', component: () => import('../pages/CanvasPage.vue') }, + { + path: '/canvas/:id', + name: 'project-canvas', + component: () => import('../pages/ProjectCanvasPage.vue'), + props: true + }, + { + path: '/projects', + name: 'projects', + component: () => import('../pages/ProjectsPage.vue') + }, { path: '/components', name: 'components', diff --git a/frontend/src/services/dynamicComponents.ts b/frontend/src/services/dynamicComponents.ts index 5805504..868b962 100644 --- a/frontend/src/services/dynamicComponents.ts +++ b/frontend/src/services/dynamicComponents.ts @@ -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 }> { diff --git a/frontend/src/services/toolRegistry.ts b/frontend/src/services/toolRegistry.ts index d1b55bf..03a703e 100644 --- a/frontend/src/services/toolRegistry.ts +++ b/frontend/src/services/toolRegistry.ts @@ -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 = { + 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 = { }, 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, diff --git a/frontend/src/services/tools/projectCanvasTools.ts b/frontend/src/services/tools/projectCanvasTools.ts new file mode 100644 index 0000000..d36ac8e --- /dev/null +++ b/frontend/src/services/tools/projectCanvasTools.ts @@ -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, + 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) +} diff --git a/frontend/src/stores/components.ts b/frontend/src/stores/components.ts index 5e06b6a..1a07747 100644 --- a/frontend/src/stores/components.ts +++ b/frontend/src/stores/components.ts @@ -57,14 +57,21 @@ export const useComponentsStore = defineStore('components', () => { return saveComponent(currentComponent.value) } - async function deleteComponent(id: string) { + async function deleteComponent(id: string): Promise<{ success: boolean; error?: string; usedBy?: { id: string; name: string }[] }> { try { - await componentsApi.delete(id) + const result = await componentsApi.delete(id) + if (!result.success && result.usedBy) { + return { + success: false, + error: result.error || 'Component in use', + usedBy: result.usedBy + } + } await fetchComponents() - return true + return { success: true } } catch (e) { console.error('Error deleting component:', e) - return false + return { success: false, error: e instanceof Error ? e.message : 'Unknown error' } } } diff --git a/frontend/src/stores/projectCanvas.ts b/frontend/src/stores/projectCanvas.ts new file mode 100644 index 0000000..b6cc8dc --- /dev/null +++ b/frontend/src/stores/projectCanvas.ts @@ -0,0 +1,327 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import type { ProjectCanvas, CanvasComponent, ComponentUsage } from '../types/canvas' + +const API_URL = 'http://localhost:4101' + +export const useProjectCanvasStore = defineStore('projectCanvas', () => { + // State + const canvases = ref([]) + const activeCanvas = ref(null) + const activeCanvasComponents = ref([]) + const loading = ref(false) + const saving = ref(false) + const error = ref(null) + + // State adicional + const toolbarCanvases = ref([]) + const defaultCanvas = ref(null) + + // Getters + const projectCanvases = computed(() => + canvases.value.filter(c => c.type === 'project') + ) + + const systemCanvases = computed(() => + canvases.value.filter(c => c.type === 'system') + ) + + const dynamicCanvas = computed(() => + canvases.value.find(c => c.type === 'dynamic') + ) + + const canvasCount = computed(() => canvases.value.length) + + const hasActiveCanvas = computed(() => activeCanvas.value !== null) + + const hasDefaultCanvas = computed(() => defaultCanvas.value !== null) + + // Actions + async function fetchCanvases() { + loading.value = true + error.value = null + try { + const res = await fetch(`${API_URL}/api/canvas`) + if (!res.ok) throw new Error('Failed to fetch canvases') + canvases.value = await res.json() + } catch (e) { + error.value = e instanceof Error ? e.message : 'Unknown error' + console.error('Error fetching canvases:', e) + } finally { + loading.value = false + } + } + + async function fetchToolbarCanvases() { + try { + const res = await fetch(`${API_URL}/api/canvas/toolbar`) + if (!res.ok) throw new Error('Failed to fetch toolbar canvases') + toolbarCanvases.value = await res.json() + } catch (e) { + console.error('Error fetching toolbar canvases:', e) + toolbarCanvases.value = [] + } + } + + async function fetchDefaultCanvas(): Promise { + try { + const res = await fetch(`${API_URL}/api/canvas/default`) + if (!res.ok) throw new Error('Failed to fetch default canvas') + const data = await res.json() + if (data.hasDefault) { + defaultCanvas.value = data.canvas + return data.canvas + } + defaultCanvas.value = null + return null + } catch (e) { + console.error('Error fetching default canvas:', e) + defaultCanvas.value = null + return null + } + } + + async function fetchCanvasById(id: string): Promise { + try { + const res = await fetch(`${API_URL}/api/canvas/${id}`) + if (!res.ok) { + if (res.status === 404) return null + throw new Error('Failed to fetch canvas') + } + return await res.json() + } catch (e) { + console.error('Error fetching canvas:', e) + return null + } + } + + async function createCanvas(data: Partial): Promise { + saving.value = true + error.value = null + try { + const res = await fetch(`${API_URL}/api/canvas`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }) + if (!res.ok) throw new Error('Failed to create canvas') + const result = await res.json() + await fetchCanvases() + return result.id + } catch (e) { + error.value = e instanceof Error ? e.message : 'Unknown error' + console.error('Error creating canvas:', e) + return null + } finally { + saving.value = false + } + } + + async function updateCanvas(id: string, data: Partial): Promise { + saving.value = true + error.value = null + try { + const res = await fetch(`${API_URL}/api/canvas/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }) + if (!res.ok) { + const errorData = await res.json() + throw new Error(errorData.error || 'Failed to update canvas') + } + await fetchCanvases() + // Update active canvas if it's the one being updated + if (activeCanvas.value?.id === id) { + activeCanvas.value = await fetchCanvasById(id) + } + return true + } catch (e) { + error.value = e instanceof Error ? e.message : 'Unknown error' + console.error('Error updating canvas:', e) + return false + } finally { + saving.value = false + } + } + + async function deleteCanvas(id: string): Promise { + error.value = null + try { + const res = await fetch(`${API_URL}/api/canvas/${id}`, { + method: 'DELETE' + }) + if (!res.ok) { + const errorData = await res.json() + throw new Error(errorData.error || 'Failed to delete canvas') + } + await fetchCanvases() + if (activeCanvas.value?.id === id) { + clearActiveCanvas() + } + return true + } catch (e) { + error.value = e instanceof Error ? e.message : 'Unknown error' + console.error('Error deleting canvas:', e) + return false + } + } + + async function cloneCanvas(id: string, newName?: string): Promise { + saving.value = true + error.value = null + try { + const res = await fetch(`${API_URL}/api/canvas/${id}/clone`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: newName }) + }) + if (!res.ok) throw new Error('Failed to clone canvas') + const result = await res.json() + await fetchCanvases() + return result.id + } catch (e) { + error.value = e instanceof Error ? e.message : 'Unknown error' + console.error('Error cloning canvas:', e) + return null + } finally { + saving.value = false + } + } + + // Canvas Components + async function fetchCanvasComponents(canvasId: string) { + loading.value = true + try { + const res = await fetch(`${API_URL}/api/canvas/${canvasId}/components`) + if (!res.ok) throw new Error('Failed to fetch canvas components') + activeCanvasComponents.value = await res.json() + } catch (e) { + console.error('Error fetching canvas components:', e) + activeCanvasComponents.value = [] + } finally { + loading.value = false + } + } + + async function addComponentToCanvas( + canvasId: string, + componentId: string, + props?: Record, + position?: number + ): Promise { + try { + const res = await fetch(`${API_URL}/api/canvas/${canvasId}/components`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ component_id: componentId, props, position }) + }) + if (!res.ok) throw new Error('Failed to add component to canvas') + await fetchCanvasComponents(canvasId) + return true + } catch (e) { + console.error('Error adding component to canvas:', e) + return false + } + } + + async function removeComponentFromCanvas(canvasId: string, componentId: string): Promise { + try { + const res = await fetch(`${API_URL}/api/canvas/${canvasId}/components/${componentId}`, { + method: 'DELETE' + }) + if (!res.ok) throw new Error('Failed to remove component from canvas') + await fetchCanvasComponents(canvasId) + return true + } catch (e) { + console.error('Error removing component from canvas:', e) + return false + } + } + + async function updateCanvasComponent( + canvasId: string, + componentId: string, + data: { position?: number; props?: Record; layout?: any; is_visible?: boolean } + ): Promise { + try { + const res = await fetch(`${API_URL}/api/canvas/${canvasId}/components/${componentId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }) + if (!res.ok) throw new Error('Failed to update canvas component') + await fetchCanvasComponents(canvasId) + return true + } catch (e) { + console.error('Error updating canvas component:', e) + return false + } + } + + // Component Usage + async function getComponentUsage(componentId: string): Promise { + try { + const res = await fetch(`${API_URL}/api/components/${componentId}/usage`) + if (!res.ok) throw new Error('Failed to get component usage') + return await res.json() + } catch (e) { + console.error('Error getting component usage:', e) + return null + } + } + + // Canvas Activation + function setActiveCanvas(canvas: ProjectCanvas) { + activeCanvas.value = canvas + } + + function clearActiveCanvas() { + activeCanvas.value = null + activeCanvasComponents.value = [] + } + + async function activateCanvas(id: string): Promise { + const canvas = await fetchCanvasById(id) + if (!canvas) return false + setActiveCanvas(canvas) + await fetchCanvasComponents(id) + return true + } + + return { + // State + canvases, + activeCanvas, + activeCanvasComponents, + toolbarCanvases, + defaultCanvas, + loading, + saving, + error, + // Getters + projectCanvases, + systemCanvases, + dynamicCanvas, + canvasCount, + hasActiveCanvas, + hasDefaultCanvas, + // Actions + fetchCanvases, + fetchToolbarCanvases, + fetchDefaultCanvas, + fetchCanvasById, + createCanvas, + updateCanvas, + deleteCanvas, + cloneCanvas, + fetchCanvasComponents, + addComponentToCanvas, + removeComponentFromCanvas, + updateCanvasComponent, + getComponentUsage, + setActiveCanvas, + clearActiveCanvas, + activateCanvas + } +}) diff --git a/frontend/src/types/canvas.ts b/frontend/src/types/canvas.ts new file mode 100644 index 0000000..5ece321 --- /dev/null +++ b/frontend/src/types/canvas.ts @@ -0,0 +1,71 @@ +export type CanvasType = 'dynamic' | 'project' | 'system' | 'external' + +export interface CanvasConfig { + layout?: 'free' | 'grid' | 'stack' + settings?: { + backgroundColor?: string + padding?: string + gridColumns?: number + gridGap?: string + } + permissions?: { + canAddComponents?: boolean + canRemoveComponents?: boolean + canEditTools?: boolean + canChangeTheme?: boolean + } +} + +export interface ProjectCanvas { + id: string + name: string + description?: string + type: CanvasType + theme_id?: string | null + config: CanvasConfig | null + tools: string[] + is_default: boolean + is_system: boolean + show_in_toolbar: boolean + toolbar_icon?: string | null + toolbar_order: number + created_at?: string + updated_at?: string +} + +export interface CanvasComponentLayout { + x: number + y: number + width: number + height: number +} + +export interface CanvasComponent { + id: number + canvasId: string + componentId: string + position: number + props: Record + layout: CanvasComponentLayout | null + isVisible: boolean + createdAt?: string + component?: { + id: string + name: string + template: string + setup?: string + style?: string + props?: string[] + imports?: string[] + } +} + +export interface ComponentUsage { + componentId: string + usedBy: Array<{ + id: string + name: string + type: CanvasType + }> + canDelete: boolean +} diff --git a/server/index.ts b/server/index.ts index 180d132..a1f3379 100644 --- a/server/index.ts +++ b/server/index.ts @@ -52,6 +52,50 @@ db.run(` ) `) +// Tabla para project canvas +db.run(` + CREATE TABLE IF NOT EXISTS project_canvas ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + type TEXT NOT NULL DEFAULT 'project', + theme_id TEXT, + config TEXT, + tools TEXT, + is_default INTEGER DEFAULT 0, + is_system INTEGER DEFAULT 0, + show_in_toolbar INTEGER DEFAULT 0, + toolbar_icon TEXT, + toolbar_order INTEGER DEFAULT 99, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP + ) +`) + +// Migrar tabla existente si falta la columna +try { + db.run(`ALTER TABLE project_canvas ADD COLUMN show_in_toolbar INTEGER DEFAULT 0`) + db.run(`ALTER TABLE project_canvas ADD COLUMN toolbar_icon TEXT`) + db.run(`ALTER TABLE project_canvas ADD COLUMN toolbar_order INTEGER DEFAULT 99`) +} catch (e) { + // Columnas ya existen +} + +// Tabla para relación canvas-componentes +db.run(` + CREATE TABLE IF NOT EXISTS canvas_components ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + canvas_id TEXT NOT NULL, + component_id TEXT NOT NULL, + position INTEGER DEFAULT 0, + props TEXT, + layout TEXT, + is_visible INTEGER DEFAULT 1, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + UNIQUE(canvas_id, component_id) + ) +`) + // Insertar temas del sistema si no existen const existingThemes = db.query('SELECT COUNT(*) as count FROM themes WHERE is_system = 1').get() as { count: number } if (existingThemes.count === 0) { @@ -277,7 +321,7 @@ Bun.serve({ } // Obtener componente por ID - if (url.pathname.startsWith('/api/components/')) { + if (url.pathname.startsWith('/api/components/') && !url.pathname.includes('/usage')) { const id = url.pathname.split('/').pop() if (req.method === 'GET') { @@ -289,6 +333,22 @@ Bun.serve({ } if (req.method === 'DELETE') { + // Verificar si el componente está en uso por algún canvas + const usage = db.query(` + SELECT pc.id, pc.name + FROM canvas_components cc + JOIN project_canvas pc ON cc.canvas_id = pc.id + WHERE cc.component_id = ? + `).all(id) as { id: string; name: string }[] + + if (usage.length > 0) { + return Response.json({ + error: 'Component in use', + message: `Cannot delete component. It is used by: ${usage.map(u => u.name).join(', ')}`, + usedBy: usage + }, { status: 409, headers: corsHeaders }) + } + db.run('DELETE FROM vue_components WHERE id = ?', [id]) return Response.json({ success: true }, { headers: corsHeaders }) } @@ -488,6 +548,319 @@ Bun.serve({ } } + // ===================== + // API de Canvas + // ===================== + + // GET /api/canvas/toolbar - Canvas para mostrar en toolbar + if (url.pathname === '/api/canvas/toolbar') { + if (req.method === 'GET') { + const rows = db.query('SELECT * FROM project_canvas WHERE show_in_toolbar = 1 ORDER BY toolbar_order ASC, name ASC').all() + const canvases = (rows as any[]).map(row => ({ + ...row, + is_default: !!row.is_default, + is_system: !!row.is_system, + show_in_toolbar: !!row.show_in_toolbar, + config: row.config ? JSON.parse(row.config) : null, + tools: row.tools ? JSON.parse(row.tools) : [] + })) + return Response.json(canvases, { headers: corsHeaders }) + } + } + + // GET /api/canvas/default - Canvas por defecto (para homepage) + if (url.pathname === '/api/canvas/default') { + if (req.method === 'GET') { + const row = db.query('SELECT * FROM project_canvas WHERE is_default = 1 LIMIT 1').get() as any + if (!row) { + return Response.json({ hasDefault: false }, { headers: corsHeaders }) + } + return Response.json({ + hasDefault: true, + canvas: { + ...row, + is_default: !!row.is_default, + is_system: !!row.is_system, + show_in_toolbar: !!row.show_in_toolbar, + config: row.config ? JSON.parse(row.config) : null, + tools: row.tools ? JSON.parse(row.tools) : [] + } + }, { headers: corsHeaders }) + } + } + + // GET /api/canvas - Lista todos los canvas + // POST /api/canvas - Crea un nuevo canvas + if (url.pathname === '/api/canvas') { + if (req.method === 'GET') { + const rows = db.query('SELECT * FROM project_canvas ORDER BY is_system DESC, is_default DESC, name ASC').all() + const canvases = (rows as any[]).map(row => ({ + ...row, + is_default: !!row.is_default, + is_system: !!row.is_system, + show_in_toolbar: !!row.show_in_toolbar, + config: row.config ? JSON.parse(row.config) : null, + tools: row.tools ? JSON.parse(row.tools) : [] + })) + return Response.json(canvases, { headers: corsHeaders }) + } + + if (req.method === 'POST') { + const body = await req.json() + const id = body.id || `canvas-${Date.now()}` + const stmt = db.prepare(` + INSERT OR REPLACE INTO project_canvas + (id, name, description, type, theme_id, config, tools, is_default, is_system, show_in_toolbar, toolbar_icon, toolbar_order, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + `) + stmt.run( + id, + body.name, + body.description || '', + body.type || 'project', + body.theme_id || null, + JSON.stringify(body.config || {}), + JSON.stringify(body.tools || []), + body.is_default ? 1 : 0, + body.is_system ? 1 : 0, + body.show_in_toolbar ? 1 : 0, + body.toolbar_icon || null, + body.toolbar_order ?? 99 + ) + return Response.json({ success: true, id }, { headers: corsHeaders }) + } + } + + // Operaciones sobre un canvas específico + if (url.pathname.startsWith('/api/canvas/') && !url.pathname.includes('/components')) { + const pathParts = url.pathname.split('/') + const id = pathParts[3] + const action = pathParts[4] + + // POST /api/canvas/:id/clone - Clonar canvas + if (action === 'clone' && req.method === 'POST') { + const original = db.query('SELECT * FROM project_canvas WHERE id = ?').get(id) as any + if (!original) { + return Response.json({ error: 'Canvas not found' }, { status: 404, headers: corsHeaders }) + } + + const body = await req.json() + const newId = `canvas-${Date.now()}` + const newName = body.name || `${original.name} (copia)` + + // Clonar el canvas + const stmt = db.prepare(` + INSERT INTO project_canvas + (id, name, description, type, theme_id, config, tools, is_default, is_system) + VALUES (?, ?, ?, 'project', ?, ?, ?, 0, 0) + `) + stmt.run(newId, newName, original.description, original.theme_id, original.config, original.tools) + + // Clonar los componentes del canvas + const components = db.query('SELECT * FROM canvas_components WHERE canvas_id = ?').all(id) as any[] + if (components.length > 0) { + const compStmt = db.prepare(` + INSERT INTO canvas_components (canvas_id, component_id, position, props, layout, is_visible) + VALUES (?, ?, ?, ?, ?, ?) + `) + for (const comp of components) { + compStmt.run(newId, comp.component_id, comp.position, comp.props, comp.layout, comp.is_visible) + } + } + + return Response.json({ success: true, id: newId }, { headers: corsHeaders }) + } + + // GET /api/canvas/:id - Obtener canvas + if (req.method === 'GET' && !action) { + const row = db.query('SELECT * FROM project_canvas WHERE id = ?').get(id) as any + if (!row) { + return Response.json({ error: 'Canvas not found' }, { status: 404, headers: corsHeaders }) + } + return Response.json({ + ...row, + is_default: !!row.is_default, + is_system: !!row.is_system, + show_in_toolbar: !!row.show_in_toolbar, + config: row.config ? JSON.parse(row.config) : null, + tools: row.tools ? JSON.parse(row.tools) : [] + }, { headers: corsHeaders }) + } + + // PUT /api/canvas/:id - Actualizar canvas + if (req.method === 'PUT' && !action) { + const canvas = db.query('SELECT * FROM project_canvas WHERE id = ?').get(id) as any + if (!canvas) { + return Response.json({ error: 'Canvas not found' }, { status: 404, headers: corsHeaders }) + } + + const body = await req.json() + const updates: string[] = [] + const values: any[] = [] + + // System canvas solo puede modificar toolbar settings y is_default + if (canvas.is_system) { + if (body.is_default !== undefined) { updates.push('is_default = ?'); values.push(body.is_default ? 1 : 0) } + if (body.show_in_toolbar !== undefined) { updates.push('show_in_toolbar = ?'); values.push(body.show_in_toolbar ? 1 : 0) } + if (body.toolbar_icon !== undefined) { updates.push('toolbar_icon = ?'); values.push(body.toolbar_icon) } + if (body.toolbar_order !== undefined) { updates.push('toolbar_order = ?'); values.push(body.toolbar_order) } + } else { + // Non-system canvas puede modificar todo + if (body.name !== undefined) { updates.push('name = ?'); values.push(body.name) } + if (body.description !== undefined) { updates.push('description = ?'); values.push(body.description) } + if (body.theme_id !== undefined) { updates.push('theme_id = ?'); values.push(body.theme_id) } + if (body.config !== undefined) { updates.push('config = ?'); values.push(JSON.stringify(body.config)) } + if (body.tools !== undefined) { updates.push('tools = ?'); values.push(JSON.stringify(body.tools)) } + if (body.is_default !== undefined) { updates.push('is_default = ?'); values.push(body.is_default ? 1 : 0) } + if (body.show_in_toolbar !== undefined) { updates.push('show_in_toolbar = ?'); values.push(body.show_in_toolbar ? 1 : 0) } + if (body.toolbar_icon !== undefined) { updates.push('toolbar_icon = ?'); values.push(body.toolbar_icon) } + if (body.toolbar_order !== undefined) { updates.push('toolbar_order = ?'); values.push(body.toolbar_order) } + } + + if (updates.length > 0) { + updates.push('updated_at = CURRENT_TIMESTAMP') + values.push(id) + const sql = `UPDATE project_canvas SET ${updates.join(', ')} WHERE id = ?` + db.run(sql, values) + } + + return Response.json({ success: true, id }, { headers: corsHeaders }) + } + + // DELETE /api/canvas/:id - Eliminar canvas + if (req.method === 'DELETE' && !action) { + const canvas = db.query('SELECT is_system FROM project_canvas WHERE id = ?').get(id) as { is_system: number } | null + if (canvas?.is_system) { + return Response.json({ error: 'Cannot delete system canvas' }, { status: 403, headers: corsHeaders }) + } + // canvas_components se eliminan automáticamente por CASCADE + db.run('DELETE FROM project_canvas WHERE id = ?', [id]) + return Response.json({ success: true }, { headers: corsHeaders }) + } + } + + // ===================== + // API de Canvas Components + // ===================== + + // GET /api/canvas/:id/components - Lista componentes del canvas + // POST /api/canvas/:id/components - Agrega componente al canvas + const canvasComponentsMatch = url.pathname.match(/^\/api\/canvas\/([^/]+)\/components\/?$/) + if (canvasComponentsMatch) { + const canvasId = canvasComponentsMatch[1] + + if (req.method === 'GET') { + const rows = db.query(` + SELECT cc.*, vc.name, vc.template, vc.setup, vc.style, vc.props as component_props, vc.imports + FROM canvas_components cc + JOIN vue_components vc ON cc.component_id = vc.id + WHERE cc.canvas_id = ? + ORDER BY cc.position ASC + `).all(canvasId) as any[] + + const components = rows.map(row => ({ + id: row.id, + canvasId: row.canvas_id, + componentId: row.component_id, + position: row.position, + props: row.props ? JSON.parse(row.props) : {}, + layout: row.layout ? JSON.parse(row.layout) : null, + isVisible: !!row.is_visible, + createdAt: row.created_at, + component: { + id: row.component_id, + name: row.name, + template: row.template, + setup: row.setup, + style: row.style, + props: row.component_props ? JSON.parse(row.component_props) : [], + imports: row.imports ? JSON.parse(row.imports) : [] + } + })) + + return Response.json(components, { headers: corsHeaders }) + } + + if (req.method === 'POST') { + const body = await req.json() + + // Verificar que el componente existe + const component = db.query('SELECT id FROM vue_components WHERE id = ?').get(body.component_id) + if (!component) { + return Response.json({ error: 'Component not found' }, { status: 404, headers: corsHeaders }) + } + + // Obtener la siguiente posición + const maxPos = db.query('SELECT MAX(position) as max FROM canvas_components WHERE canvas_id = ?').get(canvasId) as { max: number | null } + const position = body.position ?? ((maxPos?.max ?? -1) + 1) + + const stmt = db.prepare(` + INSERT OR REPLACE INTO canvas_components + (canvas_id, component_id, position, props, layout, is_visible) + VALUES (?, ?, ?, ?, ?, ?) + `) + stmt.run( + canvasId, + body.component_id, + position, + JSON.stringify(body.props || {}), + body.layout ? JSON.stringify(body.layout) : null, + body.is_visible !== false ? 1 : 0 + ) + + return Response.json({ success: true }, { headers: corsHeaders }) + } + } + + // PUT/DELETE /api/canvas/:canvasId/components/:componentId + const canvasComponentMatch = url.pathname.match(/^\/api\/canvas\/([^/]+)\/components\/([^/]+)$/) + if (canvasComponentMatch) { + const canvasId = canvasComponentMatch[1] + const componentId = canvasComponentMatch[2] + + if (req.method === 'PUT') { + const body = await req.json() + const updates: string[] = [] + const values: any[] = [] + + if (body.position !== undefined) { updates.push('position = ?'); values.push(body.position) } + if (body.props !== undefined) { updates.push('props = ?'); values.push(JSON.stringify(body.props)) } + if (body.layout !== undefined) { updates.push('layout = ?'); values.push(JSON.stringify(body.layout)) } + if (body.is_visible !== undefined) { updates.push('is_visible = ?'); values.push(body.is_visible ? 1 : 0) } + + if (updates.length > 0) { + values.push(canvasId, componentId) + const sql = `UPDATE canvas_components SET ${updates.join(', ')} WHERE canvas_id = ? AND component_id = ?` + db.run(sql, values) + } + + return Response.json({ success: true }, { headers: corsHeaders }) + } + + if (req.method === 'DELETE') { + db.run('DELETE FROM canvas_components WHERE canvas_id = ? AND component_id = ?', [canvasId, componentId]) + return Response.json({ success: true }, { headers: corsHeaders }) + } + } + + // GET /api/components/:id/usage - Canvas que usan el componente + const componentUsageMatch = url.pathname.match(/^\/api\/components\/([^/]+)\/usage$/) + if (componentUsageMatch && req.method === 'GET') { + const componentId = componentUsageMatch[1] + const usage = db.query(` + SELECT pc.id, pc.name, pc.type + FROM canvas_components cc + JOIN project_canvas pc ON cc.canvas_id = pc.id + WHERE cc.component_id = ? + `).all(componentId) as { id: string; name: string; type: string }[] + + return Response.json({ + componentId, + usedBy: usage, + canDelete: usage.length === 0 + }, { headers: corsHeaders }) + } + return new Response('Not Found', { status: 404, headers: corsHeaders }) } })