@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
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { Link, useLocation } from "@tanstack/react-router";
|
|
19
19
|
import { useEffect, useState } from "react";
|
|
20
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
20
21
|
import { Layers, Plus, PinOff } from "lucide-react";
|
|
21
22
|
import {
|
|
22
23
|
SidebarGroup,
|
|
@@ -29,22 +30,23 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
|
|
|
29
30
|
import { cn } from "@/lib/utils";
|
|
30
31
|
import { isParentActive } from "@/lib/sidebar-utils";
|
|
31
32
|
import { LucideIconByName } from "@/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name";
|
|
32
|
-
import { CreateWorkspaceDialog } from "@/components/workspace/create-workspace-dialog";
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
|
|
33
|
+
import { CreateWorkspaceDialog } from "@/components/workspace/dialogs/create-workspace-dialog";
|
|
34
|
+
import { WORKSPACES_QUERY_KEY } from "@/lib/collections/reference/workspaces-collection";
|
|
35
|
+
import {
|
|
36
|
+
listPinnedWorkspacesBriefFn,
|
|
37
|
+
toggleWorkspacePinFn,
|
|
38
|
+
} from "@/server/serverFns/workspace";
|
|
36
39
|
import { m } from "@/paraglide/messages.js";
|
|
37
40
|
|
|
38
41
|
function WorkspaceLinks({ pathname }: { pathname: string }) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}> | undefined) ?? [];
|
|
42
|
+
// useQuery 直读 per-request queryClient(_nav loader 已 SSR 预取),
|
|
43
|
+
// 列表随 SSR HTML 直出,刷新无需等待;避免 module-level collection 的跨请求泄漏
|
|
44
|
+
const { data } = useQuery({
|
|
45
|
+
queryKey: WORKSPACES_QUERY_KEY,
|
|
46
|
+
queryFn: () => listPinnedWorkspacesBriefFn(),
|
|
47
|
+
});
|
|
48
|
+
const queryClient = useQueryClient();
|
|
49
|
+
const workspacesList = data ?? [];
|
|
48
50
|
|
|
49
51
|
return (
|
|
50
52
|
<SidebarMenu className="space-y-1">
|
|
@@ -90,7 +92,7 @@ function WorkspaceLinks({ pathname }: { pathname: string }) {
|
|
|
90
92
|
className="absolute right-1 top-1/2 -translate-y-1/2 flex size-5 items-center justify-center rounded text-sidebar-foreground/70 opacity-0 transition-opacity group-hover/ws:opacity-100 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
|
91
93
|
onClick={async () => {
|
|
92
94
|
await toggleWorkspacePinFn({ data: { workspaceId: ws.id, pinned: false } });
|
|
93
|
-
(
|
|
95
|
+
void queryClient.invalidateQueries({ queryKey: WORKSPACES_QUERY_KEY });
|
|
94
96
|
window.dispatchEvent(new CustomEvent("workspace-pinned", { detail: { workspaceId: ws.id, pinned: false } }));
|
|
95
97
|
}}
|
|
96
98
|
>
|
|
@@ -112,16 +114,12 @@ export function NavWorkspace() {
|
|
|
112
114
|
const { state } = useSidebar();
|
|
113
115
|
const { pathname } = useLocation();
|
|
114
116
|
const isCollapsed = state === "collapsed";
|
|
115
|
-
const [mounted, setMounted] = useState(false);
|
|
116
117
|
const [createOpen, setCreateOpen] = useState(false);
|
|
117
|
-
|
|
118
|
-
useEffect(() => {
|
|
119
|
-
setMounted(true);
|
|
120
|
-
}, []);
|
|
118
|
+
const queryClient = useQueryClient();
|
|
121
119
|
|
|
122
120
|
useEffect(() => {
|
|
123
121
|
const refresh = () => {
|
|
124
|
-
(
|
|
122
|
+
void queryClient.invalidateQueries({ queryKey: WORKSPACES_QUERY_KEY });
|
|
125
123
|
};
|
|
126
124
|
window.addEventListener("workspace-created", refresh);
|
|
127
125
|
window.addEventListener("workspace-deleted", refresh);
|
|
@@ -133,7 +131,7 @@ export function NavWorkspace() {
|
|
|
133
131
|
window.removeEventListener("workspace-updated", refresh);
|
|
134
132
|
window.removeEventListener("workspace-pinned", refresh);
|
|
135
133
|
};
|
|
136
|
-
}, []);
|
|
134
|
+
}, [queryClient]);
|
|
137
135
|
|
|
138
136
|
if (isCollapsed) {
|
|
139
137
|
return (
|
|
@@ -173,7 +171,7 @@ export function NavWorkspace() {
|
|
|
173
171
|
<Plus className="size-3.5" />
|
|
174
172
|
</button>
|
|
175
173
|
</div>
|
|
176
|
-
|
|
174
|
+
<WorkspaceLinks pathname={pathname} />
|
|
177
175
|
<CreateWorkspaceDialog open={createOpen} onOpenChange={setCreateOpen} />
|
|
178
176
|
</SidebarGroup>
|
|
179
177
|
);
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { useEffect, useState } from "react"
|
|
21
21
|
import type { AvatarConfig, AvatarPart, ShapeType } from "@allbluecn/kernel"
|
|
22
|
+
import { sanitizeSvg } from "../../../lib/sanitize"
|
|
22
23
|
|
|
23
24
|
const noneParts: AvatarPart[] = ["glasses", "hair", "accessories", "details", "beard"]
|
|
24
25
|
|
|
@@ -120,7 +121,7 @@ export function NotionAvatar({
|
|
|
120
121
|
aspectRatio: "1 / 1",
|
|
121
122
|
...style,
|
|
122
123
|
}}
|
|
123
|
-
dangerouslySetInnerHTML={{ __html: svgContent }}
|
|
124
|
+
dangerouslySetInnerHTML={{ __html: sanitizeSvg(svgContent) }}
|
|
124
125
|
/>
|
|
125
126
|
)
|
|
126
127
|
}
|
|
@@ -35,8 +35,8 @@ const NUMBER_TAIL_ALLOWLIST = new Set([
|
|
|
35
35
|
]);
|
|
36
36
|
|
|
37
37
|
describe("CURATED_ICONS 精选名单完整性", () => {
|
|
38
|
-
it("数量在目标区间
|
|
39
|
-
expect(CURATED_ICONS.length).toBeGreaterThanOrEqual(
|
|
38
|
+
it("数量在目标区间 380-520", () => {
|
|
39
|
+
expect(CURATED_ICONS.length).toBeGreaterThanOrEqual(380);
|
|
40
40
|
expect(CURATED_ICONS.length).toBeLessThanOrEqual(520);
|
|
41
41
|
});
|
|
42
42
|
|
|
@@ -34,11 +34,12 @@ describe("LucideIconByName", () => {
|
|
|
34
34
|
expect(svg?.innerHTML).toContain("<path");
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
it("精选外/未知 name
|
|
37
|
+
it("精选外/未知 name 渲染默认占位图标(FaceSlightlySmilingPlus)", () => {
|
|
38
38
|
const { container } = render(<LucideIconByName name="not-exist-icon" />);
|
|
39
39
|
const svg = container.querySelector("svg");
|
|
40
40
|
expect(svg).toBeTruthy();
|
|
41
|
-
expect(svg?.
|
|
41
|
+
expect(svg?.getAttribute("class")).toContain("lucide-face-slightly-smiling-plus");
|
|
42
|
+
expect(svg?.innerHTML).toContain("<path");
|
|
42
43
|
});
|
|
43
44
|
|
|
44
45
|
it("undefined name 渲染 fallback", () => {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// 机械禁则由 lucide-curated-icons.test.ts 强制校验。
|
|
21
21
|
|
|
22
22
|
import {
|
|
23
|
-
Accessibility, Activity, Ambulance, Ampersand, Anchor,
|
|
23
|
+
Accessibility, Activity, Ambulance, Ampersand, Anchor, Aperture, Archive, ArrowDown, ArrowDownLeft,
|
|
24
24
|
ArrowDownRight, ArrowLeft, ArrowRight, ArrowUp, ArrowUpLeft, ArrowUpRight, Asterisk, AtSign, Atom, AudioLines,
|
|
25
25
|
Award, BadgeCheck, Bandage, Banknote, Barcode, Bath, Battery, BatteryCharging, Bed, BedDouble,
|
|
26
26
|
Beer, Bell, BellPlus, BellRing, Bike, Binary, Binoculars, Bird, Bold, Book,
|
|
@@ -35,15 +35,15 @@ import {
|
|
|
35
35
|
EthernetPort, ExternalLink, Eye, Factory, FastForward, File, FileArchive, FileBadge, FileBox, FileCheck,
|
|
36
36
|
FileClock, FileCode, FilePlus, FileText, Files, Film, Fish, Flag, FlaskConical, Flower,
|
|
37
37
|
Folder, FolderCheck, FolderClock, FolderClosed, FolderCode, FolderGit, FolderHeart, FolderKanban, FolderOpen, FolderPlus,
|
|
38
|
-
FolderRoot, FolderSearch, FolderSync, FolderTree,
|
|
38
|
+
FolderRoot, FolderSearch, FolderSync, FolderTree, Funnel, Gamepad, Gauge, Gift, Ghost,
|
|
39
39
|
GitBranch, GitCommitHorizontal, GitCommitVertical, GitCompare, GitFork, GitGraph, GitMerge, Globe, GraduationCap, Grid2x2Check,
|
|
40
40
|
Grid2x2Plus, Guitar, Hammer, HardDrive, Hash, Headphones, Headset, Heart, HeartPulse, Helicopter,
|
|
41
41
|
Hexagon, Hospital, Hourglass, IdCard, Image, ImageDown, ImagePlus, ImageUp, Images, Inbox,
|
|
42
42
|
Infinity, Info, Italic, Joystick, Key, KeyRound, Keyboard, Lamp, Landmark, Laptop,
|
|
43
|
-
|
|
43
|
+
Layers, LayoutDashboard, LayoutFreeform, LayoutGrid, LayoutList, LayoutPanelLeft, LayoutTemplate, Leaf, Library,
|
|
44
44
|
Lightbulb, Link, List, ListChecks, ListOrdered, Loader, LoaderCircle, LoaderPinwheel, LocateFixed, Lock,
|
|
45
45
|
LogIn, LogOut, Luggage, Mail, MailCheck, MailOpen, MailPlus, MailSearch, MailWarning, Mailbox,
|
|
46
|
-
Map, MapPin, Medal, Megaphone,
|
|
46
|
+
Map, MapPin, Medal, Megaphone, MessageCircle, MessageCircleCheck, MessageCircleHeart, MessageCircleMore, MessageCirclePlus,
|
|
47
47
|
MessageCircleQuestionMark, MessageSquare, MessageSquareCheck, MessageSquareCode, MessageSquareLock, MessagesSquare, Mic, Microscope, Minus, Monitor,
|
|
48
48
|
Moon, Mountain, MountainSnow, Mouse, MousePointer, MousePointerClick, Music, Navigation, Network, Newspaper,
|
|
49
49
|
Nfc, Octagon, PaintBucket, Paintbrush, Palette, PanelTop, PartyPopper, Pause, PenLine, PenTool,
|
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
Reply, Rewind, Rocket, Route, Sailboat, Satellite, Save, Scan, ScanBarcode, ScanFace,
|
|
53
53
|
ScanLine, ScanSearch, ScanText, School, Search, SearchAlert, SearchCode, Send, Server, Shapes,
|
|
54
54
|
Shield, ShieldAlert, ShieldCheck, Ship, ShoppingBag, ShoppingBasket, ShoppingCart, Sigma, Signal, Signpost,
|
|
55
|
-
Siren, SkipBack, SkipForward, Slash, SlidersHorizontal, Smartphone,
|
|
55
|
+
Siren, SkipBack, SkipForward, Slash, SlidersHorizontal, Smartphone, Skull, Snowflake, Sofa,
|
|
56
56
|
Spade, Sparkles, Spline, Sprout, Square, SquarePen, SquareTerminal, Stamp, Star, StarCheck,
|
|
57
57
|
StarHalf, StarMinus, StarPlus, Stethoscope, Store, Strikethrough, Sun, Sunrise, Sunset, SwatchBook,
|
|
58
58
|
Syringe, Table, TableOfContents, Tablet, Tag, Tags, Target, Tent, Terminal, TestTube,
|
|
@@ -391,11 +391,6 @@ export const CURATED_ICONS: CuratedIcon[] = [
|
|
|
391
391
|
{ name: "star-minus", Icon: StarMinus, zh: ["减星"] },
|
|
392
392
|
{ name: "star-plus", Icon: StarPlus, zh: ["加星"] },
|
|
393
393
|
{ name: "thumbs-down", Icon: ThumbsDown, zh: ["踩"] },
|
|
394
|
-
{ name: "smile", Icon: Smile, zh: ["微笑"] },
|
|
395
|
-
{ name: "frown", Icon: Frown, zh: ["愁眉"] },
|
|
396
|
-
{ name: "meh", Icon: Meh, zh: ["一般"] },
|
|
397
|
-
{ name: "laugh", Icon: Laugh, zh: ["大笑"] },
|
|
398
|
-
{ name: "angry", Icon: Angry, zh: ["愤怒"] },
|
|
399
394
|
{ name: "map", Icon: Map, zh: ["地图"] },
|
|
400
395
|
{ name: "map-pin", Icon: MapPin, zh: ["地图钉"] },
|
|
401
396
|
{ name: "navigation", Icon: Navigation, zh: ["导航"] },
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import type { ReactNode } from "react";
|
|
19
|
-
import {
|
|
19
|
+
import { FaceSlightlySmilingPlus } from "lucide-react";
|
|
20
20
|
import { cn } from "@/lib/utils";
|
|
21
21
|
import { CURATED_ICON_MAP } from "./curated-icons";
|
|
22
22
|
import { pascalToKebab } from "./icon-data";
|
|
@@ -48,7 +48,7 @@ interface LucideIconByNameProps {
|
|
|
48
48
|
name?: string;
|
|
49
49
|
size?: number;
|
|
50
50
|
className?: string;
|
|
51
|
-
/** 默认:
|
|
51
|
+
/** 默认:FaceSlightlySmilingPlus 微笑脸(未选择图标时的占位) */
|
|
52
52
|
fallback?: ReactNode;
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -64,7 +64,7 @@ export function LucideIconByName({
|
|
|
64
64
|
return (
|
|
65
65
|
<>
|
|
66
66
|
{fallback ?? (
|
|
67
|
-
<
|
|
67
|
+
<FaceSlightlySmilingPlus
|
|
68
68
|
size={size}
|
|
69
69
|
aria-hidden
|
|
70
70
|
className={cn("text-muted-foreground", className)}
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
// 测试专用样板组件:VRT 视觉回归基线的主题表面聚合。
|
|
19
|
+
// 不接入任何路由,不加入 shared-kit/index.ts 导出(避免被产品代码误引用)。
|
|
20
|
+
// 文案为测试样板硬编码中文(不出现在产品 UI),豁免 i18n。
|
|
21
|
+
|
|
22
|
+
"use client"
|
|
23
|
+
|
|
24
|
+
import { Button } from "@/components/ui/button"
|
|
25
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
26
|
+
import { Input } from "@/components/ui/input"
|
|
27
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
|
28
|
+
|
|
29
|
+
export function ThemeSurface() {
|
|
30
|
+
return (
|
|
31
|
+
<div data-testid="theme-surface" className="bg-background text-foreground p-6 space-y-4 w-96">
|
|
32
|
+
<Card>
|
|
33
|
+
<CardHeader><CardTitle className="text-base">语义色表面</CardTitle></CardHeader>
|
|
34
|
+
<CardContent className="space-y-3">
|
|
35
|
+
<Input placeholder="输入框 placeholder" />
|
|
36
|
+
<div className="flex gap-2">
|
|
37
|
+
<Button size="sm">主按钮</Button>
|
|
38
|
+
<Button size="sm" variant="secondary">次按钮</Button>
|
|
39
|
+
<Button size="sm" variant="destructive">危险</Button>
|
|
40
|
+
<Tooltip>
|
|
41
|
+
<TooltipTrigger asChild>
|
|
42
|
+
<Button size="sm" variant="outline">带提示</Button>
|
|
43
|
+
</TooltipTrigger>
|
|
44
|
+
<TooltipContent>工具提示内容</TooltipContent>
|
|
45
|
+
</Tooltip>
|
|
46
|
+
</div>
|
|
47
|
+
<div className="rounded-lg border bg-muted p-3 text-sm text-muted-foreground">muted 区域</div>
|
|
48
|
+
</CardContent>
|
|
49
|
+
</Card>
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -1,15 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const GROUP_LABELS: Record<string, () => string> = {
|
|
4
|
-
legal: m.story_knowledgeDialog_groupLegal,
|
|
5
|
-
compliance: m.story_knowledgeDialog_groupCompliance,
|
|
6
|
-
dictionary: m.story_knowledgeDialog_groupDictionary,
|
|
7
|
-
bookmark: m.story_knowledgeDialog_groupBookmark,
|
|
8
|
-
summary: m.story_knowledgeDialog_groupSummary,
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** 通用知识库分组 key → 标签(未知 key 原样返回) */
|
|
12
|
-
export function generalGroupLabel(group: string): string {
|
|
13
|
-
const labelFn = GROUP_LABELS[group]
|
|
14
|
-
return labelFn ? labelFn() : group
|
|
15
|
-
}
|
|
1
|
+
export { generalGroupLabel } from "@/components/knowledge/general-groups";
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import * as React from "react";
|
|
19
19
|
import { useServerFn } from "@tanstack/react-start";
|
|
20
20
|
import { toast } from "sonner";
|
|
21
|
-
import {
|
|
21
|
+
import { BookOpenText, BookMarked, Bookmark, Check, Scale, ShieldCheck, Sprout } from "lucide-react";
|
|
22
22
|
import { Button } from "@/components/ui/button";
|
|
23
23
|
import { Input } from "@/components/ui/input";
|
|
24
24
|
import { Badge } from "@/components/ui/badge";
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
type SearchDocRow,
|
|
41
41
|
} from "@/server/serverFns/story";
|
|
42
42
|
import { generalGroupLabel } from "./general-groups";
|
|
43
|
+
import { KbFilterPopover, type KbFilter } from "@/components/knowledge/kb-filter-popover";
|
|
43
44
|
import type { DocBrief } from "../types";
|
|
44
45
|
import { m } from "@/paraglide/messages.js";
|
|
45
46
|
|
|
@@ -50,24 +51,6 @@ interface KbBrief {
|
|
|
50
51
|
visibility: string;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
type KbFilter = { type: "kb"; id: string } | { type: "general"; group: string } | null;
|
|
54
|
-
|
|
55
|
-
const GENERAL_GROUP_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
56
|
-
legal: Scale,
|
|
57
|
-
compliance: ShieldCheck,
|
|
58
|
-
dictionary: BookMarked,
|
|
59
|
-
bookmark: Bookmark,
|
|
60
|
-
summary: Sprout,
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const GENERAL_GROUP_META = [
|
|
64
|
-
{ key: "legal" },
|
|
65
|
-
{ key: "compliance" },
|
|
66
|
-
{ key: "dictionary" },
|
|
67
|
-
{ key: "bookmark" },
|
|
68
|
-
{ key: "summary" },
|
|
69
|
-
] as const;
|
|
70
|
-
|
|
71
54
|
export interface KnowledgeSelectDialogProps {
|
|
72
55
|
open: boolean;
|
|
73
56
|
onOpenChange: (open: boolean) => void;
|
|
@@ -98,7 +81,6 @@ export function KnowledgeSelectDialog({
|
|
|
98
81
|
const [selected, setSelected] = React.useState<Set<string>>(new Set());
|
|
99
82
|
const [loading, setLoading] = React.useState(false);
|
|
100
83
|
const [submitting, setSubmitting] = React.useState(false);
|
|
101
|
-
const [kbPopoverOpen, setKbPopoverOpen] = React.useState(false);
|
|
102
84
|
|
|
103
85
|
const linkedIds = React.useMemo(() => new Set(linkedDocs.map((d) => d.id)), [linkedDocs]);
|
|
104
86
|
|
|
@@ -156,7 +138,6 @@ export function KnowledgeSelectDialog({
|
|
|
156
138
|
|
|
157
139
|
const handleKbSelect = (filter: KbFilter) => {
|
|
158
140
|
setKbFilter(filter);
|
|
159
|
-
setKbPopoverOpen(false);
|
|
160
141
|
void loadDocs(filter, search);
|
|
161
142
|
};
|
|
162
143
|
|
|
@@ -281,7 +262,6 @@ export function KnowledgeSelectDialog({
|
|
|
281
262
|
}
|
|
282
263
|
};
|
|
283
264
|
|
|
284
|
-
const activeKb = kbFilter?.type === "kb" ? kbs.find((k) => k.id === kbFilter.id) : undefined;
|
|
285
265
|
|
|
286
266
|
return (
|
|
287
267
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
@@ -290,71 +270,17 @@ export function KnowledgeSelectDialog({
|
|
|
290
270
|
<DialogTitle>{m.story_knowledgeDialog_title()}</DialogTitle>
|
|
291
271
|
</DialogHeader>
|
|
292
272
|
|
|
293
|
-
<
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
{activeKb
|
|
305
|
-
? activeKb.name
|
|
306
|
-
: kbFilter?.type === "general"
|
|
307
|
-
? generalGroupLabel(kbFilter.group)
|
|
308
|
-
: m.story_knowledgeDialog_allKbs()}
|
|
309
|
-
<ChevronsUpDown className="size-3.5 opacity-50" />
|
|
310
|
-
</Button>
|
|
311
|
-
</PopoverTrigger>
|
|
312
|
-
<PopoverContent align="end" className="w-60 p-1">
|
|
313
|
-
<button
|
|
314
|
-
type="button"
|
|
315
|
-
className="flex w-full items-center justify-between rounded-sm px-2 py-1.5 mt-1 text-sm hover:bg-muted"
|
|
316
|
-
onClick={() => handleKbSelect(null)}
|
|
317
|
-
>
|
|
318
|
-
{m.story_knowledgeDialog_allKbs()}
|
|
319
|
-
{kbFilter === null && <Check className="size-3.5" />}
|
|
320
|
-
</button>
|
|
321
|
-
<div className="border-t px-2 pt-3 text-xs font-medium text-muted-foreground">
|
|
322
|
-
{m.story_knowledgeDialog_userKbs()}
|
|
323
|
-
</div>
|
|
324
|
-
{kbs.map((kb) => (
|
|
325
|
-
<button
|
|
326
|
-
key={kb.id}
|
|
327
|
-
type="button"
|
|
328
|
-
className="flex w-full items-center justify-between rounded-sm px-2 py-1.5 text-sm hover:bg-muted"
|
|
329
|
-
onClick={() => handleKbSelect({ type: "kb", id: kb.id })}
|
|
330
|
-
>
|
|
331
|
-
<span className="truncate">{kb.name}</span>
|
|
332
|
-
{kbFilter?.type === "kb" && kbFilter.id === kb.id && <Check className="size-3.5" />}
|
|
333
|
-
</button>
|
|
334
|
-
))}
|
|
335
|
-
<div className=" border-t px-2 pt-3 text-xs font-medium text-muted-foreground">
|
|
336
|
-
{m.story_knowledgeDialog_generalKbs()}
|
|
337
|
-
</div>
|
|
338
|
-
{GENERAL_GROUP_META.map((group) => {
|
|
339
|
-
const Icon = GENERAL_GROUP_ICONS[group.key];
|
|
340
|
-
return (
|
|
341
|
-
<button
|
|
342
|
-
key={group.key}
|
|
343
|
-
type="button"
|
|
344
|
-
className="flex w-full items-center justify-between rounded-sm px-2 py-1.5 text-sm hover:bg-muted"
|
|
345
|
-
onClick={() => handleKbSelect({ type: "general", group: group.key })}
|
|
346
|
-
>
|
|
347
|
-
<span className="flex min-w-0 items-center gap-2">
|
|
348
|
-
<Icon className="size-3.5 shrink-0 text-muted-foreground" />
|
|
349
|
-
<span className="truncate">{generalGroupLabel(group.key)}</span>
|
|
350
|
-
</span>
|
|
351
|
-
{kbFilter?.type === "general" && kbFilter.group === group.key && <Check className="size-3.5" />}
|
|
352
|
-
</button>
|
|
353
|
-
);
|
|
354
|
-
})}
|
|
355
|
-
</PopoverContent>
|
|
356
|
-
</Popover>
|
|
357
|
-
</div>
|
|
273
|
+
<KbFilterPopover
|
|
274
|
+
search={search}
|
|
275
|
+
onSearchChange={handleSearch}
|
|
276
|
+
filter={kbFilter}
|
|
277
|
+
onFilterChange={(f) => { setKbFilter(f); void loadDocs(f, search); }}
|
|
278
|
+
userKbs={kbs}
|
|
279
|
+
searchPlaceholder={m.story_knowledgeDialog_searchPlaceholder()}
|
|
280
|
+
allLabel={m.story_knowledgeDialog_allKbs()}
|
|
281
|
+
userKbsLabel={m.story_knowledgeDialog_userKbs()}
|
|
282
|
+
generalKbsLabel={m.story_knowledgeDialog_generalKbs()}
|
|
283
|
+
/>
|
|
358
284
|
|
|
359
285
|
<div className="h-80 overflow-y-auto rounded-md border p-1">
|
|
360
286
|
{loading ? (
|
|
@@ -79,7 +79,13 @@ export function StoryListView({ stories: initialStories, users: initialUsers, pr
|
|
|
79
79
|
const [users, setUsers] = useState<UserBrief[]>(initialUsers);
|
|
80
80
|
const [projectId, setProjectId] = useState<string | undefined>(() => {
|
|
81
81
|
try {
|
|
82
|
-
|
|
82
|
+
const stored = localStorage.getItem(STORY_PROJECT_FILTER_KEY) ?? undefined;
|
|
83
|
+
// 清理过期残留(项目已删除或 localStorage 跨账号残留),避免列表查询空集
|
|
84
|
+
if (stored && stored !== "__archived__" && !initialProjects.some((p) => p.id === stored)) {
|
|
85
|
+
localStorage.removeItem(STORY_PROJECT_FILTER_KEY);
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
return stored;
|
|
83
89
|
} catch {
|
|
84
90
|
return undefined;
|
|
85
91
|
}
|
|
@@ -315,8 +315,6 @@ function tableMoveRowColumn({
|
|
|
315
315
|
const moveOperation =
|
|
316
316
|
finalOrientation === "row" ? moveTableRow : moveTableColumn
|
|
317
317
|
|
|
318
|
-
console.log({ from, to, finalOrientation, direction })
|
|
319
|
-
|
|
320
318
|
const dispatch = (tr: Transaction) => editor.view.dispatch(tr)
|
|
321
319
|
|
|
322
320
|
if (editor.state.selection instanceof CellSelection) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { WorkspaceActivityCard } from "../../activity/workspace-activity-card";
|
|
4
|
+
|
|
5
|
+
vi.mock("@/components/project/project-activity-list", () => ({
|
|
6
|
+
ProjectActivityList: ({ activities }: { activities: any[] }) => (
|
|
7
|
+
<ul data-testid="activity-list">
|
|
8
|
+
{activities.map((a: any) => (
|
|
9
|
+
<li key={a.id}>{a.action}</li>
|
|
10
|
+
))}
|
|
11
|
+
</ul>
|
|
12
|
+
),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("@tanstack/react-router", () => ({
|
|
16
|
+
Link: ({ children }: any) => <a>{children}</a>,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
describe("WorkspaceActivityCard", () => {
|
|
20
|
+
it("渲染活动列表", () => {
|
|
21
|
+
const ACTIVITIES = [
|
|
22
|
+
{
|
|
23
|
+
id: "a1",
|
|
24
|
+
action: "创建项目",
|
|
25
|
+
projectId: "p1",
|
|
26
|
+
userId: "u1",
|
|
27
|
+
targetType: null,
|
|
28
|
+
targetId: null,
|
|
29
|
+
metadata: {},
|
|
30
|
+
createdAt: new Date(),
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
render(<WorkspaceActivityCard activities={ACTIVITIES} workspaceId="ws1" />);
|
|
34
|
+
expect(screen.getByTestId("activity-list")).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { WorkspaceContributionCard } from "../../contribution/workspace-contribution-card";
|
|
4
|
+
|
|
5
|
+
vi.mock("@/components/shared-kit/data-viz/contribution-graph-tooltip", () => ({
|
|
6
|
+
ContributionGraphWithTooltip: ({ title }: { title: string }) => (
|
|
7
|
+
<div data-testid="contribution-graph">{title}</div>
|
|
8
|
+
),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
describe("WorkspaceContributionCard", () => {
|
|
12
|
+
it("渲染贡献图卡片", () => {
|
|
13
|
+
const data = [{ date: "2024-01-01", count: 5, level: 3 }];
|
|
14
|
+
render(<WorkspaceContributionCard data={data} />);
|
|
15
|
+
expect(screen.getByTestId("contribution-graph")).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { CreateWorkspaceDialog } from "../../dialogs/create-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
|
+
Link: ({ children }: any) => <a>{children}</a>,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock("@tanstack/react-form", () => {
|
|
31
|
+
const makeField = (value: string) => ({
|
|
32
|
+
value,
|
|
33
|
+
handleChange: vi.fn(),
|
|
34
|
+
handleBlur: vi.fn(),
|
|
35
|
+
meta: { errors: [] },
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
useForm: () => {
|
|
39
|
+
const fields = {
|
|
40
|
+
name: makeField(""),
|
|
41
|
+
icon: makeField("folder"),
|
|
42
|
+
description: makeField(""),
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
handleSubmit: (e: any) => e.preventDefault(),
|
|
46
|
+
setFieldValue: () => {},
|
|
47
|
+
state: { values: { name: "", icon: "folder", description: "" } },
|
|
48
|
+
fields,
|
|
49
|
+
Field: ({ name, children }: { name: "name" | "icon" | "description"; children: unknown }) =>
|
|
50
|
+
typeof children === "function"
|
|
51
|
+
? (children as (field: unknown) => unknown)(fields[name])
|
|
52
|
+
: null,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("CreateWorkspaceDialog", () => {
|
|
59
|
+
it("打开时渲染标题", () => {
|
|
60
|
+
render(<CreateWorkspaceDialog open={true} onOpenChange={() => {}} />);
|
|
61
|
+
expect(screen.getByRole("heading")).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
});
|