forked from EduCraft/curriculum-project-hub
feat: let Feishu onboarding create projects in folders
This commit is contained in:
@@ -4,31 +4,42 @@ export interface OnboardingProjectOption {
|
||||
readonly folderName?: string | undefined;
|
||||
}
|
||||
|
||||
export interface OnboardingFolderOption {
|
||||
readonly folderId: string;
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
export interface ProjectOnboardingActionValue {
|
||||
readonly action: "create_project_from_chat" | "bind_project";
|
||||
readonly organization_id: string;
|
||||
readonly project_id?: string | undefined;
|
||||
readonly folder_id?: string | undefined;
|
||||
}
|
||||
|
||||
export function buildUnboundChatOnboardingCard(params: {
|
||||
readonly organizationId: string;
|
||||
readonly organizationName: string;
|
||||
readonly folders: readonly OnboardingFolderOption[];
|
||||
readonly projects: readonly OnboardingProjectOption[];
|
||||
readonly canCreateProject: boolean;
|
||||
}): Record<string, unknown> {
|
||||
const actions: unknown[] = [];
|
||||
if (params.canCreateProject) {
|
||||
actions.push({
|
||||
tag: "button",
|
||||
text: { tag: "plain_text", content: "新建并绑定项目" },
|
||||
type: "primary",
|
||||
value: {
|
||||
project_onboarding: {
|
||||
action: "create_project_from_chat",
|
||||
organization_id: params.organizationId,
|
||||
const folders = params.folders.length === 0 ? [{ folderId: undefined, name: "默认位置" }] : params.folders.slice(0, 3);
|
||||
for (const folder of folders) {
|
||||
actions.push({
|
||||
tag: "button",
|
||||
text: { tag: "plain_text", content: `新建到 ${buttonLabel(folder.name, 14)}` },
|
||||
type: "primary",
|
||||
value: {
|
||||
project_onboarding: {
|
||||
action: "create_project_from_chat",
|
||||
organization_id: params.organizationId,
|
||||
...(folder.folderId !== undefined ? { folder_id: folder.folderId } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const project of params.projects.slice(0, 5)) {
|
||||
@@ -53,7 +64,7 @@ export function buildUnboundChatOnboardingCard(params: {
|
||||
`这个飞书群还没有绑定项目。`,
|
||||
``,
|
||||
`组织: **${escapeMarkdown(params.organizationName)}**`,
|
||||
`可以新建一个项目并绑定到本群,或绑定你已经有管理权限的未绑定项目。`,
|
||||
`可以选择 folder 新建项目并绑定到本群,或绑定你已经有管理权限的未绑定项目。`,
|
||||
].join("\n"),
|
||||
},
|
||||
];
|
||||
@@ -101,16 +112,21 @@ export function projectOnboardingActionFromValue(value: unknown): ProjectOnboard
|
||||
const rawAction = (action as { action?: unknown }).action;
|
||||
const organizationId = (action as { organization_id?: unknown }).organization_id;
|
||||
const projectId = (action as { project_id?: unknown }).project_id;
|
||||
const folderId = (action as { folder_id?: unknown }).folder_id;
|
||||
if ((rawAction !== "create_project_from_chat" && rawAction !== "bind_project") || typeof organizationId !== "string" || organizationId === "") {
|
||||
return null;
|
||||
}
|
||||
if (rawAction === "bind_project" && (typeof projectId !== "string" || projectId === "")) {
|
||||
return null;
|
||||
}
|
||||
if (folderId !== undefined && (typeof folderId !== "string" || folderId === "")) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
action: rawAction,
|
||||
organization_id: organizationId,
|
||||
...(typeof projectId === "string" && projectId !== "" ? { project_id: projectId } : {}),
|
||||
...(typeof folderId === "string" && folderId !== "" ? { folder_id: folderId } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -126,7 +142,11 @@ function unwrapValue(value: unknown): unknown {
|
||||
function buttonProjectLabel(project: OnboardingProjectOption): string {
|
||||
const folderPrefix = project.folderName === undefined ? "" : `${project.folderName} / `;
|
||||
const label = `${folderPrefix}${project.name}`;
|
||||
return label.length <= 24 ? label : `${label.slice(0, 21)}...`;
|
||||
return buttonLabel(label, 24);
|
||||
}
|
||||
|
||||
function buttonLabel(label: string, maxLength: number): string {
|
||||
return label.length <= maxLength ? label : `${label.slice(0, maxLength - 3)}...`;
|
||||
}
|
||||
|
||||
function escapeMarkdown(value: string): string {
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
buildProjectOnboardingResolvedCard,
|
||||
buildUnboundChatOnboardingCard,
|
||||
projectOnboardingActionFromValue,
|
||||
type OnboardingFolderOption,
|
||||
type OnboardingProjectOption,
|
||||
type ProjectOnboardingActionValue,
|
||||
} from "./projectOnboardingCard.js";
|
||||
@@ -575,6 +576,7 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
||||
chatId,
|
||||
name,
|
||||
workspaceRoot: deps.projectWorkspaceRoot,
|
||||
folderId: action.folder_id,
|
||||
});
|
||||
await resolveProjectOnboardingCard(rt, messageId, {
|
||||
title: "已创建并绑定项目",
|
||||
@@ -860,6 +862,7 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
||||
actorFeishuOpenId: senderOpenId,
|
||||
isOrgAdmin: isOrgAdminRole(organization.role),
|
||||
});
|
||||
const folders = await listCreatableRootFolders(organization.organizationId);
|
||||
|
||||
await sendCard(
|
||||
rt,
|
||||
@@ -867,6 +870,7 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
||||
buildUnboundChatOnboardingCard({
|
||||
organizationId: organization.organizationId,
|
||||
organizationName: organization.organizationName,
|
||||
folders,
|
||||
projects,
|
||||
canCreateProject,
|
||||
}),
|
||||
@@ -956,6 +960,16 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
||||
return allowed;
|
||||
}
|
||||
|
||||
async function listCreatableRootFolders(organizationId: string): Promise<readonly OnboardingFolderOption[]> {
|
||||
const folders = await deps.prisma.folder.findMany({
|
||||
where: { organizationId, parentId: null, archivedAt: null },
|
||||
select: { id: true, name: true },
|
||||
orderBy: [{ sortKey: "asc" }, { name: "asc" }],
|
||||
take: 3,
|
||||
});
|
||||
return folders.map((folder) => ({ folderId: folder.id, name: folder.name }));
|
||||
}
|
||||
|
||||
return Object.assign(onMessage, { onCardAction, approvalManager });
|
||||
}
|
||||
|
||||
|
||||
@@ -168,12 +168,15 @@ describe("trigger full lifecycle (integration)", () => {
|
||||
expect(rt.sentCards).toHaveLength(1);
|
||||
expect(rt.sentTexts.at(-1)).toContain("这个飞书群还没有绑定项目");
|
||||
const values = cardActionValues(rt.sentCards[0]);
|
||||
expect(values).toContainEqual({
|
||||
project_onboarding: {
|
||||
expect(values).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
project_onboarding: expect.objectContaining({
|
||||
action: "create_project_from_chat",
|
||||
organization_id: DEFAULT_ORG_ID,
|
||||
},
|
||||
});
|
||||
folder_id: expect.any(String),
|
||||
}),
|
||||
}),
|
||||
]));
|
||||
expect(runAgentCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user