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 { nanoid } from 'nanoid'
|
||||||
import 'reactflow/dist/style.css'
|
import 'reactflow/dist/style.css'
|
||||||
import Toolbar from '@/components/editor/Toolbar'
|
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 DialogueNode from '@/components/editor/nodes/DialogueNode'
|
||||||
import ChoiceNode from '@/components/editor/nodes/ChoiceNode'
|
import ChoiceNode from '@/components/editor/nodes/ChoiceNode'
|
||||||
import VariableNode from '@/components/editor/nodes/VariableNode'
|
import VariableNode from '@/components/editor/nodes/VariableNode'
|
||||||
|
|
@ -166,6 +168,8 @@ function FlowchartEditorInner({ projectId, initialData }: FlowchartEditorProps)
|
||||||
|
|
||||||
const [contextMenu, setContextMenu] = useState<ContextMenuState>(null)
|
const [contextMenu, setContextMenu] = useState<ContextMenuState>(null)
|
||||||
const [conditionEditor, setConditionEditor] = useState<ConditionEditorState>(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)
|
// Check for saved draft on initial render (lazy initialization)
|
||||||
const [draftState, setDraftState] = useState<{
|
const [draftState, setDraftState] = useState<{
|
||||||
|
|
@ -308,9 +312,43 @@ function FlowchartEditorInner({ projectId, initialData }: FlowchartEditorProps)
|
||||||
setNodes((nodes) => [...nodes, newNode])
|
setNodes((nodes) => [...nodes, newNode])
|
||||||
}, [getViewportCenter, setNodes])
|
}, [getViewportCenter, setNodes])
|
||||||
|
|
||||||
const handleSave = useCallback(() => {
|
const handleSave = useCallback(async () => {
|
||||||
// TODO: Implement in US-034
|
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(() => {
|
const handleExport = useCallback(() => {
|
||||||
// TODO: Implement in US-035
|
// TODO: Implement in US-035
|
||||||
|
|
@ -509,6 +547,7 @@ function FlowchartEditorInner({ projectId, initialData }: FlowchartEditorProps)
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
onExport={handleExport}
|
onExport={handleExport}
|
||||||
onImport={handleImport}
|
onImport={handleImport}
|
||||||
|
isSaving={isSaving}
|
||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
|
|
@ -586,6 +625,15 @@ function FlowchartEditorInner({ projectId, initialData }: FlowchartEditorProps)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Toast notification */}
|
||||||
|
{toast && (
|
||||||
|
<Toast
|
||||||
|
message={toast.message}
|
||||||
|
type={toast.type}
|
||||||
|
onClose={() => setToast(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ type ToolbarProps = {
|
||||||
onSave: () => void
|
onSave: () => void
|
||||||
onExport: () => void
|
onExport: () => void
|
||||||
onImport: () => void
|
onImport: () => void
|
||||||
|
isSaving?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Toolbar({
|
export default function Toolbar({
|
||||||
|
|
@ -16,6 +17,7 @@ export default function Toolbar({
|
||||||
onSave,
|
onSave,
|
||||||
onExport,
|
onExport,
|
||||||
onImport,
|
onImport,
|
||||||
|
isSaving = false,
|
||||||
}: ToolbarProps) {
|
}: ToolbarProps) {
|
||||||
return (
|
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">
|
<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">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={onSave}
|
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>
|
||||||
<button
|
<button
|
||||||
onClick={onExport}
|
onClick={onExport}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue