Files
resolutionflow/frontend/src/pages/MyTreesPage.tsx
chihlasm 0484a1cb12 fix: restore removed icon imports in MyTreesPage, add default export to SessionHistoryPage
- FolderTree, Plus, ListOrdered, Wrench are still used in empty state
  and tree card rendering — restore the imports
- SessionHistoryPage needs default export for lazyWithRetry

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 01:54:48 +00:00

353 lines
13 KiB
TypeScript

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<TreeWithStats[]>([])
const [isLoading, setIsLoading] = useState(true)
const [treeToDelete, setTreeToDelete] = useState<TreeWithStats | null>(null)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const [treeToShare, setTreeToShare] = useState<TreeWithStats | null>(null)
const [showShareModal, setShowShareModal] = useState(false)
const [forkTarget, setForkTarget] = useState<TreeWithStats | null>(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<string, string>()
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 (
<div className="overflow-y-auto h-full">
<PageMeta title="My Flows" />
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
<div className="mb-6 flex items-center justify-between sm:mb-8">
<div>
<h1 className="text-2xl font-bold font-heading text-foreground sm:text-3xl">My Flows</h1>
<p className="mt-2 text-muted-foreground">
Your forked and custom flows
</p>
</div>
{canCreateTrees && (
<CreateFlowDropdown label="Create New" />
)}
</div>
{/* Loading State */}
{isLoading ? (
<div className="flex justify-center py-12">
<Spinner />
</div>
) : trees.length === 0 ? (
<div className="rounded-lg border border-dashed border-border bg-accent px-4 py-12 text-center">
<FolderTree className="mx-auto mb-4 h-12 w-12 text-muted-foreground" />
<h2 className="mb-2 text-lg font-semibold text-foreground">No personal flows yet</h2>
<p className="mb-4 text-sm text-muted-foreground">
Fork a flow from the library to customize it for your workflow
</p>
<div className="flex items-center justify-center gap-3">
<Link
to="/trees"
className={cn(
'inline-flex items-center gap-2 rounded-md bg-primary text-white px-4 py-2 text-sm font-medium',
'hover:brightness-110'
)}
>
Browse Library
</Link>
{canCreateTrees && (
<Link
to="/trees/new"
className={cn(
'inline-flex items-center gap-2 rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground',
'hover:bg-accent hover:text-foreground'
)}
>
<Plus className="h-4 w-4" />
Create from Scratch
</Link>
)}
</div>
</div>
) : (
<StaggerList className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{trees.map((tree) => (
<div
key={tree.id}
className="bg-card border border-border rounded-xl p-4 transition-all hover:border-border/80 sm:p-6"
>
{/* Header */}
<div className="mb-3 flex items-start justify-between gap-2">
<div className="flex items-center gap-2">
{tree.tree_type === 'procedural' && (
<ListOrdered className="h-4 w-4 shrink-0 text-muted-foreground" />
)}
{tree.tree_type === 'maintenance' && (
<Wrench className="h-4 w-4 shrink-0 text-amber-400" />
)}
<h3 className="font-semibold text-foreground">{tree.name}</h3>
{tree.parent_tree_id && (
<span className="shrink-0 rounded-full bg-violet-400/15 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-violet-400">
Fork
</span>
)}
</div>
<div className="flex items-center gap-1.5">
{tree.tree_type === 'procedural' && (
<span className="rounded-full bg-blue-400/10 px-2 py-0.5 text-[10px] font-medium text-blue-400">
Procedure
</span>
)}
{tree.tree_type === 'maintenance' && (
<span className="rounded-full bg-amber-400/10 px-2 py-0.5 text-[10px] font-medium text-amber-400">
Maintenance
</span>
)}
{tree.category_info && (
<span className="rounded-full bg-accent px-2 py-0.5 text-xs text-muted-foreground">
{tree.category_info.name}
</span>
)}
</div>
</div>
{/* Description */}
<p className="mb-3 text-sm text-muted-foreground line-clamp-2">
{tree.description || 'No description available'}
</p>
{/* Fork Badge */}
{tree.parent_tree_id && (
<div className="mb-3 flex items-center gap-2 rounded-md bg-accent px-2 py-1.5 text-sm">
<GitBranch className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">
Forked from{' '}
<Link
to={`/trees/${tree.parent_tree_id}/navigate`}
className="font-medium text-foreground hover:underline"
>
original
</Link>
</span>
</div>
)}
{/* Tags */}
{tree.tags && tree.tags.length > 0 && (
<div className="mb-3">
<TagBadges tags={tree.tags} maxVisible={3} />
</div>
)}
{/* Stats */}
<div className="mb-4 flex items-center gap-4 text-xs text-muted-foreground">
<div className="flex items-center gap-1">
<Clock className="h-3.5 w-3.5" />
<span>{formatDate(tree.lastUsed)}</span>
</div>
<div className="flex items-center gap-1">
<TrendingUp className="h-3.5 w-3.5" />
<span>{tree.sessionCount || 0} uses</span>
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-2">
<Button
onClick={() => handleStartSession(tree)}
className="flex-1"
>
<Play className="h-4 w-4" />
Start
</Button>
{canEditTree({ author_id: tree.author_id, account_id: tree.account_id }) && (
<Link
to={getEditPath(tree)}
className={cn(
'rounded-md border border-border p-2 text-muted-foreground',
'hover:bg-accent hover:text-foreground'
)}
title="Edit tree"
>
<Pencil className="h-4 w-4" />
</Link>
)}
<Button
variant="secondary"
size="icon"
onClick={() => {
setTreeToShare(tree)
setShowShareModal(true)
}}
title="Share tree"
>
<Share2 className="h-4 w-4" />
</Button>
<Button
variant="secondary"
size="icon"
onClick={() => setForkTarget(tree)}
title="Fork flow"
>
<GitBranch className="h-4 w-4" />
</Button>
<Button
variant="destructive"
size="icon"
onClick={() => {
setTreeToDelete(tree)
setShowDeleteConfirm(true)
}}
title="Delete tree"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</StaggerList>
)}
{/* Delete Confirmation */}
<ConfirmDialog
isOpen={showDeleteConfirm}
onClose={() => {
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 && (
<ShareTreeModal
tree={treeToShare}
isOpen={showShareModal}
onClose={() => {
setShowShareModal(false)
setTreeToShare(null)
}}
/>
)}
{/* Fork Modal */}
{forkTarget && (
<ForkModal
treeId={forkTarget.id}
treeName={forkTarget.name}
onClose={() => setForkTarget(null)}
/>
)}
</div>
</div>
)
}
export default MyTreesPage