Files
paperclip/ui/src/components/GoalTree.tsx
Forgotten 2ec45c49af feat(ui): add auth pages, company rail, inbox redesign, and page improvements
Add Auth sign-in/sign-up page and InviteLanding page for invite acceptance.
Add CloudAccessGate that checks deployment mode and redirects to /auth when
session is required. Add CompanyRail with drag-and-drop company switching.
Add MarkdownBody prose renderer. Redesign Inbox with category filters and
inline join-request approval. Refactor AgentDetail to overview/configure/runs
views with claude-login support. Replace navigate() anti-patterns with <Link>
components in Dashboard and MetricCard. Add live-run indicators in sidebar
agents. Fix LiveUpdatesProvider cache key resolution for issue identifiers.
Add auth, health, and access API clients.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 14:41:21 -06:00

117 lines
2.9 KiB
TypeScript

import type { Goal } from "@paperclip/shared";
import { Link } from "react-router-dom";
import { StatusBadge } from "./StatusBadge";
import { ChevronRight } from "lucide-react";
import { cn } from "../lib/utils";
import { useState } from "react";
interface GoalTreeProps {
goals: Goal[];
goalLink?: (goal: Goal) => string;
onSelect?: (goal: Goal) => void;
}
interface GoalNodeProps {
goal: Goal;
children: Goal[];
allGoals: Goal[];
depth: number;
goalLink?: (goal: Goal) => string;
onSelect?: (goal: Goal) => void;
}
function GoalNode({ goal, children, allGoals, depth, goalLink, onSelect }: GoalNodeProps) {
const [expanded, setExpanded] = useState(true);
const hasChildren = children.length > 0;
const link = goalLink?.(goal);
const inner = (
<>
{hasChildren ? (
<button
className="p-0.5"
onClick={(e) => {
e.preventDefault();
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} />
</>
);
const classes = cn(
"flex items-center gap-2 px-3 py-1.5 text-sm transition-colors cursor-pointer hover:bg-accent/50",
);
return (
<div>
{link ? (
<Link
to={link}
className={cn(classes, "no-underline text-inherit")}
style={{ paddingLeft: `${depth * 16 + 12}px` }}
>
{inner}
</Link>
) : (
<div
className={classes}
style={{ paddingLeft: `${depth * 16 + 12}px` }}
onClick={() => onSelect?.(goal)}
>
{inner}
</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}
goalLink={goalLink}
onSelect={onSelect}
/>
))}
</div>
)}
</div>
);
}
export function GoalTree({ goals, goalLink, 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 py-1">
{roots.map((goal) => (
<GoalNode
key={goal.id}
goal={goal}
children={goals.filter((g) => g.parentId === goal.id)}
allGoals={goals}
depth={0}
goalLink={goalLink}
onSelect={onSelect}
/>
))}
</div>
);
}