Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox, MyIssues. New feature components: AgentProperties, GoalProperties, IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add heartbeats API client. Restyle all list pages (Agents, Issues, Goals, Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar, and improved layouts. Add routing for detail views. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
91
ui/src/components/GoalTree.tsx
Normal file
91
ui/src/components/GoalTree.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { Goal } from "@paperclip/shared";
|
||||
import { StatusBadge } from "./StatusBadge";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { cn } from "../lib/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
interface GoalTreeProps {
|
||||
goals: Goal[];
|
||||
onSelect?: (goal: Goal) => void;
|
||||
}
|
||||
|
||||
interface GoalNodeProps {
|
||||
goal: Goal;
|
||||
children: Goal[];
|
||||
allGoals: Goal[];
|
||||
depth: number;
|
||||
onSelect?: (goal: Goal) => void;
|
||||
}
|
||||
|
||||
function GoalNode({ goal, children, allGoals, depth, onSelect }: GoalNodeProps) {
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
const hasChildren = children.length > 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-1.5 rounded-md text-sm transition-colors cursor-pointer hover:bg-accent/50",
|
||||
)}
|
||||
style={{ paddingLeft: `${depth * 20 + 12}px` }}
|
||||
onClick={() => onSelect?.(goal)}
|
||||
>
|
||||
{hasChildren ? (
|
||||
<button
|
||||
className="p-0.5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setExpanded(!expanded);
|
||||
}}
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn("h-3 w-3 transition-transform", expanded && "rotate-90")}
|
||||
/>
|
||||
</button>
|
||||
) : (
|
||||
<span className="w-4" />
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground capitalize">{goal.level}</span>
|
||||
<span className="flex-1 truncate">{goal.title}</span>
|
||||
<StatusBadge status={goal.status} />
|
||||
</div>
|
||||
{hasChildren && expanded && (
|
||||
<div>
|
||||
{children.map((child) => (
|
||||
<GoalNode
|
||||
key={child.id}
|
||||
goal={child}
|
||||
children={allGoals.filter((g) => g.parentId === child.id)}
|
||||
allGoals={allGoals}
|
||||
depth={depth + 1}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function GoalTree({ goals, onSelect }: GoalTreeProps) {
|
||||
const roots = goals.filter((g) => !g.parentId);
|
||||
|
||||
if (goals.length === 0) {
|
||||
return <p className="text-sm text-muted-foreground">No goals.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border border-border rounded-md py-1">
|
||||
{roots.map((goal) => (
|
||||
<GoalNode
|
||||
key={goal.id}
|
||||
goal={goal}
|
||||
children={goals.filter((g) => g.parentId === goal.id)}
|
||||
allGoals={goals}
|
||||
depth={0}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user