feat(FN-4371): complete remaining decision-only task handling
Fusion-Task-Id: FN-4371 Fusion-Task-Lineage: 4175ba4c-b7cb-4f3a-87c5-75e51f06c0ff
This commit is contained in:
@@ -132,7 +132,8 @@
|
||||
|
||||
.card-status-badge,
|
||||
.card-priority-badge,
|
||||
.card-size-badge {
|
||||
.card-size-badge,
|
||||
.card-no-commits-expected-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 0.625rem;
|
||||
@@ -148,6 +149,13 @@
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.card-no-commits-expected-badge {
|
||||
background: color-mix(in srgb, var(--text-muted) 18%, transparent);
|
||||
border-color: color-mix(in srgb, var(--text-muted) 35%, transparent);
|
||||
color: var(--text-muted);
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.card-status-badge.stalled-review {
|
||||
background: color-mix(in srgb, var(--color-warning) 18%, transparent);
|
||||
color: var(--color-warning);
|
||||
|
||||
@@ -1519,6 +1519,9 @@ function TaskCardComponent({
|
||||
<span className="visually-hidden">Fast mode</span>
|
||||
</span>
|
||||
)}
|
||||
{task.noCommitsExpected === true && (
|
||||
<span className="card-no-commits-expected-badge" title="Decision-only task">decision-only</span>
|
||||
)}
|
||||
{task.missionId && (
|
||||
<span
|
||||
className="card-mission-badge"
|
||||
|
||||
@@ -612,6 +612,8 @@ export function TaskDetailContent({
|
||||
const [isSavingInlinePriority, setIsSavingInlinePriority] = useState(false);
|
||||
const [inlineExecutionMode, setInlineExecutionMode] = useState<"standard" | "fast">(normalizeExecutionModeValue(task.executionMode));
|
||||
const [isSavingInlineExecutionMode, setIsSavingInlineExecutionMode] = useState(false);
|
||||
const [inlineNoCommitsExpected, setInlineNoCommitsExpected] = useState<boolean>(task.noCommitsExpected === true);
|
||||
const [isSavingInlineNoCommitsExpected, setIsSavingInlineNoCommitsExpected] = useState(false);
|
||||
const mountedRef = useRef(false);
|
||||
|
||||
// Split-menu dropdown state for footer actions
|
||||
@@ -689,6 +691,10 @@ export function TaskDetailContent({
|
||||
setInlineExecutionMode(normalizeExecutionModeValue(task.executionMode));
|
||||
}, [task.id, task.executionMode]);
|
||||
|
||||
useEffect(() => {
|
||||
setInlineNoCommitsExpected(task.noCommitsExpected === true);
|
||||
}, [task.id, task.noCommitsExpected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (githubTrackingEnabledDraft === null) return;
|
||||
if ((workingTask.githubTracking?.enabled === true) === githubTrackingEnabledDraft) {
|
||||
@@ -1231,6 +1237,29 @@ export function TaskDetailContent({
|
||||
}
|
||||
}, [task.id, task.executionMode, projectId, inlineExecutionMode, onTaskUpdated, addToast]);
|
||||
|
||||
const handleInlineNoCommitsExpectedToggle = useCallback(async () => {
|
||||
const nextValue = !inlineNoCommitsExpected;
|
||||
const previousValue = inlineNoCommitsExpected;
|
||||
|
||||
setInlineNoCommitsExpected(nextValue);
|
||||
setIsSavingInlineNoCommitsExpected(true);
|
||||
|
||||
try {
|
||||
const updatedTask = await updateTask(task.id, { noCommitsExpected: nextValue }, projectId);
|
||||
const normalizedUpdatedValue = updatedTask.noCommitsExpected === true;
|
||||
setInlineNoCommitsExpected(normalizedUpdatedValue);
|
||||
onTaskUpdated?.(updatedTask);
|
||||
addToast(`No-commits expectation ${normalizedUpdatedValue ? "enabled" : "disabled"}`, "success");
|
||||
} catch (err) {
|
||||
setInlineNoCommitsExpected(previousValue);
|
||||
addToast(`Failed to update ${task.id}: ${getErrorMessage(err)}`, "error");
|
||||
} finally {
|
||||
if (mountedRef.current) {
|
||||
setIsSavingInlineNoCommitsExpected(false);
|
||||
}
|
||||
}
|
||||
}, [task.id, projectId, inlineNoCommitsExpected, onTaskUpdated, addToast]);
|
||||
|
||||
// Handle keyboard shortcuts for edit mode
|
||||
const handleEditKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (!isEditing) return;
|
||||
@@ -2147,6 +2176,21 @@ export function TaskDetailContent({
|
||||
<span>{inlineExecutionMode === "fast" ? "Fast" : "Standard"}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="checkbox-label" htmlFor="detail-no-commits-expected-toggle">
|
||||
<input
|
||||
id="detail-no-commits-expected-toggle"
|
||||
type="checkbox"
|
||||
checked={inlineNoCommitsExpected}
|
||||
disabled={isSavingInlineNoCommitsExpected}
|
||||
onChange={() => {
|
||||
void handleInlineNoCommitsExpectedToggle();
|
||||
}}
|
||||
/>
|
||||
No commits expected (decision-only task)
|
||||
</label>
|
||||
<small>Allows the task to complete without producing git commits. Use for evaluation, verification, or audit tasks where the deliverable is the recorded decision.</small>
|
||||
</div>
|
||||
{provenanceDisplay && (
|
||||
<div className="detail-provenance">
|
||||
<GitBranch aria-hidden="true" />
|
||||
|
||||
@@ -578,6 +578,16 @@ describe("TaskCard", () => {
|
||||
expect(screen.queryByText("paused by agent")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders decision-only badge when noCommitsExpected is true", () => {
|
||||
render(<TaskCard task={makeTask({ noCommitsExpected: true })} onOpenDetail={noop} addToast={noop} />);
|
||||
expect(screen.getByText("decision-only")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("hides decision-only badge when noCommitsExpected is false", () => {
|
||||
render(<TaskCard task={makeTask({ noCommitsExpected: false })} onOpenDetail={noop} addToast={noop} />);
|
||||
expect(screen.queryByText("decision-only")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render fan-out badge when fanout is missing or zero", () => {
|
||||
const { container, rerender } = render(
|
||||
<TaskCard task={makeTask({ column: "todo" })} onOpenDetail={noop} addToast={noop} />,
|
||||
|
||||
@@ -1159,6 +1159,30 @@ describe("TaskDetailModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("toggles no-commits-expected checkbox and patches task", async () => {
|
||||
const { updateTask } = await import("../../api");
|
||||
const mockUpdate = vi.mocked(updateTask);
|
||||
mockUpdate.mockResolvedValueOnce(makeTask({ id: "FN-001", column: "todo", noCommitsExpected: true }) as Task);
|
||||
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ id: "FN-001", column: "todo", noCommitsExpected: false })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByLabelText("No commits expected (decision-only task)"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdate).toHaveBeenCalledWith("FN-001", { noCommitsExpected: true }, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it("pre-populates form with existing task values", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
|
||||
Reference in New Issue
Block a user