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
256 lines
9.7 KiB
TypeScript
256 lines
9.7 KiB
TypeScript
import { db } from '../db'
|
|
import { jsonResponse, errorResponse } from '../utils/cors'
|
|
|
|
function parseCanvas(row: any) {
|
|
return {
|
|
...row,
|
|
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 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 AND (status = \'active\' OR status IS NULL) LIMIT 1').get() as any
|
|
if (!row) {
|
|
return jsonResponse({ hasDefault: false })
|
|
}
|
|
return jsonResponse({ hasDefault: true, canvas: parseCanvas(row) })
|
|
}
|
|
|
|
export async function handleCanvas(req: Request) {
|
|
if (req.method === 'GET') {
|
|
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))
|
|
}
|
|
|
|
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 jsonResponse({ success: true, id })
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
export async function handleCanvasById(req: Request, id: string, action?: string) {
|
|
// POST /api/canvas/:id/clone
|
|
if (action === 'clone' && req.method === 'POST') {
|
|
const original = db.query('SELECT * FROM project_canvas WHERE id = ?').get(id) as any
|
|
if (!original) {
|
|
return errorResponse('Canvas not found', 404)
|
|
}
|
|
|
|
const body = await req.json()
|
|
const newId = `canvas-${Date.now()}`
|
|
const newName = body.name || `${original.name} (copia)`
|
|
|
|
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)
|
|
|
|
// Clone canvas components
|
|
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 jsonResponse({ success: true, id: newId })
|
|
}
|
|
|
|
// GET /api/canvas/:id
|
|
if (req.method === 'GET' && !action) {
|
|
const row = db.query('SELECT * FROM project_canvas WHERE id = ?').get(id) as any
|
|
if (!row) {
|
|
return errorResponse('Canvas not found', 404)
|
|
}
|
|
return jsonResponse(parseCanvas(row))
|
|
}
|
|
|
|
// PUT /api/canvas/:id
|
|
if (req.method === 'PUT' && !action) {
|
|
const canvas = db.query('SELECT * FROM project_canvas WHERE id = ?').get(id) as any
|
|
if (!canvas) {
|
|
return errorResponse('Canvas not found', 404)
|
|
}
|
|
|
|
const body = await req.json()
|
|
const updates: string[] = []
|
|
const values: any[] = []
|
|
|
|
// System canvas can only modify toolbar settings and 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 {
|
|
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 (body.status !== undefined) { updates.push('status = ?'); values.push(body.status) }
|
|
}
|
|
|
|
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 jsonResponse({ success: true, 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('UPDATE project_canvas SET status = \'archived\', show_in_toolbar = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [id])
|
|
return jsonResponse({ success: true })
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
// Canvas Components API
|
|
export async function handleCanvasComponents(req: Request, canvasId: string) {
|
|
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 jsonResponse(components)
|
|
}
|
|
|
|
if (req.method === 'POST') {
|
|
const body = await req.json()
|
|
|
|
// Verify component exists
|
|
const component = db.query('SELECT id FROM vue_components WHERE id = ?').get(body.component_id)
|
|
if (!component) {
|
|
return errorResponse('Component not found', 404)
|
|
}
|
|
|
|
// Get next position
|
|
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 jsonResponse({ success: true })
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
export async function handleCanvasComponentById(req: Request, canvasId: string, componentId: string) {
|
|
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 jsonResponse({ success: true })
|
|
}
|
|
|
|
if (req.method === 'DELETE') {
|
|
db.run('DELETE FROM canvas_components WHERE canvas_id = ? AND component_id = ?', [canvasId, componentId])
|
|
return jsonResponse({ success: true })
|
|
}
|
|
|
|
return null
|
|
}
|