@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.
- package/package.json +239 -30
- package/src/components/iteration/auto-schedule-dialog.tsx +150 -0
- package/src/components/iteration/burndown-chart.tsx +63 -0
- package/src/components/iteration/constants.ts +28 -0
- package/src/components/iteration/create-iteration-dialog.tsx +140 -0
- package/src/components/iteration/iteration-card.tsx +60 -0
- package/src/components/iteration/iteration-editor.tsx +116 -0
- package/src/components/iteration/iteration-progress-bar.tsx +44 -0
- package/src/components/iteration/iteration-status-badge.tsx +33 -0
- package/src/components/knowledge/dialog/knowledge-form-dialog.tsx +21 -30
- package/src/components/knowledge/knowledge-toolbar.tsx +124 -1
- package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +2 -2
- package/src/components/layout/sidebar-03/nav-workspace.tsx +2 -2
- package/src/components/project/create-project-dialog.tsx +44 -57
- package/src/components/shared/__tests__/lucide-curated-icons.test.ts +83 -0
- package/src/components/shared/__tests__/lucide-icon-data.test.ts +131 -0
- package/src/components/shared/__tests__/lucide-icon-picker.test.tsx +197 -0
- package/src/components/shared/lucide-icon-picker/curated-icons.ts +478 -0
- package/src/components/shared/lucide-icon-picker/icon-data.ts +109 -0
- package/src/components/shared/lucide-icon-picker/index.tsx +82 -0
- package/src/components/shared/lucide-icon-picker/lucide-icon-by-name.tsx +78 -0
- package/src/components/shared/lucide-icon-picker/panel.tsx +163 -0
- package/src/components/story/__tests__/constants.test.ts +255 -0
- package/src/components/story/ai/story-ai-panel.tsx +23 -15
- package/src/components/story/ai/story-ai-preview-list.tsx +19 -0
- package/src/components/story/constants.ts +43 -1
- package/src/components/story/create-story-dialog.tsx +42 -16
- package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +114 -0
- package/src/components/story/detail/__tests__/categories.test.tsx +62 -0
- package/src/components/story/detail/__tests__/dependencies-button.test.tsx +49 -0
- package/src/components/story/detail/activity-timeline.tsx +26 -4
- package/src/components/story/detail/story-detail-body.tsx +90 -38
- package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +857 -0
- package/src/components/story/detail/story-detail-toolbar/categories.tsx +23 -18
- package/src/components/story/detail/story-detail-toolbar/children.tsx +9 -4
- package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +95 -27
- package/src/components/story/detail/story-detail-toolbar.tsx +159 -8
- package/src/components/story/detail/story-properties.tsx +21 -4
- package/src/components/story/display/__tests__/group-fields.test.ts +107 -0
- package/src/components/story/display/group-resolver.tsx +2 -2
- package/src/components/story/inline-editors/__tests__/dependency-type-select.test.tsx +155 -0
- package/src/components/story/inline-editors/__tests__/labels-editor.test.tsx +155 -0
- package/src/components/story/inline-editors/__tests__/priority-editor.test.tsx +173 -0
- package/src/components/story/inline-editors/__tests__/state-editor.test.tsx +174 -0
- package/src/components/story/inline-editors/dependency-type-select.tsx +180 -0
- package/src/components/story/inline-editors/index.ts +2 -1
- package/src/components/story/inline-editors/labels-editor.tsx +159 -3
- package/src/components/story/inline-editors/story-select-dialog.tsx +41 -63
- package/src/components/story/peek-panel.tsx +121 -21
- package/src/components/story/relations/__tests__/attachment-manage-dialog.test.tsx +171 -0
- package/src/components/story/relations/__tests__/knowledge-select-dialog.test.tsx +174 -0
- package/src/components/story/relations/__tests__/link-manage-dialog.test.tsx +93 -0
- package/src/components/story/relations/attachment-manage-dialog.tsx +490 -0
- package/src/components/story/relations/file-preview.tsx +173 -0
- package/src/components/story/relations/format.ts +120 -0
- package/src/components/story/relations/general-groups.ts +32 -0
- package/src/components/story/relations/knowledge-select-dialog.tsx +403 -0
- package/src/components/story/relations/link-manage-dialog.tsx +192 -0
- package/src/components/story/relations/story-ref-row.tsx +17 -27
- package/src/components/story/story-panel-header.tsx +25 -19
- package/src/components/story/story-toolbar.tsx +3 -3
- package/src/components/story/transfer-story-popover.tsx +2 -2
- package/src/components/story/types.ts +14 -0
- package/src/components/story/views/create-view-dialog.tsx +3 -3
- package/src/components/story/views/story-list/__tests__/group-header.test.tsx +219 -0
- package/src/components/story/views/story-list/group.tsx +1 -1
- package/src/components/story/views/story-list/inline-story-creator.tsx +2 -2
- package/src/components/story/views/story-list/row.tsx +1 -1
- package/src/components/workspace/create-workspace-dialog.tsx +41 -49
- package/src/lib/__tests__/project-display-id.test.ts +8 -5
- package/src/lib/changelog/sdk-versions.ts +20 -20
- package/src/lib/iteration/__tests__/status.test.ts +41 -0
- package/src/lib/iteration/status.ts +28 -0
- package/src/lib/project-display-id.ts +14 -5
- package/src/lib/project-module-meta.ts +77 -0
- package/src/lib/story/__tests__/video-embed.test.ts +77 -0
- package/src/lib/story/video-embed.ts +59 -0
- package/src/lib/utils.d.ts +1 -1
- package/src/plugins/.gen-manifest.json +13 -0
- package/src/plugins/modules.gen.ts +20 -0
- package/src/plugins/plugins.gen.ts +20 -0
- package/src/plugins/ui.gen.ts +8 -0
- package/src/providers/index.tsx +6 -0
- package/src/routeTree.gen.ts +96 -0
- package/src/routes/__root.tsx +0 -8
- package/src/routes/_nav/inspiration/route.tsx +8 -0
- package/src/routes/_nav/knowledge/index.lazy.tsx +2 -2
- package/src/routes/_nav/prd/$id/route.tsx +9 -0
- package/src/routes/_nav/prd/index.tsx +9 -0
- package/src/routes/_nav/prd/route.tsx +8 -0
- package/src/routes/_nav/projects/$projectId/$storyId/route.lazy.tsx +52 -25
- package/src/routes/_nav/projects/$projectId/$storyId/route.tsx +6 -3
- package/src/routes/_nav/projects/$projectId/index.lazy.tsx +32 -15
- package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.lazy.tsx +289 -0
- package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.tsx +27 -0
- package/src/routes/_nav/projects/$projectId/iterations/index.lazy.tsx +111 -0
- package/src/routes/_nav/projects/$projectId/iterations/index.tsx +24 -0
- package/src/routes/_nav/projects/$projectId/route.tsx +4 -2
- package/src/routes/_nav/projects/$projectId/settings/route.lazy.tsx +78 -7
- package/src/routes/_nav/projects/$projectId/settings/route.tsx +6 -2
- package/src/routes/_nav/stories/$storyId/route.lazy.tsx +5 -1
- package/src/routes/_nav/stories/$storyId/route.tsx +4 -0
- package/src/routes/_nav/tags/$id/route.tsx +8 -0
- package/src/routes/_nav/tags/index.tsx +8 -0
- package/src/routes/_nav/tags/route.tsx +8 -0
- package/src/routes/_nav/tags/settings/route.tsx +9 -0
- package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +3 -3
- package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +4 -4
- package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
- package/src/routes/api/attachments/$id/content.ts +60 -0
- package/src/routes/api/attachments/upload.ts +148 -0
- package/src/routes/share/prd/$prdId/route.tsx +10 -0
- package/src/server/serverFns/__tests__/project-kb.test.ts +147 -0
- package/src/server/serverFns/iteration/__tests__/auto-schedule-core.test.ts +62 -0
- package/src/server/serverFns/iteration/__tests__/burndown-core.test.ts +90 -0
- package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +211 -0
- package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +155 -0
- package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +136 -0
- package/src/server/serverFns/iteration/auto-schedule-core.ts +54 -0
- package/src/server/serverFns/iteration/auto-schedule-internal.ts +88 -0
- package/src/server/serverFns/iteration/burndown-core.ts +60 -0
- package/src/server/serverFns/iteration/index.ts +22 -0
- package/src/server/serverFns/iteration/iteration-auto-schedule.ts +66 -0
- package/src/server/serverFns/iteration/iteration-burndown.ts +70 -0
- package/src/server/serverFns/iteration/iteration-crud.ts +208 -0
- package/src/server/serverFns/iteration/iteration-stories.ts +111 -0
- package/src/server/serverFns/iteration/iteration-transfer.ts +42 -0
- package/src/server/serverFns/iteration/schemas.ts +58 -0
- package/src/server/serverFns/iteration/transfer-internal.ts +62 -0
- package/src/server/serverFns/knowledge.ts +50 -4
- package/src/server/serverFns/project/module-overview.ts +109 -0
- package/src/server/serverFns/project.ts +45 -2
- package/src/server/serverFns/story/__tests__/kb-scope.test.ts +56 -0
- package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +100 -0
- package/src/server/serverFns/story/__tests__/labels.test.ts +67 -0
- package/src/server/serverFns/story/__tests__/story-assoc.test.ts +222 -0
- package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +132 -2
- package/src/server/serverFns/story/index.ts +9 -4
- package/src/server/serverFns/story/kb-general.ts +39 -0
- package/src/server/serverFns/story/kb-scope.ts +52 -0
- package/src/server/serverFns/story/link-preview.ts +115 -0
- package/src/server/serverFns/story/story-assoc.ts +372 -51
- package/src/server/serverFns/story/story-attachments.ts +32 -45
- package/src/{components/project/project-icon.tsx → server/serverFns/story/story-crud/label-palette.ts} +16 -19
- package/src/server/serverFns/story/story-crud/label-suggest-core.ts +88 -0
- package/src/server/serverFns/story/story-crud/label-suggest.ts +38 -0
- package/src/{components/knowledge/knowledge-icon.tsx → server/serverFns/story/story-crud/label-upsert.ts} +13 -19
- package/src/server/serverFns/story/story-crud/labels.ts +3 -5
- package/src/server/serverFns/story/story-crud.ts +53 -6
- package/src/server/serverFns/story/story-dependencies.ts +158 -5
- package/src/server/serverFns/story/story-links.ts +43 -7
- package/src/server/serverFns/story/story-transfer.ts +5 -4
- package/src/server/serverFns/story/story-tree.ts +28 -1
- package/src/server/serverFns/story/types.ts +38 -0
- package/src/styles/globals.css +25 -0
- package/src/types/project.ts +5 -5
- package/vite.shared.ts +2 -0
- package/src/components/project/project-animated-icon.tsx +0 -73
- package/src/components/shared/icon-packs/index.ts +0 -1
- package/src/components/shared/icon-packs/knowledge.ts +0 -1
- package/src/components/shared/icon-packs/prd.ts +0 -1
- package/src/components/shared/icon-packs/project.ts +0 -1
- package/src/components/shared/icon-picker.tsx +0 -1
- package/src/components/story/detail/story-detail-toolbar/attachments.tsx +0 -73
- package/src/components/story/detail/story-detail-toolbar/knowledge.tsx +0 -73
- package/src/components/story/detail/story-detail-toolbar/links.tsx +0 -73
- package/src/components/story/relations/attachments-panel.tsx +0 -179
- package/src/components/story/relations/knowledge-panel.tsx +0 -284
- package/src/components/story/relations/links-panel.tsx +0 -240
- package/src/lib/project-mock.ts +0 -156
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
|
-
import { GitPullRequestArrow, CircleFadingArrowUp, Repeat2, CalendarPlus2, Hash, MessageCircleCheck, RefreshCcwDot, CircleFadingPlus, CircleArrowUp, CircleArrowRight, CircleArrowDown, CircleSlash, CircleDashed, CircleCheckBig, LoaderCircle, CheckCircle2, XCircle, FilePlus, FilePenLine, MessageSquareText, Flag, UserRound, Text, Calendar, CalendarCheck, Tags, TextInitial, Building2, GitBranch, type LucideIcon } from "lucide-react";
|
|
18
|
+
import { GitPullRequestArrow, CircleFadingArrowUp, Repeat, Repeat2, CalendarPlus2, Hash, MessageCircleCheck, RefreshCcwDot, CircleFadingPlus, CircleArrowUp, CircleArrowRight, CircleArrowDown, CircleSlash, CircleDashed, CircleCheckBig, LoaderCircle, CheckCircle2, XCircle, FilePlus, FilePenLine, MessageSquareText, Flag, UserRound, Text, Calendar, CalendarCheck, Tags, TextInitial, Building2, GitBranch, ListTree, ArrowLeftRight, ArrowDownToDot, Link2, Paperclip, BookOpen, Unlink, Trash2, BookX, FileX, ArrowRightFromLine, ArrowLeftToLine, ArrowUpFromDot, type LucideIcon } from "lucide-react";
|
|
19
19
|
import type { DependencyEdgeType, DependencyGroup } from "./types";
|
|
20
20
|
|
|
21
21
|
export type StoryState = "backlog" | "todo" | "in_progress" | "done" | "cancelled";
|
|
@@ -101,6 +101,19 @@ export const ACTIVITY_LABEL: Record<string, string> = {
|
|
|
101
101
|
project_transferred: "activity.project_transferred",
|
|
102
102
|
parent_changed: "activity.parent_changed",
|
|
103
103
|
parent_removed: "activity.parent_removed",
|
|
104
|
+
child_added: "activity.child_added",
|
|
105
|
+
dependency_added: "activity.dependency_added",
|
|
106
|
+
dependency_removed: "activity.dependency_removed",
|
|
107
|
+
link_added: "activity.link_added",
|
|
108
|
+
link_updated: "activity.link_updated",
|
|
109
|
+
link_removed: "activity.link_removed",
|
|
110
|
+
attachment_added: "activity.attachment_added",
|
|
111
|
+
attachment_updated: "activity.attachment_updated",
|
|
112
|
+
attachment_removed: "activity.attachment_removed",
|
|
113
|
+
kb_linked: "activity.kb_linked",
|
|
114
|
+
kb_unlinked: "activity.kb_unlinked",
|
|
115
|
+
prd_unlinked: "activity.prd_unlinked",
|
|
116
|
+
iteration_changed: "activity.iteration_changed",
|
|
104
117
|
};
|
|
105
118
|
|
|
106
119
|
export const ACTIVITY_ICON: Record<string, LucideIcon> = {
|
|
@@ -119,6 +132,19 @@ startDate_changed: CalendarPlus2,
|
|
|
119
132
|
project_transferred: Repeat2,
|
|
120
133
|
parent_changed: GitPullRequestArrow,
|
|
121
134
|
parent_removed: GitPullRequestArrow,
|
|
135
|
+
child_added: ListTree,
|
|
136
|
+
dependency_added: ArrowLeftRight,
|
|
137
|
+
dependency_removed: Unlink,
|
|
138
|
+
link_added: Link2,
|
|
139
|
+
link_updated: Link2,
|
|
140
|
+
link_removed: Unlink,
|
|
141
|
+
attachment_added: Paperclip,
|
|
142
|
+
attachment_updated: FilePenLine,
|
|
143
|
+
attachment_removed: Trash2,
|
|
144
|
+
kb_linked: BookOpen,
|
|
145
|
+
kb_unlinked: BookX,
|
|
146
|
+
prd_unlinked: FileX,
|
|
147
|
+
iteration_changed: Repeat,
|
|
122
148
|
};
|
|
123
149
|
|
|
124
150
|
// ====== 依赖关系常量(对齐 Plane,新模型) ======
|
|
@@ -144,6 +170,10 @@ export interface DependencyTypeOption {
|
|
|
144
170
|
edge: DependencyEdgeType;
|
|
145
171
|
/** 与 group 等价的方向标志:true = 存 (Y, X, edge) */
|
|
146
172
|
reversed: boolean;
|
|
173
|
+
/** 选择器/行内标签显示的图标(lucide 组件引用) */
|
|
174
|
+
icon: LucideIcon;
|
|
175
|
+
/** 图标旋转角度(deg),每个图标方向语义不同可单独设定;0 或不填不旋转 */
|
|
176
|
+
iconRotation: number;
|
|
147
177
|
}
|
|
148
178
|
|
|
149
179
|
export const DEPENDENCY_TYPE_OPTIONS: DependencyTypeOption[] = [
|
|
@@ -155,6 +185,8 @@ export const DEPENDENCY_TYPE_OPTIONS: DependencyTypeOption[] = [
|
|
|
155
185
|
tooltipLabelKey: "dependency.tooltips.blocked_by",
|
|
156
186
|
edge: "BLOCKS",
|
|
157
187
|
reversed: true,
|
|
188
|
+
icon: ArrowRightFromLine,
|
|
189
|
+
iconRotation: 0,
|
|
158
190
|
},
|
|
159
191
|
{
|
|
160
192
|
key: "starts_after",
|
|
@@ -164,6 +196,8 @@ export const DEPENDENCY_TYPE_OPTIONS: DependencyTypeOption[] = [
|
|
|
164
196
|
tooltipLabelKey: "dependency.tooltips.starts_after",
|
|
165
197
|
edge: "STARTS_BEFORE",
|
|
166
198
|
reversed: true,
|
|
199
|
+
icon: ArrowUpFromDot,
|
|
200
|
+
iconRotation: 90,
|
|
167
201
|
},
|
|
168
202
|
{
|
|
169
203
|
key: "finishes_after",
|
|
@@ -173,6 +207,8 @@ export const DEPENDENCY_TYPE_OPTIONS: DependencyTypeOption[] = [
|
|
|
173
207
|
tooltipLabelKey: "dependency.tooltips.finishes_after",
|
|
174
208
|
edge: "FINISHES_BEFORE",
|
|
175
209
|
reversed: true,
|
|
210
|
+
icon: ArrowLeftToLine,
|
|
211
|
+
iconRotation: 0,
|
|
176
212
|
},
|
|
177
213
|
{
|
|
178
214
|
key: "blocking",
|
|
@@ -182,6 +218,8 @@ export const DEPENDENCY_TYPE_OPTIONS: DependencyTypeOption[] = [
|
|
|
182
218
|
tooltipLabelKey: "dependency.tooltips.blocking",
|
|
183
219
|
edge: "BLOCKS",
|
|
184
220
|
reversed: false,
|
|
221
|
+
icon: ArrowRightFromLine,
|
|
222
|
+
iconRotation: 0,
|
|
185
223
|
},
|
|
186
224
|
{
|
|
187
225
|
key: "starts_before",
|
|
@@ -191,6 +229,8 @@ export const DEPENDENCY_TYPE_OPTIONS: DependencyTypeOption[] = [
|
|
|
191
229
|
tooltipLabelKey: "dependency.tooltips.starts_before",
|
|
192
230
|
edge: "STARTS_BEFORE",
|
|
193
231
|
reversed: false,
|
|
232
|
+
icon: ArrowUpFromDot,
|
|
233
|
+
iconRotation: 90,
|
|
194
234
|
},
|
|
195
235
|
{
|
|
196
236
|
key: "finishes_before",
|
|
@@ -200,6 +240,8 @@ export const DEPENDENCY_TYPE_OPTIONS: DependencyTypeOption[] = [
|
|
|
200
240
|
tooltipLabelKey: "dependency.tooltips.finishes_before",
|
|
201
241
|
edge: "FINISHES_BEFORE",
|
|
202
242
|
reversed: false,
|
|
243
|
+
icon: ArrowLeftToLine,
|
|
244
|
+
iconRotation: 0,
|
|
203
245
|
},
|
|
204
246
|
];
|
|
205
247
|
|
|
@@ -20,7 +20,7 @@ import { CalendarCheck, CalendarPlus2, Sprout } from "lucide-react"
|
|
|
20
20
|
import { UserAvatar } from "@allbluecn/ui"
|
|
21
21
|
import { useForm } from "@tanstack/react-form"
|
|
22
22
|
import { useTranslation } from "react-i18next"
|
|
23
|
-
import {
|
|
23
|
+
import { LucideIconByName } from "@/components/shared/lucide-icon-picker/lucide-icon-by-name"
|
|
24
24
|
import { Button } from "@/components/ui/button"
|
|
25
25
|
import { Input } from "@/components/ui/input"
|
|
26
26
|
|
|
@@ -63,7 +63,10 @@ interface CreateStoryDialogProps {
|
|
|
63
63
|
initialState?: string
|
|
64
64
|
initialParentId?: string
|
|
65
65
|
initialProjectId?: string
|
|
66
|
-
onSuccess?: (
|
|
66
|
+
onSuccess?: (
|
|
67
|
+
story: { id: string; name: string; identifier?: string },
|
|
68
|
+
activities?: import("./types").ActivityItem[],
|
|
69
|
+
) => void
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
export function CreateStoryDialog({
|
|
@@ -120,10 +123,12 @@ export function CreateStoryDialog({
|
|
|
120
123
|
form.setFieldValue("startDate", initialStartDate ?? "")
|
|
121
124
|
form.setFieldValue("targetDate", "")
|
|
122
125
|
form.setFieldValue("labelIds", [])
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
if (!continuousCreate) {
|
|
127
|
+
if (initialProjectId) {
|
|
128
|
+
form.setFieldValue("projectId", initialProjectId)
|
|
129
|
+
} else {
|
|
130
|
+
ensureBuiltinProjectFn().then((p) => form.setFieldValue("projectId", p.id)).catch(() => {})
|
|
131
|
+
}
|
|
127
132
|
}
|
|
128
133
|
setDescription({ html: "", markdown: "" })
|
|
129
134
|
setIsEditorFocused(false)
|
|
@@ -173,14 +178,35 @@ export function CreateStoryDialog({
|
|
|
173
178
|
labelIds: values.labelIds,
|
|
174
179
|
},
|
|
175
180
|
})
|
|
176
|
-
|
|
177
|
-
|
|
181
|
+
if (tiptapEditorRef.current) {
|
|
182
|
+
tiptapEditorRef.current.commands.clearContent()
|
|
183
|
+
}
|
|
184
|
+
if (continuousCreate) {
|
|
185
|
+
const currentProjectId = form.state.values.projectId
|
|
186
|
+
form.setFieldValue("name", "")
|
|
187
|
+
setDescription({ html: "", markdown: "" })
|
|
188
|
+
form.setFieldValue("state", initialState ?? "backlog")
|
|
189
|
+
form.setFieldValue("priority", "none")
|
|
190
|
+
form.setFieldValue("assigneeId", "none")
|
|
191
|
+
form.setFieldValue("startDate", initialStartDate ?? "")
|
|
192
|
+
form.setFieldValue("targetDate", "")
|
|
193
|
+
form.setFieldValue("labelIds", [])
|
|
194
|
+
if (currentProjectId) {
|
|
195
|
+
form.setFieldValue("projectId", currentProjectId)
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
form.reset()
|
|
199
|
+
setDescription({ html: "", markdown: "" })
|
|
200
|
+
}
|
|
178
201
|
if (!continuousCreate) onOpenChange(false)
|
|
179
|
-
onSuccess?.(
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
202
|
+
onSuccess?.(
|
|
203
|
+
{
|
|
204
|
+
id: res.id,
|
|
205
|
+
name: res.name,
|
|
206
|
+
identifier: res.identifier,
|
|
207
|
+
},
|
|
208
|
+
res.activities,
|
|
209
|
+
)
|
|
184
210
|
} catch (err) {
|
|
185
211
|
setError(err instanceof Error ? err.message : t("create.failed"))
|
|
186
212
|
} finally {
|
|
@@ -214,7 +240,7 @@ export function CreateStoryDialog({
|
|
|
214
240
|
className="h-8 gap-1.5 border-0 bg-muted/50 px-2.5 text-sm font-medium"
|
|
215
241
|
>
|
|
216
242
|
{selectedProject ? (
|
|
217
|
-
<
|
|
243
|
+
<LucideIconByName name={selectedProject.icon} size={16} />
|
|
218
244
|
) : (
|
|
219
245
|
<Sprout className="size-4 text-muted-foreground" />
|
|
220
246
|
)}
|
|
@@ -230,7 +256,7 @@ export function CreateStoryDialog({
|
|
|
230
256
|
className={`flex w-full items-center justify-between gap-2 rounded px-2 py-1.5 text-sm ${field.value === p.id ? "bg-accent font-medium" : "hover:bg-muted"}`}
|
|
231
257
|
>
|
|
232
258
|
<span className="flex min-w-0 items-center gap-2">
|
|
233
|
-
<
|
|
259
|
+
<LucideIconByName name={p.icon} size={16} />
|
|
234
260
|
<span className="truncate">{p.name}</span>
|
|
235
261
|
</span>
|
|
236
262
|
{p.displayId && (
|
|
@@ -318,7 +344,7 @@ export function CreateStoryDialog({
|
|
|
318
344
|
<DateEditor value={field.value || null} onChange={(v) => field.handleChange(v ?? "")} label={t("create.dueDate")} icon={CalendarCheck} />
|
|
319
345
|
)} />
|
|
320
346
|
<form.Field name="labelIds" children={(field) => (
|
|
321
|
-
<LabelsEditor labelIds={field.value as string[]} labels={labels} onChange={(v) => field.handleChange(v as string[])} variant="button" />
|
|
347
|
+
<LabelsEditor labelIds={field.value as string[]} labels={labels} onChange={(v) => field.handleChange(v as string[])} variant="button" aiContext={{ name: () => ((form.state.values.name as string) ?? "").trim() }} />
|
|
322
348
|
)} />
|
|
323
349
|
</div>
|
|
324
350
|
|
|
@@ -0,0 +1,114 @@
|
|
|
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 React from "react";
|
|
20
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
21
|
+
|
|
22
|
+
vi.mock("@tanstack/react-start", () => ({
|
|
23
|
+
useServerFn: (fn: any) => vi.fn().mockImplementation((args: any) => fn(args)),
|
|
24
|
+
}));
|
|
25
|
+
vi.mock("@/server/serverFns/story", () => ({
|
|
26
|
+
removeLinkFn: vi.fn(async () => ({})),
|
|
27
|
+
removeAttachmentFn: vi.fn(async () => ({})),
|
|
28
|
+
unlinkDocFn: vi.fn(async () => ({})),
|
|
29
|
+
checkLinkEmbeddableFn: vi.fn(async () => ({ embeddable: true })),
|
|
30
|
+
}));
|
|
31
|
+
vi.mock("@/server/serverFns/knowledge", () => ({
|
|
32
|
+
getDocumentFn: vi.fn(async () => ({ content: { html: "<p>hello</p>", markdown: "hello" } })),
|
|
33
|
+
}));
|
|
34
|
+
vi.mock("sonner", () => ({
|
|
35
|
+
toast: { success: vi.fn(), error: vi.fn() },
|
|
36
|
+
}));
|
|
37
|
+
vi.mock("@tanstack/react-router", () => ({
|
|
38
|
+
Link: ({ children, to, params }: any) => React.createElement("a", { href: to, "data-to": to, ...params }, children),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
import { AssociatedAttachmentsPanel } from "../story-detail-toolbar/associated-attachments-panel";
|
|
42
|
+
import type { RelationsData } from "../../types";
|
|
43
|
+
|
|
44
|
+
const data = {
|
|
45
|
+
links: [{ id: "l1", url: "https://a.com", title: "站点A", description: null }],
|
|
46
|
+
attachments: [
|
|
47
|
+
{ id: "a1", fileName: "文件.png", fileSize: 2048, mimeType: "image/png", storageUrl: "/m/1" },
|
|
48
|
+
],
|
|
49
|
+
knowledge: {
|
|
50
|
+
prds: [],
|
|
51
|
+
docs: [
|
|
52
|
+
{ id: "d1", title: "知识页", kbId: "kb1", kbName: "团队库", kbIcon: "notebook", docIcon: null, parentId: null },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
} as unknown as RelationsData;
|
|
56
|
+
|
|
57
|
+
function setup(props?: { data: RelationsData }) {
|
|
58
|
+
const onOpenDialog = vi.fn();
|
|
59
|
+
render(
|
|
60
|
+
<AssociatedAttachmentsPanel
|
|
61
|
+
storyId="s1"
|
|
62
|
+
data={props?.data ?? data}
|
|
63
|
+
onRefresh={vi.fn()}
|
|
64
|
+
onOpenDialog={onOpenDialog}
|
|
65
|
+
/>,
|
|
66
|
+
);
|
|
67
|
+
return { onOpenDialog };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe("AssociatedAttachmentsPanel", () => {
|
|
71
|
+
it("三类资源合并渲染,计数 badge 为总数 3,默认列表视图", () => {
|
|
72
|
+
setup();
|
|
73
|
+
expect(screen.getByText("关联附件")).toBeInTheDocument();
|
|
74
|
+
expect(screen.getByText("3")).toBeInTheDocument();
|
|
75
|
+
expect(screen.getByText("站点A")).toBeInTheDocument();
|
|
76
|
+
expect(screen.getByText("文件.png")).toBeInTheDocument();
|
|
77
|
+
expect(screen.getByText("知识页")).toBeInTheDocument();
|
|
78
|
+
expect(screen.getByText("团队库")).toBeInTheDocument();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("点击预览按钮切换到预览模式,渲染网格布局", () => {
|
|
82
|
+
setup();
|
|
83
|
+
fireEvent.click(screen.getByRole("button", { name: "预览" }));
|
|
84
|
+
expect(screen.getByTestId("associated-items").getAttribute("data-mode")).toBe("preview");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("空数据整面板不渲染", () => {
|
|
88
|
+
const empty = {
|
|
89
|
+
links: [],
|
|
90
|
+
attachments: [],
|
|
91
|
+
knowledge: { prds: [], docs: [] },
|
|
92
|
+
} as unknown as RelationsData;
|
|
93
|
+
const { container } = render(
|
|
94
|
+
<AssociatedAttachmentsPanel
|
|
95
|
+
storyId="s1"
|
|
96
|
+
data={empty}
|
|
97
|
+
onRefresh={vi.fn()}
|
|
98
|
+
onOpenDialog={vi.fn()}
|
|
99
|
+
/>,
|
|
100
|
+
);
|
|
101
|
+
expect(container.querySelector("[data-testid='associated-panel']")).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("点击编辑链接回调 onOpenDialog('link', link)", () => {
|
|
105
|
+
const { onOpenDialog } = setup();
|
|
106
|
+
const editBtns = screen.getAllByRole("button", { name: "编辑" });
|
|
107
|
+
// 第一个是链接的编辑按钮,第二个是附件的编辑按钮
|
|
108
|
+
fireEvent.click(editBtns[0]);
|
|
109
|
+
expect(onOpenDialog).toHaveBeenCalledWith(
|
|
110
|
+
"link",
|
|
111
|
+
expect.objectContaining({ id: "l1" }),
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
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 } from "vitest";
|
|
19
|
+
import { render, screen } from "@testing-library/react";
|
|
20
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
21
|
+
import { StoryDetailCategories } from "../story-detail-toolbar/categories";
|
|
22
|
+
|
|
23
|
+
vi.mock("react-i18next", () => ({
|
|
24
|
+
useTranslation: () => ({ t: (key: string) => key }),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
const renderWithProviders = (ui: React.ReactElement) =>
|
|
28
|
+
render(<TooltipProvider>{ui}</TooltipProvider>);
|
|
29
|
+
|
|
30
|
+
describe("StoryDetailCategories", () => {
|
|
31
|
+
it("渲染 6 个 category button", () => {
|
|
32
|
+
renderWithProviders(<StoryDetailCategories />);
|
|
33
|
+
const buttons = screen.getAllByRole("button");
|
|
34
|
+
expect(buttons).toHaveLength(6);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("渲染 6 个 category 标签", () => {
|
|
38
|
+
renderWithProviders(<StoryDetailCategories />);
|
|
39
|
+
expect(screen.getByText("categories.prd.label")).toBeTruthy();
|
|
40
|
+
expect(screen.getByText("categories.ui.label")).toBeTruthy();
|
|
41
|
+
expect(screen.getByText("categories.bug.label")).toBeTruthy();
|
|
42
|
+
expect(screen.getByText("categories.testCase.label")).toBeTruthy();
|
|
43
|
+
expect(screen.getByText("categories.acceptance.label")).toBeTruthy();
|
|
44
|
+
expect(screen.getByText("categories.retro.label")).toBeTruthy();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("bug 和 test-case 之间有一个分隔线", () => {
|
|
48
|
+
renderWithProviders(<StoryDetailCategories />);
|
|
49
|
+
const spans = document.querySelectorAll("span");
|
|
50
|
+
const separator = Array.from(spans).find((s) =>
|
|
51
|
+
s.className.includes("w-px") && s.className.includes("h-4"),
|
|
52
|
+
);
|
|
53
|
+
expect(separator).toBeTruthy();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("bug 前后各有一个 button", () => {
|
|
57
|
+
renderWithProviders(<StoryDetailCategories />);
|
|
58
|
+
const bugBtn = screen.getByText("categories.bug.label")
|
|
59
|
+
.closest("button")!;
|
|
60
|
+
expect(bugBtn).toBeTruthy();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
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 } from "vitest";
|
|
19
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
20
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
21
|
+
import { StoryDetailDependencies } from "../story-detail-toolbar/dependencies";
|
|
22
|
+
|
|
23
|
+
vi.mock("react-i18next", () => ({
|
|
24
|
+
useTranslation: () => ({ t: (key: string) => key }),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
const renderWithProviders = (ui: React.ReactElement) =>
|
|
28
|
+
render(<TooltipProvider>{ui}</TooltipProvider>);
|
|
29
|
+
|
|
30
|
+
describe("StoryDetailDependencies", () => {
|
|
31
|
+
it("渲染 button,包含 Waypoints 图标和文本", () => {
|
|
32
|
+
renderWithProviders(
|
|
33
|
+
<StoryDetailDependencies onOpenAdd={vi.fn()} />,
|
|
34
|
+
);
|
|
35
|
+
const btn = screen.getByRole("button");
|
|
36
|
+
expect(btn).toBeTruthy();
|
|
37
|
+
expect(screen.getByText("relations.addDependency")).toBeTruthy();
|
|
38
|
+
expect(btn).toHaveAttribute("title", "relations.dependencyTitle");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("点击 button 触发 onOpenAdd", () => {
|
|
42
|
+
const onOpenAdd = vi.fn();
|
|
43
|
+
renderWithProviders(
|
|
44
|
+
<StoryDetailDependencies onOpenAdd={onOpenAdd} />,
|
|
45
|
+
);
|
|
46
|
+
fireEvent.click(screen.getByRole("button"));
|
|
47
|
+
expect(onOpenAdd).toHaveBeenCalledTimes(1);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -30,11 +30,11 @@ import {
|
|
|
30
30
|
type StoryState,
|
|
31
31
|
type StoryPriority,
|
|
32
32
|
} from "@/components/story/constants";
|
|
33
|
-
import {
|
|
33
|
+
import { LucideIconByName } from "@/components/shared/lucide-icon-picker/lucide-icon-by-name";
|
|
34
34
|
import { listProjectsBriefFn } from "@/server/serverFns/project";
|
|
35
35
|
import type { ActivityItem, UserBrief } from "@/components/story/types";
|
|
36
36
|
import { groupActivities, type ActivityGroup } from "@/lib/story/activity-grouper";
|
|
37
|
-
import { ArrowUpFromDot, ChevronDown, Dot, Sparkles, Layers, UserRound } from "lucide-react";
|
|
37
|
+
import { ArrowUpFromDot, ArrowRight, ChevronDown, Dot, Sparkles, Layers, UserRound } from "lucide-react";
|
|
38
38
|
import { sanitize } from "@/lib/sanitize";
|
|
39
39
|
|
|
40
40
|
function getDiffDecorator(
|
|
@@ -55,7 +55,7 @@ function getDiffDecorator(
|
|
|
55
55
|
return <Icon className="size-3 shrink-0 text-muted-foreground" />;
|
|
56
56
|
}
|
|
57
57
|
if (action === "project_transferred") {
|
|
58
|
-
return <
|
|
58
|
+
return <LucideIconByName name={projectIconName} size={12} className="shrink-0" />;
|
|
59
59
|
}
|
|
60
60
|
if (action === "assignee_changed") {
|
|
61
61
|
// 优先用用户头像,找不到时降级为通用图标
|
|
@@ -250,7 +250,7 @@ function DiffCompare({ oldValue, newValue, oldDecorator, newDecorator, plain, bl
|
|
|
250
250
|
</div>
|
|
251
251
|
</div>
|
|
252
252
|
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-10 flex size-5 items-center justify-center rounded-full bg-background border">
|
|
253
|
-
<
|
|
253
|
+
<ArrowRight className="size-2.5 text-muted-foreground" />
|
|
254
254
|
</div>
|
|
255
255
|
<div className="w-1/2 bg-green-50/40 dark:bg-green-950/20 p-2 pl-4">
|
|
256
256
|
<div className="flex items-center gap-1.5">
|
|
@@ -322,11 +322,33 @@ interface ActivityTimelineProps {
|
|
|
322
322
|
users?: UserBrief[];
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
/** 关联类 action 集合:描述文案带 {{value}} 插值(对象名或数量) */
|
|
326
|
+
const OBJECT_ACTIVITY_ACTIONS: Record<string, true> = {
|
|
327
|
+
child_added: true,
|
|
328
|
+
dependency_added: true,
|
|
329
|
+
dependency_removed: true,
|
|
330
|
+
link_added: true,
|
|
331
|
+
link_updated: true,
|
|
332
|
+
link_removed: true,
|
|
333
|
+
attachment_added: true,
|
|
334
|
+
attachment_updated: true,
|
|
335
|
+
attachment_removed: true,
|
|
336
|
+
kb_linked: true,
|
|
337
|
+
kb_unlinked: true,
|
|
338
|
+
prd_unlinked: true,
|
|
339
|
+
};
|
|
340
|
+
|
|
325
341
|
function describeActivity(a: ActivityItem, t: TFunction): string {
|
|
326
342
|
if (a.action === "project_transferred" || a.action === "identifier_changed") {
|
|
327
343
|
return t(ACTIVITY_LABEL[a.action] ?? a.action);
|
|
328
344
|
}
|
|
329
345
|
|
|
346
|
+
// 关联类动作(添加子需求/依赖/链接/附件/知识库):newValue 为对象描述或数量,走插值文案
|
|
347
|
+
if (a.action in OBJECT_ACTIVITY_ACTIONS) {
|
|
348
|
+
const key = ACTIVITY_LABEL[a.action];
|
|
349
|
+
return t(key, { value: a.newValue ?? "" });
|
|
350
|
+
}
|
|
351
|
+
|
|
330
352
|
const fieldLabel = ACTIVITY_LABEL[a.action] ?? a.action;
|
|
331
353
|
|
|
332
354
|
if (!a.field) {
|