feat: Add multi-canvas system with project canvas support

- Add project_canvas and canvas_components tables for persistent canvas storage
- Add ProjectCanvas store with full CRUD operations
- Add ProjectCanvasPage for rendering saved canvas with components
- Add ProjectsPage for managing canvas list (create, clone, delete)
- Add HomePage that loads default canvas or falls back to dynamic canvas
- Add toolbar support for displaying canvas as pages with custom icons
- Add component usage validation to prevent deletion of components in use
- Add MCP tools for canvas management (list, create, update, delete, clone)
- Update router with /canvas/:id and /projects routes
- Update Toolbar to show dynamic canvas pages from database
This commit is contained in:
2026-02-13 06:32:46 -06:00
parent 2e64dceb1e
commit 8a017db777
13 changed files with 2016 additions and 13 deletions

View File

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

View File

@@ -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<ProjectCanvas[]>([])
const activeCanvas = ref<ProjectCanvas | null>(null)
const activeCanvasComponents = ref<CanvasComponent[]>([])
const loading = ref(false)
const saving = ref(false)
const error = ref<string | null>(null)
// State adicional
const toolbarCanvases = ref<ProjectCanvas[]>([])
const defaultCanvas = ref<ProjectCanvas | null>(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<ProjectCanvas | null> {
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<ProjectCanvas | null> {
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<ProjectCanvas>): Promise<string | null> {
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<ProjectCanvas>): Promise<boolean> {
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<boolean> {
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<string | null> {
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<string, any>,
position?: number
): Promise<boolean> {
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<boolean> {
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<string, any>; layout?: any; is_visible?: boolean }
): Promise<boolean> {
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<ComponentUsage | null> {
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<boolean> {
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
}
})