@tuturuuu/ui 0.7.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 (226) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/biome.json +1 -1
  3. package/package.json +75 -73
  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/currency-input.test.tsx +43 -0
  29. package/src/components/ui/currency-input.tsx +1 -1
  30. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
  31. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
  32. package/src/components/ui/custom/combobox.test.tsx +195 -0
  33. package/src/components/ui/custom/combobox.tsx +273 -156
  34. package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
  35. package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
  36. package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
  37. package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
  38. package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
  39. package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
  40. package/src/components/ui/custom/theme-toggle.tsx +1 -1
  41. package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +60 -35
  42. package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +176 -167
  43. package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +16 -10
  44. package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +75 -36
  45. package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +39 -42
  46. package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +1 -1
  47. package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +113 -91
  48. package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +73 -32
  49. package/src/components/ui/custom/workspace-select.tsx +8 -3
  50. package/src/components/ui/dialog.test.tsx +52 -0
  51. package/src/components/ui/dialog.tsx +6 -2
  52. package/src/components/ui/dropdown-menu.tsx +5 -1
  53. package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
  54. package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
  55. package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
  56. package/src/components/ui/finance/debts/debts-page.tsx +15 -2
  57. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
  58. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
  59. package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
  60. package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
  61. package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
  62. package/src/components/ui/finance/invoices/utils.ts +3 -1
  63. package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
  64. package/src/components/ui/finance/transactions/form-types.ts +3 -0
  65. package/src/components/ui/finance/transactions/form.tsx +2 -0
  66. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
  67. package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
  68. package/src/components/ui/finance/transactions/transaction-card.tsx +21 -9
  69. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
  70. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
  71. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
  72. package/src/components/ui/finance/wallets/form.test.tsx +51 -3
  73. package/src/components/ui/finance/wallets/form.tsx +15 -4
  74. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  75. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
  76. package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
  77. package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
  78. package/src/components/ui/input-otp.tsx +1 -1
  79. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
  80. package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
  81. package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
  82. package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
  83. package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
  84. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
  85. package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
  86. package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
  87. package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
  88. package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
  89. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
  90. package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
  91. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
  92. package/src/components/ui/money-input.test.tsx +64 -0
  93. package/src/components/ui/money-input.tsx +63 -0
  94. package/src/components/ui/navigation-menu.tsx +1 -1
  95. package/src/components/ui/pagination.tsx +1 -1
  96. package/src/components/ui/radio-group.tsx +1 -1
  97. package/src/components/ui/select.tsx +5 -1
  98. package/src/components/ui/sheet.tsx +1 -1
  99. package/src/components/ui/sidebar.tsx +1 -1
  100. package/src/components/ui/storefront/cart-popover.tsx +61 -0
  101. package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
  102. package/src/components/ui/storefront/cart-summary.tsx +104 -80
  103. package/src/components/ui/storefront/checkout-overlay.tsx +26 -0
  104. package/src/components/ui/storefront/hero-panel.tsx +2 -8
  105. package/src/components/ui/storefront/image-panel.tsx +6 -0
  106. package/src/components/ui/storefront/index.ts +11 -0
  107. package/src/components/ui/storefront/listing-card.tsx +84 -22
  108. package/src/components/ui/storefront/merch-sections.tsx +70 -0
  109. package/src/components/ui/storefront/product-detail.tsx +289 -0
  110. package/src/components/ui/storefront/product-dialog.tsx +72 -0
  111. package/src/components/ui/storefront/storefront-surface.test.tsx +221 -3
  112. package/src/components/ui/storefront/storefront-surface.tsx +288 -153
  113. package/src/components/ui/storefront/types.ts +27 -1
  114. package/src/components/ui/storefront/utils.ts +117 -27
  115. package/src/components/ui/text-editor/__tests__/content-migration.test.ts +32 -0
  116. package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
  117. package/src/components/ui/text-editor/__tests__/image-extension.test.ts +69 -1
  118. package/src/components/ui/text-editor/__tests__/video-extension.test.ts +47 -0
  119. package/src/components/ui/text-editor/background-color-extension.ts +62 -0
  120. package/src/components/ui/text-editor/color-controls.tsx +284 -0
  121. package/src/components/ui/text-editor/content-migration.ts +41 -18
  122. package/src/components/ui/text-editor/editor.tsx +69 -14
  123. package/src/components/ui/text-editor/extensions.ts +9 -3
  124. package/src/components/ui/text-editor/highlight-extension.ts +22 -0
  125. package/src/components/ui/text-editor/image-extension.ts +40 -18
  126. package/src/components/ui/text-editor/tool-bar.tsx +9 -16
  127. package/src/components/ui/text-editor/video-extension.ts +11 -2
  128. package/src/components/ui/toast.tsx +1 -1
  129. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
  130. package/src/components/ui/tu-do/boards/__tests__/workspace-projects-client-page.test.tsx +70 -1
  131. package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
  132. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
  133. package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +127 -0
  134. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +113 -46
  135. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
  136. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
  137. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
  138. package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
  139. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
  140. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
  141. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +51 -9
  142. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
  143. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
  144. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -0
  145. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +127 -38
  146. package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
  147. package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
  148. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
  149. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
  150. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
  151. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
  152. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
  153. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
  154. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
  155. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
  156. package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
  157. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +410 -4
  158. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +106 -14
  159. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
  160. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
  161. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
  162. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +186 -0
  163. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +59 -2
  164. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
  165. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
  166. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
  167. package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
  168. package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
  169. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
  170. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-display.ts +9 -0
  171. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +8 -16
  172. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +5 -25
  173. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.test.ts +36 -1
  174. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.ts +51 -2
  175. package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
  176. package/src/components/ui/tu-do/boards/workspace-projects-client-page.tsx +13 -3
  177. package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
  178. package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
  179. package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
  180. package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
  181. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
  182. package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
  183. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
  184. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
  185. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
  186. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +237 -3
  187. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
  188. package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
  189. package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
  190. package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
  191. package/src/components/ui/tu-do/shared/board-header.tsx +465 -937
  192. package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
  193. package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
  194. package/src/components/ui/tu-do/shared/board-views.tsx +596 -82
  195. package/src/components/ui/tu-do/shared/cursor-overlay-multi-wrapper.tsx +53 -12
  196. package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
  197. package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
  198. package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
  199. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
  200. package/src/components/ui/tu-do/shared/task-dialog-presentation.test.ts +53 -0
  201. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +19 -0
  202. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +57 -0
  203. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +136 -111
  204. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -1
  205. package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
  206. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
  207. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
  208. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
  209. package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
  210. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +44 -15
  211. package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
  212. package/src/declarations.d.ts +1 -0
  213. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
  214. package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
  215. package/src/hooks/__tests__/useBoardRealtime.test.tsx +2 -2
  216. package/src/hooks/__tests__/useCursorTracking.test.tsx +212 -0
  217. package/src/hooks/use-calendar-sync.tsx +247 -243
  218. package/src/hooks/use-calendar.tsx +323 -138
  219. package/src/hooks/use-task-actions.ts +24 -0
  220. package/src/hooks/use-user-workspace-config.ts +75 -0
  221. package/src/hooks/use-workspace-currency.ts +8 -3
  222. package/src/hooks/useBoardRealtime.ts +6 -3
  223. package/src/hooks/useBoardRealtime.types.ts +11 -0
  224. package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
  225. package/src/hooks/useCursorTracking.ts +91 -27
  226. package/src/hooks/useTaskUserRealtime.ts +5 -3
