* 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>
123 lines
3.2 KiB
TypeScript
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(),
|
|
])
|
|
},
|
|
}))
|