feat(FN-5061): merge fusion/fn-5061
This commit is contained in:
@@ -1337,6 +1337,7 @@ function AppInner() {
|
||||
workflowStepNameLookup={workflowStepNameLookup}
|
||||
disableDrag={true}
|
||||
prAuthAvailable={prAuthAvailable}
|
||||
autoMergeEnabled={autoMerge}
|
||||
/>
|
||||
),
|
||||
addToast,
|
||||
|
||||
@@ -275,8 +275,9 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
|
||||
workflowStepNameLookup={workflowStepNameLookup}
|
||||
blockerFanoutMap={blockerFanoutMap}
|
||||
prAuthAvailable={prAuthAvailable}
|
||||
autoMerge={autoMerge}
|
||||
{...(col === "triage" ? { onQuickCreate, onNewTask, onPlanningMode, onSubtaskBreakdown } : {})}
|
||||
{...(col === "in-review" ? { autoMerge, onToggleAutoMerge } : {})}
|
||||
{...(col === "in-review" ? { onToggleAutoMerge } : {})}
|
||||
{...(col === "done" ? { onArchiveAllDone } : {})}
|
||||
{...(col === "archived" ? { collapsed: archivedCollapsed, onToggleCollapse: handleToggleArchivedCollapse } : {})}
|
||||
/>
|
||||
|
||||
@@ -505,6 +505,7 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask,
|
||||
workflowStepNameLookup={workflowStepNameLookup}
|
||||
blockerFanoutMap={blockerFanoutMap}
|
||||
prAuthAvailable={prAuthAvailable}
|
||||
autoMergeEnabled={Boolean(autoMerge)}
|
||||
/>
|
||||
))
|
||||
)
|
||||
@@ -533,6 +534,7 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask,
|
||||
workflowStepNameLookup={workflowStepNameLookup}
|
||||
fanout={blockerFanoutMap?.get(task.id)}
|
||||
prAuthAvailable={prAuthAvailable}
|
||||
autoMergeEnabled={Boolean(autoMerge)}
|
||||
/>
|
||||
))}
|
||||
{shouldPaginate && hiddenTaskCount > 0 && (
|
||||
|
||||
@@ -132,10 +132,6 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card-meta-move {
|
||||
margin-left: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-retry-badge {
|
||||
cursor: pointer;
|
||||
@@ -562,20 +558,13 @@
|
||||
margin-top: var(--space-xs);
|
||||
}
|
||||
|
||||
.card-bottom-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: var(--space-sm);
|
||||
}
|
||||
|
||||
.card-bottom-right-row {
|
||||
.card-action-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-bottom-right-row:focus-within .card-send-back-btn {
|
||||
opacity: 1;
|
||||
gap: var(--space-xs);
|
||||
margin-top: var(--space-xs);
|
||||
flex-wrap: wrap;
|
||||
row-gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.card-provider-icons {
|
||||
@@ -1213,16 +1202,9 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-bottom-right-row .card-send-back-menu {
|
||||
top: auto;
|
||||
left: auto;
|
||||
bottom: calc(100% + var(--space-xs));
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.card-meta-move .card-send-back-menu {
|
||||
left: auto;
|
||||
right: 0;
|
||||
.card-action-row .card-send-back-menu {
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.card-send-back-menu-item {
|
||||
@@ -1351,9 +1333,6 @@
|
||||
min-height: var(--card-chip-height-mobile);
|
||||
}
|
||||
|
||||
.card-bottom-row {
|
||||
margin-top: var(--space-xs);
|
||||
}
|
||||
|
||||
/* Card: smaller status badges for 280px width */
|
||||
.card-status-badge,
|
||||
|
||||
@@ -297,6 +297,8 @@ interface TaskCardProps {
|
||||
fanout?: BlockerFanoutEntry;
|
||||
/** Whether GitHub CLI auth is available for creating PRs from task cards. */
|
||||
prAuthAvailable?: boolean;
|
||||
/** Whether project-level auto-merge is enabled (hides manual Create PR quick action when true). */
|
||||
autoMergeEnabled?: boolean;
|
||||
}
|
||||
|
||||
function areTaskBadgeInfosEqual(
|
||||
@@ -425,6 +427,7 @@ function areTaskCardPropsEqual(previous: TaskCardProps, next: TaskCardProps): bo
|
||||
previous.globalPaused === next.globalPaused &&
|
||||
previous.taskStuckTimeoutMs === next.taskStuckTimeoutMs &&
|
||||
previous.prAuthAvailable === next.prAuthAvailable &&
|
||||
previous.autoMergeEnabled === next.autoMergeEnabled &&
|
||||
previous.onOpenDetail === next.onOpenDetail &&
|
||||
previous.addToast === next.addToast &&
|
||||
previous.onUpdateTask === next.onUpdateTask &&
|
||||
@@ -526,6 +529,7 @@ function TaskCardComponent({
|
||||
disableDrag,
|
||||
fanout,
|
||||
prAuthAvailable,
|
||||
autoMergeEnabled = false,
|
||||
}: TaskCardProps) {
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [fileDragOver, setFileDragOver] = useState(false);
|
||||
@@ -1063,6 +1067,7 @@ function TaskCardComponent({
|
||||
const showInReviewMoveControl = task.column === "in-review" && Boolean(onMoveTask);
|
||||
const showCreatePrQuickAction =
|
||||
task.column === "in-review"
|
||||
&& autoMergeEnabled !== true
|
||||
&& !livePrInfo
|
||||
&& prAuthAvailable === true
|
||||
&& !isPaused
|
||||
@@ -1074,8 +1079,7 @@ function TaskCardComponent({
|
||||
|| task.status === "queued"
|
||||
|| Boolean(task.blockedBy)
|
||||
|| Boolean(task.overlapBlockedBy)
|
||||
|| Boolean(fanout && fanout.totalCount > 0)
|
||||
|| showCreatePrQuickAction;
|
||||
|| Boolean(fanout && fanout.totalCount > 0);
|
||||
|
||||
const enterEditMode = useCallback((e?: React.MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
@@ -1875,52 +1879,7 @@ function TaskCardComponent({
|
||||
</span>
|
||||
)}
|
||||
{(queued || task.status === "queued") && task.column !== "in-progress" && <span className="queued-badge"><Clock size={12} style={{ verticalAlign: "middle" }} /> Queued</span>}
|
||||
{showCreatePrQuickAction && (
|
||||
<button
|
||||
type="button"
|
||||
className="card-create-pr-action"
|
||||
title="Create a PR for this task"
|
||||
aria-label="Create pull request"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setIsPrCreateOpen(true);
|
||||
}}
|
||||
>
|
||||
<GitPullRequest size={12} />
|
||||
Create PR
|
||||
</button>
|
||||
)}
|
||||
{showInReviewMoveControl && (
|
||||
<div className="card-meta-move">
|
||||
<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>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)}
|
||||
{(task.assignedAgentId || taskProviders.length > 0) && (
|
||||
@@ -1946,9 +1905,24 @@ function TaskCardComponent({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showInReviewMoveControl && !metaRowVisible && (
|
||||
<div className="card-bottom-row">
|
||||
<div className="card-bottom-right-row">
|
||||
{(showCreatePrQuickAction || showInReviewMoveControl) && (
|
||||
<div className="card-action-row">
|
||||
{showCreatePrQuickAction && (
|
||||
<button
|
||||
type="button"
|
||||
className="card-create-pr-action"
|
||||
title="Create a PR for this task"
|
||||
aria-label="Create pull request"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setIsPrCreateOpen(true);
|
||||
}}
|
||||
>
|
||||
<GitPullRequest size={12} />
|
||||
Create PR
|
||||
</button>
|
||||
)}
|
||||
{showInReviewMoveControl && (
|
||||
<div className="card-send-back" ref={sendBackRef}>
|
||||
<button
|
||||
className="card-send-back-btn"
|
||||
@@ -1976,7 +1950,7 @@ function TaskCardComponent({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<PluginSlot slotId="task-card-badge" projectId={projectId} />
|
||||
|
||||
@@ -31,6 +31,8 @@ interface WorktreeGroupProps {
|
||||
blockerFanoutMap?: ReadonlyMap<string, BlockerFanoutEntry>;
|
||||
/** Whether GitHub CLI auth is available for creating PRs from task cards. */
|
||||
prAuthAvailable?: boolean;
|
||||
/** Whether project-level auto-merge is enabled, which hides manual Create PR card actions. */
|
||||
autoMergeEnabled?: boolean;
|
||||
}
|
||||
|
||||
function WorktreeGroupComponent({
|
||||
@@ -50,6 +52,7 @@ function WorktreeGroupComponent({
|
||||
workflowStepNameLookup,
|
||||
blockerFanoutMap,
|
||||
prAuthAvailable,
|
||||
autoMergeEnabled,
|
||||
}: WorktreeGroupProps) {
|
||||
return (
|
||||
<div className="worktree-group">
|
||||
@@ -60,7 +63,7 @@ function WorktreeGroupComponent({
|
||||
<span className="worktree-label">{label}</span>
|
||||
</div>
|
||||
{activeTasks.map((task) => (
|
||||
<TaskCard key={task.id} task={task} projectId={projectId} onOpenDetail={onOpenDetail} addToast={addToast} globalPaused={globalPaused} onUpdateTask={onUpdateTask} onRetryTask={onRetryTask} onOpenDetailWithTab={onOpenDetailWithTab} taskStuckTimeoutMs={taskStuckTimeoutMs} onOpenMission={onOpenMission} lastFetchTimeMs={lastFetchTimeMs} workflowStepNameLookup={workflowStepNameLookup} fanout={blockerFanoutMap?.get(task.id)} prAuthAvailable={prAuthAvailable} />
|
||||
<TaskCard key={task.id} task={task} projectId={projectId} onOpenDetail={onOpenDetail} addToast={addToast} globalPaused={globalPaused} onUpdateTask={onUpdateTask} onRetryTask={onRetryTask} onOpenDetailWithTab={onOpenDetailWithTab} taskStuckTimeoutMs={taskStuckTimeoutMs} onOpenMission={onOpenMission} lastFetchTimeMs={lastFetchTimeMs} workflowStepNameLookup={workflowStepNameLookup} fanout={blockerFanoutMap?.get(task.id)} prAuthAvailable={prAuthAvailable} autoMergeEnabled={autoMergeEnabled} />
|
||||
))}
|
||||
{queuedTasks.map((task) => (
|
||||
<TaskCard
|
||||
@@ -80,6 +83,7 @@ function WorktreeGroupComponent({
|
||||
workflowStepNameLookup={workflowStepNameLookup}
|
||||
fanout={blockerFanoutMap?.get(task.id)}
|
||||
prAuthAvailable={prAuthAvailable}
|
||||
autoMergeEnabled={autoMergeEnabled}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -413,6 +413,34 @@ describe("TaskCard", () => {
|
||||
expect(screen.queryByRole("button", { name: "Create pull request" })).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render Create PR quick action when autoMergeEnabled is true", () => {
|
||||
render(
|
||||
<TaskCard
|
||||
task={makeTask({ column: "in-review", paused: false, userPaused: false, prInfo: undefined as any })}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
prAuthAvailable={true}
|
||||
autoMergeEnabled={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole("button", { name: "Create pull request" })).toBeNull();
|
||||
});
|
||||
|
||||
it("renders Create PR quick action when autoMergeEnabled is false", () => {
|
||||
render(
|
||||
<TaskCard
|
||||
task={makeTask({ column: "in-review", paused: false, userPaused: false, prInfo: undefined as any })}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
prAuthAvailable={true}
|
||||
autoMergeEnabled={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Create pull request" })).toBeDefined();
|
||||
});
|
||||
|
||||
it("does not render Create PR quick action when task already has prInfo", () => {
|
||||
render(
|
||||
<TaskCard
|
||||
@@ -1621,7 +1649,7 @@ describe("TaskCard", () => {
|
||||
expect(actionsContainer?.contains(archiveBtn)).toBe(true);
|
||||
});
|
||||
|
||||
it("renders in-review Move control inline in card-meta when meta row is visible", () => {
|
||||
it("renders in-review Move control inside card-action-row when meta row is visible", () => {
|
||||
const { container } = render(
|
||||
<TaskCard
|
||||
task={makeTask({ column: "in-review", blockedBy: "FN-777" })}
|
||||
@@ -1633,15 +1661,16 @@ describe("TaskCard", () => {
|
||||
|
||||
const moveButton = screen.getByRole("button", { name: "Move task" });
|
||||
const metaRow = container.querySelector(".card-meta");
|
||||
const bottomRow = container.querySelector(".card-bottom-row");
|
||||
const actionRow = container.querySelector(".card-action-row");
|
||||
|
||||
expect(metaRow).not.toBeNull();
|
||||
expect(metaRow?.contains(moveButton)).toBe(true);
|
||||
expect(moveButton.closest(".card-meta")).not.toBeNull();
|
||||
expect(bottomRow).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();
|
||||
});
|
||||
|
||||
it("keeps in-review Move control in card-bottom-row when meta row is not visible", () => {
|
||||
it("keeps in-review Move control in card-action-row when meta row is not visible", () => {
|
||||
const { container } = render(
|
||||
<TaskCard
|
||||
task={makeTask({ column: "in-review", dependencies: [], blockedBy: undefined, overlapBlockedBy: undefined, status: undefined as any })}
|
||||
@@ -1652,17 +1681,37 @@ describe("TaskCard", () => {
|
||||
);
|
||||
|
||||
const moveButton = screen.getByRole("button", { name: "Move task" });
|
||||
const bottomRow = container.querySelector(".card-bottom-row");
|
||||
const actionRow = container.querySelector(".card-action-row");
|
||||
|
||||
expect(moveButton.closest(".card-bottom-right-row")).not.toBeNull();
|
||||
expect(bottomRow).not.toBeNull();
|
||||
expect(bottomRow?.contains(moveButton)).toBe(true);
|
||||
expect(actionRow).not.toBeNull();
|
||||
expect(actionRow?.contains(moveButton)).toBe(true);
|
||||
expect(moveButton.closest(".card-meta")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders Create PR before Move inside card-action-row", () => {
|
||||
render(
|
||||
<TaskCard
|
||||
task={makeTask({ column: "in-review", paused: false, userPaused: false, prInfo: undefined as any })}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
onMoveTask={vi.fn()}
|
||||
prAuthAvailable={true}
|
||||
autoMergeEnabled={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
const createPrButton = screen.getByRole("button", { name: "Create pull request" });
|
||||
const moveButton = screen.getByRole("button", { name: "Move task" });
|
||||
const actionRow = createPrButton.closest(".card-action-row");
|
||||
|
||||
expect(actionRow).not.toBeNull();
|
||||
expect(moveButton.closest(".card-action-row")).toBe(actionRow);
|
||||
expect(createPrButton.compareDocumentPosition(moveButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "inline meta-row variant", task: makeTask({ column: "in-review", blockedBy: "FN-777" }) },
|
||||
{ name: "fallback bottom-row variant", task: makeTask({ column: "in-review", dependencies: [], blockedBy: undefined, overlapBlockedBy: undefined, status: undefined as any }) },
|
||||
{ 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 }) => {
|
||||
const onMoveTask = vi.fn();
|
||||
render(
|
||||
|
||||
Reference in New Issue
Block a user