@tuturuuu/ui 0.9.0 → 0.10.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.
Files changed (94) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/package.json +6 -5
  3. package/src/components/ui/custom/__tests__/settings-dialog-search.test.ts +78 -0
  4. package/src/components/ui/custom/__tests__/settings-dialog-shell-compile-graph.test.ts +76 -0
  5. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +27 -1
  6. package/src/components/ui/custom/nav-link.test.tsx +165 -0
  7. package/src/components/ui/custom/nav-link.tsx +69 -11
  8. package/src/components/ui/custom/navigation.tsx +1 -0
  9. package/src/components/ui/custom/settings/task-settings.tsx +104 -0
  10. package/src/components/ui/custom/settings-dialog-search-loader.d.ts +5 -0
  11. package/src/components/ui/custom/settings-dialog-search-loader.js +3 -0
  12. package/src/components/ui/custom/settings-dialog-search.ts +75 -0
  13. package/src/components/ui/custom/settings-dialog-shell.tsx +63 -27
  14. package/src/components/ui/custom/workspace-select-helpers.ts +23 -0
  15. package/src/components/ui/custom/workspace-select.tsx +17 -16
  16. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +16 -0
  17. package/src/components/ui/tu-do/boards/__tests__/task-board-form.test.tsx +12 -0
  18. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +4 -328
  19. package/src/components/ui/tu-do/boards/board-share-settings-panel.tsx +351 -0
  20. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +50 -37
  21. package/src/components/ui/tu-do/boards/boardId/enhanced-task-list.tsx +7 -0
  22. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-types.ts +3 -3
  23. package/src/components/ui/tu-do/boards/boardId/kanban/data/use-bulk-resources.ts +59 -5
  24. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/drag-preview.tsx +20 -1
  25. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +263 -21
  26. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +133 -14
  27. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +112 -54
  28. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +8 -2
  29. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +29 -14
  30. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +24 -1
  31. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +7 -0
  32. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +7 -1
  33. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +20 -0
  34. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +10 -0
  35. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +80 -8
  36. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +15 -0
  37. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +9 -0
  38. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +9 -0
  39. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-toolbar.tsx +9 -0
  40. package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +35 -3
  41. package/src/components/ui/tu-do/boards/form.tsx +1 -1
  42. package/src/components/ui/tu-do/hooks/__tests__/useTaskLabelManagement.test.tsx +48 -0
  43. package/src/components/ui/tu-do/hooks/__tests__/useTaskProjectManagement.test.tsx +144 -0
  44. package/src/components/ui/tu-do/hooks/useTaskDialog.ts +7 -0
  45. package/src/components/ui/tu-do/hooks/useTaskLabelManagement.ts +115 -106
  46. package/src/components/ui/tu-do/hooks/useTaskProjectManagement.ts +115 -122
  47. package/src/components/ui/tu-do/progress/task-progress-import-panel.tsx +60 -0
  48. package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +156 -0
  49. package/src/components/ui/tu-do/progress/task-progress-page.tsx +348 -0
  50. package/src/components/ui/tu-do/progress/task-progress-panels.tsx +301 -0
  51. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +26 -0
  52. package/src/components/ui/tu-do/shared/__tests__/assignee-select.test.tsx +81 -10
  53. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +116 -1
  54. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +38 -0
  55. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +128 -7
  56. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +222 -9
  57. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +21 -0
  58. package/src/components/ui/tu-do/shared/__tests__/task-cache-patches.test.ts +147 -0
  59. package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +3 -0
  60. package/src/components/ui/tu-do/shared/assignee-select.tsx +77 -26
  61. package/src/components/ui/tu-do/shared/board-client.tsx +14 -4
  62. package/src/components/ui/tu-do/shared/board-header.tsx +8 -1
  63. package/src/components/ui/tu-do/shared/board-switcher.tsx +70 -38
  64. package/src/components/ui/tu-do/shared/board-user-presence-avatars.tsx +18 -12
  65. package/src/components/ui/tu-do/shared/board-views.tsx +49 -69
  66. package/src/components/ui/tu-do/shared/list-view.tsx +21 -3
  67. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +4 -4
  68. package/src/components/ui/tu-do/shared/task-cache-patches.ts +394 -0
  69. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +21 -1
  70. package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +5 -1
  71. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +25 -2
  72. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +7 -1
  73. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-data.ts +79 -10
  74. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +76 -77
  75. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.test.tsx +63 -0
  76. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.ts +78 -69
  77. package/src/components/ui/tu-do/shared/task-edit-dialog/personal-overrides-section.tsx +28 -8
  78. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/dependencies-section.tsx +14 -3
  79. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/parent-section.tsx +6 -1
  80. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/related-section.tsx +6 -1
  81. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/subtasks-section.tsx +6 -1
  82. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/types/task-relationships.types.ts +8 -0
  83. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +8 -1
  84. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.test.tsx +150 -0
  85. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +61 -35
  86. package/src/components/ui/tu-do/shared/task-edit-dialog/task-relationships-properties.tsx +44 -2
  87. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +9 -0
  88. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +11 -0
  89. package/src/components/ui/tu-do/shared/use-progressive-board-loader.ts +2 -0
  90. package/src/hooks/__tests__/useBoardPresence.test.tsx +191 -0
  91. package/src/hooks/__tests__/useBoardRealtime.test.tsx +24 -144
  92. package/src/hooks/useBoardPresence.ts +364 -0
  93. package/src/hooks/useBoardRealtimeEventHandler.ts +34 -90
  94. package/src/lib/workspace-actions.ts +2 -6
