From d5ee533db910564f3224a5b7682672889fd9bc09 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Sun, 15 Feb 2026 01:57:04 -0600 Subject: [PATCH] feat: Add canvas gallery with soft delete, snapshots and components Replace the empty dynamic canvas placeholder with a gallery showing saved canvases, snapshots and Vue components. Users can create new canvases, restore snapshots, load components, and manage canvas toolbar/archive settings from the gallery. - Backend: soft delete (archive) instead of hard delete, status column - Frontend: CanvasGallery component with grid, search, settings popover - Show canvas name in global header when viewing a project canvas - Remove ProjectsPage (replaced by gallery), clean all references - MCP tools: project category available on canvas page, update handlers --- frontend/src/App.vue | 30 +- frontend/src/components/Canvas.vue | 60 +- frontend/src/components/CanvasGallery.vue | 944 ++++++++++++++++++ frontend/src/components/Toolbar.vue | 21 +- frontend/src/pages/ProjectCanvasPage.vue | 73 +- frontend/src/pages/ProjectsPage.vue | 504 ---------- frontend/src/router/index.ts | 5 - frontend/src/services/toolRegistry.ts | 5 +- .../services/tools/handlers/canvasHandlers.ts | 5 +- .../tools/handlers/componentHandlers.ts | 5 +- .../services/tools/handlers/globalHandlers.ts | 4 +- .../tools/handlers/projectCanvasHandlers.ts | 12 +- frontend/src/stores/projectCanvas.ts | 20 +- frontend/src/types/canvas.ts | 2 + server/db/migrations.ts | 3 +- server/routes/canvas.ts | 15 +- 16 files changed, 1055 insertions(+), 653 deletions(-) create mode 100644 frontend/src/components/CanvasGallery.vue delete mode 100644 frontend/src/pages/ProjectsPage.vue diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 03c2808..3a081e0 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -14,6 +14,7 @@ import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './servi import { setTerminalControls } from './services/tools/handlers/terminalHandlers' import { setResponseControls } from './services/tools/handlers/responseHandlers' import { useCanvasStore } from './stores/canvas' +import { useProjectCanvasStore } from './stores/projectCanvas' const route = useRoute() const router = useRouter() @@ -63,6 +64,7 @@ const terminalRef = ref | null>(null) const responseRef = ref | null>(null) const voiceRef = ref | null>(null) const canvasStore = useCanvasStore() +const projectCanvasStore = useProjectCanvasStore() // Voice FAB push-to-talk state const voicePTTActive = ref(false) @@ -238,7 +240,7 @@ function triggerToolFlash() { }, 500) } -type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' | 'source' | 'terminal' | 'tools' +type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'project-canvas' | 'database' | 'source' | 'terminal' | 'tools' onMounted(async () => { // Connect to WebSocket for Claude status updates @@ -350,6 +352,11 @@ watch(() => route.name, (newPage) => {

Agent UI

+ -
-

{{ projectCanvasStore.activeCanvas.name }}

- - {{ projectCanvasStore.activeCanvas.description }} - -
-
- Sistema -
-
-
@@ -175,59 +157,6 @@ watch(() => props.id, () => { overflow: hidden; } -.canvas-header { - display: flex; - align-items: center; - gap: 1rem; - padding: 0.75rem 1rem; - background: var(--bg-secondary); - border-bottom: 1px solid var(--border-color); -} - -.back-btn { - display: flex; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - background: transparent; - border: none; - border-radius: 8px; - color: var(--text-secondary); - cursor: pointer; - transition: all 0.15s ease; -} - -.back-btn:hover { - background: var(--bg-hover); - color: var(--text-primary); -} - -.canvas-info { - flex: 1; -} - -.canvas-info h1 { - font-size: 1rem; - font-weight: 600; - color: var(--text-primary); - margin: 0; -} - -.canvas-info span { - font-size: 0.75rem; - color: var(--text-muted); -} - -.canvas-badge { - padding: 0.25rem 0.5rem; - background: var(--accent-muted); - color: var(--accent); - font-size: 0.75rem; - font-weight: 500; - border-radius: var(--radius-full); -} - .canvas-container { flex: 1; display: flex; diff --git a/frontend/src/pages/ProjectsPage.vue b/frontend/src/pages/ProjectsPage.vue deleted file mode 100644 index d494a26..0000000 --- a/frontend/src/pages/ProjectsPage.vue +++ /dev/null @@ -1,504 +0,0 @@ - - - - - diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 41e7d71..d633fbe 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -19,11 +19,6 @@ const router = createRouter({ 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/toolRegistry.ts b/frontend/src/services/toolRegistry.ts index dcec961..5affc13 100644 --- a/frontend/src/services/toolRegistry.ts +++ b/frontend/src/services/toolRegistry.ts @@ -32,7 +32,7 @@ import { setRouter } from './tools/handlers/globalHandlers' import { setGiteaCredentials, clearGiteaCredentials } from './tools/handlers/sourceCodeHandlers' import { ALL_TOOL_METAS, getAllToolNames, type ToolCategory } from './tools/toolDefinitions' -export type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' | 'source' | 'terminal' | 'tools' | 'git' +export type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'project-canvas' | 'database' | 'source' | 'terminal' | 'tools' | 'git' // Internal webmcp functions (not exported for external use) let webmcpInstance: any = null @@ -156,9 +156,8 @@ const categoryTools: Record = { // Page to categories mapping const pageCategories: Record = { home: ['global', 'torch', 'canvas', 'component', 'project', 'terminal'], - canvas: ['global', 'torch', 'canvas', 'component', 'terminal'], + canvas: ['global', 'torch', 'canvas', 'component', 'project', 'terminal'], 'project-canvas': ['global', 'torch', 'canvas', 'component', 'project', 'terminal'], - projects: ['global', 'torch', 'project', 'terminal'], components: ['global', 'torch', 'component', 'terminal'], themes: ['global', 'torch', 'theme', 'terminal'], database: ['global', 'torch', 'database', 'terminal'], diff --git a/frontend/src/services/tools/handlers/canvasHandlers.ts b/frontend/src/services/tools/handlers/canvasHandlers.ts index 8a10876..6624467 100644 --- a/frontend/src/services/tools/handlers/canvasHandlers.ts +++ b/frontend/src/services/tools/handlers/canvasHandlers.ts @@ -49,7 +49,10 @@ function getCanvasContainer() { function removePlaceholder(container: HTMLElement) { const placeholder = container.querySelector('.canvas-placeholder') - if (placeholder) placeholder.remove() + if (placeholder) { + placeholder.remove() + window.dispatchEvent(new CustomEvent('canvas-content-rendered')) + } } function emitComponentRendered(args: any) { diff --git a/frontend/src/services/tools/handlers/componentHandlers.ts b/frontend/src/services/tools/handlers/componentHandlers.ts index 32ac95b..f86f4a0 100644 --- a/frontend/src/services/tools/handlers/componentHandlers.ts +++ b/frontend/src/services/tools/handlers/componentHandlers.ts @@ -13,7 +13,10 @@ function getCanvasContainer() { function removePlaceholder(container: HTMLElement) { const placeholder = container.querySelector('.canvas-placeholder') - if (placeholder) placeholder.remove() + if (placeholder) { + placeholder.remove() + window.dispatchEvent(new CustomEvent('canvas-content-rendered')) + } } export function createComponentHandlers(): ToolConfig[] { diff --git a/frontend/src/services/tools/handlers/globalHandlers.ts b/frontend/src/services/tools/handlers/globalHandlers.ts index 578e393..2418ba1 100644 --- a/frontend/src/services/tools/handlers/globalHandlers.ts +++ b/frontend/src/services/tools/handlers/globalHandlers.ts @@ -40,7 +40,6 @@ export function createGlobalHandlers(callbacks: ToolManagementCallbacks): ToolCo themes: 'Themes - Editor visual de temas', database: 'Database - Explorador de base de datos', source: 'Source - Navegador de codigo fuente', - projects: 'Projects - Gestiona proyectos', terminal: 'Terminal - Consola de comandos', tools: 'Tools - Gestion de herramientas MCP' } @@ -63,7 +62,7 @@ export function createGlobalHandlers(callbacks: ToolManagementCallbacks): ToolCo properties: { page: { type: 'string', - enum: ['home', 'canvas', 'components', 'themes', 'database', 'source', 'projects', 'terminal', 'tools'], + enum: ['home', 'canvas', 'components', 'themes', 'database', 'source', 'terminal', 'tools'], description: 'Pagina a la que navegar' } }, @@ -81,7 +80,6 @@ export function createGlobalHandlers(callbacks: ToolManagementCallbacks): ToolCo themes: '/themes', database: '/database', source: '/source', - projects: '/projects', terminal: '/terminal', tools: '/tools' } diff --git a/frontend/src/services/tools/handlers/projectCanvasHandlers.ts b/frontend/src/services/tools/handlers/projectCanvasHandlers.ts index ebeed50..699475b 100644 --- a/frontend/src/services/tools/handlers/projectCanvasHandlers.ts +++ b/frontend/src/services/tools/handlers/projectCanvasHandlers.ts @@ -10,12 +10,13 @@ export function createProjectCanvasHandlers(): ToolConfig[] { schema: { type: 'object', properties: { - type: { type: 'string', enum: ['all', 'project', 'system'], description: 'Filtrar por tipo' } + type: { type: 'string', enum: ['all', 'project', 'system'], description: 'Filtrar por tipo' }, + include_archived: { type: 'boolean', description: 'Incluir canvases archivados' } } }, - handler: async (args: { type?: string }) => { + handler: async (args: { type?: string; include_archived?: boolean }) => { const store = useProjectCanvasStore() - await store.fetchCanvases() + await store.fetchCanvases(args.include_archived || false) let canvases = store.canvases if (args.type === 'project') { @@ -28,6 +29,7 @@ export function createProjectCanvasHandlers(): ToolConfig[] { id: c.id, name: c.name, type: c.type, + status: c.status, description: c.description })), null, 2) } @@ -110,7 +112,7 @@ export function createProjectCanvasHandlers(): ToolConfig[] { }, { name: 'delete_canvas', - description: 'Elimina un canvas', + description: 'Archiva un canvas (soft delete)', category: 'project', schema: { type: 'object', @@ -124,7 +126,7 @@ export function createProjectCanvasHandlers(): ToolConfig[] { const success = await store.deleteCanvas(args.id) if (success) { - return `Canvas "${args.id}" eliminado` + return `Canvas "${args.id}" archivado` } return `Error: ${store.error}` } diff --git a/frontend/src/stores/projectCanvas.ts b/frontend/src/stores/projectCanvas.ts index 8e3857b..5831224 100644 --- a/frontend/src/stores/projectCanvas.ts +++ b/frontend/src/stores/projectCanvas.ts @@ -31,6 +31,14 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => { canvases.value.find(c => c.type === 'dynamic') ) + const activeCanvasesList = computed(() => + canvases.value.filter(c => c.status !== 'archived') + ) + + const archivedCanvases = computed(() => + canvases.value.filter(c => c.status === 'archived') + ) + const canvasCount = computed(() => canvases.value.length) const hasActiveCanvas = computed(() => activeCanvas.value !== null) @@ -38,11 +46,12 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => { const hasDefaultCanvas = computed(() => defaultCanvas.value !== null) // Actions - async function fetchCanvases() { + async function fetchCanvases(includeArchived = false) { loading.value = true error.value = null try { - const res = await fetch(`${API_URL}/api/canvas`) + const url = includeArchived ? `${API_URL}/api/canvas?include_archived=true` : `${API_URL}/api/canvas` + const res = await fetch(url) if (!res.ok) throw new Error('Failed to fetch canvases') canvases.value = await res.json() } catch (e) { @@ -168,6 +177,10 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => { } } + async function restoreCanvas(id: string): Promise { + return updateCanvas(id, { status: 'active' } as any) + } + async function cloneCanvas(id: string, newName?: string): Promise { saving.value = true error.value = null @@ -304,6 +317,8 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => { projectCanvases, systemCanvases, dynamicCanvas, + activeCanvasesList, + archivedCanvases, canvasCount, hasActiveCanvas, hasDefaultCanvas, @@ -315,6 +330,7 @@ export const useProjectCanvasStore = defineStore('projectCanvas', () => { createCanvas, updateCanvas, deleteCanvas, + restoreCanvas, cloneCanvas, fetchCanvasComponents, addComponentToCanvas, diff --git a/frontend/src/types/canvas.ts b/frontend/src/types/canvas.ts index 5ece321..c0ae4c4 100644 --- a/frontend/src/types/canvas.ts +++ b/frontend/src/types/canvas.ts @@ -1,4 +1,5 @@ export type CanvasType = 'dynamic' | 'project' | 'system' | 'external' +export type CanvasStatus = 'active' | 'archived' export interface CanvasConfig { layout?: 'free' | 'grid' | 'stack' @@ -21,6 +22,7 @@ export interface ProjectCanvas { name: string description?: string type: CanvasType + status: CanvasStatus theme_id?: string | null config: CanvasConfig | null tools: string[] diff --git a/server/db/migrations.ts b/server/db/migrations.ts index ad47306..15ca167 100644 --- a/server/db/migrations.ts +++ b/server/db/migrations.ts @@ -119,7 +119,8 @@ function runColumnMigrations(db: Database) { const alterStatements = [ 'ALTER TABLE project_canvas ADD COLUMN show_in_toolbar INTEGER DEFAULT 0', 'ALTER TABLE project_canvas ADD COLUMN toolbar_icon TEXT', - 'ALTER TABLE project_canvas ADD COLUMN toolbar_order INTEGER DEFAULT 99' + 'ALTER TABLE project_canvas ADD COLUMN toolbar_order INTEGER DEFAULT 99', + 'ALTER TABLE project_canvas ADD COLUMN status TEXT DEFAULT \'active\'' ] for (const sql of alterStatements) { diff --git a/server/routes/canvas.ts b/server/routes/canvas.ts index 77916bf..f920cbe 100644 --- a/server/routes/canvas.ts +++ b/server/routes/canvas.ts @@ -7,18 +7,19 @@ function parseCanvas(row: any) { is_default: !!row.is_default, is_system: !!row.is_system, show_in_toolbar: !!row.show_in_toolbar, + status: row.status || 'active', config: row.config ? JSON.parse(row.config) : null, tools: row.tools ? JSON.parse(row.tools) : [] } } export function handleToolbarCanvas() { - const rows = db.query('SELECT * FROM project_canvas WHERE show_in_toolbar = 1 ORDER BY toolbar_order ASC, name ASC').all() + const rows = db.query('SELECT * FROM project_canvas WHERE show_in_toolbar = 1 AND (status = \'active\' OR status IS NULL) ORDER BY toolbar_order ASC, name ASC').all() return jsonResponse((rows as any[]).map(parseCanvas)) } export function handleDefaultCanvas() { - const row = db.query('SELECT * FROM project_canvas WHERE is_default = 1 LIMIT 1').get() as any + const row = db.query('SELECT * FROM project_canvas WHERE is_default = 1 AND (status = \'active\' OR status IS NULL) LIMIT 1').get() as any if (!row) { return jsonResponse({ hasDefault: false }) } @@ -27,7 +28,10 @@ export function handleDefaultCanvas() { export async function handleCanvas(req: Request) { if (req.method === 'GET') { - const rows = db.query('SELECT * FROM project_canvas ORDER BY is_system DESC, is_default DESC, name ASC').all() + const url = new URL(req.url) + const includeArchived = url.searchParams.get('include_archived') === 'true' + const whereClause = includeArchived ? '' : 'WHERE (status = \'active\' OR status IS NULL)' + const rows = db.query(`SELECT * FROM project_canvas ${whereClause} ORDER BY is_system DESC, is_default DESC, name ASC`).all() return jsonResponse((rows as any[]).map(parseCanvas)) } @@ -129,6 +133,7 @@ export async function handleCanvasById(req: Request, id: string, action?: string 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 (body.status !== undefined) { updates.push('status = ?'); values.push(body.status) } } if (updates.length > 0) { @@ -141,13 +146,13 @@ export async function handleCanvasById(req: Request, id: string, action?: string return jsonResponse({ success: true, id }) } - // DELETE /api/canvas/:id + // DELETE /api/canvas/:id (soft delete - archive) 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 errorResponse('Cannot delete system canvas', 403) } - db.run('DELETE FROM project_canvas WHERE id = ?', [id]) + db.run('UPDATE project_canvas SET status = \'archived\', show_in_toolbar = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [id]) return jsonResponse({ success: true }) }