@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
@@ -5,6 +5,7 @@ import type { RealtimePresenceState } from '@tuturuuu/supabase/next/realtime';
5
5
  import type { User } from '@tuturuuu/types/primitives/User';
6
6
  import { Avatar, AvatarFallback, AvatarImage } from '@tuturuuu/ui/avatar';
7
7
  import { Button } from '@tuturuuu/ui/button';
8
+ import { useBoardPresence } from '@tuturuuu/ui/hooks/useBoardPresence';
8
9
  import type { UserPresenceState } from '@tuturuuu/ui/hooks/usePresence';
9
10
  import { Popover, PopoverContent, PopoverTrigger } from '@tuturuuu/ui/popover';
10
11
  import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
@@ -105,12 +106,17 @@ export function BoardUserPresenceAvatarsComponent({
105
106
  onListStatusFilterChange: (filter: ListStatusFilter) => void;
106
107
  }) {
107
108
  const wsPresence = useOptionalWorkspacePresenceContext();
109
+ const useWorkspacePresence = wsPresence?.realtimeEnabled === true;
110
+ const boardPresence = useBoardPresence(boardId, {
111
+ enabled: !useWorkspacePresence,
112
+ });
113
+ const activePresence = useWorkspacePresence ? wsPresence : boardPresence;
108
114
  const { openTaskById } = useTaskDialog();
109
115
  const { state: dialogState } = useTaskDialogContext();
110
116
 
111
117
  // Extract stable function refs to avoid re-running effects on every context change
112
- const wsUpdateLocation = wsPresence?.updateLocation;
113
- const wsUpdateMetadata = wsPresence?.updateMetadata;
118
+ const updateLocation = activePresence?.updateLocation;
119
+ const updateMetadata = activePresence?.updateMetadata;
114
120
 
115
121
  // Derive dialog props — only track task presence when editing a real task
116
122
  const dialogOpen = dialogState.isOpen;
@@ -124,26 +130,26 @@ export function BoardUserPresenceAvatarsComponent({
124
130
  // When the dialog is open with a real task → track { board, boardId, taskId }
125
131
  // Otherwise → track { board, boardId } (board-level only)
126
132
  useEffect(() => {
127
- if (!wsUpdateLocation || !boardId) return;
133
+ if (!updateLocation || !boardId) return;
128
134
  if (dialogOpen && dialogTaskId) {
129
- wsUpdateLocation({ type: 'board', boardId, taskId: dialogTaskId });
135
+ updateLocation({ type: 'board', boardId, taskId: dialogTaskId });
130
136
  } else {
131
- wsUpdateLocation({ type: 'board', boardId });
137
+ updateLocation({ type: 'board', boardId });
132
138
  }
133
139
 
134
140
  return () => {
135
- wsUpdateLocation({ type: 'other' });
141
+ updateLocation({ type: 'other' });
136
142
  };
137
- }, [wsUpdateLocation, boardId, dialogOpen, dialogTaskId]);
143
+ }, [updateLocation, boardId, dialogOpen, dialogTaskId]);
138
144
 
139
145
  // Update metadata (filters) separately so it doesn't overwrite task-level location
140
146
  useEffect(() => {
141
- if (!wsUpdateMetadata || !currentMetadata) return;
142
- wsUpdateMetadata(currentMetadata as Record<string, any>);
143
- }, [wsUpdateMetadata, currentMetadata]);
147
+ if (!updateMetadata || !currentMetadata) return;
148
+ updateMetadata(currentMetadata as Record<string, any>);
149
+ }, [updateMetadata, currentMetadata]);
144
150
 
145
- const boardViewers = wsPresence?.getBoardViewers(boardId) ?? [];
146
- const currentUserId = wsPresence?.currentUserId;
151
+ const boardViewers = activePresence?.getBoardViewers(boardId) ?? [];
152
+ const currentUserId = activePresence?.currentUserId;
147
153
 
148
154
  const presenceState: RealtimePresenceState<UserPresenceState> =