@@ -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
@@ -263,6 +292,7 @@ export function applyTaskDropPreviewToCache({
263
292
  }
264
293
 
265
294
  return {
295
+ localMutationAt,
266
296
  previousFullTasks: snapshot.fullTasks,
267
297
  previousTasks: snapshot.tasks,
268
298
  previewSortKey: previewTasks.previewSortKey,
@@ -270,6 +300,18 @@ export function applyTaskDropPreviewToCache({
270
300
  };
271
301
  }
272
302
 
303
+ export function hasTaskLocalMutationAt(
304
+ tasks: Task[] | undefined,
305
+ taskId: string,
306
+ localMutationAt: number
307
+ ) {
308
+ const task = tasks?.find((item) => item.id === taskId) as
309
+ | (Task & { _localMutationAt?: unknown })
310
+ | undefined;
311
+
312
+ return task?._localMutationAt === localMutationAt;
313
+ }
314
+
273
315
  export function mergeTaskIntoBoardTaskCache(
274
316
  currentTasks: Task[] | undefined,
275
317
  nextTask: 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
+ }
@@ -1,13 +1,16 @@
1
+ import { QueryClient } from '@tanstack/react-query';
1
2
  import type { Task } from '@tuturuuu/types/primitives/Task';
2
3
  import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
3
4
  import { describe, expect, it } from 'vitest';
4
5
  import {
6
+ applyTaskDropPreviewToCache,
5
7
  getProjectedTaskDropOrderFromPreview,
6
8
  getTaskDropEndPreviewFromRects,
7
9
  getTaskDropPositionFromRects,
8
10
  getTaskDropPreviewCacheTasks,
9
11
  getTaskDropPreviewFromRects,
10
12
  getTaskInsertionIndex,
13
+ hasTaskLocalMutationAt,
11
14
  insertTaskAtDropPosition,
12
15
  mergePersonalPlacementMutationTask,
13
16
  mergeTaskIntoBoardTaskCache,
@@ -683,4 +686,64 @@ describe('task drag insertion helpers', () => {
683
686
  })
684
687
  );
685
688
  });
