feat(FN-4540): relocate in-review move control to TaskCard

Relocated the in-review move control within TaskCard, refactoring both the component layout and styling with accompanying tests — the CSS and TSX changes indicate a significant repositioning of controls while the new test file validates the updated behavior.

Fusion-Task-Id: FN-4540
This commit is contained in:
Fusion
2026-05-14 16:07:39 -07:00
committed by gsxdsm
parent f9af9fc66e
commit 4f215ebf6f
3 changed files with 125 additions and 45 deletions

View File

@@ -552,11 +552,31 @@
margin-top: var(--space-xs);
}
.card-bottom-row {
margin-top: var(--space-sm);
}
.card-bottom-row--split {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-sm);
}
.card-bottom-left-row {
display: flex;
align-items: center;
min-width: 0;
}
.card-bottom-left-row:focus-within .card-send-back-btn {
opacity: 1;
}
.card-bottom-right-row {
display: flex;
justify-content: flex-end;
align-items: center;
margin-top: var(--space-sm);
min-width: 0;
}
@@ -1137,6 +1157,13 @@
overflow: hidden;
}
.card-bottom-left-row .card-send-back-menu {
top: auto;
right: auto;
bottom: calc(100% + var(--space-xs));
left: 0;
}
.card-send-back-menu-item {
display: block;
width: 100%;
@@ -1259,10 +1286,14 @@
font-size: 0.625rem;
}
.card-bottom-right-row {
.card-bottom-row {
margin-top: var(--space-xs);
}
.card-bottom-row--split {
gap: var(--space-xs);
}
/* Card: smaller status badges for 280px width */
.card-status-badge,
.card-priority-badge,

View File

@@ -801,6 +801,7 @@ function TaskCardComponent({
const showTrackingIndicator = hasGithubTrackingLink
&& !hasMatchingIssueInfoBadge
&& !hasMatchingSourceIssue;
const showInReviewMoveControl = task.column === "in-review" && Boolean(onMoveTask);
const branchMetadata = useMemo(() => getVisibleTaskCardBranches(task), [task.id, task.branch, task.baseBranch]);
const hasBranchMetadata = Boolean(branchMetadata.branch || branchMetadata.baseBranch);
const isAgentCreated = isAgentCreatedTask(task);
@@ -1610,35 +1611,6 @@ function TaskCardComponent({
)}
</div>
)}
{task.column === "in-review" && onMoveTask && (
<div className="card-send-back" ref={sendBackRef}>
<button
className="card-send-back-btn"
onClick={handleSendBackClick}
title="Move task"
aria-label="Move task"
aria-haspopup="menu"
aria-expanded={showSendBackMenu}
>
Move
<ChevronDown size={10} />
</button>
{showSendBackMenu && (
<div className="card-send-back-menu" role="menu">
{VALID_TRANSITIONS["in-review"].map((col) => (
<button
key={col}
className="card-send-back-menu-item"
role="menuitem"
onClick={(e) => handleSendBackOptionClick(e, col)}
>
{col === "done" ? "Done (no merge)" : COLUMN_LABELS[col]}
</button>
))}
</div>
)}
</div>
)}
{task.size && (
<span className={`card-size-badge size-${task.size.toLowerCase()}`}>
{task.size}
@@ -1850,20 +1822,55 @@ function TaskCardComponent({
)}
</div>
)}
{showTrackingIndicator && githubTrackedIssue && (
<div className="card-bottom-right-row">
<a
className="card-github-tracking-chip card-github-tracking-link"
href={githubTrackedIssue.url}
target="_blank"
rel="noopener noreferrer"
title={`Linked GitHub issue: ${githubTrackedIssue.owner}/${githubTrackedIssue.repo}#${githubTrackedIssue.number}`}
aria-label={`Linked GitHub issue #${githubTrackedIssue.number}`}
onClick={(e) => e.stopPropagation()}
>
<ProviderIcon provider="github" size="sm" />
<span>{`#${githubTrackedIssue.number}`}</span>
</a>
{(showInReviewMoveControl || (showTrackingIndicator && githubTrackedIssue)) && (
<div className={`card-bottom-row${showInReviewMoveControl && showTrackingIndicator ? " card-bottom-row--split" : ""}`}>
{showInReviewMoveControl && (
<div className="card-bottom-left-row">
<div className="card-send-back" ref={sendBackRef}>
<button
className="card-send-back-btn"
onClick={handleSendBackClick}
title="Move task"
aria-label="Move task"
aria-haspopup="menu"
aria-expanded={showSendBackMenu}
>
Move
<ChevronDown size={10} />
</button>
{showSendBackMenu && (
<div className="card-send-back-menu" role="menu">
{VALID_TRANSITIONS["in-review"].map((col) => (
<button
key={col}
className="card-send-back-menu-item"
role="menuitem"
onClick={(e) => handleSendBackOptionClick(e, col)}
>
{col === "done" ? "Done (no merge)" : COLUMN_LABELS[col]}
</button>
))}
</div>
)}
</div>
</div>
)}
{showTrackingIndicator && githubTrackedIssue && (
<div className="card-bottom-right-row">
<a
className="card-github-tracking-chip card-github-tracking-link"
href={githubTrackedIssue.url}
target="_blank"
rel="noopener noreferrer"
title={`Linked GitHub issue: ${githubTrackedIssue.owner}/${githubTrackedIssue.repo}#${githubTrackedIssue.number}`}
aria-label={`Linked GitHub issue #${githubTrackedIssue.number}`}
onClick={(e) => e.stopPropagation()}
>
<ProviderIcon provider="github" size="sm" />
<span>{`#${githubTrackedIssue.number}`}</span>
</a>
</div>
)}
</div>
)}
<PluginSlot slotId="task-card-badge" projectId={projectId} />

View File

@@ -1281,6 +1281,48 @@ describe("TaskCard", () => {
expect(actionsContainer?.contains(archiveBtn)).toBe(true);
});
it("FN-4540 renders in-review Move control in card-bottom-left-row and keeps menu behavior", () => {
const onMoveTask = vi.fn();
const { container } = render(
<TaskCard
task={makeTask({ column: "in-review" })}
onOpenDetail={noop}
addToast={noop}
onMoveTask={onMoveTask}
/>,
);
const moveButton = screen.getByRole("button", { name: "Move task" });
const actionsContainer = container.querySelector(".card-header-actions");
const bottomLeft = moveButton.closest(".card-bottom-left-row");
expect(actionsContainer).not.toBeNull();
expect(actionsContainer?.contains(moveButton)).toBe(false);
expect(bottomLeft).not.toBeNull();
fireEvent.click(moveButton);
fireEvent.click(screen.getByRole("menuitem", { name: "Done (no merge)" }));
expect(onMoveTask).toHaveBeenCalledWith("FN-001", "done", undefined);
});
it("FN-4540 keeps in-progress Send back control in card-header-actions", () => {
const { container } = render(
<TaskCard
task={makeTask({ column: "in-progress" })}
onOpenDetail={noop}
addToast={noop}
onMoveTask={vi.fn()}
/>,
);
const sendBackButton = screen.getByRole("button", { name: "Send back" });
const actionsContainer = container.querySelector(".card-header-actions");
expect(actionsContainer).not.toBeNull();
expect(actionsContainer?.contains(sendBackButton)).toBe(true);
});
it("shows timer chip for in-progress cards summing workflow runtime + timed events", () => {
const { container } = render(
<TaskCard