forked from EduCraft/curriculum-project-hub
feat: add paginated project discovery
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
import { afterAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
browseBindableFolder,
|
||||
discoverBindableProjects,
|
||||
normalizeProjectSearchQuery,
|
||||
} from "../../src/projectDiscovery.js";
|
||||
import { DEFAULT_ORG_ID, prisma, resetDb } from "./helpers.js";
|
||||
|
||||
describe("project discovery", () => {
|
||||
beforeEach(async () => {
|
||||
await resetDb();
|
||||
await seedUser("owner", "ou_owner", "OWNER");
|
||||
await seedUser("member", "ou_member", "MEMBER");
|
||||
});
|
||||
|
||||
afterAll(async () => prisma.$disconnect());
|
||||
|
||||
it("normalizes project codes across punctuation, width and case", () => {
|
||||
expect(normalizeProjectSearchQuery(" TH-141 ")).toBe("th141");
|
||||
expect(normalizeProjectSearchQuery("th_141")).toBe("th141");
|
||||
});
|
||||
|
||||
it("searches normalized codes, Chinese names and complete folder breadcrumbs", async () => {
|
||||
const root = await createFolder("root-legacy", null, "旧教学资产");
|
||||
const subject = await createFolder("folder-physics", root.id, "物理竞赛教研");
|
||||
const thermal = await createFolder("folder-thermal", subject.id, "TH_热学专题");
|
||||
await createProject("project-th141", thermal.id, "TH-141_表面张力的严肃理论");
|
||||
await createProject("project-fullwidth", thermal.id, "TH-142_全角编号");
|
||||
await createProject("project-explicit-code", thermal.id, "表面课程", "PHY-001");
|
||||
await createProject("project-other", thermal.id, "TH-211_理想气体静力学");
|
||||
|
||||
const byCode = await discover("TH141");
|
||||
expect(byCode.items[0]).toMatchObject({
|
||||
projectId: "project-th141",
|
||||
breadcrumb: "旧教学资产 / 物理竞赛教研 / TH_热学专题",
|
||||
});
|
||||
await expect(discover("表面张力")).resolves.toMatchObject({
|
||||
totalItems: 1,
|
||||
items: [{ projectId: "project-th141" }],
|
||||
});
|
||||
await expect(discover("TH142")).resolves.toMatchObject({
|
||||
items: [expect.objectContaining({ projectId: "project-fullwidth" })],
|
||||
});
|
||||
await expect(discover("PHY001")).resolves.toMatchObject({
|
||||
items: [expect.objectContaining({ projectId: "project-explicit-code" })],
|
||||
});
|
||||
const byPath = await discover("物理竞赛教研");
|
||||
expect(byPath.items.map((item) => item.projectId)).toEqual(expect.arrayContaining([
|
||||
"project-th141",
|
||||
"project-other",
|
||||
]));
|
||||
await expect(discover("物理竞赛 TH141")).resolves.toMatchObject({
|
||||
items: [expect.objectContaining({ projectId: "project-th141" })],
|
||||
});
|
||||
await expect(discover("%")).resolves.toMatchObject({ totalItems: 0 });
|
||||
});
|
||||
|
||||
it("refreshes descendant breadcrumbs after a folder rename", async () => {
|
||||
const root = await createFolder("root-before", null, "旧目录");
|
||||
const child = await createFolder("child", root.id, "子目录");
|
||||
await createProject("project-refresh", child.id, "项目");
|
||||
|
||||
await prisma.folder.update({ where: { id: root.id }, data: { name: "新目录" } });
|
||||
|
||||
await expect(discover("新目录")).resolves.toMatchObject({
|
||||
totalItems: 1,
|
||||
items: [{ projectId: "project-refresh", breadcrumb: "新目录 / 子目录" }],
|
||||
});
|
||||
await expect(discover("旧目录")).resolves.toMatchObject({ totalItems: 0 });
|
||||
});
|
||||
|
||||
it("filters authorization before counting and paginating", async () => {
|
||||
for (let index = 0; index < 12; index += 1) {
|
||||
await createProject(`project-${index}`, null, `TH-${index}`);
|
||||
}
|
||||
await prisma.permissionGrant.create({
|
||||
data: {
|
||||
resourceType: "PROJECT",
|
||||
resourceId: "project-11",
|
||||
principalType: "USER",
|
||||
principalId: "ou_member",
|
||||
role: "MANAGE",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await discoverBindableProjects({
|
||||
prisma,
|
||||
organizationId: DEFAULT_ORG_ID,
|
||||
actorFeishuOpenId: "ou_member",
|
||||
isOrgAdmin: false,
|
||||
query: "TH",
|
||||
page: 2,
|
||||
pageSize: 5,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({ page: 1, totalItems: 1, totalPages: 1 });
|
||||
expect(result.items.map((item) => item.projectId)).toEqual(["project-11"]);
|
||||
});
|
||||
|
||||
it("browses the preserved folder tree and paginates projects", async () => {
|
||||
const root = await createFolder("root", null, "旧教学资产");
|
||||
const child = await createFolder("child", root.id, "物理竞赛教研");
|
||||
await createProject("project-root", root.id, "根目录项目");
|
||||
|
||||
const result = await browseBindableFolder({
|
||||
prisma,
|
||||
organizationId: DEFAULT_ORG_ID,
|
||||
actorFeishuOpenId: "ou_owner",
|
||||
isOrgAdmin: true,
|
||||
folderId: root.id,
|
||||
});
|
||||
|
||||
expect(result.breadcrumb).toBe("旧教学资产");
|
||||
expect(result.parentFolderId).toBeNull();
|
||||
expect(result.childFolders).toEqual([{ folderId: child.id, name: "物理竞赛教研" }]);
|
||||
expect(result.projects).toEqual([{
|
||||
projectId: "project-root",
|
||||
name: "根目录项目",
|
||||
breadcrumb: "旧教学资产",
|
||||
}]);
|
||||
});
|
||||
|
||||
it("hides the system Inbox at root and presents its projects as unclassified", async () => {
|
||||
const inbox = await prisma.folder.findFirstOrThrow({
|
||||
where: { organizationId: DEFAULT_ORG_ID, kind: "SYSTEM_INBOX" },
|
||||
});
|
||||
const businessRoot = await createFolder("business-root", null, "业务目录");
|
||||
const businessInbox = await createFolder("business-inbox", businessRoot.id, "Inbox");
|
||||
await createProject("project-inbox", inbox.id, "新项目");
|
||||
|
||||
const result = await browseBindableFolder({
|
||||
prisma,
|
||||
organizationId: DEFAULT_ORG_ID,
|
||||
actorFeishuOpenId: "ou_owner",
|
||||
isOrgAdmin: true,
|
||||
folderId: null,
|
||||
});
|
||||
|
||||
expect(result.childFolders.map((folder) => folder.folderId)).not.toContain(inbox.id);
|
||||
expect(result.projects).toContainEqual({
|
||||
projectId: "project-inbox",
|
||||
name: "新项目",
|
||||
breadcrumb: "未分类",
|
||||
});
|
||||
|
||||
const businessResult = await browseBindableFolder({
|
||||
prisma,
|
||||
organizationId: DEFAULT_ORG_ID,
|
||||
actorFeishuOpenId: "ou_owner",
|
||||
isOrgAdmin: true,
|
||||
folderId: businessRoot.id,
|
||||
});
|
||||
expect(businessResult.childFolders).toContainEqual({ folderId: businessInbox.id, name: "Inbox" });
|
||||
});
|
||||
|
||||
it("shares one card page budget across folders and projects", async () => {
|
||||
const root = await createFolder("folder-page-root", null, "目录分页");
|
||||
for (let index = 0; index < 6; index += 1) {
|
||||
await createFolder(`folder-page-${index}`, root.id, `目录-${index}`);
|
||||
await createProject(`project-page-${index}`, root.id, `项目-${index}`);
|
||||
}
|
||||
|
||||
const first = await browseBindableFolder({
|
||||
prisma,
|
||||
organizationId: DEFAULT_ORG_ID,
|
||||
actorFeishuOpenId: "ou_owner",
|
||||
isOrgAdmin: true,
|
||||
folderId: root.id,
|
||||
pageSize: 8,
|
||||
});
|
||||
const second = await browseBindableFolder({
|
||||
prisma,
|
||||
organizationId: DEFAULT_ORG_ID,
|
||||
actorFeishuOpenId: "ou_owner",
|
||||
isOrgAdmin: true,
|
||||
folderId: root.id,
|
||||
page: 2,
|
||||
pageSize: 8,
|
||||
});
|
||||
|
||||
expect(first).toMatchObject({ page: 1, totalPages: 2, totalFolders: 6, totalProjects: 6 });
|
||||
expect(first.childFolders.length + first.projects.length).toBe(8);
|
||||
expect(second.childFolders.length + second.projects.length).toBe(4);
|
||||
expect([...first.projects, ...second.projects].map((project) => project.projectId).sort()).toEqual(
|
||||
Array.from({ length: 6 }, (_, index) => `project-page-${index}`),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
async function seedUser(id: string, feishuOpenId: string, role: "OWNER" | "MEMBER"): Promise<void> {
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
id,
|
||||
feishuOpenId,
|
||||
displayName: id,
|
||||
organizationMemberships: { create: { organizationId: DEFAULT_ORG_ID, role } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function createFolder(id: string, parentId: string | null, name: string) {
|
||||
return prisma.folder.create({
|
||||
data: { id, organizationId: DEFAULT_ORG_ID, parentId, name },
|
||||
});
|
||||
}
|
||||
|
||||
async function createProject(id: string, folderId: string | null, name: string, code?: string) {
|
||||
return prisma.project.create({
|
||||
data: {
|
||||
id,
|
||||
organizationId: DEFAULT_ORG_ID,
|
||||
folderId,
|
||||
...(code !== undefined ? { code } : {}),
|
||||
name,
|
||||
workspaceDir: `/tmp/${id}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function discover(query: string) {
|
||||
return discoverBindableProjects({
|
||||
prisma,
|
||||
organizationId: DEFAULT_ORG_ID,
|
||||
actorFeishuOpenId: "ou_owner",
|
||||
isOrgAdmin: true,
|
||||
query,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user