From e24fb37c8bb67b639fb4e5ad7d53b5935e1f1b88 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 22 Jul 2026 16:13:00 -0700 Subject: [PATCH] FN-8492: keep mobile task footer actions on one row Keep task-detail footer actions compact and readable across mobile widths. - Prevent mobile footer controls from wrapping while preserving shrink behavior. - Add ellipsis-safe labels for actions and review controls. - Cover standard and in-review footer layouts with responsive tests. - Add a patch changeset for the mobile footer fix. Files changed: .../fn-8492-mobile-task-footer-single-row.md | 7 ++ .../dashboard/app/components/TaskDetailModal.css | 74 ++++++++++++++++- .../dashboard/app/components/TaskDetailModal.tsx | 8 +- ...etailModal.responsive-and-dependencies.test.tsx | 94 +++++++++++++++++++++- 4 files changed, 175 insertions(+), 8 deletions(-) Fusion-Task-Id: FN-8492 Fusion-Task-Lineage: dae7a449-5022-4bfe-a879-880349341594 Co-authored-by: Fusion (runfusion.ai) --- .../fn-8492-mobile-task-footer-single-row.md | 7 ++ .../app/components/TaskDetailModal.css | 74 ++++++++++++++- .../app/components/TaskDetailModal.tsx | 8 +- ...Modal.responsive-and-dependencies.test.tsx | 94 ++++++++++++++++++- 4 files changed, 175 insertions(+), 8 deletions(-) create mode 100644 .changeset/fn-8492-mobile-task-footer-single-row.md diff --git a/.changeset/fn-8492-mobile-task-footer-single-row.md b/.changeset/fn-8492-mobile-task-footer-single-row.md new file mode 100644 index 0000000000..5316563fa6 --- /dev/null +++ b/.changeset/fn-8492-mobile-task-footer-single-row.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Keep task detail footer actions on a single row on mobile. +category: fix +dev: Mobile TaskDetailModal `.modal-actions` nowrap + tokenized compression so Actions/Move/Merge fit without overflow (FN-8492). diff --git a/packages/dashboard/app/components/TaskDetailModal.css b/packages/dashboard/app/components/TaskDetailModal.css index 6cd9eca4f2..37cda58ae5 100644 --- a/packages/dashboard/app/components/TaskDetailModal.css +++ b/packages/dashboard/app/components/TaskDetailModal.css @@ -3394,16 +3394,82 @@ Live and Feed Activity expansion overlays the content instead of reserving a too font-size: 16px; } - .modal-actions { - flex-wrap: wrap; - gap: var(--space-sm); + /* + FNXC:TaskDetailModalResponsive 2026-07-22-00:00: + FN-8492 requires the task-detail footer to keep Actions, Move, and the + Merge/review control on one row from 320 CSS px through the 768px mobile + breakpoint without horizontal overflow. `nowrap` alone only trades wrapping + for overflow, so scoped tokenized packing, flex shrink guards, and label + ellipsis let every text-bearing footer control yield while upward menus stay + unclipped. + */ + .task-detail-content .modal-actions { + flex-wrap: nowrap; + align-items: center; + gap: var(--space-xs); padding: var(--space-md) calc(var(--space-md) + var(--space-xs) / 2) calc(var(--space-md) + env(safe-area-inset-bottom, 0px)); } - .modal-actions .btn { + .task-detail-content .modal-actions-spacer { + flex: 1 1 0; min-width: 0; } + .task-detail-content .detail-actions-dropdown, + .task-detail-content .detail-move-dropdown { + min-width: 0; + flex-shrink: 1; + } + + .task-detail-content .detail-move-dropdown { + flex: 1 1 auto; + } + + .task-detail-content .detail-move-actions-in-review { + display: flex; + flex: 1 1 auto; + flex-wrap: nowrap; + align-items: center; + min-width: 0; + gap: var(--space-xs); + } + + .task-detail-content .detail-move-actions-in-review > div, + .task-detail-content .detail-move-actions-in-review > .btn { + flex: 1 1 0; + min-width: 0; + } + + .task-detail-content .modal-actions .btn { + flex: 0 1 auto; + min-width: 0; + max-width: 100%; + padding-inline: var(--space-xs); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .task-detail-content .detail-move-btn { + display: flex; + min-width: 0; + max-width: 100%; + } + + .task-detail-content .detail-footer-button-label, + .task-detail-content .detail-move-btn__label { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .task-detail-content .detail-move-btn__arrow, + .task-detail-content .modal-actions .btn > svg { + flex-shrink: 0; + } + /* Mobile dropdown menus — position above action bar, constrained to viewport */ .detail-actions-menu, .detail-move-menu { diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx index aa1dbcf176..67ca662a4c 100644 --- a/packages/dashboard/app/components/TaskDetailModal.tsx +++ b/packages/dashboard/app/components/TaskDetailModal.tsx @@ -6334,7 +6334,9 @@ export function TaskDetailContent({ aria-haspopup="menu" aria-expanded={showActionsMenu} > - {t("taskDetail.actions.menuBtn", "Actions")} + + {t("taskDetail.actions.menuBtn", "Actions")} + {showActionsMenu && ( @@ -6407,7 +6409,9 @@ export function TaskDetailContent({ onClick={reviewAction.onSelect} disabled={reviewAction.disabled} > - {reviewAction.label} + + {reviewAction.label} + )} diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx index e3b69405a7..4ec011bec8 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx @@ -886,6 +886,96 @@ describe("TaskDetailModal", () => { expect((container.querySelector(".detail-tabs") as HTMLElement).style.borderBottom).toBe(""); }); + it("keeps every mobile task footer control shrinkable on one row from 320 CSS px through 768px", () => { + const css = readDashboardStylesSource(); + const mobileBlock = getCssAtRuleBlockContainingExactRule(css, "@media (max-width: 768px)", ".task-detail-content .modal-actions"); + const footerBlock = getExactCssRuleBlock(mobileBlock, ".task-detail-content .modal-actions"); + const spacerBlock = getExactCssRuleBlock(mobileBlock, ".task-detail-content .modal-actions-spacer"); + const inReviewBlock = getExactCssRuleBlock(mobileBlock, ".task-detail-content .detail-move-actions-in-review"); + const buttonBlock = getExactCssRuleBlock(mobileBlock, ".task-detail-content .modal-actions .btn"); + const labelBlock = getExactCssRuleBlock( + mobileBlock, + ".task-detail-content .detail-footer-button-label,\n .task-detail-content .detail-move-btn__label", + ); + const dropdownBlock = getExactCssRuleBlock( + mobileBlock, + ".task-detail-content .detail-actions-dropdown,\n .task-detail-content .detail-move-dropdown", + ); + const expandedChatBlock = getExactCssRuleBlock(css, ".task-detail-content--chat-expanded .modal-actions"); + + /* + FNXC:TaskDetailModalResponsive 2026-07-22-00:00: + FN-8492's supported mobile fit contract is 320 CSS px through 768px. Keep + these source assertions together: `nowrap` without shrink guards and + text ellipsis would merely turn the original second row into overflow. + */ + expect(footerBlock).toContain("flex-wrap: nowrap;"); + expect(footerBlock).not.toContain("flex-wrap: wrap;"); + expect(footerBlock).toContain("align-items: center;"); + expect(footerBlock).toContain("gap: var(--space-xs);"); + expect(spacerBlock).toContain("flex: 1 1 0;"); + expect(spacerBlock).toContain("min-width: 0;"); + expect(dropdownBlock).toContain("min-width: 0;"); + expect(dropdownBlock).toContain("flex-shrink: 1;"); + expect(inReviewBlock).toContain("display: flex;"); + expect(inReviewBlock).toContain("flex-wrap: nowrap;"); + expect(inReviewBlock).toContain("min-width: 0;"); + expect(inReviewBlock).toContain("gap: var(--space-xs);"); + expect(buttonBlock).toContain("flex: 0 1 auto;"); + expect(buttonBlock).toContain("min-width: 0;"); + expect(buttonBlock).toContain("padding-inline: var(--space-xs);"); + expect(buttonBlock).toContain("white-space: nowrap;"); + expect(buttonBlock).toContain("overflow: hidden;"); + expect(buttonBlock).toContain("text-overflow: ellipsis;"); + expect(labelBlock).toContain("min-width: 0;"); + expect(labelBlock).toContain("white-space: nowrap;"); + expect(labelBlock).toContain("overflow: hidden;"); + expect(labelBlock).toContain("text-overflow: ellipsis;"); + expect(expandedChatBlock).toContain("display: none;"); + expect(css).toMatch(/\.task-detail-content--planner-chat-expanded \.modal-actions,[\s\S]*?\{\s*display:\s*none;/); + }); + + it("keeps dense in-review and standard task controls in their shared footer", () => { + const { container, unmount } = render( + , + ); + const inReviewFooter = container.querySelector(".modal-actions"); + + expect(inReviewFooter).toBeTruthy(); + expect(inReviewFooter?.contains(screen.getByRole("button", { name: "Actions" }))).toBe(true); + expect(inReviewFooter?.querySelector(".detail-move-btn")).toBeTruthy(); + expect(inReviewFooter?.contains(screen.getByRole("button", { name: "Merge & Close" }))).toBe(true); + + unmount(); + + const standard = render( + , + ); + const standardFooter = standard.container.querySelector(".modal-actions"); + + expect(standardFooter).toBeTruthy(); + expect(standardFooter?.contains(screen.getByRole("button", { name: "Actions" }))).toBe(true); + expect(standardFooter?.querySelector(".detail-move-btn")).toBeTruthy(); + }); + it("modal-actions contains Delete and Pause buttons for non-done tasks (via Actions dropdown)", () => { render( { />, ); - const button = screen.getByText("Awaiting PR checks") as HTMLButtonElement; + const button = screen.getByText("Awaiting PR checks").closest("button") as HTMLButtonElement; expect(button.disabled).toBe(true); expect(screen.queryByText("Merge & Close")).toBeNull(); }); @@ -1697,7 +1787,7 @@ describe("TaskDetailModal", () => { />, ); - const button = screen.getByText("Creating PR…") as HTMLButtonElement; + const button = screen.getByText("Creating PR…").closest("button") as HTMLButtonElement; expect(button.disabled).toBe(true); expect(screen.queryByText("Merge & Close")).toBeNull(); });