Aymeric Rabot bee4613b82
Add project versioning & publish flow (#130)
* Add project versioning & publish flow

Introduce project model versioning and publishing support.

- DB: add published_model_version column to projects (migration + schema + types).
- Models: add version status types and helpers, implement getProjectVersionStatus, enhanced getProjectModel to load draft/published/legacy fallbacks, saveProjectModel now manages draft creation/updating and avoids no-op saves, saveProjectVersion to lock/save/publish versions and create next draft, publishProjectModel to republish specific saved versions. Includes scene-graph equality checks and authenticated project ownership checks.
- UI: AppSidebar shows publish/draft status, polling refresh, and Save / Save & publish / Publish actions that flush the editor scene before version operations. Hook update: load scene from result.data.model.scene_graph.
- Public project loader: prefer published version with legacy fallbacks.

This enables safe draft editing, explicit saves, publishing, and keeps autosave working across version locks.

* Add project versioning UI, APIs & preview mode

Introduce full version management support: UI, backend actions, and editor integration.

- UI: Add a Versions popover to the app sidebar with search, relative timestamps, preview, restore and publish controls; disable version actions while previewing and show proper labels/states. Imported new icons, popover and tooltip primitives and added formatRelativeTime helper.
- Actions: Add ProjectVersionListItem type and APIs getProjectVersionList and getProjectVersionByNumber to list and fetch saved (non-draft) versions.
- Hooks: Extract applySceneGraphToEditor and sync editor selection logic; ensure scene loading applies selection and supports preview mode (which suppresses autosave).
- Store: Add isVersionPreviewMode to project store with setter and isSceneLoading flag usage to prevent autosave while previewing.
- Misc: Wire up version list loading, previewing, restoring and publishing flows, and refresh version status/list after actions.

These changes enable browsing historical project versions, previewing them without triggering autosaves, and restoring or publishing selected versions.

* Improve version preview, restore flow & draft handling

- Add scene graph snapshot to preserve unsaved work during preview
- Include draft versions and metadata in version list
- Add getProjectVersionById for direct ID-based lookups
- Add autosave status to project store
- Refactor sidebar version UI (remove search, use save icon)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:56:46 -05:00

123 lines
3.2 KiB
TypeScript

/**
* Project store - Zustand store for project state management
*/
import { create } from 'zustand'
import type { Project } from './types'
import {
getActiveProject,
getUserProjects,
getProjectById,
} from './actions'
interface ProjectStore {
// Autosave lifecycle for the latest draft scene
autosaveStatus: 'idle' | 'pending' | 'saving' | 'saved' | 'paused'
// State
activeProject: Project | null
projects: Project[]
isLoading: boolean
isSceneLoading: boolean
isVersionPreviewMode: boolean
error: string | null
// Actions
fetchProjects: () => Promise<void>
fetchActiveProject: () => Promise<void>
setActiveProject: (projectId: string) => Promise<void>
setIsSceneLoading: (loading: boolean) => void
setIsVersionPreviewMode: (preview: boolean) => void
setAutosaveStatus: (status: ProjectStore['autosaveStatus']) => void
initialize: () => Promise<void>
updateActiveThumbnail: (thumbnailUrl: string) => void
}
export const useProjectStore = create<ProjectStore>((set, get) => ({
// Initial state
autosaveStatus: 'idle',
activeProject: null,
projects: [],
isLoading: true,
isSceneLoading: false,
isVersionPreviewMode: false,
error: null,
// Fetch all projects
fetchProjects: async () => {
const result = await getUserProjects()
if (result.success) {
set({ projects: result.data || [], error: null })
} else {
set({ error: result.error || 'Failed to fetch projects', projects: [] })
}
},
// Fetch the active project from database
fetchActiveProject: async () => {
set({ isLoading: true })
const result = await getActiveProject()
if (result.success) {
set({
activeProject: result.data || null,
isLoading: false,
error: null
})
// Note: Auto-select logic removed - now using URL-based routing
// The URL parameter determines which project to load
} else {
set({
error: result.error || 'Failed to fetch active project',
activeProject: null,
isLoading: false
})
}
},
// Set active project by fetching it directly by ID (URL-based, no session update)
setActiveProject: async (projectId: string) => {
set({ isLoading: true })
const result = await getProjectById(projectId)
if (result.success && result.data) {
set({ activeProject: result.data, isLoading: false, error: null })
} else {
set({ isLoading: false, error: result.error || 'Project not found' })
}
},
setIsSceneLoading: (loading: boolean) => {
set({ isSceneLoading: loading })
},
setIsVersionPreviewMode: (preview: boolean) => {
set({ isVersionPreviewMode: preview })
},
setAutosaveStatus: (status) => {
set({ autosaveStatus: status })
},
// Patch the active project's thumbnail URL in place (no refetch)
updateActiveThumbnail: (thumbnailUrl: string) => {
set((state) => ({
activeProject: state.activeProject
? { ...state.activeProject, thumbnail_url: thumbnailUrl }
: null,
}))
},
// Initialize - fetch both projects and active project
initialize: async () => {
set({ isLoading: true })
await Promise.all([
get().fetchProjects(),
get().fetchActiveProject(),
])
},
}))