@allbluecn/web-app 0.5.0 → 0.6.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.
Files changed (170) hide show
  1. package/package.json +239 -30
  2. package/src/components/iteration/auto-schedule-dialog.tsx +150 -0
  3. package/src/components/iteration/burndown-chart.tsx +63 -0
  4. package/src/components/iteration/constants.ts +28 -0
  5. package/src/components/iteration/create-iteration-dialog.tsx +140 -0
  6. package/src/components/iteration/iteration-card.tsx +60 -0
  7. package/src/components/iteration/iteration-editor.tsx +116 -0
  8. package/src/components/iteration/iteration-progress-bar.tsx +44 -0
  9. package/src/components/iteration/iteration-status-badge.tsx +33 -0
  10. package/src/components/knowledge/dialog/knowledge-form-dialog.tsx +21 -30
  11. package/src/components/knowledge/knowledge-toolbar.tsx +124 -1
  12. package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +2 -2
  13. package/src/components/layout/sidebar-03/nav-workspace.tsx +2 -2
  14. package/src/components/project/create-project-dialog.tsx +44 -57
  15. package/src/components/shared/__tests__/lucide-curated-icons.test.ts +83 -0
  16. package/src/components/shared/__tests__/lucide-icon-data.test.ts +131 -0
  17. package/src/components/shared/__tests__/lucide-icon-picker.test.tsx +197 -0
  18. package/src/components/shared/lucide-icon-picker/curated-icons.ts +478 -0
  19. package/src/components/shared/lucide-icon-picker/icon-data.ts +109 -0
  20. package/src/components/shared/lucide-icon-picker/index.tsx +82 -0
  21. package/src/components/shared/lucide-icon-picker/lucide-icon-by-name.tsx +78 -0
  22. package/src/components/shared/lucide-icon-picker/panel.tsx +163 -0
  23. package/src/components/story/__tests__/constants.test.ts +255 -0
  24. package/src/components/story/ai/story-ai-panel.tsx +23 -15
  25. package/src/components/story/ai/story-ai-preview-list.tsx +19 -0
  26. package/src/components/story/constants.ts +43 -1
  27. package/src/components/story/create-story-dialog.tsx +42 -16
  28. package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +114 -0
  29. package/src/components/story/detail/__tests__/categories.test.tsx +62 -0
  30. package/src/components/story/detail/__tests__/dependencies-button.test.tsx +49 -0
  31. package/src/components/story/detail/activity-timeline.tsx +26 -4
  32. package/src/components/story/detail/story-detail-body.tsx +90 -38
  33. package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +857 -0
  34. package/src/components/story/detail/story-detail-toolbar/categories.tsx +23 -18
  35. package/src/components/story/detail/story-detail-toolbar/children.tsx +9 -4
  36. package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +95 -27
  37. package/src/components/story/detail/story-detail-toolbar.tsx +159 -8
  38. package/src/components/story/detail/story-properties.tsx +21 -4
  39. package/src/components/story/display/__tests__/group-fields.test.ts +107 -0
  40. package/src/components/story/display/group-resolver.tsx +2 -2
  41. package/src/components/story/inline-editors/__tests__/dependency-type-select.test.tsx +155 -0
  42. package/src/components/story/inline-editors/__tests__/labels-editor.test.tsx +155 -0
  43. package/src/components/story/inline-editors/__tests__/priority-editor.test.tsx +173 -0
  44. package/src/components/story/inline-editors/__tests__/state-editor.test.tsx +174 -0
  45. package/src/components/story/inline-editors/dependency-type-select.tsx +180 -0
  46. package/src/components/story/inline-editors/index.ts +2 -1
  47. package/src/components/story/inline-editors/labels-editor.tsx +159 -3
  48. package/src/components/story/inline-editors/story-select-dialog.tsx +41 -63
  49. package/src/components/story/peek-panel.tsx +121 -21
  50. package/src/components/story/relations/__tests__/attachment-manage-dialog.test.tsx +171 -0
  51. package/src/components/story/relations/__tests__/knowledge-select-dialog.test.tsx +174 -0
  52. package/src/components/story/relations/__tests__/link-manage-dialog.test.tsx +93 -0
  53. package/src/components/story/relations/attachment-manage-dialog.tsx +490 -0
  54. package/src/components/story/relations/file-preview.tsx +173 -0
  55. package/src/components/story/relations/format.ts +120 -0
  56. package/src/components/story/relations/general-groups.ts +32 -0
  57. package/src/components/story/relations/knowledge-select-dialog.tsx +403 -0
  58. package/src/components/story/relations/link-manage-dialog.tsx +192 -0
  59. package/src/components/story/relations/story-ref-row.tsx +17 -27
  60. package/src/components/story/story-panel-header.tsx +25 -19
  61. package/src/components/story/story-toolbar.tsx +3 -3
  62. package/src/components/story/transfer-story-popover.tsx +2 -2
  63. package/src/components/story/types.ts +14 -0
  64. package/src/components/story/views/create-view-dialog.tsx +3 -3
  65. package/src/components/story/views/story-list/__tests__/group-header.test.tsx +219 -0
  66. package/src/components/story/views/story-list/group.tsx +1 -1
  67. package/src/components/story/views/story-list/inline-story-creator.tsx +2 -2
  68. package/src/components/story/views/story-list/row.tsx +1 -1
  69. package/src/components/workspace/create-workspace-dialog.tsx +41 -49
  70. package/src/lib/__tests__/project-display-id.test.ts +8 -5
  71. package/src/lib/changelog/sdk-versions.ts +20 -20
  72. package/src/lib/iteration/__tests__/status.test.ts +41 -0
  73. package/src/lib/iteration/status.ts +28 -0
  74. package/src/lib/project-display-id.ts +14 -5
  75. package/src/lib/project-module-meta.ts +77 -0
  76. package/src/lib/story/__tests__/video-embed.test.ts +77 -0
  77. package/src/lib/story/video-embed.ts +59 -0
  78. package/src/lib/utils.d.ts +1 -1
  79. package/src/plugins/.gen-manifest.json +13 -0
  80. package/src/plugins/modules.gen.ts +20 -0
  81. package/src/plugins/plugins.gen.ts +20 -0
  82. package/src/plugins/ui.gen.ts +8 -0
  83. package/src/providers/index.tsx +6 -0
  84. package/src/routeTree.gen.ts +96 -0
  85. package/src/routes/__root.tsx +0 -8
  86. package/src/routes/_nav/inspiration/route.tsx +8 -0
  87. package/src/routes/_nav/knowledge/index.lazy.tsx +2 -2
  88. package/src/routes/_nav/prd/$id/route.tsx +9 -0
  89. package/src/routes/_nav/prd/index.tsx +9 -0
  90. package/src/routes/_nav/prd/route.tsx +8 -0
  91. package/src/routes/_nav/projects/$projectId/$storyId/route.lazy.tsx +52 -25
  92. package/src/routes/_nav/projects/$projectId/$storyId/route.tsx +6 -3
  93. package/src/routes/_nav/projects/$projectId/index.lazy.tsx +32 -15
  94. package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.lazy.tsx +289 -0
  95. package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.tsx +27 -0
  96. package/src/routes/_nav/projects/$projectId/iterations/index.lazy.tsx +111 -0
  97. package/src/routes/_nav/projects/$projectId/iterations/index.tsx +24 -0
  98. package/src/routes/_nav/projects/$projectId/route.tsx +4 -2
  99. package/src/routes/_nav/projects/$projectId/settings/route.lazy.tsx +78 -7
  100. package/src/routes/_nav/projects/$projectId/settings/route.tsx +6 -2
  101. package/src/routes/_nav/stories/$storyId/route.lazy.tsx +5 -1
  102. package/src/routes/_nav/stories/$storyId/route.tsx +4 -0
  103. package/src/routes/_nav/tags/$id/route.tsx +8 -0
  104. package/src/routes/_nav/tags/index.tsx +8 -0
  105. package/src/routes/_nav/tags/route.tsx +8 -0
  106. package/src/routes/_nav/tags/settings/route.tsx +9 -0
  107. package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +3 -3
  108. package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +4 -4
  109. package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
  110. package/src/routes/api/attachments/$id/content.ts +60 -0
  111. package/src/routes/api/attachments/upload.ts +148 -0
  112. package/src/routes/share/prd/$prdId/route.tsx +10 -0
  113. package/src/server/serverFns/__tests__/project-kb.test.ts +147 -0
  114. package/src/server/serverFns/iteration/__tests__/auto-schedule-core.test.ts +62 -0
  115. package/src/server/serverFns/iteration/__tests__/burndown-core.test.ts +90 -0
  116. package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +211 -0
  117. package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +155 -0
  118. package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +136 -0
  119. package/src/server/serverFns/iteration/auto-schedule-core.ts +54 -0
  120. package/src/server/serverFns/iteration/auto-schedule-internal.ts +88 -0
  121. package/src/server/serverFns/iteration/burndown-core.ts +60 -0
  122. package/src/server/serverFns/iteration/index.ts +22 -0
  123. package/src/server/serverFns/iteration/iteration-auto-schedule.ts +66 -0
  124. package/src/server/serverFns/iteration/iteration-burndown.ts +70 -0
  125. package/src/server/serverFns/iteration/iteration-crud.ts +208 -0
  126. package/src/server/serverFns/iteration/iteration-stories.ts +111 -0
  127. package/src/server/serverFns/iteration/iteration-transfer.ts +42 -0
  128. package/src/server/serverFns/iteration/schemas.ts +58 -0
  129. package/src/server/serverFns/iteration/transfer-internal.ts +62 -0
  130. package/src/server/serverFns/knowledge.ts +50 -4
  131. package/src/server/serverFns/project/module-overview.ts +109 -0
  132. package/src/server/serverFns/project.ts +45 -2
  133. package/src/server/serverFns/story/__tests__/kb-scope.test.ts +56 -0
  134. package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +100 -0
  135. package/src/server/serverFns/story/__tests__/labels.test.ts +67 -0
  136. package/src/server/serverFns/story/__tests__/story-assoc.test.ts +222 -0
  137. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +132 -2
  138. package/src/server/serverFns/story/index.ts +9 -4
  139. package/src/server/serverFns/story/kb-general.ts +39 -0
  140. package/src/server/serverFns/story/kb-scope.ts +52 -0
  141. package/src/server/serverFns/story/link-preview.ts +115 -0
  142. package/src/server/serverFns/story/story-assoc.ts +372 -51
  143. package/src/server/serverFns/story/story-attachments.ts +32 -45
  144. package/src/{components/project/project-icon.tsx → server/serverFns/story/story-crud/label-palette.ts} +16 -19
  145. package/src/server/serverFns/story/story-crud/label-suggest-core.ts +88 -0
  146. package/src/server/serverFns/story/story-crud/label-suggest.ts +38 -0
  147. package/src/{components/knowledge/knowledge-icon.tsx → server/serverFns/story/story-crud/label-upsert.ts} +13 -19
  148. package/src/server/serverFns/story/story-crud/labels.ts +3 -5
  149. package/src/server/serverFns/story/story-crud.ts +53 -6
  150. package/src/server/serverFns/story/story-dependencies.ts +158 -5
  151. package/src/server/serverFns/story/story-links.ts +43 -7
  152. package/src/server/serverFns/story/story-transfer.ts +5 -4
  153. package/src/server/serverFns/story/story-tree.ts +28 -1
  154. package/src/server/serverFns/story/types.ts +38 -0
  155. package/src/styles/globals.css +25 -0
  156. package/src/types/project.ts +5 -5
  157. package/vite.shared.ts +2 -0
  158. package/src/components/project/project-animated-icon.tsx +0 -73
  159. package/src/components/shared/icon-packs/index.ts +0 -1
  160. package/src/components/shared/icon-packs/knowledge.ts +0 -1
  161. package/src/components/shared/icon-packs/prd.ts +0 -1
  162. package/src/components/shared/icon-packs/project.ts +0 -1
  163. package/src/components/shared/icon-picker.tsx +0 -1
  164. package/src/components/story/detail/story-detail-toolbar/attachments.tsx +0 -73
  165. package/src/components/story/detail/story-detail-toolbar/knowledge.tsx +0 -73
  166. package/src/components/story/detail/story-detail-toolbar/links.tsx +0 -73
  167. package/src/components/story/relations/attachments-panel.tsx +0 -179
  168. package/src/components/story/relations/knowledge-panel.tsx +0 -284
  169. package/src/components/story/relations/links-panel.tsx +0 -240
  170. package/src/lib/project-mock.ts +0 -156
