import "./GroupTaskModal.css"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { CheckCircle2, CircleDashed, ExternalLink, Loader2, X } from "lucide-react"; import { apiAbandonBranchGroup, apiGetBranchGroup, apiPromoteBranchGroup, type BranchGroupSummary } from "../api"; import { subscribeSse } from "../sse-bus"; interface GroupTaskModalProps { isOpen: boolean; onClose: () => void; groupId: string | null; projectId?: string; onOpenMemberTask: (taskId: string) => void; } export function GroupTaskModal({ isOpen, onClose, groupId, projectId, onOpenMemberTask }: GroupTaskModalProps) { const { t } = useTranslation("app"); const [group, setGroup] = useState(null); const [loading, setLoading] = useState(false); const [promoting, setPromoting] = useState(false); const [abandoning, setAbandoning] = useState(false); const [error, setError] = useState(null); const loadGroup = useCallback(async () => { if (!groupId) return; setLoading(true); try { const response = await apiGetBranchGroup(groupId, projectId); setGroup(response.group); setError(null); } catch (loadError) { setError(loadError instanceof Error ? loadError.message : t("groupTask.errorLoading", "Failed to load branch group")); } finally { setLoading(false); } }, [groupId, projectId]); useEffect(() => { if (!isOpen || !groupId) return; void loadGroup(); }, [groupId, isOpen, loadGroup]); useEffect(() => { if (!isOpen || !groupId) return; const query = projectId ? `?projectId=${encodeURIComponent(projectId)}` : ""; return subscribeSse(`/api/events${query}`, { events: { "task:updated": (event) => { try { const payload = JSON.parse(event.data) as { projectId?: string }; if (projectId && payload.projectId && payload.projectId !== projectId) { return; } } catch { // no-op } void loadGroup(); }, }, onReconnect: () => { void loadGroup(); }, }); }, [groupId, isOpen, loadGroup, projectId]); const completionText = useMemo(() => { if (!group) return ""; return t("groupTask.completionText", "{{landed}} of {{total}} members finished", { landed: group.completion.landed, total: group.completion.total }); }, [group, t]); const completionPercent = useMemo(() => { if (!group || group.completion.total <= 0) return 0; return (group.completion.landed / group.completion.total) * 100; }, [group]); const onPromote = useCallback(async () => { if (!groupId) return; setPromoting(true); try { await apiPromoteBranchGroup(groupId, projectId); await loadGroup(); } finally { setPromoting(false); } }, [groupId, loadGroup, projectId]); const onAbandon = useCallback(async () => { if (!groupId) return; setAbandoning(true); try { await apiAbandonBranchGroup(groupId, projectId); await loadGroup(); } finally { setAbandoning(false); } }, [groupId, loadGroup, projectId]); if (!isOpen || !groupId) return null; return (
event.stopPropagation()}>

{t("groupTask.title", "Branch Group {{id}}", { id: groupId })}

{loading && (
{t("groupTask.loading", "Loading branch group…")}
)} {!loading && error &&
{error}
} {!loading && !error && !group &&
{t("groupTask.unavailable", "Branch group unavailable")}
} {!loading && !error && group && ( <>
{t("groupTask.sharedBranch", "Shared branch")} {group.branchName}
{t("groupTask.status", "Status")} {group.status}
{completionText}

{t("groupTask.members", "Members")}

    {group.members.map((member) => (
  • {member.taskId} · {member.title} {member.column} {member.landed ? : }
  • ))}
{group.prUrl && (
PR #{group.prNumber ?? "—"} ({group.prState})
)} {(group.prState === "merged" || group.prState === "closed") && (
{group.prState === "merged" ? t("groupTask.prMerged", "Group PR merged") : t("groupTask.prClosed", "Group PR closed")}
)} {(group.completion.complete || group.prState === "open") && group.prState !== "merged" && group.prState !== "closed" && (
{/* Promote stays gated on completion; Abandon below is reachable whenever the PR is open, even if completion later reverts. */} {group.completion.complete && (group.autoMerge ? ( {t("groupTask.autoMergeEnabled", "Auto-merge enabled")} ) : ( ))} {group.prState === "open" && ( )}
)} )}
); }