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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user