FN-5822: wire shared branch-group visibility and merge controls
Add dashboard and API support for viewing shared branch groups and triggering group merge actions. - add branch-group API routes and register them in integrated routers - extend dashboard legacy API client with branch-group fetch and merge-control actions - add BranchGroupCard UI + styles and surface it in task/detail/subtask views - add dashboard/API test coverage for branch-group routes and UI integration points - document branch-group behavior and add a changeset for @runfusion/fusion Files changed: .changeset/fn-5822-branch-group-dashboard.md | 5 + docs/dashboard-guide.md | 31 ++++++ packages/dashboard/app/api/legacy.ts | 54 +++++++++ .../dashboard/app/components/BranchGroupCard.css | 83 ++++++++++++++ .../dashboard/app/components/BranchGroupCard.tsx | 123 +++++++++++++++++++++ .../app/components/SubtaskBreakdownModal.tsx | 3 + packages/dashboard/app/components/TaskCard.tsx | 19 ++++ .../dashboard/app/components/TaskDetailModal.tsx | 4 + .../components/__tests__/BranchGroupCard.test.tsx | 96 ++++++++++++++++ .../__tests__/SubtaskBreakdownModal.test.tsx | 1 + .../app/components/__tests__/TaskCard.test.tsx | 16 ++++ .../components/__tests__/TaskDetailModal.test.tsx | 22 ++++ .../src/__tests__/routes-branch-groups.test.ts | 119 ++++++++++++++++++++ .../src/routes/register-branch-groups-routes.ts | 118 ++++++++++++++++++++ .../src/routes/register-integrated-routers.ts | 13 +++ packages/dashboard/vitest.config.ts | 4 +- 16 files changed, 709 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-5822 Fusion-Task-Lineage: 199c25b8-f6ec-43b4-8fab-509f23fb5ac7
This commit is contained in:
@@ -77,6 +77,8 @@ import type {
|
||||
ProjectNodePathMapping,
|
||||
ApprovalRequestStatus,
|
||||
TaskIdIntegrityReport,
|
||||
BranchGroup,
|
||||
BranchGroupPrState,
|
||||
} from "@fusion/core";
|
||||
import type { PlanningQuestion, PlanningSummary } from "@fusion/core";
|
||||
import type { GithubIssueAction, ScheduledTask, ScheduledTaskCreateInput, ScheduledTaskUpdateInput, AutomationRunResult, Routine, RoutineCreateInput, RoutineUpdateInput, RoutineExecutionResult } from "@fusion/core";
|
||||
@@ -564,6 +566,58 @@ export function mergeTask(id: string, projectId?: string): Promise<MergeResult>
|
||||
return api<MergeResult>(withProjectId(`/tasks/${id}/merge`, projectId), { method: "POST" });
|
||||
}
|
||||
|
||||
export interface BranchGroupMemberSummary {
|
||||
taskId: string;
|
||||
title: string;
|
||||
column: Task["column"];
|
||||
landed: boolean;
|
||||
}
|
||||
|
||||
export interface BranchGroupSummary extends BranchGroup {
|
||||
members: BranchGroupMemberSummary[];
|
||||
completion: {
|
||||
landed: number;
|
||||
total: number;
|
||||
complete: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PromoteBranchGroupResult {
|
||||
groupId: string;
|
||||
status?: BranchGroup["status"];
|
||||
prState?: BranchGroupPrState;
|
||||
prNumber?: number;
|
||||
prUrl?: string;
|
||||
}
|
||||
|
||||
export function apiListBranchGroups(projectId?: string, status?: BranchGroup["status"]): Promise<{ groups: BranchGroupSummary[] }> {
|
||||
const search = new URLSearchParams();
|
||||
if (status) search.set("status", status);
|
||||
const suffix = search.size > 0 ? `?${search.toString()}` : "";
|
||||
return api<{ groups: BranchGroupSummary[] }>(withProjectId(`/branch-groups${suffix}`, projectId));
|
||||
}
|
||||
|
||||
export function apiGetBranchGroup(id: string, projectId?: string): Promise<{ group: BranchGroupSummary }> {
|
||||
return api<{ group: BranchGroupSummary }>(withProjectId(`/branch-groups/${id}`, projectId));
|
||||
}
|
||||
|
||||
export function apiAssignTaskBranchGroup(
|
||||
payload: { taskId: string; groupId?: string | null; branchName?: string },
|
||||
projectId?: string,
|
||||
): Promise<{ taskId: string; groupId: string | null }> {
|
||||
return api<{ taskId: string; groupId: string | null }>(withProjectId("/branch-groups/assign", projectId), {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
}
|
||||
|
||||
export function apiPromoteBranchGroup(id: string, projectId?: string): Promise<PromoteBranchGroupResult> {
|
||||
return api<PromoteBranchGroupResult>(withProjectId(`/branch-groups/${id}/promote`, projectId), {
|
||||
method: "POST",
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
}
|
||||
|
||||
export type RecoverBranchBindingOutcome =
|
||||
| { taskId: string; result: "applied"; branch: string; aheadCount: number; integrationBase: string; previousBranch: string | null }
|
||||
| { taskId: string; result: "skipped"; reason: "binding-intact" | "no-live-branch" | "ambiguous-candidates" | "no-unique-work"; candidates?: Array<{ branch: string; aheadCount: number }> };
|
||||
|
||||
83
packages/dashboard/app/components/BranchGroupCard.css
Normal file
83
packages/dashboard/app/components/BranchGroupCard.css
Normal file
@@ -0,0 +1,83 @@
|
||||
.branch-group-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.branch-group-card-error {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.branch-group-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.branch-group-card-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.branch-group-card-badge {
|
||||
background: var(--surface-elevated);
|
||||
}
|
||||
|
||||
.branch-group-card-progress-text {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.branch-group-card-progress {
|
||||
width: 100%;
|
||||
height: var(--space-xs);
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--surface-elevated);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.branch-group-card-progress-fill {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: var(--color-info);
|
||||
}
|
||||
|
||||
.branch-group-card-members {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--space-xs) / 2);
|
||||
}
|
||||
|
||||
.branch-group-card-member {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.branch-group-card-member-title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.branch-group-card-member-status {
|
||||
color: var(--text-muted);
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.branch-group-card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.branch-group-card-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
123
packages/dashboard/app/components/BranchGroupCard.tsx
Normal file
123
packages/dashboard/app/components/BranchGroupCard.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import "./BranchGroupCard.css";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { CheckCircle2, CircleDashed, ExternalLink, GitBranch, GitPullRequest, Loader2 } from "lucide-react";
|
||||
import type { BranchGroupSummary } from "../api";
|
||||
import { apiGetBranchGroup, apiPromoteBranchGroup } from "../api";
|
||||
import { subscribeSse } from "../sse-bus";
|
||||
|
||||
interface BranchGroupCardProps {
|
||||
groupId: string;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
export function BranchGroupCard({ groupId, projectId }: BranchGroupCardProps) {
|
||||
const [group, setGroup] = useState<BranchGroupSummary | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [promoting, setPromoting] = useState(false);
|
||||
|
||||
const loadGroup = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiGetBranchGroup(groupId, projectId);
|
||||
setGroup(response.group);
|
||||
setError(null);
|
||||
} catch (loadError) {
|
||||
const message = loadError instanceof Error ? loadError.message : "Failed to load branch group";
|
||||
setError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [groupId, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
void loadGroup();
|
||||
}, [loadGroup]);
|
||||
|
||||
useEffect(() => {
|
||||
const query = projectId ? `?projectId=${encodeURIComponent(projectId)}` : "";
|
||||
return subscribeSse(`/api/events${query}`, {
|
||||
events: {
|
||||
"task:updated": () => {
|
||||
void loadGroup();
|
||||
},
|
||||
},
|
||||
onReconnect: () => {
|
||||
void loadGroup();
|
||||
},
|
||||
});
|
||||
}, [loadGroup, projectId]);
|
||||
|
||||
const completionText = useMemo(() => {
|
||||
if (!group) return "";
|
||||
return `${group.completion.landed} of ${group.completion.total} members finished`;
|
||||
}, [group]);
|
||||
|
||||
const onPromote = useCallback(async () => {
|
||||
setPromoting(true);
|
||||
try {
|
||||
await apiPromoteBranchGroup(groupId, projectId);
|
||||
await loadGroup();
|
||||
} finally {
|
||||
setPromoting(false);
|
||||
}
|
||||
}, [groupId, loadGroup, projectId]);
|
||||
|
||||
if (loading) {
|
||||
return <div className="card branch-group-card"><Loader2 className="spin" size={14} /> Loading branch group…</div>;
|
||||
}
|
||||
|
||||
if (error || !group) {
|
||||
return <div className="card branch-group-card branch-group-card-error">{error ?? "Branch group unavailable"}</div>;
|
||||
}
|
||||
|
||||
const completionPercent = group.completion.total > 0
|
||||
? (group.completion.landed / group.completion.total) * 100
|
||||
: 0;
|
||||
const complete = group.completion.complete;
|
||||
|
||||
return (
|
||||
<section className="card branch-group-card">
|
||||
<header className="branch-group-card-header">
|
||||
<div className="branch-group-card-title">
|
||||
<GitBranch size={14} />
|
||||
<strong>{group.branchName}</strong>
|
||||
</div>
|
||||
<span className="badge branch-group-card-badge">Group {group.id}</span>
|
||||
</header>
|
||||
<div className="branch-group-card-progress-text">{completionText}</div>
|
||||
<div className="branch-group-card-progress" role="progressbar" aria-valuenow={group.completion.landed} aria-valuemin={0} aria-valuemax={group.completion.total}>
|
||||
<span className="branch-group-card-progress-fill" style={{ width: `${completionPercent}%` }} />
|
||||
</div>
|
||||
|
||||
<ul className="branch-group-card-members">
|
||||
{group.members.map((member) => (
|
||||
<li key={member.taskId} className="branch-group-card-member">
|
||||
<span className={`status-dot ${member.landed ? "status-dot--online" : "status-dot--pending"}`} />
|
||||
<span className="branch-group-card-member-title">{member.taskId} · {member.title}</span>
|
||||
<span className="branch-group-card-member-status">{member.landed ? <CheckCircle2 size={14} /> : <CircleDashed size={14} />}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{complete && (
|
||||
<div className="branch-group-card-actions">
|
||||
{group.prUrl && (
|
||||
<a className="btn" href={group.prUrl} target="_blank" rel="noreferrer">
|
||||
<GitPullRequest size={14} /> PR #{group.prNumber ?? "—"}
|
||||
<ExternalLink size={12} />
|
||||
</a>
|
||||
)}
|
||||
{group.autoMerge ? (
|
||||
<span className="badge">Auto-merge enabled</span>
|
||||
) : (
|
||||
<button type="button" className="btn" onClick={() => void onPromote()} disabled={promoting}>
|
||||
{promoting ? <Loader2 size={14} className="spin" /> : <GitPullRequest size={14} />}
|
||||
{group.prState === "none" ? "Open PR" : "Merge group into main"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -734,6 +734,9 @@ export function SubtaskBreakdownModal({ isOpen, onClose, initialDescription, onT
|
||||
<option value="shared">Shared merge target — subtasks run on their own branches</option>
|
||||
<option value="per-task-derived">Per-task branches derived from planning branch</option>
|
||||
</select>
|
||||
{branchAssignmentMode === "shared" && branchName.trim() && (
|
||||
<p className="text-muted">Grouped on shared branch <strong>{branchName.trim()}</strong></p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{subtasks.map((subtask, index) => {
|
||||
|
||||
@@ -1891,6 +1891,25 @@ function TaskCardComponent({
|
||||
<span className="card-branch-value">{branchMetadata.baseBranch}</span>
|
||||
</span>
|
||||
)}
|
||||
{task.branchContext?.groupId && (
|
||||
<span
|
||||
className="card-branch-chip"
|
||||
title={
|
||||
task.branchContext.assignmentMode === "shared" && branchMetadata.branch
|
||||
? `${task.branchContext.groupId} · ${branchMetadata.branch}`
|
||||
: task.branchContext.groupId
|
||||
}
|
||||
>
|
||||
<span className="card-branch-label">
|
||||
{task.branchContext.assignmentMode === "shared" ? "Shared" : "Group"}
|
||||
</span>
|
||||
<span className="card-branch-value">
|
||||
{task.branchContext.assignmentMode === "shared" && branchMetadata.branch
|
||||
? branchMetadata.branch
|
||||
: task.branchContext.groupId}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showProgressSection && (() => {
|
||||
|
||||
@@ -38,6 +38,7 @@ import { WorkflowResultsTab } from "./WorkflowResultsTab";
|
||||
import { RoutingTab } from "./RoutingTab";
|
||||
import { TaskDocumentsTab } from "./TaskDocumentsTab";
|
||||
import { TaskTokenStatsPanel } from "./TaskTokenStatsPanel";
|
||||
import { BranchGroupCard } from "./BranchGroupCard";
|
||||
import { PluginSlot } from "./PluginSlot";
|
||||
import { ProviderIcon } from "./ProviderIcon";
|
||||
import { subscribeSse } from "../sse-bus";
|
||||
@@ -2624,6 +2625,9 @@ export function TaskDetailContent({
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{task.branchContext?.groupId && (
|
||||
<BranchGroupCard groupId={task.branchContext.groupId} projectId={projectId} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{task.status === "failed" && task.error && (
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import React from "react";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { BranchGroupCard } from "../BranchGroupCard";
|
||||
|
||||
const apiGetBranchGroup = vi.fn();
|
||||
const apiPromoteBranchGroup = vi.fn();
|
||||
|
||||
vi.mock("../../api", () => ({
|
||||
apiGetBranchGroup: (...args: unknown[]) => apiGetBranchGroup(...args),
|
||||
apiPromoteBranchGroup: (...args: unknown[]) => apiPromoteBranchGroup(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../sse-bus", () => ({
|
||||
subscribeSse: () => () => {},
|
||||
}));
|
||||
|
||||
vi.mock("lucide-react", () => ({
|
||||
CheckCircle2: () => null,
|
||||
CircleDashed: () => null,
|
||||
ExternalLink: () => null,
|
||||
GitBranch: () => null,
|
||||
GitPullRequest: () => null,
|
||||
Loader2: () => null,
|
||||
}));
|
||||
|
||||
function makeGroup(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: "BG-1",
|
||||
sourceType: "planning",
|
||||
sourceId: "PS-1",
|
||||
branchName: "feature/shared",
|
||||
autoMerge: false,
|
||||
prState: "none",
|
||||
status: "open",
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
members: [
|
||||
{ taskId: "FN-1", title: "one", column: "done", landed: true },
|
||||
{ taskId: "FN-2", title: "two", column: "in-review", landed: false },
|
||||
],
|
||||
completion: { landed: 1, total: 2, complete: false },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("BranchGroupCard", () => {
|
||||
beforeEach(() => {
|
||||
apiGetBranchGroup.mockReset();
|
||||
apiPromoteBranchGroup.mockReset();
|
||||
});
|
||||
|
||||
it("hides promote control while incomplete", async () => {
|
||||
apiGetBranchGroup.mockResolvedValue({ group: makeGroup() });
|
||||
render(<BranchGroupCard groupId="BG-1" />);
|
||||
await screen.findByText("1 of 2 members finished");
|
||||
expect(screen.queryByRole("button", { name: /open pr|merge group into main/i })).toBeNull();
|
||||
});
|
||||
|
||||
it("shows promote and calls API when complete + autoMerge off", async () => {
|
||||
apiGetBranchGroup
|
||||
.mockResolvedValueOnce({ group: makeGroup({ completion: { landed: 2, total: 2, complete: true }, members: [{ taskId: "FN-1", title: "one", column: "done", landed: true }, { taskId: "FN-2", title: "two", column: "done", landed: true }] }) })
|
||||
.mockResolvedValueOnce({ group: makeGroup({ completion: { landed: 2, total: 2, complete: true }, members: [{ taskId: "FN-1", title: "one", column: "done", landed: true }, { taskId: "FN-2", title: "two", column: "done", landed: true }], prState: "open", prNumber: 22, prUrl: "https://example/pr/22" }) });
|
||||
apiPromoteBranchGroup.mockResolvedValue({ groupId: "BG-1", prState: "open" });
|
||||
|
||||
render(<BranchGroupCard groupId="BG-1" />);
|
||||
const button = await screen.findByRole("button", { name: /open pr/i });
|
||||
fireEvent.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(apiPromoteBranchGroup).toHaveBeenCalledWith("BG-1", undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows auto-merge badge when complete and autoMerge is on", async () => {
|
||||
apiGetBranchGroup.mockResolvedValue({
|
||||
group: makeGroup({
|
||||
autoMerge: true,
|
||||
completion: { landed: 2, total: 2, complete: true },
|
||||
members: [{ taskId: "FN-1", title: "one", column: "done", landed: true }, { taskId: "FN-2", title: "two", column: "done", landed: true }],
|
||||
}),
|
||||
});
|
||||
|
||||
render(<BranchGroupCard groupId="BG-1" />);
|
||||
expect(await screen.findByText("Auto-merge enabled")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /open pr|merge group into main/i })).toBeNull();
|
||||
});
|
||||
|
||||
it("renders tracked group PR link when present", async () => {
|
||||
apiGetBranchGroup.mockResolvedValue({
|
||||
group: makeGroup({ completion: { landed: 2, total: 2, complete: true }, members: [{ taskId: "FN-1", title: "one", column: "done", landed: true }, { taskId: "FN-2", title: "two", column: "done", landed: true }], prState: "open", prNumber: 9, prUrl: "https://example/pr/9" }),
|
||||
});
|
||||
render(<BranchGroupCard groupId="BG-1" />);
|
||||
expect(await screen.findByRole("link", { name: /pr #9/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -304,6 +304,7 @@ describe("SubtaskBreakdownModal", () => {
|
||||
fireEvent.change(branchNameInput, { target: { value: "feature/planning-shared" } });
|
||||
fireEvent.change(mergeTargetInput, { target: { value: "develop" } });
|
||||
fireEvent.change(branchModeSelect, { target: { value: "shared" } });
|
||||
expect(await screen.findByText("Grouped on shared branch")).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText("Create Tasks"));
|
||||
|
||||
|
||||
@@ -1393,6 +1393,22 @@ describe("TaskCard", () => {
|
||||
expect(screen.getByText("develop")).toBeDefined();
|
||||
});
|
||||
|
||||
it("shows shared group chip with shared branch label for grouped tasks", () => {
|
||||
render(
|
||||
<TaskCard
|
||||
task={makeTask({
|
||||
branch: "feature/shared-branch",
|
||||
branchContext: { groupId: "BG-22", source: "planning", assignmentMode: "shared" },
|
||||
})}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Shared")).toBeDefined();
|
||||
expect(screen.getAllByText("feature/shared-branch").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("keeps long non-default branch names readable via text and title semantics", () => {
|
||||
const longBranch = "feature/fn-3423-display-very-long-working-branch-name-for-card-metadata";
|
||||
const { container } = render(
|
||||
|
||||
@@ -13,6 +13,10 @@ import {
|
||||
} from "./TaskDetailModal.test-helpers";
|
||||
import { TaskDetailModal } from "../TaskDetailModal";
|
||||
|
||||
vi.mock("../BranchGroupCard", () => ({
|
||||
BranchGroupCard: ({ groupId }: { groupId: string }) => <div>Mock Branch Group {groupId}</div>,
|
||||
}));
|
||||
|
||||
setupTaskDetailModalHooks();
|
||||
|
||||
describe("TaskDetailModal GitHub tracking CTA", () => {
|
||||
@@ -88,6 +92,24 @@ describe("TaskDetailModal GitHub tracking CTA", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("TaskDetailModal branch group surfacing", () => {
|
||||
it("renders branch group card when task has group context", () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ branchContext: { groupId: "BG-1", source: "planning", assignmentMode: "shared" } })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Mock Branch Group BG-1")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("TaskDetailModal delete affordance", () => {
|
||||
it("archives done task when Archive Instead is chosen", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
Reference in New Issue
Block a user