feat: add flow export/import frontend + backend tests
Frontend: - ExportFlowModal with JSON/XML format selection + download - ImportFlowModal with drag-drop file picker + preview step - rfflowParser for client-side JSON/XML .rfflow parsing - Export buttons on editor toolbar and library action menus - Import button on library page next to Create New - Provenance display for imported flows in editor - flowTransfer API client + types Backend: - Fix regex->pattern deprecation in export endpoint - 12 integration tests covering export, import, round-trip, access control, tag/category creation, version validation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState, useCallback, useMemo } from 'react'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { X, RotateCcw, Play, Sparkles } from 'lucide-react'
|
||||
import { X, RotateCcw, Play, Sparkles, FileUp } from 'lucide-react'
|
||||
import { treesApi } from '@/api/trees'
|
||||
import { categoriesApi } from '@/api/categories'
|
||||
import { foldersApi } from '@/api/folders'
|
||||
@@ -8,6 +8,8 @@ import { sessionsApi } from '@/api/sessions'
|
||||
import type { TreeListItem, CategoryListItem, FolderListItem, Session } from '@/types'
|
||||
import { FolderEditModal } from '@/components/library/FolderEditModal'
|
||||
import { ForkModal } from '@/components/library/ForkModal'
|
||||
import { ExportFlowModal } from '@/components/library/ExportFlowModal'
|
||||
import { ImportFlowModal } from '@/components/library/ImportFlowModal'
|
||||
import { ConfirmDialog } from '@/components/common/ConfirmDialog'
|
||||
import { TreeGridView } from '@/components/library/TreeGridView'
|
||||
import { TreeListView } from '@/components/library/TreeListView'
|
||||
@@ -76,6 +78,10 @@ export function TreeLibraryPage() {
|
||||
// Fork modal state
|
||||
const [forkTarget, setForkTarget] = useState<TreeListItem | null>(null)
|
||||
|
||||
// Import/Export modal state
|
||||
const [showImportModal, setShowImportModal] = useState(false)
|
||||
const [exportTarget, setExportTarget] = useState<TreeListItem | null>(null)
|
||||
|
||||
// AI builder state
|
||||
const [showAIBuilder, setShowAIBuilder] = useState(false)
|
||||
const { aiEnabled } = useCachedQuota()
|
||||
@@ -250,6 +256,11 @@ export function TreeLibraryPage() {
|
||||
if (tree) setForkTarget(tree)
|
||||
}
|
||||
|
||||
const handleExportTree = (treeId: string) => {
|
||||
const tree = trees.find((t) => t.id === treeId)
|
||||
if (tree) setExportTarget(tree)
|
||||
}
|
||||
|
||||
const hasActiveFilters =
|
||||
selectedCategoryId || selectedTags.length > 0 || searchQuery || selectedFolderId
|
||||
|
||||
@@ -282,6 +293,13 @@ export function TreeLibraryPage() {
|
||||
Flow Assist
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setShowImportModal(true)}
|
||||
className="flex items-center gap-2 rounded-lg border border-border bg-[rgba(255,255,255,0.04)] px-4 py-2 text-sm font-medium text-foreground hover:border-[rgba(255,255,255,0.12)] transition-colors"
|
||||
>
|
||||
<FileUp className="h-4 w-4" />
|
||||
Import
|
||||
</button>
|
||||
<CreateFlowDropdown
|
||||
aiEnabled={aiEnabled}
|
||||
onOpenAIBuilder={() => setShowAIBuilder(true)}
|
||||
@@ -493,6 +511,7 @@ export function TreeLibraryPage() {
|
||||
setShowDeleteConfirm(true)
|
||||
}}
|
||||
onForkTree={handleForkTree}
|
||||
onExportTree={handleExportTree}
|
||||
pinnedTreeIds={pinnedTreeIds}
|
||||
onTogglePin={togglePin}
|
||||
pinLoadingTreeIds={pinLoadingTreeIds}
|
||||
@@ -509,6 +528,7 @@ export function TreeLibraryPage() {
|
||||
setShowDeleteConfirm(true)
|
||||
}}
|
||||
onForkTree={handleForkTree}
|
||||
onExportTree={handleExportTree}
|
||||
pinnedTreeIds={pinnedTreeIds}
|
||||
onTogglePin={togglePin}
|
||||
pinLoadingTreeIds={pinLoadingTreeIds}
|
||||
@@ -530,6 +550,7 @@ export function TreeLibraryPage() {
|
||||
)
|
||||
}}
|
||||
onForkTree={handleForkTree}
|
||||
onExportTree={handleExportTree}
|
||||
pinnedTreeIds={pinnedTreeIds}
|
||||
onTogglePin={togglePin}
|
||||
pinLoadingTreeIds={pinLoadingTreeIds}
|
||||
@@ -582,6 +603,20 @@ export function TreeLibraryPage() {
|
||||
onClose={() => setForkTarget(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{exportTarget && (
|
||||
<ExportFlowModal
|
||||
treeId={exportTarget.id}
|
||||
treeName={exportTarget.name}
|
||||
onClose={() => setExportTarget(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showImportModal && (
|
||||
<ImportFlowModal
|
||||
onClose={() => { setShowImportModal(false); loadTrees() }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user