import { useEffect, useState } from 'react' import { useNavigate, Link } from 'react-router-dom' import { Play, Pencil, Share2, Trash2, GitBranch, Clock, TrendingUp, FolderTree, Plus, ListOrdered, Wrench } from 'lucide-react' import { PageMeta } from '@/components/common/PageMeta' import { StaggerList } from '@/components/common/StaggerList' import { Button } from '@/components/ui/Button' import { treesApi } from '@/api/trees' import { sessionsApi } from '@/api/sessions' import type { TreeListItem } from '@/types' import { TagBadges } from '@/components/common/TagBadges' import { ConfirmDialog } from '@/components/common/ConfirmDialog' import { ShareTreeModal } from '@/components/library/ShareTreeModal' import { Spinner } from '@/components/common/Spinner' import { cn } from '@/lib/utils' import { useAuthStore } from '@/store/authStore' import { usePermissions } from '@/hooks/usePermissions' import { toast } from '@/lib/toast' import { ForkModal } from '@/components/library/ForkModal' import { CreateFlowDropdown } from '@/components/common/CreateFlowDropdown' interface TreeWithStats extends TreeListItem { lastUsed?: string sessionCount?: number parent_tree_id?: string | null parent_tree_name?: string | null } export function MyTreesPage() { const navigate = useNavigate() const { user } = useAuthStore() const { canEditTree, canCreateTrees } = usePermissions() const [trees, setTrees] = useState([]) const [isLoading, setIsLoading] = useState(true) const [treeToDelete, setTreeToDelete] = useState(null) const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) const [isDeleting, setIsDeleting] = useState(false) const [treeToShare, setTreeToShare] = useState(null) const [showShareModal, setShowShareModal] = useState(false) const [forkTarget, setForkTarget] = useState(null) useEffect(() => { loadMyTrees() }, [user?.id]) const loadMyTrees = async () => { if (!user?.id) return setIsLoading(true) try { // Fetch trees and recent sessions in parallel (2 API calls total, not N+1) const [userTrees, recentSessions] = await Promise.all([ treesApi.list({ author_id: user.id }), sessionsApi.list({ size: 100 }), ]) // Build a map of tree_id -> most recent session start time const lastUsedMap = new Map() for (const session of recentSessions) { const existing = lastUsedMap.get(session.tree_id) if (session.started_at && (!existing || new Date(session.started_at) > new Date(existing))) { lastUsedMap.set(session.tree_id, session.started_at) } } const treesWithStats: TreeWithStats[] = userTrees.map((tree) => ({ ...tree, lastUsed: lastUsedMap.get(tree.id), sessionCount: tree.usage_count ?? 0, })) setTrees(treesWithStats) } catch (err) { toast.error('Failed to load your flows') console.error(err) } finally { setIsLoading(false) } } const handleStartSession = (tree: TreeWithStats) => { if (tree.tree_type === 'maintenance') { navigate(`/flows/${tree.id}/maintenance`) } else if (tree.tree_type === 'procedural') { navigate(`/flows/${tree.id}/navigate`) } else { navigate(`/trees/${tree.id}/navigate`) } } const getEditPath = (tree: TreeWithStats) => { return tree.tree_type === 'procedural' || tree.tree_type === 'maintenance' ? `/flows/${tree.id}/edit` : `/trees/${tree.id}/edit` } const handleDeleteTree = async () => { if (!treeToDelete) return setIsDeleting(true) try { await treesApi.delete(treeToDelete.id) setTrees(trees.filter((t) => t.id !== treeToDelete.id)) toast.success(`"${treeToDelete.name}" deleted successfully`) } catch (err) { console.error('Failed to delete flow:', err) toast.error('Failed to delete flow') } finally { setIsDeleting(false) setShowDeleteConfirm(false) setTreeToDelete(null) } } const formatDate = (dateString?: string) => { if (!dateString) return 'Never' return new Date(dateString).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }) } return (

My Flows

Your forked and custom flows

{canCreateTrees && ( )}
{/* Loading State */} {isLoading ? (
) : trees.length === 0 ? (

No personal flows yet

Fork a flow from the library to customize it for your workflow

Browse Library {canCreateTrees && ( Create from Scratch )}
) : ( {trees.map((tree) => (
{/* Header */}
{tree.tree_type === 'procedural' && ( )} {tree.tree_type === 'maintenance' && ( )}

{tree.name}

{tree.parent_tree_id && ( Fork )}
{tree.tree_type === 'procedural' && ( Procedure )} {tree.tree_type === 'maintenance' && ( Maintenance )} {tree.category_info && ( {tree.category_info.name} )}
{/* Description */}

{tree.description || 'No description available'}

{/* Fork Badge */} {tree.parent_tree_id && (
Forked from{' '} original
)} {/* Tags */} {tree.tags && tree.tags.length > 0 && (
)} {/* Stats */}
{formatDate(tree.lastUsed)}
{tree.sessionCount || 0} uses
{/* Actions */}
{canEditTree({ author_id: tree.author_id, account_id: tree.account_id }) && ( )}
))}
)} {/* Delete Confirmation */} { setShowDeleteConfirm(false) setTreeToDelete(null) }} onConfirm={handleDeleteTree} title="Delete Flow" message={`Are you sure you want to delete "${treeToDelete?.name}"? This action can be undone by an administrator.`} confirmLabel="Delete" confirmVariant="destructive" isLoading={isDeleting} /> {/* Share Tree Modal */} {treeToShare && ( { setShowShareModal(false) setTreeToShare(null) }} /> )} {/* Fork Modal */} {forkTarget && ( setForkTarget(null)} /> )}
) } export default MyTreesPage