@allbluecn/web-app 0.4.22 → 0.5.1
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 +12 -8
- package/src/components/knowledge/knowledge-toolbar.tsx +124 -1
- 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 +124 -51
- package/src/components/story/create-story-dialog.tsx +12 -6
- package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +114 -0
- package/src/components/story/detail/activity-timeline.tsx +15 -0
- package/src/components/story/detail/story-detail-body.tsx +14 -3
- package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +748 -0
- package/src/components/story/detail/story-detail-toolbar/categories.tsx +23 -18
- package/src/components/story/detail/story-detail-toolbar/children.tsx +51 -274
- package/src/components/story/detail/story-detail-toolbar/dependencies.tsx +13 -45
- package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +473 -0
- package/src/components/story/detail/story-detail-toolbar.tsx +237 -10
- 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 +107 -16
- package/src/components/story/peek-panel.tsx +8 -1
- 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/{config/sidebar-notifications.ts → components/story/relations/general-groups.ts} +12 -30
- package/src/components/story/relations/knowledge-select-dialog.tsx +403 -0
- package/src/components/story/relations/link-manage-dialog.tsx +191 -0
- 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 +30 -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/lib/story/__tests__/video-embed.test.ts +77 -0
- package/src/lib/story/video-embed.ts +59 -0
- package/src/routeTree.gen.ts +65 -0
- package/src/routes/_nav/notifications/route.lazy.tsx +22 -0
- package/src/routes/_nav/notifications/route.tsx +30 -0
- package/src/routes/_nav/projects/$projectId/settings/route.lazy.tsx +74 -3
- package/src/routes/_nav/projects/$projectId/settings/route.tsx +6 -2
- package/src/routes/_nav/settings/notifications/route.lazy.tsx +6 -5
- package/src/routes/api/attachments/$id/content.ts +60 -0
- package/src/routes/api/attachments/upload.ts +148 -0
- 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__/project-kb.test.ts +147 -0
- 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/knowledge.ts +50 -4
- package/src/server/serverFns/locale.ts +15 -3
- package/src/server/serverFns/notifications.ts +18 -4
- package/src/server/serverFns/project.ts +43 -0
- package/src/server/serverFns/story/__tests__/kb-scope.test.ts +56 -0
- package/src/server/serverFns/story/__tests__/story-assoc.test.ts +222 -0
- package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +265 -0
- package/src/server/serverFns/story/__tests__/story-groups.test.ts +19 -1
- package/src/server/serverFns/story/index.ts +7 -6
- 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 +339 -50
- package/src/server/serverFns/story/story-attachments.ts +12 -43
- package/src/server/serverFns/story/story-crud.ts +21 -4
- package/src/server/serverFns/story/story-dependencies.ts +138 -162
- package/src/server/serverFns/story/story-groups.ts +23 -3
- package/src/server/serverFns/story/story-links.ts +13 -1
- package/src/server/serverFns/story/story-transfer.ts +5 -3
- package/src/server/serverFns/story/story-tree.ts +28 -1
- package/src/server/serverFns/story/types.ts +37 -0
- package/src/server/serverFns/team.ts +51 -2
- package/src/styles/globals.css +25 -0
- package/vite.shared.ts +2 -0
- package/src/components/notifications/notification-list.tsx +0 -232
- package/src/components/notifications/notification-preferences.tsx +0 -156
- 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/relation-badge.tsx +0 -57
- package/src/components/story/relations/attachments-panel.tsx +0 -179
- 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/knowledge-panel.tsx +0 -284
- package/src/components/story/relations/links-panel.tsx +0 -240
- package/src/components/story/relations/tab.tsx +0 -212
|
@@ -0,0 +1,473 @@
|
|
|
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 * as React from "react";
|
|
19
|
+
import { useNavigate } from "@tanstack/react-router";
|
|
20
|
+
import { useTranslation } from "react-i18next";
|
|
21
|
+
import { Plus, ChevronDown, ChartNoAxesGantt } from "lucide-react";
|
|
22
|
+
import { Button } from "@/components/ui/button";
|
|
23
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
24
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
25
|
+
import { useServerFn } from "@tanstack/react-start";
|
|
26
|
+
import { toast } from "sonner";
|
|
27
|
+
import {
|
|
28
|
+
removeDependencyFn,
|
|
29
|
+
bulkArchiveStoriesFn,
|
|
30
|
+
bulkDeleteStoriesFn,
|
|
31
|
+
bulkUpdateStoriesFn,
|
|
32
|
+
} from "@/server/serverFns/story";
|
|
33
|
+
import { BatchToolbar } from "../../views/story-list/batch-toolbar";
|
|
34
|
+
import { useGroupedStories } from "../../display/use-grouped-stories";
|
|
35
|
+
import { VirtualGroupHeader } from "../../views/story-list/group";
|
|
36
|
+
import { DisplayOptionsBody } from "../../display/display-dropdown";
|
|
37
|
+
import { StoryRefRow } from "../../relations/story-ref-row";
|
|
38
|
+
import type { DisplayOptions, GroupNode } from "../../display/types";
|
|
39
|
+
import type { StoryPatch } from "../../display/group-fields";
|
|
40
|
+
import type { RelationsData, UserBrief, DependencyRow } from "../../types";
|
|
41
|
+
|
|
42
|
+
type DependencySection = "dependsOn" | "dependents";
|
|
43
|
+
|
|
44
|
+
// fetch 网络层失败(服务未启动/断网)时浏览器抛 TypeError,文案为英文原始信息
|
|
45
|
+
function isNetworkError(e: unknown): boolean {
|
|
46
|
+
const msg = e instanceof Error ? e.message : "";
|
|
47
|
+
return /failed to fetch|load failed|network error|fetch failed/i.test(msg);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface StoryDetailDependencyPanelProps {
|
|
51
|
+
storyId: string;
|
|
52
|
+
/** 当前需求编号(tooltip 文案用) */
|
|
53
|
+
storyIdentifier?: string;
|
|
54
|
+
projectId: string | null;
|
|
55
|
+
data: RelationsData["dependencies"];
|
|
56
|
+
onRefresh: (next: (prev: RelationsData) => RelationsData) => void;
|
|
57
|
+
refreshDependencies: () => void;
|
|
58
|
+
users?: UserBrief[];
|
|
59
|
+
projects?: { id: string; name: string; icon: string; displayId: string | null }[];
|
|
60
|
+
labels?: { id: string; name: string; color: string }[];
|
|
61
|
+
onOpenAdd: () => void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function StoryDetailDependencyPanel({
|
|
65
|
+
storyId,
|
|
66
|
+
storyIdentifier,
|
|
67
|
+
projectId,
|
|
68
|
+
data,
|
|
69
|
+
onRefresh,
|
|
70
|
+
refreshDependencies,
|
|
71
|
+
users,
|
|
72
|
+
projects,
|
|
73
|
+
labels,
|
|
74
|
+
onOpenAdd,
|
|
75
|
+
}: StoryDetailDependencyPanelProps) {
|
|
76
|
+
const { t } = useTranslation("story");
|
|
77
|
+
const navigate = useNavigate();
|
|
78
|
+
const removeDependency = useServerFn(removeDependencyFn);
|
|
79
|
+
const [selectedIds, setSelectedIds] = React.useState<Set<string>>(() => new Set());
|
|
80
|
+
const [busy, setBusy] = React.useState(false);
|
|
81
|
+
const [collapsed, setCollapsed] = React.useState<Record<string, boolean>>({});
|
|
82
|
+
const [filters, setFilters] = React.useState<DisplayOptions>(() => ({
|
|
83
|
+
groupBy: "none",
|
|
84
|
+
subGroupBy: "none",
|
|
85
|
+
orderBy: "created",
|
|
86
|
+
orderDir: "desc",
|
|
87
|
+
properties: ["id", "priority", "state", "assignee", "startDate", "dueDate", "labels"],
|
|
88
|
+
showEmptyGroups: false,
|
|
89
|
+
showSubItems: false,
|
|
90
|
+
pageSize: 100,
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
const allRows = React.useMemo(
|
|
94
|
+
() => [...data.dependsOn, ...data.dependents],
|
|
95
|
+
[data],
|
|
96
|
+
);
|
|
97
|
+
const rowById = React.useMemo(
|
|
98
|
+
() => new Map(allRows.map((r) => [r.id, r])),
|
|
99
|
+
[allRows],
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const dependsOnGroups = useGroupedStories({
|
|
103
|
+
stories: data.dependsOn,
|
|
104
|
+
filters,
|
|
105
|
+
users: users ?? [],
|
|
106
|
+
labels: labels ?? [],
|
|
107
|
+
projects: projects ?? [],
|
|
108
|
+
});
|
|
109
|
+
const dependentsGroups = useGroupedStories({
|
|
110
|
+
stories: data.dependents,
|
|
111
|
+
filters,
|
|
112
|
+
users: users ?? [],
|
|
113
|
+
labels: labels ?? [],
|
|
114
|
+
projects: projects ?? [],
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const selectedStories = React.useMemo(
|
|
118
|
+
() => allRows.filter((s) => selectedIds.has(s.id)),
|
|
119
|
+
[allRows, selectedIds],
|
|
120
|
+
);
|
|
121
|
+
const canArchive = React.useMemo(
|
|
122
|
+
() => selectedStories.some((s) => s.state === "done" || s.state === "cancelled"),
|
|
123
|
+
[selectedStories],
|
|
124
|
+
);
|
|
125
|
+
const allSelected = allRows.length > 0 && selectedIds.size === allRows.length;
|
|
126
|
+
const showSelect = selectedIds.size > 0;
|
|
127
|
+
const totalCount = data.dependsOn.length + data.dependents.length;
|
|
128
|
+
|
|
129
|
+
if (totalCount === 0) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const updateFilters = (patch: Partial<DisplayOptions>) => {
|
|
134
|
+
setFilters((prev) => ({ ...prev, ...patch }));
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const handleOpenStory = (id: string) => {
|
|
138
|
+
navigate({ to: "/stories/$storyId", params: { storyId: id } });
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const applyRowsUpdate = (
|
|
142
|
+
next: (rows: DependencyRow[]) => DependencyRow[],
|
|
143
|
+
group: DependencySection,
|
|
144
|
+
) => {
|
|
145
|
+
onRefresh((prev) => ({
|
|
146
|
+
...prev,
|
|
147
|
+
dependencies: { ...prev.dependencies, [group]: next(prev.dependencies[group]) },
|
|
148
|
+
}));
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const handleRemoveRow = async (rowId: string) => {
|
|
152
|
+
const row = rowById.get(rowId);
|
|
153
|
+
if (!row) return;
|
|
154
|
+
const group: DependencySection = data.dependsOn.some((r) => r.id === rowId)
|
|
155
|
+
? "dependsOn"
|
|
156
|
+
: "dependents";
|
|
157
|
+
try {
|
|
158
|
+
await removeDependency({ data: { depId: row.depId } });
|
|
159
|
+
toast.success(t("dependency.removed"));
|
|
160
|
+
applyRowsUpdate((rows) => rows.filter((r) => r.id !== rowId), group);
|
|
161
|
+
setSelectedIds((prev) => {
|
|
162
|
+
const next = new Set(prev);
|
|
163
|
+
next.delete(rowId);
|
|
164
|
+
return next;
|
|
165
|
+
});
|
|
166
|
+
} catch (e) {
|
|
167
|
+
toast.error(isNetworkError(e) ? t("dependency.networkFailed") : (e as Error)?.message || t("dependency.removeFailed"));
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const handleBulkRemoveRelations = async (storyIds: string[]) => {
|
|
172
|
+
if (storyIds.length === 0) return;
|
|
173
|
+
setBusy(true);
|
|
174
|
+
try {
|
|
175
|
+
for (const id of storyIds) {
|
|
176
|
+
const row = rowById.get(id);
|
|
177
|
+
if (row) await removeDependency({ data: { depId: row.depId } });
|
|
178
|
+
}
|
|
179
|
+
toast.success(t("dependency.removed"));
|
|
180
|
+
setSelectedIds(new Set());
|
|
181
|
+
refreshDependencies();
|
|
182
|
+
} catch (e) {
|
|
183
|
+
toast.error(isNetworkError(e) ? t("dependency.networkFailed") : (e as Error)?.message || t("dependency.removeFailed"));
|
|
184
|
+
} finally {
|
|
185
|
+
setBusy(false);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const handleBulkArchive = async (storyIds?: string[]) => {
|
|
190
|
+
const ids = storyIds ?? [...selectedIds];
|
|
191
|
+
if (ids.length === 0) return;
|
|
192
|
+
setBusy(true);
|
|
193
|
+
try {
|
|
194
|
+
const res = await bulkArchiveStoriesFn({ data: { storyIds: ids } });
|
|
195
|
+
toast.success(
|
|
196
|
+
res.skipped > 0
|
|
197
|
+
? t("children.bulkArchivedSkipped", { archived: res.archived, skipped: res.skipped })
|
|
198
|
+
: t("children.bulkArchived", { count: res.archived }),
|
|
199
|
+
);
|
|
200
|
+
setSelectedIds(new Set());
|
|
201
|
+
refreshDependencies();
|
|
202
|
+
} catch (e) {
|
|
203
|
+
toast.error((e as Error)?.message || t("children.archiveFailed"));
|
|
204
|
+
} finally {
|
|
205
|
+
setBusy(false);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const handleBulkDelete = async (storyIds?: string[]) => {
|
|
210
|
+
const ids = storyIds ?? [...selectedIds];
|
|
211
|
+
if (ids.length === 0) return;
|
|
212
|
+
setBusy(true);
|
|
213
|
+
try {
|
|
214
|
+
const res = await bulkDeleteStoriesFn({ data: { storyIds: ids } });
|
|
215
|
+
toast.success(t("children.bulkDeleted", { count: res.deleted }));
|
|
216
|
+
setSelectedIds(new Set());
|
|
217
|
+
refreshDependencies();
|
|
218
|
+
} catch (e) {
|
|
219
|
+
toast.error((e as Error)?.message || t("children.deleteFailed"));
|
|
220
|
+
} finally {
|
|
221
|
+
setBusy(false);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const handleBulkUpdate = async (patch: StoryPatch) => {
|
|
226
|
+
if (selectedIds.size === 0) return;
|
|
227
|
+
setBusy(true);
|
|
228
|
+
try {
|
|
229
|
+
await bulkUpdateStoriesFn({ data: { storyIds: [...selectedIds], patch } });
|
|
230
|
+
setSelectedIds(new Set());
|
|
231
|
+
toast.success(t("dependency.bulkUpdated"));
|
|
232
|
+
refreshDependencies();
|
|
233
|
+
} catch (e) {
|
|
234
|
+
toast.error((e as Error)?.message || t("children.bulkUpdateFailed"));
|
|
235
|
+
} finally {
|
|
236
|
+
setBusy(false);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const toggleStory = (id: string) => {
|
|
241
|
+
setSelectedIds((prev) => {
|
|
242
|
+
const next = new Set(prev);
|
|
243
|
+
if (next.has(id)) next.delete(id);
|
|
244
|
+
else next.add(id);
|
|
245
|
+
return next;
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const toggleGroup = (ids: string[]) => {
|
|
250
|
+
setSelectedIds((prev) => {
|
|
251
|
+
const next = new Set(prev);
|
|
252
|
+
const allChosen = ids.length > 0 && ids.every((id) => next.has(id));
|
|
253
|
+
if (allChosen) {
|
|
254
|
+
for (const id of ids) next.delete(id);
|
|
255
|
+
} else {
|
|
256
|
+
for (const id of ids) next.add(id);
|
|
257
|
+
}
|
|
258
|
+
return next;
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const toggleCollapse = (key: string) => {
|
|
263
|
+
setCollapsed((prev) => ({ ...prev, [key]: !prev[key] }));
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const computeGroupCheckState = (ids: string[]): boolean | "indeterminate" => {
|
|
267
|
+
if (ids.length === 0) return false;
|
|
268
|
+
let count = 0;
|
|
269
|
+
for (const id of ids) if (selectedIds.has(id)) count++;
|
|
270
|
+
if (count === ids.length) return true;
|
|
271
|
+
return count > 0 ? "indeterminate" : false;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const handleStoryRefresh = (rowId: string, patch: Record<string, unknown>) => {
|
|
275
|
+
for (const group of ["dependsOn", "dependents"] as const) {
|
|
276
|
+
if (patch.deleted) {
|
|
277
|
+
applyRowsUpdate((rows) => rows.filter((r) => r.id !== rowId), group);
|
|
278
|
+
} else {
|
|
279
|
+
applyRowsUpdate(
|
|
280
|
+
(rows) => rows.map((r) => (r.id === rowId ? ({ ...r, ...patch } as DependencyRow) : r)),
|
|
281
|
+
group,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (patch.deleted) {
|
|
286
|
+
setSelectedIds((prev) => {
|
|
287
|
+
const next = new Set(prev);
|
|
288
|
+
next.delete(rowId);
|
|
289
|
+
return next;
|
|
290
|
+
});
|
|
291
|
+
} else {
|
|
292
|
+
refreshDependencies();
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const renderRow = (row: DependencyRow, group: DependencySection) => (
|
|
297
|
+
<StoryRefRow
|
|
298
|
+
key={row.depId}
|
|
299
|
+
story={row}
|
|
300
|
+
users={users ?? []}
|
|
301
|
+
labels={labels ?? []}
|
|
302
|
+
parentProjectId={projectId}
|
|
303
|
+
projects={projects ?? []}
|
|
304
|
+
selected={selectedIds.has(row.id)}
|
|
305
|
+
properties={filters.properties}
|
|
306
|
+
onToggle={toggleStory}
|
|
307
|
+
onOpenStory={handleOpenStory}
|
|
308
|
+
onRemove={handleRemoveRow}
|
|
309
|
+
removeMenuLabel={t("dependency.remove")}
|
|
310
|
+
dependencyMeta={{ depType: row.depType, group }}
|
|
311
|
+
currentIdentifier={storyIdentifier}
|
|
312
|
+
onStoryRefresh={handleStoryRefresh}
|
|
313
|
+
/>
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const renderGroupItems = (node: GroupNode, group: DependencySection, depth: number, collapseKey: string) => {
|
|
317
|
+
const isCollapsed = collapsed[collapseKey] ?? false;
|
|
318
|
+
const ids = node.items.map((s) => s.id);
|
|
319
|
+
return (
|
|
320
|
+
<React.Fragment key={collapseKey}>
|
|
321
|
+
<div style={{ height: 36 }}>
|
|
322
|
+
<VirtualGroupHeader
|
|
323
|
+
node={node}
|
|
324
|
+
depth={depth}
|
|
325
|
+
collapsed={isCollapsed}
|
|
326
|
+
collapseKey={collapseKey}
|
|
327
|
+
onToggle={toggleCollapse}
|
|
328
|
+
selectState={computeGroupCheckState(ids)}
|
|
329
|
+
onToggleSelect={() => toggleGroup(ids)}
|
|
330
|
+
showSelect={showSelect}
|
|
331
|
+
showRoleLabel={
|
|
332
|
+
filters.subGroupBy === "assignee" || filters.subGroupBy === "createdBy"
|
|
333
|
+
}
|
|
334
|
+
groupDimension={depth === 0 ? filters.groupBy : filters.subGroupBy}
|
|
335
|
+
/>
|
|
336
|
+
</div>
|
|
337
|
+
{!isCollapsed && (
|
|
338
|
+
<React.Fragment>
|
|
339
|
+
{node.subGroups && node.subGroups.length > 0 ? (
|
|
340
|
+
node.subGroups.map((subGroup) =>
|
|
341
|
+
renderGroupItems(
|
|
342
|
+
subGroup,
|
|
343
|
+
group,
|
|
344
|
+
depth + 1,
|
|
345
|
+
`${collapseKey}/${subGroup.key}`,
|
|
346
|
+
),
|
|
347
|
+
)
|
|
348
|
+
) : (
|
|
349
|
+
node.items.map((s) => renderRow(s as DependencyRow, group))
|
|
350
|
+
)}
|
|
351
|
+
</React.Fragment>
|
|
352
|
+
)}
|
|
353
|
+
</React.Fragment>
|
|
354
|
+
);
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const renderSection = (
|
|
358
|
+
titleKey: string,
|
|
359
|
+
sectionKey: DependencySection,
|
|
360
|
+
rows: DependencyRow[],
|
|
361
|
+
groups: GroupNode[],
|
|
362
|
+
) => {
|
|
363
|
+
const isCollapsed = collapsed[`${sectionKey}:root`] ?? false;
|
|
364
|
+
return (
|
|
365
|
+
<Collapsible open={!isCollapsed} onOpenChange={(open) => setCollapsed((p) => ({ ...p, [`${sectionKey}:root`]: !open }))}>
|
|
366
|
+
<CollapsibleTrigger asChild>
|
|
367
|
+
<div className="flex h-9 cursor-pointer items-center gap-1.5 bg-muted/50 px-3 text-sm font-medium hover:text-foreground transition-colors">
|
|
368
|
+
<ChevronDown className={`size-3 shrink-0 text-muted-foreground transition-transform ${isCollapsed ? "-rotate-90" : "rotate-0"}`} />
|
|
369
|
+
<span>{t(titleKey)}</span>
|
|
370
|
+
<span className="ml-1 text-xs text-muted-foreground">{rows.length}</span>
|
|
371
|
+
</div>
|
|
372
|
+
</CollapsibleTrigger>
|
|
373
|
+
<CollapsibleContent>
|
|
374
|
+
{rows.length === 0 ? (
|
|
375
|
+
<p className="flex items-center gap-1.5 px-2 py-1.5 text-xs text-muted-foreground">
|
|
376
|
+
<span className="w-4 shrink-0" aria-hidden />
|
|
377
|
+
{t("relation.none")}
|
|
378
|
+
</p>
|
|
379
|
+
) : filters.groupBy === "none" ? (
|
|
380
|
+
rows.map((row) => renderRow(row, sectionKey))
|
|
381
|
+
) : (
|
|
382
|
+
groups.map((node) => renderGroupItems(node, sectionKey, 0, `${sectionKey}:${node.key}`))
|
|
383
|
+
)}
|
|
384
|
+
</CollapsibleContent>
|
|
385
|
+
</Collapsible>
|
|
386
|
+
);
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<Collapsible defaultOpen>
|
|
391
|
+
<CollapsibleTrigger
|
|
392
|
+
asChild
|
|
393
|
+
className="group flex w-full cursor-pointer items-center gap-2 pb-3 pt-4 text-xs font-semibold hover:text-foreground transition-colors"
|
|
394
|
+
>
|
|
395
|
+
<div
|
|
396
|
+
role="button"
|
|
397
|
+
tabIndex={0}
|
|
398
|
+
onKeyDown={(e) => {
|
|
399
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
400
|
+
e.preventDefault();
|
|
401
|
+
(e.currentTarget as HTMLDivElement).click();
|
|
402
|
+
}
|
|
403
|
+
}}
|
|
404
|
+
>
|
|
405
|
+
<div className="flex items-center gap-1.5">
|
|
406
|
+
<span>{t("relations.dependency")}</span>
|
|
407
|
+
<span className="inline-flex min-w-[18px] items-center justify-center rounded-full bg-primary px-1 text-[10px] font-medium text-primary-foreground">
|
|
408
|
+
{totalCount}
|
|
409
|
+
</span>
|
|
410
|
+
<ChevronDown className="size-3 text-muted-foreground transition-transform group-data-[state=open]:rotate-0 group-data-[state=closed]:-rotate-90" />
|
|
411
|
+
</div>
|
|
412
|
+
<div className="flex items-center gap-1 ml-auto" onClick={(e) => e.stopPropagation()}>
|
|
413
|
+
<Popover>
|
|
414
|
+
<PopoverTrigger asChild>
|
|
415
|
+
<Button variant="ghost" size="sm" className="size-7 p-0">
|
|
416
|
+
<ChartNoAxesGantt className="size-3.5" />
|
|
417
|
+
</Button>
|
|
418
|
+
</PopoverTrigger>
|
|
419
|
+
<PopoverContent align="end" className="w-75 p-0">
|
|
420
|
+
<div className="max-h-[60vh] overflow-y-auto">
|
|
421
|
+
<DisplayOptionsBody
|
|
422
|
+
filters={filters}
|
|
423
|
+
onChange={updateFilters}
|
|
424
|
+
layout="list"
|
|
425
|
+
showSubItems={false}
|
|
426
|
+
/>
|
|
427
|
+
</div>
|
|
428
|
+
</PopoverContent>
|
|
429
|
+
</Popover>
|
|
430
|
+
<Button variant="ghost" size="sm" className="size-7 p-0" onClick={onOpenAdd}>
|
|
431
|
+
<Plus className="size-3.5" />
|
|
432
|
+
</Button>
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
</CollapsibleTrigger>
|
|
436
|
+
<CollapsibleContent className="pb-2">
|
|
437
|
+
<div className="flex flex-col gap-0 border rounded-lg overflow-hidden">
|
|
438
|
+
{renderSection("dependency.dependsOnTitle", "dependsOn", data.dependsOn, dependsOnGroups)}
|
|
439
|
+
{renderSection("dependency.dependentsTitle", "dependents", data.dependents, dependentsGroups)}
|
|
440
|
+
<span className="-ml-1">
|
|
441
|
+
<BatchToolbar
|
|
442
|
+
selectedCount={selectedIds.size}
|
|
443
|
+
totalLoaded={allRows.length}
|
|
444
|
+
allSelected={allSelected}
|
|
445
|
+
canArchive={canArchive}
|
|
446
|
+
canMove={false}
|
|
447
|
+
canMoveUp={false}
|
|
448
|
+
canMoveDown={false}
|
|
449
|
+
busy={busy}
|
|
450
|
+
users={users ?? []}
|
|
451
|
+
labels={labels ?? []}
|
|
452
|
+
projects={projects ?? []}
|
|
453
|
+
stories={selectedStories}
|
|
454
|
+
onToggleAll={() =>
|
|
455
|
+
setSelectedIds(
|
|
456
|
+
allSelected ? new Set() : new Set(allRows.map((r) => r.id)),
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
onMoveUp={() => {}}
|
|
460
|
+
onMoveDown={() => {}}
|
|
461
|
+
onArchive={handleBulkArchive}
|
|
462
|
+
onDelete={handleBulkDelete}
|
|
463
|
+
onBulkUpdate={handleBulkUpdate}
|
|
464
|
+
onClearSelection={() => setSelectedIds(new Set())}
|
|
465
|
+
onTransfer={() => {}}
|
|
466
|
+
onRemoveRelations={handleBulkRemoveRelations}
|
|
467
|
+
/>
|
|
468
|
+
</span>
|
|
469
|
+
</div>
|
|
470
|
+
</CollapsibleContent>
|
|
471
|
+
</Collapsible>
|
|
472
|
+
);
|
|
473
|
+
}
|