689
+
690
+ it('marks optimistic previews so stale drag rollbacks can be skipped', () => {
691
+ const queryClient = new QueryClient();
692
+ const boardId = 'board-1';
693
+ const activeTask = createTask({
694
+ id: 'task-1',
695
+ list_id: 'source-list',
696
+ sort_key: 1_000_000,
697
+ });
698
+ const targetTask = createTask({
699
+ id: 'task-2',
700
+ list_id: 'target-list',
701
+ sort_key: 2_000_000,
702
+ });
703
+ const snapshot = {
704
+ fullTasks: [activeTask, targetTask],
705
+ tasks: [activeTask, targetTask],
706
+ };
707
+
708
+ queryClient.setQueryData(['tasks', boardId], snapshot.tasks);
709
+ queryClient.setQueryData(['tasks-full', boardId], snapshot.fullTasks);
710
+
711
+ const preview = applyTaskDropPreviewToCache({
712
+ activeTask,
713
+ boardId,
714
+ orderedTasks: [targetTask, activeTask],
715
+ queryClient,
716
+ snapshot,
717
+ targetList: createList({ id: 'target-list' }),
718
+ targetListId: 'target-list',
719
+ });
720
+
721
+ expect(preview?.localMutationAt).toEqual(expect.any(Number));
722
+ expect(
723
+ hasTaskLocalMutationAt(
724
+ queryClient.getQueryData<Task[]>(['tasks', boardId]),
725
+ activeTask.id,
726
+ preview?.localMutationAt ?? -1
727
+ )
728
+ ).toBe(true);
729
+
730
+ queryClient.setQueryData<Task[]>(['tasks', boardId], (currentTasks) =>
731
+ currentTasks?.map((task) =>
732
+ task.id === activeTask.id
733
+ ? ({
734
+ ...task,
735
+ _localMutationAt: (preview?.localMutationAt ?? 0) + 1,
736
+ } as Task & { _localMutationAt: number })
737
+ : task
738
+ )
739
+ );
740
+
741
+ expect(
742
+ hasTaskLocalMutationAt(
743
+ queryClient.getQueryData<Task[]>(['tasks', boardId]),
744
+ activeTask.id,
745
+ preview?.localMutationAt ?? -1
746
+ )
747
+ ).toBe(false);
748
+ });
686
749
  });
@@ -22,12 +22,14 @@ 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';
28
29
  import { calculateSortKeyWithRetry as createCalculateSortKeyWithRetry } from './kanban-sort-helpers';
