feat: [US-034] - Save project to database
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e8dbd00d4c
commit
01f5428dd9
|
|
@ -22,6 +22,8 @@ import ReactFlow, {
|
|||
import { nanoid } from 'nanoid'
|
||||
import 'reactflow/dist/style.css'
|
||||
import Toolbar from '@/components/editor/Toolbar'
|
||||
import Toast from '@/components/Toast'
|
||||
import { createClient } from '@/lib/supabase/client'
|
||||
import DialogueNode from '@/components/editor/nodes/DialogueNode'
|
||||
import ChoiceNode from '@/components/editor/nodes/ChoiceNode'
|
||||
import VariableNode from '@/components/editor/nodes/VariableNode'
|
||||
|
|
@ -166,6 +168,8 @@ function FlowchartEditorInner({ projectId, initialData }: FlowchartEditorProps)
|
|||
|
||||
const [contextMenu, setContextMenu] = useState<ContextMenuState>(null)
|
||||
const [conditionEditor, setConditionEditor] = useState<ConditionEditorState>(null)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null)
|
||||
|
||||
// Check for saved draft on initial render (lazy initialization)
|
||||
const [draftState, setDraftState] = useState<{
|
||||
|
|
@ -308,9 +312,43 @@ function FlowchartEditorInner({ projectId, initialData }: FlowchartEditorProps)
|
|||
setNodes((nodes) => [...nodes, newNode])
|
||||
}, [getViewportCenter, setNodes])
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
// TODO: Implement in US-034
|
||||
}, [])
|
||||
const handleSave = useCallback(async () => {
|
||||
if (isSaving) return
|
||||
|
||||
setIsSaving(true)
|
||||
|
||||
try {
|
||||
const supabase = createClient()
|
||||
|
||||
// Convert React Flow state to FlowchartData
|
||||
const flowchartData: FlowchartData = {
|
||||
nodes: fromReactFlowNodes(nodes),
|
||||
edges: fromReactFlowEdges(edges),
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from('projects')
|
||||
.update({
|
||||
flowchart_data: flowchartData,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq('id', projectId)
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
// Clear LocalStorage draft after successful save
|
||||
clearDraft(projectId)
|
||||
|
||||
setToast({ message: 'Project saved successfully', type: 'success' })
|
||||
} catch (error) {
|
||||
console.error('Failed to save project:', error)
|
||||
setToast({ message: 'Failed to save project. Please try again.', type: 'error' })
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}, [isSaving, nodes, edges, projectId])
|
||||
|
||||
const handleExport = useCallback(() => {
|
||||
// TODO: Implement in US-035
|
||||
|
|
@ -509,6 +547,7 @@ function FlowchartEditorInner({ projectId, initialData }: FlowchartEditorProps)
|
|||
onSave={handleSave}
|
||||
onExport={handleExport}
|
||||
onImport={handleImport}
|
||||
isSaving={isSaving}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<ReactFlow
|
||||
|
|
@ -586,6 +625,15 @@ function FlowchartEditorInner({ projectId, initialData }: FlowchartEditorProps)
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Toast notification */}
|
||||
{toast && (
|
||||
<Toast
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={() => setToast(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ type ToolbarProps = {
|
|||
onSave: () => void
|
||||
onExport: () => void
|
||||
onImport: () => void
|
||||
isSaving?: boolean
|
||||
}
|
||||
|
||||
export default function Toolbar({
|
||||
|
|
@ -16,6 +17,7 @@ export default function Toolbar({
|
|||
onSave,
|
||||
onExport,
|
||||
onImport,
|
||||
isSaving = false,
|
||||
}: ToolbarProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between border-b border-zinc-200 bg-zinc-50 px-4 py-2 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
|
|
@ -46,9 +48,32 @@ export default function Toolbar({
|
|||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onSave}
|
||||
className="rounded border border-zinc-300 bg-white px-3 py-1.5 text-sm font-medium text-zinc-700 hover:bg-zinc-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:border-zinc-600 dark:bg-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-600 dark:focus:ring-offset-zinc-800"
|
||||
disabled={isSaving}
|
||||
className="flex items-center gap-1.5 rounded border border-zinc-300 bg-white px-3 py-1.5 text-sm font-medium text-zinc-700 hover:bg-zinc-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-600 dark:bg-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-600 dark:focus:ring-offset-zinc-800"
|
||||
>
|
||||
Save
|
||||
{isSaving && (
|
||||
<svg
|
||||
className="h-4 w-4 animate-spin"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{isSaving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
<button
|
||||
onClick={onExport}
|
||||
|
|
|
|||
Loading…
Reference in New Issue