@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,343 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Adapted from @hextaui/team-notifications (https://www.hextaui.com)
|
|
3
|
+
import { useMemo, useState } from 'react'
|
|
4
|
+
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'
|
|
5
|
+
import { useNavigate } from '@tanstack/react-router'
|
|
6
|
+
import { useTranslation } from 'react-i18next'
|
|
7
|
+
import {
|
|
8
|
+
Bell,
|
|
9
|
+
Check,
|
|
10
|
+
CheckCheck,
|
|
11
|
+
CreditCard,
|
|
12
|
+
Megaphone,
|
|
13
|
+
MessageSquare,
|
|
14
|
+
MoreVertical,
|
|
15
|
+
ShieldCheck,
|
|
16
|
+
Trash2,
|
|
17
|
+
Users,
|
|
18
|
+
X,
|
|
19
|
+
} from 'lucide-react'
|
|
20
|
+
import type { LucideIcon } from 'lucide-react'
|
|
21
|
+
import { Badge } from '@/components/ui/badge'
|
|
22
|
+
import { Button } from '@/components/ui/button'
|
|
23
|
+
import {
|
|
24
|
+
Card,
|
|
25
|
+
CardContent,
|
|
26
|
+
CardDescription,
|
|
27
|
+
CardHeader,
|
|
28
|
+
CardTitle,
|
|
29
|
+
} from '@/components/ui/card'
|
|
30
|
+
import {
|
|
31
|
+
DropdownMenu,
|
|
32
|
+
DropdownMenuContent,
|
|
33
|
+
DropdownMenuItem,
|
|
34
|
+
DropdownMenuSeparator,
|
|
35
|
+
DropdownMenuTrigger,
|
|
36
|
+
} from '@/components/ui/dropdown-menu'
|
|
37
|
+
import {
|
|
38
|
+
Empty,
|
|
39
|
+
EmptyHeader,
|
|
40
|
+
EmptyMedia,
|
|
41
|
+
EmptyTitle,
|
|
42
|
+
} from '@/components/ui/empty'
|
|
43
|
+
import {
|
|
44
|
+
Select,
|
|
45
|
+
SelectContent,
|
|
46
|
+
SelectItem,
|
|
47
|
+
SelectTrigger,
|
|
48
|
+
SelectValue,
|
|
49
|
+
} from '@/components/ui/select'
|
|
50
|
+
import { Separator } from '@/components/ui/separator'
|
|
51
|
+
import { cn } from '@/lib/utils'
|
|
52
|
+
import { useNotifications, NOTIFICATION_LIST_KEY, NOTIFICATION_ROOT_KEY } from '@/hooks/use-notifications'
|
|
53
|
+
import type { NotificationCategory, NotificationItem } from '@allbluecn/shared'
|
|
54
|
+
import {
|
|
55
|
+
clearNotificationsFn,
|
|
56
|
+
deleteNotificationFn,
|
|
57
|
+
listNotificationsFn,
|
|
58
|
+
markAllNotificationsReadFn,
|
|
59
|
+
markNotificationReadFn,
|
|
60
|
+
} from '@/server/serverFns/notifications'
|
|
61
|
+
|
|
62
|
+
export const CATEGORY_ICONS: Record<NotificationCategory, LucideIcon> & { all: LucideIcon } = {
|
|
63
|
+
all: Bell,
|
|
64
|
+
team: Users,
|
|
65
|
+
collaboration: MessageSquare,
|
|
66
|
+
system: Megaphone,
|
|
67
|
+
billing: CreditCard,
|
|
68
|
+
security: ShieldCheck,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const CATEGORY_FILTERS: ('all' | NotificationCategory)[] = [
|
|
72
|
+
'all',
|
|
73
|
+
'team',
|
|
74
|
+
'collaboration',
|
|
75
|
+
'system',
|
|
76
|
+
'billing',
|
|
77
|
+
'security',
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
type StatusFilter = 'all' | 'unread' | 'read'
|
|
81
|
+
|
|
82
|
+
const STATUS_FILTERS: StatusFilter[] = ['all', 'unread', 'read']
|
|
83
|
+
|
|
84
|
+
const STATUS_LABEL_KEYS: Record<StatusFilter, string> = {
|
|
85
|
+
all: 'filters.all',
|
|
86
|
+
unread: 'unreadBadge',
|
|
87
|
+
read: 'filters.read',
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function formatRelativeTime(
|
|
91
|
+
createdAt: string,
|
|
92
|
+
t: (key: string, opts?: Record<string, unknown>) => string
|
|
93
|
+
): string {
|
|
94
|
+
const diffMs = Date.now() - new Date(createdAt).getTime()
|
|
95
|
+
const minutes = Math.floor(diffMs / 60000)
|
|
96
|
+
const hours = Math.floor(minutes / 60)
|
|
97
|
+
const days = Math.floor(hours / 24)
|
|
98
|
+
if (minutes < 1) return t('relativeTime.justNow')
|
|
99
|
+
if (minutes < 60) return t('relativeTime.minutesAgo', { count: minutes })
|
|
100
|
+
if (hours < 24) return t('relativeTime.hoursAgo', { count: hours })
|
|
101
|
+
return t('relativeTime.daysAgo', { count: Math.min(days, 30) })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function TeamNotificationsPanel() {
|
|
105
|
+
const { t } = useTranslation('notifications')
|
|
106
|
+
const navigate = useNavigate()
|
|
107
|
+
const queryClient = useQueryClient()
|
|
108
|
+
const { unreadCount } = useNotifications({ toastEnabled: false })
|
|
109
|
+
const [categoryFilter, setCategoryFilter] = useState<'all' | NotificationCategory>('all')
|
|
110
|
+
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all')
|
|
111
|
+
|
|
112
|
+
const { data, isLoading, fetchNextPage, isFetchingNextPage, hasNextPage } = useInfiniteQuery({
|
|
113
|
+
queryKey: [...NOTIFICATION_LIST_KEY, categoryFilter === 'all' ? undefined : categoryFilter],
|
|
114
|
+
queryFn: async ({ pageParam }) => {
|
|
115
|
+
const base = categoryFilter === 'all' ? {} : { category: categoryFilter }
|
|
116
|
+
return listNotificationsFn({
|
|
117
|
+
data: pageParam ? { ...base, cursor: pageParam } : base,
|
|
118
|
+
})
|
|
119
|
+
},
|
|
120
|
+
initialPageParam: undefined as string | undefined,
|
|
121
|
+
getNextPageParam: (lastPage) =>
|
|
122
|
+
lastPage.hasMore ? lastPage.items[lastPage.items.length - 1].id : undefined,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const items = useMemo(() => data?.pages.flatMap((page) => page.items) ?? [], [data])
|
|
126
|
+
|
|
127
|
+
const filteredItems = useMemo(
|
|
128
|
+
() =>
|
|
129
|
+
items.filter((item) => {
|
|
130
|
+
if (statusFilter === 'unread') return !item.readAt
|
|
131
|
+
if (statusFilter === 'read') return !!item.readAt
|
|
132
|
+
return true
|
|
133
|
+
}),
|
|
134
|
+
[items, statusFilter]
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const invalidateNotificationData = () => {
|
|
138
|
+
queryClient.invalidateQueries({ queryKey: NOTIFICATION_ROOT_KEY })
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const handleMarkRead = async (id: string) => {
|
|
142
|
+
await markNotificationReadFn({ data: { id } })
|
|
143
|
+
invalidateNotificationData()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const handleMarkAllRead = async () => {
|
|
147
|
+
await markAllNotificationsReadFn({ data: {} })
|
|
148
|
+
invalidateNotificationData()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const handleDelete = async (id: string) => {
|
|
152
|
+
await deleteNotificationFn({ data: { id } })
|
|
153
|
+
invalidateNotificationData()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const handleClearAll = async () => {
|
|
157
|
+
await clearNotificationsFn({ data: {} })
|
|
158
|
+
invalidateNotificationData()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const handleOpenItem = (item: NotificationItem) => {
|
|
162
|
+
if (!item.readAt) void handleMarkRead(item.id)
|
|
163
|
+
if (item.link) navigate({ to: item.link as never })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<Card className="w-full flex-1 bg-transparent shadow-none ring-0">
|
|
168
|
+
<CardHeader className="shrink-0">
|
|
169
|
+
<div className="flex flex-col gap-4">
|
|
170
|
+
<div className="flex flex-col flex-wrap gap-3 md:flex-row md:items-start md:justify-between">
|
|
171
|
+
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
|
172
|
+
{/* <CardTitle>{t('title')}</CardTitle> */}
|
|
173
|
+
<CardDescription>
|
|
174
|
+
{unreadCount > 0 ? (
|
|
175
|
+
<span className="text-primary">{t('unreadCount', { count: unreadCount })}</span>
|
|
176
|
+
) : (
|
|
177
|
+
t('allCaughtUp')
|
|
178
|
+
)}
|
|
179
|
+
</CardDescription>
|
|
180
|
+
</div>
|
|
181
|
+
<div className="flex gap-2">
|
|
182
|
+
{unreadCount > 0 && (
|
|
183
|
+
<Button onClick={handleMarkAllRead} size="sm" type="button" variant="outline">
|
|
184
|
+
<CheckCheck className="size-4" />
|
|
185
|
+
{t('actions.markAllRead')}
|
|
186
|
+
</Button>
|
|
187
|
+
)}
|
|
188
|
+
{items.length > 0 && (
|
|
189
|
+
<Button onClick={handleClearAll} size="sm" type="button" variant="outline">
|
|
190
|
+
<X className="size-4" />
|
|
191
|
+
{t('actions.clearAll')}
|
|
192
|
+
</Button>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
<div className="flex flex-wrap gap-2">
|
|
197
|
+
<Select
|
|
198
|
+
onValueChange={(value) => setCategoryFilter(value as 'all' | NotificationCategory)}
|
|
199
|
+
value={categoryFilter}
|
|
200
|
+
>
|
|
201
|
+
<SelectTrigger className="w-full md:w-[140px]">
|
|
202
|
+
<SelectValue placeholder={t('filters.allTypes')} />
|
|
203
|
+
</SelectTrigger>
|
|
204
|
+
<SelectContent>
|
|
205
|
+
{CATEGORY_FILTERS.map((key) => (
|
|
206
|
+
<SelectItem key={key} value={key}>
|
|
207
|
+
{t(key === 'all' ? 'filters.allTypes' : `tabs.${key}`)}
|
|
208
|
+
</SelectItem>
|
|
209
|
+
))}
|
|
210
|
+
</SelectContent>
|
|
211
|
+
</Select>
|
|
212
|
+
<Select
|
|
213
|
+
onValueChange={(value) => setStatusFilter(value as StatusFilter)}
|
|
214
|
+
value={statusFilter}
|
|
215
|
+
>
|
|
216
|
+
<SelectTrigger className="w-full md:w-[140px]">
|
|
217
|
+
<SelectValue placeholder={t('filters.all')} />
|
|
218
|
+
</SelectTrigger>
|
|
219
|
+
<SelectContent>
|
|
220
|
+
{STATUS_FILTERS.map((key) => (
|
|
221
|
+
<SelectItem key={key} value={key}>
|
|
222
|
+
{t(STATUS_LABEL_KEYS[key])}
|
|
223
|
+
</SelectItem>
|
|
224
|
+
))}
|
|
225
|
+
</SelectContent>
|
|
226
|
+
</Select>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</CardHeader>
|
|
230
|
+
<CardContent className="min-h-0 flex-1 overflow-y-auto">
|
|
231
|
+
{filteredItems.length === 0 ? (
|
|
232
|
+
<Empty>
|
|
233
|
+
<EmptyHeader>
|
|
234
|
+
<EmptyMedia variant="icon">
|
|
235
|
+
<Bell className="size-6" />
|
|
236
|
+
</EmptyMedia>
|
|
237
|
+
<EmptyTitle>
|
|
238
|
+
{categoryFilter !== 'all' || statusFilter !== 'all'
|
|
239
|
+
? t('emptyFiltered')
|
|
240
|
+
: t('empty')}
|
|
241
|
+
</EmptyTitle>
|
|
242
|
+
</EmptyHeader>
|
|
243
|
+
</Empty>
|
|
244
|
+
) : (
|
|
245
|
+
<div className="flex flex-col gap-0">
|
|
246
|
+
{filteredItems.map((item, idx) => {
|
|
247
|
+
const Icon = CATEGORY_ICONS[item.category]
|
|
248
|
+
const isUnread = !item.readAt
|
|
249
|
+
const isFirst = idx === 0
|
|
250
|
+
const isLast = idx === filteredItems.length - 1
|
|
251
|
+
return (
|
|
252
|
+
<div key={item.id}>
|
|
253
|
+
<div
|
|
254
|
+
className={cn(
|
|
255
|
+
'flex cursor-pointer items-start gap-4 p-4 transition-colors',
|
|
256
|
+
isUnread && 'bg-primary/5',
|
|
257
|
+
'hover:bg-muted/50',
|
|
258
|
+
isFirst && 'rounded-t-lg',
|
|
259
|
+
isLast && 'rounded-b-lg'
|
|
260
|
+
)}
|
|
261
|
+
onClick={() => handleOpenItem(item)}
|
|
262
|
+
>
|
|
263
|
+
<div
|
|
264
|
+
className={cn(
|
|
265
|
+
'flex size-10 shrink-0 items-center justify-center rounded-full',
|
|
266
|
+
isUnread ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'
|
|
267
|
+
)}
|
|
268
|
+
>
|
|
269
|
+
<Icon className="size-5" />
|
|
270
|
+
</div>
|
|
271
|
+
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
|
272
|
+
<div className="flex flex-wrap items-center gap-1">
|
|
273
|
+
<span className="font-medium text-sm">{item.title}</span>
|
|
274
|
+
{isUnread && <div className="size-2 shrink-0 rounded-full bg-primary" />}
|
|
275
|
+
<Badge className="text-xs" variant="outline">
|
|
276
|
+
{t(`tabs.${item.category}`)}
|
|
277
|
+
</Badge>
|
|
278
|
+
</div>
|
|
279
|
+
{item.body && (
|
|
280
|
+
<p className="wrap-break-word text-muted-foreground text-sm">{item.body}</p>
|
|
281
|
+
)}
|
|
282
|
+
<span className="text-muted-foreground text-xs">
|
|
283
|
+
{formatRelativeTime(item.createdAt, t)}
|
|
284
|
+
</span>
|
|
285
|
+
</div>
|
|
286
|
+
<DropdownMenu>
|
|
287
|
+
<DropdownMenuTrigger asChild>
|
|
288
|
+
<Button
|
|
289
|
+
aria-label={t('moreOptions')}
|
|
290
|
+
size="icon-sm"
|
|
291
|
+
type="button"
|
|
292
|
+
variant="ghost"
|
|
293
|
+
onClick={(e) => e.stopPropagation()}
|
|
294
|
+
>
|
|
295
|
+
<MoreVertical className="size-4" />
|
|
296
|
+
</Button>
|
|
297
|
+
</DropdownMenuTrigger>
|
|
298
|
+
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
|
|
299
|
+
{isUnread && (
|
|
300
|
+
<DropdownMenuItem onClick={() => void handleMarkRead(item.id)}>
|
|
301
|
+
<Check className="size-4" />
|
|
302
|
+
{t('actions.markRead')}
|
|
303
|
+
</DropdownMenuItem>
|
|
304
|
+
)}
|
|
305
|
+
{item.link && (
|
|
306
|
+
<DropdownMenuItem onClick={() => navigate({ to: item.link as never })}>
|
|
307
|
+
<MessageSquare className="size-4" />
|
|
308
|
+
{t('actions.view')}
|
|
309
|
+
</DropdownMenuItem>
|
|
310
|
+
)}
|
|
311
|
+
<DropdownMenuSeparator />
|
|
312
|
+
<DropdownMenuItem
|
|
313
|
+
variant="destructive"
|
|
314
|
+
onClick={() => void handleDelete(item.id)}
|
|
315
|
+
>
|
|
316
|
+
<Trash2 className="size-4" />
|
|
317
|
+
{t('actions.delete')}
|
|
318
|
+
</DropdownMenuItem>
|
|
319
|
+
</DropdownMenuContent>
|
|
320
|
+
</DropdownMenu>
|
|
321
|
+
</div>
|
|
322
|
+
{idx < filteredItems.length - 1 && <Separator />}
|
|
323
|
+
</div>
|
|
324
|
+
)
|
|
325
|
+
})}
|
|
326
|
+
{Boolean(hasNextPage) && (
|
|
327
|
+
<Button
|
|
328
|
+
className="mt-4"
|
|
329
|
+
disabled={isFetchingNextPage}
|
|
330
|
+
onClick={() => void fetchNextPage()}
|
|
331
|
+
size="sm"
|
|
332
|
+
type="button"
|
|
333
|
+
variant="ghost"
|
|
334
|
+
>
|
|
335
|
+
{t('actions.loadMore')}
|
|
336
|
+
</Button>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
)}
|
|
340
|
+
</CardContent>
|
|
341
|
+
</Card>
|
|
342
|
+
)
|
|
343
|
+
}
|
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
approveTeamInvitationFn,
|
|
32
32
|
approveTeamJoinRequestFn,
|
|
33
33
|
rejectTeamJoinRequestFn,
|
|
34
|
+
deleteTeamInvitationFn,
|
|
35
|
+
deleteTeamJoinRequestFn,
|
|
34
36
|
removeTeamMemberFn,
|
|
35
37
|
leaveTeamFn,
|
|
36
38
|
clearExpiredInvitationsFn,
|
|
@@ -44,6 +46,7 @@ import {
|
|
|
44
46
|
CardTitle,
|
|
45
47
|
} from "@/components/ui/card"
|
|
46
48
|
import { Progress } from "@/components/ui/progress"
|
|
49
|
+
import { Skeleton } from "@/components/ui/skeleton"
|
|
47
50
|
import {
|
|
48
51
|
AlertDialog,
|
|
49
52
|
AlertDialogAction,
|
|
@@ -331,6 +334,36 @@ export default function TeamClient({ user }: TeamClientProps) {
|
|
|
331
334
|
await rejectRequestMutation.mutateAsync(requestId)
|
|
332
335
|
}
|
|
333
336
|
|
|
337
|
+
const deleteInvitationMutation = useMutation({
|
|
338
|
+
mutationFn: (invitationId: string) => deleteTeamInvitationFn({ data: { invitationId } }),
|
|
339
|
+
onSuccess: () => {
|
|
340
|
+
toast.success(t("team.invite.deleteRecord"))
|
|
341
|
+
invalidate()
|
|
342
|
+
},
|
|
343
|
+
onError: (err) => {
|
|
344
|
+
toast.error(err instanceof Error ? err.message : t("team.inviteCreateFailed"))
|
|
345
|
+
},
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
const deleteRequestMutation = useMutation({
|
|
349
|
+
mutationFn: (requestId: string) => deleteTeamJoinRequestFn({ data: { requestId } }),
|
|
350
|
+
onSuccess: () => {
|
|
351
|
+
toast.success(t("team.invite.deleteRecord"))
|
|
352
|
+
invalidate()
|
|
353
|
+
},
|
|
354
|
+
onError: (err) => {
|
|
355
|
+
toast.error(err instanceof Error ? err.message : t("team.inviteCreateFailed"))
|
|
356
|
+
},
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
const handleDeleteInvitation = async (invitationId: string) => {
|
|
360
|
+
await deleteInvitationMutation.mutateAsync(invitationId)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const handleDeleteRequest = async (requestId: string) => {
|
|
364
|
+
await deleteRequestMutation.mutateAsync(requestId)
|
|
365
|
+
}
|
|
366
|
+
|
|
334
367
|
const handleRemove = async (memberId: string) => {
|
|
335
368
|
const membershipId = membershipIdByUserId[memberId]
|
|
336
369
|
if (membershipId) {
|
|
@@ -425,18 +458,24 @@ export default function TeamClient({ user }: TeamClientProps) {
|
|
|
425
458
|
/>
|
|
426
459
|
</div>
|
|
427
460
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
461
|
+
{inviteData ? (
|
|
462
|
+
<TeamInvitations
|
|
463
|
+
invitations={mappedInvitations}
|
|
464
|
+
joinRequests={mappedJoinRequests}
|
|
465
|
+
onCreate={handleCreate}
|
|
466
|
+
onRefreshLink={handleRefreshLink}
|
|
467
|
+
onRevoke={handleRevoke}
|
|
468
|
+
onResend={handleResend}
|
|
469
|
+
onApproveLink={handleApproveLink}
|
|
470
|
+
onApproveRequest={handleApproveRequest}
|
|
471
|
+
onRejectRequest={handleRejectRequest}
|
|
472
|
+
onDeleteInvitation={handleDeleteInvitation}
|
|
473
|
+
onDeleteRequest={handleDeleteRequest}
|
|
474
|
+
onClearExpired={handleClearExpired}
|
|
475
|
+
/>
|
|
476
|
+
) : (
|
|
477
|
+
<Skeleton className="h-96 w-full rounded-lg" />
|
|
478
|
+
)}
|
|
440
479
|
|
|
441
480
|
<TeamMemberList
|
|
442
481
|
members={mappedMembers}
|