@allbluecn/web-app 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/package.json +11 -7
  2. package/src/components/knowledge/knowledge-toolbar.tsx +124 -1
  3. package/src/components/story/constants.ts +11 -1
  4. package/src/components/story/create-story-dialog.tsx +12 -6
  5. package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +114 -0
  6. package/src/components/story/detail/activity-timeline.tsx +15 -0
  7. package/src/components/story/detail/story-detail-body.tsx +1 -0
  8. package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +748 -0
  9. package/src/components/story/detail/story-detail-toolbar/categories.tsx +23 -18
  10. package/src/components/story/detail/story-detail-toolbar/children.tsx +2 -0
  11. package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +29 -21
  12. package/src/components/story/detail/story-detail-toolbar.tsx +154 -8
  13. package/src/components/story/inline-editors/story-select-dialog.tsx +19 -4
  14. package/src/components/story/relations/__tests__/attachment-manage-dialog.test.tsx +171 -0
  15. package/src/components/story/relations/__tests__/knowledge-select-dialog.test.tsx +174 -0
  16. package/src/components/story/relations/__tests__/link-manage-dialog.test.tsx +93 -0
  17. package/src/components/story/relations/attachment-manage-dialog.tsx +490 -0
  18. package/src/components/story/relations/file-preview.tsx +173 -0
  19. package/src/components/story/relations/format.ts +120 -0
  20. package/src/components/story/relations/general-groups.ts +32 -0
  21. package/src/components/story/relations/knowledge-select-dialog.tsx +403 -0
  22. package/src/components/story/relations/link-manage-dialog.tsx +191 -0
  23. package/src/components/story/types.ts +13 -0
  24. package/src/lib/changelog/sdk-versions.ts +19 -19
  25. package/src/lib/story/__tests__/video-embed.test.ts +77 -0
  26. package/src/lib/story/video-embed.ts +59 -0
  27. package/src/plugins/.gen-manifest.json +13 -0
  28. package/src/plugins/modules.gen.ts +20 -0
  29. package/src/plugins/plugins.gen.ts +20 -0
  30. package/src/plugins/ui.gen.ts +8 -0
  31. package/src/routeTree.gen.ts +42 -0
  32. package/src/routes/_nav/inspiration/route.tsx +8 -0
  33. package/src/routes/_nav/prd/$id/route.tsx +9 -0
  34. package/src/routes/_nav/prd/index.tsx +9 -0
  35. package/src/routes/_nav/prd/route.tsx +8 -0
  36. package/src/routes/_nav/projects/$projectId/settings/route.lazy.tsx +74 -3
  37. package/src/routes/_nav/projects/$projectId/settings/route.tsx +6 -2
  38. package/src/routes/_nav/tags/$id/route.tsx +8 -0
  39. package/src/routes/_nav/tags/index.tsx +8 -0
  40. package/src/routes/_nav/tags/route.tsx +8 -0
  41. package/src/routes/_nav/tags/settings/route.tsx +9 -0
  42. package/src/routes/api/attachments/$id/content.ts +60 -0
  43. package/src/routes/api/attachments/upload.ts +148 -0
  44. package/src/routes/share/prd/$prdId/route.tsx +10 -0
  45. package/src/server/serverFns/__tests__/project-kb.test.ts +147 -0
  46. package/src/server/serverFns/knowledge.ts +50 -4
  47. package/src/server/serverFns/project.ts +43 -0
  48. package/src/server/serverFns/story/__tests__/kb-scope.test.ts +56 -0
  49. package/src/server/serverFns/story/__tests__/story-assoc.test.ts +222 -0
  50. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +26 -0
  51. package/src/server/serverFns/story/index.ts +6 -4
  52. package/src/server/serverFns/story/kb-general.ts +39 -0
  53. package/src/server/serverFns/story/kb-scope.ts +52 -0
  54. package/src/server/serverFns/story/link-preview.ts +115 -0
  55. package/src/server/serverFns/story/story-assoc.ts +339 -50
  56. package/src/server/serverFns/story/story-attachments.ts +12 -43
  57. package/src/server/serverFns/story/story-crud.ts +21 -4
  58. package/src/server/serverFns/story/story-dependencies.ts +35 -3
  59. package/src/server/serverFns/story/story-links.ts +13 -1
  60. package/src/server/serverFns/story/story-tree.ts +28 -1
  61. package/src/server/serverFns/story/types.ts +37 -0
  62. package/src/styles/globals.css +25 -0
  63. package/vite.shared.ts +2 -0
  64. package/src/components/story/detail/story-detail-toolbar/attachments.tsx +0 -73
  65. package/src/components/story/detail/story-detail-toolbar/knowledge.tsx +0 -73
  66. package/src/components/story/detail/story-detail-toolbar/links.tsx +0 -73
  67. package/src/components/story/relations/attachments-panel.tsx +0 -179
  68. package/src/components/story/relations/knowledge-panel.tsx +0 -284
  69. package/src/components/story/relations/links-panel.tsx +0 -240
