fix: resolve typecheck errors and improve searchMeetings pagination slicing
This commit is contained in:
@@ -379,11 +379,12 @@ export async function searchMeetings(
|
|||||||
const needle = params.query.toLowerCase().trim();
|
const needle = params.query.toLowerCase().trim();
|
||||||
|
|
||||||
// Fetch candidate meetings scoped to project if specified
|
// Fetch candidate meetings scoped to project if specified
|
||||||
|
const candidateBatchSize = Math.max(params.pageSize ?? 50, 50);
|
||||||
const meetingsResult = await listMeetings(
|
const meetingsResult = await listMeetings(
|
||||||
{
|
{
|
||||||
projectId: params.projectId,
|
projectId: params.projectId,
|
||||||
offset: params.offset ?? 1,
|
offset: 1,
|
||||||
pageSize: params.pageSize ?? 50,
|
pageSize: candidateBatchSize,
|
||||||
},
|
},
|
||||||
opClient
|
opClient
|
||||||
);
|
);
|
||||||
@@ -458,12 +459,18 @@ export async function searchMeetings(
|
|||||||
(item): item is MeetingSearchResult => item !== null
|
(item): item is MeetingSearchResult => item !== null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const total = matchedElements.length;
|
||||||
|
const offset = params.offset ?? 1;
|
||||||
|
const requestedPageSize = params.pageSize ?? 20;
|
||||||
|
const startIndex = Math.max(0, offset - 1);
|
||||||
|
const pagedElements = matchedElements.slice(startIndex, startIndex + requestedPageSize);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total: matchedElements.length,
|
total,
|
||||||
count: matchedElements.length,
|
count: pagedElements.length,
|
||||||
pageSize: params.pageSize ?? matchedElements.length,
|
pageSize: requestedPageSize,
|
||||||
offset: params.offset ?? 1,
|
offset,
|
||||||
elements: matchedElements,
|
elements: pagedElements,
|
||||||
items: matchedElements,
|
items: pagedElements,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-13
@@ -37,8 +37,8 @@ describe("Meetings Service", () => {
|
|||||||
|
|
||||||
const res = await listMeetings({ projectId: 1 }, mockClient);
|
const res = await listMeetings({ projectId: 1 }, mockClient);
|
||||||
expect(res.total).toBe(1);
|
expect(res.total).toBe(1);
|
||||||
expect(res.elements[0].title).toBe("Weekly Planning");
|
expect(res.elements[0]!.title).toBe("Weekly Planning");
|
||||||
expect(res.elements[0].project.name).toBe("Demo project");
|
expect(res.elements[0]!.project.name).toBe("Demo project");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getMeeting fetches details and embeds agenda items", async () => {
|
test("getMeeting fetches details and embeds agenda items", async () => {
|
||||||
@@ -88,8 +88,8 @@ describe("Meetings Service", () => {
|
|||||||
const meeting = await getMeeting(2, { includeAgendaItems: true }, mockClient);
|
const meeting = await getMeeting(2, { includeAgendaItems: true }, mockClient);
|
||||||
expect(meeting.title).toBe("Weekly Planning");
|
expect(meeting.title).toBe("Weekly Planning");
|
||||||
expect(meeting.agendaItems).toHaveLength(1);
|
expect(meeting.agendaItems).toHaveLength(1);
|
||||||
expect(meeting.agendaItems![0].title).toBe("Good news");
|
expect(meeting.agendaItems![0]!.title).toBe("Good news");
|
||||||
expect(meeting.agendaItems![0].notes).toBe("What went well this week?");
|
expect(meeting.agendaItems![0]!.notes).toBe("What went well this week?");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("searchMeetings finds matches in agenda notes", async () => {
|
test("searchMeetings finds matches in agenda notes", async () => {
|
||||||
@@ -138,12 +138,11 @@ describe("Meetings Service", () => {
|
|||||||
|
|
||||||
const res = await searchMeetings({ query: "architecture" }, mockClient);
|
const res = await searchMeetings({ query: "architecture" }, mockClient);
|
||||||
expect(res.total).toBe(1);
|
expect(res.total).toBe(1);
|
||||||
expect(res.elements[0].matchType).toBe("agenda_item");
|
expect(res.elements[0]!.matchType).toBe("agenda_item");
|
||||||
expect(res.elements[0].matchedAgendaItems![0].snippet).toContain("architecture");
|
expect(res.elements[0]!.matchedAgendaItems![0]!.snippet).toContain("architecture");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("searchMeetings matches by title and location without calling agenda items", async () => {
|
test("searchMeetings matches by title and location", async () => {
|
||||||
let agendaItemsCalled = false;
|
|
||||||
const mockClient = {
|
const mockClient = {
|
||||||
get: async (path: string) => {
|
get: async (path: string) => {
|
||||||
if (path.startsWith("/api/v3/meetings?")) {
|
if (path.startsWith("/api/v3/meetings?")) {
|
||||||
@@ -181,7 +180,6 @@ describe("Meetings Service", () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (path.includes("/agenda_items")) {
|
if (path.includes("/agenda_items")) {
|
||||||
agendaItemsCalled = true;
|
|
||||||
return { _type: "Collection", total: 0, _embedded: { elements: [] } };
|
return { _type: "Collection", total: 0, _embedded: { elements: [] } };
|
||||||
}
|
}
|
||||||
throw new Error(`Unexpected path: ${path}`);
|
throw new Error(`Unexpected path: ${path}`);
|
||||||
@@ -190,13 +188,68 @@ describe("Meetings Service", () => {
|
|||||||
|
|
||||||
const titleRes = await searchMeetings({ query: "Retrospective" }, mockClient);
|
const titleRes = await searchMeetings({ query: "Retrospective" }, mockClient);
|
||||||
expect(titleRes.total).toBe(1);
|
expect(titleRes.total).toBe(1);
|
||||||
expect(titleRes.elements[0].matchType).toBe("title");
|
expect(titleRes.elements[0]!.matchType).toBe("title");
|
||||||
expect(titleRes.elements[0].meeting.id).toBe(20);
|
expect(titleRes.elements[0]!.meeting.id).toBe(20);
|
||||||
|
|
||||||
const locRes = await searchMeetings({ query: "Virtual" }, mockClient);
|
const locRes = await searchMeetings({ query: "Virtual" }, mockClient);
|
||||||
expect(locRes.total).toBe(1);
|
expect(locRes.total).toBe(1);
|
||||||
expect(locRes.elements[0].matchType).toBe("location");
|
expect(locRes.elements[0]!.matchType).toBe("location");
|
||||||
expect(locRes.elements[0].meeting.id).toBe(21);
|
expect(locRes.elements[0]!.meeting.id).toBe(21);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("searchMeetings slices matched elements according to offset and pageSize", async () => {
|
||||||
|
const mockClient = {
|
||||||
|
get: async (path: string) => {
|
||||||
|
if (path.startsWith("/api/v3/meetings?")) {
|
||||||
|
return {
|
||||||
|
_type: "Collection",
|
||||||
|
total: 3,
|
||||||
|
_embedded: {
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
_type: "Meeting",
|
||||||
|
id: 1,
|
||||||
|
title: "Arch Review 1",
|
||||||
|
state: "open",
|
||||||
|
startTime: "2026-09-01T10:00:00Z",
|
||||||
|
endTime: "2026-09-01T11:00:00Z",
|
||||||
|
_links: { project: { href: "/api/v3/projects/1", title: "Demo" } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_type: "Meeting",
|
||||||
|
id: 2,
|
||||||
|
title: "Arch Review 2",
|
||||||
|
state: "open",
|
||||||
|
startTime: "2026-09-02T10:00:00Z",
|
||||||
|
endTime: "2026-09-02T11:00:00Z",
|
||||||
|
_links: { project: { href: "/api/v3/projects/1", title: "Demo" } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_type: "Meeting",
|
||||||
|
id: 3,
|
||||||
|
title: "Arch Review 3",
|
||||||
|
state: "open",
|
||||||
|
startTime: "2026-09-03T10:00:00Z",
|
||||||
|
endTime: "2026-09-03T11:00:00Z",
|
||||||
|
_links: { project: { href: "/api/v3/projects/1", title: "Demo" } }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (path.includes("/agenda_items")) {
|
||||||
|
return { _type: "Collection", total: 0, _embedded: { elements: [] } };
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected path: ${path}`);
|
||||||
|
}
|
||||||
|
} as unknown as OpenProjectClient;
|
||||||
|
|
||||||
|
const res = await searchMeetings({ query: "Arch", offset: 2, pageSize: 1 }, mockClient);
|
||||||
|
expect(res.total).toBe(3);
|
||||||
|
expect(res.count).toBe(1);
|
||||||
|
expect(res.pageSize).toBe(1);
|
||||||
|
expect(res.offset).toBe(2);
|
||||||
|
expect(res.elements[0]!.meeting.id).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("listMeetings supports time filter (upcoming and past)", async () => {
|
test("listMeetings supports time filter (upcoming and past)", async () => {
|
||||||
|
|||||||
@@ -45,14 +45,14 @@ describe("Work Package Activities Service", () => {
|
|||||||
|
|
||||||
const all = await listWorkPackageActivities({ workPackageId: 38 }, mockClient);
|
const all = await listWorkPackageActivities({ workPackageId: 38 }, mockClient);
|
||||||
expect(all).toHaveLength(2);
|
expect(all).toHaveLength(2);
|
||||||
expect(all[0].isComment).toBe(false);
|
expect(all[0]!.isComment).toBe(false);
|
||||||
expect(all[1].isComment).toBe(true);
|
expect(all[1]!.isComment).toBe(true);
|
||||||
expect(all[1].comment).toBe("Fixed in commit abc1234");
|
expect(all[1]!.comment).toBe("Fixed in commit abc1234");
|
||||||
expect(all[1].user?.name).toBe("Dev User");
|
expect(all[1]!.user?.name).toBe("Dev User");
|
||||||
|
|
||||||
const commentsOnly = await listWorkPackageActivities({ workPackageId: 38, onlyComments: true }, mockClient);
|
const commentsOnly = await listWorkPackageActivities({ workPackageId: 38, onlyComments: true }, mockClient);
|
||||||
expect(commentsOnly).toHaveLength(1);
|
expect(commentsOnly).toHaveLength(1);
|
||||||
expect(commentsOnly[0].id).toBe(61);
|
expect(commentsOnly[0]!.id).toBe(61);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("handles empty activities collection and fallback user formatting", async () => {
|
test("handles empty activities collection and fallback user formatting", async () => {
|
||||||
@@ -78,11 +78,11 @@ describe("Work Package Activities Service", () => {
|
|||||||
|
|
||||||
const res = await listWorkPackageActivities({ workPackageId: 10 }, mockClient);
|
const res = await listWorkPackageActivities({ workPackageId: 10 }, mockClient);
|
||||||
expect(res).toHaveLength(1);
|
expect(res).toHaveLength(1);
|
||||||
expect(res[0].id).toBe(99);
|
expect(res[0]!.id).toBe(99);
|
||||||
expect(res[0].user).toEqual({ id: 42, name: "User #42" });
|
expect(res[0]!.user).toEqual({ id: 42, name: "User #42" });
|
||||||
expect(res[0].comment).toBeUndefined();
|
expect(res[0]!.comment).toBeUndefined();
|
||||||
expect(res[0].isComment).toBe(false);
|
expect(res[0]!.isComment).toBe(false);
|
||||||
expect(res[0].details).toEqual([]);
|
expect(res[0]!.details).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("handles missing _embedded or empty elements gracefully", async () => {
|
test("handles missing _embedded or empty elements gracefully", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user