From f8d75352492f3a50c33f73dc31689aa92659d03a Mon Sep 17 00:00:00 2001 From: Jesse <15653378+squarezw@user.noreply.gitee.com> Date: Tue, 15 Sep 2026 19:10:21 +0800 Subject: [PATCH 1/3] feat(skills): add creator filter helpers --- lib/skillCreatorFilter.ts | 27 ++++++++++++++ test/skillCreatorFilter.test.ts | 63 +++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 lib/skillCreatorFilter.ts create mode 100644 test/skillCreatorFilter.test.ts diff --git a/lib/skillCreatorFilter.ts b/lib/skillCreatorFilter.ts new file mode 100644 index 0000000..e1686b3 --- /dev/null +++ b/lib/skillCreatorFilter.ts @@ -0,0 +1,27 @@ +import type { Skill } from "@/types/skill"; + +export type CreatorFilterValue = number | null; + +export interface SkillCreator { + userId: number; + name: string; +} + +export function getSkillCreators(skills: Skill[]): SkillCreator[] { + const creators = new Map(); + + for (const skill of skills) { + if (skill.user_id == null || !skill.author || creators.has(skill.user_id)) continue; + creators.set(skill.user_id, { userId: skill.user_id, name: skill.author }); + } + + return [...creators.values()]; +} + +export function filterSkillsByCreator( + skills: Skill[], + creatorId: CreatorFilterValue +): Skill[] { + if (creatorId == null) return skills; + return skills.filter((skill) => skill.user_id === creatorId); +} diff --git a/test/skillCreatorFilter.test.ts b/test/skillCreatorFilter.test.ts new file mode 100644 index 0000000..5c6b425 --- /dev/null +++ b/test/skillCreatorFilter.test.ts @@ -0,0 +1,63 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { filterSkillsByCreator, getSkillCreators } from "../lib/skillCreatorFilter.ts"; +import type { Skill } from "../types/skill.ts"; + +function skill(overrides: Partial): Skill { + return { + id: 1, + name: "sample-skill", + display_name: "Sample skill", + description: "", + content: "", + published_content: null, + requires: null, + visibility: "private", + is_active: true, + created_at: "2026-09-15T00:00:00Z", + updated_at: "2026-09-15T00:00:00Z", + ...overrides, + }; +} + +test("getSkillCreators: retains first-seen order and de-duplicates by user ID", () => { + const creators = getSkillCreators([ + skill({ id: 1, user_id: 11, author: "Alex" }), + skill({ id: 2, user_id: 22, author: "Alex" }), + skill({ id: 3, user_id: 11, author: "Alex renamed" }), + ]); + + assert.deepEqual(creators, [ + { userId: 11, name: "Alex" }, + { userId: 22, name: "Alex" }, + ]); +}); + +test("getSkillCreators: skips skills without a stable creator ID or display name", () => { + const creators = getSkillCreators([ + skill({ id: 1, user_id: null, author: "No ID" }), + skill({ id: 2, user_id: 12, author: null }), + skill({ id: 3, user_id: 13, author: "Jordan" }), + ]); + + assert.deepEqual(creators, [{ userId: 13, name: "Jordan" }]); +}); + +test("filterSkillsByCreator: matches only the selected stable user ID", () => { + const skills = [ + skill({ id: 1, user_id: 11, author: "Alex" }), + skill({ id: 2, user_id: 22, author: "Alex" }), + skill({ id: 3, user_id: 11, author: "Alex" }), + ]; + + assert.deepEqual( + filterSkillsByCreator(skills, 11).map(({ id }) => id), + [1, 3] + ); +}); + +test("filterSkillsByCreator: clearing the selection returns the existing list", () => { + const skills = [skill({ id: 1, user_id: 11 }), skill({ id: 2, user_id: 22 })]; + + assert.strictEqual(filterSkillsByCreator(skills, null), skills); +}); From 53144424c42c64f5312e4357178c26a3a35d7fd9 Mon Sep 17 00:00:00 2001 From: Jesse <15653378+squarezw@user.noreply.gitee.com> Date: Tue, 15 Sep 2026 19:12:13 +0800 Subject: [PATCH 2/3] feat(skills): filter skills by creator --- app/skills/page.tsx | 290 ++++++++++++++++++++++--------------- messages/en/skills.json | 3 + messages/zh-CN/skills.json | 3 + 3 files changed, 177 insertions(+), 119 deletions(-) diff --git a/app/skills/page.tsx b/app/skills/page.tsx index f0f604d..e943c1a 100644 --- a/app/skills/page.tsx +++ b/app/skills/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { Badge } from "@/components/ui/badge"; @@ -32,12 +32,9 @@ import { Loader2, Plus, Search, Sparkles, Terminal, Upload } from "lucide-react" import { useDebounce } from "use-debounce"; import { useCurrentUser } from "@/hooks/useCurrentUser"; import { useSkills } from "@/hooks/useSkills"; -import { - hasUnpublishedChanges, - resolveReviewStatus, - reviewStatusBadge, -} from "@/lib/reviewStatus"; +import { hasUnpublishedChanges, resolveReviewStatus, reviewStatusBadge } from "@/lib/reviewStatus"; import { canEditSkill } from "@/lib/skillPermissions"; +import { filterSkillsByCreator, getSkillCreators } from "@/lib/skillCreatorFilter"; import { checkDeptAdmin, checkSuperAdmin, checkTenantAdmin } from "@/lib/clientPermissions"; import type { Skill } from "@/types/skill"; @@ -59,6 +56,7 @@ export default function SkillsPage() { const [search, setSearch] = useState(""); const [debouncedSearch] = useDebounce(search, 300); + const [creatorId, setCreatorId] = useState(null); // 组织筛选。null = 这个角色不显示筛选器(普通用户)。 const [orgFilter, setOrgFilter] = useState(null); @@ -84,6 +82,11 @@ export default function SkillsPage() { tenantId: orgFilter?.tenantId ?? null, deptId: orgFilter?.deptId ?? null, }); + const creators = useMemo(() => getSkillCreators(skills), [skills]); + const filteredSkills = useMemo( + () => filterSkillsByCreator(skills, creatorId), + [skills, creatorId] + ); // 删除被引用时(409)弹引用应用清单 const [referencedApps, setReferencedApps] = useState(null); @@ -163,129 +166,178 @@ export default function SkillsPage() { ) : ( - - - - {t("displayName")} - {t("description")} - {t("author")} - {t("visibility")} - {t("columnOwner")} - {tc("status")} - {tc("actions")} - - - - {skills.map((skill) => { - const status = resolveReviewStatus(skill.status, skill.published_content); - const badge = reviewStatusBadge(status); - const unpublishedChanges = hasUnpublishedChanges( - skill.content, - skill.published_content - ); - const canEdit = canEditSkill(skill, user, isSuperAdmin, isTenantAdmin); - return ( - router.push(`/skills/${skill.id}`) : undefined} + <> +
+ {t("filterCreator")} +
+
+ + {creators.map((creator) => ( + + ))} +
+
+
+ + {filteredSkills.length === 0 ? ( +
+

{t("noSkillsForCreator")}

+ +
+ ) : ( +
+ + + {t("displayName")} + {t("description")} + {t("author")} + {t("visibility")} + {t("columnOwner")} + {tc("status")} + {tc("actions")} + + + + {filteredSkills.map((skill) => { + const status = resolveReviewStatus(skill.status, skill.published_content); + const badge = reviewStatusBadge(status); + const unpublishedChanges = hasUnpublishedChanges( + skill.content, + skill.published_content + ); + const canEdit = canEditSkill(skill, user, isSuperAdmin, isTenantAdmin); + return ( + router.push(`/skills/${skill.id}`) : undefined} + > + +
+ {/* 不换行:加了徽标后这一格更窄,中文显示名会被压成一列一个字 */} + {skill.display_name} + {/* 会在沙箱里跑代码的 skill:图标而非徽标,这一列还要放 「平台维护」徽标,两个方块并排会把显示名挤掉 */} - {skill.is_executable && ( - - {t("executableHint")} - - )} - {/* 内置技能没有编辑/删除按钮,光藏起来用户不知道为什么。 + {skill.is_executable && ( + + {t("executableHint")} + + )} + {/* 内置技能没有编辑/删除按钮,光藏起来用户不知道为什么。 徽标 + hover 说明,比一个凭空消失的按钮好懂。 */} - {skill.is_managed && ( + {skill.is_managed && ( + + {t("managed")} + + )} +
+
+ + + {skill.description} + + + + {/* 作者账号注销后 author 为空:显示占位符而不是空白单元格, + 空白会让人以为是渲染坏了 */} + {skill.author || —} + + - {t("managed")} + {t(`visibility_${skill.visibility}`)} - )} - - - - - {skill.description} - - - - {/* 作者账号注销后 author 为空:显示占位符而不是空白单元格, - 空白会让人以为是渲染坏了 */} - {skill.author || —} - - - - {t(`visibility_${skill.visibility}`)} - - - {/* 归属:这个 Skill 挂在哪个租户/部门。筛选了却看不出每条挂在哪, + + {/* 归属:这个 Skill 挂在哪个租户/部门。筛选了却看不出每条挂在哪, 用户没法判断筛选到底生效没有,也没法判断该改谁的归属。 部门优先显示 —— 它是更细的那一层,也是复用范围的起点。 */} - - {skill.owner_dept_name || skill.owner_tenant_name ? ( - - {skill.owner_dept_name || skill.owner_tenant_name} - - ) : ( - {t("ownerUnassigned")} - )} - - -
- - {t(badge.labelKey)} - - {unpublishedChanges && ( - - {t("statusUnpublishedChanges")} - - )} - {!skill.is_active && {t("inactive")}} -
-
- e.stopPropagation()}> -
- {/* 没有写权限的人:编辑/删除藏掉,**查看也一并藏掉**——跨租户的 + + {skill.owner_dept_name || skill.owner_tenant_name ? ( + + {skill.owner_dept_name || skill.owner_tenant_name} + + ) : ( + {t("ownerUnassigned")} + )} + + +
+ + {t(badge.labelKey)} + + {unpublishedChanges && ( + + {t("statusUnpublishedChanges")} + + )} + {!skill.is_active && ( + {t("inactive")} + )} +
+
+ e.stopPropagation()}> +
+ {/* 没有写权限的人:编辑/删除藏掉,**查看也一并藏掉**——跨租户的 公开技能在列表里只展示一行,既不能改也不能点进去看详情。 操作列空白,而不是放一个「查看」按钮骗人能进去。 */} - {canEdit && ( - <> - - - - )} -
-
- - ); - })} - -
+ {canEdit && ( + <> + + + + )} + + + + ); + })} + + + )} + )} diff --git a/messages/en/skills.json b/messages/en/skills.json index a63db40..90f273f 100644 --- a/messages/en/skills.json +++ b/messages/en/skills.json @@ -5,6 +5,9 @@ "editSkill": "Edit Skill", "noSkills": "No skills", "noSkillsDesc": "Create your first reusable skill", + "filterCreator": "Creator", + "allCreators": "All creators", + "noSkillsForCreator": "No skills from this creator in the current results", "noPermission": "No permission to access skill management", "skillNotFound": "Skill not found", "backToList": "Back to Skills", diff --git a/messages/zh-CN/skills.json b/messages/zh-CN/skills.json index 4ed6acf..4ec7c65 100644 --- a/messages/zh-CN/skills.json +++ b/messages/zh-CN/skills.json @@ -5,6 +5,9 @@ "editSkill": "编辑 Skill", "noSkills": "暂无 Skill", "noSkillsDesc": "创建你的第一个可复用 Skill", + "filterCreator": "创建者", + "allCreators": "全部创建者", + "noSkillsForCreator": "当前范围内没有该创建者的 Skill", "noPermission": "没有权限访问 Skill 管理", "skillNotFound": "Skill 不存在", "backToList": "返回 Skill 列表", From da5e4efe69302daaf400c2a2c73bc5c6347d2666 Mon Sep 17 00:00:00 2001 From: Jesse <15653378+squarezw@user.noreply.gitee.com> Date: Tue, 15 Sep 2026 19:12:19 +0800 Subject: [PATCH 3/3] style(skills): format creator filter helper --- lib/skillCreatorFilter.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/skillCreatorFilter.ts b/lib/skillCreatorFilter.ts index e1686b3..9bdb748 100644 --- a/lib/skillCreatorFilter.ts +++ b/lib/skillCreatorFilter.ts @@ -18,10 +18,7 @@ export function getSkillCreators(skills: Skill[]): SkillCreator[] { return [...creators.values()]; } -export function filterSkillsByCreator( - skills: Skill[], - creatorId: CreatorFilterValue -): Skill[] { +export function filterSkillsByCreator(skills: Skill[], creatorId: CreatorFilterValue): Skill[] { if (creatorId == null) return skills; return skills.filter((skill) => skill.user_id === creatorId); }