feat: restore saved documents through facade
This commit is contained in:
12
src/App.tsx
12
src/App.tsx
@@ -468,7 +468,7 @@ function Viewport({ selectedObject, setSelectedObject, workbench, facade, docume
|
||||
function PageFrame({ page, onNavigate, onOpenWorkspace, showNotice, facade }: { page: Page; onNavigate: (page: Page) => void; onOpenWorkspace: () => void; showNotice: (message: string) => void; facade: BitBybitWebCadFacade }) {
|
||||
const pages: Record<Exclude<Page, 'workspace'>, ReactNode> = {
|
||||
start: <StartPage onNavigate={onNavigate} onOpenWorkspace={onOpenWorkspace} showNotice={showNotice} />,
|
||||
projects: <ProjectsPage onNavigate={onNavigate} onOpenWorkspace={onOpenWorkspace} showNotice={showNotice} />,
|
||||
projects: <ProjectsPage onNavigate={onNavigate} onOpenWorkspace={onOpenWorkspace} showNotice={showNotice} facade={facade} />,
|
||||
import: <FileFlowPage mode="import" onNavigate={onNavigate} showNotice={showNotice} facade={facade} />,
|
||||
export: <FileFlowPage mode="export" onNavigate={onNavigate} showNotice={showNotice} facade={facade} />,
|
||||
settings: <SettingsPage onNavigate={onNavigate} showNotice={showNotice} />,
|
||||
@@ -500,8 +500,14 @@ function ProjectCard({ project, index, onClick }: { project: typeof projects[num
|
||||
return <button className="project-card" onClick={onClick}><div className={`project-preview preview-${index}`}><div className="preview-shape" /><div className="preview-grid" /></div><div className="project-card-info"><div className="project-card-title"><strong>{project.name}</strong><MoreHorizontal size={15} /></div><span>{project.path}</span><div className="project-card-meta"><span>{project.modified}</span><span>{project.objects} objects</span><Badge tone={project.state === 'Unsaved' ? 'amber' : project.state === 'Read only' ? 'muted' : 'green'}>{project.state}</Badge></div></div></button>
|
||||
}
|
||||
|
||||
function ProjectsPage({ onNavigate, onOpenWorkspace, showNotice }: { onNavigate: (page: Page) => void; onOpenWorkspace: () => void; showNotice: (message: string) => void }) {
|
||||
return <div className="projects-page"><PageHeader eyebrow="Project manager" title="Your projects." description="Local-first project storage with explicit save and recovery states." actions={<><button className="button button-outline" onClick={() => onNavigate('import')}><Upload size={16} />Import</button><button className="button button-primary" onClick={onOpenWorkspace}><Plus size={16} />New project</button></>} /><div className="manager-toolbar"><div className="large-search"><Search size={16} /><input placeholder="Search projects" /><kbd>/</kbd></div><div className="toolbar-select"><span>Sort by</span><select><option>Last modified</option><option>Name</option><option>Object count</option></select><ChevronDown size={14} /></div><IconButton icon={LayoutGrid} label="Grid view" active /><IconButton icon={ListTree} label="List view" /></div><div className="project-list">{projects.map((project, index) => <div className="project-list-row" key={project.name}><div className={`list-thumbnail preview-${index}`}><div className="preview-shape" /><div className="preview-grid" /></div><div className="list-main"><strong>{project.name}</strong><span>{project.path}</span></div><span className="list-count">{project.objects} objects</span><span className="list-modified">{project.modified}</span><Badge tone={project.state === 'Unsaved' ? 'amber' : project.state === 'Read only' ? 'muted' : 'green'}>{project.state}</Badge><button className="row-action" title="More project actions" onClick={() => showNotice(`Actions for ${project.name}`)}><MoreHorizontal size={16} /></button></div>)}</div><div className="storage-banner"><div className="storage-icon"><HardDrive size={18} /></div><div><strong>Local storage</strong><span>3.4 GB of 10 GB used · Last backup just now</span></div><button className="text-button" onClick={() => onNavigate('settings')}>Manage storage <ArrowRight size={14} /></button></div></div>
|
||||
function ProjectsPage({ onNavigate, onOpenWorkspace, showNotice, facade }: { onNavigate: (page: Page) => void; onOpenWorkspace: () => void; showNotice: (message: string) => void; facade: BitBybitWebCadFacade }) {
|
||||
const openSavedProject = async () => {
|
||||
const loaded = await facade.app.document.load('doc-pump-housing')
|
||||
if (!loaded) { showNotice('No saved local snapshot found'); return }
|
||||
showNotice(`Loaded ${loaded.label}`)
|
||||
onOpenWorkspace()
|
||||
}
|
||||
return <div className="projects-page"><PageHeader eyebrow="Project manager" title="Your projects." description="Local-first project storage with explicit save and recovery states." actions={<><button className="button button-outline" onClick={() => onNavigate('import')}><Upload size={16} />Import</button><button className="button button-primary" onClick={onOpenWorkspace}><Plus size={16} />New project</button></>} /><div className="manager-toolbar"><div className="large-search"><Search size={16} /><input placeholder="Search projects" /><kbd>/</kbd></div><div className="toolbar-select"><span>Sort by</span><select><option>Last modified</option><option>Name</option><option>Object count</option></select><ChevronDown size={14} /></div><IconButton icon={LayoutGrid} label="Grid view" active /><IconButton icon={ListTree} label="List view" /></div><div className="project-list">{projects.map((project, index) => <button className="project-list-row" key={project.name} onClick={() => void openSavedProject()}><div className={`list-thumbnail preview-${index}`}><div className="preview-shape" /><div className="preview-grid" /></div><div className="list-main"><strong>{project.name}</strong><span>{project.path}</span></div><span className="list-count">{project.objects} objects</span><span className="list-modified">{project.modified}</span><Badge tone={project.state === 'Unsaved' ? 'amber' : project.state === 'Read only' ? 'muted' : 'green'}>{project.state}</Badge><span className="row-action" title="Open project"><ArrowRight size={16} /></span></button>)}</div><div className="storage-banner"><div className="storage-icon"><HardDrive size={18} /></div><div><strong>Local storage</strong><span>3.4 GB of 10 GB used · Last backup just now</span></div><button className="text-button" onClick={() => onNavigate('settings')}>Manage storage <ArrowRight size={14} /></button></div></div>
|
||||
}
|
||||
|
||||
function FileFlowPage({ mode, onNavigate, showNotice, facade }: { mode: 'import' | 'export'; onNavigate: (page: Page) => void; showNotice: (message: string) => void; facade: BitBybitWebCadFacade }) {
|
||||
|
||||
@@ -456,6 +456,16 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
emitState()
|
||||
return result
|
||||
}
|
||||
const loadDocument = async (documentId: string) => {
|
||||
recomputeCoordinator.cancel()
|
||||
const loaded = await projectPersistence.load(documentId)
|
||||
if (!loaded) return null
|
||||
clearFeatureShapes()
|
||||
const document = cloneDocumentSnapshot(loaded)
|
||||
commit({ ...state, document, selectedObjectId: '', task: null })
|
||||
notify(`Loaded ${document.label}`)
|
||||
return getState().document
|
||||
}
|
||||
const getSketch = (objectId: string) => {
|
||||
const object = state.document.objects.find((candidate) => candidate.id === objectId)
|
||||
return object?.sketch ? cloneSketch(object.sketch) : null
|
||||
@@ -559,7 +569,7 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
}
|
||||
|
||||
const facade: BitBybitWebCadFacade = {
|
||||
app: { document: { getActive: () => getState().document, getObject: (objectId) => { const object = state.document.objects.find((candidate) => candidate.id === objectId); return object ? { ...object, properties: object.properties.map((property) => ({ ...property, options: property.options ? [...property.options] : undefined })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined } : null }, create: (label) => { recomputeCoordinator.cancel(); clearFeatureShapes(); commit({ ...state, document: createDocument(label), selectedObjectId: '' }); return getState().document }, markDirty: () => { commit({ ...state, document: { ...state.document, dirty: true } }) }, setProperty, setExpression, recompute: recomputeDocument, recomputeAsync: recomputeDocumentAsync, cancelRecompute: () => recomputeCoordinator.cancel(), getDependencies: () => (state.document.dependencies ?? []).map((edge) => ({ ...edge })) }, expression: { evaluate: (expression, variables = {}) => evaluateQuantityExpression(expression, new Map(Object.entries(variables))), dimensionForUnit: quantityDimensionForUnit }, sketcher: { get: getSketch, addGeometry: addSketchGeometry, addConstraint: addSketchConstraint, solve: solveSketchObject } },
|
||||
app: { document: { getActive: () => getState().document, getObject: (objectId) => { const object = state.document.objects.find((candidate) => candidate.id === objectId); return object ? { ...object, properties: object.properties.map((property) => ({ ...property, options: property.options ? [...property.options] : undefined })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined } : null }, create: (label) => { recomputeCoordinator.cancel(); clearFeatureShapes(); commit({ ...state, document: createDocument(label), selectedObjectId: '' }); return getState().document }, load: loadDocument, markDirty: () => { commit({ ...state, document: { ...state.document, dirty: true } }) }, setProperty, setExpression, recompute: recomputeDocument, recomputeAsync: recomputeDocumentAsync, cancelRecompute: () => recomputeCoordinator.cancel(), getDependencies: () => (state.document.dependencies ?? []).map((edge) => ({ ...edge })) }, expression: { evaluate: (expression, variables = {}) => evaluateQuantityExpression(expression, new Map(Object.entries(variables))), dimensionForUnit: quantityDimensionForUnit }, sketcher: { get: getSketch, addGeometry: addSketchGeometry, addConstraint: addSketchConstraint, solve: solveSketchObject } },
|
||||
history: { canUndo: () => undoStack.length > 0, canRedo: () => redoStack.length > 0, undo: () => { const previous = undoStack.pop(); if (!previous) return; clearFeatureShapes(); redoStack.push(getState()); state = previous; emitState(); notify('Undo applied') }, redo: () => { const next = redoStack.pop(); if (!next) return; clearFeatureShapes(); undoStack.push(getState()); state = next; emitState(); notify('Redo applied') } },
|
||||
gui: { workbench: { list: () => Object.keys(workbenchDefinitions) as WorkbenchId[], getActive: () => state.activeWorkbench, setActive }, command: { getState: (commandId) => commandState(commandId, state.activeWorkbench, state.selectedObjectId, state.document.objects.find((object) => object.id === state.selectedObjectId)?.typeId), list: (workbench) => workbenchDefinitions[workbench].groups.flatMap((group) => group.commands), execute } },
|
||||
selection: { getObjectId: () => state.selectedObjectId, select, clear: () => select('') },
|
||||
|
||||
@@ -340,6 +340,7 @@ export interface BitBybitWebCadFacade {
|
||||
getActive(): DocumentSnapshot
|
||||
getObject(objectId: string): DocumentObjectSnapshot | null
|
||||
create(label?: string): DocumentSnapshot
|
||||
load(documentId: string): Promise<DocumentSnapshot | null>
|
||||
markDirty(): void
|
||||
setProperty(input: SetPropertyInput): void
|
||||
setExpression(input: SetExpressionInput): void
|
||||
|
||||
Reference in New Issue
Block a user