@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
|
@@ -1,190 +0,0 @@
|
|
|
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 { Link } from "@tanstack/react-router";
|
|
20
|
-
import { Button } from "@/components/ui/button";
|
|
21
|
-
import { Badge } from "@/components/ui/badge";
|
|
22
|
-
import {
|
|
23
|
-
DropdownMenu,
|
|
24
|
-
DropdownMenuContent,
|
|
25
|
-
DropdownMenuItem,
|
|
26
|
-
DropdownMenuTrigger,
|
|
27
|
-
} from "@/components/ui/dropdown-menu";
|
|
28
|
-
import { Plus, X, ChevronDown } from "lucide-react";
|
|
29
|
-
import { useServerFn } from "@tanstack/react-start"
|
|
30
|
-
import { useTranslation } from "react-i18next";
|
|
31
|
-
import { addDependencyFn, removeDependencyFn } from "@/server/serverFns/story";
|
|
32
|
-
import { toast } from "sonner";
|
|
33
|
-
import { StorySearchPopover } from "../search-popover";
|
|
34
|
-
import { STATE_LABEL, STATE_COLOR, DEPENDENCY_TYPES, DEPENDENCY_LABELS } from "../constants";
|
|
35
|
-
import type { DependencyGroup, DependencyType, DependencyItem } from "../types";
|
|
36
|
-
|
|
37
|
-
type ServerDependencyType = "BLOCKS" | "BLOCKED_BY" | "RELATES_TO";
|
|
38
|
-
|
|
39
|
-
function toServerType(type: DependencyType): ServerDependencyType {
|
|
40
|
-
return type === "blocks" ? "BLOCKS" : type === "blocked_by" ? "BLOCKED_BY" : "RELATES_TO";
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface DependencyPanelProps {
|
|
44
|
-
storyId: string;
|
|
45
|
-
data: DependencyGroup;
|
|
46
|
-
onUpdate: (updates: Partial<DependencyGroup>) => void;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function DependencyPanel({ storyId, data, onUpdate }: DependencyPanelProps) {
|
|
50
|
-
const { t } = useTranslation("story");
|
|
51
|
-
const [selectedType, setSelectedType] = React.useState<DependencyType>("blocks");
|
|
52
|
-
const [adding, setAdding] = React.useState(false);
|
|
53
|
-
const addDependency = useServerFn(addDependencyFn);
|
|
54
|
-
const removeDependency = useServerFn(removeDependencyFn);
|
|
55
|
-
|
|
56
|
-
const handleAddDependency = async (targetStoryId: string, story?: { id: string; title: string; identifier: string }) => {
|
|
57
|
-
try {
|
|
58
|
-
await addDependency({
|
|
59
|
-
data: {
|
|
60
|
-
sourceId: storyId,
|
|
61
|
-
targetId: targetStoryId,
|
|
62
|
-
type: toServerType(selectedType),
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
toast.success(t("relation.added"));
|
|
66
|
-
|
|
67
|
-
const groupKey = selectedType === "blocks" ? "blocking" : selectedType === "blocked_by" ? "blockedBy" : "relatesTo";
|
|
68
|
-
onUpdate({
|
|
69
|
-
[groupKey]: [
|
|
70
|
-
...data[groupKey],
|
|
71
|
-
{
|
|
72
|
-
id: targetStoryId,
|
|
73
|
-
title: story?.title || t("relation.newTitle"),
|
|
74
|
-
status: "backlog",
|
|
75
|
-
type: selectedType,
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
});
|
|
79
|
-
setAdding(false);
|
|
80
|
-
} catch (error: any) {
|
|
81
|
-
console.error(t("relation.addFailed"), error);
|
|
82
|
-
toast.error(error.message || t("relation.addFailed"));
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const handleRemoveDependency = async (story: DependencyItem) => {
|
|
87
|
-
try {
|
|
88
|
-
const type = story.type;
|
|
89
|
-
const sourceId = storyId;
|
|
90
|
-
const targetId = story.id;
|
|
91
|
-
|
|
92
|
-
await removeDependency({ data: { sourceId, targetId, type: toServerType(type) } });
|
|
93
|
-
toast.success(t("relation.removed"));
|
|
94
|
-
|
|
95
|
-
const groupKey = type === "blocks" ? "blocking" : type === "blocked_by" ? "blockedBy" : "relatesTo";
|
|
96
|
-
onUpdate({
|
|
97
|
-
[groupKey]: data[groupKey].filter((d) => d.id !== story.id),
|
|
98
|
-
});
|
|
99
|
-
} catch (error: any) {
|
|
100
|
-
console.error(t("relation.removeFailed"), error);
|
|
101
|
-
toast.error(error.message || t("relation.removeFailed"));
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const renderDependencySection = (
|
|
106
|
-
title: string,
|
|
107
|
-
groupKey: keyof DependencyGroup,
|
|
108
|
-
type: DependencyType
|
|
109
|
-
) => {
|
|
110
|
-
const items = data[groupKey];
|
|
111
|
-
|
|
112
|
-
return (
|
|
113
|
-
<div key={groupKey}>
|
|
114
|
-
<p className="text-xs font-medium text-muted-foreground mb-2">
|
|
115
|
-
{title} ({items.length})
|
|
116
|
-
</p>
|
|
117
|
-
{items.length > 0 ? (
|
|
118
|
-
<div className="space-y-1">
|
|
119
|
-
{items.map((item) => (
|
|
120
|
-
<div key={item.id} className="group flex items-center gap-2 rounded-md border px-3 py-2">
|
|
121
|
-
<Link
|
|
122
|
-
to="/stories/$storyId"
|
|
123
|
-
params={{ storyId: item.id }}
|
|
124
|
-
className="font-mono text-xs text-primary hover:underline"
|
|
125
|
-
>
|
|
126
|
-
{item.id.slice(0, 8)}
|
|
127
|
-
</Link>
|
|
128
|
-
<span className="flex-1 truncate text-sm">{item.title}</span>
|
|
129
|
-
<Badge variant="outline" className={STATE_COLOR[item.status as keyof typeof STATE_COLOR]}>
|
|
130
|
-
{t(STATE_LABEL[item.status as keyof typeof STATE_LABEL])}
|
|
131
|
-
</Badge>
|
|
132
|
-
<button
|
|
133
|
-
onClick={() => handleRemoveDependency(item)}
|
|
134
|
-
className="opacity-0 transition-opacity group-hover:opacity-100"
|
|
135
|
-
>
|
|
136
|
-
<X className="size-3.5 text-muted-foreground hover:text-destructive" />
|
|
137
|
-
</button>
|
|
138
|
-
</div>
|
|
139
|
-
))}
|
|
140
|
-
</div>
|
|
141
|
-
) : (
|
|
142
|
-
<p className="py-1 text-xs text-muted-foreground">{t("relation.none")}</p>
|
|
143
|
-
)}
|
|
144
|
-
</div>
|
|
145
|
-
);
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
return (
|
|
149
|
-
<div className="space-y-3 py-2">
|
|
150
|
-
{renderDependencySection(t("dependency.blocking"), "blocking", "blocks")}
|
|
151
|
-
{renderDependencySection(t("dependency.blocked_by"), "blockedBy", "blocked_by")}
|
|
152
|
-
{renderDependencySection(t("dependency.relatesTo"), "relatesTo", "relates_to")}
|
|
153
|
-
|
|
154
|
-
{adding ? (
|
|
155
|
-
<div className="flex items-center gap-2">
|
|
156
|
-
<DropdownMenu>
|
|
157
|
-
<DropdownMenuTrigger asChild>
|
|
158
|
-
<Button variant="outline" size="sm">
|
|
159
|
-
{t(DEPENDENCY_LABELS[selectedType])}
|
|
160
|
-
<ChevronDown className="ml-1 size-3" />
|
|
161
|
-
</Button>
|
|
162
|
-
</DropdownMenuTrigger>
|
|
163
|
-
<DropdownMenuContent>
|
|
164
|
-
{Object.values(DEPENDENCY_TYPES).map((type) => (
|
|
165
|
-
<DropdownMenuItem key={type} onClick={() => setSelectedType(type)}>
|
|
166
|
-
{t(DEPENDENCY_LABELS[type])}
|
|
167
|
-
</DropdownMenuItem>
|
|
168
|
-
))}
|
|
169
|
-
</DropdownMenuContent>
|
|
170
|
-
</DropdownMenu>
|
|
171
|
-
<StorySearchPopover
|
|
172
|
-
onSelect={handleAddDependency}
|
|
173
|
-
excludeIds={[storyId]}
|
|
174
|
-
trigger={
|
|
175
|
-
<Button variant="outline" size="sm">
|
|
176
|
-
<Plus className="mr-2 h-4 w-4" />
|
|
177
|
-
{t("relation.search")}
|
|
178
|
-
</Button>
|
|
179
|
-
}
|
|
180
|
-
/>
|
|
181
|
-
</div>
|
|
182
|
-
) : (
|
|
183
|
-
<Button variant="outline" size="sm" onClick={() => setAdding(true)}>
|
|
184
|
-
<Plus className="mr-2 h-4 w-4" />
|
|
185
|
-
{t("relation.add")}
|
|
186
|
-
</Button>
|
|
187
|
-
)}
|
|
188
|
-
</div>
|
|
189
|
-
);
|
|
190
|
-
}
|
|
@@ -1,180 +0,0 @@
|
|
|
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 { Plus, Minus, GitBranch } from "lucide-react";
|
|
20
|
-
import { useTranslation } from "react-i18next";
|
|
21
|
-
import { Button } from "@/components/ui/button";
|
|
22
|
-
import { StorySearchPopover } from "../search-popover";
|
|
23
|
-
import { useServerFn } from "@tanstack/react-start";
|
|
24
|
-
import { addChildFn, removeChildFn, getChildrenCountFn, getStoryDetailFn } from "@/server/serverFns/story";
|
|
25
|
-
import { toast } from "sonner";
|
|
26
|
-
import { type StoryBrief, StoryListItem } from "../types";
|
|
27
|
-
|
|
28
|
-
export interface HierarchyPanelProps {
|
|
29
|
-
storyId: string;
|
|
30
|
-
data: {
|
|
31
|
-
parent: StoryBrief | null;
|
|
32
|
-
children: StoryListItem[];
|
|
33
|
-
hasChildren: boolean;
|
|
34
|
-
};
|
|
35
|
-
onUpdate: (updates: Partial<{ parent: StoryBrief | null; children: StoryListItem[]; hasChildren: boolean }>) => void;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function HierarchyPanel({ storyId, data, onUpdate }: HierarchyPanelProps) {
|
|
39
|
-
const { t } = useTranslation("story");
|
|
40
|
-
const [addingChild, setAddingChild] = React.useState(false);
|
|
41
|
-
const addChild = useServerFn(addChildFn);
|
|
42
|
-
const removeChild = useServerFn(removeChildFn);
|
|
43
|
-
const getChildrenCount = useServerFn(getChildrenCountFn);
|
|
44
|
-
const getStoryDetail = useServerFn(getStoryDetailFn);
|
|
45
|
-
|
|
46
|
-
const handleAddChild = async (childId: string) => {
|
|
47
|
-
try {
|
|
48
|
-
await addChild({ data: { parentId: storyId, childId } });
|
|
49
|
-
toast.success(t("hierarchy.childAdded"));
|
|
50
|
-
|
|
51
|
-
const [{ count }, storyDetail] = await Promise.all([
|
|
52
|
-
getChildrenCount({ data: { parentId: storyId } }),
|
|
53
|
-
getStoryDetail({ data: { storyId: childId } }),
|
|
54
|
-
]);
|
|
55
|
-
|
|
56
|
-
onUpdate({
|
|
57
|
-
children: [...data.children, storyDetail as StoryListItem],
|
|
58
|
-
hasChildren: count > 0,
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
setAddingChild(false);
|
|
62
|
-
} catch (error: any) {
|
|
63
|
-
console.error(t("hierarchy.addChildFailed"), error);
|
|
64
|
-
toast.error(error.message || t("hierarchy.addChildFailed"));
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const handleRemoveChild = async (childId: string) => {
|
|
69
|
-
try {
|
|
70
|
-
await removeChild({ data: { childId } });
|
|
71
|
-
toast.success(t("hierarchy.childRemoved"));
|
|
72
|
-
|
|
73
|
-
const { count } = await getChildrenCount({ data: { parentId: storyId } });
|
|
74
|
-
onUpdate({
|
|
75
|
-
children: data.children.filter((c) => c.id !== childId),
|
|
76
|
-
hasChildren: count > 0,
|
|
77
|
-
});
|
|
78
|
-
} catch (error: any) {
|
|
79
|
-
console.error(t("hierarchy.removeChildFailed"), error);
|
|
80
|
-
toast.error(error.message || t("hierarchy.removeChildFailed"));
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
return (
|
|
85
|
-
<div className="space-y-4">
|
|
86
|
-
<div>
|
|
87
|
-
<div className="text-sm font-medium mb-2">{t("hierarchy.parent")}</div>
|
|
88
|
-
{data.parent ? (
|
|
89
|
-
<div className="flex items-center justify-between p-3 bg-muted rounded-md">
|
|
90
|
-
<div className="flex items-center gap-2">
|
|
91
|
-
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
|
92
|
-
<span className="text-sm">{data.parent.title}</span>
|
|
93
|
-
</div>
|
|
94
|
-
<Button
|
|
95
|
-
type="button"
|
|
96
|
-
variant="ghost"
|
|
97
|
-
size="sm"
|
|
98
|
-
onClick={() => handleRemoveChild(storyId)}
|
|
99
|
-
>
|
|
100
|
-
<Minus className="h-4 w-4" />
|
|
101
|
-
</Button>
|
|
102
|
-
</div>
|
|
103
|
-
) : (
|
|
104
|
-
<div className="flex items-center justify-between">
|
|
105
|
-
<span className="text-sm text-muted-foreground">{t("hierarchy.noParent")}</span>
|
|
106
|
-
<StorySearchPopover
|
|
107
|
-
onSelect={handleAddChild}
|
|
108
|
-
placeholder={t("hierarchy.setParent")}
|
|
109
|
-
trigger={
|
|
110
|
-
<Button type="button" variant="outline" size="sm">
|
|
111
|
-
<Plus className="h-4 w-4 mr-2" />
|
|
112
|
-
{t("hierarchy.setParent")}
|
|
113
|
-
</Button>
|
|
114
|
-
}
|
|
115
|
-
/>
|
|
116
|
-
</div>
|
|
117
|
-
)}
|
|
118
|
-
</div>
|
|
119
|
-
|
|
120
|
-
<div>
|
|
121
|
-
<div className="flex items-center justify-between mb-2">
|
|
122
|
-
<div className="text-sm font-medium">{t("hierarchy.children")}</div>
|
|
123
|
-
<Button
|
|
124
|
-
type="button"
|
|
125
|
-
variant="outline"
|
|
126
|
-
size="sm"
|
|
127
|
-
onClick={() => setAddingChild(!addingChild)}
|
|
128
|
-
>
|
|
129
|
-
<Plus className="h-4 w-4 mr-2" />
|
|
130
|
-
{t("hierarchy.addChild")}
|
|
131
|
-
</Button>
|
|
132
|
-
</div>
|
|
133
|
-
|
|
134
|
-
{addingChild && (
|
|
135
|
-
<div className="mb-3">
|
|
136
|
-
<StorySearchPopover
|
|
137
|
-
onSelect={handleAddChild}
|
|
138
|
-
placeholder={t("hierarchy.searchStory")}
|
|
139
|
-
excludeIds={[storyId, ...data.children.map((c) => c.id)]}
|
|
140
|
-
trigger={
|
|
141
|
-
<Button type="button" variant="outline" size="sm" className="w-full">
|
|
142
|
-
<Plus className="h-4 w-4 mr-2" />
|
|
143
|
-
{t("hierarchy.selectChild")}
|
|
144
|
-
</Button>
|
|
145
|
-
}
|
|
146
|
-
/>
|
|
147
|
-
</div>
|
|
148
|
-
)}
|
|
149
|
-
|
|
150
|
-
{data.children.length > 0 ? (
|
|
151
|
-
<div className="space-y-2">
|
|
152
|
-
{data.children.map((child) => (
|
|
153
|
-
<div
|
|
154
|
-
key={child.id}
|
|
155
|
-
className="flex items-center justify-between p-3 bg-muted rounded-md"
|
|
156
|
-
>
|
|
157
|
-
<div className="flex items-center gap-2">
|
|
158
|
-
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
|
159
|
-
<span className="text-sm">{child.name}</span>
|
|
160
|
-
</div>
|
|
161
|
-
<Button
|
|
162
|
-
type="button"
|
|
163
|
-
variant="ghost"
|
|
164
|
-
size="sm"
|
|
165
|
-
onClick={() => handleRemoveChild(child.id)}
|
|
166
|
-
>
|
|
167
|
-
<Minus className="h-4 w-4" />
|
|
168
|
-
</Button>
|
|
169
|
-
</div>
|
|
170
|
-
))}
|
|
171
|
-
</div>
|
|
172
|
-
) : (
|
|
173
|
-
<div className="text-sm text-muted-foreground text-center py-4">
|
|
174
|
-
{t("hierarchy.noChildren")}
|
|
175
|
-
</div>
|
|
176
|
-
)}
|
|
177
|
-
</div>
|
|
178
|
-
</div>
|
|
179
|
-
);
|
|
180
|
-
}
|
|
@@ -1,212 +0,0 @@
|
|
|
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 { useTranslation } from "react-i18next";
|
|
20
|
-
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
|
21
|
-
import { RELATIONS_ACCORDION_ITEMS } from "../constants";
|
|
22
|
-
import { HierarchyPanel } from "./hierarchy-panel";
|
|
23
|
-
import { DependencyPanel } from "./dependency-panel";
|
|
24
|
-
import { LinksPanel } from "./links-panel";
|
|
25
|
-
import { AttachmentsPanel } from "./attachments-panel";
|
|
26
|
-
import { KnowledgePanel } from "./knowledge-panel";
|
|
27
|
-
import { type RelationsData } from "../types";
|
|
28
|
-
import {
|
|
29
|
-
listChildStoriesFn,
|
|
30
|
-
getDependenciesFn,
|
|
31
|
-
getLinksFn,
|
|
32
|
-
getAttachmentsFn,
|
|
33
|
-
getLinkedPrdsFn,
|
|
34
|
-
getLinkedDocsFn,
|
|
35
|
-
} from "@/server/serverFns/story";
|
|
36
|
-
import { toast } from "sonner";
|
|
37
|
-
|
|
38
|
-
export interface RelationsTabProps {
|
|
39
|
-
storyId: string;
|
|
40
|
-
initialData?: RelationsData;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function RelationsTab({ storyId, initialData }: RelationsTabProps) {
|
|
44
|
-
const { t } = useTranslation("story");
|
|
45
|
-
const [data, setData] = React.useState<RelationsData>(
|
|
46
|
-
initialData || {
|
|
47
|
-
hierarchy: { parent: null, children: [], hasChildren: false },
|
|
48
|
-
dependencies: { blocking: [], blockedBy: [], relatesTo: [] },
|
|
49
|
-
links: [],
|
|
50
|
-
attachments: [],
|
|
51
|
-
knowledge: { prds: [], docs: [] },
|
|
52
|
-
}
|
|
53
|
-
);
|
|
54
|
-
const [loading, setLoading] = React.useState(false);
|
|
55
|
-
|
|
56
|
-
const refreshAll = React.useCallback(async () => {
|
|
57
|
-
setLoading(true);
|
|
58
|
-
try {
|
|
59
|
-
const [childrenStories, deps, links, attachments, prds, docs] = await Promise.all([
|
|
60
|
-
listChildStoriesFn({ data: { parentId: storyId } }),
|
|
61
|
-
getDependenciesFn({ data: { storyId } }),
|
|
62
|
-
getLinksFn({ data: { storyId } }),
|
|
63
|
-
getAttachmentsFn({ data: { storyId } }),
|
|
64
|
-
getLinkedPrdsFn({ data: { storyId } }),
|
|
65
|
-
getLinkedDocsFn({ data: { storyId } }),
|
|
66
|
-
]);
|
|
67
|
-
|
|
68
|
-
setData({
|
|
69
|
-
hierarchy: {
|
|
70
|
-
parent: null,
|
|
71
|
-
children: childrenStories,
|
|
72
|
-
hasChildren: childrenStories.length > 0,
|
|
73
|
-
},
|
|
74
|
-
dependencies: deps as RelationsData["dependencies"],
|
|
75
|
-
links,
|
|
76
|
-
attachments,
|
|
77
|
-
knowledge: {
|
|
78
|
-
prds,
|
|
79
|
-
docs,
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
} catch (error) {
|
|
83
|
-
console.error(t("relationsTab.refreshFailed"), error);
|
|
84
|
-
toast.error(t("relationsTab.refreshFailed"));
|
|
85
|
-
} finally {
|
|
86
|
-
setLoading(false);
|
|
87
|
-
}
|
|
88
|
-
}, [storyId]);
|
|
89
|
-
|
|
90
|
-
React.useEffect(() => {
|
|
91
|
-
if (!initialData) {
|
|
92
|
-
refreshAll();
|
|
93
|
-
}
|
|
94
|
-
}, [initialData, refreshAll]);
|
|
95
|
-
|
|
96
|
-
const updateHierarchy = React.useCallback(
|
|
97
|
-
(updates: Partial<RelationsData["hierarchy"]>) => {
|
|
98
|
-
setData((prev) => ({
|
|
99
|
-
...prev,
|
|
100
|
-
hierarchy: { ...prev.hierarchy, ...updates },
|
|
101
|
-
}));
|
|
102
|
-
},
|
|
103
|
-
[]
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
const updateDependencies = React.useCallback(
|
|
107
|
-
(updates: Partial<RelationsData["dependencies"]>) => {
|
|
108
|
-
setData((prev) => ({
|
|
109
|
-
...prev,
|
|
110
|
-
dependencies: { ...prev.dependencies, ...updates },
|
|
111
|
-
}));
|
|
112
|
-
},
|
|
113
|
-
[]
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
const updateLinks = React.useCallback(
|
|
117
|
-
(newLinks: RelationsData["links"]) => {
|
|
118
|
-
setData((prev) => ({ ...prev, links: newLinks }));
|
|
119
|
-
},
|
|
120
|
-
[]
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
const updateAttachments = React.useCallback(
|
|
124
|
-
(newAttachments: RelationsData["attachments"]) => {
|
|
125
|
-
setData((prev) => ({ ...prev, attachments: newAttachments }));
|
|
126
|
-
},
|
|
127
|
-
[]
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
const updateKnowledge = React.useCallback(
|
|
131
|
-
(updates: Partial<RelationsData["knowledge"]>) => {
|
|
132
|
-
setData((prev) => ({
|
|
133
|
-
...prev,
|
|
134
|
-
knowledge: { ...prev.knowledge, ...updates },
|
|
135
|
-
}));
|
|
136
|
-
},
|
|
137
|
-
[]
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
return (
|
|
141
|
-
<div className="space-y-4">
|
|
142
|
-
<div className="flex items-center justify-between">
|
|
143
|
-
<h3 className="text-lg font-semibold">{t("relationsTab.title")}</h3>
|
|
144
|
-
<button
|
|
145
|
-
type="button"
|
|
146
|
-
onClick={refreshAll}
|
|
147
|
-
disabled={loading}
|
|
148
|
-
className="text-sm text-muted-foreground hover:text-foreground disabled:opacity-50"
|
|
149
|
-
>
|
|
150
|
-
{t(loading ? "relationsTab.refreshing" : "relationsTab.refresh")}
|
|
151
|
-
</button>
|
|
152
|
-
</div>
|
|
153
|
-
|
|
154
|
-
<Accordion type="multiple" defaultValue={Object.values(RELATIONS_ACCORDION_ITEMS)} className="w-full">
|
|
155
|
-
<AccordionItem value={RELATIONS_ACCORDION_ITEMS.HIERARCHY}>
|
|
156
|
-
<AccordionTrigger>{t("relationsTab.hierarchy")}</AccordionTrigger>
|
|
157
|
-
<AccordionContent>
|
|
158
|
-
<HierarchyPanel
|
|
159
|
-
storyId={storyId}
|
|
160
|
-
data={data.hierarchy}
|
|
161
|
-
onUpdate={updateHierarchy}
|
|
162
|
-
/>
|
|
163
|
-
</AccordionContent>
|
|
164
|
-
</AccordionItem>
|
|
165
|
-
|
|
166
|
-
<AccordionItem value={RELATIONS_ACCORDION_ITEMS.DEPENDENCIES}>
|
|
167
|
-
<AccordionTrigger>{t("relationsTab.dependencies")}</AccordionTrigger>
|
|
168
|
-
<AccordionContent>
|
|
169
|
-
<DependencyPanel
|
|
170
|
-
storyId={storyId}
|
|
171
|
-
data={data.dependencies}
|
|
172
|
-
onUpdate={updateDependencies}
|
|
173
|
-
/>
|
|
174
|
-
</AccordionContent>
|
|
175
|
-
</AccordionItem>
|
|
176
|
-
|
|
177
|
-
<AccordionItem value={RELATIONS_ACCORDION_ITEMS.LINKS}>
|
|
178
|
-
<AccordionTrigger>{t("relationsTab.links")}</AccordionTrigger>
|
|
179
|
-
<AccordionContent>
|
|
180
|
-
<LinksPanel
|
|
181
|
-
storyId={storyId}
|
|
182
|
-
data={data.links}
|
|
183
|
-
onUpdate={updateLinks}
|
|
184
|
-
/>
|
|
185
|
-
</AccordionContent>
|
|
186
|
-
</AccordionItem>
|
|
187
|
-
|
|
188
|
-
<AccordionItem value={RELATIONS_ACCORDION_ITEMS.ATTACHMENTS}>
|
|
189
|
-
<AccordionTrigger>{t("relationsTab.attachments")}</AccordionTrigger>
|
|
190
|
-
<AccordionContent>
|
|
191
|
-
<AttachmentsPanel
|
|
192
|
-
storyId={storyId}
|
|
193
|
-
data={data.attachments}
|
|
194
|
-
onUpdate={updateAttachments}
|
|
195
|
-
/>
|
|
196
|
-
</AccordionContent>
|
|
197
|
-
</AccordionItem>
|
|
198
|
-
|
|
199
|
-
<AccordionItem value={RELATIONS_ACCORDION_ITEMS.KNOWLEDGE}>
|
|
200
|
-
<AccordionTrigger>{t("relationsTab.knowledge")}</AccordionTrigger>
|
|
201
|
-
<AccordionContent>
|
|
202
|
-
<KnowledgePanel
|
|
203
|
-
storyId={storyId}
|
|
204
|
-
data={data.knowledge}
|
|
205
|
-
onUpdate={updateKnowledge}
|
|
206
|
-
/>
|
|
207
|
-
</AccordionContent>
|
|
208
|
-
</AccordionItem>
|
|
209
|
-
</Accordion>
|
|
210
|
-
</div>
|
|
211
|
-
);
|
|
212
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
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 { getBoringAvatarUrl } from "@/lib/avatar";
|
|
19
|
-
|
|
20
|
-
export type Notification = {
|
|
21
|
-
id: string;
|
|
22
|
-
avatar: string;
|
|
23
|
-
fallback: string;
|
|
24
|
-
text: string;
|
|
25
|
-
time: string;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export const notifications: Notification[] = [
|
|
29
|
-
{
|
|
30
|
-
id: "1",
|
|
31
|
-
avatar: getBoringAvatarUrl("order-1", { size: 40, square: true }),
|
|
32
|
-
fallback: "OM",
|
|
33
|
-
text: "New order received.",
|
|
34
|
-
time: "10m ago",
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
id: "2",
|
|
38
|
-
avatar: getBoringAvatarUrl("server-upgrade", { size: 40, square: true }),
|
|
39
|
-
fallback: "JL",
|
|
40
|
-
text: "Server upgrade completed.",
|
|
41
|
-
time: "1h ago",
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
id: "3",
|
|
45
|
-
avatar: getBoringAvatarUrl("new-user- signup", { size: 40, square: true }),
|
|
46
|
-
fallback: "HH",
|
|
47
|
-
text: "New user signed up.",
|
|
48
|
-
time: "2h ago",
|
|
49
|
-
},
|
|
50
|
-
];
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"routeFiles": [
|
|
3
|
-
"src/routes/_nav/tags/route.tsx",
|
|
4
|
-
"src/routes/_nav/tags/index.tsx",
|
|
5
|
-
"src/routes/_nav/tags/$id/route.tsx",
|
|
6
|
-
"src/routes/_nav/tags/settings/route.tsx",
|
|
7
|
-
"src/routes/_nav/inspiration/route.tsx",
|
|
8
|
-
"src/routes/_nav/prd/route.tsx",
|
|
9
|
-
"src/routes/_nav/prd/index.tsx",
|
|
10
|
-
"src/routes/_nav/prd/$id/route.tsx",
|
|
11
|
-
"src/routes/share/prd/$prdId/route.tsx"
|
|
12
|
-
]
|
|
13
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
// AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
|
|
2
|
-
import type { ProFeature } from '@allbluecn/kernel'
|
|
3
|
-
|
|
4
|
-
export const activePluginIds = ['@allbluecn/tags', '@allbluecn/inspiration', '@allbluecn/prd'] as const
|
|
5
|
-
export type ActivePluginId = (typeof activePluginIds)[number]
|
|
6
|
-
|
|
7
|
-
export const implementedModules: Record<ProFeature, boolean> = {
|
|
8
|
-
billing: false,
|
|
9
|
-
hostedAi: false,
|
|
10
|
-
sso: false,
|
|
11
|
-
audit: false,
|
|
12
|
-
permissions: false,
|
|
13
|
-
license: false,
|
|
14
|
-
versionHistory: false,
|
|
15
|
-
automation: false,
|
|
16
|
-
templates: false,
|
|
17
|
-
analytics: false,
|
|
18
|
-
globalSearch: false,
|
|
19
|
-
compliance: false,
|
|
20
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
// AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
|
|
2
|
-
// profile: oss (validated: 3 plugins)
|
|
3
|
-
// 拓扑序:@allbluecn/tags -> @allbluecn/inspiration -> @allbluecn/prd
|
|
4
|
-
import tags from '@allbluecn/plugins-core/tags'
|
|
5
|
-
import inspiration from '@allbluecn/plugins-core/inspiration'
|
|
6
|
-
import prd from '@allbluecn/plugins-core/prd'
|
|
7
|
-
import { defineProfile, type ProfileDefinition } from '@allbluecn/kernel'
|
|
8
|
-
|
|
9
|
-
export const profile: ProfileDefinition = defineProfile({
|
|
10
|
-
id: 'oss',
|
|
11
|
-
plugins: [tags, inspiration, prd],
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
export const pluginIds: string[] = ['@allbluecn/tags', '@allbluecn/inspiration', '@allbluecn/prd']
|
|
15
|
-
|
|
16
|
-
export const pluginMiddleware: unknown[] = [
|
|
17
|
-
...tags.contributes.serverMiddleware ?? [],
|
|
18
|
-
...inspiration.contributes.serverMiddleware ?? [],
|
|
19
|
-
...prd.contributes.serverMiddleware ?? [],
|
|
20
|
-
]
|