Files
resolutionflow/frontend/src/components/library/TreeTableView.tsx
chihlasm 61c410e366 feat: replace orange-* Tailwind classes with blue-* equivalents
orange-400→blue-400, orange-500→blue-500, orange-600→blue-600
across ~21 component files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:20:24 +00:00

282 lines
12 KiB
TypeScript

import { useState } from 'react'
import { Link } from 'react-router-dom'
import { Pencil, Globe, Lock, ChevronUp, ChevronDown, GitBranch, FileText, Trash2, Download, ClipboardList } from 'lucide-react'
import type { TreeListItem } from '@/types'
import { TagBadges } from '@/components/common/TagBadges'
import { cn } from '@/lib/utils'
import { usePermissions } from '@/hooks/usePermissions'
import { getTreeEditorPath } from '@/lib/routing'
interface TreeTableViewProps {
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
onSortChange?: (sortBy: string) => void
onForkTree?: (treeId: string) => void
onExportTree?: (treeId: string) => void
}
type SortColumn = 'name' | 'category' | 'version' | 'usage' | 'updated'
export function TreeTableView({
trees,
onStartSession,
onPrepareSession,
onTagClick,
onDeleteTree,
onSortChange,
onForkTree,
onExportTree,
}: TreeTableViewProps) {
const { canEditTree } = usePermissions()
const [sortColumn, setSortColumn] = useState<SortColumn | null>(null)
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc')
const handleSort = (column: SortColumn) => {
const newDirection =
sortColumn === column && sortDirection === 'asc' ? 'desc' : 'asc'
setSortColumn(column)
setSortDirection(newDirection)
// Map to API sort values
const sortMap: Record<string, string> = {
name_asc: 'name',
name_desc: 'name_desc',
usage_asc: 'usage_count',
usage_desc: 'usage_count',
updated_asc: 'updated_at',
updated_desc: 'updated_at',
version_asc: 'version',
version_desc: 'version',
}
const sortKey = `${column}_${newDirection}`
const apiSort = sortMap[sortKey] || 'usage_count'
onSortChange?.(apiSort)
}
const getSortIcon = (column: SortColumn) => {
if (sortColumn !== column) return null
return sortDirection === 'asc' ? (
<ChevronUp className="h-3.5 w-3.5" />
) : (
<ChevronDown className="h-3.5 w-3.5" />
)
}
const formatDate = (dateString: string) => {
const date = new Date(dateString)
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
}
return (
<div className="overflow-x-auto rounded-2xl border border-border">
<table className="w-full">
<thead className="bg-accent/50 sticky top-0 z-10">
<tr className="border-b border-border">
<th
className="px-4 py-3 text-left text-sm font-medium text-muted-foreground cursor-pointer hover:text-foreground"
onClick={() => handleSort('name')}
>
<div className="flex items-center gap-1">
Name
{getSortIcon('name')}
</div>
</th>
<th className="hidden md:table-cell px-4 py-3 text-left text-sm font-medium text-muted-foreground">
Description
</th>
<th
className="hidden lg:table-cell px-4 py-3 text-left text-sm font-medium text-muted-foreground cursor-pointer hover:text-foreground"
onClick={() => handleSort('category')}
>
<div className="flex items-center gap-1">
Category
{getSortIcon('category')}
</div>
</th>
<th className="hidden xl:table-cell px-4 py-3 text-left text-sm font-medium text-muted-foreground">
Tags
</th>
<th
className="hidden sm:table-cell px-4 py-3 text-center text-sm font-medium text-muted-foreground cursor-pointer hover:text-foreground"
onClick={() => handleSort('version')}
>
<div className="flex items-center justify-center gap-1">
Ver.
{getSortIcon('version')}
</div>
</th>
<th
className="hidden sm:table-cell px-4 py-3 text-center text-sm font-medium text-muted-foreground cursor-pointer hover:text-foreground"
onClick={() => handleSort('usage')}
>
<div className="flex items-center justify-center gap-1">
Uses
{getSortIcon('usage')}
</div>
</th>
<th
className="hidden md:table-cell px-4 py-3 text-left text-sm font-medium text-muted-foreground cursor-pointer hover:text-foreground"
onClick={() => handleSort('updated')}
>
<div className="flex items-center gap-1">
Updated
{getSortIcon('updated')}
</div>
</th>
<th className="px-4 py-3 text-right text-sm font-medium text-muted-foreground">
Actions
</th>
</tr>
</thead>
<tbody className="bg-transparent">
{trees.map((tree) => (
<tr key={tree.id} className="border-b border-border last:border-0 hover:bg-accent/50">
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<span className="font-medium text-foreground truncate max-w-[200px]">
{tree.name}
</span>
{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 shrink-0">
<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>
)}
{tree.is_public ? (
<span title="Public tree">
<Globe className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
</span>
) : (
<span title="Private tree">
<Lock className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
</span>
)}
</div>
</td>
<td className="hidden md:table-cell px-4 py-3 text-sm text-muted-foreground">
<span className="truncate block max-w-[250px]">
{tree.description || 'No description'}
</span>
</td>
<td className="hidden lg:table-cell px-4 py-3">
{tree.category_info && (
<span className="inline-block rounded-full bg-primary/10 border border-primary/20 px-2 py-0.5 text-xs text-primary">
{tree.category_info.name}
</span>
)}
</td>
<td className="hidden xl:table-cell px-4 py-3">
{tree.tags && tree.tags.length > 0 && (
<TagBadges tags={tree.tags} maxVisible={2} onTagClick={onTagClick} />
)}
</td>
<td className="hidden sm:table-cell px-4 py-3 text-center text-sm text-muted-foreground">
v{tree.version}
</td>
<td className="hidden sm:table-cell px-4 py-3 text-center text-sm text-muted-foreground">
{tree.usage_count}
</td>
<td className="hidden md:table-cell px-4 py-3 text-sm text-muted-foreground">
{formatDate(tree.updated_at)}
</td>
<td className="px-4 py-3">
<div className="flex items-center justify-end gap-2">
{onExportTree && (
<button
type="button"
onClick={() => onExportTree(tree.id)}
className={cn(
'rounded-md border border-border p-1.5 text-muted-foreground',
'hover:bg-accent hover:text-foreground'
)}
title="Export flow"
aria-label="Export flow"
>
<Download className="h-3.5 w-3.5" />
</button>
)}
{onForkTree && (
<button
type="button"
onClick={() => onForkTree(tree.id)}
className={cn(
'rounded-md border border-border p-1.5 text-muted-foreground',
'hover:bg-accent hover:text-foreground'
)}
title="Fork tree"
aria-label="Fork tree"
>
<GitBranch className="h-3.5 w-3.5" />
</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-1.5 text-muted-foreground',
'hover:bg-accent hover:text-foreground'
)}
title="Edit tree"
aria-label="Edit tree"
>
<Pencil className="h-3.5 w-3.5" />
</Link>
<button
type="button"
onClick={() => onDeleteTree(tree)}
className={cn(
'rounded-md border border-border p-1.5 text-muted-foreground',
'hover:bg-red-500/20 hover:text-red-400'
)}
title="Delete tree"
aria-label="Delete tree"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</>
)}
{onPrepareSession && tree.tree_type !== 'troubleshooting' && (
<button
type="button"
onClick={() => onPrepareSession(tree)}
className={cn(
'rounded-md border border-border p-1.5 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-3.5 w-3.5" />
</button>
)}
<button
type="button"
onClick={() => onStartSession(tree.id, tree.tree_type)}
className={cn(
'rounded-md border border-primary/40 px-3 py-1.5 text-xs font-medium text-primary',
'hover:bg-primary/10 hover:border-primary/60 transition-colors whitespace-nowrap'
)}
>
Start
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}