feat(FN-3972): align metadata controls in TaskDetailModal

Refined TaskDetailModal metadata controls layout and styling, including test coverage to lock down the component contract.

Fusion-Task-Id: FN-3972
This commit is contained in:
Fusion
2026-05-10 21:29:58 -07:00
committed by gsxdsm
parent 407ce618b7
commit 5d5689d597
4 changed files with 108 additions and 45 deletions

View File

@@ -167,12 +167,18 @@
}
}
.detail-priority-chip {
.detail-meta-inline-controls {
--detail-priority-control-min-height: calc(var(--space-2xl) + var(--space-xs));
display: inline-flex;
align-items: stretch;
gap: var(--space-xs);
}
.detail-priority-chip {
gap: var(--space-xs);
margin-left: var(--space-xs);
min-height: var(--detail-priority-control-min-height);
box-sizing: border-box;
}
.detail-priority-chip--saving {
@@ -211,8 +217,8 @@
display: inline-flex;
align-items: center;
gap: var(--space-xs);
margin-left: var(--space-xs);
min-height: calc(var(--space-2xl) + var(--space-xs));
min-height: var(--detail-priority-control-min-height);
box-sizing: border-box;
}
.detail-execution-mode-toggle svg {
@@ -1529,20 +1535,17 @@
align-items: stretch;
}
.detail-priority-chip {
margin-left: 0;
margin-top: var(--space-xs);
.detail-meta-inline-controls {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--space-xs);
}
.detail-priority-select {
min-height: var(--detail-priority-control-min-height);
}
.detail-execution-mode-toggle {
margin-left: 0;
margin-top: var(--space-xs);
}
.detail-source-header {
align-items: flex-start;
}

View File

@@ -1956,39 +1956,41 @@ export function TaskDetailContent({
);
})()}
<div className="detail-meta">
<label
className={`card-priority-badge card-priority-badge--${inlinePriority} detail-priority-chip ${isSavingInlinePriority ? "detail-priority-chip--saving" : ""}`}
>
<span>Priority:</span>
<select
className="detail-priority-select"
value={inlinePriority}
onChange={(event) => {
void handleInlinePriorityChange(event.target.value);
}}
disabled={isSavingInlinePriority}
aria-label="Task priority"
<div className="detail-meta-inline-controls" data-testid="detail-meta-inline-controls">
<label
className={`card-priority-badge card-priority-badge--${inlinePriority} detail-priority-chip ${isSavingInlinePriority ? "detail-priority-chip--saving" : ""}`}
>
{TASK_PRIORITIES.map((priorityOption) => (
<option key={priorityOption} value={priorityOption}>
{priorityOption}
</option>
))}
</select>
</label>
<button
type="button"
className={`btn btn-sm detail-execution-mode-toggle ${inlineExecutionMode === "fast" ? "detail-execution-mode-toggle--fast" : ""} ${isSavingInlineExecutionMode ? "detail-execution-mode-toggle--saving" : ""}`}
onClick={() => {
void handleInlineExecutionModeToggle();
}}
disabled={isSavingInlineExecutionMode}
aria-label={`Execution mode: ${inlineExecutionMode}`}
aria-pressed={inlineExecutionMode === "fast"}
>
<Zap aria-hidden="true" />
<span>{inlineExecutionMode === "fast" ? "Fast" : "Standard"}</span>
</button>
<span>Priority:</span>
<select
className="detail-priority-select"
value={inlinePriority}
onChange={(event) => {
void handleInlinePriorityChange(event.target.value);
}}
disabled={isSavingInlinePriority}
aria-label="Task priority"
>
{TASK_PRIORITIES.map((priorityOption) => (
<option key={priorityOption} value={priorityOption}>
{priorityOption}
</option>
))}
</select>
</label>
<button
type="button"
className={`btn btn-sm detail-execution-mode-toggle ${inlineExecutionMode === "fast" ? "detail-execution-mode-toggle--fast" : ""} ${isSavingInlineExecutionMode ? "detail-execution-mode-toggle--saving" : ""}`}
onClick={() => {
void handleInlineExecutionModeToggle();
}}
disabled={isSavingInlineExecutionMode}
aria-label={`Execution mode: ${inlineExecutionMode}`}
aria-pressed={inlineExecutionMode === "fast"}
>
<Zap aria-hidden="true" />
<span>{inlineExecutionMode === "fast" ? "Fast" : "Standard"}</span>
</button>
</div>
{provenanceDisplay && (
<div className="detail-provenance">
<GitBranch aria-hidden="true" />

View File

@@ -853,6 +853,36 @@ describe("TaskDetailModal", () => {
expect(prioritySelect.value).toBe("normal");
});
it("renders priority select and execution mode toggle together and keeps both interactive", async () => {
const { updateTask } = await import("../../api");
const mockUpdate = vi.mocked(updateTask);
mockUpdate
.mockResolvedValueOnce(makeTask({ id: "FN-001", column: "todo", priority: "urgent", executionMode: "standard" }) as Task)
.mockResolvedValueOnce(makeTask({ id: "FN-001", column: "todo", priority: "urgent", executionMode: "fast" }) as Task);
render(
<TaskDetailModal
task={makeTask({ id: "FN-001", column: "todo", priority: "high", executionMode: "standard" })}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
fireEvent.change(screen.getByRole("combobox", { name: "Task priority" }), {
target: { value: "urgent" },
});
fireEvent.click(screen.getByRole("button", { name: "Execution mode: standard" }));
await waitFor(() => {
expect(mockUpdate).toHaveBeenNthCalledWith(1, "FN-001", { priority: "urgent" }, undefined);
expect(mockUpdate).toHaveBeenNthCalledWith(2, "FN-001", { executionMode: "fast" }, undefined);
});
});
it("updates priority inline and propagates successful save", async () => {
const { updateTask } = await import("../../api");
const mockUpdate = vi.mocked(updateTask);

View File

@@ -430,10 +430,38 @@ describe("TaskDetailModal", () => {
fireEvent.click(screen.getByRole("button", { name: "Review" }));
expect(await screen.findByText("CHANGES_REQUESTED")).toBeTruthy();
expect(screen.getByText("changes requested review is active")).toBeTruthy();
expect(screen.getByText(/No review items yet\./i)).toBeTruthy();
});
describe("inline execution mode toggle", () => {
it("keeps inline priority and execution controls aligned with shared sizing and gap", () => {
render(
<TaskDetailModal
task={makeTask({ column: "todo", priority: "high", executionMode: "fast" })}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
const controls = screen.getByTestId("detail-meta-inline-controls");
const priorityControl = screen.getByRole("combobox", { name: "Task priority" });
const priorityChip = priorityControl.closest(".detail-priority-chip") as HTMLElement;
const modeToggle = screen.getByRole("button", { name: "Execution mode: fast" });
const controlsStyle = getComputedStyle(controls);
const priorityStyle = getComputedStyle(priorityChip);
const modeStyle = getComputedStyle(modeToggle);
expect(controlsStyle.gap).not.toBe("");
expect(controlsStyle.gap).not.toBe("normal");
expect(priorityStyle.minHeight).toBe(modeStyle.minHeight);
expect(priorityStyle.minHeight).not.toBe("0px");
});
it("renders standard mode as an unpressed toggle", () => {
render(
<TaskDetailModal