149
155
  useMemo(() => {
@@ -94,17 +94,6 @@ const DEFAULT_TASK_FILTERS: TaskFilters = {
94
94
  sourceBoardIds: [],
95
95
  };
96
96
 
97
- function hasAssignedExternalTasks(tasks: Task[], boardId: string) {
98
- const externalStagingListId = getPersonalExternalStagingListId(boardId);
99
-
100
- return tasks.some(
101
- (task) =>
102
- task.is_personal_external === true ||
103
- task.list_id === externalStagingListId ||
104
- Boolean(task.source_workspace_id)
105
- );
106
- }
107
-
108
97
  function getClosedTaskListCollapsedStorageKey(boardId: string, listId: string) {
109
98
  return `${CLOSED_TASK_LIST_COLLAPSED_STORAGE_PREFIX}:${boardId}:${listId}`;
110
99
  }
@@ -246,13 +235,18 @@ function sortLocalTasks(tasks: Task[], sortBy: TaskFilters['sortBy']) {
246
235
  interface Props {
247
236
  workspace: Workspace;
248
237
  workspaceTier?: WorkspaceProductTier | null;
249
- board: WorkspaceTaskBoard;
238
+ board: WorkspaceTaskBoard & {
239
+ access_type?: 'member' | 'guest';
240
+ guest_permission?: 'view' | 'edit' | null;
241
+ has_guest_access?: boolean;
242
+ };
250
243
  tasks: Task[];
251
244
  lists: TaskList[];
252
245
  workspaceLabels: WorkspaceLabel[];
253
246
  availableViews?: ViewType[];
254
247
  canManageBoard?: boolean;
255
248
  currentUserId?: string;
249
+ defaultView?: ViewType;
256
250
  idleBottomIsland?: ReactNode;
257
251
  publicHeaderPrefix?: ReactNode;
258
252
  publicView?: boolean;
@@ -268,6 +262,7 @@ export function BoardViews({
268
262
  availableViews,
269
263
  canManageBoard = true,
270
264
  currentUserId,
265
+ defaultView,
271
266
  idleBottomIsland,
272
267
  publicHeaderPrefix,
273
268
  publicView = false,
@@ -297,6 +292,18 @@ export function BoardViews({
297
292
  useState(false);
298
293
  const { createTask } = useTaskDialog();
299
294
  const localTaskState = readOnly || publicView;
295
+ const boardAssigneesEnabled =
296
+ !workspace.personal ||
297
+ board.access_type === 'guest' ||
298
+ board.has_guest_access === true;
299
+ const assigneeMemberSource: 'workspace' | 'board' | 'workspace-and-board' =
300
+ board.access_type === 'guest'
301
+ ? 'board'
302
+ : board.has_guest_access === true
303
+ ? workspace.personal
304
+ ? 'board'
305
+ : 'workspace-and-board'
306
+ : 'workspace';
300
307
  const { data: pinnedSpecialListsRaw } = useUserWorkspaceConfig(
301
308
  effectiveWorkspaceId,
302
309
  TASK_BOARD_PINNED_SPECIAL_LISTS_CONFIG_ID,
@@ -452,6 +459,7 @@ export function BoardViews({
452
459
  const result = await listWorkspaceTasks(effectiveWorkspaceId, {
453
460
  ...taskQueryOptions,
454
461
  boardId: board.id,
462
+ includeRelationshipSummary: false,
455
463
  listStatuses: listStatusesForQuery,
456
464
  limit: 200,
457
465
  });
@@ -545,12 +553,15 @@ export function BoardViews({
545
553
  : (new URLSearchParams(window.location.search).get(
546
554
  'view'
547
555
  ) as ViewType | null);
548
- const defaultView = enabledViews?.[0] ?? 'kanban';
556
+ const fallbackView = enabledViews?.[0] ?? 'kanban';
557
+ const routeDefaultView =
558
+ defaultView && viewIsEnabled(defaultView) ? defaultView : null;
559
+ const effectiveDefaultView = routeDefaultView ?? fallbackView;
549
560
  const initialView =
550
561
  requestedView && viewIsEnabled(requestedView) ? requestedView : null;
551
562
 
552
563
  if (!savedConfig) {
553
- setCurrentView(initialView ?? defaultView);
564
+ setCurrentView(initialView ?? effectiveDefaultView);
554
565
  setFilters(DEFAULT_TASK_FILTERS);
555
566
  setListStatusFilter('all');
556
567
  return;
@@ -558,16 +569,17 @@ export function BoardViews({
558
569
 
559
570
  setCurrentView(
560
571
  initialView ??
572
+ routeDefaultView ??
561
573
  (viewIsEnabled(savedConfig.currentView)
562
574
  ? savedConfig.currentView
563
- : defaultView)
575
+ : effectiveDefaultView)
564
576
  );
565
577
  setFilters({
566
578
  ...DEFAULT_TASK_FILTERS,
567
579
  ...savedConfig.filters,
568
580
  });
569
581
  setListStatusFilter(savedConfig.listStatusFilter);
570
- }, [board.id, enabledViews, viewIsEnabled]);
582
+ }, [board.id, defaultView, enabledViews, viewIsEnabled]);
571
583
 
572
584
  useEffect(() => {
573
585
  if (!workspace.personal || typeof window === 'undefined') {
@@ -581,15 +593,11 @@ export function BoardViews({
581
593
  const storedPreference =
582
594
  storedValue === null ? null : storedValue === 'true';
583
595
 
584
- setExternalTasksCollapsed(
585
- storedPreference ?? !hasAssignedExternalTasks(tasks, board.id)
586
- );
587
- }, [board.id, tasks, workspace.personal]);
596
+ setExternalTasksCollapsed(storedPreference ?? true);
597
+ }, [board.id, workspace.personal]);
588
598
 
589
599
  const handleExternalTasksCollapsedChange = useCallback(
590
600
  (collapsed: boolean) => {
591
- if (collapsed && specialTaskListPins.external_tasks) return;
592
-
593
601
  setExternalTasksCollapsed(collapsed);
594
602
 
595
603
  if (!workspace.personal || typeof window === 'undefined') return;
@@ -599,7 +607,7 @@ export function BoardViews({
599
607
  String(collapsed)
600
608
  );
601
609
  },
602
- [board.id, specialTaskListPins.external_tasks, workspace.personal]
610
+ [board.id, workspace.personal]
603
611
  );
604
612
 
605
613
  useEffect(() => {
@@ -635,16 +643,6 @@ export function BoardViews({
635
643
 
636
644
  const handleTaskListCollapsedChange = useCallback(
637
645
  (listId: string, collapsed: boolean) => {
638
- if (
639
- collapsed &&
640
- specialTaskListPins.closed_tasks &&
641
- boardLists.some(
642
- (list) => list.id === listId && list.status === 'closed'
643
- )
644
- ) {
645
- return;
646
- }
647
-
648
646
  setClosedTaskListsCollapsed((previous) => ({
649
647
  ...previous,
650
648
  [listId]: collapsed,
@@ -657,7 +655,7 @@ export function BoardViews({
657
655
  String(collapsed)
658
656
  );
659
657
  },
660
- [board.id, boardLists, specialTaskListPins.closed_tasks]
658
+ [board.id]
661
659
  );
662
660
 
663
661
  useEffect(() => {
@@ -674,7 +672,7 @@ export function BoardViews({
674
672
 
675
673
  next[section] =
676
674
  storedValue === null
677
- ? (previous[section] ?? false)
675
+ ? (previous[section] ?? true)
678
676
  : storedValue === 'true';
679
677
  }
680
678
 
@@ -684,14 +682,6 @@ export function BoardViews({
684
682
 
685
683
  const handleDeadlineSectionCollapsedChange = useCallback(
686
684
  (section: KanbanDeadlineSection, collapsed: boolean) => {
687
- if (
688
- collapsed &&
689
- ((section === 'overdue' && specialTaskListPins.overdue) ||
690
- (section === 'upcoming' && specialTaskListPins.upcoming))
691
- ) {
692
- return;
693
- }
694
-
695
685
  setDeadlineSectionsCollapsed((previous) => ({
696
686
  ...previous,
697
687
  [section]: collapsed,
@@ -704,7 +694,7 @@ export function BoardViews({
704
694
  String(collapsed)
705
695
  );
706
696
  },
707
- [board.id, specialTaskListPins.overdue, specialTaskListPins.upcoming]
697
+ [board.id]
708
698
  );
709
699
 
710
700
  const externalStagingList = useMemo<TaskList | null>(() => {
@@ -722,15 +712,12 @@ export function BoardViews({
722
712
  color: 'CYAN',
723
713
  position: Number.MIN_SAFE_INTEGER,
724
714
  is_external_staging: true,
725
- is_external_collapsed: specialTaskListPins.external_tasks
726
- ? false
727
- : externalTasksCollapsed,
715
+ is_external_collapsed: externalTasksCollapsed,
728
716
  };
729
717
  }, [
730
718
  board.created_at,
731
719
  board.id,
732
720
  externalTasksCollapsed,
733
- specialTaskListPins.external_tasks,
734
721
  tTasks,
735
722
  workspace.personal,
736
723
  ]);
@@ -742,38 +729,22 @@ export function BoardViews({
742
729
  list.status === 'closed'
743
730
  ? {
744
731
  ...list,
745
- is_collapsed: specialTaskListPins.closed_tasks
746
- ? false
747
- : (closedTaskListsCollapsed[list.id] ?? true),
732
+ is_collapsed: closedTaskListsCollapsed[list.id] ?? true,
748
733
  }
749
734
  : list
750
735
  );
751
736
  return externalStagingList
752
737
  ? [externalStagingList, ...realLists]
753
738
  : realLists;
754
- }, [
755
- boardLists,
756
- closedTaskListsCollapsed,
757
- externalStagingList,
758
- specialTaskListPins.closed_tasks,
759
- ]);
739
+ }, [boardLists, closedTaskListsCollapsed, externalStagingList]);
760
740
 
761
741
  const effectiveDeadlineSectionsCollapsed =
762
742
  useMemo<KanbanDeadlineCollapsedState>(
763
743
  () => ({
764
- overdue: specialTaskListPins.overdue
765
- ? false
766
- : deadlineSectionsCollapsed.overdue,
767
- upcoming: specialTaskListPins.upcoming
768
- ? false
769
- : deadlineSectionsCollapsed.upcoming,
744
+ overdue: deadlineSectionsCollapsed.overdue,
745
+ upcoming: deadlineSectionsCollapsed.upcoming,
770
746
  }),
771
- [
772
- deadlineSectionsCollapsed.overdue,
773
- deadlineSectionsCollapsed.upcoming,
774
- specialTaskListPins.overdue,
775
- specialTaskListPins.upcoming,
776
- ]
747
+ [deadlineSectionsCollapsed.overdue, deadlineSectionsCollapsed.upcoming]
777
748
  );
778
749
 
779
750
  const { data: filteredListCounts, isFetching: isFilteredListCountsFetching } =
@@ -1049,6 +1020,8 @@ export function BoardViews({
1049
1020
  specialTaskListPins={specialTaskListPins}
1050
1021
  onSpecialTaskListPinnedChange={handleSpecialTaskListPinnedChange}
1051
1022
  onBulkSelectionActiveChange={setKanbanBulkSelectionActive}
1023
+ canUseBoardAssignees={boardAssigneesEnabled}
1024
+ assigneeMemberSource={assigneeMemberSource}
1052
1025
  readOnly={readOnly}
1053
1026
  />
1054
1027
  );
@@ -1082,6 +1055,8 @@ export function BoardViews({
1082
1055
  tasks={effectiveTasks}
1083
1056
  lists={filteredLists}
1084
1057
  isPersonalWorkspace={workspace.personal}
1058
+ canUseBoardAssignees={boardAssigneesEnabled}
1059
+ assigneeMemberSource={assigneeMemberSource}
1085
1060
  preserveTaskOrder={!!filters.sortBy}
1086
1061
  searchQuery={filters.searchQuery}
1087
1062
  readOnly={readOnly}
@@ -1094,6 +1069,9 @@ export function BoardViews({
1094
1069
  boardId={board.id}
1095
1070
  tasks={effectiveTasks}
1096
1071
  lists={filteredLists}
1072
+ isPersonalWorkspace={workspace.personal}
1073
+ canUseBoardAssignees={boardAssigneesEnabled}
1074
+ assigneeMemberSource={assigneeMemberSource}
1097
1075
  onTaskPartialUpdate={handleTaskPartialUpdate}
1098
1076
  />
1099
1077
  );
@@ -1182,6 +1160,8 @@ export function BoardViews({
1182
1160
  specialTaskListPins={specialTaskListPins}
1183
1161
  onSpecialTaskListPinnedChange={handleSpecialTaskListPinnedChange}
1184
1162
  onBulkSelectionActiveChange={setKanbanBulkSelectionActive}
1163
+ canUseBoardAssignees={boardAssigneesEnabled}
1164
+ assigneeMemberSource={assigneeMemberSource}
1185
1165
  readOnly={readOnly}
1186
1166
  />
1187
1167
  );
@@ -61,6 +61,7 @@ import { useLocale, useTranslations } from 'next-intl';
61
61
  import { useTheme } from 'next-themes';
62
62
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
63
63
  import { useBulkOperations } from '../boards/boardId/kanban/bulk/bulk-operations';
64
+ import type { TaskCardAssigneeMemberSource } from '../boards/boardId/task-card/task-card';
64
65
  import {
65
66
  getTaskCardHydratingOpenOptions,
66
67
  isExternalTaskSnapshot,
@@ -85,6 +86,8 @@ interface Props {
85
86
  tasks: Task[];
86
87
  lists: TaskList[];
87
88
  isPersonalWorkspace?: boolean;
89
+ canUseBoardAssignees?: boolean;
90
+ assigneeMemberSource?: TaskCardAssigneeMemberSource;
88
91
  preserveTaskOrder?: boolean;
89
92
  searchQuery?: string;
90
93
  weekStartsOn?: 0 | 1 | 6;
@@ -118,6 +121,7 @@ function ReadOnlyListView({
118
121
  tasks,
119
122
  lists,
120
123
  isPersonalWorkspace = false,
124
+ canUseBoardAssignees,
121
125
  preserveTaskOrder = false,
122
126
  searchQuery,
123
127
  }: Props) {
@@ -127,6 +131,7 @@ function ReadOnlyListView({
127
131
  const dateLocale = locale === 'vi' ? vi : enUS;
128
132
  const [sortField, setSortField] = useState<ListViewSortField>('created_at');
129
133
  const [sortOrder, setSortOrder] = useState<ListViewSortOrder>('desc');
134
+ const showAssignees = canUseBoardAssignees ?? !isPersonalWorkspace;
130
135
  const listsById = useMemo(
131
136
  () => new Map(lists.map((list) => [list.id, list])),
132
137
  [lists]
@@ -227,7 +232,7 @@ function ReadOnlyListView({
227
232
  {getSortIcon('end_date')}
228
233
  </Button>
229
234
  </TableHead>
230
- {!isPersonalWorkspace && (
235
+ {showAssignees && (
231
236
  <TableHead className="h-9 w-32 px-2">
232
237
  <span className="font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
233
238
  {tc('assignee')}
@@ -300,7 +305,7 @@ function ReadOnlyListView({
300
305
  </span>
301
306
  )}
302
307
  </TableCell>
303
- {!isPersonalWorkspace && (
308
+ {showAssignees && (
304
309
  <TableCell className="px-2 py-2">
305
310
  {task.assignees && task.assignees.length > 0 && (
306
311
  <div className="flex flex-wrap gap-1">
@@ -337,6 +342,8 @@ function InteractiveListView({
337
342
  tasks,
338
343
  lists,
339
344
  isPersonalWorkspace = false,
345
+ canUseBoardAssignees,
346
+ assigneeMemberSource,
340
347
  preserveTaskOrder = false,
341
348
  searchQuery,
342
349
  weekStartsOn = 0,
@@ -360,6 +367,9 @@ function InteractiveListView({
360
367
  const previousWorkspaceIdRef = useRef(workspaceId);
361
368
  const previousBoardIdRef = useRef(boardId);
362
369
  const { openTask, openTaskById } = useTaskDialog();
370
+ const showAssignees = canUseBoardAssignees ?? !isPersonalWorkspace;
371
+ const effectiveAssigneeMemberSource =
372
+ assigneeMemberSource ?? (isPersonalWorkspace ? 'board' : 'workspace');
363
373
 
364
374
  // Infinite scroll
365
375
  const [displayCount, setDisplayCount] = useState(50);
@@ -372,7 +382,7 @@ function InteractiveListView({
372
382
  priority: true,
373
383
  start_date: false,
374
384
  end_date: true,
375
- assignees: !isPersonalWorkspace,
385
+ assignees: showAssignees,
376
386
  actions: true,
377
387
  });
378
388
 
@@ -516,6 +526,10 @@ function InteractiveListView({
516
526
  availableLists: lists,
517
527
  effectiveWorkspaceId: workspaceId,
518
528
  isPersonalWorkspace,
529
+ canUseBoardAssignees: task.source_workspace_id ? true : showAssignees,
530
+ assigneeMemberSource: task.source_workspace_id
531
+ ? 'workspace'
532
+ : effectiveAssigneeMemberSource,
519
533
  })
520
534
  );
521
535
  return;
@@ -524,6 +538,8 @@ function InteractiveListView({
524
538
  openTask(task, boardId, lists, false, {
525
539
  taskWsId: workspaceId,
526
540
  taskWorkspacePersonal: isPersonalWorkspace,
541
+ canUseBoardAssignees: showAssignees,
542
+ assigneeMemberSource: effectiveAssigneeMemberSource,
527
543
  });
528
544
  }
529
545
 
@@ -972,6 +988,8 @@ function InteractiveListView({
972
988
  workspaceId={workspaceId}
973
989
  lists={lists}
974
990
  isPersonalWorkspace={isPersonalWorkspace}
991
+ canUseBoardAssignees={showAssignees}
992
+ assigneeMemberSource={effectiveAssigneeMemberSource}
975
993
  onUpdate={() => {
976
994
  void queryClient.invalidateQueries({
977
995
  queryKey: ['tasks', boardId],
@@ -14,15 +14,15 @@ export function TaskBoardLoadingState({
14
14
  <div
15
15
  aria-busy="true"
16
16
  className={cn(
17
- 'w-full overflow-hidden bg-transparent',
17
+ 'overflow-hidden bg-transparent',
18
18
  root
19
- ? '-m-4 h-[calc(100dvh+2rem)] min-h-[calc(32rem+2rem)]'
20
- : 'h-[calc(100dvh-1rem)] min-h-[32rem]',
19
+ ? '-m-4 h-[calc(100dvh+2rem)] min-h-[calc(32rem+2rem)] w-[calc(100%+2rem)] min-w-[calc(100%+2rem)]'
20
+ : 'h-[calc(100dvh-1rem)] min-h-[32rem] w-full',
21
21
  className
22
22
  )}
23
23
  data-testid="task-board-loading-state"
24
24
  >
25
- <KanbanSkeleton />
25
+ <KanbanSkeleton root={root} />
26
26
  </div>
27
27
  );
28
28
  }