feat: Auto-save components, soft delete, tags, compact WCO header
- Auto-save rendered Vue components to DB on render_vue_component - Soft delete (archive) instead of hard delete for components - Tags support for component categorization - Gallery limited to 10 most recent items per section - Upsert with ON CONFLICT for component saves - PUT endpoint for partial component updates - Collapsible toolbar with animated toggle button - Window Controls Overlay support for PWA titlebar - Compact header mode (32px) with hidden dot toggle - Dynamic theme-color meta sync for Windows titlebar
This commit is contained in:
@@ -120,7 +120,9 @@ function runColumnMigrations(db: Database) {
|
||||
'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 status TEXT DEFAULT \'active\''
|
||||
'ALTER TABLE project_canvas ADD COLUMN status TEXT DEFAULT \'active\'',
|
||||
'ALTER TABLE vue_components ADD COLUMN tags TEXT',
|
||||
'ALTER TABLE vue_components ADD COLUMN status TEXT DEFAULT \'active\''
|
||||
]
|
||||
|
||||
for (const sql of alterStatements) {
|
||||
|
||||
@@ -2,18 +2,47 @@ import { db } from '../db'
|
||||
import { jsonResponse, errorResponse } from '../utils/cors'
|
||||
|
||||
export async function handleComponents(req: Request) {
|
||||
const url = new URL(req.url)
|
||||
|
||||
if (req.method === 'GET') {
|
||||
const rows = db.query('SELECT * FROM vue_components ORDER BY updated_at DESC').all()
|
||||
const includeArchived = url.searchParams.get('include_archived') === 'true'
|
||||
const limit = parseInt(url.searchParams.get('limit') || '0') || 0
|
||||
|
||||
let sql = 'SELECT * FROM vue_components'
|
||||
const params: any[] = []
|
||||
|
||||
if (!includeArchived) {
|
||||
sql += " WHERE (status = 'active' OR status IS NULL)"
|
||||
}
|
||||
|
||||
sql += ' ORDER BY updated_at DESC'
|
||||
|
||||
if (limit > 0) {
|
||||
sql += ' LIMIT ?'
|
||||
params.push(limit)
|
||||
}
|
||||
|
||||
const rows = db.query(sql).all(...params)
|
||||
return jsonResponse(rows)
|
||||
}
|
||||
|
||||
if (req.method === 'POST') {
|
||||
const body = await req.json()
|
||||
const id = body.id || `comp-${Date.now()}`
|
||||
const tags = body.tags ? JSON.stringify(body.tags) : null
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT OR REPLACE INTO vue_components
|
||||
(id, name, template, setup, style, props, imports, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
INSERT INTO vue_components (id, name, template, setup, style, props, imports, tags, status, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'active', CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
template = excluded.template,
|
||||
setup = excluded.setup,
|
||||
style = excluded.style,
|
||||
props = excluded.props,
|
||||
imports = excluded.imports,
|
||||
tags = COALESCE(excluded.tags, tags),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`)
|
||||
stmt.run(
|
||||
id,
|
||||
@@ -22,13 +51,14 @@ export async function handleComponents(req: Request) {
|
||||
body.setup || '',
|
||||
body.style || '',
|
||||
JSON.stringify(body.props || []),
|
||||
JSON.stringify(body.imports || [])
|
||||
JSON.stringify(body.imports || []),
|
||||
tags
|
||||
)
|
||||
return jsonResponse({ success: true, id })
|
||||
}
|
||||
|
||||
if (req.method === 'DELETE') {
|
||||
db.run('DELETE FROM vue_components')
|
||||
db.run("UPDATE vue_components SET status = 'archived', updated_at = CURRENT_TIMESTAMP")
|
||||
return jsonResponse({ success: true })
|
||||
}
|
||||
|
||||
@@ -44,8 +74,37 @@ export async function handleComponentById(req: Request, id: string) {
|
||||
return jsonResponse(row)
|
||||
}
|
||||
|
||||
if (req.method === 'PUT') {
|
||||
const body = await req.json()
|
||||
const fields: string[] = []
|
||||
const params: any[] = []
|
||||
|
||||
const allowedFields = ['name', 'template', 'setup', 'style', 'props', 'imports', 'tags', 'status']
|
||||
for (const field of allowedFields) {
|
||||
if (body[field] !== undefined) {
|
||||
if (field === 'props' || field === 'imports' || field === 'tags') {
|
||||
fields.push(`${field} = ?`)
|
||||
params.push(JSON.stringify(body[field]))
|
||||
} else {
|
||||
fields.push(`${field} = ?`)
|
||||
params.push(body[field])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fields.length === 0) {
|
||||
return errorResponse('No fields to update', 400)
|
||||
}
|
||||
|
||||
fields.push('updated_at = CURRENT_TIMESTAMP')
|
||||
params.push(id)
|
||||
|
||||
db.run(`UPDATE vue_components SET ${fields.join(', ')} WHERE id = ?`, params)
|
||||
return jsonResponse({ success: true })
|
||||
}
|
||||
|
||||
if (req.method === 'DELETE') {
|
||||
// Check if component is in use by any canvas
|
||||
// Check if component is in use by any canvas (warn only)
|
||||
const usage = db.query(`
|
||||
SELECT pc.id, pc.name
|
||||
FROM canvas_components cc
|
||||
@@ -53,16 +112,13 @@ export async function handleComponentById(req: Request, id: string) {
|
||||
WHERE cc.component_id = ?
|
||||
`).all(id) as { id: string; name: string }[]
|
||||
|
||||
if (usage.length > 0) {
|
||||
return jsonResponse({
|
||||
error: 'Component in use',
|
||||
message: `Cannot delete component. It is used by: ${usage.map(u => u.name).join(', ')}`,
|
||||
usedBy: usage
|
||||
}, 409)
|
||||
}
|
||||
|
||||
db.run('DELETE FROM vue_components WHERE id = ?', [id])
|
||||
return jsonResponse({ success: true })
|
||||
db.run("UPDATE vue_components SET status = 'archived', updated_at = CURRENT_TIMESTAMP WHERE id = ?", [id])
|
||||
return jsonResponse({
|
||||
success: true,
|
||||
warning: usage.length > 0
|
||||
? `Component is used by: ${usage.map(u => u.name).join(', ')}`
|
||||
: undefined
|
||||
})
|
||||
}
|
||||
|
||||
return null
|
||||
|
||||
Reference in New Issue
Block a user