feat(FN-3940): expose github tracking controls in task detail modal
Adds GitHub tracking controls to the TaskDetailModal with corresponding test coverage, and documents the feature in the dashboard guide and task management docs. Fusion-Task-Id: FN-3940
This commit is contained in:
@@ -384,6 +384,7 @@ function getProvenanceLabel(task: Task | TaskDetail, options: ProvenanceLabelOpt
|
||||
const DESCRIPTION_TRUNCATE_LENGTH = 200;
|
||||
|
||||
const EDITABLE_COLUMNS: Set<Column> = new Set(["triage", "todo"]);
|
||||
const GITHUB_TRACKING_EDITABLE_COLUMNS: Set<Column> = new Set(["triage", "todo", "in-progress", "in-review"]);
|
||||
|
||||
export function TaskDetailContent({
|
||||
task,
|
||||
@@ -775,14 +776,16 @@ export function TaskDetailContent({
|
||||
|
||||
// Check if task can be edited
|
||||
const canEdit = EDITABLE_COLUMNS.has(task.column) && !isSaving;
|
||||
const canEditGithubTracking = GITHUB_TRACKING_EDITABLE_COLUMNS.has(task.column) && !isSaving;
|
||||
const githubTrackingEnabled = task.githubTracking?.enabled === true;
|
||||
const githubTrackedIssue = task.githubTracking?.issue;
|
||||
const showGithubTrackingSection = githubTrackingEnabled || Boolean(githubTrackedIssue);
|
||||
const showGithubTrackingSection = canEditGithubTracking || githubTrackingEnabled || Boolean(githubTrackedIssue);
|
||||
const githubTrackingStatus = githubTrackedIssue ? "Linked" : githubTrackingEnabled ? "Enabled" : "Disabled";
|
||||
const effectiveGithubRepoDefault = resolveEffectiveGithubRepoDefault(settings ?? null, globalSettings);
|
||||
const githubRepoOverrideTrimmed = githubRepoOverrideDraft.trim();
|
||||
|
||||
const handleToggleGithubTracking = useCallback(async () => {
|
||||
if (!canEdit || isSavingGithubTracking) return;
|
||||
if (!canEditGithubTracking || isSavingGithubTracking) return;
|
||||
setIsSavingGithubTracking(true);
|
||||
try {
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
@@ -796,10 +799,10 @@ export function TaskDetailContent({
|
||||
} finally {
|
||||
if (mountedRef.current) setIsSavingGithubTracking(false);
|
||||
}
|
||||
}, [addToast, canEdit, githubTrackingEnabled, isSavingGithubTracking, onTaskUpdated, projectId, task.id]);
|
||||
}, [addToast, canEditGithubTracking, githubTrackingEnabled, isSavingGithubTracking, onTaskUpdated, projectId, task.id]);
|
||||
|
||||
const handleSaveGithubRepoOverride = useCallback(async () => {
|
||||
if (!canEdit || isSavingGithubTracking) return;
|
||||
if (!canEditGithubTracking || isSavingGithubTracking) return;
|
||||
if (githubRepoOverrideTrimmed.length > 0 && !REPO_OVERRIDE_RE.test(githubRepoOverrideTrimmed)) {
|
||||
setGithubRepoOverrideError("Repository override must be in owner/repo format");
|
||||
return;
|
||||
@@ -818,7 +821,7 @@ export function TaskDetailContent({
|
||||
} finally {
|
||||
if (mountedRef.current) setIsSavingGithubTracking(false);
|
||||
}
|
||||
}, [addToast, canEdit, githubRepoOverrideTrimmed, isSavingGithubTracking, onTaskUpdated, projectId, task.id]);
|
||||
}, [addToast, canEditGithubTracking, githubRepoOverrideTrimmed, isSavingGithubTracking, onTaskUpdated, projectId, task.id]);
|
||||
|
||||
const enterEditMode = useCallback(() => {
|
||||
if (!canEdit) return;
|
||||
@@ -2333,9 +2336,13 @@ export function TaskDetailContent({
|
||||
<span className="detail-source-label">GitHub tracking</span>
|
||||
<span className="detail-source-provider-badge" aria-label="GitHub tracking status">
|
||||
<GitBranch aria-hidden="true" />
|
||||
<span>{githubTrackedIssue ? "Linked" : "Pending"}</span>
|
||||
<span>{githubTrackingStatus}</span>
|
||||
</span>
|
||||
{!githubTrackedIssue && <span className="detail-source-empty">Not yet created</span>}
|
||||
{!githubTrackedIssue && (
|
||||
<span className="detail-source-empty">
|
||||
{githubTrackingEnabled ? "Issue not yet created" : "Tracking is currently disabled"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{githubTrackedIssue && (
|
||||
@@ -2362,7 +2369,7 @@ export function TaskDetailContent({
|
||||
</div>
|
||||
</dl>
|
||||
)}
|
||||
{canEdit && (
|
||||
{canEditGithubTracking && (
|
||||
<div className="detail-github-tracking-controls">
|
||||
<label className="checkbox-label" htmlFor="detail-github-tracking-toggle">
|
||||
<input
|
||||
|
||||
@@ -106,7 +106,6 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
expect(screen.queryByLabelText("GitHub source issue")).toBeNull();
|
||||
expect(document.querySelector(".detail-source-provider-badge")).toBeNull();
|
||||
expect(screen.getByRole("link", { name: "(#42)" })).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -2125,10 +2124,27 @@ describe("TaskDetailModal", () => {
|
||||
expect(screen.getByRole("link", { name: "runfusion/fusion#123" })).toHaveAttribute("href", "https://github.com/runfusion/fusion/issues/123");
|
||||
});
|
||||
|
||||
it("hides section when tracking is disabled and no issue exists", () => {
|
||||
it("shows section when tracking is disabled and task is in an eligible column", () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ githubTracking: { enabled: false } })}
|
||||
task={makeTask({ column: "todo", githubTracking: { enabled: false } })}
|
||||
onClose={noop}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("GitHub tracking")).toBeTruthy();
|
||||
expect(screen.getByText("Tracking is currently disabled")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("hides section when tracking is disabled and task is not in an eligible column", () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ column: "done", githubTracking: { enabled: false } })}
|
||||
onClose={noop}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
onMoveTask={noopMove}
|
||||
@@ -2141,7 +2157,7 @@ describe("TaskDetailModal", () => {
|
||||
expect(screen.queryByText("GitHub tracking")).toBeNull();
|
||||
});
|
||||
|
||||
it("sends githubTracking enabled toggle payload", async () => {
|
||||
it("sends githubTracking disabled→enabled toggle payload", async () => {
|
||||
const { updateTask } = await import("../../api");
|
||||
const mockUpdate = vi.mocked(updateTask);
|
||||
mockUpdate.mockResolvedValueOnce({ id: "FN-001" } as Task);
|
||||
@@ -2177,6 +2193,35 @@ describe("TaskDetailModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("sends githubTracking enabled→disabled toggle payload", async () => {
|
||||
const { updateTask } = await import("../../api");
|
||||
const mockUpdate = vi.mocked(updateTask);
|
||||
mockUpdate.mockResolvedValueOnce({ id: "FN-001" } as Task);
|
||||
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({
|
||||
id: "FN-001",
|
||||
column: "in-progress",
|
||||
githubTracking: {
|
||||
enabled: true,
|
||||
},
|
||||
})}
|
||||
onClose={noop}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByLabelText("Enable GitHub tracking"));
|
||||
await waitFor(() => {
|
||||
expect(mockUpdate).toHaveBeenCalledWith("FN-001", { githubTracking: { enabled: false } }, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it("sends repo override updates and null when cleared", async () => {
|
||||
const { updateTask } = await import("../../api");
|
||||
const mockUpdate = vi.mocked(updateTask);
|
||||
|
||||
Reference in New Issue
Block a user