Persist GitLab tracking metadata and surface it across task APIs and dashboard views. - add task-store schema, migration, and update helpers for linked GitLab items and stale state - expose validated GitLab tracking updates through task workflow routes and legacy task payloads - render GitLab badges, detail-panel metadata, open/unlink actions, styling, i18n strings, and docs - cover persistence, route validation, task card badges, and task detail interactions with tests - add a patch changeset for the published Fusion package Files changed: .changeset/fn-7425-gitlab-tracking.md | 7 ++ docs/cli-reference.md | 2 +- docs/dashboard-guide.md | 3 + packages/core/src/__tests__/db-migrate.test.ts | 29 +++++ .../src/__tests__/store-gitlab-tracking.test.ts | 138 +++++++++++++++++++++ packages/core/src/db.ts | 10 +- packages/core/src/gitlab-tracking.ts | 10 ++ packages/core/src/index.ts | 3 +- packages/core/src/store.ts | 135 +++++++++++++++++++- packages/core/src/types.ts | 49 ++++++++ packages/dashboard/app/api/legacy.ts | 3 + packages/dashboard/app/components/GitLabBadge.tsx | 33 +++++ packages/dashboard/app/components/TaskCard.tsx | 7 +- .../dashboard/app/components/TaskCardBadge.tsx | 15 ++- .../dashboard/app/components/TaskDetailModal.css | 47 +++++-- .../dashboard/app/components/TaskDetailModal.tsx | 134 +++++++++++++++++++- .../app/components/__tests__/TaskCard.test.tsx | 49 ++++++++ .../TaskDetailModal.gitlab-tracking.test.tsx | 121 ++++++++++++++++++ .../__tests__/TaskDetailModal.test-helpers.ts | 1 + packages/dashboard/app/styles.css | 9 ++ .../src/__tests__/routes-tasks-ops.test.ts | 136 ++++++++++++++++++++ packages/dashboard/src/gitlab.ts | 24 +++- packages/dashboard/src/routes/register-gitlab.ts | 1 + .../src/routes/register-task-workflow-routes.ts | 106 +++++++++++++++- packages/i18n/locales/en/app.json | 31 +++++ packages/i18n/src/resources.d.ts | 31 +++++ 26 files changed, 1106 insertions(+), 28 deletions(-) Fusion-Task-Id: FN-7425 Fusion-Task-Lineage: 9b5e6005-7284-402e-995c-553be2275eff Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
122 lines
5.0 KiB
TypeScript
122 lines
5.0 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { render, screen, waitFor } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { makeTask, mockConfirm, noop, noopDelete, noopMerge, noopMove, noopOpenDetail, setupTaskDetailModalHooks } from "./TaskDetailModal.test-helpers";
|
|
import { TaskDetailModal } from "../TaskDetailModal";
|
|
|
|
setupTaskDetailModalHooks();
|
|
|
|
const projectIssue = {
|
|
kind: "project_issue" as const,
|
|
url: "https://gitlab.com/acme/app/-/issues/42",
|
|
instanceUrl: "https://gitlab.com",
|
|
host: "gitlab.com",
|
|
iid: 42,
|
|
projectPath: "acme/app",
|
|
title: "Project issue",
|
|
state: "opened",
|
|
createdAt: "2026-07-02T00:00:00.000Z",
|
|
lastSyncedAt: "2026-07-02T00:01:00.000Z",
|
|
};
|
|
|
|
function renderModal(task = makeTask({ column: "todo", gitlabTracking: { item: projectIssue } }), onTaskUpdated = vi.fn()) {
|
|
return render(
|
|
<TaskDetailModal
|
|
initialTab="definition"
|
|
task={task}
|
|
onClose={noop}
|
|
onMoveTask={noopMove}
|
|
onDeleteTask={noopDelete}
|
|
onMergeTask={noopMerge}
|
|
onOpenDetail={noopOpenDetail}
|
|
onTaskUpdated={onTaskUpdated}
|
|
addToast={noop}
|
|
/>,
|
|
);
|
|
}
|
|
|
|
describe("TaskDetailModal GitLab tracking", () => {
|
|
it("renders linked project issue metadata with provider-correct labels and actions", async () => {
|
|
const user = userEvent.setup();
|
|
renderModal();
|
|
|
|
expect(screen.getByText("GitLab tracking")).toBeInTheDocument();
|
|
expect(screen.getByText("Linked")).toBeInTheDocument();
|
|
expect(screen.getByRole("link", { name: "Project issue #42" })).toHaveAttribute("href", projectIssue.url);
|
|
|
|
await user.click(screen.getByRole("button", { name: "Expand GitLab tracking details" }));
|
|
expect(screen.getByText("Kind")).toBeInTheDocument();
|
|
expect(screen.getByText("gitlab.com")).toBeInTheDocument();
|
|
expect(screen.getByRole("link", { name: "Open linked GitLab item" })).toHaveAttribute("href", projectIssue.url);
|
|
expect(screen.getByRole("button", { name: "Unlink GitLab item" })).toBeInTheDocument();
|
|
expect(screen.queryByText("GitHub tracking")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("renders group issues, merge requests, stale state, and GitHub coexistence", async () => {
|
|
const user = userEvent.setup();
|
|
const staleGroupIssue = {
|
|
kind: "group_issue" as const,
|
|
url: "https://git.example.test/groups/platform/-/issues/9",
|
|
instanceUrl: "https://git.example.test",
|
|
host: "git.example.test",
|
|
iid: 9,
|
|
groupPath: "platform",
|
|
title: "Group issue",
|
|
state: "opened",
|
|
createdAt: "2026-07-02T00:00:00.000Z",
|
|
staleAt: "2026-07-02T01:00:00.000Z",
|
|
staleReason: "GitLab sync failed",
|
|
};
|
|
const task = makeTask({
|
|
column: "todo",
|
|
gitlabTracking: { item: staleGroupIssue },
|
|
githubTracking: { enabled: true, repoOverride: "runfusion/fusion" },
|
|
});
|
|
const { rerender } = renderModal(task);
|
|
|
|
expect(screen.getByText("Stale")).toBeInTheDocument();
|
|
expect(screen.getByRole("link", { name: "Group issue #9" })).toBeInTheDocument();
|
|
expect(screen.getByText("GitHub tracking")).toBeInTheDocument();
|
|
|
|
await user.click(screen.getByRole("button", { name: "Expand GitLab tracking details" }));
|
|
expect(screen.getByText("GitLab sync failed")).toBeInTheDocument();
|
|
|
|
rerender(
|
|
<TaskDetailModal
|
|
initialTab="definition"
|
|
task={makeTask({ column: "todo", gitlabTracking: { item: { ...projectIssue, kind: "merge_request", iid: 5, url: "https://gitlab.com/acme/app/-/merge_requests/5", title: "MR" } } })}
|
|
onClose={noop}
|
|
onMoveTask={noopMove}
|
|
onDeleteTask={noopDelete}
|
|
onMergeTask={noopMerge}
|
|
onOpenDetail={noopOpenDetail}
|
|
addToast={noop}
|
|
/>,
|
|
);
|
|
expect(screen.getByRole("link", { name: "Merge request !5" })).toBeInTheDocument();
|
|
});
|
|
|
|
it("unlinks after confirmation and does not render empty GitLab shells", async () => {
|
|
const user = userEvent.setup();
|
|
const onTaskUpdated = vi.fn();
|
|
const { updateTask } = await import("../../api");
|
|
vi.mocked(updateTask).mockResolvedValueOnce(makeTask({ column: "todo", gitlabTracking: { unlinkedAt: "2026-07-02T00:00:00.000Z" } }) as any);
|
|
mockConfirm.mockResolvedValueOnce(false).mockResolvedValueOnce(true);
|
|
renderModal(undefined, onTaskUpdated);
|
|
|
|
await user.click(screen.getByRole("button", { name: "Expand GitLab tracking details" }));
|
|
await user.click(screen.getByRole("button", { name: "Unlink GitLab item" }));
|
|
expect(updateTask).not.toHaveBeenCalled();
|
|
|
|
await user.click(screen.getByRole("button", { name: "Unlink GitLab item" }));
|
|
await waitFor(() => expect(updateTask).toHaveBeenCalledWith("FN-099", { gitlabTracking: { item: null } }, undefined));
|
|
expect(onTaskUpdated).toHaveBeenCalled();
|
|
});
|
|
|
|
it("omits GitLab tracking section when metadata is empty", () => {
|
|
renderModal(makeTask({ column: "todo", gitlabTracking: undefined }));
|
|
expect(screen.queryByTestId("detail-gitlab-tracking-section")).not.toBeInTheDocument();
|
|
expect(screen.queryByLabelText(/GitLab/i)).not.toBeInTheDocument();
|
|
});
|
|
});
|