29
30
  import {
30
31
  applyTaskDropPreviewToCache,
32
+ hasTaskLocalMutationAt,
31
33
  mergePersonalPlacementMutationTask,
32
34
  setBoardTaskCache,
33
35
  } from './task-drag-cache';
@@ -61,10 +63,15 @@ import type {
61
63
  TaskRect,
62
64
  VerticalRect,
63
65
  } from './task-drag-types';
66
+ import {
67
+ compareTasksByEffectiveSortKey,
68
+ getEffectiveTaskSortKey,
69
+ } from './task-sort-key';
64
70
 
65
71
  export {
66
72
  applyTaskDropPreviewToCache,
67
73
  getTaskDropPreviewCacheTasks,
74
+ hasTaskLocalMutationAt,
68
75
  mergePersonalPlacementMutationTask,
69
76
  mergeTaskIntoBoardTaskCache,
70
77
  } from './task-drag-cache';
@@ -504,6 +511,16 @@ export function useKanbanDnd({
504
511
  queryClient.setQueryData<Task[]>(queryKey, (currentTasks) => {
505
512
  if (!currentTasks) return previousCache;
506
513
 
514
+ if (
515
+ !hasTaskLocalMutationAt(
516
+ currentTasks,
517
+ task.id,
518
+ nextTask._localMutationAt
519
+ )
520
+ ) {
521
+ return currentTasks;
522
+ }
523
+
507
524
  if (!previousTaskValue) {
508
525
  return currentTasks.filter((item) => item.id !== task.id);
509
526
  }
@@ -1079,17 +1096,43 @@ export function useKanbanDnd({
1079
1096
  if (!boardId || !optimisticDropPreview) return;
1080
1097
 
1081
1098
  if (optimisticDropPreview.previousTasks) {
1082
- queryClient.setQueryData(
1083
- ['tasks', boardId],
1084
- optimisticDropPreview.previousTasks
1085
- );
1099
+ const currentTasks = queryClient.getQueryData<Task[]>([
1100
+ 'tasks',
1101
+ boardId,
1102
+ ]);
1103
+
1104
+ if (
1105
+ hasTaskLocalMutationAt(
1106
+ currentTasks,
1107
+ activeTaskForDrop.id,
1108
+ optimisticDropPreview.localMutationAt
1109
+ )
1110
+ ) {
1111
+ queryClient.setQueryData(
1112
+ ['tasks', boardId],
1113
+ optimisticDropPreview.previousTasks
1114
+ );
1115
+ }
1086
1116
  }
1087
1117
 
1088
1118
  if (optimisticDropPreview.previousFullTasks) {
1089
- queryClient.setQueryData(
1090
- ['tasks-full', boardId],
1091
- optimisticDropPreview.previousFullTasks
1092
- );
1119
+ const currentFullTasks = queryClient.getQueryData<Task[]>([
1120
+ 'tasks-full',
1121
+ boardId,
1122
+ ]);
1123
+
1124
+ if (
1125
+ hasTaskLocalMutationAt(
1126
+ currentFullTasks,
1127
+ activeTaskForDrop.id,
1128
+ optimisticDropPreview.localMutationAt
1129
+ )
1130
+ ) {
1131
+ queryClient.setQueryData(
1132
+ ['tasks-full', boardId],
1133
+ optimisticDropPreview.previousFullTasks
1134
+ );
1135
+ }
1093
1136
  }
1094
1137
  };
1095
1138
 
@@ -1124,14 +1167,14 @@ export function useKanbanDnd({
1124
1167
  const nextTask = projectedDropOrder[1];
1125
1168
  newSortKey = await calculateSortKeyWithRetry(
1126
1169
  null,
1127
- nextTask?.sort_key ?? null,
1170
+ nextTask ? getEffectiveTaskSortKey(nextTask) : null,
1128
1171
  targetListId,
1129
1172
  projectedDropOrder
1130
1173
  );
1131
1174
  } else if (newIndex === projectedDropOrder.length - 1) {
1132
1175
  const prevTask = projectedDropOrder[projectedDropOrder.length - 2];
1133
1176
  newSortKey = await calculateSortKeyWithRetry(
1134
- prevTask?.sort_key ?? null,
1177
+ prevTask ? getEffectiveTaskSortKey(prevTask) : null,
1135
1178
  null,
1136
1179
  targetListId,
1137
1180
  projectedDropOrder
@@ -1140,8 +1183,8 @@ export function useKanbanDnd({
1140
1183
  const prevTask = projectedDropOrder[newIndex - 1];
1141
1184
  const nextTask = projectedDropOrder[newIndex + 1];
1142
1185
  newSortKey = await calculateSortKeyWithRetry(
1143
- prevTask?.sort_key ?? null,
1144
- nextTask?.sort_key ?? null,
1186
+ prevTask ? getEffectiveTaskSortKey(prevTask) : null,
1187
+ nextTask ? getEffectiveTaskSortKey(nextTask) : null,
1145
1188
  targetListId,
1146
1189
  projectedDropOrder
1147
1190
  );
@@ -1157,7 +1200,8 @@ export function useKanbanDnd({
1157
1200
  const needsUpdate =
1158
1201
  dropChangesVisualOrder ||
1159
1202
  (newSortKey !== null &&
1160
- (activeTaskForDrop.sort_key ?? MAX_SAFE_INTEGER_SORT) !== newSortKey);
1203
+ (getEffectiveTaskSortKey(activeTaskForDrop) ??
1204
+ MAX_SAFE_INTEGER_SORT) !== newSortKey);
1161
1205
 
1162
1206
  let shouldPreservePendingAfterDragReset = false;
1163
1207
  const persistPersonalPlacementMove = (
@@ -1173,6 +1217,19 @@ export function useKanbanDnd({
1173
1217
  shouldPreservePendingAfterDragReset = true;
1174
1218
 
1175
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
+ })
1176
1233
  .catch((error) => {
1177
1234
  console.error('Failed to update personal task placement:', error);
1178
1235
  rollbackOptimisticDropPreview();
@@ -1193,16 +1250,9 @@ export function useKanbanDnd({
1193
1250
  .map((taskId) => baseTasks.find((t) => t.id === taskId))
1194
1251
  .filter((t): t is Task => t !== undefined);
1195
1252
 
1196
- const sortedTasksToMove = selectedTaskObjects.sort((a, b) => {
1197
- const sortA = a.sort_key ?? MAX_SAFE_INTEGER_SORT;
1198
- const sortB = b.sort_key ?? MAX_SAFE_INTEGER_SORT;
1199
- if (sortA !== sortB) return sortA - sortB;
1200
- if (!a.created_at || !b.created_at) return 0;
1201
- return (
1202
- new Date(a.created_at).getTime() -
1203
- new Date(b.created_at).getTime()
1204
- );
1205
- });
1253
+ const sortedTasksToMove = selectedTaskObjects.sort(
1254
+ compareTasksByEffectiveSortKey
1255
+ );
1206
1256
 
1207
1257
  if (
1208
1258
  targetIsExternalStaging &&
@@ -1268,7 +1318,7 @@ export function useKanbanDnd({
1268
1318
  const nextTask = simulatedTargetList[1];
1269
1319
  batchSortKey = await calculateSortKeyWithRetry(
1270
1320
  null,
1271
- nextTask?.sort_key ?? null,
1321
+ nextTask ? getEffectiveTaskSortKey(nextTask) : null,
1272
1322
  targetListId,
1273
1323
  targetListTasks
1274
1324
  );
@@ -1278,7 +1328,7 @@ export function useKanbanDnd({
1278
1328
  ) {
1279
1329
  const prevTask = simulatedTargetList[positionInSimulated - 1];
1280
1330
  batchSortKey = await calculateSortKeyWithRetry(
1281
- prevTask?.sort_key ?? null,
1331
+ prevTask ? getEffectiveTaskSortKey(prevTask) : null,
1282
1332
  null,
1283
1333
  targetListId,
1284
1334
  targetListTasks
@@ -1296,8 +1346,8 @@ export function useKanbanDnd({
1296
1346
 
1297
1347
  if (!prevIsMoving && !nextIsMoving) {
1298
1348
  batchSortKey = await calculateSortKeyWithRetry(
1299
- prevTask?.sort_key ?? null,
1300
- nextTask?.sort_key ?? null,
1349
+ prevTask ? getEffectiveTaskSortKey(prevTask) : null,
1350
+ nextTask ? getEffectiveTaskSortKey(nextTask) : null,
1301
1351
  targetListId,
1302
1352
  targetListTasks
1303
1353
  );
@@ -1311,8 +1361,10 @@ export function useKanbanDnd({
1311
1361
  }
1312
1362
  }
1313
1363
  batchSortKey = await calculateSortKeyWithRetry(
1314
- stationaryPrev?.sort_key ?? null,
1315
- nextTask?.sort_key ?? null,
1364
+ stationaryPrev
1365
+ ? getEffectiveTaskSortKey(stationaryPrev)
1366
+ : null,
1367
+ nextTask ? getEffectiveTaskSortKey(nextTask) : null,
1316
1368
  targetListId,
1317
1369
  targetListTasks
1318
1370
  );
@@ -1330,8 +1382,10 @@ export function useKanbanDnd({
1330
1382
  }
1331
1383
  }
1332
1384
  batchSortKey = await calculateSortKeyWithRetry(
1333
- prevTask?.sort_key ?? null,
1334
- stationaryNext?.sort_key ?? null,
1385
+ prevTask ? getEffectiveTaskSortKey(prevTask) : null,
1386
+ stationaryNext
1387
+ ? getEffectiveTaskSortKey(stationaryNext)
1388
+ : null,
1335
1389
  targetListId,
1336
1390
  targetListTasks
1337
1391
  );
@@ -1360,8 +1414,8 @@ export function useKanbanDnd({
1360
1414
  }
1361
1415
 
1362
1416
  batchSortKey = await calculateSortKeyWithRetry(
1363
- boundaryPrev?.sort_key ?? null,
1364
- boundaryNext?.sort_key ?? null,
1417
+ boundaryPrev ? getEffectiveTaskSortKey(boundaryPrev) : null,
1418
+ boundaryNext ? getEffectiveTaskSortKey(boundaryNext) : null,
1365
1419
  targetListId,
1366
1420
  targetListTasks
1367
1421
  );
@@ -1392,6 +1446,7 @@ export function useKanbanDnd({
1392
1446
  closed_at: updatedTask.closed_at,
1393
1447
  },
1394
1448
  });
1449
+ void invalidateKanbanDeadlineTasks(queryClient, boardId);
1395
1450
  },
1396
1451
  onSettled: () => {
1397
1452
  clearPendingTaskIds(pendingTaskIds);
@@ -1437,14 +1492,45 @@ export function useKanbanDnd({
1437
1492
  closed_at: updatedTask.closed_at,
1438
1493
  },
1439
1494
  });
1495
+ void invalidateKanbanDeadlineTasks(queryClient, boardId);
1440
1496
  };
1441
1497
 
1442
1498
  if (repairedTaskSortKeys.length > 0) {
1443
1499
  void (async () => {
1444
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
+
1445
1510
  const results = await Promise.allSettled(
1446
- repairedTaskSortKeys.map((repair) =>
1447
- 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(
1448
1534
  {
1449
1535
  taskId: repair.taskId,
1450
1536
  newListId: repair.listId,
@@ -1457,14 +1543,17 @@ export function useKanbanDnd({
1457
1543
  {
1458
1544
  onSuccess: handleReorderSuccess,
1459
1545
  }
1460
- )
1461
- )
1546
+ );
1547
+ })
1462
1548
  );
1463
1549
  const failedResults = results.filter(
1464
1550
  (result) => result.status === 'rejected'
1465
1551
  );
1466
1552
 
1467
- if (failedResults.length === 0) return;
1553
+ if (failedResults.length === 0) {
1554
+ void invalidateKanbanDeadlineTasks(queryClient, boardId);
1555
+ return;
1556
+ }
1468
1557
 
1469
1558
  console.error(
1470
1559
  'Failed to persist repaired task sort keys:',