@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.
Files changed (99) hide show
  1. package/package.json +12 -8
  2. package/src/components/knowledge/knowledge-toolbar.tsx +124 -1
  3. package/src/components/layout/sidebar-03/app-sidebar.tsx +3 -4
  4. package/src/components/layout/sidebar-03/nav-notifications.tsx +135 -52
  5. package/src/components/layout/sidebar-03/user-menu.tsx +4 -2
  6. package/src/components/notifications/settings-notifications.tsx +198 -0
  7. package/src/components/notifications/team-notifications.tsx +343 -0
  8. package/src/components/settings/personal-team/team/team-client.tsx +51 -12
  9. package/src/components/settings/personal-team/team/team-invitations.tsx +181 -181
  10. package/src/components/shared/wave-physics-loader.tsx +155 -0
  11. package/src/components/story/__tests__/dependency-type-badge.test.ts +89 -0
  12. package/src/components/story/constants.ts +124 -51
  13. package/src/components/story/create-story-dialog.tsx +12 -6
  14. package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +114 -0
  15. package/src/components/story/detail/activity-timeline.tsx +15 -0
  16. package/src/components/story/detail/story-detail-body.tsx +14 -3
  17. package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +748 -0
  18. package/src/components/story/detail/story-detail-toolbar/categories.tsx +23 -18
  19. package/src/components/story/detail/story-detail-toolbar/children.tsx +51 -274
  20. package/src/components/story/detail/story-detail-toolbar/dependencies.tsx +13 -45
  21. package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +473 -0
  22. package/src/components/story/detail/story-detail-toolbar.tsx +237 -10
  23. package/src/components/story/display/__tests__/use-paged-story-groups.test.ts +152 -1
  24. package/src/components/story/display/use-paged-story-groups.ts +370 -28
  25. package/src/components/story/inline-editors/story-select-dialog.tsx +107 -16
  26. package/src/components/story/peek-panel.tsx +8 -1
  27. package/src/components/story/relations/__tests__/attachment-manage-dialog.test.tsx +171 -0
  28. package/src/components/story/relations/__tests__/knowledge-select-dialog.test.tsx +174 -0
  29. package/src/components/story/relations/__tests__/link-manage-dialog.test.tsx +93 -0
  30. package/src/components/story/relations/attachment-manage-dialog.tsx +490 -0
  31. package/src/components/story/relations/file-preview.tsx +173 -0
  32. package/src/components/story/relations/format.ts +120 -0
  33. package/src/{config/sidebar-notifications.ts → components/story/relations/general-groups.ts} +12 -30
  34. package/src/components/story/relations/knowledge-select-dialog.tsx +403 -0
  35. package/src/components/story/relations/link-manage-dialog.tsx +191 -0
  36. package/src/components/story/relations/story-ref-row.tsx +319 -0
  37. package/src/components/story/story-list-view.tsx +3 -1
  38. package/src/components/story/story-panel-header.tsx +4 -1
  39. package/src/components/story/types.ts +30 -18
  40. package/src/components/story/views/story-list/batch-toolbar.tsx +12 -1
  41. package/src/components/story/views/story-list/index.tsx +16 -5
  42. package/src/components/story/views/story-list/row.tsx +5 -4
  43. package/src/components/ui/separator.tsx +1 -1
  44. package/src/hooks/use-notifications.ts +4 -1
  45. package/src/lib/__tests__/datetime.test.ts +16 -1
  46. package/src/lib/changelog/sdk-versions.ts +19 -19
  47. package/src/lib/datetime.ts +1 -0
  48. package/src/lib/story/__tests__/video-embed.test.ts +77 -0
  49. package/src/lib/story/video-embed.ts +59 -0
  50. package/src/routeTree.gen.ts +65 -0
  51. package/src/routes/_nav/notifications/route.lazy.tsx +22 -0
  52. package/src/routes/_nav/notifications/route.tsx +30 -0
  53. package/src/routes/_nav/projects/$projectId/settings/route.lazy.tsx +74 -3
  54. package/src/routes/_nav/projects/$projectId/settings/route.tsx +6 -2
  55. package/src/routes/_nav/settings/notifications/route.lazy.tsx +6 -5
  56. package/src/routes/api/attachments/$id/content.ts +60 -0
  57. package/src/routes/api/attachments/upload.ts +148 -0
  58. package/src/routes/api/notifications/stream.ts +4 -0
  59. package/src/server/notifications/__tests__/notification-service.test.ts +189 -0
  60. package/src/server/notifications/notification-service.ts +137 -27
  61. package/src/server/serverFns/__tests__/project-kb.test.ts +147 -0
  62. package/src/server/serverFns/__tests__/team-invitation.test.ts +73 -7
  63. package/src/server/serverFns/__tests__/team-join-request.test.ts +5 -3
  64. package/src/server/serverFns/knowledge.ts +50 -4
  65. package/src/server/serverFns/locale.ts +15 -3
  66. package/src/server/serverFns/notifications.ts +18 -4
  67. package/src/server/serverFns/project.ts +43 -0
  68. package/src/server/serverFns/story/__tests__/kb-scope.test.ts +56 -0
  69. package/src/server/serverFns/story/__tests__/story-assoc.test.ts +222 -0
  70. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +265 -0
  71. package/src/server/serverFns/story/__tests__/story-groups.test.ts +19 -1
  72. package/src/server/serverFns/story/index.ts +7 -6
  73. package/src/server/serverFns/story/kb-general.ts +39 -0
  74. package/src/server/serverFns/story/kb-scope.ts +52 -0
  75. package/src/server/serverFns/story/link-preview.ts +115 -0
  76. package/src/server/serverFns/story/story-assoc.ts +339 -50
  77. package/src/server/serverFns/story/story-attachments.ts +12 -43
  78. package/src/server/serverFns/story/story-crud.ts +21 -4
  79. package/src/server/serverFns/story/story-dependencies.ts +138 -162
  80. package/src/server/serverFns/story/story-groups.ts +23 -3
  81. package/src/server/serverFns/story/story-links.ts +13 -1
  82. package/src/server/serverFns/story/story-transfer.ts +5 -3
  83. package/src/server/serverFns/story/story-tree.ts +28 -1
  84. package/src/server/serverFns/story/types.ts +37 -0
  85. package/src/server/serverFns/team.ts +51 -2
  86. package/src/styles/globals.css +25 -0
  87. package/vite.shared.ts +2 -0
  88. package/src/components/notifications/notification-list.tsx +0 -232
  89. package/src/components/notifications/notification-preferences.tsx +0 -156
  90. package/src/components/story/detail/story-detail-toolbar/attachments.tsx +0 -73
  91. package/src/components/story/detail/story-detail-toolbar/knowledge.tsx +0 -73
  92. package/src/components/story/detail/story-detail-toolbar/links.tsx +0 -73
  93. package/src/components/story/relation-badge.tsx +0 -57
  94. package/src/components/story/relations/attachments-panel.tsx +0 -179
  95. package/src/components/story/relations/dependency-panel.tsx +0 -190
  96. package/src/components/story/relations/hierarchy-panel.tsx +0 -180
  97. package/src/components/story/relations/knowledge-panel.tsx +0 -284
  98. package/src/components/story/relations/links-panel.tsx +0 -240
  99. 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
- <TeamInvitations
429
- invitations={mappedInvitations}
430
- joinRequests={mappedJoinRequests}
431
- onCreate={handleCreate}
432
- onRefreshLink={handleRefreshLink}
433
- onRevoke={handleRevoke}
434
- onResend={handleResend}
435
- onApproveLink={handleApproveLink}
436
- onApproveRequest={handleApproveRequest}
437
- onRejectRequest={handleRejectRequest}
438
- onClearExpired={handleClearExpired}
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}