FN-7279: move workflow badges to task timestamps

Move the task-detail workflow badge into the Updated timestamp metadata row.

- Render one canonical workflow badge in the timestamp group for desktop and mobile task details.
- Remove desktop/mobile workflow badge variants and keep timestamp layout from wrapping on mobile.
- Update rendering and responsive CSS tests for the new badge ownership.
- Add a patch changeset for the published Fusion package.

Files changed:
 .../fn-7279-task-detail-workflow-timestamps.md     |  7 ++++
 .../dashboard/app/components/TaskDetailModal.css   | 43 ++++++++++------------
 .../dashboard/app/components/TaskDetailModal.tsx   |  7 +---
 .../__tests__/TaskDetailModal.rendering.test.tsx   | 25 ++++++++-----
 ...etailModal.responsive-and-dependencies.test.tsx | 27 ++++++++------
 5 files changed, 58 insertions(+), 51 deletions(-)

Fusion-Task-Id: FN-7279

Fusion-Task-Lineage: a1f11f51-99c9-42c7-bbb6-bb58384594a2

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-30 08:40:14 -07:00
parent 5814049023
commit 85e925a12a
5 changed files with 57 additions and 50 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Move task-detail workflow badges into the Updated timestamp metadata row.
category: fix
dev: Uses one canonical task-detail workflow badge across desktop and mobile detail surfaces.

View File

@@ -29,7 +29,7 @@ The gray top header band (task id + column badge) was over-padded. Trim its vert
.detail-title-row { .detail-title-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: var(--space-sm);
} }
.detail-id { .detail-id {
@@ -39,28 +39,33 @@ The gray top header band (task id + column badge) was over-padded. Trim its vert
color: var(--text-muted); color: var(--text-muted);
} }
.detail-column-badge, .detail-column-badge {
.detail-workflow-badge { font-size: 0.6875rem;
font-size: 11px; padding: calc(var(--space-xs) / 2) var(--space-sm);
padding: 2px 8px;
border-radius: var(--radius-pill); border-radius: var(--radius-pill);
font-weight: 600; font-weight: 600;
letter-spacing: 0.5px; letter-spacing: 0.03125rem;
}
.detail-column-badge {
text-transform: uppercase; text-transform: uppercase;
} }
.detail-workflow-badge { .detail-workflow-badge {
font-size: 0.6875rem;
padding: calc(var(--space-xs) / 2) var(--space-sm);
border-radius: var(--radius-pill);
font-weight: 600;
letter-spacing: 0.03125rem;
display: inline-flex;
align-items: center;
flex: 0 1 auto;
min-width: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-muted); color: var(--text-muted);
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);
@@ -314,24 +319,14 @@ The task-detail modal metadata must keep priority, execution mode, provenance, P
/* /*
FNXC:TaskDetailWorkflow 2026-06-30-07:37: 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. Task details show workflow identity in the Updated timestamp section across desktop and mobile. Keep the canonical badge inline with Created/Updated metadata so header rows, embedded detail hosts, and mobile back-header details do not render duplicate workflow badges or empty mobile-only shells.
*/ */
.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

