Files
fusion/packages/dashboard/app/components/GroupTaskModal.tsx
gsxdsm 1e49494bac feat(i18n): full-sweep string migration — 5,930 keys across 5 locales (#1352)
Migration (multi-agent sweep over 216 files, 60 batches):
- Every user-visible dashboard + TUI string moved to t() with the exact
  English inline default (en rendering byte-identical)
- Catalogs merged from per-batch fragments: en/zh-CN/zh-TW/fr/es now
  carry ~5,930 keys each across common/app/errors/cli namespaces;
  CLI bundles regenerated (6 locales incl. ko)

Integration fixes:
- 18 type errors: reserved {{count}} interpolations renamed, malformed
  plural call, hand-rolled t-param types replaced with TFunction<"app">
- 23 lint errors: superseded label constants/helpers removed
- ExecutorStatusBar hook-order violation (keyboard-open early return
  moved below hooks)
- TUI tests wrapped in I18nextProvider (uninitialized fallback renders
  literal {{placeholders}}); dashboard vitest.setup boots a minimal en
  i18next instance for the same reason

Known WIP (next commits): ~457 residual strings across 50 batches,
Korean drafts for swept keys, and a dashboard test-suite pass that is
still being stabilized (~283 failures under investigation — fake-timer
waitFor interaction, likely stale node_modules vs merged lockfile).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 19:06:53 -07:00

162 lines
7.0 KiB
TypeScript

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 { 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<BranchGroupSummary | null>(null);
const [loading, setLoading] = useState(false);
const [promoting, setPromoting] = useState(false);
const [error, setError] = useState<string | null>(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]);
if (!isOpen || !groupId) return null;
return (
<div className="modal-overlay open" onClick={onClose}>
<div className="modal modal-lg group-task-modal" role="dialog" aria-modal="true" aria-label={t("groupTask.ariaLabel", "Branch group details")} onClick={(event) => event.stopPropagation()}>
<div className="modal-header">
<h2>{t("groupTask.title", "Branch Group {{id}}", { id: groupId })}</h2>
<button type="button" className="modal-close" onClick={onClose} aria-label={t("actions.closeModal", "Close modal")}>
<X />
</button>
</div>
<div className="modal-body group-task-modal-body">
{loading && (
<div className="card group-task-modal-state"><Loader2 className="spin" /> {t("groupTask.loading", "Loading branch group…")}</div>
)}
{!loading && error && <div className="card group-task-modal-state group-task-modal-error">{error}</div>}
{!loading && !error && !group && <div className="card group-task-modal-state">{t("groupTask.unavailable", "Branch group unavailable")}</div>}
{!loading && !error && group && (
<>
<section className="card group-task-modal-summary">
<div className="group-task-modal-summary-row">
<span className="group-task-modal-label">{t("groupTask.sharedBranch", "Shared branch")}</span>
<strong>{group.branchName}</strong>
</div>
<div className="group-task-modal-summary-row">
<span className="group-task-modal-label">{t("groupTask.status", "Status")}</span>
<span className="badge">{group.status}</span>
</div>
<div className="group-task-modal-progress-text">{completionText}</div>
<div className="branch-group-card-progress" role="progressbar" aria-valuenow={group.completion.landed} aria-valuemin={0} aria-valuemax={group.completion.total}>
<span className="branch-group-card-progress-fill" style={{ width: `${completionPercent}%` }} />
</div>
</section>
<section className="card group-task-modal-members-card">
<h3>{t("groupTask.members", "Members")}</h3>
<ul className="group-task-modal-members">
{group.members.map((member) => (
<li key={member.taskId} className="group-task-modal-member">
<span className={`status-dot ${member.landed ? "status-dot--online" : "status-dot--pending"}`} />
<span className="group-task-modal-member-main">{member.taskId} · {member.title}</span>
<span className="badge">{member.column}</span>
<span className="group-task-modal-member-status">{member.landed ? <CheckCircle2 /> : <CircleDashed />}</span>
<button type="button" className="btn btn-sm" onClick={() => onOpenMemberTask(member.taskId)}>{t("groupTask.openTask", "Open task")}</button>
</li>
))}
</ul>
</section>
{group.prUrl && (
<section className="card group-task-modal-pr">
<a className="btn" href={group.prUrl} target="_blank" rel="noreferrer">
PR #{group.prNumber ?? "—"} ({group.prState}) <ExternalLink />
</a>
</section>
)}
{group.completion.complete && (
<section className="card group-task-modal-actions">
{group.autoMerge ? (
<span className="badge">{t("groupTask.autoMergeEnabled", "Auto-merge enabled")}</span>
) : (
<button type="button" className="btn" onClick={() => void onPromote()} disabled={promoting}>
{promoting ? <Loader2 className="spin" /> : null}
{group.prState === "none" ? t("groupTask.openPR", "Open PR") : t("groupTask.mergeIntoMain", "Merge group into main")}
</button>
)}
</section>
)}
</>
)}
</div>
</div>
</div>
);
}