@tuturuuu/ui 0.8.0 → 0.9.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 (182) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/biome.json +1 -1
  3. package/package.json +73 -71
  4. package/src/components/ui/accordion.tsx +1 -1
  5. package/src/components/ui/breadcrumb.tsx +1 -1
  6. package/src/components/ui/calendar-app/calendar-page-shell.tsx +4 -0
  7. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +239 -33
  8. package/src/components/ui/calendar-app/components/load-smart-scheduling-tasks.tsx +143 -0
  9. package/src/components/ui/calendar-app/components/priority-view.tsx +10 -3
  10. package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +4 -116
  11. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +67 -2
  12. package/src/components/ui/calendar.tsx +1 -1
  13. package/src/components/ui/carousel.tsx +1 -1
  14. package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +1 -1
  15. package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +1 -1
  16. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +1 -1
  17. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +1 -1
  18. package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +1 -1
  19. package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +1 -1
  20. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +2 -2
  21. package/src/components/ui/chat/chat-agent-details-utils.test.ts +1 -1
  22. package/src/components/ui/chat/chat-agent-details-utils.tsx +1 -1
  23. package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +2 -2
  24. package/src/components/ui/checkbox.tsx +1 -1
  25. package/src/components/ui/color-picker.tsx +1 -1
  26. package/src/components/ui/command.tsx +1 -1
  27. package/src/components/ui/context-menu.tsx +5 -1
  28. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
  29. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
  30. package/src/components/ui/custom/combobox.test.tsx +195 -0
  31. package/src/components/ui/custom/combobox.tsx +273 -156
  32. package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
  33. package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
  34. package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
  35. package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
  36. package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
  37. package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
  38. package/src/components/ui/custom/theme-toggle.tsx +1 -1
  39. package/src/components/ui/custom/workspace-select.tsx +8 -3
  40. package/src/components/ui/dialog.test.tsx +52 -0
  41. package/src/components/ui/dialog.tsx +6 -2
  42. package/src/components/ui/dropdown-menu.tsx +5 -1
  43. package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
  44. package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
  45. package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
  46. package/src/components/ui/finance/debts/debts-page.tsx +15 -2
  47. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
  48. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
  49. package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
  50. package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
  51. package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
  52. package/src/components/ui/finance/invoices/utils.ts +3 -1
  53. package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
  54. package/src/components/ui/finance/transactions/form-types.ts +1 -0
  55. package/src/components/ui/finance/transactions/form.tsx +2 -0
  56. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
  57. package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
  58. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
  59. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
  60. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
  61. package/src/components/ui/finance/wallets/form.test.tsx +51 -3
  62. package/src/components/ui/finance/wallets/form.tsx +15 -4
  63. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  64. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
  65. package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
  66. package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
  67. package/src/components/ui/input-otp.tsx +1 -1
  68. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
  69. package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
  70. package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
  71. package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
  72. package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
  73. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
  74. package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
  75. package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
  76. package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
  77. package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
  78. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
  79. package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
  80. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
  81. package/src/components/ui/navigation-menu.tsx +1 -1
  82. package/src/components/ui/pagination.tsx +1 -1
  83. package/src/components/ui/radio-group.tsx +1 -1
  84. package/src/components/ui/select.tsx +5 -1
  85. package/src/components/ui/sheet.tsx +1 -1
  86. package/src/components/ui/sidebar.tsx +1 -1
  87. package/src/components/ui/storefront/cart-popover.tsx +61 -0
  88. package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
  89. package/src/components/ui/storefront/cart-summary.tsx +93 -154
  90. package/src/components/ui/storefront/checkout-overlay.tsx +4 -5
  91. package/src/components/ui/storefront/listing-card.tsx +1 -1
  92. package/src/components/ui/storefront/merch-sections.tsx +70 -0
  93. package/src/components/ui/storefront/product-detail.tsx +1 -1
  94. package/src/components/ui/storefront/storefront-surface.test.tsx +106 -11
  95. package/src/components/ui/storefront/storefront-surface.tsx +101 -166
  96. package/src/components/ui/storefront/types.ts +4 -0
  97. package/src/components/ui/storefront/utils.ts +6 -0
  98. package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
  99. package/src/components/ui/text-editor/background-color-extension.ts +62 -0
  100. package/src/components/ui/text-editor/color-controls.tsx +284 -0
  101. package/src/components/ui/text-editor/editor.tsx +69 -14
  102. package/src/components/ui/text-editor/extensions.ts +8 -2
  103. package/src/components/ui/text-editor/highlight-extension.ts +22 -0
  104. package/src/components/ui/text-editor/tool-bar.tsx +9 -16
  105. package/src/components/ui/toast.tsx +1 -1
  106. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
  107. package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
  108. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
  109. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +112 -43
  110. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
  111. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
  112. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
  113. package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
  114. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
  115. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
  116. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +38 -9
  117. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
  118. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
  119. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +81 -30
  120. package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
  121. package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
  122. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
  123. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
  124. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
  125. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
  126. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
  127. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
  128. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
  129. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
  130. package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
  131. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +397 -2
  132. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +103 -13
  133. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
  134. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
  135. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
  136. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +26 -4
  137. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +5 -2
  138. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
  139. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
  140. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
  141. package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
  142. package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
  143. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
  144. package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
  145. package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
  146. package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
  147. package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
  148. package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
  149. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
  150. package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
  151. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
  152. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
  153. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
  154. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +203 -2
  155. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
  156. package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
  157. package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
  158. package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
  159. package/src/components/ui/tu-do/shared/board-header.tsx +464 -975
  160. package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
  161. package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
  162. package/src/components/ui/tu-do/shared/board-views.tsx +587 -75
  163. package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
  164. package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
  165. package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
  166. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
  167. package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
  168. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
  169. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
  170. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
  171. package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
  172. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +2 -1
  173. package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
  174. package/src/declarations.d.ts +1 -0
  175. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
  176. package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
  177. package/src/hooks/use-calendar-sync.tsx +247 -243
  178. package/src/hooks/use-calendar.tsx +323 -138
  179. package/src/hooks/use-task-actions.ts +24 -0
  180. package/src/hooks/use-user-workspace-config.ts +75 -0
  181. package/src/hooks/use-workspace-currency.ts +8 -3
  182. package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
