@allbluecn/web-app 0.4.22 → 0.5.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 +8 -8
- package/src/components/layout/sidebar-03/app-sidebar.tsx +3 -4
- package/src/components/layout/sidebar-03/nav-notifications.tsx +135 -52
- package/src/components/layout/sidebar-03/user-menu.tsx +4 -2
- package/src/components/notifications/settings-notifications.tsx +198 -0
- package/src/components/notifications/team-notifications.tsx +343 -0
- package/src/components/settings/personal-team/team/team-client.tsx +51 -12
- package/src/components/settings/personal-team/team/team-invitations.tsx +181 -181
- package/src/components/shared/wave-physics-loader.tsx +155 -0
- package/src/components/story/__tests__/dependency-type-badge.test.ts +89 -0
- package/src/components/story/constants.ts +114 -51
- package/src/components/story/detail/story-detail-body.tsx +13 -3
- package/src/components/story/detail/story-detail-toolbar/children.tsx +49 -274
- package/src/components/story/detail/story-detail-toolbar/dependencies.tsx +13 -45
- package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +465 -0
- package/src/components/story/detail/story-detail-toolbar.tsx +84 -3
- package/src/components/story/display/__tests__/use-paged-story-groups.test.ts +152 -1
- package/src/components/story/display/use-paged-story-groups.ts +370 -28
- package/src/components/story/inline-editors/story-select-dialog.tsx +88 -12
- package/src/components/story/peek-panel.tsx +8 -1
- package/src/components/story/relations/story-ref-row.tsx +319 -0
- package/src/components/story/story-list-view.tsx +3 -1
- package/src/components/story/story-panel-header.tsx +4 -1
- package/src/components/story/types.ts +17 -18
- package/src/components/story/views/story-list/batch-toolbar.tsx +12 -1
- package/src/components/story/views/story-list/index.tsx +16 -5
- package/src/components/story/views/story-list/row.tsx +5 -4
- package/src/components/ui/separator.tsx +1 -1
- package/src/hooks/use-notifications.ts +4 -1
- package/src/lib/__tests__/datetime.test.ts +16 -1
- package/src/lib/changelog/sdk-versions.ts +19 -19
- package/src/lib/datetime.ts +1 -0
- package/src/routeTree.gen.ts +23 -0
- package/src/routes/_nav/notifications/route.lazy.tsx +22 -0
- package/src/routes/_nav/notifications/route.tsx +30 -0
- package/src/routes/_nav/settings/notifications/route.lazy.tsx +6 -5
- package/src/routes/api/notifications/stream.ts +4 -0
- package/src/server/notifications/__tests__/notification-service.test.ts +189 -0
- package/src/server/notifications/notification-service.ts +137 -27
- package/src/server/serverFns/__tests__/team-invitation.test.ts +73 -7
- package/src/server/serverFns/__tests__/team-join-request.test.ts +5 -3
- package/src/server/serverFns/locale.ts +15 -3
- package/src/server/serverFns/notifications.ts +18 -4
- package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +239 -0
- package/src/server/serverFns/story/__tests__/story-groups.test.ts +19 -1
- package/src/server/serverFns/story/index.ts +1 -2
- package/src/server/serverFns/story/story-dependencies.ts +107 -163
- package/src/server/serverFns/story/story-groups.ts +23 -3
- package/src/server/serverFns/story/story-transfer.ts +5 -3
- package/src/server/serverFns/team.ts +51 -2
- package/src/components/notifications/notification-list.tsx +0 -232
- package/src/components/notifications/notification-preferences.tsx +0 -156
- package/src/components/story/relation-badge.tsx +0 -57
- package/src/components/story/relations/dependency-panel.tsx +0 -190
- package/src/components/story/relations/hierarchy-panel.tsx +0 -180
- package/src/components/story/relations/tab.tsx +0 -212
- package/src/config/sidebar-notifications.ts +0 -50
- package/src/plugins/.gen-manifest.json +0 -13
- package/src/plugins/modules.gen.ts +0 -20
- package/src/plugins/plugins.gen.ts +0 -20
- package/src/plugins/ui.gen.ts +0 -8
- package/src/routes/_nav/inspiration/route.tsx +0 -8
- package/src/routes/_nav/prd/$id/route.tsx +0 -9
- package/src/routes/_nav/prd/index.tsx +0 -9
- package/src/routes/_nav/prd/route.tsx +0 -8
- package/src/routes/_nav/tags/$id/route.tsx +0 -8
- package/src/routes/_nav/tags/index.tsx +0 -8
- package/src/routes/_nav/tags/route.tsx +0 -8
- package/src/routes/_nav/tags/settings/route.tsx +0 -9
- package/src/routes/share/prd/$prdId/route.tsx +0 -10
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
DEPENDENCY_TYPE_OPTIONS,
|
|
4
|
+
dependencyTypeBadgeKey,
|
|
5
|
+
dependencyTypeOption,
|
|
6
|
+
toDependencyEdge,
|
|
7
|
+
} from "../constants";
|
|
8
|
+
|
|
9
|
+
describe("DEPENDENCY_TYPE_OPTIONS", () => {
|
|
10
|
+
it("共 6 项,key 唯一,每组各 3 项", () => {
|
|
11
|
+
expect(DEPENDENCY_TYPE_OPTIONS).toHaveLength(6);
|
|
12
|
+
const keys = DEPENDENCY_TYPE_OPTIONS.map((o) => o.key);
|
|
13
|
+
expect(new Set(keys).size).toBe(6);
|
|
14
|
+
expect(
|
|
15
|
+
DEPENDENCY_TYPE_OPTIONS.filter((o) => o.group === "dependsOn"),
|
|
16
|
+
).toHaveLength(3);
|
|
17
|
+
expect(
|
|
18
|
+
DEPENDENCY_TYPE_OPTIONS.filter((o) => o.group === "dependents"),
|
|
19
|
+
).toHaveLength(3);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("每项含 optionLabelKey/badgeLabelKey/tooltipLabelKey/edge/group,edge 为三种正向类型且每组各覆盖一次", () => {
|
|
23
|
+
for (const group of ["dependsOn", "dependents"] as const) {
|
|
24
|
+
const edges = new Set(
|
|
25
|
+
DEPENDENCY_TYPE_OPTIONS.filter((o) => o.group === group).map(
|
|
26
|
+
(o) => o.edge,
|
|
27
|
+
),
|
|
28
|
+
);
|
|
29
|
+
expect(edges).toEqual(
|
|
30
|
+
new Set(["BLOCKS", "STARTS_BEFORE", "FINISHES_BEFORE"]),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
for (const o of DEPENDENCY_TYPE_OPTIONS) {
|
|
34
|
+
expect(o.optionLabelKey).toMatch(/^dependency\.options\./);
|
|
35
|
+
expect(o.badgeLabelKey).toMatch(/^dependency\.labels\./);
|
|
36
|
+
expect(o.tooltipLabelKey).toMatch(/^dependency\.tooltips\./);
|
|
37
|
+
expect(o.reversed).toBe(o.group === "dependsOn");
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("dependencyTypeOption / dependencyTypeBadgeKey", () => {
|
|
43
|
+
it("dependents 组(我领先)→ blocking/starts_before/finishes_before", () => {
|
|
44
|
+
expect(dependencyTypeBadgeKey("dependents", "BLOCKS")).toBe("blocking");
|
|
45
|
+
expect(
|
|
46
|
+
dependencyTypeBadgeKey("dependents", "STARTS_BEFORE"),
|
|
47
|
+
).toBe("starts_before");
|
|
48
|
+
expect(
|
|
49
|
+
dependencyTypeBadgeKey("dependents", "FINISHES_BEFORE"),
|
|
50
|
+
).toBe("finishes_before");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("dependsOn 组(对方领先)→ blocked_by/starts_after/finishes_after", () => {
|
|
54
|
+
expect(dependencyTypeBadgeKey("dependsOn", "BLOCKS")).toBe("blocked_by");
|
|
55
|
+
expect(
|
|
56
|
+
dependencyTypeBadgeKey("dependsOn", "STARTS_BEFORE"),
|
|
57
|
+
).toBe("starts_after");
|
|
58
|
+
expect(
|
|
59
|
+
dependencyTypeBadgeKey("dependsOn", "FINISHES_BEFORE"),
|
|
60
|
+
).toBe("finishes_after");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("dependencyTypeOption 返回含句式 label 的完整选项", () => {
|
|
64
|
+
const o = dependencyTypeOption("dependsOn", "BLOCKS");
|
|
65
|
+
expect(o.key).toBe("blocked_by");
|
|
66
|
+
expect(o.optionLabelKey).toBe("dependency.options.blocked_by");
|
|
67
|
+
expect(o.badgeLabelKey).toBe("dependency.labels.blocked_by");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("toDependencyEdge", () => {
|
|
72
|
+
it("reversed=false:我领先,source=我", () => {
|
|
73
|
+
const o = DEPENDENCY_TYPE_OPTIONS.find((x) => x.key === "blocking")!;
|
|
74
|
+
expect(toDependencyEdge(o, "s1", "s2")).toEqual({
|
|
75
|
+
sourceId: "s1",
|
|
76
|
+
targetId: "s2",
|
|
77
|
+
type: "BLOCKS",
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("reversed=true:对方领先,source/target 交换", () => {
|
|
82
|
+
const o = DEPENDENCY_TYPE_OPTIONS.find((x) => x.key === "blocked_by")!;
|
|
83
|
+
expect(toDependencyEdge(o, "s1", "s2")).toEqual({
|
|
84
|
+
sourceId: "s2",
|
|
85
|
+
targetId: "s1",
|
|
86
|
+
type: "BLOCKS",
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -15,8 +15,8 @@
|
|
|
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 {
|
|
19
|
-
import type {
|
|
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";
|
|
19
|
+
import type { DependencyEdgeType, DependencyGroup } from "./types";
|
|
20
20
|
|
|
21
21
|
export type StoryState = "backlog" | "todo" | "in_progress" | "done" | "cancelled";
|
|
22
22
|
export type StoryPriority = "urgent" | "high" | "medium" | "low" | "none";
|
|
@@ -121,52 +121,115 @@ startDate_changed: CalendarPlus2,
|
|
|
121
121
|
parent_removed: GitPullRequestArrow,
|
|
122
122
|
};
|
|
123
123
|
|
|
124
|
-
// ======
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
124
|
+
// ====== 依赖关系常量(对齐 Plane,新模型) ======
|
|
125
|
+
|
|
126
|
+
// UI 依赖类型选择项(Plane 模型:3 种依赖类型 FS/SS/FF × 2 个方向视角)
|
|
127
|
+
// group="dependents"(我领先,所选需求等我):存 (X, Y, edge)
|
|
128
|
+
// group="dependsOn"(对方领先,我等所选需求):存 (Y, X, edge)
|
|
129
|
+
export interface DependencyTypeOption {
|
|
130
|
+
key:
|
|
131
|
+
| "blocking"
|
|
132
|
+
| "blocked_by"
|
|
133
|
+
| "starts_before"
|
|
134
|
+
| "starts_after"
|
|
135
|
+
| "finishes_before"
|
|
136
|
+
| "finishes_after";
|
|
137
|
+
group: keyof DependencyGroup;
|
|
138
|
+
/** 选择器文案 key(被选需求未知,用「所选需求」占位) */
|
|
139
|
+
optionLabelKey: string;
|
|
140
|
+
/** 行内标签文案 key({{identifier}} 插值被选需求编号) */
|
|
141
|
+
badgeLabelKey: string;
|
|
142
|
+
/** tooltip 文案 key({{identifier}} 插值被选需求编号 + {{me}} 插值当前需求编号) */
|
|
143
|
+
tooltipLabelKey: string;
|
|
144
|
+
edge: DependencyEdgeType;
|
|
145
|
+
/** 与 group 等价的方向标志:true = 存 (Y, X, edge) */
|
|
146
|
+
reversed: boolean;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const DEPENDENCY_TYPE_OPTIONS: DependencyTypeOption[] = [
|
|
150
|
+
{
|
|
151
|
+
key: "blocked_by",
|
|
152
|
+
group: "dependsOn",
|
|
153
|
+
optionLabelKey: "dependency.options.blocked_by",
|
|
154
|
+
badgeLabelKey: "dependency.labels.blocked_by",
|
|
155
|
+
tooltipLabelKey: "dependency.tooltips.blocked_by",
|
|
156
|
+
edge: "BLOCKS",
|
|
157
|
+
reversed: true,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
key: "starts_after",
|
|
161
|
+
group: "dependsOn",
|
|
162
|
+
optionLabelKey: "dependency.options.starts_after",
|
|
163
|
+
badgeLabelKey: "dependency.labels.starts_after",
|
|
164
|
+
tooltipLabelKey: "dependency.tooltips.starts_after",
|
|
165
|
+
edge: "STARTS_BEFORE",
|
|
166
|
+
reversed: true,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
key: "finishes_after",
|
|
170
|
+
group: "dependsOn",
|
|
171
|
+
optionLabelKey: "dependency.options.finishes_after",
|
|
172
|
+
badgeLabelKey: "dependency.labels.finishes_after",
|
|
173
|
+
tooltipLabelKey: "dependency.tooltips.finishes_after",
|
|
174
|
+
edge: "FINISHES_BEFORE",
|
|
175
|
+
reversed: true,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
key: "blocking",
|
|
179
|
+
group: "dependents",
|
|
180
|
+
optionLabelKey: "dependency.options.blocking",
|
|
181
|
+
badgeLabelKey: "dependency.labels.blocking",
|
|
182
|
+
tooltipLabelKey: "dependency.tooltips.blocking",
|
|
183
|
+
edge: "BLOCKS",
|
|
184
|
+
reversed: false,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
key: "starts_before",
|
|
188
|
+
group: "dependents",
|
|
189
|
+
optionLabelKey: "dependency.options.starts_before",
|
|
190
|
+
badgeLabelKey: "dependency.labels.starts_before",
|
|
191
|
+
tooltipLabelKey: "dependency.tooltips.starts_before",
|
|
192
|
+
edge: "STARTS_BEFORE",
|
|
193
|
+
reversed: false,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
key: "finishes_before",
|
|
197
|
+
group: "dependents",
|
|
198
|
+
optionLabelKey: "dependency.options.finishes_before",
|
|
199
|
+
badgeLabelKey: "dependency.labels.finishes_before",
|
|
200
|
+
tooltipLabelKey: "dependency.tooltips.finishes_before",
|
|
201
|
+
edge: "FINISHES_BEFORE",
|
|
202
|
+
reversed: false,
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
// 展示推导:分组 × 存储边类型 → 选择项(查 DEPENDENCY_TYPE_OPTIONS,映射真相单一)
|
|
207
|
+
export function dependencyTypeOption(
|
|
208
|
+
group: keyof DependencyGroup,
|
|
209
|
+
depType: DependencyEdgeType,
|
|
210
|
+
): DependencyTypeOption {
|
|
211
|
+
const option = DEPENDENCY_TYPE_OPTIONS.find(
|
|
212
|
+
(o) => o.edge === depType && o.group === group,
|
|
213
|
+
);
|
|
214
|
+
if (!option) throw new Error(`未知的依赖组合: ${group} ${depType}`);
|
|
215
|
+
return option;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 兼容保留:仅需要 key 时使用
|
|
219
|
+
export function dependencyTypeBadgeKey(
|
|
220
|
+
group: keyof DependencyGroup,
|
|
221
|
+
depType: DependencyEdgeType,
|
|
222
|
+
): DependencyTypeOption["key"] {
|
|
223
|
+
return dependencyTypeOption(group, depType).key;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 选择项 → 规范化存储边(提交时方向映射用)
|
|
227
|
+
export function toDependencyEdge(
|
|
228
|
+
option: DependencyTypeOption,
|
|
229
|
+
currentStoryId: string,
|
|
230
|
+
targetStoryId: string,
|
|
231
|
+
): { sourceId: string; targetId: string; type: DependencyEdgeType } {
|
|
232
|
+
return option.reversed
|
|
233
|
+
? { sourceId: targetStoryId, targetId: currentStoryId, type: option.edge }
|
|
234
|
+
: { sourceId: currentStoryId, targetId: targetStoryId, type: option.edge };
|
|
235
|
+
}
|
|
@@ -35,6 +35,7 @@ import { Button } from "@/components/ui/button";
|
|
|
35
35
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
36
36
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
37
37
|
import { updateStoryFn, listStoryReactionsFn, toggleStoryReactionFn } from "@/server/serverFns/story";
|
|
38
|
+
import { formatTimestamp } from "@/lib/datetime";
|
|
38
39
|
import { toast } from "sonner";
|
|
39
40
|
import type { StoryDetail, UserBrief, CommentWithReplies, ActivityItem, ReactionGroup, RelationsData } from "../types";
|
|
40
41
|
import { parseStoryDescription, serializeStoryDescription } from "../types";
|
|
@@ -321,13 +322,21 @@ export function StoryDetailBody({
|
|
|
321
322
|
updateField("description", json);
|
|
322
323
|
}, [updateField]);
|
|
323
324
|
|
|
325
|
+
// 截止日期已过期(未完成/未取消)→ 红色,与子需求列表样式一致
|
|
326
|
+
const dueOverdue = !!(
|
|
327
|
+
story.targetDate &&
|
|
328
|
+
story.state !== "done" &&
|
|
329
|
+
story.state !== "cancelled" &&
|
|
330
|
+
new Date(story.targetDate.slice(0, 10)) < new Date(new Date().toISOString().slice(0, 10))
|
|
331
|
+
);
|
|
332
|
+
|
|
324
333
|
const metaBar = (
|
|
325
334
|
<div className="flex flex-wrap items-center gap-2 px-6">
|
|
326
335
|
<StateEditor value={story.state} onChange={(v) => updateField("state", v)} variant="button" />
|
|
327
336
|
<PriorityEditor value={story.priority} onChange={(v) => updateField("priority", v)} variant="button" />
|
|
328
337
|
<AssigneeEditor assignee={story.assignee} users={users} onChange={(v) => updateField("assigneeId", v)} variant="button" />
|
|
329
338
|
<DateEditor value={story.startDate} onChange={(v) => updateField("startDate", v)} label={t("detail.startDate")} icon={CalendarPlus2} />
|
|
330
|
-
<DateEditor value={story.targetDate} onChange={(v) => updateField("targetDate", v)} label={t("detail.dueDate")} icon={CalendarCheck} />
|
|
339
|
+
<DateEditor value={story.targetDate} onChange={(v) => updateField("targetDate", v)} label={t("detail.dueDate")} icon={CalendarCheck} overdue={dueOverdue} />
|
|
331
340
|
</div>
|
|
332
341
|
);
|
|
333
342
|
|
|
@@ -532,6 +541,7 @@ export function StoryDetailBody({
|
|
|
532
541
|
<div className="py-2">
|
|
533
542
|
<StoryDetailToolbar
|
|
534
543
|
storyId={story.id}
|
|
544
|
+
storyIdentifier={identifier}
|
|
535
545
|
projectId={story.projectId}
|
|
536
546
|
data={relationsData}
|
|
537
547
|
onRefresh={onRelationsUpdate ?? (() => { })}
|
|
@@ -567,12 +577,12 @@ export function StoryDetailBody({
|
|
|
567
577
|
<div className="flex items-center gap-2 text-xs">
|
|
568
578
|
<ClockPlus className="size-4 shrink-0 text-muted-foreground" />
|
|
569
579
|
<span className="text-muted-foreground shrink-0">{t("detail.createdAt")}</span>
|
|
570
|
-
<span>{
|
|
580
|
+
<span>{formatTimestamp(story.createdAt)}</span>
|
|
571
581
|
</div>
|
|
572
582
|
<div className="flex items-center gap-2 text-xs">
|
|
573
583
|
<ClockArrowUp className="size-4 shrink-0 text-muted-foreground" />
|
|
574
584
|
<span className="text-muted-foreground shrink-0">{t("detail.updatedAt")}</span>
|
|
575
|
-
<span>{
|
|
585
|
+
<span>{formatTimestamp(story.updatedAt)}</span>
|
|
576
586
|
</div>
|
|
577
587
|
</div>
|
|
578
588
|
|