175 lines
6.7 KiB
TypeScript
175 lines
6.7 KiB
TypeScript
import { Link } from 'react-router-dom'
|
|
import { Pencil, Globe, Lock, Trash2, GitBranch, FileText, Download, ClipboardList } from 'lucide-react'
|
|
import type { TreeListItem } from '@/types'
|
|
import { TagBadges } from '@/components/common/TagBadges'
|
|
import { StaggerList } from '@/components/common/StaggerList'
|
|
import { cn } from '@/lib/utils'
|
|
import { usePermissions } from '@/hooks/usePermissions'
|
|
import { getTreeEditorPath } from '@/lib/routing'
|
|
|
|
interface TreeGridViewProps {
|
|
trees: TreeListItem[]
|
|
onStartSession: (treeId: string, treeType?: string) => void
|
|
onPrepareSession?: (tree: TreeListItem) => void
|
|
onTagClick: (tag: string) => void
|
|
onFolderCreated: (parentId?: string | null) => void
|
|
onDeleteTree: (tree: TreeListItem) => void
|
|
onForkTree?: (treeId: string) => void
|
|
onExportTree?: (treeId: string) => void
|
|
}
|
|
|
|
export function TreeGridView({
|
|
trees,
|
|
onStartSession,
|
|
onPrepareSession,
|
|
onTagClick,
|
|
onDeleteTree,
|
|
onForkTree,
|
|
onExportTree,
|
|
}: TreeGridViewProps) {
|
|
const { canEditTree, canDeleteTree } = usePermissions()
|
|
|
|
return (
|
|
<StaggerList className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
{trees.map((tree) => (
|
|
<div
|
|
key={tree.id}
|
|
data-testid="tree-card"
|
|
data-tree-id={tree.id}
|
|
className="relative bg-card border border-border rounded-2xl p-4 transition-all hover:-translate-y-0.5 hover:border-primary/30 hover:shadow-md sm:p-6"
|
|
>
|
|
<div className="mb-2 flex items-start justify-between gap-2">
|
|
<div className="flex items-center gap-2">
|
|
<h3 className="font-semibold text-foreground">{tree.name}</h3>
|
|
{tree.status === 'draft' && (
|
|
<span className="inline-flex items-center gap-1 rounded-full bg-yellow-400/10 px-2 py-0.5 text-xs font-medium text-yellow-400">
|
|
<FileText className="h-3 w-3" />
|
|
Draft
|
|
</span>
|
|
)}
|
|
{'fork_info' in tree && Boolean((tree as Record<string, unknown>).fork_info) && (
|
|
<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-2">
|
|
{tree.is_public ? (
|
|
<span title="Public tree">
|
|
<Globe className="h-4 w-4 text-muted-foreground" />
|
|
</span>
|
|
) : (
|
|
<span title="Private tree">
|
|
<Lock className="h-4 w-4 text-muted-foreground" />
|
|
</span>
|
|
)}
|
|
{tree.category_info && (
|
|
<span className="rounded-full bg-primary/10 border border-primary/20 px-2 py-0.5 text-xs text-primary">
|
|
{tree.category_info.name}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<p className="mb-3 text-sm text-muted-foreground line-clamp-2">
|
|
{tree.description || 'No description available'}
|
|
</p>
|
|
|
|
{/* Tags */}
|
|
{tree.tags && tree.tags.length > 0 && (
|
|
<div className="mb-3">
|
|
<TagBadges tags={tree.tags} maxVisible={3} onTagClick={onTagClick} />
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs text-muted-foreground">
|
|
v{tree.version} · {tree.usage_count} uses
|
|
</span>
|
|
<div className="flex items-center gap-2">
|
|
{onExportTree && (
|
|
<button
|
|
type="button"
|
|
onClick={() => onExportTree(tree.id)}
|
|
className={cn(
|
|
'rounded-md border border-border p-2 text-muted-foreground',
|
|
'hover:bg-accent hover:text-foreground'
|
|
)}
|
|
title="Export flow"
|
|
aria-label="Export flow"
|
|
>
|
|
<Download className="h-4 w-4" />
|
|
</button>
|
|
)}
|
|
{onForkTree && (
|
|
<button
|
|
type="button"
|
|
onClick={() => onForkTree(tree.id)}
|
|
className={cn(
|
|
'rounded-md border border-border p-2 text-muted-foreground',
|
|
'hover:bg-accent hover:text-foreground'
|
|
)}
|
|
title="Fork tree"
|
|
aria-label="Fork tree"
|
|
>
|
|
<GitBranch className="h-4 w-4" />
|
|
</button>
|
|
)}
|
|
{canEditTree({ author_id: tree.author_id, account_id: tree.account_id }) && (
|
|
<Link
|
|
to={getTreeEditorPath(tree.id, tree.tree_type)}
|
|
className={cn(
|
|
'rounded-md border border-border p-2 text-muted-foreground',
|
|
'hover:bg-accent hover:text-foreground'
|
|
)}
|
|
title="Edit tree"
|
|
aria-label="Edit tree"
|
|
>
|
|
<Pencil className="h-4 w-4" />
|
|
</Link>
|
|
)}
|
|
{canDeleteTree() && (
|
|
<button
|
|
type="button"
|
|
onClick={() => onDeleteTree(tree)}
|
|
className={cn(
|
|
'rounded-md border border-border p-1.5 text-muted-foreground',
|
|
'hover:bg-red-400/10 hover:text-red-400'
|
|
)}
|
|
title="Delete tree"
|
|
aria-label="Delete tree"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</button>
|
|
)}
|
|
{onPrepareSession && tree.tree_type !== 'troubleshooting' && (
|
|
<button
|
|
type="button"
|
|
onClick={() => onPrepareSession(tree)}
|
|
className={cn(
|
|
'rounded-md border border-border p-2 text-muted-foreground',
|
|
'hover:bg-blue-500/10 hover:text-blue-400 hover:border-blue-500/30'
|
|
)}
|
|
title="Prepare session for engineer"
|
|
aria-label="Prepare session"
|
|
>
|
|
<ClipboardList className="h-4 w-4" />
|
|
</button>
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={() => onStartSession(tree.id, tree.tree_type)}
|
|
className={cn(
|
|
'rounded-md border border-primary/40 px-3 py-2 text-sm font-medium text-primary',
|
|
'hover:bg-primary/10 hover:border-primary/60 transition-colors'
|
|
)}
|
|
>
|
|
Start Session
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</StaggerList>
|
|
)
|
|
}
|