feat: [US-035] - Export project as .vnflow file

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Gustavo Henrique Santos Souza de Miranda 2026-01-22 18:20:18 -03:00
parent 59cda43987
commit 78479f3234
4 changed files with 44 additions and 4 deletions

View File

@ -620,7 +620,7 @@
"Verify in browser using dev-browser skill" "Verify in browser using dev-browser skill"
], ],
"priority": 35, "priority": 35,
"passes": false, "passes": true,
"notes": "" "notes": ""
}, },
{ {

View File

@ -506,3 +506,17 @@
- Toast state uses object with message and type for flexibility - Toast state uses object with message and type for flexibility
- Loading spinner SVG with animate-spin class for visual feedback during save - Loading spinner SVG with animate-spin class for visual feedback during save
--- ---
## 2026-01-22 - US-035
- What was implemented: Export project as .vnflow file functionality
- Files changed:
- src/app/editor/[projectId]/FlowchartEditor.tsx - implemented handleExport with blob creation and download trigger, added projectName prop
- src/app/editor/[projectId]/page.tsx - passed projectName prop to FlowchartEditor
- **Learnings for future iterations:**
- Use Blob with type 'application/json' for JSON file downloads
- JSON.stringify(data, null, 2) creates pretty-printed JSON with 2-space indentation
- URL.createObjectURL creates a temporary URL for the blob
- Create temporary anchor element with download attribute to trigger file download
- Remember to cleanup: remove the anchor from DOM and revoke the object URL
- Props needed for export: pass data down from server components (e.g., projectName) to client components that need them
---

View File

@ -53,6 +53,7 @@ type ConditionEditorState = {
type FlowchartEditorProps = { type FlowchartEditorProps = {
projectId: string projectId: string
projectName: string
initialData: FlowchartData initialData: FlowchartData
} }
@ -145,7 +146,7 @@ function flowchartDataEquals(a: FlowchartData, b: FlowchartData): boolean {
} }
// Inner component that uses useReactFlow hook // Inner component that uses useReactFlow hook
function FlowchartEditorInner({ projectId, initialData }: FlowchartEditorProps) { function FlowchartEditorInner({ projectId, projectName, initialData }: FlowchartEditorProps) {
// Define custom node types - memoized to prevent re-renders // Define custom node types - memoized to prevent re-renders
const nodeTypes: NodeTypes = useMemo( const nodeTypes: NodeTypes = useMemo(
() => ({ () => ({
@ -351,8 +352,32 @@ function FlowchartEditorInner({ projectId, initialData }: FlowchartEditorProps)
}, [isSaving, nodes, edges, projectId]) }, [isSaving, nodes, edges, projectId])
const handleExport = useCallback(() => { const handleExport = useCallback(() => {
// TODO: Implement in US-035 // Convert React Flow state to FlowchartData
}, []) const flowchartData: FlowchartData = {
nodes: fromReactFlowNodes(nodes),
edges: fromReactFlowEdges(edges),
}
// Create pretty-printed JSON
const jsonContent = JSON.stringify(flowchartData, null, 2)
// Create blob with JSON content
const blob = new Blob([jsonContent], { type: 'application/json' })
// Create download URL
const url = URL.createObjectURL(blob)
// Create temporary link element and trigger download
const link = document.createElement('a')
link.href = url
link.download = `${projectName}.vnflow`
document.body.appendChild(link)
link.click()
// Cleanup
document.body.removeChild(link)
URL.revokeObjectURL(url)
}, [nodes, edges, projectName])
const handleImport = useCallback(() => { const handleImport = useCallback(() => {
// TODO: Implement in US-036 // TODO: Implement in US-036

View File

@ -66,6 +66,7 @@ export default async function EditorPage({ params }: PageProps) {
<div className="flex-1"> <div className="flex-1">
<FlowchartEditor <FlowchartEditor
projectId={project.id} projectId={project.id}
projectName={project.name}
initialData={flowchartData} initialData={flowchartData}
/> />
</div> </div>