feat(FN-5088): merge fusion/fn-5088
This commit is contained in:
5
.changeset/fn-5088-inline-move-in-meta-row.md
Normal file
5
.changeset/fn-5088-inline-move-in-meta-row.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
TaskCard in-review Move dropdown is now rendered inline with the file-overlap and queued badges in the meta row, falling back to the bottom action row only when the meta row is not rendered.
|
||||
@@ -1206,6 +1206,10 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.card-meta .card-send-back {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.card-send-back-menu-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
@@ -1088,6 +1088,37 @@ function TaskCardComponent({
|
||||
|| Boolean(task.blockedBy)
|
||||
|| Boolean(task.overlapBlockedBy)
|
||||
|| Boolean(fanout && fanout.totalCount > 0);
|
||||
const shouldRenderActionRow = showCreatePrQuickAction || (showInReviewMoveControl && !metaRowVisible);
|
||||
|
||||
const renderInReviewMoveControl = () => (
|
||||
<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>
|
||||
);
|
||||
|
||||
const enterEditMode = useCallback((e?: React.MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
@@ -1897,7 +1928,7 @@ function TaskCardComponent({
|
||||
</span>
|
||||
)}
|
||||
{(queued || task.status === "queued") && task.column !== "in-progress" && <span className="queued-badge"><Clock size={12} style={{ verticalAlign: "middle" }} /> Queued</span>}
|
||||
|
||||
{showInReviewMoveControl && renderInReviewMoveControl()}
|
||||
</div>
|
||||
)}
|
||||
{(task.assignedAgentId || taskProviders.length > 0) && (
|
||||
@@ -1923,7 +1954,7 @@ function TaskCardComponent({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(showCreatePrQuickAction || showInReviewMoveControl) && (
|
||||
{shouldRenderActionRow && (
|
||||
<div className="card-action-row">
|
||||
{showCreatePrQuickAction && (
|
||||
<button
|
||||
@@ -1940,35 +1971,7 @@ function TaskCardComponent({
|
||||
Create PR
|
||||
</button>
|
||||
)}
|
||||
{showInReviewMoveControl && (
|
||||
<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>
|
||||
)}
|
||||
{showInReviewMoveControl && !metaRowVisible && renderInReviewMoveControl()}
|
||||
</div>
|
||||
)}
|
||||
<PluginSlot slotId="task-card-badge" projectId={projectId} />
|
||||
|
||||
@@ -1692,25 +1692,46 @@ describe("TaskCard", () => {
|
||||
expect(actionsContainer?.contains(archiveBtn)).toBe(true);
|
||||
});
|
||||
|
||||
it("renders in-review Move control inside card-action-row when meta row is visible", () => {
|
||||
it("renders in-review Move control inline in card-meta for overlap-blocked tasks", () => {
|
||||
const { container } = render(
|
||||
<TaskCard
|
||||
task={makeTask({ column: "in-review", blockedBy: "FN-777" })}
|
||||
task={makeTask({ column: "in-review", overlapBlockedBy: "FN-OVER", blockedBy: undefined })}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
onMoveTask={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const moveButton = screen.getByRole("button", { name: "Move task" });
|
||||
const moveControl = container.querySelector(".card-send-back");
|
||||
const metaRow = container.querySelector(".card-meta");
|
||||
const actionRow = container.querySelector(".card-action-row");
|
||||
|
||||
expect(metaRow).not.toBeNull();
|
||||
expect(metaRow?.contains(moveButton)).toBe(false);
|
||||
expect(actionRow).not.toBeNull();
|
||||
expect(actionRow?.contains(moveButton)).toBe(true);
|
||||
expect(moveButton.closest(".card-action-row")).not.toBeNull();
|
||||
expect(moveControl).not.toBeNull();
|
||||
expect(metaRow?.contains(moveControl as HTMLElement)).toBe(true);
|
||||
expect(container.querySelector(".card-action-row")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders in-review Move control after queued badge in card-meta", () => {
|
||||
const { container } = render(
|
||||
<TaskCard
|
||||
task={makeTask({ column: "in-review", status: "queued" as any, dependencies: [], blockedBy: undefined, overlapBlockedBy: undefined })}
|
||||
queued={true}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
onMoveTask={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const metaRow = container.querySelector(".card-meta");
|
||||
const queuedBadge = container.querySelector(".queued-badge");
|
||||
const moveControl = container.querySelector(".card-send-back");
|
||||
|
||||
expect(metaRow).not.toBeNull();
|
||||
expect(queuedBadge).not.toBeNull();
|
||||
expect(moveControl).not.toBeNull();
|
||||
expect(metaRow?.contains(moveControl as HTMLElement)).toBe(true);
|
||||
expect(queuedBadge?.compareDocumentPosition(moveControl as HTMLElement) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(container.querySelector(".card-action-row")).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps in-review Move control in card-action-row when meta row is not visible", () => {
|
||||
@@ -1751,23 +1772,24 @@ describe("TaskCard", () => {
|
||||
expect(moveButton.closest(".card-action-row")).toBe(actionRow);
|
||||
expect(createPrButton.compareDocumentPosition(moveButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
|
||||
const moveControl = container.querySelector(".card-action-row .card-send-back") as HTMLElement | null;
|
||||
const moveControl = moveButton.closest(".card-send-back") as HTMLElement | null;
|
||||
expect(moveControl).not.toBeNull();
|
||||
expect(getComputedStyle(moveControl as HTMLElement).marginLeft).toBe("auto");
|
||||
|
||||
fireEvent.click(moveButton);
|
||||
const menu = screen.getByRole("menu");
|
||||
expect(moveControl?.contains(menu)).toBe(true);
|
||||
const menuStyle = getComputedStyle(menu);
|
||||
expect(menuStyle.right).toBe("0px");
|
||||
expect(menuStyle.left).not.toBe("0px");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "meta-row-visible variant", task: makeTask({ column: "in-review", blockedBy: "FN-777" }) },
|
||||
{ name: "no-meta variant", task: makeTask({ column: "in-review", dependencies: [], blockedBy: undefined, overlapBlockedBy: undefined, status: undefined as any }) },
|
||||
])("keeps Move dropdown behavior for $name", ({ task }) => {
|
||||
{ name: "meta-row-visible variant", task: makeTask({ column: "in-review", blockedBy: "FN-777" }), expectedContainer: ".card-meta" },
|
||||
{ name: "no-meta variant", task: makeTask({ column: "in-review", dependencies: [], blockedBy: undefined, overlapBlockedBy: undefined, status: undefined as any }), expectedContainer: ".card-action-row" },
|
||||
])("keeps Move dropdown behavior for $name", ({ task, expectedContainer }) => {
|
||||
const onMoveTask = vi.fn();
|
||||
render(
|
||||
const { container } = render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={noop}
|
||||
@@ -1776,7 +1798,11 @@ describe("TaskCard", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Move task" }));
|
||||
const host = container.querySelector(expectedContainer);
|
||||
const moveButton = screen.getByRole("button", { name: "Move task" });
|
||||
expect(host?.contains(moveButton)).toBe(true);
|
||||
|
||||
fireEvent.click(moveButton);
|
||||
|
||||
expect(screen.getAllByRole("menuitem").length).toBeGreaterThan(0);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user