@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
@@ -30,6 +30,7 @@ import {
30
30
  DropdownMenuTrigger,
31
31
  } from '@tuturuuu/ui/dropdown-menu';
32
32
  import { cn } from '@tuturuuu/utils/format';
33
+ import type React from 'react';
33
34
  import { useMemo, useState } from 'react';
34
35
  import { TaskCard } from '../../task';
35
36
  import type { KanbanDeadlineSections } from './kanban-deadline-tasks';
@@ -64,6 +65,8 @@ interface KanbanDeadlinePanelsProps {
64
65
  boardId: string;
65
66
  bulkUpdateCustomDueDate: (date: Date | null) => Promise<void>;
66
67
  isPersonalWorkspace: boolean;
68
+ canUseBoardAssignees?: boolean;
69
+ assigneeMemberSource?: 'workspace' | 'board' | 'workspace-and-board';
67
70
  labels: KanbanDeadlineLabels;
68
71
  onClearSelection: () => void;
69
72
  onSectionCollapsedChange?: (
@@ -78,8 +81,10 @@ interface KanbanDeadlinePanelsProps {
78
81
  onUpdate: () => void;
79
82
  optimisticUpdateInProgress: Set<string>;
80
83
  sections: KanbanDeadlineSections;
84
+ loading?: boolean;
81
85
  collapsedSections?: KanbanDeadlineCollapsedState;
82
86
  pinnedSections?: KanbanDeadlineCollapsedState;
87
+ stickyOffsets?: Partial<Record<KanbanDeadlineSection, string>>;
83
88
  deadlineNow?: number;
84
89
  selectedTasks: Set<string>;
85
90
  taskLists: TaskList[];
@@ -216,6 +221,34 @@ function getTaskListForDeadlineTask(task: Task, lists: TaskList[]) {
216
221
  );
217
222
  }
218
223
 
224
+ function DeadlineSectionSkeleton({
225
+ section,
226
+ }: {
227
+ section: KanbanDeadlineSection;
228
+ }) {
229
+ return (
230
+ <div
231
+ aria-hidden="true"
232
+ className="space-y-2"
233
+ data-testid={`kanban-deadline-section-${section}-loading`}
234
+ >
235
+ {Array.from({ length: 3 }, (_, index) => (
236
+ <div
237
+ className="rounded-lg border border-border/60 bg-background/60 p-3"
238
+ key={index}
239
+ >
240
+ <div className="h-3 w-3/4 animate-pulse rounded bg-muted" />
241
+ <div className="mt-3 h-2 w-1/2 animate-pulse rounded bg-muted" />
242
+ <div className="mt-2 flex gap-2">
243
+ <div className="h-2 w-14 animate-pulse rounded bg-muted" />
244
+ <div className="h-2 w-10 animate-pulse rounded bg-muted" />
245
+ </div>
246
+ </div>
247
+ ))}
248
+ </div>
249
+ );
250
+ }
251
+
219
252
  function DeadlineSection({
220
253
  availableLists,
221
254
  boardId,
@@ -223,9 +256,12 @@ function DeadlineSection({
223
256
  collapsed,
224
257
  config,
225
258
  deadlineNow,
259
+ loading,
226
260
  labels,
227
261
  isMultiSelectMode,
228
262
  isPersonalWorkspace,
263
+ canUseBoardAssignees,
264
+ assigneeMemberSource,
229
265
  onClearSelection,
230
266
  onCollapsedChange,
231
267
  onPinnedChange,
@@ -234,6 +270,7 @@ function DeadlineSection({
234
270
  optimisticUpdateInProgress,
235
271
  pinned,
236
272
  selectedTasks,
273
+ stickyOffset,
237
274
  taskLists,
238
275
  tasks,
239
276
  workspaceId,
@@ -244,9 +281,12 @@ function DeadlineSection({
244
281
  collapsed: boolean;
245
282
  config: DeadlineSectionConfig;
246
283
  deadlineNow?: number;
284
+ loading?: boolean;
247
285
  labels: KanbanDeadlineLabels;
248
286
  isMultiSelectMode: boolean;
249
287
  isPersonalWorkspace: boolean;
288
+ canUseBoardAssignees?: boolean;
289
+ assigneeMemberSource?: 'workspace' | 'board' | 'workspace-and-board';
250
290
  onClearSelection: () => void;
251
291
  onCollapsedChange?: (
252
292
  section: KanbanDeadlineSection,
@@ -258,6 +298,7 @@ function DeadlineSection({
258
298
  optimisticUpdateInProgress: Set<string>;
259
299
  pinned?: boolean;
260
300
  selectedTasks: Set<string>;
301
+ stickyOffset?: string;
261
302
  taskLists: TaskList[];
262
303
  tasks: Task[];
263
304
  workspaceId: string;
@@ -297,17 +338,23 @@ function DeadlineSection({
297
338
  return sortDeadlineTasks(filteredTasks, sortBy);
298
339
  }, [includeDocuments, includeExternal, sortBy, taskListById, tasks]);
299
340
  const filterCount = (includeDocuments ? 0 : 1) + (includeExternal ? 0 : 1);
300
-
301
- if (tasks.length === 0) return null;
341
+ const stickyStyle: React.CSSProperties | undefined = stickyOffset
342
+ ? {
343
+ left: `calc(var(--kanban-snap-left-padding) + ${stickyOffset})`,
344
+ }
345
+ : undefined;
302
346
 
303
347
  if (collapsed) {
304
348
  return (
305
349
  <Card
306
350
  className={cn(
307
351
  'group flex h-full w-14 shrink-0 snap-start flex-col items-center overflow-hidden rounded-xl border border-dashed transition-all duration-200 hover:shadow-md',
352
+ stickyOffset && 'sticky z-30',
308
353
  config.collapsedClassName
309
354
  )}
355
+ data-kanban-pinned-special={stickyOffset ? 'true' : undefined}
310
356
  data-testid={`kanban-deadline-section-${config.section}-collapsed`}
357
+ style={stickyStyle}
311
358
  >
312
359
  <button
313
360
  type="button"
@@ -341,9 +388,12 @@ function DeadlineSection({
341
388
  <Card
342
389
  className={cn(
343
390
  'flex h-full w-[var(--kanban-column-width)] shrink-0 snap-start flex-col overflow-hidden rounded-xl border border-dashed shadow-xs',
391
+ stickyOffset && 'sticky z-30',
344
392
  config.panelClassName
345
393
  )}
394
+ data-kanban-pinned-special={stickyOffset ? 'true' : undefined}
346
395
  data-testid={`kanban-deadline-section-${config.section}`}
396
+ style={stickyStyle}
347
397
  >
348
398
  <div className="flex items-center justify-between gap-3 border-border/70 border-b p-3">
349
399
  <div className="flex min-w-0 flex-1 items-center gap-2">
@@ -510,58 +560,62 @@ function DeadlineSection({
510
560
  )}
511
561
  </Button>
512
562
  ) : null}
513
- {!pinned && (
514
- <Button
515
- type="button"
516
- variant="ghost"
517
- size="xs"
518
- className={cn(
519
- 'h-7 w-7 p-0 hover:bg-muted/40',
520
- config.titleClassName
521
- )}
522
- title={collapseLabel}
523
- aria-label={collapseLabel}
524
- onClick={() => onCollapsedChange?.(config.section, true)}
525
- >
526
- <ChevronLeft className="h-3.5 w-3.5" />
527
- </Button>
528
- )}
563
+ <Button
564
+ type="button"
565
+ variant="ghost"
566
+ size="xs"
567
+ className={cn(
568
+ 'h-7 w-7 p-0 hover:bg-muted/40',
569
+ config.titleClassName
570
+ )}
571
+ title={collapseLabel}
572
+ aria-label={collapseLabel}
573
+ onClick={() => onCollapsedChange?.(config.section, true)}
574
+ >
575
+ <ChevronLeft className="h-3.5 w-3.5" />
576
+ </Button>
529
577
  </div>
530
578
  </div>
531
579
 
532
580
  <div className="scrollbar-thin min-h-0 flex-1 space-y-2 overflow-y-auto p-2">
533
- {visibleTasks.map((task) => {
534
- const taskList = getTaskListForDeadlineTask(task, taskLists);
535
-
536
- return (
537
- <div
538
- key={task.id}
539
- className="shrink-0"
540
- data-testid={`kanban-deadline-task-card-${task.id}`}
541
- >
542
- <TaskCard
543
- availableLists={availableLists}
544
- boardId={boardId}
545
- bulkUpdateCustomDueDate={bulkUpdateCustomDueDate}
546
- dragDisabled
547
- isMultiSelectMode={isMultiSelectMode}
548
- isPersonalWorkspace={isPersonalWorkspace}
549
- isSelected={selectedTasks.has(task.id)}
550
- onClearSelection={onClearSelection}
551
- onSelect={onTaskSelect}
552
- onUpdate={onUpdate}
553
- optimisticUpdateInProgress={optimisticUpdateInProgress}
554
- selectedTasks={selectedTasks}
555
- deadlineContext={config.section}
556
- deadlineNow={deadlineNow}
557
- sortableId={`deadline-${config.section}-${task.id}`}
558
- task={task}
559
- taskList={taskList}
560
- workspaceId={workspaceId}
561
- />
562
- </div>
563
- );
564
- })}
581
+ {loading && visibleTasks.length === 0 ? (
582
+ <DeadlineSectionSkeleton section={config.section} />
583
+ ) : (
584
+ visibleTasks.map((task) => {
585
+ const taskList = getTaskListForDeadlineTask(task, taskLists);
586
+
587
+ return (
588
+ <div
589
+ key={task.id}
590
+ className="shrink-0"
591
+ data-testid={`kanban-deadline-task-card-${task.id}`}
592
+ >
593
+ <TaskCard
594
+ availableLists={availableLists}
595
+ boardId={boardId}
596
+ bulkUpdateCustomDueDate={bulkUpdateCustomDueDate}
597
+ dragDisabled
598
+ isMultiSelectMode={isMultiSelectMode}
599
+ isPersonalWorkspace={isPersonalWorkspace}
600
+ canUseBoardAssignees={canUseBoardAssignees}
601
+ assigneeMemberSource={assigneeMemberSource}
602
+ isSelected={selectedTasks.has(task.id)}
603
+ onClearSelection={onClearSelection}
604
+ onSelect={onTaskSelect}
605
+ onUpdate={onUpdate}
606
+ optimisticUpdateInProgress={optimisticUpdateInProgress}
607
+ selectedTasks={selectedTasks}
608
+ deadlineContext={config.section}
609
+ deadlineNow={deadlineNow}
610
+ sortableId={`deadline-${config.section}-${task.id}`}
611
+ task={task}
612
+ taskList={taskList}
613
+ workspaceId={workspaceId}
614
+ />
615
+ </div>
616
+ );
617
+ })
618
+ )}
565
619
  </div>
566
620
  </Card>
567
621
  );
@@ -573,6 +627,8 @@ export function KanbanDeadlinePanels({
573
627
  bulkUpdateCustomDueDate,
574
628
  isMultiSelectMode,
575
629
  isPersonalWorkspace,
630
+ canUseBoardAssignees,
631
+ assigneeMemberSource,
576
632
  labels,
577
633
  onClearSelection,
578
634
  onSectionCollapsedChange,
@@ -581,17 +637,15 @@ export function KanbanDeadlinePanels({
581
637
  onUpdate,
582
638
  optimisticUpdateInProgress,
583
639
  sections,
640
+ loading,
584
641
  collapsedSections,
585
642
  pinnedSections,
643
+ stickyOffsets,
586
644
  deadlineNow,
587
645
  selectedTasks,
588
646
  taskLists,
589
647
  workspaceId,
590
648
  }: KanbanDeadlinePanelsProps) {
591
- if (sections.overdue.length === 0 && sections.upcoming.length === 0) {
592
- return null;
593
- }
594
-
595
649
  const configs: DeadlineSectionConfig[] = [
596
650
  {
597
651
  icon: AlertTriangle,
@@ -625,8 +679,11 @@ export function KanbanDeadlinePanels({
625
679
  config={config}
626
680
  deadlineNow={deadlineNow}
627
681
  labels={labels}
682
+ loading={loading === true && sections[config.section].length === 0}
628
683
  isMultiSelectMode={isMultiSelectMode}
629
684
  isPersonalWorkspace={isPersonalWorkspace}
685
+ canUseBoardAssignees={canUseBoardAssignees}
686
+ assigneeMemberSource={assigneeMemberSource}
630
687
  onClearSelection={onClearSelection}
631
688
  onCollapsedChange={onSectionCollapsedChange}
632
689
  onPinnedChange={onSectionPinnedChange}
@@ -635,6 +692,7 @@ export function KanbanDeadlinePanels({
635
692
  optimisticUpdateInProgress={optimisticUpdateInProgress}
636
693
  pinned={pinnedSections?.[config.section] === true}
637
694
  selectedTasks={selectedTasks}
695
+ stickyOffset={stickyOffsets?.[config.section]}
638
696
  taskLists={taskLists}
639
697
  tasks={sections[config.section]}
640
698
  workspaceId={workspaceId}
@@ -71,14 +71,20 @@ function KanbanColumnSkeleton({
71
71
  );
72
72
  }
73
73
 
74
- export function KanbanSkeleton() {
74
+ export function KanbanSkeleton({ root = false }: { root?: boolean }) {
75
75
  return (
76
76
  <div
77
77
  aria-hidden="true"
78
78
  className="h-full overflow-hidden bg-transparent"
79
79
  data-testid="kanban-skeleton"
80
80
  >
81
- <div className="flex h-full gap-2 overflow-hidden p-2 sm:gap-3">
81
+ <div
82
+ className={cn(
83
+ 'flex h-full min-w-0 gap-2 overflow-hidden sm:gap-3',
84
+ root ? 'py-2 pr-0 pl-2' : 'p-2'
85
+ )}
86
+ data-testid="kanban-skeleton-frame"
87
+ >
82
88
  <div className="hidden shrink-0 gap-2 sm:flex">
83
89
  {RAILS.map((rail) => (
84
90
  <div
@@ -102,6 +102,8 @@ interface Props {
102
102
  pinned: boolean
103
103
  ) => void;
104
104
  onBulkSelectionActiveChange?: (active: boolean) => void;
105
+ canUseBoardAssignees?: boolean;
106
+ assigneeMemberSource?: 'workspace' | 'board' | 'workspace-and-board';
105
107
  readOnly?: boolean;
106
108
  }
107
109
 
@@ -125,6 +127,8 @@ export function KanbanBoard({
125
127
  specialTaskListPins,
126
128
  onSpecialTaskListPinnedChange,
127
129
  onBulkSelectionActiveChange,
130
+ canUseBoardAssignees,
131
+ assigneeMemberSource,
128
132
  readOnly = false,
129
133
  }: Props) {
130
134
  const tCommon = useTranslations('common');
@@ -162,26 +166,28 @@ export function KanbanBoard({
162
166
  const reorderTaskMutation = useReorderTask(boardId ?? '', workspaceId);
163
167
  const { createTask } = useTaskDialog();
164
168
  const { weekStartsOn } = useCalendarPreferences();
169
+ const boardAssigneesEnabled = canUseBoardAssignees ?? !workspace.personal;
165
170
 
166
171
  const { data: boardConfig } = useBoardConfig(
167
172
  readOnly ? null : boardId,
168
173
  readOnly ? null : workspaceId
169
174
  );
170
- const { data: deadlineTasks = [] } = useQuery({
171
- enabled: Boolean(boardId) && !readOnly,
172
- queryFn: () =>
173
- listKanbanDeadlineTasks({
174
- boardId: boardId ?? '',
175
- taskQueryOptions: deadlineTaskQueryOptions,
175
+ const { data: deadlineTasks = [], isPending: deadlineTasksPending } =
176
+ useQuery({
177
+ enabled: Boolean(boardId) && !readOnly,
178
+ queryFn: () =>
179
+ listKanbanDeadlineTasks({
180
+ boardId: boardId ?? '',
181
+ taskQueryOptions: deadlineTaskQueryOptions,
182
+ workspaceId,
183
+ }),
184
+ queryKey: getKanbanDeadlineTasksQueryKey(
176
185
  workspaceId,
177
- }),
178
- queryKey: getKanbanDeadlineTasksQueryKey(
179
- workspaceId,
180
- boardId,
181
- deadlineTaskQueryOptions
182
- ),
183
- staleTime: 30_000,
184
- });
186
+ boardId,
187
+ deadlineTaskQueryOptions
188
+ ),
189
+ staleTime: 30_000,
190
+ });
185
191
  const persistListPositions = useCallback(
186
192
  async (updates: Array<{ listId: string; newPosition: number }>) => {
187
193
  if (!boardId || updates.length === 0) return;
@@ -297,6 +303,9 @@ export function KanbanBoard({
297
303
  // Resources Hooks
298
304
  const { workspaceLabels, workspaceProjects, workspaceMembers } =
299
305
  useBulkResources({
306
+ boardId,
307
+ canUseBoardAssignees: boardAssigneesEnabled,
308
+ assigneeMemberSource,
300
309
  workspace,
301
310
  isMultiSelectMode,
302
311
  selectedCount: selectedTasks.size,
@@ -325,6 +334,7 @@ export function KanbanBoard({
325
334
  columns: orderedRealColumns,
326
335
  workspaceLabels,
327
336
  workspaceProjects,
337
+ workspaceMembers,
328
338
  weekStartsOn,
329
339
  setBulkWorking,
330
340
  clearSelection,
@@ -523,6 +533,8 @@ export function KanbanBoard({
523
533
  boardId={boardId ?? ''}
524
534
  workspaceId={workspaceId}
525
535
  isPersonalWorkspace={workspace.personal}
536
+ canUseBoardAssignees={boardAssigneesEnabled}
537
+ assigneeMemberSource={assigneeMemberSource}
526
538
  cursorsEnabled={cursorsEnabled}
527
539
  disableSort={disableSort}
528
540
  selectedTasks={readOnly ? new Set<string>() : selectedTasks}
@@ -545,6 +557,7 @@ export function KanbanBoard({
545
557
  columnsId={columnsId}
546
558
  deadlineLabels={deadlineLabels}
547
559
  deadlineSections={deadlineSections}
560
+ deadlineSectionsLoading={deadlineTasksPending}
548
561
  deadlineSectionsCollapsed={deadlineSectionsCollapsed}
549
562
  deadlineNow={deadlineNow}
550
563
  onDeadlineSectionCollapsedChange={onDeadlineSectionCollapsedChange}
@@ -564,6 +577,8 @@ export function KanbanBoard({
564
577
  columns={orderedColumns}
565
578
  boardId={boardId ?? ''}
566
579
  isPersonalWorkspace={workspace.personal}
580
+ canUseBoardAssignees={boardAssigneesEnabled}
581
+ assigneeMemberSource={assigneeMemberSource}
567
582
  isMultiSelectMode={isMultiSelectMode}
568
583
  selectedTasks={selectedTasks}
569
584
  onUpdate={() => {}}
@@ -72,16 +72,18 @@ const BOARD_ID = '11111111-1111-1111-1111-111111111111';
72
72
 
73
73
  type BoardClientElement = ReactElement<{
74
74
  currentUserId?: string;
75
+ rootLoading?: boolean;
75
76
  workspace: unknown;
76
77
  workspaceTier: unknown;
77
78
  }>;
78
79
 
79
- function renderServerPage() {
80
+ function renderServerPage(options?: { rootLoading?: boolean }) {
80
81
  return TaskBoardServerPage({
81
82
  params: Promise.resolve({
82
83
  boardId: BOARD_ID,
83
84
  wsId: 'ws-1',
84
85
  }),
86
+ rootLoading: options?.rootLoading,
85
87
  }) as Promise<BoardClientElement>;
86
88
  }
87
89
 
@@ -183,4 +185,25 @@ describe('TaskBoardServerPage', () => {
183
185
  expect(element.props.workspace).toBe(workspace);
184
186
  expect(element.props.workspaceTier).toBe('FREE');
185
187
  });
188
+
189
+ it('passes full-bleed loading through to the board client when requested', async () => {
190
+ const workspace = {
191
+ id: 'ws-board',
192
+ joined: true,
193
+ personal: false,
194
+ tier: 'FREE',
195
+ };
196
+ mocks.getWorkspaceTaskBoard.mockResolvedValue({
197
+ board: {
198
+ access_type: 'member',
199
+ id: BOARD_ID,
200
+ ws_id: 'ws-board',
201
+ },
202
+ });
203
+ mocks.getWorkspace.mockResolvedValue(workspace);
204
+
205
+ const element = await renderServerPage({ rootLoading: true });
206
+
207
+ expect(element.props.rootLoading).toBe(true);
208
+ });
186
209
  });
@@ -14,6 +14,7 @@ import { getWorkspace } from '@tuturuuu/utils/workspace-helper';
14
14
  import { headers } from 'next/headers';
15
15
  import { notFound, redirect } from 'next/navigation';
16
16
  import type { ReactNode } from 'react';
17
+ import type { ViewType } from '../../shared/board-views';
17
18
 
18
19
  interface Props {
19
20
  params: Promise<{
@@ -22,7 +23,9 @@ interface Props {
22
23
  }>;
23
24
  /** Route prefix for tasks URLs. Defaults to '/tasks' (web app). Set to '' for satellite apps. */
24
25
  routePrefix?: string;
26
+ defaultView?: ViewType;
25
27
  idleBottomIsland?: ReactNode;
28
+ rootLoading?: boolean;
26
29
  }
27
30
 
28
31
  type AuthorizedWorkspace = Workspace & {
@@ -71,9 +74,11 @@ async function getAuthorizedBoard(wsId: string, boardId: string) {
71
74
  * Used by both apps/web and apps/tasks.
72
75
  */
73
76
  export default async function TaskBoardServerPage({
77
+ defaultView,
74
78
  idleBottomIsland,
75
79
  params,
76
80
  routePrefix = '/tasks',
81
+ rootLoading = false,
77
82
  }: Props) {
78
83
  const { wsId: id, boardId } = await params;
79
84
 
@@ -95,8 +100,10 @@ export default async function TaskBoardServerPage({
95
100
  workspace={workspace}
96
101
  workspaceTier={workspace.tier ?? null}
97
102
  currentUserId={user.id}
103
+ defaultView={defaultView}
98
104
  routePrefix={routePrefix}
99
105
  idleBottomIsland={idleBottomIsland}
106
+ rootLoading={rootLoading}
100
107
  />
101
108
  );
102
109
  }
@@ -1,7 +1,7 @@
1
1
  import type { Task } from '@tuturuuu/types/primitives/Task';
2
2
  import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
3
3
  import { type MouseEvent, useEffect, useRef } from 'react';
4
- import { TaskCard } from './task-card';
4
+ import { TaskCard, type TaskCardAssigneeMemberSource } from './task-card';
5
5
 
6
6
  interface MeasuredTaskCardProps {
7
7
  task: Task;
@@ -13,6 +13,8 @@ interface MeasuredTaskCardProps {
13
13
  isSelected: boolean;
14
14
  isMultiSelectMode?: boolean;
15
15
  isPersonalWorkspace?: boolean;
16
+ canUseBoardAssignees?: boolean;
17
+ assigneeMemberSource?: TaskCardAssigneeMemberSource;
16
18
  onSelect?: (taskId: string, event: MouseEvent) => void;
17
19
  onClearSelection?: () => void;
18
20
  suppressSortableTransform?: boolean;
@@ -34,6 +36,8 @@ export function MeasuredTaskCard({
34
36
  isSelected,
35
37
  isMultiSelectMode,
36
38
  isPersonalWorkspace,
39
+ canUseBoardAssignees,
40
+ assigneeMemberSource,
37
41
  onSelect,
38
42
  onClearSelection,
39
43
  suppressSortableTransform,
@@ -99,6 +103,8 @@ export function MeasuredTaskCard({
99
103
  isSelected={isSelected}
100
104
  isMultiSelectMode={isMultiSelectMode}
101
105
  isPersonalWorkspace={isPersonalWorkspace}
106
+ canUseBoardAssignees={canUseBoardAssignees}
107
+ assigneeMemberSource={assigneeMemberSource}
102
108
  onSelect={onSelect}
103
109
  onClearSelection={onClearSelection}
104
110
  suppressSortableTransform={suppressSortableTransform}
@@ -75,6 +75,8 @@ describe('getTaskCardHydratingOpenOptions', () => {
75
75
  ],
76
76
  taskWsId: 'source-workspace',
77
77
  taskWorkspacePersonal: false,
78
+ canUseBoardAssignees: true,
79
+ assigneeMemberSource: 'workspace',
78
80
  initialSharedContext: {
79
81
  boardConfig: {
80
82
  id: 'source-board',
@@ -118,10 +120,28 @@ describe('getTaskCardHydratingOpenOptions', () => {
118
120
  availableLists: [list],
119
121
  taskWsId: 'workspace-1',
120
122
  taskWorkspacePersonal: true,
123
+ canUseBoardAssignees: false,
124
+ assigneeMemberSource: undefined,
121
125
  initialSharedContext: undefined,
122
126
  });
123
127
  });
124
128
 
129
+ it('can keep assignees enabled for shared personal board tasks', () => {
130
+ expect(
131
+ getTaskCardHydratingOpenOptions({
132
+ task,
133
+ boardId: 'board-1',
134
+ availableLists: [list],
135
+ canUseBoardAssignees: true,
136
+ effectiveWorkspaceId: 'workspace-1',
137
+ isPersonalWorkspace: true,
138
+ })
139
+ ).toMatchObject({
140
+ canUseBoardAssignees: true,
141
+ taskWorkspacePersonal: true,
142
+ });
143
+ });
144
+
125
145
  it('treats source metadata as an external task snapshot', () => {
126
146
  expect(
127
147
  isExternalTaskSnapshot({
@@ -6,6 +6,8 @@ interface TaskCardOpenOptionsInput {
6
6
  task: Task;
7
7
  boardId: string;
8
8
  availableLists?: TaskList[];
9
+ assigneeMemberSource?: 'workspace' | 'board' | 'workspace-and-board';
10
+ canUseBoardAssignees?: boolean;
9
11
  effectiveWorkspaceId?: string;
10
12
  isPersonalWorkspace: boolean;
11
13
  }
@@ -96,6 +98,8 @@ export function getTaskCardHydratingOpenOptions({
96
98
  task,
97
99
  boardId,
98
100
  availableLists,
101
+ assigneeMemberSource,
102
+ canUseBoardAssignees,
99
103
  effectiveWorkspaceId,
100
104
  isPersonalWorkspace,
101
105
  }: TaskCardOpenOptionsInput) {
@@ -110,6 +114,9 @@ export function getTaskCardHydratingOpenOptions({
110
114
  ...task,
111
115
  list_id: task.source_list_id ?? task.list_id,
112
116
  };
117
+ const sourceBoardAssigneesEnabled = sourceWorkspaceId
118
+ ? true
119
+ : !isPersonalWorkspace;
113
120
 
114
121
  return {
115
122
  initialTask,
@@ -122,6 +129,9 @@ export function getTaskCardHydratingOpenOptions({
122
129
  : availableLists,
123
130
  taskWsId: sourceWorkspaceId ?? effectiveWorkspaceId,
124
131
  taskWorkspacePersonal: sourceWorkspaceId ? false : isPersonalWorkspace,
132
+ canUseBoardAssignees: canUseBoardAssignees ?? sourceBoardAssigneesEnabled,
133
+ assigneeMemberSource:
134
+ assigneeMemberSource ?? (sourceWorkspaceId ? 'workspace' : undefined),
125
135
  initialSharedContext,
126
136
  };
127
137
  }