@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
|
@@ -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 {
|
|
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
|
-
<
|
|
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 {
|
|
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: "
|
|
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", "
|
|
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-
|
|
113
|
-
<DialogHeader
|
|
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
|
|
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
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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('
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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.
|
|
99
|
-
"lastUpdate": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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.
|
|
155
|
-
"lastUpdate": "
|
|
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
|
+
}
|