@@ -1,22 +1,8 @@
1
1
  'use client';
2
2
 
3
- import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
- import { Loader2, Share2, Trash2, Users } from '@tuturuuu/icons';
5
- import {
6
- createWorkspaceTaskBoardShare,
7
- deleteWorkspaceTaskBoardShare,
8
- listWorkspaceTaskBoardShares,
9
- listWorkspaceTaskBoardViewableMembers,
10
- updateWorkspaceTaskBoardShare,
11
- type WorkspaceTaskBoardShare,
12
- type WorkspaceTaskBoardSharePermission,
13
- type WorkspaceTaskBoardViewableMember,
14
- } from '@tuturuuu/internal-api/tasks';
3
+ import { Share2 } from '@tuturuuu/icons';
15
4
  import type { WorkspaceTaskBoard } from '@tuturuuu/types';
16
- import { Avatar, AvatarFallback, AvatarImage } from '@tuturuuu/ui/avatar';
17
- import { Badge } from '@tuturuuu/ui/badge';
18
5
  import { Button } from '@tuturuuu/ui/button';
19
- import { Combobox } from '@tuturuuu/ui/custom/combobox';
20
6
  import {
21
7
  Dialog,
22
8
  DialogContent,
@@ -25,13 +11,9 @@ import {
25
11
  DialogHeader,
26
12
  DialogTitle,
27
13
  } from '@tuturuuu/ui/dialog';
28
- import { Input } from '@tuturuuu/ui/input';
29
- import { getInitials } from '@tuturuuu/utils/name-helper';
30
14
  import { useTranslations } from 'next-intl';
31
- import { useRef, useState } from 'react';
32
- import { toast } from 'sonner';
33
- import { BoardPublicLinkSection } from './board-public-link-section';
34
- import { ShareSection } from './share-section';
15
+ import { useRef } from 'react';
16
+ import { BoardShareSettingsPanel } from './board-share-settings-panel';
35
17
 
36
18
  interface BoardShareDialogProps {
37
19
  board: Pick<WorkspaceTaskBoard, 'id' | 'name'>;
@@ -40,25 +22,6 @@ interface BoardShareDialogProps {
40
22
  wsId: string;
41
23
  }
42
24
 
43
- function shareDisplayName(share: WorkspaceTaskBoardShare) {
44
- return (
45
- share.user?.display_name ||
46
- (share.user?.handle ? `@${share.user.handle}` : null) ||
47
- share.email ||
48
- share.user_id ||
49
- 'Guest'
50
- );
51
- }
52
-
53
- function viewableMemberDisplayName(member: WorkspaceTaskBoardViewableMember) {
54
- return (
55
- member.display_name ||
56
- (member.handle ? `@${member.handle}` : null) ||
57
- member.email ||
58
- member.user_id
59
- );
60
- }
61
-
62
25
  export function BoardShareDialog({
63
26
  board,
64
27
  onOpenChange,
@@ -66,121 +29,8 @@ export function BoardShareDialog({
66
29
  wsId,
67
30
  }: BoardShareDialogProps) {
68
31
  const t = useTranslations();
69
- const queryClient = useQueryClient();
70
- const [email, setEmail] = useState('');
71
- const [permission, setPermission] =
72
- useState<WorkspaceTaskBoardSharePermission>('view');
73
- const [membersOpen, setMembersOpen] = useState(false);
74
- const [guestsOpen, setGuestsOpen] = useState(false);
75
32
  const initialFocusRef = useRef<HTMLDivElement>(null);
76
33
 
77
- const queryKey = ['task-board-shares', wsId, board.id] as const;
78
- const sharesQuery = useQuery({
79
- queryKey,
80
- queryFn: () => listWorkspaceTaskBoardShares(wsId, board.id),
81
- enabled: open,
82
- });
83
- const viewableMembersQuery = useQuery({
84
- queryKey: ['task-board-viewable-members', wsId, board.id] as const,
85
- queryFn: () => listWorkspaceTaskBoardViewableMembers(wsId, board.id),
86
- enabled: open && membersOpen,
87
- staleTime: 60_000,
88
- });
89
-
90
- const createMutation = useMutation({
91
- mutationFn: () =>
92
- createWorkspaceTaskBoardShare(wsId, board.id, {
93
- email,
94
- permission,
95
- }),
96
- onSuccess: () => {
97
- setEmail('');
98
- setPermission('view');
99
- void queryClient.invalidateQueries({ queryKey });
100
- toast.success(t('ws-task-boards.share.saved'));
101
- },
102
- onError: () => {
103
- toast.error(t('common.error'));
104
- },
105
- });
106
-
107
- const updateMutation = useMutation({
108
- mutationFn: ({
109
- nextPermission,
110
- shareId,
111
- }: {
112
- nextPermission: WorkspaceTaskBoardSharePermission;
113
- shareId: string;
114
- }) =>
115
- updateWorkspaceTaskBoardShare(wsId, board.id, {
116
- shareId,
117
- permission: nextPermission,
118
- }),
119
- onSuccess: () => {
120
- void queryClient.invalidateQueries({ queryKey });
121
- },
122
- onError: () => {
123
- toast.error(t('common.error'));
124
- },
125
- });
126
-
127
- const deleteMutation = useMutation({
128
- mutationFn: (shareId: string) =>
129
- deleteWorkspaceTaskBoardShare(wsId, board.id, shareId),
130
- onSuccess: () => {
131
- void queryClient.invalidateQueries({ queryKey });
132
- toast.success(t('ws-task-boards.share.removed'));
133
- },
134
- onError: () => {
135
- toast.error(t('common.error'));
136
- },
137
- });
138
-
139
- const shares = sharesQuery.data?.shares ?? [];
140
- const membersCount = viewableMembersQuery.data?.members.length;
141
- const membersStatusBadge = viewableMembersQuery.isLoading ? (
142
- <Badge variant="secondary" className="gap-1 px-2 py-0.5 text-[10px]">
143
- <Loader2 className="h-3 w-3 animate-spin" />
144
- {t('common.loading')}
145
- </Badge>
146
- ) : typeof membersCount === 'number' ? (
147
- <Badge variant="secondary" className="px-2 py-0.5 text-[10px]">
148
- {membersCount}
149
- </Badge>
150
- ) : (
151
- <Badge variant="outline" className="px-2 py-0.5 text-[10px]">
152
- {t('common.workspace')}
153
- </Badge>
154
- );
155
- const guestsStatusBadge = sharesQuery.isLoading ? (
156
- <Badge variant="secondary" className="gap-1 px-2 py-0.5 text-[10px]">
157
- <Loader2 className="h-3 w-3 animate-spin" />
158
- {t('common.loading')}
159
- </Badge>
160
- ) : shares.length ? (
161
- <Badge variant="secondary" className="px-2 py-0.5 text-[10px]">
162
- {shares.length}
163
- </Badge>
164
- ) : (
165
- <Badge variant="outline" className="px-2 py-0.5 text-[10px]">
166
- {t('common.none')}
167
- </Badge>
168
- );
169
- const canSubmit =
170
- email.trim().length > 0 &&
171
- !createMutation.isPending &&
172
- !sharesQuery.isLoading;
173
- const permissionOptions = [
174
- {
175
- value: 'view',
176
- label: t('ws-task-boards.share.permission.view'),
177
- },
178
- {
179
- value: 'edit',
180
- label: t('ws-task-boards.share.permission.edit'),
181
- },
182
- ];
183
-
184
34
  return (
185
35
  <Dialog open={open} onOpenChange={onOpenChange}>
186
36
  <DialogContent
@@ -206,181 +56,7 @@ export function BoardShareDialog({
206
56
  </DialogHeader>
207
57
  <div ref={initialFocusRef} tabIndex={-1} className="sr-only" />
208
58
 
209
- <div className="space-y-2">
210
- <BoardPublicLinkSection boardId={board.id} open={open} wsId={wsId} />
211
-
212
- <ShareSection
213
- open={membersOpen}
214
- onOpenChange={setMembersOpen}
215
- title={t('ws-task-boards.share.workspace_members.title')}
216
- tooltip={t('ws-task-boards.share.workspace_members.tooltip')}
217
- icon={<Users className="h-4 w-4 text-muted-foreground" />}
218
- statusBadge={membersStatusBadge}
219
- >
220
- {viewableMembersQuery.isLoading ? (
221
- <div className="flex items-center gap-2 text-muted-foreground text-sm">
222
- <Loader2 className="h-4 w-4 animate-spin" />
223
- {t('common.loading')}
224
- </div>
225
- ) : (viewableMembersQuery.data?.members ?? []).length === 0 ? (
226
- <div className="text-muted-foreground text-sm">
227
- {t('ws-task-boards.share.workspace_members.empty')}
228
- </div>
229
- ) : (
230
- <div className="divide-y rounded-md border">
231
- {viewableMembersQuery.data?.members.map((member) => (
232
- <div
233
- key={member.user_id}
234
- className="flex flex-col gap-3 p-3 sm:flex-row sm:items-center"
235
- >
236
- <div className="flex min-w-0 flex-1 items-center gap-3">
237
- <Avatar className="h-8 w-8">
238
- <AvatarImage src={member.avatar_url ?? undefined} />
239
- <AvatarFallback>
240
- {getInitials(viewableMemberDisplayName(member))}
241
- </AvatarFallback>
242
- </Avatar>
243
- <div className="min-w-0">
244
- <div className="truncate font-medium text-sm">
245
- {viewableMemberDisplayName(member)}
246
- </div>
247
- <div className="truncate text-muted-foreground text-xs">
248
- {member.email || member.user_id}
249
- </div>
250
- </div>
251
- </div>
252
- <div className="flex flex-wrap items-center gap-1.5">
253
- {member.is_creator && (
254
- <Badge variant="secondary">
255
- {t('ws-task-boards.share.workspace_members.creator')}
256
- </Badge>
257
- )}
258
- {member.roles.slice(0, 2).map((role) => (
259
- <Badge key={role.id} variant="outline">
260
- {role.name}
261
- </Badge>
262
- ))}
263
- <Badge variant="outline">
264
- {t('ws-task-boards.share.workspace_members.badge')}
265
- </Badge>
266
- </div>
267
- </div>
268
- ))}
269
- </div>
270
- )}
271
- </ShareSection>
272
-
273
- <ShareSection
274
- open={guestsOpen}
275
- onOpenChange={setGuestsOpen}
276
- title={t('ws-task-boards.share.guests.title')}
277
- tooltip={t('ws-task-boards.share.guests.tooltip')}
278
- icon={<Users className="h-4 w-4 text-muted-foreground" />}
279
- statusBadge={guestsStatusBadge}
280
- >
281
- <div className="space-y-3">
282
- <div className="grid gap-2 sm:grid-cols-[1fr_8rem_auto]">
283
- <Input
284
- type="email"
285
- value={email}
286
- onChange={(event) => setEmail(event.target.value)}
287
- placeholder={t('ws-task-boards.share.email_placeholder')}
288
- />
289
- <Combobox
290
- mode="single"
291
- options={permissionOptions}
292
- selected={permission}
293
- onChange={(value) =>
294
- setPermission(value as WorkspaceTaskBoardSharePermission)
295
- }
296
- placeholder={t('ws-task-boards.share.permission.view')}
297
- searchPlaceholder={t('common.search_members')}
298
- className="[&_button]:h-9"
299
- />
300
- <Button
301
- type="button"
302
- onClick={() => createMutation.mutate()}
303
- disabled={!canSubmit}
304
- >
305
- {createMutation.isPending ? (
306
- <Loader2 className="h-4 w-4 animate-spin" />
307
- ) : (
308
- t('common.share')
309
- )}
310
- </Button>
311
- </div>
312
-
313
- {sharesQuery.isLoading ? (
314
- <div className="rounded-md border border-dashed p-4 text-muted-foreground text-sm">
315
- {t('common.loading')}
316
- </div>
317
- ) : shares.length === 0 ? (
318
- <div className="rounded-md border border-dashed p-4 text-muted-foreground text-sm">
319
- {t('ws-task-boards.share.empty')}
320
- </div>
321
- ) : (
322
- <div className="divide-y rounded-md border">
323
- {shares.map((share) => (
324
- <div
325
- key={share.id}
326
- className="flex flex-col gap-3 p-3 sm:flex-row sm:items-center"
327
- >
328
- <div className="flex min-w-0 flex-1 items-center gap-3">
329
- <Avatar className="h-8 w-8">
330
- <AvatarImage
331
- src={share.user?.avatar_url ?? undefined}
332
- />
333
- <AvatarFallback>
334
- {getInitials(shareDisplayName(share))}
335
- </AvatarFallback>
336
- </Avatar>
337
- <div className="min-w-0">
338
- <div className="truncate font-medium text-sm">
339
- {shareDisplayName(share)}
340
- </div>
341
- <div className="truncate text-muted-foreground text-xs">
342
- {share.email || share.user_id}
343
- </div>
344
- </div>
345
- </div>
346
-
347
- <Badge variant="outline" className="w-fit">
348
- {t('common.guest_access')}
349
- </Badge>
350
-
351
- <Combobox
352
- mode="single"
353
- options={permissionOptions}
354
- selected={share.permission}
355
- onChange={(value) =>
356
- updateMutation.mutate({
357
- shareId: share.id,
358
- nextPermission:
359
- value as WorkspaceTaskBoardSharePermission,
360
- })
361
- }
362
- placeholder={t('ws-task-boards.share.permission.view')}
363
- searchPlaceholder={t('common.search_members')}
364
- className="w-28 [&_button]:h-9"
365
- />
366
-
367
- <Button
368
- type="button"
369
- variant="ghost"
370
- size="icon"
371
- onClick={() => deleteMutation.mutate(share.id)}
372
- disabled={deleteMutation.isPending}
373
- aria-label={t('common.remove')}
374
- >
375
- <Trash2 className="h-4 w-4" />
376
- </Button>
377
- </div>
378
- ))}
379
- </div>
380
- )}
381
- </div>
382
- </ShareSection>
383
- </div>
59
+ <BoardShareSettingsPanel board={board} enabled={open} wsId={wsId} />
384
60
 
385
61
  <DialogFooter>
386
62
  <Button variant="outline" onClick={() => onOpenChange(false)}>