diff --git a/frontend/src/app/boards/[boardId]/page.tsx b/frontend/src/app/boards/[boardId]/page.tsx index 6449ea63..266ce164 100644 --- a/frontend/src/app/boards/[boardId]/page.tsx +++ b/frontend/src/app/boards/[boardId]/page.tsx @@ -24,6 +24,10 @@ import { Markdown } from "@/components/atoms/Markdown"; import { StatusDot } from "@/components/atoms/StatusDot"; import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; import { TaskBoard } from "@/components/organisms/TaskBoard"; +import { + DependencyBanner, + type DependencyBannerDependency, +} from "@/components/molecules/DependencyBanner"; import { DashboardShell } from "@/components/templates/DashboardShell"; import { BoardChatComposer } from "@/components/BoardChatComposer"; import { Button } from "@/components/ui/button"; @@ -2211,6 +2215,30 @@ export default function BoardDetailPage() { [loadComments], ); + const selectedTaskDependencies = useMemo(() => { + if (!selectedTask) return []; + const blockedDependencyIds = new Set(selectedTask.blocked_by_task_ids ?? []); + return (selectedTask.depends_on_task_ids ?? []).map((dependencyId) => { + const dependencyTask = taskById.get(dependencyId); + const statusLabel = dependencyTask?.status + ? dependencyTask.status.replace(/_/g, " ") + : "unknown"; + return { + id: dependencyId, + title: dependencyTask?.title ?? dependencyId, + statusLabel, + isBlocking: blockedDependencyIds.has(dependencyId), + isDone: dependencyTask?.status === "done", + disabled: !dependencyTask, + onClick: dependencyTask + ? () => { + openComments({ id: dependencyId }); + } + : undefined, + }; + }); + }, [openComments, selectedTask, taskById]); + useEffect(() => { if (!taskIdFromUrl) return; if (openedTaskIdFromUrlRef.current === taskIdFromUrl) return; @@ -3382,63 +3410,14 @@ export default function BoardDetailPage() {

Dependencies

- {selectedTask?.depends_on_task_ids?.length ? ( -
- {selectedTask.depends_on_task_ids.map((depId) => { - const depTask = taskById.get(depId); - const title = depTask?.title ?? depId; - const statusLabel = depTask?.status - ? depTask.status.replace(/_/g, " ") - : "unknown"; - const isDone = depTask?.status === "done"; - const isBlocking = ( - selectedTask.blocked_by_task_ids ?? [] - ).includes(depId); - return ( - - ); - })} -
- ) : ( -

No dependencies.

- )} - {selectedTask?.is_blocked ? ( -
- Blocked by incomplete dependencies. -
- ) : null} + + {selectedTask?.is_blocked + ? "Blocked by incomplete dependencies." + : null} +
diff --git a/frontend/src/components/molecules/DependencyBanner.tsx b/frontend/src/components/molecules/DependencyBanner.tsx new file mode 100644 index 00000000..aad178d2 --- /dev/null +++ b/frontend/src/components/molecules/DependencyBanner.tsx @@ -0,0 +1,92 @@ +import { type ReactNode } from "react"; + +import { cn } from "@/lib/utils"; + +export interface DependencyBannerDependency { + id: string; + title: string; + statusLabel: string; + isBlocking?: boolean; + isDone?: boolean; + onClick?: () => void; + disabled?: boolean; +} + +interface DependencyBannerProps { + variant?: DependencyBannerVariant; + dependencies?: DependencyBannerDependency[]; + children?: ReactNode; + className?: string; + emptyMessage?: string; +} + +const toneClassByVariant: Record = { + blocked: "border-rose-200 bg-rose-50 text-rose-700", + resolved: "border-blue-200 bg-blue-50 text-blue-700", +}; + +export function DependencyBanner({ + variant = "blocked", + dependencies = [], + children, + className, + emptyMessage = "No dependencies.", +}: DependencyBannerProps) { + return ( +
+ {dependencies.length > 0 ? ( + dependencies.map((dependency) => { + const isBlocking = dependency.isBlocking === true; + const isDone = dependency.isDone === true; + return ( + + ); + }) + ) : ( +

{emptyMessage}

+ )} + {children ? ( +
+ {children} +
+ ) : null} +
+ ); +}