Introduce a dedicated modal flow for grouped shared-branch tasks in the dashboard. - Add GroupTaskModal component and styles for grouped task details and actions. - Wire grouped-task modal state through App, modal manager hooks, and modal container components. - Update board/column/task-card interactions to open grouped tasks in the new modal. - Adjust subtask breakdown behavior and add focused tests for grouped task modal/task-card flows. - Document the grouped-task modal behavior and add a changeset for @runfusion/fusion. Files changed: .changeset/fn-5825-group-task-modal.md | 5 + docs/dashboard-guide.md | 5 +- packages/dashboard/app/App.tsx | 6 + packages/dashboard/app/components/AppModals.tsx | 24 ++++ packages/dashboard/app/components/Board.tsx | 4 +- packages/dashboard/app/components/Column.tsx | 4 +- .../dashboard/app/components/GroupTaskModal.css | 79 ++++++++++ .../dashboard/app/components/GroupTaskModal.tsx | 159 +++++++++++++++++++++ .../app/components/SubtaskBreakdownModal.tsx | 16 ++- packages/dashboard/app/components/TaskCard.tsx | 8 ++ .../components/__tests__/GroupTaskModal.test.tsx | 105 ++++++++++++++ .../app/components/__tests__/TaskCard.test.tsx | 18 +++ packages/dashboard/app/hooks/useModalManager.ts | 16 +++ packages/dashboard/vitest.config.ts | 2 +- 14 files changed, 445 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-5825 Fusion-Task-Lineage: d9317a58-05ce-4437-8311-b7e2a186537b
106 lines
4.0 KiB
TypeScript
106 lines
4.0 KiB
TypeScript
import { render, screen, waitFor } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { GroupTaskModal } from "../GroupTaskModal";
|
|
import { apiGetBranchGroup, apiPromoteBranchGroup } from "../../api";
|
|
|
|
vi.mock("../../api", async () => {
|
|
const actual = await vi.importActual<typeof import("../../api")>("../../api");
|
|
return {
|
|
...actual,
|
|
apiGetBranchGroup: vi.fn(),
|
|
apiPromoteBranchGroup: vi.fn(),
|
|
};
|
|
});
|
|
|
|
vi.mock("../../hooks/useNavigationHistory", () => ({
|
|
useNavigationHistory: () => ({ canGoBack: false, goBack: vi.fn() }),
|
|
useNavigationHistoryContext: () => ({ pushNav: vi.fn(), replaceCurrent: vi.fn() }),
|
|
}));
|
|
|
|
const mockedGet = vi.mocked(apiGetBranchGroup);
|
|
const mockedPromote = vi.mocked(apiPromoteBranchGroup);
|
|
|
|
function makeGroup(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
id: "BG-1",
|
|
branchName: "feature/shared",
|
|
status: "open",
|
|
autoMerge: false,
|
|
prState: "none",
|
|
prUrl: null,
|
|
prNumber: null,
|
|
completion: { landed: 1, total: 2, complete: false },
|
|
members: [
|
|
{ taskId: "FN-1", title: "First", column: "done", landed: true },
|
|
{ taskId: "FN-2", title: "Second", column: "todo", landed: false },
|
|
],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("GroupTaskModal", () => {
|
|
beforeEach(() => {
|
|
mockedPromote.mockReset();
|
|
mockedGet.mockReset();
|
|
});
|
|
|
|
it("renders group summary and member open action", async () => {
|
|
mockedGet.mockResolvedValue({ group: makeGroup() } as Awaited<ReturnType<typeof apiGetBranchGroup>>);
|
|
const onOpenMemberTask = vi.fn();
|
|
|
|
render(<GroupTaskModal isOpen onClose={vi.fn()} groupId="BG-1" onOpenMemberTask={onOpenMemberTask} />);
|
|
|
|
expect(await screen.findByText("feature/shared")).toBeDefined();
|
|
expect(screen.getByText("1 of 2 members finished")).toBeDefined();
|
|
await userEvent.click(screen.getAllByRole("button", { name: "Open task" })[0]);
|
|
expect(onOpenMemberTask).toHaveBeenCalledWith("FN-1");
|
|
});
|
|
|
|
it("hides promote controls until complete", async () => {
|
|
mockedGet.mockResolvedValue({ group: makeGroup() } as Awaited<ReturnType<typeof apiGetBranchGroup>>);
|
|
|
|
render(<GroupTaskModal isOpen onClose={vi.fn()} groupId="BG-1" onOpenMemberTask={vi.fn()} />);
|
|
|
|
await screen.findByText("1 of 2 members finished");
|
|
expect(screen.queryByRole("button", { name: /open pr|merge group into main/i })).toBeNull();
|
|
});
|
|
|
|
it("promotes when complete and auto-merge is off", async () => {
|
|
mockedGet
|
|
.mockResolvedValueOnce({
|
|
group: makeGroup({ completion: { landed: 2, total: 2, complete: true }, members: [
|
|
{ taskId: "FN-1", title: "First", column: "done", landed: true },
|
|
{ taskId: "FN-2", title: "Second", column: "done", landed: true },
|
|
] }),
|
|
} as Awaited<ReturnType<typeof apiGetBranchGroup>>)
|
|
.mockResolvedValueOnce({
|
|
group: makeGroup({ completion: { landed: 2, total: 2, complete: true } }),
|
|
} as Awaited<ReturnType<typeof apiGetBranchGroup>>);
|
|
|
|
mockedPromote.mockResolvedValue({ ok: true } as Awaited<ReturnType<typeof apiPromoteBranchGroup>>);
|
|
|
|
render(<GroupTaskModal isOpen onClose={vi.fn()} groupId="BG-1" onOpenMemberTask={vi.fn()} />);
|
|
|
|
const action = await screen.findByRole("button", { name: /open pr/i });
|
|
await userEvent.click(action);
|
|
await waitFor(() => expect(mockedPromote).toHaveBeenCalledWith("BG-1", undefined));
|
|
});
|
|
|
|
it("renders tracked pr info when present", async () => {
|
|
mockedGet.mockResolvedValue({
|
|
group: makeGroup({
|
|
completion: { landed: 2, total: 2, complete: true },
|
|
prState: "open",
|
|
prUrl: "https://github.com/org/repo/pull/1",
|
|
prNumber: 1,
|
|
}),
|
|
} as Awaited<ReturnType<typeof apiGetBranchGroup>>);
|
|
|
|
render(<GroupTaskModal isOpen onClose={vi.fn()} groupId="BG-1" onOpenMemberTask={vi.fn()} />);
|
|
|
|
const link = await screen.findByRole("link", { name: /PR #1/i });
|
|
expect(link.getAttribute("href")).toContain("/pull/1");
|
|
expect(link.textContent).toContain("open");
|
|
});
|
|
});
|