@allbluecn/web-app 0.10.3 → 0.11.0
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.
- package/package.json +21 -11
- package/public/favicon.ico +0 -0
- package/public/logo/allblue-logo-app-icon.svg +7 -0
- package/public/logo/allblue-logo-dark-square.svg +4 -0
- package/public/logo/allblue-logo-dark.svg +4 -0
- package/public/logo/allblue-logo-light-square.svg +4 -0
- package/public/logo/allblue-logo-light.svg +4 -0
- package/src/__tests__/bench/baselines/format-timestamp.json +44 -0
- package/src/__tests__/bench/core-hot-path.bench.ts +71 -0
- package/src/__tests__/integration/db-crud.integration.test.ts +79 -0
- package/src/__tests__/rejects-tothrow-regression.test.ts +31 -0
- package/src/__tests__/security/ai/prompt-injection.test.ts +105 -0
- package/src/__tests__/security/ssrf-url-whitelist.test.ts +54 -0
- package/src/__tests__/security/xss-sanitize.test.ts +99 -0
- package/src/__tests__/setup.ts +9 -0
- package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-dark-baseline-1-chromium-darwin.png +0 -0
- package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-light-baseline-1-chromium-darwin.png +0 -0
- package/src/__tests__/visual/__screenshots__/user-avatar.vrt.test.tsx/user-avatar-initial-fallback-baseline-1-chromium-darwin.png +0 -0
- package/src/__tests__/visual/theme-surface.vrt.test.tsx +54 -0
- package/src/__tests__/visual/user-avatar.vrt.test.tsx +32 -0
- package/src/components/error/global-error-boundary.tsx +2 -2
- package/src/components/knowledge/general-groups.ts +15 -0
- package/src/components/knowledge/kb-filter-popover.tsx +143 -0
- package/src/components/knowledge-public/legal-compliance-editor.tsx +4 -2
- package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +9 -6
- package/src/components/layout/sidebar-03/nav-knowledge.tsx +19 -17
- package/src/components/layout/sidebar-03/nav-workspace.tsx +20 -22
- package/src/components/shared-kit/avatars/notion-avatar.tsx +2 -1
- package/src/components/shared-kit/icons/__tests__/lucide-curated-icons.test.ts +2 -2
- package/src/components/shared-kit/icons/__tests__/lucide-icon-picker.test.tsx +3 -2
- package/src/components/shared-kit/icons/lucide-icon-picker/curated-icons.ts +5 -10
- package/src/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name.tsx +3 -3
- package/src/components/shared-kit/theme/theme-surface.tsx +52 -0
- package/src/components/story/relations/general-groups.ts +1 -15
- package/src/components/story/relations/knowledge-select-dialog.tsx +13 -87
- package/src/components/story/story-list-view.tsx +7 -1
- package/src/components/tiptap/node/table-node/ui/table-move-row-column-button/use-table-move-row-column.ts +0 -2
- package/src/components/workspace/__tests__/activity/workspace-activity-card.test.tsx +36 -0
- package/src/components/workspace/__tests__/contribution/workspace-contribution-card.test.tsx +17 -0
- package/src/components/workspace/__tests__/dialogs/create-workspace-dialog.test.tsx +63 -0
- package/src/components/workspace/__tests__/dialogs/delete-workspace-options.test.tsx +89 -0
- package/src/components/workspace/__tests__/dialogs/edit-workspace-dialog.test.tsx +68 -0
- package/src/components/workspace/__tests__/header/workspace-header.test.tsx +40 -0
- package/src/components/workspace/__tests__/kb/workspace-kb-card.test.tsx +154 -0
- package/src/components/workspace/__tests__/members/workspace-member-item.test.tsx +24 -0
- package/src/components/workspace/__tests__/members/workspace-members-manager.test.tsx +51 -0
- package/src/components/workspace/__tests__/projects/workspace-project-list.test.tsx +46 -0
- package/src/components/workspace/{create-workspace-dialog.tsx → dialogs/create-workspace-dialog.tsx} +1 -1
- package/src/components/workspace/{edit-workspace-dialog.tsx → dialogs/edit-workspace-dialog.tsx} +1 -1
- package/src/components/workspace/kb/workspace-kb-card.tsx +594 -0
- package/src/components/workspace/kb/workspace-kb-manage-dialog.tsx +174 -0
- package/src/components/workspace/kb/workspace-quick-doc-dialog.tsx +431 -0
- package/src/components/workspace/{workspace-members-manager.tsx → members/workspace-members-manager.tsx} +78 -40
- package/src/lib/__tests__/msw-sample.test.ts +68 -0
- package/src/lib/__tests__/pgvector.test.ts +2 -2
- package/src/lib/changelog/sdk-versions.ts +21 -21
- package/src/lib/collections/knowledge/knowledge-bases-collection.ts +3 -1
- package/src/lib/collections/reference/workspaces-collection.ts +3 -1
- package/src/lib/fetch-json.ts +23 -0
- package/src/{components/workspace/workspace-kb-card.tsx → lib/i18n/resolver.ts} +6 -12
- package/src/lib/sanitize.ts +14 -1
- package/src/lib/session.ts +9 -1
- package/src/lib/vector-store/__tests__/factory.test.ts +1 -1
- package/src/lib/vector-store/__tests__/sqlite-vec-store.test.ts +2 -2
- package/src/plugins/core/tags/__tests__/tags.test.ts +10 -10
- package/src/routes/__tests__/-edition-routing.test.tsx +4 -4
- package/src/routes/__tests__/route-tree-structure.test.ts +65 -0
- package/src/routes/_nav/knowledge/compliance/index.lazy.tsx +5 -2
- package/src/routes/_nav/knowledge/index.lazy.tsx +1 -1
- package/src/routes/_nav/knowledge/laws/index.lazy.tsx +5 -2
- package/src/routes/_nav/route.tsx +21 -0
- package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +13 -11
- package/src/routes/_nav/workspaces/$workspaceId/route.tsx +4 -2
- package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +1 -1
- package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
- package/src/server/__tests__/agent-crud.test.ts +4 -4
- package/src/server/__tests__/chat-conversations.test.ts +1 -3
- package/src/server/__tests__/fetch-guard.test.ts +99 -0
- package/src/server/__tests__/permissions.test.ts +1 -3
- package/src/server/__tests__/resources.team-seats.test.ts +2 -0
- package/src/server/__tests__/skill-crud.test.ts +3 -3
- package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +95 -0
- package/src/server/ai-gateway/__tests__/kb-context-guard.test.ts +136 -0
- package/src/server/ai-gateway/__tests__/knowledge-search-fence.test.ts +240 -0
- package/src/server/ai-gateway/__tests__/llm-fence.test.ts +125 -0
- package/src/server/ai-gateway/__tests__/run-store.test.ts +52 -0
- package/src/server/ai-gateway/ai-output-guard.ts +77 -0
- package/src/server/ai-gateway/index.ts +5 -0
- package/src/server/ai-gateway/kb-context-guard.ts +46 -0
- package/src/server/ai-gateway/llm-fence.ts +82 -0
- package/src/server/ai-gateway/persistence/run-store.ts +19 -0
- package/src/server/ai-gateway/story-prompt.ts +4 -2
- package/src/server/ai-gateway/tools/definitions.ts +12 -6
- package/src/server/ai-gateway/tools/server.ts +97 -54
- package/src/server/builtin-project.ts +21 -0
- package/src/server/email/sender.ts +8 -2
- package/src/server/fetch-guard.ts +170 -0
- package/src/server/middlewares/security-headers.ts +113 -0
- package/src/server/serverFns/__tests__/billing.test.ts +263 -0
- package/src/server/serverFns/__tests__/link-preview.test.ts +239 -0
- package/src/server/serverFns/__tests__/project-kb.test.ts +3 -3
- package/src/server/serverFns/__tests__/project-team-assign.test.ts +1 -1
- package/src/server/serverFns/__tests__/story-comments.test.ts +265 -0
- package/src/server/serverFns/__tests__/story-crud-archive-detail.test.ts +266 -0
- package/src/server/serverFns/__tests__/story-crud-labels-users.test.ts +227 -0
- package/src/server/serverFns/__tests__/story-crud.test.ts +526 -0
- package/src/server/serverFns/__tests__/story-links.test.ts +262 -0
- package/src/server/serverFns/__tests__/story-list-query.test.ts +259 -0
- package/src/server/serverFns/__tests__/story-reactions.test.ts +230 -0
- package/src/server/serverFns/__tests__/story-tree.test.ts +317 -0
- package/src/server/serverFns/__tests__/story-types.test.ts +294 -0
- package/src/server/serverFns/__tests__/team-accept.test.ts +6 -6
- package/src/server/serverFns/__tests__/team-invitation.test.ts +3 -3
- package/src/server/serverFns/__tests__/team-join-request.test.ts +8 -8
- package/src/server/serverFns/__tests__/update-seats.test.ts +4 -4
- package/src/server/serverFns/__tests__/workspace-kb-idor.test.ts +160 -0
- package/src/server/serverFns/__tests__/workspace-kb-utils.test.ts +54 -0
- package/src/server/serverFns/__tests__/workspace-kb.test.ts +348 -0
- package/src/server/serverFns/__tests__/workspace.test.ts +28 -8
- package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +3 -5
- package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +1 -1
- package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +2 -2
- package/src/server/serverFns/kb-doc-search.ts +111 -0
- package/src/server/serverFns/knowledge-public/compliance/create.ts +2 -1
- package/src/server/serverFns/knowledge-public/legal/create.ts +2 -1
- package/src/server/serverFns/story/__tests__/bulk.test.ts +1 -1
- package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +2 -6
- package/src/server/serverFns/story/__tests__/story-assoc.test.ts +2 -4
- package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +2 -2
- package/src/server/serverFns/story/__tests__/story-groups.test.ts +2 -2
- package/src/server/serverFns/story/kb-general-consts.ts +39 -0
- package/src/server/serverFns/story/kb-general.ts +30 -17
- package/src/server/serverFns/story/story-assoc.ts +13 -103
- package/src/server/serverFns/story/story-crud.ts +2 -5
- package/src/server/serverFns/story/story-groups.ts +4 -13
- package/src/server/serverFns/team.ts +1 -1
- package/src/server/serverFns/workspace-kb-utils.ts +138 -0
- package/src/server/serverFns/workspace-kb.ts +403 -0
- package/src/server/serverFns/workspace.ts +4 -1
- package/src/shared/ai-cli/__tests__/adapter.test.ts +67 -0
- package/src/shared/ai-cli/__tests__/cli-detector.test.ts +99 -0
- package/src/shared/ai-cli/adapter.ts +2 -2
- package/src/shared/ai-cli/cli-detector.ts +18 -9
- package/src/start.ts +2 -1
- package/src/styles/globals.css +114 -0
- package/src/types/jest-dom-vitest.d.ts +8 -7
- package/vite.shared.ts +18 -1
- package/public/file.svg +0 -1
- package/public/globe.svg +0 -1
- package/public/window.svg +0 -1
- package/src/lib/__tests__/cli-detector.test.ts +0 -80
- /package/src/components/workspace/{workspace-activity-card.tsx → activity/workspace-activity-card.tsx} +0 -0
- /package/src/components/workspace/{workspace-contribution-card.tsx → contribution/workspace-contribution-card.tsx} +0 -0
- /package/src/components/workspace/{delete-workspace-options.tsx → dialogs/delete-workspace-options.tsx} +0 -0
- /package/src/components/workspace/{workspace-header.tsx → header/workspace-header.tsx} +0 -0
- /package/src/components/workspace/{workspace-member-item.tsx → members/workspace-member-item.tsx} +0 -0
- /package/src/components/workspace/{workspace-project-list.tsx → projects/workspace-project-list.tsx} +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { render } from "@testing-library/react";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { DeleteWorkspaceOptions } from "../../dialogs/delete-workspace-options";
|
|
7
|
+
|
|
8
|
+
vi.mock("@/components/ui/field", () => ({
|
|
9
|
+
Field: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
10
|
+
FieldContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
11
|
+
FieldDescription: ({ children }: { children: ReactNode }) => (
|
|
12
|
+
<p>{children}</p>
|
|
13
|
+
),
|
|
14
|
+
FieldLabel: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
15
|
+
FieldTitle: ({ children }: { children: ReactNode }) => <span>{children}</span>,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("@/components/ui/radio-group", () => ({
|
|
19
|
+
RadioGroup: ({ children }: { children: ReactNode }) => (
|
|
20
|
+
<div data-radix-radio-group>{children}</div>
|
|
21
|
+
),
|
|
22
|
+
RadioGroupItem: () => <input type="radio" />,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("@/components/ui/select", () => ({
|
|
26
|
+
Select: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
27
|
+
SelectContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
28
|
+
SelectItem: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
29
|
+
SelectTrigger: ({ children }: { children: ReactNode }) => (
|
|
30
|
+
<div data-radix-select-trigger>{children}</div>
|
|
31
|
+
),
|
|
32
|
+
SelectValue: () => <span />,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.mock("@/components/ui/separator", () => ({
|
|
36
|
+
Separator: () => <hr />,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
describe("DeleteWorkspaceOptions", () => {
|
|
40
|
+
const baseProps = {
|
|
41
|
+
projectCount: 2,
|
|
42
|
+
deleteMode: "transfer" as const,
|
|
43
|
+
onDeleteModeChange: () => {},
|
|
44
|
+
storyMode: "transfer" as const,
|
|
45
|
+
onStoryModeChange: () => {},
|
|
46
|
+
knowledgeBaseMode: "delete" as const,
|
|
47
|
+
onKnowledgeBaseModeChange: () => {},
|
|
48
|
+
targetWorkspaceId: "ws-2",
|
|
49
|
+
onTargetWorkspaceIdChange: () => {},
|
|
50
|
+
workspaceOptions: [
|
|
51
|
+
{ id: "ws-2", name: "其他工作区" },
|
|
52
|
+
{ id: "ws-3", name: "另一个工作区" },
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
it("转移模式下渲染两组处理选项并显示目标工作区下拉", () => {
|
|
57
|
+
const { container } = render(<DeleteWorkspaceOptions {...baseProps} />);
|
|
58
|
+
|
|
59
|
+
expect(
|
|
60
|
+
container.querySelectorAll("[data-radix-radio-group]"),
|
|
61
|
+
).toHaveLength(2);
|
|
62
|
+
expect(container.querySelector("[data-radix-select-trigger]")).not.toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("清空模式下渲染三组处理选项且无目标工作区下拉", () => {
|
|
66
|
+
const { container } = render(
|
|
67
|
+
<DeleteWorkspaceOptions
|
|
68
|
+
{...baseProps}
|
|
69
|
+
deleteMode="clear"
|
|
70
|
+
storyMode="clear"
|
|
71
|
+
/>,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(
|
|
75
|
+
container.querySelectorAll("[data-radix-radio-group]"),
|
|
76
|
+
).toHaveLength(3);
|
|
77
|
+
expect(container.querySelector("[data-radix-select-trigger]")).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("无项目时仅渲染知识库处理选项", () => {
|
|
81
|
+
const { container } = render(
|
|
82
|
+
<DeleteWorkspaceOptions {...baseProps} projectCount={0} />,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(
|
|
86
|
+
container.querySelectorAll("[data-radix-radio-group]"),
|
|
87
|
+
).toHaveLength(1);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { EditWorkspaceDialog } from "../../dialogs/edit-workspace-dialog";
|
|
4
|
+
|
|
5
|
+
vi.mock("@/components/ui/button", () => ({
|
|
6
|
+
Button: ({ children, ...props }: any) => <button {...props}>{children}</button>,
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock("@/components/ui/input", () => ({
|
|
10
|
+
Input: (props: any) => <input {...props} />,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("@/components/ui/dialog", () => ({
|
|
14
|
+
Dialog: ({ children }: any) => <div>{children}</div>,
|
|
15
|
+
DialogContent: ({ children }: any) => <div>{children}</div>,
|
|
16
|
+
DialogFooter: ({ children }: any) => <div>{children}</div>,
|
|
17
|
+
DialogHeader: ({ children }: any) => <div>{children}</div>,
|
|
18
|
+
DialogTitle: ({ children }: any) => <h2>{children}</h2>,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("@/components/shared-kit/icons/lucide-icon-picker", () => ({
|
|
22
|
+
LucideIconPicker: () => <div data-testid="icon-picker" />,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("@tanstack/react-router", () => ({
|
|
26
|
+
useRouter: () => ({ navigate: vi.fn() }),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock("@tanstack/react-form", () => {
|
|
30
|
+
const makeField = (value: string) => ({
|
|
31
|
+
value,
|
|
32
|
+
handleChange: vi.fn(),
|
|
33
|
+
handleBlur: vi.fn(),
|
|
34
|
+
meta: { errors: [] },
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
useForm: () => {
|
|
38
|
+
const fields = {
|
|
39
|
+
name: makeField("测试"),
|
|
40
|
+
icon: makeField("folder"),
|
|
41
|
+
description: makeField(""),
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
handleSubmit: (e: any) => e.preventDefault(),
|
|
45
|
+
setFieldValue: () => {},
|
|
46
|
+
state: { values: { name: "测试", icon: "folder", description: "" } },
|
|
47
|
+
fields,
|
|
48
|
+
Field: ({ name, children }: { name: "name" | "icon" | "description"; children: unknown }) =>
|
|
49
|
+
typeof children === "function"
|
|
50
|
+
? (children as (field: unknown) => unknown)(fields[name])
|
|
51
|
+
: null,
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("EditWorkspaceDialog", () => {
|
|
58
|
+
it("打开时渲染标题", () => {
|
|
59
|
+
render(
|
|
60
|
+
<EditWorkspaceDialog
|
|
61
|
+
open={true}
|
|
62
|
+
onOpenChange={() => {}}
|
|
63
|
+
workspace={{ id: "ws1", name: "测试", icon: "folder", description: null }}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
expect(screen.getByRole("heading")).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { WorkspaceHeader } from "../../header/workspace-header";
|
|
4
|
+
|
|
5
|
+
vi.mock("@/components/ui/button", () => ({
|
|
6
|
+
Button: ({ children, ...props }: any) => <button {...props}>{children}</button>,
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock("@/components/ui/dropdown-menu", () => ({
|
|
10
|
+
DropdownMenu: ({ children }: any) => <div>{children}</div>,
|
|
11
|
+
DropdownMenuTrigger: ({ children }: any) => <div>{children}</div>,
|
|
12
|
+
DropdownMenuContent: ({ children }: any) => <div>{children}</div>,
|
|
13
|
+
DropdownMenuItem: ({ children }: any) => <div>{children}</div>,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock("@/components/ui/separator", () => ({
|
|
17
|
+
Separator: () => <div data-testid="separator" />,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("@/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name", () => ({
|
|
21
|
+
LucideIconByName: () => <div data-testid="icon" />,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock("@tanstack/react-router", () => ({
|
|
25
|
+
Link: ({ children }: any) => <a>{children}</a>,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
describe("WorkspaceHeader", () => {
|
|
29
|
+
it("渲染工作空间名称", () => {
|
|
30
|
+
const WORKSPACE = {
|
|
31
|
+
id: "ws1",
|
|
32
|
+
name: "测试工作空间",
|
|
33
|
+
description: null,
|
|
34
|
+
icon: "folder",
|
|
35
|
+
_count: { projects: 0 },
|
|
36
|
+
};
|
|
37
|
+
render(<WorkspaceHeader workspace={WORKSPACE} onEdit={() => {}} onDelete={() => {}} />);
|
|
38
|
+
expect(screen.getByText("测试工作空间")).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
const mocks = vi.hoisted(() => ({
|
|
5
|
+
invalidate: vi.fn(),
|
|
6
|
+
setKbs: vi.fn(),
|
|
7
|
+
renameDoc: vi.fn(),
|
|
8
|
+
removeDoc: vi.fn(),
|
|
9
|
+
reorderDocs: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("@tanstack/react-router", () => ({
|
|
13
|
+
useRouter: () => ({ invalidate: mocks.invalidate }),
|
|
14
|
+
Link: ({ to, children, ...rest }: any) => (
|
|
15
|
+
<a href={typeof to === "string" ? to : "#"} {...rest}>{children}</a>
|
|
16
|
+
),
|
|
17
|
+
}));
|
|
18
|
+
vi.mock("@tanstack/react-start", () => ({
|
|
19
|
+
useServerFn: (fn: any) => async (...args: any[]) => fn(...args),
|
|
20
|
+
}));
|
|
21
|
+
vi.mock("@/server/serverFns/workspace-kb", () => ({
|
|
22
|
+
getWorkspaceKbCardFn: vi.fn().mockResolvedValue({ kbs: [], quickDocs: [] }),
|
|
23
|
+
listWorkspaceAvailableKbsFn: vi.fn(),
|
|
24
|
+
setWorkspaceKbsFn: mocks.setKbs,
|
|
25
|
+
renameQuickDocFn: mocks.renameDoc,
|
|
26
|
+
removeQuickDocFn: mocks.removeDoc,
|
|
27
|
+
reorderQuickDocsFn: mocks.reorderDocs,
|
|
28
|
+
searchQuickDocCandidatesFn: vi.fn(),
|
|
29
|
+
addQuickDocsFn: vi.fn(),
|
|
30
|
+
}));
|
|
31
|
+
vi.mock("@dnd-kit/react", () => ({
|
|
32
|
+
DragDropProvider: ({ children }: any) => <div>{children}</div>,
|
|
33
|
+
useDroppable: () => ({ ref: null, isDropTarget: false }),
|
|
34
|
+
}));
|
|
35
|
+
vi.mock("@dnd-kit/react/sortable", () => ({
|
|
36
|
+
useSortable: () => ({ ref: undefined, handleRef: undefined, isDragging: false }),
|
|
37
|
+
}));
|
|
38
|
+
vi.mock("sonner", () => ({
|
|
39
|
+
toast: { error: vi.fn(), success: vi.fn(), warning: vi.fn() },
|
|
40
|
+
}));
|
|
41
|
+
vi.mock("../workspace-kb-manage-dialog", () => ({
|
|
42
|
+
WorkspaceKbManageDialog: () => null,
|
|
43
|
+
}));
|
|
44
|
+
vi.mock("../workspace-quick-doc-dialog", () => ({
|
|
45
|
+
WorkspaceQuickDocDialog: () => null,
|
|
46
|
+
}));
|
|
47
|
+
vi.mock("@/components/ui/tooltip", () => ({
|
|
48
|
+
Tooltip: ({ children }: any) => <span data-testid="tooltip-wrapper">{children}</span>,
|
|
49
|
+
TooltipContent: ({ children }: any) => <span>{children}</span>,
|
|
50
|
+
TooltipTrigger: ({ children }: any) => <span data-testid="tooltip-trigger">{children}</span>,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
import { WorkspaceKbCard } from "../../kb/workspace-kb-card";
|
|
54
|
+
|
|
55
|
+
const KB = { id: "kb1", name: "团队库", icon: "notebook", visibility: "team" };
|
|
56
|
+
const DOC_ROW = {
|
|
57
|
+
id: "q1", displayTitle: "接入指南", columnIndex: 0, sortOrder: 0, label: null, icon: null, source: "doc" as const,
|
|
58
|
+
docId: "d1", kbId: "kb1", kbName: "团队库", docIcon: null,
|
|
59
|
+
group: null, externalId: null, faviconUrl: null, sourceTitle: "接入指南", url: null, stale: false,
|
|
60
|
+
};
|
|
61
|
+
const STALE_ROW = {
|
|
62
|
+
...DOC_ROW, id: "q2", displayTitle: "旧法条", source: "general" as const,
|
|
63
|
+
docId: null, kbId: null, kbName: null, group: "legal" as const, externalId: "gone",
|
|
64
|
+
faviconUrl: null, sourceTitle: null, url: null, stale: true,
|
|
65
|
+
};
|
|
66
|
+
const EXTERNAL_URL_ROW = {
|
|
67
|
+
...DOC_ROW, id: "q3", displayTitle: "外部工具", source: "general" as const,
|
|
68
|
+
docId: null, kbId: null, kbName: null, group: "bookmark" as const, externalId: "ext123",
|
|
69
|
+
faviconUrl: null, sourceTitle: "PNG to ICO", url: "https://tool.xuecan.net/png-to-ico/", stale: false,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
vi.clearAllMocks();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("WorkspaceKbCard 渲染", () => {
|
|
77
|
+
it("空数据显示空状态且「前往知识库」禁用", () => {
|
|
78
|
+
render(<WorkspaceKbCard workspaceId="ws1" data={{ kbs: [], quickDocs: [] }} />);
|
|
79
|
+
expect(screen.getByText(/快捷知识入口/)).toBeInTheDocument();
|
|
80
|
+
// 前往知识库图标按钮 disabled,无 aria-label
|
|
81
|
+
const buttons = screen.getAllByRole("button");
|
|
82
|
+
const disabledBtn = buttons.find((b) => b.getAttribute("disabled") !== null);
|
|
83
|
+
expect(disabledBtn).toBeTruthy();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("渲染快捷文档行(5列布局)", () => {
|
|
87
|
+
render(<WorkspaceKbCard workspaceId="ws1" data={{ kbs: [], quickDocs: [DOC_ROW] }} />);
|
|
88
|
+
expect(screen.getByText("接入指南")).toBeInTheDocument();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("失效条目显示「源已失效」", () => {
|
|
92
|
+
render(<WorkspaceKbCard workspaceId="ws1" data={{ kbs: [], quickDocs: [STALE_ROW] }} />);
|
|
93
|
+
expect(screen.getByText(/源已失效/)).toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("已关联知识库时不显示知识库名称(chip 已移除),前往按钮 tooltip 显示库名", () => {
|
|
97
|
+
render(<WorkspaceKbCard workspaceId="ws1" data={{ kbs: [KB], quickDocs: [] }} />);
|
|
98
|
+
// chip 行已移除,但 goToKb tooltip 仍显示库名
|
|
99
|
+
expect(screen.getByText("团队库")).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("WorkspaceKbCard 乐观更新", () => {
|
|
104
|
+
it("删除确认后乐观移除并调 removeQuickDocFn", async () => {
|
|
105
|
+
render(<WorkspaceKbCard workspaceId="ws1" data={{ kbs: [], quickDocs: [DOC_ROW] }} />);
|
|
106
|
+
fireEvent.click(screen.getByRole("button", { name: "删除" }));
|
|
107
|
+
fireEvent.click(screen.getByRole("button", { name: "确认" }));
|
|
108
|
+
await waitFor(() => {
|
|
109
|
+
expect(mocks.removeDoc).toHaveBeenCalledWith({ data: { quickDocId: "q1" } });
|
|
110
|
+
});
|
|
111
|
+
expect(screen.queryByText("接入指南")).not.toBeInTheDocument();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("重命名提交调用 renameQuickDocFn 且 UI 更新", async () => {
|
|
115
|
+
render(<WorkspaceKbCard workspaceId="ws1" data={{ kbs: [], quickDocs: [DOC_ROW] }} />);
|
|
116
|
+
fireEvent.click(screen.getByRole("button", { name: "重命名" }));
|
|
117
|
+
const input = screen.getByRole("textbox");
|
|
118
|
+
fireEvent.change(input, { target: { value: "新标题" } });
|
|
119
|
+
fireEvent.keyDown(input, { key: "Enter" });
|
|
120
|
+
await waitFor(() => {
|
|
121
|
+
expect(mocks.renameDoc).toHaveBeenCalledWith({
|
|
122
|
+
data: { quickDocId: "q1", displayTitle: "新标题" },
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
expect(screen.getByText("新标题")).toBeInTheDocument();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("外部 URL 卡片使用 <a href> 而非 Link", () => {
|
|
129
|
+
render(<WorkspaceKbCard workspaceId="ws1" data={{ kbs: [], quickDocs: [EXTERNAL_URL_ROW] }} />);
|
|
130
|
+
// 外部 URL 应渲染为普通 <a> 标签,而非 TanStack Router Link(mock 为 <a>)
|
|
131
|
+
const link = screen.getByText("外部工具").closest("a");
|
|
132
|
+
expect(link).toBeInTheDocument();
|
|
133
|
+
expect((link as HTMLAnchorElement).href).toBe("https://tool.xuecan.net/png-to-ico/");
|
|
134
|
+
expect((link as HTMLAnchorElement).target).toBe("_blank");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("编辑状态下点击行不触发导航", async () => {
|
|
138
|
+
render(<WorkspaceKbCard workspaceId="ws1" data={{ kbs: [], quickDocs: [DOC_ROW] }} />);
|
|
139
|
+
// 进入编辑模式
|
|
140
|
+
fireEvent.click(screen.getByRole("button", { name: "重命名" }));
|
|
141
|
+
// 编辑态:快捷文档行的主链接不应存在(渲染为 span,无导航能力)
|
|
142
|
+
const row = document.querySelector("[data-quick-doc-row]");
|
|
143
|
+
expect(row).toBeInTheDocument();
|
|
144
|
+
// 主链接区域(包含 title 的 <a>)应不存在
|
|
145
|
+
const mainLink = row!.querySelector<HTMLAnchorElement>('.flex-1.truncate > a');
|
|
146
|
+
expect(mainLink).not.toBeInTheDocument();
|
|
147
|
+
// Input 应存在且可编辑
|
|
148
|
+
expect(screen.getByRole("textbox")).toBeInTheDocument();
|
|
149
|
+
// 点击行区域不应有导航(因为没有 Link)
|
|
150
|
+
fireEvent.click(row!);
|
|
151
|
+
// 编辑状态应保持(点击行内不会意外退出)
|
|
152
|
+
expect(screen.getByRole("textbox")).toBeInTheDocument();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { WorkspaceMemberItem } from "../../members/workspace-member-item";
|
|
4
|
+
|
|
5
|
+
vi.mock("@/components/shared-kit", () => ({
|
|
6
|
+
UserAvatar: ({ name }: { name?: string }) => <div data-testid="user-avatar">{name?.[0] ?? "?"}</div>,
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
const MEMBER = {
|
|
10
|
+
id: "m1",
|
|
11
|
+
userId: "u1",
|
|
12
|
+
role: "member" as const,
|
|
13
|
+
status: "active" as const,
|
|
14
|
+
joinedAt: new Date(),
|
|
15
|
+
user: { id: "u1", username: "张三", email: "zhangsan@example.com" },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe("WorkspaceMemberItem", () => {
|
|
19
|
+
it("渲染成员用户名和邮箱", () => {
|
|
20
|
+
render(<WorkspaceMemberItem member={MEMBER} workspaceOwnerId="owner1" onRemove={() => {}} />);
|
|
21
|
+
expect(screen.getByText("张三")).toBeInTheDocument();
|
|
22
|
+
expect(screen.getByText("zhangsan@example.com")).toBeInTheDocument();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { WorkspaceMembersManager } from "../../members/workspace-members-manager";
|
|
4
|
+
|
|
5
|
+
vi.mock("@/components/ui/button", () => ({
|
|
6
|
+
Button: ({ children, ...props }: any) => <button {...props}>{children}</button>,
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock("@/components/ui/input", () => ({
|
|
10
|
+
Input: (props: any) => <input {...props} />,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("@/components/ui/dialog", () => ({
|
|
14
|
+
Dialog: ({ children }: any) => <div>{children}</div>,
|
|
15
|
+
DialogContent: ({ children }: any) => <div>{children}</div>,
|
|
16
|
+
DialogFooter: ({ children }: any) => <div>{children}</div>,
|
|
17
|
+
DialogHeader: ({ children }: any) => <div>{children}</div>,
|
|
18
|
+
DialogTitle: ({ children }: any) => <h2>{children}</h2>,
|
|
19
|
+
DialogDescription: ({ children }: any) => <p>{children}</p>,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock("@/components/ui/sonner", () => ({
|
|
23
|
+
toast: { error: vi.fn(), success: vi.fn() },
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock("@/components/ui/tooltip", () => ({
|
|
27
|
+
TooltipProvider: ({ children }: any) => <div>{children}</div>,
|
|
28
|
+
Tooltip: ({ children }: any) => <div>{children}</div>,
|
|
29
|
+
TooltipTrigger: ({ children }: any) => <div>{children}</div>,
|
|
30
|
+
TooltipContent: ({ children }: any) => <div>{children}</div>,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
vi.mock("@/server/serverFns/workspace", () => ({
|
|
34
|
+
listWorkspaceMembersFn: vi.fn().mockResolvedValue([]),
|
|
35
|
+
inviteWorkspaceMemberFn: vi.fn().mockResolvedValue({}),
|
|
36
|
+
removeWorkspaceMemberFn: vi.fn().mockResolvedValue({}),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
describe("WorkspaceMembersManager", () => {
|
|
40
|
+
it("渲染组件不崩溃", () => {
|
|
41
|
+
render(
|
|
42
|
+
<WorkspaceMembersManager
|
|
43
|
+
workspaceId="ws1"
|
|
44
|
+
workspaceOwnerId="owner1"
|
|
45
|
+
members={[]}
|
|
46
|
+
onMembersUpdated={() => {}}
|
|
47
|
+
/>,
|
|
48
|
+
);
|
|
49
|
+
expect(screen.getAllByText(/成员/).length).toBeGreaterThan(0);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { WorkspaceProjectList } from "../../projects/workspace-project-list";
|
|
4
|
+
|
|
5
|
+
vi.mock("@/components/ui/button", () => ({
|
|
6
|
+
Button: ({ children, ...props }: any) => <button {...props}>{children}</button>,
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock("@/components/ui/card", () => ({
|
|
10
|
+
Card: ({ children, className, ...props }: any) => <div className={className} {...props}>{children}</div>,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("@/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name", () => ({
|
|
14
|
+
LucideIconByName: () => <div data-testid="icon" />,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("@/components/project/create-project-dialog", () => ({
|
|
18
|
+
CreateProjectDialog: () => null,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("@tanstack/react-router", () => ({
|
|
22
|
+
Link: ({ children }: any) => <a>{children}</a>,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const PROJECTS = [
|
|
26
|
+
{
|
|
27
|
+
id: "p1",
|
|
28
|
+
name: "产品需求",
|
|
29
|
+
description: "PRD 管理项目",
|
|
30
|
+
icon: "lightbulb",
|
|
31
|
+
updatedAt: new Date("2024-01-15"),
|
|
32
|
+
_count: { members: 3 },
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
describe("WorkspaceProjectList", () => {
|
|
37
|
+
it("空状态显示提示卡片", () => {
|
|
38
|
+
render(<WorkspaceProjectList projects={[]} workspaceId="ws1" workspaceName="测试" />);
|
|
39
|
+
expect(screen.getByText(/暂无项目/)).toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("有项目时显示项目列表", () => {
|
|
43
|
+
render(<WorkspaceProjectList projects={PROJECTS} workspaceId="ws1" workspaceName="测试" />);
|
|
44
|
+
expect(screen.getByText("产品需求")).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
});
|
package/src/components/workspace/{create-workspace-dialog.tsx → dialogs/create-workspace-dialog.tsx}
RENAMED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
} from "@/components/ui/dialog";
|
|
30
30
|
import { LucideIconPicker } from "@/components/shared-kit/icons/lucide-icon-picker";
|
|
31
31
|
import { createWorkspaceFn } from "@/server/serverFns/workspace";
|
|
32
|
-
import { createWorkspaceSchema, type CreateWorkspaceValues } from "
|
|
32
|
+
import { createWorkspaceSchema, type CreateWorkspaceValues } from "../schemas/create-workspace";
|
|
33
33
|
import { m } from "@/paraglide/messages.js";
|
|
34
34
|
|
|
35
35
|
interface CreateWorkspaceDialogProps {
|
package/src/components/workspace/{edit-workspace-dialog.tsx → dialogs/edit-workspace-dialog.tsx}
RENAMED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
} from "@/components/ui/dialog";
|
|
30
30
|
import { LucideIconPicker } from "@/components/shared-kit/icons/lucide-icon-picker";
|
|
31
31
|
import { updateWorkspaceFn } from "@/server/serverFns/workspace";
|
|
32
|
-
import { createWorkspaceSchema, type CreateWorkspaceValues } from "
|
|
32
|
+
import { createWorkspaceSchema, type CreateWorkspaceValues } from "../schemas/create-workspace";
|
|
33
33
|
import { m } from "@/paraglide/messages.js";
|
|
34
34
|
|
|
35
35
|
interface EditWorkspaceDialogProps {
|