@@ -4,6 +4,7 @@ import { type QueryClient, useMutation } from '@tanstack/react-query';
4
4
  import { bulkWorkspaceTasks } from '@tuturuuu/internal-api/tasks';
5
5
  import { toast } from '@tuturuuu/ui/sonner';
6
6
  import type { BoardBroadcastFn } from '../../../../shared/board-broadcast-context';
7
+ import { invalidateKanbanDeadlineTasks } from '../data/kanban-deadline-query';
7
8
  import type { BulkOperationI18n } from './bulk-operation-i18n';
8
9
  import {
9
10
  type BulkTaskWorkspaceGroup,
@@ -390,6 +391,7 @@ export function useBulkDeleteTasks(
390
391
  for (const tid of succeededTaskIds) {
391
392
  broadcast?.('task:delete', { taskId: tid });
392
393
  }
394
+ void invalidateKanbanDeadlineTasks(queryClient, boardId);
393
395
 
394
396
  clearSelection();
395
397
  setBulkDeleteOpen(false);
@@ -6,6 +6,7 @@ import type { Task } from '@tuturuuu/types/primitives/Task';
6
6
  import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
7
7
  import { toast } from '@tuturuuu/ui/sonner';
8
8
  import type { BoardBroadcastFn } from '../../../../shared/board-broadcast-context';
9
+ import { invalidateKanbanDeadlineTasks } from '../data/kanban-deadline-query';
9
10
  import type { BulkOperationI18n } from './bulk-operation-i18n';
10
11
  import { getInternalApiOptions } from './bulk-operation-utils';
11
12
  import {
@@ -184,6 +185,8 @@ export function useBulkMoveToBoard(
184
185
  for (const tid of movedTaskIds) {
185
186
  broadcast?.('task:delete', { taskId: tid });
186
187
  }
188
+ void invalidateKanbanDeadlineTasks(queryClient, boardId);
189
+ void invalidateKanbanDeadlineTasks(queryClient, data.targetBoardId);
187
190
 
188
191
  if (data.failures.length > 0) {
189
192
  toast.warning(
@@ -428,6 +431,7 @@ export function useBulkMoveToList(
428
431
  },
429
432
  });
430
433
  }
434
+ void invalidateKanbanDeadlineTasks(queryClient, boardId);
431
435
 
432
436
  if (data.failures.length > 0) {
433
437
  toast.warning(
@@ -631,6 +635,7 @@ export function useBulkMoveToStatus(
631
635
  },
632
636
  });
633
637
  }
638
+ void invalidateKanbanDeadlineTasks(queryClient, boardId);
634
639
 
635
640
  if (data.failures.length > 0) {
636
641
  toast.warning(
@@ -4,6 +4,7 @@ import { type QueryClient, useMutation } from '@tanstack/react-query';
4
4
  import type { Task } from '@tuturuuu/types/primitives/Task';
5
5
  import { toast } from '@tuturuuu/ui/sonner';
6
6
  import type { BoardBroadcastFn } from '../../../../shared/board-broadcast-context';
7
+ import { invalidateKanbanDeadlineTasks } from '../data/kanban-deadline-query';
7
8
  import type { BulkOperationI18n } from './bulk-operation-i18n';
8
9
  import {
9
10
  bulkWorkspaceTasksByEffectiveWorkspace,
@@ -322,6 +323,7 @@ export function useBulkUpdateDueDate(
322
323
  task: { id: tid, end_date: data.end_date },
323
324
  });
324
325
  }
326
+ void invalidateKanbanDeadlineTasks(queryClient, boardId);
325
327
 
326
328
  if (data.failures.length > 0) {
327
329
  toast.warning(
@@ -434,6 +436,7 @@ export function useBulkUpdateCustomDueDate(
434
436
  task: { id: tid, end_date: data.end_date },
435
437
  });
436
438
  }
439
+ void invalidateKanbanDeadlineTasks(queryClient, boardId);
437
440
 
438
441
  if (data.failures.length > 0) {
439
442
  toast.warning(
@@ -1,22 +1,70 @@
1
+ import type { QueryClient } from '@tanstack/react-query';
1
2
  import { listWorkspaceTasks } from '@tuturuuu/internal-api';
3
+ import type { ListWorkspaceTasksOptions } from '@tuturuuu/internal-api/tasks';
2
4
  import type { Task } from '@tuturuuu/types/primitives/Task';
3
5
 
4
6
  const KANBAN_DEADLINE_TASK_PAGE_SIZE = 200;
7
+ export const KANBAN_DEADLINE_TASKS_QUERY_KEY = 'kanban-deadline-tasks';
8
+
9
+ export function getKanbanDeadlineTasksQueryKey(
10
+ workspaceId: string,
11
+ boardId: string | null | undefined,
12
+ taskQueryOptions?: ListWorkspaceTasksOptions
13
+ ) {
14
+ return [
15
+ KANBAN_DEADLINE_TASKS_QUERY_KEY,
16
+ workspaceId,
17
+ boardId,
18
+ taskQueryOptions,
19
+ ] as const;
20
+ }
21
+
22
+ export function invalidateKanbanDeadlineTasks(
23
+ queryClient: QueryClient,
24
+ boardId?: string | null
25
+ ) {
26
+ return queryClient.invalidateQueries({
27
+ predicate: (query) => {
28
+ const queryKey = query.queryKey;
29
+ if (!Array.isArray(queryKey)) return false;
30
+ if (queryKey[0] !== KANBAN_DEADLINE_TASKS_QUERY_KEY) return false;
31
+ return !boardId || queryKey[2] === boardId;
32
+ },
33
+ });
34
+ }
5
35
 
6
36
  interface ListKanbanDeadlineTasksOptions {
7
37
  boardId: string;
38
+ taskQueryOptions?: ListWorkspaceTasksOptions;
8
39
  workspaceId: string;
9
40
  }
10
41
 
11
42
  export async function listKanbanDeadlineTasks({
12
43
  boardId,
44
+ taskQueryOptions,
13
45
  workspaceId,
14
46
  }: ListKanbanDeadlineTasksOptions): Promise<Task[]> {
15
47
  const tasks: Task[] = [];
16
48
  let offset = 0;
49
+ const {
50
+ boardId: _boardId,
51
+ closed: _closed,
52
+ completed: _completed,
53
+ externalSortBy: _externalSortBy,
54
+ hasDueDate: _hasDueDate,
55
+ includeCount: _includeCount,
56
+ includeListCounts: _includeListCounts,
57
+ includeRelationshipSummary: _includeRelationshipSummary,
58
+ limit: _limit,
59
+ listId: _listId,
60
+ offset: _offset,
61
+ sortBy: _sortBy,
62
+ ...filterOptions
63
+ } = taskQueryOptions ?? {};
17
64
 
18
65
  while (true) {
19
66
  const response = await listWorkspaceTasks(workspaceId, {
67
+ ...filterOptions,
20
68
  boardId,
21
69
  closed: 'exclude',
22
70
  completed: 'exclude',
@@ -25,9 +73,9 @@ export async function listKanbanDeadlineTasks({
25
73
  includeCount: true,
26
74
  includeRelationshipSummary: false,
27
75
  limit: KANBAN_DEADLINE_TASK_PAGE_SIZE,
28
- listStatuses: ['not_started', 'active'],
76
+ listStatuses: filterOptions.listStatuses ?? ['not_started', 'active'],
29
77
  offset,
30
- sourceScope: 'all_visible',
78
+ sourceScope: filterOptions.sourceScope ?? 'all_visible',
31
79
  });
32
80
 
33
81
  tasks.push(...response.tasks);
@@ -43,6 +43,23 @@ describe('getColumnReorderUpdates', () => {
43
43
  expect(getColumnReorderUpdates(columns, 'todo', 'doing')).toBeNull();
44
44
  });
45
45
 
46
+ it('excludes synthetic external staging lanes from persisted position repairs', () => {
47
+ const externalLane = {
48
+ ...makeList('external', 'not_started', -1),
49
+ is_external_staging: true,
50
+ };
51
+ const columns = [
52
+ externalLane,
53
+ makeList('todo', 'not_started', 0),
54
+ makeList('backlog', 'not_started', 1),
55
+ ];
56
+
57
+ expect(getColumnReorderUpdates(columns, 'backlog', 'todo')).toEqual([
58
+ { listId: 'backlog', newPosition: 0 },
59
+ { listId: 'todo', newPosition: 1 },
60
+ ]);
61
+ });
62
+
46
63
  it('sorts columns in the same order they are rendered in Kanban', () => {
47
64
  const columns = [
48
65
  makeList('closed', 'closed', 0),
@@ -39,7 +39,10 @@ export function getColumnReorderUpdates(
39
39
  }
40
40
 
41
41
  const statusColumns = [...columns]
42
- .filter((column) => column.status === activeColumn.status)
42
+ .filter(
43
+ (column) =>
44
+ column.status === activeColumn.status && !column.is_external_staging
45
+ )
43
46
  .sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
44
47
 
45
48
  const activeIndex = statusColumns.findIndex(
@@ -3,17 +3,27 @@ import type { Task } from '@tuturuuu/types/primitives/Task';
3
3
  import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
4
4
  import { MAX_SAFE_INTEGER_SORT } from '../kanban-constants';
5
5
  import type { DragCacheSnapshot, TaskSortKeyRepair } from './task-drag-types';
6
+ import { getEffectiveTaskSortKey } from './task-sort-key';
6
7
 
7
8
  const SORT_KEY_BASE_UNIT = 1_000_000;
8
9
  const SORT_KEY_DEFAULT = SORT_KEY_BASE_UNIT * 1000;
9
10
  const SORT_KEY_MIN_GAP = 1000;
10
11
 
12
+ type SortKeyPlanTask = Pick<
13
+ Task,
14
+ | 'id'
15
+ | 'is_personal_external'
16
+ | 'is_personal_external_default'
17
+ | 'personal_sort_key'
18
+ | 'sort_key'
19
+ >;
20
+
11
21
  function getTaskSortKeyInsertionContext({
12
22
  activeTaskId,
13
23
  orderedTasks,
14
24
  }: {
15
25
  activeTaskId: string;
16
- orderedTasks: Pick<Task, 'id' | 'sort_key'>[];
26
+ orderedTasks: SortKeyPlanTask[];
17
27
  }) {
18
28
  const activeIndex = orderedTasks.findIndex(
19
29
  (task) => task.id === activeTaskId
@@ -26,10 +36,15 @@ function getTaskSortKeyInsertionContext({
26
36
  };
27
37
  }
28
38
 
39
+ const nextTask = orderedTasks[activeIndex + 1];
40
+ const previousTask = orderedTasks[activeIndex - 1];
41
+
29
42
  return {
30
43
  activeIndex,
31
- nextSortKey: orderedTasks[activeIndex + 1]?.sort_key ?? null,
32
- previousSortKey: orderedTasks[activeIndex - 1]?.sort_key ?? null,
44
+ nextSortKey: nextTask ? getEffectiveTaskSortKey(nextTask) : null,
45
+ previousSortKey: previousTask
46
+ ? getEffectiveTaskSortKey(previousTask)
47
+ : null,
33
48
  };
34
49
  }
35
50
 
@@ -94,7 +109,7 @@ function getPreviewSortKeyPlan({
94
109
  targetListId,
95
110
  }: {
96
111
  activeTaskId: string;
97
- orderedTasks: Pick<Task, 'id' | 'sort_key'>[];
112
+ orderedTasks: SortKeyPlanTask[];
98
113
  targetListId: string;
99
114
  }): {
100
115
  previewSortKey: number;
@@ -119,10 +134,14 @@ function getPreviewSortKeyPlan({
119
134
  });
120
135
  const effectiveOrderedTasks = orderedTasks.map((task) => ({
121
136
  ...task,
122
- sort_key: task.id === activeTaskId ? previewSortKey : task.sort_key,
137
+ effective_sort_key:
138
+ task.id === activeTaskId ? previewSortKey : getEffectiveTaskSortKey(task),
123
139
  }));
124
140
  const orderNeedsRepair = effectiveOrderedTasks.some((task, index) => {
125
- if (typeof task.sort_key !== 'number' || !Number.isFinite(task.sort_key)) {
141
+ if (
142
+ typeof task.effective_sort_key !== 'number' ||
143
+ !Number.isFinite(task.effective_sort_key)
144
+ ) {
126
145
  return true;
127
146
  }
128
147
 
@@ -130,13 +149,16 @@ function getPreviewSortKeyPlan({
130
149
  if (!previousTask) return false;
131
150
 
132
151
  if (
133
- typeof previousTask.sort_key !== 'number' ||
134
- !Number.isFinite(previousTask.sort_key)
152
+ typeof previousTask.effective_sort_key !== 'number' ||
153
+ !Number.isFinite(previousTask.effective_sort_key)
135
154
  ) {
136
155
  return true;
137
156
  }
138
157
 
139
- return task.sort_key - previousTask.sort_key < SORT_KEY_MIN_GAP;
158
+ return (
159
+ task.effective_sort_key - previousTask.effective_sort_key <
160
+ SORT_KEY_MIN_GAP
161
+ );
140
162
  });
141
163
 
142
164
  if (
@@ -197,6 +219,9 @@ export function getTaskDropPreviewCacheTasks({
197
219
  ...task,
198
220
  list_id: targetListId,
199
221
  sort_key: previewSortKey,
222
+ personal_sort_key: task.is_personal_external
223
+ ? previewSortKey
224
+ : task.personal_sort_key,
200
225
  completed: targetIsCompleted,
201
226
  completed_at: targetIsCompleted
202
227
  ? (task.completed_at ?? mutationTimestamp)
@@ -210,6 +235,10 @@ export function getTaskDropPreviewCacheTasks({
210
235
  ? ({
211
236
  ...task,
212
237
  sort_key: repairedSortKeysByTaskId.get(task.id) ?? task.sort_key,
238
+ personal_sort_key: task.is_personal_external
239
+ ? (repairedSortKeysByTaskId.get(task.id) ??
240
+ task.personal_sort_key)
241
+ : task.personal_sort_key,
213
242
  _localMutationAt: localMutationAt,
214
243
  } as Task & { _localMutationAt: number })
215
244
  : task
@@ -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
- import { MAX_SAFE_INTEGER_SORT } from '../kanban-constants';
4
3
  import type { DragPreviewPosition, TaskDropPosition } from './task-drag-types';
4
+ import { compareTasksByEffectiveSortKey } from './task-sort-key';
5
5
 
6
6
  export function getNeighborTaskIds(tasks: Task[], taskId: string) {
7
7
  const taskIndex = tasks.findIndex((task) => task.id === taskId);
@@ -142,13 +142,7 @@ export function sortTasksForList({
142
142
  }
143
143
 
144
144
  if (!disableSort) {
145
- const sortA = a.sort_key ?? MAX_SAFE_INTEGER_SORT;
146
- const sortB = b.sort_key ?? MAX_SAFE_INTEGER_SORT;
147
- if (sortA !== sortB) return sortA - sortB;
148
- if (!a.created_at || !b.created_at) return 0;
149
- return (
150
- new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
151
- );
145
+ return compareTasksByEffectiveSortKey(a, b);
152
146
  }
153
147
 
154
148
  return 0;
@@ -0,0 +1,47 @@
1
+ import type { Task } from '@tuturuuu/types/primitives/Task';
2
+ import { MAX_SAFE_INTEGER_SORT } from '../kanban-constants';
3
+
4
+ type SortKeyTask = Pick<
5
+ Task,
6
+ | 'is_personal_external'
7
+ | 'is_personal_external_default'
8
+ | 'personal_sort_key'
9
+ | 'sort_key'
10
+ >;
11
+
12
+ type SortableTask = SortKeyTask & Pick<Task, 'created_at'>;
13
+
14
+ export function getEffectiveTaskSortKey(task: SortKeyTask) {
15
+ if (typeof task.sort_key === 'number' && Number.isFinite(task.sort_key)) {
16
+ return task.sort_key;
17
+ }
18
+
19
+ if (
20
+ task.is_personal_external === true &&
21
+ task.is_personal_external_default !== true &&
22
+ typeof task.personal_sort_key === 'number' &&
23
+ Number.isFinite(task.personal_sort_key)
24
+ ) {
25
+ return task.personal_sort_key;
26
+ }
27
+
28
+ return null;
29
+ }
30
+
31
+ export function compareTasksByEffectiveSortKey(
32
+ left: SortableTask,
33
+ right: SortableTask
34
+ ) {
35
+ const leftSortKey = getEffectiveTaskSortKey(left) ?? MAX_SAFE_INTEGER_SORT;
36
+ const rightSortKey = getEffectiveTaskSortKey(right) ?? MAX_SAFE_INTEGER_SORT;
37
+
38
+ if (leftSortKey !== rightSortKey) {
39
+ return leftSortKey - rightSortKey;
40
+ }
41
+
42
+ if (!left.created_at || !right.created_at) return 0;
43
+
44
+ return (
45
+ new Date(left.created_at).getTime() - new Date(right.created_at).getTime()
46
+ );
47
+ }
@@ -22,6 +22,7 @@ import {
22
22
  import { hasDraggableData } from '@tuturuuu/utils/task-helpers';
23
23
  import { useCallback, useRef, useState } from 'react';
24
24
  import { useBoardBroadcast } from '../../../../shared/board-broadcast-context';
25
+ import { invalidateKanbanDeadlineTasks } from '../data/kanban-deadline-query';
25
26
  import { MAX_SAFE_INTEGER_SORT } from '../kanban-constants';
26
27
  import { useAutoScroll } from './auto-scroll';
27
28
  import { getColumnReorderUpdates } from './column-reorder';
@@ -62,6 +63,10 @@ import type {
62
63
  TaskRect,
63
64
  VerticalRect,
64
65
  } from './task-drag-types';
66
+ import {
67
+ compareTasksByEffectiveSortKey,
68
+ getEffectiveTaskSortKey,
69
+ } from './task-sort-key';
65
70
 
66
71
  export {
67
72
  applyTaskDropPreviewToCache,
@@ -1162,14 +1167,14 @@ export function useKanbanDnd({
1162
1167
  const nextTask = projectedDropOrder[1];
1163
1168
  newSortKey = await calculateSortKeyWithRetry(
1164
1169
  null,
1165
- nextTask?.sort_key ?? null,
1170
+ nextTask ? getEffectiveTaskSortKey(nextTask) : null,
1166
1171
  targetListId,
1167
1172
  projectedDropOrder
1168
1173
  );
1169
1174
  } else if (newIndex === projectedDropOrder.length - 1) {
1170
1175
  const prevTask = projectedDropOrder[projectedDropOrder.length - 2];
1171
1176
  newSortKey = await calculateSortKeyWithRetry(
1172
- prevTask?.sort_key ?? null,
1177
+ prevTask ? getEffectiveTaskSortKey(prevTask) : null,
1173
1178
  null,
1174
1179
  targetListId,
1175
1180
  projectedDropOrder
@@ -1178,8 +1183,8 @@ export function useKanbanDnd({
1178
1183
  const prevTask = projectedDropOrder[newIndex - 1];
1179
1184
  const nextTask = projectedDropOrder[newIndex + 1];
1180
1185
  newSortKey = await calculateSortKeyWithRetry(
1181
- prevTask?.sort_key ?? null,
1182
- nextTask?.sort_key ?? null,
1186
+ prevTask ? getEffectiveTaskSortKey(prevTask) : null,
1187
+ nextTask ? getEffectiveTaskSortKey(nextTask) : null,
1183
1188
  targetListId,
1184
1189
  projectedDropOrder
1185
1190
  );
@@ -1195,7 +1200,8 @@ export function useKanbanDnd({
1195
1200
  const needsUpdate =
1196
1201
  dropChangesVisualOrder ||
1197
1202
  (newSortKey !== null &&
1198
- (activeTaskForDrop.sort_key ?? MAX_SAFE_INTEGER_SORT) !== newSortKey);
1203
+ (getEffectiveTaskSortKey(activeTaskForDrop) ??
1204
+ MAX_SAFE_INTEGER_SORT) !== newSortKey);
1199
1205
 
1200
1206
  let shouldPreservePendingAfterDragReset = false;
1201
1207
  const persistPersonalPlacementMove = (
@@ -1211,6 +1217,19 @@ export function useKanbanDnd({
1211
1217
  shouldPreservePendingAfterDragReset = true;
1212
1218
 
1213
1219
  void movePersonalPlacementTask(task, targetListId, sortKey, order)
1220
+ .then((updatedTask) => {
1221
+ broadcast?.('task:upsert', {
1222
+ task: {
1223
+ id: updatedTask.id,
1224
+ list_id: updatedTask.list_id,
1225
+ sort_key: updatedTask.sort_key,
1226
+ personal_sort_key: updatedTask.personal_sort_key,
1227
+ completed_at: updatedTask.completed_at,
1228
+ closed_at: updatedTask.closed_at,
1229
+ },
1230
+ });
1231
+ void invalidateKanbanDeadlineTasks(queryClient, boardId);
1232
+ })
1214
1233
  .catch((error) => {
1215
1234
  console.error('Failed to update personal task placement:', error);
1216
1235
  rollbackOptimisticDropPreview();
@@ -1231,16 +1250,9 @@ export function useKanbanDnd({
1231
1250
  .map((taskId) => baseTasks.find((t) => t.id === taskId))
1232
1251
  .filter((t): t is Task => t !== undefined);
1233
1252
 
1234
- const sortedTasksToMove = selectedTaskObjects.sort((a, b) => {
1235
- const sortA = a.sort_key ?? MAX_SAFE_INTEGER_SORT;
1236
- const sortB = b.sort_key ?? MAX_SAFE_INTEGER_SORT;
1237
- if (sortA !== sortB) return sortA - sortB;
1238
- if (!a.created_at || !b.created_at) return 0;
1239
- return (
1240
- new Date(a.created_at).getTime() -
1241
- new Date(b.created_at).getTime()
1242
- );
1243
- });
1253
+ const sortedTasksToMove = selectedTaskObjects.sort(
1254
+ compareTasksByEffectiveSortKey
1255
+ );
1244
1256
 
1245
1257
  if (
1246
1258
  targetIsExternalStaging &&
@@ -1306,7 +1318,7 @@ export function useKanbanDnd({
1306
1318
  const nextTask = simulatedTargetList[1];
1307
1319
  batchSortKey = await calculateSortKeyWithRetry(
1308
1320
  null,
1309
- nextTask?.sort_key ?? null,
1321
+ nextTask ? getEffectiveTaskSortKey(nextTask) : null,
1310
1322
  targetListId,
1311
1323
  targetListTasks
1312
1324
  );
@@ -1316,7 +1328,7 @@ export function useKanbanDnd({
1316
1328
  ) {
1317
1329
  const prevTask = simulatedTargetList[positionInSimulated - 1];
1318
1330
  batchSortKey = await calculateSortKeyWithRetry(
1319
- prevTask?.sort_key ?? null,
1331
+ prevTask ? getEffectiveTaskSortKey(prevTask) : null,
1320
1332
  null,
1321
1333
  targetListId,
1322
1334
  targetListTasks
@@ -1334,8 +1346,8 @@ export function useKanbanDnd({
1334
1346
 
1335
1347
  if (!prevIsMoving && !nextIsMoving) {
1336
1348
  batchSortKey = await calculateSortKeyWithRetry(
1337
- prevTask?.sort_key ?? null,
1338
- nextTask?.sort_key ?? null,
1349
+ prevTask ? getEffectiveTaskSortKey(prevTask) : null,
1350
+ nextTask ? getEffectiveTaskSortKey(nextTask) : null,
1339
1351
  targetListId,
1340
1352
  targetListTasks
1341
1353
  );
@@ -1349,8 +1361,10 @@ export function useKanbanDnd({
1349
1361
  }
1350
1362
  }
1351
1363
  batchSortKey = await calculateSortKeyWithRetry(
1352
- stationaryPrev?.sort_key ?? null,
1353
- nextTask?.sort_key ?? null,
1364
+ stationaryPrev
1365
+ ? getEffectiveTaskSortKey(stationaryPrev)
1366
+ : null,
1367
+ nextTask ? getEffectiveTaskSortKey(nextTask) : null,
1354
1368
  targetListId,
1355
1369
  targetListTasks
1356
1370
  );
@@ -1368,8 +1382,10 @@ export function useKanbanDnd({
1368
1382
  }
1369
1383
  }
1370
1384
  batchSortKey = await calculateSortKeyWithRetry(
1371
- prevTask?.sort_key ?? null,
1372
- stationaryNext?.sort_key ?? null,
1385
+ prevTask ? getEffectiveTaskSortKey(prevTask) : null,
1386
+ stationaryNext
1387
+ ? getEffectiveTaskSortKey(stationaryNext)
1388
+ : null,
1373
1389
  targetListId,
1374
1390
  targetListTasks
1375
1391
  );
@@ -1398,8 +1414,8 @@ export function useKanbanDnd({
1398
1414
  }
1399
1415
 
1400
1416
  batchSortKey = await calculateSortKeyWithRetry(
1401
- boundaryPrev?.sort_key ?? null,
1402
- boundaryNext?.sort_key ?? null,
1417
+ boundaryPrev ? getEffectiveTaskSortKey(boundaryPrev) : null,
1418
+ boundaryNext ? getEffectiveTaskSortKey(boundaryNext) : null,
1403
1419
  targetListId,
1404
1420
  targetListTasks
1405
1421
  );
@@ -1430,6 +1446,7 @@ export function useKanbanDnd({
1430
1446
  closed_at: updatedTask.closed_at,
1431
1447
  },
1432
1448
  });
1449
+ void invalidateKanbanDeadlineTasks(queryClient, boardId);
1433
1450
  },
1434
1451
  onSettled: () => {
1435
1452
  clearPendingTaskIds(pendingTaskIds);
@@ -1475,14 +1492,45 @@ export function useKanbanDnd({
1475
1492
  closed_at: updatedTask.closed_at,
1476
1493
  },
1477
1494
  });
1495
+ void invalidateKanbanDeadlineTasks(queryClient, boardId);
1478
1496
  };
1479
1497
 
1480
1498
  if (repairedTaskSortKeys.length > 0) {
1481
1499
  void (async () => {
1482
1500
  try {
1501
+ const repairTaskById = new Map<string, Task>();
1502
+ for (const task of [
1503
+ ...baseTasks,
1504
+ ...(optimisticDropPreview?.previousTasks ?? []),
1505
+ ...(optimisticDropPreview?.previousFullTasks ?? []),
1506
+ ]) {
1507
+ repairTaskById.set(task.id, task);
1508
+ }
1509
+
1483
1510
  const results = await Promise.allSettled(
1484
- repairedTaskSortKeys.map((repair) =>
1485
- reorderTaskMutation.mutateAsync(
1511
+ repairedTaskSortKeys.map(async (repair) => {
1512
+ const repairTask = repairTaskById.get(repair.taskId);
1513
+
1514
+ if (repairTask && usesPersonalPlacement(repairTask)) {
1515
+ const updatedTask = await movePersonalPlacementTask(
1516
+ repairTask,
1517
+ repair.listId,
1518
+ repair.sortKey
1519
+ );
1520
+ broadcast?.('task:upsert', {
1521
+ task: {
1522
+ id: updatedTask.id,
1523
+ list_id: updatedTask.list_id,
1524
+ sort_key: updatedTask.sort_key,
1525
+ personal_sort_key: updatedTask.personal_sort_key,
1526
+ completed_at: updatedTask.completed_at,
1527
+ closed_at: updatedTask.closed_at,
1528
+ },
1529
+ });
1530
+ return updatedTask;
1531
+ }
1532
+
1533
+ return reorderTaskMutation.mutateAsync(
1486
1534
  {
1487
1535
  taskId: repair.taskId,
1488
1536
  newListId: repair.listId,
@@ -1495,14 +1543,17 @@ export function useKanbanDnd({
1495
1543
  {
1496
1544
  onSuccess: handleReorderSuccess,
1497
1545
  }
1498
- )
1499
- )
1546
+ );
1547
+ })
1500
1548
  );
1501
1549
  const failedResults = results.filter(
1502
1550
  (result) => result.status === 'rejected'
1503
1551
  );
1504
1552
 
1505
- if (failedResults.length === 0) return;
1553
+ if (failedResults.length === 0) {
1554
+ void invalidateKanbanDeadlineTasks(queryClient, boardId);
1555
+ return;
1556
+ }
1506
1557
 
1507
1558
  console.error(
1508
1559
  'Failed to persist repaired task sort keys:',