feat: add listWorkPackageActivities domain service
This commit is contained in:
@@ -91,3 +91,84 @@ export async function searchWorkPackages(
|
|||||||
client
|
client
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ActivityDetail {
|
||||||
|
format: string;
|
||||||
|
raw: string;
|
||||||
|
html?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkPackageActivity {
|
||||||
|
id: number;
|
||||||
|
version: number;
|
||||||
|
createdAt: string;
|
||||||
|
user?: { id: number; name: string };
|
||||||
|
comment?: string;
|
||||||
|
details: ActivityDetail[];
|
||||||
|
isComment: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListWorkPackageActivitiesOptions {
|
||||||
|
workPackageId: number;
|
||||||
|
onlyComments?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists activities (field changes, comments, status updates) for a specific work package.
|
||||||
|
*/
|
||||||
|
export async function listWorkPackageActivities(
|
||||||
|
options: ListWorkPackageActivitiesOptions,
|
||||||
|
client?: OpenProjectClient
|
||||||
|
): Promise<WorkPackageActivity[]> {
|
||||||
|
const opClient = resolveClient(client);
|
||||||
|
const response = await opClient.get<Record<string, unknown>>(
|
||||||
|
`/api/v3/work_packages/${options.workPackageId}/activities`
|
||||||
|
);
|
||||||
|
|
||||||
|
const elements = (response._embedded as { elements?: unknown[] })?.elements ?? [];
|
||||||
|
const activities: WorkPackageActivity[] = [];
|
||||||
|
|
||||||
|
for (const item of elements) {
|
||||||
|
const rawItem = item as Record<string, unknown>;
|
||||||
|
const commentObj = rawItem.comment as { raw?: string } | undefined;
|
||||||
|
const commentRaw = typeof commentObj?.raw === "string" ? commentObj.raw.trim() : "";
|
||||||
|
const isComment = commentRaw.length > 0;
|
||||||
|
|
||||||
|
if (options.onlyComments && !isComment) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const links = (rawItem._links as Record<string, { href?: string; title?: string }>) ?? {};
|
||||||
|
const userLink = links.user;
|
||||||
|
let user: { id: number; name: string } | undefined;
|
||||||
|
if (userLink?.href) {
|
||||||
|
const match = userLink.href.match(/\/users\/(\d+)/);
|
||||||
|
if (match) {
|
||||||
|
user = {
|
||||||
|
id: Number(match[1]),
|
||||||
|
name: userLink.title ?? `User #${match[1]}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawDetails = (rawItem.details as Array<{ format?: string; raw?: string; html?: string }>) ?? [];
|
||||||
|
const details: ActivityDetail[] = rawDetails.map((d) => ({
|
||||||
|
format: d.format ?? "custom",
|
||||||
|
raw: d.raw ?? "",
|
||||||
|
html: d.html,
|
||||||
|
}));
|
||||||
|
|
||||||
|
activities.push({
|
||||||
|
id: Number(rawItem.id),
|
||||||
|
version: Number(rawItem.version ?? 1),
|
||||||
|
createdAt: String(rawItem.createdAt ?? ""),
|
||||||
|
user,
|
||||||
|
comment: commentRaw || undefined,
|
||||||
|
details,
|
||||||
|
isComment,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return activities;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { OpenProjectClient } from "../src/client/api-client.ts";
|
||||||
|
import { listWorkPackageActivities } from "../src/services/work-packages.ts";
|
||||||
|
|
||||||
|
describe("Work Package Activities Service", () => {
|
||||||
|
test("parses activities and separates comments from field changes", async () => {
|
||||||
|
const mockClient = {
|
||||||
|
get: async (path: string) => {
|
||||||
|
expect(path).toBe("/api/v3/work_packages/38/activities");
|
||||||
|
return {
|
||||||
|
_type: "Collection",
|
||||||
|
total: 2,
|
||||||
|
count: 2,
|
||||||
|
_embedded: {
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
_type: "Activity",
|
||||||
|
id: 60,
|
||||||
|
version: 1,
|
||||||
|
createdAt: "2026-09-05T14:24:56Z",
|
||||||
|
comment: { raw: "", html: "" },
|
||||||
|
details: [
|
||||||
|
{ format: "custom", raw: "Status set to In progress" }
|
||||||
|
],
|
||||||
|
_links: {
|
||||||
|
user: { href: "/api/v3/users/2", title: "Admin User" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_type: "Activity",
|
||||||
|
id: 61,
|
||||||
|
version: 2,
|
||||||
|
createdAt: "2026-09-05T15:00:00Z",
|
||||||
|
comment: { raw: "Fixed in commit abc1234", html: "<p>Fixed</p>" },
|
||||||
|
details: [],
|
||||||
|
_links: {
|
||||||
|
user: { href: "/api/v3/users/3", title: "Dev User" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} as unknown as OpenProjectClient;
|
||||||
|
|
||||||
|
const all = await listWorkPackageActivities({ workPackageId: 38 }, mockClient);
|
||||||
|
expect(all).toHaveLength(2);
|
||||||
|
expect(all[0].isComment).toBe(false);
|
||||||
|
expect(all[1].isComment).toBe(true);
|
||||||
|
expect(all[1].comment).toBe("Fixed in commit abc1234");
|
||||||
|
expect(all[1].user?.name).toBe("Dev User");
|
||||||
|
|
||||||
|
const commentsOnly = await listWorkPackageActivities({ workPackageId: 38, onlyComments: true }, mockClient);
|
||||||
|
expect(commentsOnly).toHaveLength(1);
|
||||||
|
expect(commentsOnly[0].id).toBe(61);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles empty activities collection and fallback user formatting", async () => {
|
||||||
|
const mockClient = {
|
||||||
|
get: async () => ({
|
||||||
|
_type: "Collection",
|
||||||
|
total: 1,
|
||||||
|
count: 1,
|
||||||
|
_embedded: {
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
id: 99,
|
||||||
|
version: 1,
|
||||||
|
createdAt: "2026-09-05T16:00:00Z",
|
||||||
|
_links: {
|
||||||
|
user: { href: "/api/v3/users/42" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} as unknown as OpenProjectClient;
|
||||||
|
|
||||||
|
const res = await listWorkPackageActivities({ workPackageId: 10 }, mockClient);
|
||||||
|
expect(res).toHaveLength(1);
|
||||||
|
expect(res[0].id).toBe(99);
|
||||||
|
expect(res[0].user).toEqual({ id: 42, name: "User #42" });
|
||||||
|
expect(res[0].comment).toBeUndefined();
|
||||||
|
expect(res[0].isComment).toBe(false);
|
||||||
|
expect(res[0].details).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles missing _embedded or empty elements gracefully", async () => {
|
||||||
|
const mockClient = {
|
||||||
|
get: async () => ({
|
||||||
|
_type: "Collection",
|
||||||
|
total: 0,
|
||||||
|
count: 0
|
||||||
|
})
|
||||||
|
} as unknown as OpenProjectClient;
|
||||||
|
|
||||||
|
const res = await listWorkPackageActivities({ workPackageId: 10 }, mockClient);
|
||||||
|
expect(res).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user