FN-7271: move mobile workflow badge into timestamp row

Move the mobile task-detail workflow badge next to the updated timestamp while preserving desktop placement.

- Add separate desktop and mobile workflow badge render targets in the task detail modal.
- Hide the header badge on mobile and reveal the timestamp-row badge with scoped responsive styles.
- Cover the mobile badge placement and CSS guardrails in task detail tests.
- Add a patch changeset for the published Fusion package.

Files changed:
 .changeset/fn-7271-mobile-workflow-badge.md         |  7 +++++++
 .../dashboard/app/components/TaskDetailModal.css    | 18 ++++++++++++++++++
 .../dashboard/app/components/TaskDetailModal.tsx    |  7 ++++++-
 .../__tests__/TaskDetailModal.rendering.test.tsx    | 10 ++++++++--
 ...DetailModal.responsive-and-dependencies.test.tsx | 21 +++++++++++++++++----
 5 files changed, 56 insertions(+), 7 deletions(-)

Fusion-Task-Id: FN-7271

Fusion-Task-Lineage: def0b5c5-b881-45f8-aa7c-80243d8de8ec

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-30 07:52:54 -07:00
parent 5ec04ec5cb
commit d04ee5b4c3
5 changed files with 56 additions and 7 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Move the mobile task-detail workflow badge beside the updated timestamp.
category: fix
dev: Keeps the desktop task-detail header badge while showing a mobile-only timestamp-group badge.

View File

@@ -57,6 +57,10 @@ The gray top header band (task id + column badge) was over-padded. Trim its vert
background: var(--surface-raised); background: var(--surface-raised);
border: 1px solid var(--border); border: 1px solid var(--border);
} }
.detail-workflow-badge--mobile {
display: none;
}
.badge-triage { .badge-triage {
background: var(--status-triage-bg); background: var(--status-triage-bg);
color: var(--triage); color: var(--triage);
@@ -308,12 +312,26 @@ The task-detail modal metadata must keep priority, execution mode, provenance, P
align-items: center; align-items: center;
} }
/*
FNXC:TaskDetailWorkflow 2026-06-30-07:37:
Mobile task details need the workflow-name badge to read with the Updated timestamp instead of the compact header band. Keep desktop's header badge placement while showing exactly one mobile badge inside the timestamp group.
*/
.detail-workflow-badge--desktop {
display: none;
}
.detail-timestamps { .detail-timestamps {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: nowrap; flex-wrap: nowrap;
} }
.detail-timestamps .detail-workflow-badge--mobile {
display: inline-flex;
align-items: center;
flex: 0 0 auto;
}
.detail-provenance-context { .detail-provenance-context {
max-width: 20ch; max-width: 20ch;
} }

View File