@@ -2716,11 +2716,6 @@ export function TaskDetailContent({
<span className={`detail-column-badge badge-${task.column}`}> <span className={`detail-column-badge badge-${task.column}`}>
{columnLabel(task.column)} {columnLabel(task.column)}
</span> </span>
{taskWorkflowName && (
<span className="detail-workflow-badge detail-workflow-badge--desktop" data-testid="task-detail-workflow-badge">
{taskWorkflowName}
</span>
)}
</div> </div>
<div className="modal-header-actions"> <div className="modal-header-actions">
{!isEditing && canEdit && ( {!isEditing && canEdit && (
@@ -3094,7 +3089,7 @@ export function TaskDetailContent({
</time> </time>
</span> </span>
{taskWorkflowName && ( {taskWorkflowName && (
<span className="detail-workflow-badge detail-workflow-badge--mobile" data-testid="task-detail-workflow-badge-mobile"> <span className="detail-workflow-badge" data-testid="task-detail-workflow-badge">
{taskWorkflowName} {taskWorkflowName}
</span> </span>
)} )}

View File

@@ -35,7 +35,7 @@ import { FileBrowserProvider } from "../../context/FileBrowserContext";
setupTaskDetailModalHooks(); setupTaskDetailModalHooks();
describe("TaskDetailModal", () => { describe("TaskDetailModal", () => {
describe("workflow header badge", () => { describe("workflow timestamp badge", () => {
const workflowPayload = { const workflowPayload = {
flagEnabled: true, flagEnabled: true,
defaultWorkflowId: "builtin:coding", defaultWorkflowId: "builtin:coding",
@@ -71,14 +71,17 @@ describe("TaskDetailModal", () => {
); );
} }
it("renders the resolved workflow name beside the task id and column badge", async () => { it("renders the resolved workflow name in the timestamp section instead of the title row", async () => {
vi.mocked(dashboardApi.fetchBoardWorkflows).mockResolvedValueOnce(workflowPayload); vi.mocked(dashboardApi.fetchBoardWorkflows).mockResolvedValueOnce(workflowPayload);
renderDetail(); const { container } = renderDetail();
const badge = await screen.findByTestId("task-detail-workflow-badge"); const badge = await screen.findByTestId("task-detail-workflow-badge");
expect(badge).toHaveTextContent("Docs"); expect(badge).toHaveTextContent("Docs");
expect(badge.parentElement).toHaveClass("detail-title-row"); expect(badge.closest(".detail-timestamps")).toBeTruthy();
expect(badge.closest(".detail-title-row")).toBeNull();
expect(container.querySelector(".detail-title-row .detail-workflow-badge")).toBeNull();
expect(screen.getAllByTestId("task-detail-workflow-badge")).toHaveLength(1);
expect(screen.getByText("FN-101")).toBeInTheDocument(); expect(screen.getByText("FN-101")).toBeInTheDocument();
expect(screen.getByText("Todo")).toBeInTheDocument(); expect(screen.getByText("Todo")).toBeInTheDocument();
expect(dashboardApi.fetchBoardWorkflows).toHaveBeenCalledTimes(1); expect(dashboardApi.fetchBoardWorkflows).toHaveBeenCalledTimes(1);
@@ -170,7 +173,7 @@ describe("TaskDetailModal", () => {
expect(container.querySelector(".detail-workflow-badge")).toBeNull(); expect(container.querySelector(".detail-workflow-badge")).toBeNull();
}); });
it("renders beside the Updated timestamp in the mobile back-header variant", async () => { it("renders the canonical badge beside the Updated timestamp in the mobile back-header variant", async () => {
vi.mocked(dashboardApi.fetchBoardWorkflows).mockResolvedValueOnce(workflowPayload); vi.mocked(dashboardApi.fetchBoardWorkflows).mockResolvedValueOnce(workflowPayload);
const { container } = render( const { container } = render(
@@ -187,13 +190,15 @@ describe("TaskDetailModal", () => {
/>, />,
); );
expect(await screen.findByTestId("task-detail-workflow-badge")).toHaveTextContent("Docs"); const badge = await screen.findByTestId("task-detail-workflow-badge");
const mobileBadge = screen.getByTestId("task-detail-workflow-badge-mobile");
const timestamps = container.querySelector(".detail-timestamps"); const timestamps = container.querySelector(".detail-timestamps");
const updatedLabel = screen.getByText("Updated").closest(".detail-timestamp-item"); const updatedLabel = screen.getByText("Updated").closest(".detail-timestamp-item");
expect(mobileBadge).toHaveTextContent("Docs"); expect(badge).toHaveTextContent("Docs");
expect(mobileBadge.parentElement).toBe(timestamps); expect(badge.parentElement).toBe(timestamps);
expect(updatedLabel?.nextElementSibling).toBe(mobileBadge); expect(updatedLabel?.nextElementSibling).toBe(badge);
expect(screen.getAllByTestId("task-detail-workflow-badge")).toHaveLength(1);
expect(screen.queryByTestId("task-detail-workflow-badge-mobile")).toBeNull();
expect(container.querySelector(".detail-title-row .detail-workflow-badge")).toBeNull();
expect(screen.getByRole("button", { name: "Back to task list" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Back to task list" })).toBeInTheDocument();
}); });
}); });

View File

@@ -121,18 +121,23 @@ describe("TaskDetailModal", () => {
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", () => { it("keeps the canonical workflow badge owned by the timestamp group across breakpoints", () => {
const css = readDashboardStylesSource(); const css = readDashboardStylesSource();
const mobileBlock = getCssAtRuleBlockContaining(css, "@media (max-width: 768px)", ".detail-timestamps .detail-workflow-badge--mobile"); const workflowBadgeBlock = css.match(/^\.detail-workflow-badge\s*\{([^}]*)\}/m)?.[1] ?? "";
const mobileTimestampBadgeBlock = getCssRuleBlock(mobileBlock, ".detail-timestamps .detail-workflow-badge--mobile"); const mobileBlock = getCssAtRuleBlockContaining(css, "@media (max-width: 768px)", ".detail-timestamps");
const mobileDesktopBadgeBlock = getCssRuleBlock(mobileBlock, ".detail-workflow-badge--desktop"); const mobileTimestampsBlock = getCssRuleBlock(mobileBlock, ".detail-timestamps");
expectBaseRule(css, ".detail-workflow-badge--mobile", "display: none;"); expect(workflowBadgeBlock).toContain("display: inline-flex;");
expect(mobileDesktopBadgeBlock).toContain("display: none;"); expect(workflowBadgeBlock).toContain("align-items: center;");
expect(mobileTimestampBadgeBlock).toContain("display: inline-flex;"); expect(workflowBadgeBlock).toContain("flex: 0 1 auto;");
expect(mobileTimestampBadgeBlock).toContain("align-items: center;"); expect(workflowBadgeBlock).toContain("text-overflow: ellipsis;");
expect(mobileTimestampBadgeBlock).toContain("flex: 0 0 auto;"); expect(mobileTimestampsBlock).toContain("display: flex;");
expect(css).not.toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.detail-title-row\s+\.detail-workflow-badge--mobile\s*\{/); expect(mobileTimestampsBlock).toContain("align-items: center;");
expect(mobileTimestampsBlock).toContain("flex-wrap: nowrap;");
expect(css).not.toMatch(/detail-workflow-badge--desktop/);
expect(css).not.toMatch(/detail-workflow-badge--mobile/);
expect(css).not.toMatch(/task-detail-workflow-badge-mobile/);
expect(css).not.toMatch(/\.detail-title-row\s+\.detail-workflow-badge\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();