@@ -0,0 +1,219 @@
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
+ import { TooltipProvider } from "@/components/ui/tooltip";
21
+ import { VirtualGroupHeader, GroupTailRow } from "../group";
22
+ import { FileText } from "lucide-react";
23
+
24
+ const renderWithProviders = (ui: React.ReactElement) =>
25
+ render(<TooltipProvider>{ui}</TooltipProvider>);
26
+
27
+ vi.mock("react-i18next", () => ({
28
+ useTranslation: () => ({ t: (key: string) => key }),
29
+ }));
30
+
31
+ vi.mock("../inline-story-creator", () => ({
32
+ InlineStoryCreator: ({ onSubmit }: any) => (
33
+ <button onClick={onSubmit} data-testid="inline-creator">inline-create</button>
34
+ ),
35
+ }));
36
+
37
+ const makeNode = (over?: any) => ({
38
+ key: "todo",
39
+ label: "待办",
40
+ color: "text-blue-500",
41
+ icon: FileText,
42
+ totalCount: 3,
43
+ items: [{ id: "s1" }],
44
+ subGroups: [],
45
+ ...over,
46
+ });
47
+
48
+ const baseHeaderProps = {
49
+ node: makeNode(),
50
+ depth: 0,
51
+ collapsed: false,
52
+ collapseKey: "todo",
53
+ onToggle: vi.fn(),
54
+ selectState: false,
55
+ onToggleSelect: vi.fn(),
56
+ showSelect: false,
57
+ showRoleLabel: false,
58
+ };
59
+
60
+ describe("VirtualGroupHeader", () => {
61
+ beforeEach(() => vi.clearAllMocks());
62
+
63
+ it("基础渲染时 node.label 文本可见", () => {
64
+ renderWithProviders(<VirtualGroupHeader {...baseHeaderProps} />);
65
+ expect(screen.getByText("待办")).toBeTruthy();
66
+ });
67
+
68
+ it("collapsed=true 时显示 ChevronRight 图标", () => {
69
+ renderWithProviders(<VirtualGroupHeader {...baseHeaderProps} collapsed />);
70
+ const paths = document.querySelectorAll("svg path");
71
+ expect(Array.from(paths).some((p) => p.getAttribute("d") === "m9 18 6-6-6-6")).toBe(true);
72
+ expect(Array.from(paths).some((p) => p.getAttribute("d") === "m6 9 6 6 6-6")).toBe(false);
73
+ });
74
+
75
+ it("collapsed=false 时显示 ChevronDown 图标", () => {
76
+ renderWithProviders(<VirtualGroupHeader {...baseHeaderProps} collapsed={false} />);
77
+ const paths = document.querySelectorAll("svg path");
78
+ expect(Array.from(paths).some((p) => p.getAttribute("d") === "m6 9 6 6 6-6")).toBe(true);
79
+ expect(Array.from(paths).some((p) => p.getAttribute("d") === "m9 18 6-6-6-6")).toBe(false);
80
+ });
81
+
82
+ it("showSelect=true 时渲染 Checkbox", () => {
83
+ renderWithProviders(
84
+ <VirtualGroupHeader {...baseHeaderProps} showSelect selectState={false} />,
85
+ );
86
+ const checkbox = screen.getByRole("checkbox", {
87
+ name: /group.selectAll/i,
88
+ });
89
+ expect(checkbox).toBeTruthy();
90
+ });
91
+
92
+ it("showRoleLabel=true + groupDimension=assignee 时显示 roleLabel", () => {
93
+ renderWithProviders(
94
+ <VirtualGroupHeader
95
+ {...baseHeaderProps}
96
+ showRoleLabel
97
+ groupDimension="assignee"
98
+ />,
99
+ );
100
+ expect(screen.getByText("group.assignee")).toBeTruthy();
101
+ });
102
+
103
+ it("showRoleLabel=true + groupDimension=createdBy 时显示 roleLabel", () => {
104
+ renderWithProviders(
105
+ <VirtualGroupHeader
106
+ {...baseHeaderProps}
107
+ showRoleLabel
108
+ groupDimension="createdBy"
109
+ />,
110
+ );
111
+ expect(screen.getByText("group.createdBy")).toBeTruthy();
112
+ });
113
+
114
+ it("showRoleLabel=true 但 groupDimension 不匹配时不显示 roleLabel", () => {
115
+ renderWithProviders(
116
+ <VirtualGroupHeader
117
+ {...baseHeaderProps}
118
+ showRoleLabel
119
+ groupDimension="state"
120
+ />,
121
+ );
122
+ expect(screen.queryByText("group.assignee")).toBeFalsy();
123
+ expect(screen.queryByText("group.createdBy")).toBeFalsy();
124
+ });
125
+
126
+ it("depth=0 + onCreate 存在时渲染 Plus 按钮", () => {
127
+ const onCreate = vi.fn();
128
+ renderWithProviders(
129
+ <VirtualGroupHeader {...baseHeaderProps} depth={0} onCreate={onCreate} />,
130
+ );
131
+ fireEvent.click(screen.getByLabelText(/group.addTo/i));
132
+ expect(onCreate).toHaveBeenCalledTimes(1);
133
+ });
134
+
135
+ it("depth>0 时不渲染 Plus 按钮", () => {
136
+ const onCreate = vi.fn();
137
+ renderWithProviders(
138
+ <VirtualGroupHeader
139
+ {...baseHeaderProps}
140
+ depth={1}
141
+ onCreate={onCreate}
142
+ />,
143
+ );
144
+ expect(screen.queryByLabelText(/group.addTo/i)).toBeFalsy();
145
+ });
146
+
147
+ it("onToggle 点击时传递正确的 collapseKey", () => {
148
+ const onToggle = vi.fn();
149
+ renderWithProviders(
150
+ <VirtualGroupHeader
151
+ {...baseHeaderProps}
152
+ collapseKey="custom-key"
153
+ onToggle={onToggle}
154
+ />,
155
+ );
156
+ fireEvent.click(screen.getByText("待办").parentElement!);
157
+ expect(onToggle).toHaveBeenCalledWith("custom-key");
158
+ });
159
+
160
+ it("display totalCount 时显示总数", () => {
161
+ renderWithProviders(<VirtualGroupHeader {...baseHeaderProps} />);
162
+ expect(screen.getByText("3")).toBeTruthy();
163
+ });
164
+ });
165
+
166
+ describe("GroupTailRow", () => {
167
+ beforeEach(() => vi.clearAllMocks());
168
+
169
+ const baseTailProps = {
170
+ rowKey: "row-1",
171
+ hasMore: false,
172
+ loading: false,
173
+ groupBy: "state",
174
+ subGroupBy: "priority",
175
+ groupKey: "todo",
176
+ setTailHeight: vi.fn(),
177
+ };
178
+
179
+ it("hasMore=false 时只显示「新建需求」按钮", () => {
180
+ renderWithProviders(<GroupTailRow {...baseTailProps} hasMore={false} />);
181
+ expect(screen.getByText("group.add")).toBeTruthy();
182
+ expect(screen.queryByText("group.loadMore")).toBeFalsy();
183
+ });
184
+
185
+ it("hasMore=true 时显示「加载更多」按钮 +「新建需求」按钮", () => {
186
+ renderWithProviders(<GroupTailRow {...baseTailProps} hasMore={true} />);
187
+ expect(screen.getByText("group.loadMore")).toBeTruthy();
188
+ expect(screen.getByText("group.add")).toBeTruthy();
189
+ });
190
+
191
+ it("loading=true 时「加载更多」按钮 disabled", () => {
192
+ renderWithProviders(<GroupTailRow {...baseTailProps} hasMore={true} loading />);
193
+ const loadMoreBtn = screen.getByText("group.loading")
194
+ .closest("button")!;
195
+ expect(loadMoreBtn).toBeDisabled();
196
+ });
197
+
198
+ it("showEditor 初始状态不渲染 InlineStoryCreator", async () => {
199
+ renderWithProviders(<GroupTailRow {...baseTailProps} />);
200
+ await waitFor(() => {
201
+ expect(screen.queryByTestId("inline-creator")).toBeFalsy();
202
+ });
203
+ });
204
+
205
+ it("点击「新建需求」按钮后渲染 InlineStoryCreator", () => {
206
+ renderWithProviders(<GroupTailRow {...baseTailProps} />);
207
+ fireEvent.click(screen.getByText("group.add"));
208
+ expect(screen.getByTestId("inline-creator")).toBeTruthy();
209
+ });
210
+
211
+ it("点击「加载更多」按钮触发 onLoadMore", () => {
212
+ const onLoadMore = vi.fn();
213
+ renderWithProviders(
214
+ <GroupTailRow {...baseTailProps} hasMore={true} onLoadMore={onLoadMore} />,
215
+ );
216
+ fireEvent.click(screen.getByText("group.loadMore"));
217
+ expect(onLoadMore).toHaveBeenCalledTimes(1);
218
+ });
219
+ });
@@ -72,7 +72,7 @@ export const VirtualGroupHeader = memo(function VirtualGroupHeader({ node, depth
72
72
  <button
73
73
  type="button"
74
74
  onClick={() => onToggle(collapseKey)}
75
- className="flex h-full flex-1 items-center gap-2 px-2 text-sm font-medium hover:brightness-95"
75
+ className="flex h-full flex-1 select-none items-center gap-2 px-2 text-sm font-medium hover:brightness-95"
76
76
  >
77
77
  <span className="flex items-center gap-1.5">
78
78
  {Icon && <Icon className={cn(iconSize, textColor)} />}
@@ -18,7 +18,7 @@
18
18
  import { memo, useEffect, useRef, useMemo } from "react";
19
19
  import { useTranslation } from "react-i18next";
20
20
  import { Loader2, CalendarCheck, CalendarPlus2, Check, X, Trees } from "lucide-react";
21
- import { ProjectIconDisplay } from "@/components/project/project-icon";
21
+ import { LucideIconByName } from "@/components/shared/lucide-icon-picker/lucide-icon-by-name";
22
22
  import type { UserBrief } from "../../types";
23
23
  import type { StoryState, StoryPriority } from "../../constants";
24
24
  import { PriorityEditor, StateEditor, AssigneeEditor, DateEditor, LabelsEditor } from "../../inline-editors";
@@ -65,7 +65,7 @@ export const InlineStoryCreator = memo(function InlineStoryCreator({ name, setNa
65
65
  <div className="flex h-14 items-center gap-2 border-b border-border/40 bg-card px-6">
66
66
  {projectContext ? (
67
67
  <div className="flex items-center gap-2 shrink-0 text-xs text-muted-foreground">
68
- <ProjectIconDisplay name={projectContext.icon} size={14} />
68
+ <LucideIconByName name={projectContext.icon} size={14} />
69
69
  <span className="font-mono">{projectContext.displayId}</span>
70
70
  </div>
71
71
  ) : (
@@ -125,7 +125,7 @@ export const StoryListRow = memo(function StoryListRow({ story, group, indent, i
125
125
  <DateEditor value={story.targetDate} onChange={setTargetDate} label={t("row.dueDate")} icon={CalendarCheck} overdue={overdue} />
126
126
  )}
127
127
  {properties.includes("labels") && (
128
- <LabelsEditor labelIds={currentLabelIds} labels={labels} onChange={setLabels} variant="button" />
128
+ <LabelsEditor labelIds={currentLabelIds} labels={labels} onChange={setLabels} variant="button" aiContext={{ name: () => story.name }} />
129
129
  )}
130
130
  </div>
131
131
  );
@@ -21,17 +21,14 @@ import { useForm } from "@tanstack/react-form";
21
21
  import { Link, useRouter } from "@tanstack/react-router";
22
22
  import { Button } from "@/components/ui/button";
23
23
  import { Input } from "@/components/ui/input";
24
- import { Textarea } from "@/components/ui/textarea";
25
24
  import {
26
25
  Dialog,
27
26
  DialogContent,
28
- DialogDescription,
29
27
  DialogFooter,
30
28
  DialogHeader,
31
29
  DialogTitle,
32
30
  } from "@/components/ui/dialog";
33
- import { IconPicker } from "@/components/shared/icon-picker";
34
- import { PROJECT_ICONS } from "@/components/shared/icon-packs/project";
31
+ import { LucideIconPicker } from "@/components/shared/lucide-icon-picker";
35
32
  import { createWorkspaceFn } from "@/server/serverFns/workspace";
36
33
  import { createWorkspaceSchema, type CreateWorkspaceValues } from "./schemas/create-workspace";
37
34
 
@@ -56,7 +53,7 @@ export function CreateWorkspaceDialog({
56
53
  defaultValues: {
57
54
  name: "",
58
55
  description: "",
59
- icon: "folder-kanban",
56
+ icon: "",
60
57
  },
61
58
  validators: [{ run: createWorkspaceSchema as any, triggers: ['change'] }],
62
59
  });
@@ -98,7 +95,7 @@ export function CreateWorkspaceDialog({
98
95
 
99
96
  const handleOpenChange = (newOpen: boolean) => {
100
97
  if (newOpen) {
101
- form.setFieldValue("icon", "folder-kanban");
98
+ form.setFieldValue("icon", "");
102
99
  form.setFieldValue("name", "");
103
100
  form.setFieldValue("description", "");
104
101
  setError("");
@@ -109,13 +106,12 @@ export function CreateWorkspaceDialog({
109
106
 
110
107
  return (
111
108
  <Dialog open={open} onOpenChange={handleOpenChange}>
112
- <DialogContent className="sm:max-w-200">
113
- <DialogHeader className="p-4">
114
- <DialogTitle>{t('createWorkspace.title')}</DialogTitle>
115
- <DialogDescription>{t('createWorkspace.description')}</DialogDescription>
109
+ <DialogContent className="sm:max-w-260 -mt-50">
110
+ <DialogHeader>
111
+ <DialogTitle className="text-xl">{t('createWorkspace.title')}</DialogTitle>
116
112
  </DialogHeader>
117
113
 
118
- <div className="space-y-4 p-4">
114
+ <div className="space-y-3 overflow-y-auto px-4 py-2 max-h-[60vh]">
119
115
  {showUpgrade ? (
120
116
  <div className="flex flex-col items-center gap-4 py-8 text-center">
121
117
  <p className="text font-medium">{t('createWorkspace.upgradeTitle')}</p>
@@ -130,49 +126,45 @@ export function CreateWorkspaceDialog({
130
126
  </div>
131
127
  ) : (
132
128
  <>
133
- <div className="flex flex-col gap-3 mb-20">
134
- <form.Field name="icon" children={(field) => (
135
- <IconPicker
136
- icons={PROJECT_ICONS}
137
- value={field.value ?? "folder-kanban"}
138
- onChange={(v) => field.handleChange(v)}
139
- />
140
- )} />
141
- </div>
129
+ <div className="flex gap-2">
130
+ <form.Field name="icon" children={(field) => (
131
+ <LucideIconPicker
132
+ value={field.value}
133
+ onChange={(v) => field.handleChange(v)}
134
+ modal
135
+ />
136
+ )} />
137
+ <form.Field name="name" children={(field) => (
138
+ <Input
139
+ value={field.value ?? ""}
140
+ onChange={(e) => field.handleChange(e.target.value)}
141
+ onBlur={field.handleBlur}
142
+ placeholder={t('createWorkspace.namePlaceholder')}
143
+ maxLength={100}
144
+ className="h-10 flex-1"
145
+ />
146
+ )} />
147
+ </div>
142
148
 
143
- <div className="flex flex-col gap-3">
144
- <label className="text-sm font-medium ml-2">{t('createWorkspace.nameLabel')}</label>
145
- <form.Field name="name" children={(field) => (
146
- <Input
147
- value={field.value ?? ""}
148
- onChange={(e) => field.handleChange(e.target.value)}
149
- onBlur={field.handleBlur}
150
- placeholder={t('createWorkspace.namePlaceholder')}
151
- maxLength={100}
152
- />
153
- )} />
154
- </div>
149
+ <form.Field name="description" children={(field) => (
150
+ <div className="notion-full-width min-h-50 max-h-150 flex flex-col overflow-y-auto border rounded-md border-border">
151
+ <textarea
152
+ value={field.value ?? ""}
153
+ onChange={(e) => field.handleChange(e.target.value)}
154
+ onBlur={field.handleBlur}
155
+ placeholder={t('createWorkspace.descriptionPlaceholder')}
156
+ maxLength={500}
157
+ className="flex-1 min-h-0 resize-none bg-transparent px-3 py-2 text-sm outline-none placeholder:text-muted-foreground"
158
+ />
159
+ </div>
160
+ )} />
155
161
 
156
- <div className="space-y-2">
157
- <label className="text-sm font-medium ml-2">{t('createWorkspace.descriptionLabel')}</label>
158
- <form.Field name="description" children={(field) => (
159
- <Textarea
160
- value={field.value ?? ""}
161
- onChange={(e) => field.handleChange(e.target.value)}
162
- onBlur={field.handleBlur}
163
- placeholder={t('createWorkspace.descriptionPlaceholder')}
164
- maxLength={500}
165
- rows={3}
166
- />
167
- )} />
168
- </div>
169
-
170
- {error && <p className="text-sm text-destructive break-words">{error}</p>}
162
+ {error && <p className="text-sm text-destructive break-words">{error}</p>}
171
163
  </>
172
164
  )}
173
165
  </div>
174
166
 
175
- <DialogFooter>
167
+ <DialogFooter className="items-center gap-3">
176
168
  <Button variant="outline" onClick={() => handleOpenChange(false)}>
177
169
  {t('createWorkspace.cancel')}
178
170
  </Button>
@@ -183,4 +175,4 @@ export function CreateWorkspaceDialog({
183
175
  </DialogContent>
184
176
  </Dialog>
185
177
  );
186
- }
178
+ }
@@ -22,8 +22,14 @@ describe('extractDisplayId', () => {
22
22
  it('提取纯字母数字并转大写', () => {
23
23
  expect(extractDisplayId('HelloWorld')).toBe('HELLOWORLD')
24
24
  })
25
- it('混合中英文取字母部分', () => {
26
- expect(extractDisplayId('Hello世界')).toBe('HELLO')
25
+ it('混合中英文:汉字转拼音首字母', () => {
26
+ expect(extractDisplayId('Hello世界')).toBe('HELLOSJ')
27
+ })
28
+ it('纯中文转拼音首字母', () => {
29
+ expect(extractDisplayId('我的项目')).toBe('WDXM')
30
+ })
31
+ it('中英混合完整示例', () => {
32
+ expect(extractDisplayId('产品需求PRD')).toBe('CPXQPRD')
27
33
  })
28
34
  it('截断到10位', () => {
29
35
  expect(extractDisplayId('HelloWorld12345')).toBe('HELLOWORLD')
@@ -31,9 +37,6 @@ describe('extractDisplayId', () => {
31
37
  it('包含数字', () => {
32
38
  expect(extractDisplayId('Project 2024')).toBe('PROJECT202')
33
39
  })
34
- it('纯中文返回空字符串', () => {
35
- expect(extractDisplayId('我的项目')).toBe('')
36
- })
37
40
  it('空字符串', () => {
38
41
  expect(extractDisplayId('')).toBe('')
39
42
  })
@@ -10,7 +10,7 @@ export const SDK_GROUPS: SdkGroup[] = [
10
10
  "name": "@tanstack/react-start",
11
11
  "displayName": "TanStack Start",
12
12
  "version": "v1.168.49",
13
- "lastUpdate": "Aug 31, 2026",
13
+ "lastUpdate": "Sep 3, 2026",
14
14
  "source": "apps/web/package.json#dependencies",
15
15
  "docsUrl": "https://tanstack.com/start/latest"
16
16
  },
@@ -18,7 +18,7 @@ export const SDK_GROUPS: SdkGroup[] = [
18
18
  "name": "@tanstack/react-router",
19
19
  "displayName": "TanStack Router",
20
20
  "version": "v1.170.32",
21
- "lastUpdate": "Aug 31, 2026",
21
+ "lastUpdate": "Sep 3, 2026",
22
22
  "source": "apps/web/package.json#dependencies",
23
23
  "docsUrl": "https://tanstack.com/router/latest"
24
24
  },
@@ -26,7 +26,7 @@ export const SDK_GROUPS: SdkGroup[] = [
26
26
  "name": "@tanstack/react-query",
27
27
  "displayName": "TanStack Query",
28
28
  "version": "v5.102.8",
29
- "lastUpdate": "Aug 31, 2026",
29
+ "lastUpdate": "Sep 3, 2026",
30
30
  "source": "apps/web/package.json#dependencies",
31
31
  "docsUrl": "https://tanstack.com/query/latest"
32
32
  },
@@ -34,7 +34,7 @@ export const SDK_GROUPS: SdkGroup[] = [
34
34
  "name": "@tanstack/ai",
35
35
  "displayName": "TanStack AI",
36
36
  "version": "v0.52.0",
37
- "lastUpdate": "Aug 31, 2026",
37
+ "lastUpdate": "Sep 3, 2026",
38
38
  "source": "apps/web/package.json#dependencies",
39
39
  "docsUrl": "https://tanstack.com/ai/latest"
40
40
  },
@@ -42,7 +42,7 @@ export const SDK_GROUPS: SdkGroup[] = [
42
42
  "name": "@tanstack/react-table",
43
43
  "displayName": "TanStack Table",
44
44
  "version": "v9.2.3",
45
- "lastUpdate": "Aug 31, 2026",
45
+ "lastUpdate": "Sep 3, 2026",
46
46
  "source": "apps/web/package.json#dependencies",
47
47
  "docsUrl": "https://tanstack.com/table/latest"
48
48
  },
@@ -50,7 +50,7 @@ export const SDK_GROUPS: SdkGroup[] = [
50
50
  "name": "@tanstack/react-form",
51
51
  "displayName": "TanStack Form",
52
52
  "version": "v2.0.0-alpha.2",
53
- "lastUpdate": "Aug 31, 2026",
53
+ "lastUpdate": "Sep 3, 2026",
54
54
  "source": "apps/web/package.json#dependencies",
55
55
  "docsUrl": "https://tanstack.com/form/latest"
56
56
  },
@@ -58,7 +58,7 @@ export const SDK_GROUPS: SdkGroup[] = [
58
58
  "name": "@tanstack/charts",
59
59
  "displayName": "TanStack Charts",
60
60
  "version": "v0.16.0",
61
- "lastUpdate": "Aug 31, 2026",
61
+ "lastUpdate": "Sep 3, 2026",
62
62
  "source": "apps/admin/package.json#dependencies",
63
63
  "docsUrl": "https://tanstack.com/charts/latest"
64
64
  },
@@ -66,7 +66,7 @@ export const SDK_GROUPS: SdkGroup[] = [
66
66
  "name": "@tanstack/react-pacer",
67
67
  "displayName": "TanStack Pacer",
68
68
  "version": "v0.23.0",
69
- "lastUpdate": "Aug 31, 2026",
69
+ "lastUpdate": "Sep 3, 2026",
70
70
  "source": "apps/web/package.json#dependencies",
71
71
  "docsUrl": "https://tanstack.com/pacer/latest"
72
72
  },
@@ -74,7 +74,7 @@ export const SDK_GROUPS: SdkGroup[] = [
74
74
  "name": "@tanstack/react-hotkeys",
75
75
  "displayName": "TanStack Hotkeys",
76
76
  "version": "v0.10.0",
77
- "lastUpdate": "Aug 31, 2026",
77
+ "lastUpdate": "Sep 3, 2026",
78
78
  "source": "apps/web/package.json#dependencies",
79
79
  "docsUrl": "https://tanstack.com/hotkeys/latest"
80
80
  },
@@ -82,7 +82,7 @@ export const SDK_GROUPS: SdkGroup[] = [
82
82
  "name": "@tanstack/react-virtual",
83
83
  "displayName": "TanStack Virtual",
84
84
  "version": "v3.14.10",
85
- "lastUpdate": "Aug 31, 2026",
85
+ "lastUpdate": "Sep 3, 2026",
86
86
  "source": "apps/admin/package.json#dependencies",
87
87
  "docsUrl": "https://tanstack.com/virtual/latest"
88
88
  }
@@ -95,15 +95,15 @@ export const SDK_GROUPS: SdkGroup[] = [
95
95
  "items": [
96
96
  {
97
97
  "name": "@allbluecn/web-app",
98
- "version": "v0.5.0",
99
- "lastUpdate": "Aug 31, 2026",
98
+ "version": "v0.6.0",
99
+ "lastUpdate": "Sep 3, 2026",
100
100
  "source": "apps/web/package.json",
101
101
  "docsUrl": "https://github.com/allbluecn/allblue"
102
102
  },
103
103
  {
104
104
  "name": "@allbluecn/kernel",
105
105
  "version": "v0.3.1",
106
- "lastUpdate": "Aug 31, 2026",
106
+ "lastUpdate": "Sep 3, 2026",
107
107
  "source": "packages/kernel/package.json",
108
108
  "docsUrl": "https://github.com/allbluecn/allblue"
109
109
  }
@@ -117,42 +117,42 @@ export const SDK_GROUPS: SdkGroup[] = [
117
117
  {
118
118
  "name": "Vite",
119
119
  "version": "v8.1.5",
120
- "lastUpdate": "Aug 31, 2026",
120
+ "lastUpdate": "Sep 3, 2026",
121
121
  "source": "apps/web/package.json#devDependencies",
122
122
  "docsUrl": "https://vite.dev/"
123
123
  },
124
124
  {
125
125
  "name": "Node.js",
126
126
  "version": "v24.19.0",
127
- "lastUpdate": "Aug 31, 2026",
127
+ "lastUpdate": "Sep 3, 2026",
128
128
  "source": ".nvmrc",
129
129
  "docsUrl": "https://nodejs.org/"
130
130
  },
131
131
  {
132
132
  "name": "Prisma",
133
133
  "version": "v7.8.0",
134
- "lastUpdate": "Aug 31, 2026",
134
+ "lastUpdate": "Sep 3, 2026",
135
135
  "source": "packages/database/package.json#devDependencies",
136
136
  "docsUrl": "https://www.prisma.io/docs"
137
137
  },
138
138
  {
139
139
  "name": "shadcn UI",
140
140
  "version": "v1.2.4",
141
- "lastUpdate": "Aug 31, 2026",
141
+ "lastUpdate": "Sep 3, 2026",
142
142
  "source": "apps/web/package.json#dependencies",
143
143
  "docsUrl": "https://ui.shadcn.com/"
144
144
  },
145
145
  {
146
146
  "name": "TipTap",
147
147
  "version": "v3.27.4",
148
- "lastUpdate": "Aug 31, 2026",
148
+ "lastUpdate": "Sep 3, 2026",
149
149
  "source": "packages/shared/package.json#devDependencies",
150
150
  "docsUrl": "https://tiptap.dev/"
151
151
  },
152
152
  {
153
153
  "name": "Lucide React",
154
- "version": "v1.24.0",
155
- "lastUpdate": "Aug 31, 2026",
154
+ "version": "v1.27.0",
155
+ "lastUpdate": "Sep 3, 2026",
156
156
  "source": "packages/ui/package.json#dependencies",
157
157
  "docsUrl": "https://lucide.dev/"
158
158
  }
@@ -0,0 +1,41 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { deriveIterationStatus, isDateRangeOverlapping, ITERATION_STATUSES } from "../status";
3
+
4
+ describe("deriveIterationStatus", () => {
5
+ it("今天早于开始日期 → upcoming", () => {
6
+ expect(deriveIterationStatus("2026-09-10", "2026-09-23", new Date("2026-09-01T22:00:00Z"))).toBe("upcoming");
7
+ });
8
+ it("startDate 当天 → active(含当天)", () => {
9
+ expect(deriveIterationStatus("2026-09-01", "2026-09-14", new Date("2026-09-01T15:00:00Z"))).toBe("active");
10
+ });
11
+ it("endDate 当天 → 仍 active(当天不判完成)", () => {
12
+ expect(deriveIterationStatus("2026-08-19", "2026-09-01", new Date("2026-09-01T23:59:59Z"))).toBe("active");
13
+ });
14
+ it("endDate 次日 → completed", () => {
15
+ expect(deriveIterationStatus("2026-08-19", "2026-09-01", new Date("2026-09-02T00:00:00Z"))).toBe("completed");
16
+ });
17
+ it("Date 对象入参(序列化后的 ISO 字符串)同样生效", () => {
18
+ expect(deriveIterationStatus(new Date("2026-09-01T00:00:00Z").toISOString(), "2026-09-14", new Date("2026-09-02T00:00:00Z"))).toBe("active");
19
+ });
20
+ });
21
+
22
+ describe("isDateRangeOverlapping", () => {
23
+ it("相交", () => {
24
+ expect(isDateRangeOverlapping({ startDate: "2026-09-01", endDate: "2026-09-14" }, { startDate: "2026-09-10", endDate: "2026-09-23" })).toBe(true);
25
+ });
26
+ it("包含", () => {
27
+ expect(isDateRangeOverlapping({ startDate: "2026-09-01", endDate: "2026-09-30" }, { startDate: "2026-09-10", endDate: "2026-09-14" })).toBe(true);
28
+ });
29
+ it("首尾相接不算重叠(endDate == startDate)", () => {
30
+ expect(isDateRangeOverlapping({ startDate: "2026-09-01", endDate: "2026-09-14" }, { startDate: "2026-09-14", endDate: "2026-09-27" })).toBe(false);
31
+ });
32
+ it("相离", () => {
33
+ expect(isDateRangeOverlapping({ startDate: "2026-09-01", endDate: "2026-09-14" }, { startDate: "2026-09-15", endDate: "2026-09-28" })).toBe(false);
34
+ });
35
+ });
36
+
37
+ describe("ITERATION_STATUSES", () => {
38
+ it("三种状态", () => {
39
+ expect(ITERATION_STATUSES).toEqual(["upcoming", "active", "completed"]);
40
+ });
41
+ });
@@ -0,0 +1,28 @@
1
+ export type IterationStatus = "upcoming" | "active" | "completed";
2
+
3
+ export const ITERATION_STATUSES: IterationStatus[] = ["upcoming", "active", "completed"];
4
+
5
+ /** 取日期部分(yyyy-MM-dd),丢弃时分秒,兼容 Date / ISO 字符串 */
6
+ export function toDatePart(d: Date | string): string {
7
+ return (typeof d === "string" ? d : d.toISOString()).slice(0, 10);
8
+ }
9
+
10
+ /** 迭代状态由日期动态推导(规格 §3.1:endDate 当天仍 active,次日起 completed) */
11
+ export function deriveIterationStatus(
12
+ startDate: string | Date,
13
+ endDate: string | Date,
14
+ now: Date = new Date(),
15
+ ): IterationStatus {
16
+ const today = toDatePart(now);
17
+ if (today < toDatePart(startDate)) return "upcoming";
18
+ if (today > toDatePart(endDate)) return "completed";
19
+ return "active";
20
+ }
21
+
22
+ /** 日期区间重叠判断(首尾相接不算重叠 → 用严格不等号) */
23
+ export function isDateRangeOverlapping(
24
+ a: { startDate: string | Date; endDate: string | Date },
25
+ b: { startDate: string | Date; endDate: string | Date },
26
+ ): boolean {
27
+ return toDatePart(a.startDate) < toDatePart(b.endDate) && toDatePart(b.startDate) < toDatePart(a.endDate);
28
+ }