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
This commit is contained in:
2026-02-15 01:57:04 -06:00
parent 9a636e26a7
commit d5ee533db9
16 changed files with 1055 additions and 653 deletions

View File

@@ -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 })
}