@@ -0,0 +1,174 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { describe, it, expect, vi, beforeEach } from "vitest";
19
+ import { render, screen, fireEvent, waitFor } from "@testing-library/react";
20
+
21
+ vi.mock("@tanstack/react-start", () => ({
22
+ useServerFn: (fn: any) => vi.fn().mockImplementation((args: any) => fn(args)),
23
+ }));
24
+
25
+ const fns = vi.hoisted(() => ({
26
+ listKbs: vi.fn(),
27
+ search: vi.fn(),
28
+ link: vi.fn(),
29
+ unlink: vi.fn(),
30
+ }));
31
+
32
+ vi.mock("@/server/serverFns/story", () => ({
33
+ listStoryAvailableKbsFn: fns.listKbs,
34
+ searchKbDocsFn: fns.search,
35
+ linkDocsFn: fns.link,
36
+ unlinkDocFn: fns.unlink,
37
+ }));
38
+
39
+ vi.mock("sonner", () => ({
40
+ toast: { success: vi.fn(), error: vi.fn(), warning: vi.fn() },
41
+ }));
42
+
43
+ import { KnowledgeSelectDialog } from "../knowledge-select-dialog";
44
+ import type { DocBrief } from "../../types";
45
+
46
+ const doc = (id: string, title: string, parentId: string | null = null): DocBrief => ({
47
+ id,
48
+ title,
49
+ parentId,
50
+ kbId: "kb1",
51
+ kbName: "团队库",
52
+ kbIcon: "notebook",
53
+ docIcon: null,
54
+ });
55
+
56
+ const LINKED: DocBrief[] = [doc("d1", "已关联页")];
57
+
58
+ function setup() {
59
+ const onUpdate = vi.fn();
60
+ render(
61
+ <KnowledgeSelectDialog
62
+ open
63
+ onOpenChange={() => {}}
64
+ storyId="s1"
65
+ linkedDocs={LINKED}
66
+ onUpdate={onUpdate}
67
+ />,
68
+ );
69
+ return { onUpdate };
70
+ }
71
+
72
+ beforeEach(() => {
73
+ vi.clearAllMocks();
74
+ fns.listKbs.mockResolvedValue([
75
+ { id: "kb1", name: "团队库", icon: "notebook", visibility: "team" },
76
+ { id: "kb2", name: "通用库", icon: "book", visibility: "general" },
77
+ ]);
78
+ fns.search.mockResolvedValue({
79
+ items: [doc("d1", "已关联页"), doc("d2", "子页", "d1"), doc("d3", "普通页")],
80
+ nextCursor: null,
81
+ });
82
+ });
83
+
84
+ describe("KnowledgeSelectDialog", () => {
85
+ it("加载可用 KB 与页面列表,已关联项默认勾选且带 badge", async () => {
86
+ setup();
87
+ // KB 名称出现在 Popover 触发器和文档列表中,用含角色断言避免歧义
88
+ await waitFor(() => expect(screen.getByText("已关联页")).toBeInTheDocument());
89
+ expect(screen.getByText("已关联")).toBeInTheDocument();
90
+ });
91
+
92
+ it("切换勾选后确认:新增走 linkDocsFn、取消走 unlinkDocFn", async () => {
93
+ fns.link.mockResolvedValue({ linked: ["d2", "d3"], skipped: [], failed: [] });
94
+ fns.unlink.mockResolvedValue({ ok: true });
95
+ const { onUpdate } = setup();
96
+ await waitFor(() => screen.getByText("普通页"));
97
+ // 勾选 d3(新增)
98
+ const checkboxes = screen.getAllByRole("checkbox");
99
+ // d3 是第三个(已关联页、子页、普通页)
100
+ fireEvent.click(checkboxes[2]);
101
+ // 取消 d1
102
+ fireEvent.click(checkboxes[0]);
103
+ fireEvent.click(screen.getByRole("button", { name: "确认" }));
104
+ await waitFor(() => expect(fns.link).toHaveBeenCalledWith({
105
+ data: { storyId: "s1", docIds: expect.arrayContaining(["d3"]) },
106
+ }));
107
+ await waitFor(() => expect(fns.unlink).toHaveBeenCalledWith({
108
+ data: { storyId: "s1", docId: "d1" },
109
+ }));
110
+ await waitFor(() => expect(onUpdate).toHaveBeenCalled());
111
+ });
112
+
113
+ it("linkDocsFn 返回 failed 时 toast warning 且不关弹窗", async () => {
114
+ fns.link.mockResolvedValue({ linked: [], skipped: [], failed: [{ docId: "d9", reason: "outOfScope" }] });
115
+ const { toast } = await import("sonner");
116
+ setup();
117
+ await waitFor(() => screen.getByText("普通页"));
118
+ const checkboxes = screen.getAllByRole("checkbox");
119
+ fireEvent.click(checkboxes[2]);
120
+ fireEvent.click(screen.getByRole("button", { name: "确认" }));
121
+ await waitFor(() => expect(toast.warning).toHaveBeenCalled());
122
+ });
123
+
124
+ it("KB 选择弹层含我的知识库与通用知识库两个分组区(五个通用组)", async () => {
125
+ setup();
126
+ await waitFor(() => screen.getByText("普通页"));
127
+ fireEvent.click(screen.getByRole("button", { name: /全部知识库/ }));
128
+ expect(screen.getByText("我的知识库")).toBeInTheDocument();
129
+ expect(screen.getByText("通用知识库")).toBeInTheDocument();
130
+ for (const label of ["法律法规", "行业合规", "词典词条", "网站收藏", "周报月报"]) {
131
+ expect(screen.getByText(label)).toBeInTheDocument();
132
+ }
133
+ });
134
+
135
+ it("选择通用知识库分组后搜索带 generalGroup 参数", async () => {
136
+ setup();
137
+ await waitFor(() => screen.getByText("普通页"));
138
+ fireEvent.click(screen.getByRole("button", { name: /全部知识库/ }));
139
+ fireEvent.click(screen.getByText("法律法规"));
140
+ await waitFor(() =>
141
+ expect(fns.search).toHaveBeenCalledWith(
142
+ expect.objectContaining({ data: expect.objectContaining({ generalGroup: "legal" }) }),
143
+ ),
144
+ );
145
+ });
146
+
147
+ it("底部全选复选框支持全选与部分选中(indeterminate)状态", async () => {
148
+ setup();
149
+ await waitFor(() => screen.getByText("普通页"));
150
+ const footerCheckbox = screen.getByRole("checkbox", { name: "全选" });
151
+ // 初始:仅 d1 已勾选(部分选中)→ indeterminate
152
+ expect(footerCheckbox.getAttribute("aria-checked")).toBe("mixed");
153
+ // 全选
154
+ fireEvent.click(footerCheckbox);
155
+ const checkboxes = screen.getAllByRole("checkbox");
156
+ for (const cb of checkboxes) expect(cb.getAttribute("aria-checked")).toBe("true");
157
+ // 再次点击取消全选
158
+ fireEvent.click(footerCheckbox);
159
+ expect(footerCheckbox.getAttribute("aria-checked")).toBe("false");
160
+ });
161
+
162
+ it("点击行任意位置切换选中(勾选框点击不重复触发)", async () => {
163
+ setup();
164
+ await waitFor(() => screen.getByText("普通页"));
165
+ // 点击行文本区域 → 选中
166
+ fireEvent.click(screen.getByText("普通页"));
167
+ const checkboxes = screen.getAllByRole("checkbox");
168
+ // d3(普通页)对应第三个行勾选框
169
+ expect(checkboxes[2].getAttribute("aria-checked")).toBe("true");
170
+ // 再次点击行 → 取消选中
171
+ fireEvent.click(screen.getByText("普通页"));
172
+ expect(checkboxes[2].getAttribute("aria-checked")).toBe("false");
173
+ });
174
+ });
@@ -0,0 +1,93 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { describe, it, expect, vi, beforeEach } from "vitest";
19
+ import { render, screen, fireEvent, waitFor } from "@testing-library/react";
20
+
21
+ vi.mock("@tanstack/react-start", () => ({
22
+ useServerFn: (fn: any) => vi.fn().mockImplementation((args: any) => fn(args)),
23
+ }));
24
+
25
+ const fns = vi.hoisted(() => ({
26
+ addLink: vi.fn(),
27
+ updateLink: vi.fn(),
28
+ removeLink: vi.fn(),
29
+ }));
30
+
31
+ vi.mock("@/server/serverFns/story", () => ({
32
+ addLinkFn: fns.addLink,
33
+ updateLinkFn: fns.updateLink,
34
+ removeLinkFn: fns.removeLink,
35
+ }));
36
+
37
+ vi.mock("sonner", () => ({
38
+ toast: { success: vi.fn(), error: vi.fn(), warning: vi.fn() },
39
+ }));
40
+
41
+ import { LinkManageDialog } from "../link-manage-dialog";
42
+ import type { StoryLinkItem } from "../../types";
43
+
44
+ const LINKS: StoryLinkItem[] = [
45
+ { id: "l1", url: "https://a.com", title: "站点A", description: null },
46
+ { id: "l2", url: "https://b.com", title: null, description: "备注" },
47
+ ];
48
+
49
+ function setup(props?: Partial<Parameters<typeof LinkManageDialog>[0]>) {
50
+ const onUpdate = vi.fn();
51
+ render(
52
+ <LinkManageDialog
53
+ open
54
+ onOpenChange={() => {}}
55
+ storyId="s1"
56
+ data={LINKS}
57
+ onUpdate={onUpdate}
58
+ initialEdit={null}
59
+ {...props}
60
+ />,
61
+ );
62
+ return { onUpdate };
63
+ }
64
+
65
+ beforeEach(() => vi.clearAllMocks());
66
+
67
+ describe("LinkManageDialog", () => {
68
+ it("不渲染已有链接列表,仅保留添加表单", () => {
69
+ setup();
70
+ expect(screen.queryByText("站点A")).not.toBeInTheDocument();
71
+ expect(screen.getByLabelText(/URL/i)).toBeInTheDocument();
72
+ });
73
+
74
+ it("填写 URL 提交调用 addLinkFn 并回调 onUpdate", async () => {
75
+ fns.addLink.mockResolvedValue({ id: "l3", url: "https://c.com", title: "C", description: null });
76
+ const { onUpdate } = setup();
77
+ fireEvent.change(screen.getByLabelText(/URL/i), { target: { value: "https://c.com" } });
78
+ fireEvent.click(screen.getByRole("button", { name: "添加" }));
79
+ await waitFor(() => expect(onUpdate).toHaveBeenCalled());
80
+ const call = fns.addLink.mock.calls[0][0];
81
+ expect(call.data.storyId).toBe("s1");
82
+ expect(call.data.url).toBe("https://c.com");
83
+ });
84
+
85
+ it("initialEdit 进入编辑态,保存调 updateLinkFn", async () => {
86
+ fns.updateLink.mockResolvedValue({});
87
+ setup({ initialEdit: LINKS[0] });
88
+ expect(screen.getByRole("button", { name: "保存" })).toBeInTheDocument();
89
+ fireEvent.click(screen.getByRole("button", { name: "保存" }));
90
+ await waitFor(() => expect(fns.updateLink).toHaveBeenCalled());
91
+ expect(fns.updateLink.mock.calls[0][0].data.linkId).toBe("l1");
92
+ });
93
+ });