@@ -2717,7 +2717,7 @@ export function TaskDetailContent({
{columnLabel(task.column)} {columnLabel(task.column)}
</span> </span>
{taskWorkflowName && ( {taskWorkflowName && (
<span className="detail-workflow-badge" data-testid="task-detail-workflow-badge"> <span className="detail-workflow-badge detail-workflow-badge--desktop" data-testid="task-detail-workflow-badge">
{taskWorkflowName} {taskWorkflowName}
</span> </span>
)} )}
@@ -3093,6 +3093,11 @@ export function TaskDetailContent({
{formatTimestamp(task.updatedAt)} {formatTimestamp(task.updatedAt)}
</time> </time>
</span> </span>
{taskWorkflowName && (
<span className="detail-workflow-badge detail-workflow-badge--mobile" data-testid="task-detail-workflow-badge-mobile">
{taskWorkflowName}
</span>
)}
</div> </div>
</div> </div>
{task.branchContext?.groupId && ( {task.branchContext?.groupId && (

View File

@@ -170,10 +170,10 @@ describe("TaskDetailModal", () => {
expect(container.querySelector(".detail-workflow-badge")).toBeNull(); expect(container.querySelector(".detail-workflow-badge")).toBeNull();
}); });
it("renders in the mobile back-header variant", async () => { it("renders beside the Updated timestamp in the mobile back-header variant", async () => {
vi.mocked(dashboardApi.fetchBoardWorkflows).mockResolvedValueOnce(workflowPayload); vi.mocked(dashboardApi.fetchBoardWorkflows).mockResolvedValueOnce(workflowPayload);
render( const { container } = render(
<TaskDetailModal <TaskDetailModal
initialTab="definition" initialTab="definition"
mobileHeaderMode="back" mobileHeaderMode="back"
@@ -188,6 +188,12 @@ describe("TaskDetailModal", () => {
); );
expect(await screen.findByTestId("task-detail-workflow-badge")).toHaveTextContent("Docs"); expect(await screen.findByTestId("task-detail-workflow-badge")).toHaveTextContent("Docs");
const mobileBadge = screen.getByTestId("task-detail-workflow-badge-mobile");
const timestamps = container.querySelector(".detail-timestamps");
const updatedLabel = screen.getByText("Updated").closest(".detail-timestamp-item");
expect(mobileBadge).toHaveTextContent("Docs");
expect(mobileBadge.parentElement).toBe(timestamps);
expect(updatedLabel?.nextElementSibling).toBe(mobileBadge);
expect(screen.getByRole("button", { name: "Back to task list" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Back to task list" })).toBeInTheDocument();
}); });
}); });

View File

@@ -120,6 +120,20 @@ describe("TaskDetailModal", () => {
expect(css).not.toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.detail-timestamps\s*\{[^}]*flex-direction:\s*column;/); expect(css).not.toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.detail-timestamps\s*\{[^}]*flex-direction:\s*column;/);
expect(css).not.toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.detail-timestamp-separator\s*\{[^}]*display:\s*none;/); expect(css).not.toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.detail-timestamp-separator\s*\{[^}]*display:\s*none;/);
}); });
it("places only the mobile workflow badge inside the timestamp group at the mobile breakpoint", () => {
const css = readDashboardStylesSource();
const mobileBlock = getCssAtRuleBlockContaining(css, "@media (max-width: 768px)", ".detail-timestamps .detail-workflow-badge--mobile");
const mobileTimestampBadgeBlock = getCssRuleBlock(mobileBlock, ".detail-timestamps .detail-workflow-badge--mobile");
const mobileDesktopBadgeBlock = getCssRuleBlock(mobileBlock, ".detail-workflow-badge--desktop");
expectBaseRule(css, ".detail-workflow-badge--mobile", "display: none;");
expect(mobileDesktopBadgeBlock).toContain("display: none;");
expect(mobileTimestampBadgeBlock).toContain("display: inline-flex;");
expect(mobileTimestampBadgeBlock).toContain("align-items: center;");
expect(mobileTimestampBadgeBlock).toContain("flex: 0 0 auto;");
expect(css).not.toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.detail-title-row\s+\.detail-workflow-badge--mobile\s*\{/);
});
it("keeps desktop and mobile modal sizing guards unchanged", () => { it("keeps desktop and mobile modal sizing guards unchanged", () => {
const css = readDashboardStylesSource(); const css = readDashboardStylesSource();
const mobileBlock = getCssAtRuleBlockContaining(css, "@media (max-width: 768px)", ".modal-overlay:has(.task-detail-modal)"); const mobileBlock = getCssAtRuleBlockContaining(css, "@media (max-width: 768px)", ".modal-overlay:has(.task-detail-modal)");
@@ -737,7 +751,7 @@ describe("TaskDetailModal", () => {
expect(screen.queryByRole("button", { name: "Finish & Close" })).toBeNull(); expect(screen.queryByRole("button", { name: "Finish & Close" })).toBeNull();
}); });
it("shows Start PR Review and calls onMergeTask for pull-request strategy when autoMerge is off and no PR exists", async () => { it("shows Start PR Review and opens PR creation for pull-request strategy when autoMerge is off and no PR exists", async () => {
const { fetchSettings } = await import("../../api"); const { fetchSettings } = await import("../../api");
const onMergeTask = vi.fn(async () => ({ merged: false } as MergeResult)); const onMergeTask = vi.fn(async () => ({ merged: false } as MergeResult));
vi.mocked(fetchSettings).mockResolvedValueOnce({ vi.mocked(fetchSettings).mockResolvedValueOnce({
@@ -764,9 +778,8 @@ describe("TaskDetailModal", () => {
const button = await screen.findByRole("button", { name: "Start PR Review" }); const button = await screen.findByRole("button", { name: "Start PR Review" });
fireEvent.click(button); fireEvent.click(button);
await waitFor(() => { expect(await screen.findByRole("heading", { name: "Create Pull Request" })).toBeInTheDocument();
expect(onMergeTask).toHaveBeenCalledWith("FN-099"); expect(onMergeTask).not.toHaveBeenCalled();
});
}); });
it("refreshes PR status for Check PR Status without merge prompt", async () => { it("refreshes PR status for Check PR Status without merge prompt", async () => {