@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
@@ -15,6 +15,7 @@ import {
15
15
  } from '@tuturuuu/utils/task-list-status';
16
16
  import { addDays } from 'date-fns';
17
17
  import { useCallback } from 'react';
18
+ import { invalidateKanbanDeadlineTasks } from '../components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query';
18
19
  import { useBoardBroadcast } from '../components/ui/tu-do/shared/board-broadcast-context';
19
20
  import {
20
21
  dispatchTaskSoundCue,
@@ -81,6 +82,9 @@ export function useTaskActions({
81
82
  }: UseTaskActionsProps) {
82
83
  const queryClient = useQueryClient();
83
84
  const broadcast = useBoardBroadcast();
85
+ const invalidateDeadlineTasks = useCallback(() => {
86
+ void invalidateKanbanDeadlineTasks(queryClient, boardId);
87
+ }, [boardId, queryClient]);
84
88
 
85
89
  const resolveWorkspaceIdForTask = useCallback(
86
90
  async (taskRecord?: Task) => {
@@ -272,6 +276,7 @@ export function useTaskActions({
272
276
  description: `Task marked as ${targetCompletionList.status === 'done' ? 'done' : 'closed'} and moved to ${targetCompletionList.name}`,
273
277
  });
274
278
  dispatchTaskActionSound('complete');
279
+ invalidateDeadlineTasks();
275
280
  } catch (error) {
276
281
  console.error('Failed to complete external task:', error);
277
282
  toast.error('Error', {
@@ -337,6 +342,7 @@ export function useTaskActions({
337
342
  closed_at: movedTask?.closed_at,
338
343
  },
339
344
  });
345
+ invalidateDeadlineTasks();
340
346
 
341
347
  toast.success('Task completed', {
342
348
  description: `Task marked as done and moved to ${targetCompletionList.name}`,
@@ -395,6 +401,7 @@ export function useTaskActions({
395
401
  },
396
402
  });
397
403
  dispatchTaskActionSound(newClosedState ? 'complete' : 'update');
404
+ invalidateDeadlineTasks();
398
405
  } catch (error) {
399
406
  // Rollback on error
400
407
  if (previousTasks) {
@@ -422,6 +429,7 @@ export function useTaskActions({
422
429
  getWorkspaceId,
423
430
  markLocallyMutatedTask,
424
431
  mergeLocallyMutatedTask,
432
+ invalidateDeadlineTasks,
425
433
  ]);
426
434
 
427
435
  const handleMoveToCompletion = useCallback(async () => {
@@ -452,6 +460,7 @@ export function useTaskActions({
452
460
  description: `Task marked as ${targetCompletionList.status === 'done' ? 'done' : 'closed'} and moved to ${targetCompletionList.name}`,
453
461
  });
454
462
  dispatchTaskActionSound('complete');
463
+ invalidateDeadlineTasks();
455
464
  return;
456
465
  }
457
466
 
@@ -514,6 +523,7 @@ export function useTaskActions({
514
523
  }
515
524
  }
516
525
  if (successCount === 0) throw new Error('Failed to move any tasks');
526
+ invalidateDeadlineTasks();
517
527
 
518
528
  if (failedTaskIds.length > 0) {
519
529
  toast.warning('Partial completion update', {
@@ -561,6 +571,7 @@ export function useTaskActions({
561
571
  getWorkspaceId,
562
572
  markLocallyMutatedTask,
563
573
  rollbackTaskIds,
574
+ invalidateDeadlineTasks,
564
575
  ]);
565
576
 
566
577
  const handleMoveToClose = useCallback(async () => {
@@ -590,6 +601,7 @@ export function useTaskActions({
590
601
  description: 'Task marked as closed',
591
602
  });
592
603
  dispatchTaskActionSound('complete');
604
+ invalidateDeadlineTasks();
593
605
  return;
594
606
  }
595
607
 
@@ -648,6 +660,7 @@ export function useTaskActions({
648
660
  }
649
661
  }
650
662
  if (successCount === 0) throw new Error('Failed to move any tasks');
663
+ invalidateDeadlineTasks();
651
664
 
652
665
  if (failedTaskIds.length > 0) {
653
666
  toast.warning('Partial close update', {
@@ -692,6 +705,7 @@ export function useTaskActions({
692
705
  getWorkspaceId,
693
706
  markLocallyMutatedTask,
694
707
  rollbackTaskIds,
708
+ invalidateDeadlineTasks,
695
709
  ]);
696
710
 
697
711
  const handleDelete = useCallback(async () => {
@@ -764,6 +778,7 @@ export function useTaskActions({
764
778
  }
765
779
 
766
780
  if (successCount === 0) throw new Error('Failed to delete any tasks');
781
+ invalidateDeadlineTasks();
767
782
 
768
783
  if (failedTaskIds.length > 0) {
769
784
  toast.warning('Partial delete update', {
@@ -812,6 +827,7 @@ export function useTaskActions({
812
827
  broadcast,
813
828
  getWorkspaceId,
814
829
  restoreDeletedTaskIds,
830
+ invalidateDeadlineTasks,
815
831
  ]);
816
832
 
817
833
  const handleRemoveAllAssignees = useCallback(async () => {
@@ -1010,6 +1026,7 @@ export function useTaskActions({
1010
1026
  description: `Task moved to ${targetList.name || 'selected list'}`,
1011
1027
  });
1012
1028
  dispatchTaskActionSound(getMoveSoundCue(targetList));
1029
+ invalidateDeadlineTasks();
1013
1030
  } catch (error) {
1014
1031
  console.error('Failed to move external task:', error);
1015
1032
  toast.error('Error', {
@@ -1115,6 +1132,7 @@ export function useTaskActions({
1115
1132
  }
1116
1133
  }
1117
1134
  if (successCount === 0) throw new Error('Failed to move any tasks');
1135
+ invalidateDeadlineTasks();
1118
1136
 
1119
1137
  if (failedTaskIds.length > 0) {
1120
1138
  toast.warning('Partial move update', {
@@ -1161,6 +1179,7 @@ export function useTaskActions({
1161
1179
  markLocallyMutatedTask,
1162
1180
  resolveWorkspaceIdForTask,
1163
1181
  rollbackTaskIds,
1182
+ invalidateDeadlineTasks,
1164
1183
  ]
1165
1184
  );
1166
1185
 
@@ -1243,6 +1262,7 @@ export function useTaskActions({
1243
1262
  task: { id: tid, end_date: newDate },
1244
1263
  });
1245
1264
  }
1265
+ invalidateDeadlineTasks();
1246
1266
 
1247
1267
  if (failedTaskIds.length > 0) {
1248
1268
  toast.warning('Partial due date update', {
@@ -1283,6 +1303,7 @@ export function useTaskActions({
1283
1303
  boardId,
1284
1304
  task,
1285
1305
  broadcast,
1306
+ invalidateDeadlineTasks,
1286
1307
  ]
1287
1308
  );
1288
1309
 
@@ -1575,6 +1596,7 @@ export function useTaskActions({
1575
1596
  // Use the centralized bulk update function from useBulkOperations
1576
1597
  try {
1577
1598
  await bulkUpdateCustomDueDate(date || null);
1599
+ invalidateDeadlineTasks();
1578
1600
  } catch (error) {
1579
1601
  console.error('Bulk custom date update failed', error);
1580
1602
  toast.error('Failed to update due date for selected tasks');
@@ -1610,6 +1632,7 @@ export function useTaskActions({
1610
1632
  broadcast?.('task:upsert', {
1611
1633
  task: { id: task.id, end_date: newDate },
1612
1634
  });
1635
+ invalidateDeadlineTasks();
1613
1636
 
1614
1637
  toast.success('Due date updated', {
1615
1638
  description: newDate
@@ -1639,6 +1662,7 @@ export function useTaskActions({
1639
1662
  queryClient,
1640
1663
  boardId,
1641
1664
  broadcast,
1665
+ invalidateDeadlineTasks,
1642
1666
  ]
1643
1667
  );
1644
1668
 
@@ -0,0 +1,75 @@
1
+ 'use client';
2
+
3
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
+ import {
5
+ getUserWorkspaceConfig,
6
+ updateUserWorkspaceConfig,
7
+ } from '@tuturuuu/internal-api/users';
8
+
9
+ export function getUserWorkspaceConfigQueryKey(
10
+ workspaceId: string,
11
+ configId: string
12
+ ) {
13
+ return ['user-workspace-config', workspaceId, configId] as const;
14
+ }
15
+
16
+ export function useUserWorkspaceConfig(
17
+ workspaceId: string,
18
+ configId: string,
19
+ defaultValue: string | null = null,
20
+ options?: {
21
+ enabled?: boolean;
22
+ staleTime?: number;
23
+ }
24
+ ) {
25
+ return useQuery({
26
+ queryKey: getUserWorkspaceConfigQueryKey(workspaceId, configId),
27
+ queryFn: async () => {
28
+ const data = await getUserWorkspaceConfig(workspaceId, configId);
29
+ return data.value ?? defaultValue;
30
+ },
31
+ enabled: options?.enabled !== false && Boolean(workspaceId && configId),
32
+ staleTime: options?.staleTime ?? 5 * 60 * 1000,
33
+ });
34
+ }
35
+
36
+ export function useUpdateUserWorkspaceConfig() {
37
+ const queryClient = useQueryClient();
38
+
39
+ return useMutation({
40
+ mutationFn: async ({
41
+ configId,
42
+ value,
43
+ workspaceId,
44
+ }: {
45
+ configId: string;
46
+ value: string | null;
47
+ workspaceId: string;
48
+ }) => updateUserWorkspaceConfig(workspaceId, configId, value),
49
+ onMutate: async ({ configId, value, workspaceId }) => {
50
+ const queryKey = getUserWorkspaceConfigQueryKey(workspaceId, configId);
51
+ await queryClient.cancelQueries({ queryKey });
52
+
53
+ const previousValue = queryClient.getQueryData<string | null>(queryKey);
54
+ queryClient.setQueryData(queryKey, value);
55
+
56
+ return { configId, previousValue, workspaceId };
57
+ },
58
+ onError: (_error, _variables, context) => {
59
+ if (!context) return;
60
+
61
+ queryClient.setQueryData(
62
+ getUserWorkspaceConfigQueryKey(context.workspaceId, context.configId),
63
+ context.previousValue ?? null
64
+ );
65
+ },
66
+ onSettled: (_data, _error, variables) => {
67
+ queryClient.invalidateQueries({
68
+ queryKey: getUserWorkspaceConfigQueryKey(
69
+ variables.workspaceId,
70
+ variables.configId
71
+ ),
72
+ });
73
+ },
74
+ });
75
+ }
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  getCurrencyLocale,
3
+ resolveSupportedCurrency,
3
4
  type SupportedCurrency,
4
5
  } from '@tuturuuu/utils/currencies';
5
6
 
@@ -8,14 +9,18 @@ import { useWorkspaceConfig } from './use-workspace-config';
8
9
  // Re-export for convenience
9
10
  export type { SupportedCurrency } from '@tuturuuu/utils/currencies';
10
11
 
11
- export const useWorkspaceCurrency = (wsId: string) => {
12
+ export const useWorkspaceCurrency = (
13
+ wsId: string,
14
+ fallbackCurrency = 'USD'
15
+ ) => {
16
+ const fallback = resolveSupportedCurrency(fallbackCurrency);
12
17
  const { data, isLoading, error } = useWorkspaceConfig<SupportedCurrency>(
13
18
  wsId,
14
19
  'DEFAULT_CURRENCY',
15
- 'USD'
20
+ fallback
16
21
  );
17
22
 
18
- const currency = (data as SupportedCurrency) ?? 'USD';
23
+ const currency = resolveSupportedCurrency(data, fallback);
19
24
 
20
25
  return {
21
26
  currency,
@@ -10,8 +10,10 @@ import { toast } from './use-toast';
10
10
  import {
11
11
  type BoardRealtimePayload,
12
12
  createRealtimeClientId,
13
+ getBoardRealtimeChannelName,
13
14
  isBoardRealtimeEnvelope,
14
15
  LOCAL_BROADCAST_CHANNEL_PREFIX,
16
+ PRIVATE_TASK_REALTIME_CHANNEL_CONFIG,
15
17
  type RealtimeChannel,
16
18
  SEEN_REALTIME_EVENT_LIMIT,
17
19
  } from './useBoardRealtime.types';
@@ -205,9 +207,10 @@ export function useBoardRealtime(
205
207
  channelRef.current = null;
206
208
  }
207
209
 
208
- const channel = supabase.channel(`board-realtime-${boardId}`, {
209
- config: { broadcast: { self: false } },
210
- });
210
+ const channel = supabase.channel(
211
+ getBoardRealtimeChannelName(boardId),
212
+ PRIVATE_TASK_REALTIME_CHANNEL_CONFIG
213
+ );
211
214
  channelRef.current = channel;
212
215
 
213
216
  channel
@@ -14,8 +14,19 @@ type BoardRealtimeEnvelope = {
14
14
  payload: BoardRealtimePayload;
15
15
  };
16
16
 
17
+ export const BOARD_REALTIME_CHANNEL_PREFIX = 'board-realtime';
17
18
  export const LOCAL_BROADCAST_CHANNEL_PREFIX = 'tuturuuu:board-realtime';
18
19
  export const SEEN_REALTIME_EVENT_LIMIT = 500;
20
+ export const PRIVATE_TASK_REALTIME_CHANNEL_CONFIG = {
21
+ config: {
22
+ broadcast: { self: false },
23
+ private: true,
24
+ },
25
+ } as const;
26
+
27
+ export function getBoardRealtimeChannelName(boardId: string) {
28
+ return `${BOARD_REALTIME_CHANNEL_PREFIX}-${boardId}`;
29
+ }
19
30
 
20
31
  const isRecord = (value: unknown): value is Record<string, unknown> =>
21
32
  typeof value === 'object' && value !== null;
@@ -188,6 +188,16 @@ function invalidateTaskMembershipQueries(
188
188
  void queryClient.invalidateQueries({
189
189
  queryKey: ['task-list-counts', boardId],
190
190
  });
191
+ void queryClient.invalidateQueries({
192
+ predicate: (query) => {
193
+ const queryKey = query.queryKey;
194
+ return (
195
+ Array.isArray(queryKey) &&
196
+ queryKey[0] === 'kanban-deadline-tasks' &&
197
+ queryKey[2] === boardId
198
+ );
199
+ },
200
+ });
191
201
  void queryClient.invalidateQueries({ queryKey: ['my-tasks'] });
192
202
  void queryClient.invalidateQueries({ queryKey: ['my-completed-tasks'] });
193
203
  }
@@ -342,6 +352,7 @@ export function useBoardRealtimeEventHandler({
342
352
  patchMyTasksCaches(queryClient, taskData);
343
353
  if (
344
354
  'list_id' in taskData ||
355
+ 'end_date' in taskData ||
345
356
  'completed' in taskData ||
346
357
  'completed_at' in taskData ||
347
358
  'closed_at' in taskData ||
@@ -5,15 +5,79 @@ import { DEV_MODE } from '@tuturuuu/utils/constants';
5
5
  import type { RefObject } from 'react';
6
6
  import { useCallback, useEffect, useRef, useState } from 'react';
7
7
  import { usePageVisibility } from './use-page-visibility';
8
+ import { PRIVATE_TASK_REALTIME_CHANNEL_CONFIG } from './useBoardRealtime.types';
9
+
10
+ type CursorUser = Pick<User, 'avatar_url' | 'display_name' | 'id'> & {
11
+ id: string;
12
+ };
8
13
 
9
14
  export interface CursorPosition {
10
15
  x: number;
11
16
  y: number;
12
- user?: User;
17
+ user?: CursorUser;
13
18
  metadata?: { [key: string]: any };
14
19
  lastUpdatedAt: number;
15
20
  }
16
21
 
22
+ type ParsedCursorPosition = CursorPosition & { user: CursorUser };
23
+
24
+ function isRecord(value: unknown): value is Record<string, unknown> {
25
+ return typeof value === 'object' && value !== null;
26
+ }
27
+
28
+ function sanitizeCursorUser(user: User | undefined): CursorUser | null {
29
+ if (!user?.id) return null;
30
+
31
+ return {
32
+ avatar_url:
33
+ typeof user.avatar_url === 'string' && user.avatar_url.length > 0
34
+ ? user.avatar_url
35
+ : null,
36
+ display_name:
37
+ typeof user.display_name === 'string' && user.display_name.length > 0
38
+ ? user.display_name
39
+ : null,
40
+ id: user.id,
41
+ };
42
+ }
43
+
44
+ function sanitizeCursorMetadata(metadata: { [key: string]: any } | undefined) {
45
+ if (metadata == null || !isRecord(metadata) || Array.isArray(metadata)) {
46
+ return undefined;
47
+ }
48
+
49
+ return metadata;
50
+ }
51
+
52
+ function parseCursorMovePayload(payload: unknown): ParsedCursorPosition | null {
53
+ if (!isRecord(payload)) return null;
54
+
55
+ const { metadata, user, x, y } = payload;
56
+ if (typeof x !== 'number' || !Number.isFinite(x)) return null;
57
+ if (typeof y !== 'number' || !Number.isFinite(y)) return null;
58
+ if (!isRecord(user) || typeof user.id !== 'string' || user.id.length === 0) {
59
+ return null;
60
+ }
61
+
62
+ return {
63
+ lastUpdatedAt: Date.now(),
64
+ metadata: sanitizeCursorMetadata(metadata as { [key: string]: any }),
65
+ user: {
66
+ avatar_url:
67
+ typeof user.avatar_url === 'string' && user.avatar_url.length > 0
68
+ ? user.avatar_url
69
+ : null,
70
+ display_name:
71
+ typeof user.display_name === 'string' && user.display_name.length > 0
72
+ ? user.display_name
73
+ : null,
74
+ id: user.id,
75
+ },
76
+ x,
77
+ y,
78
+ };
79
+ }
80
+
17
81
  export function useCursorTracking(
18
82
  channelName: string,
19
83
  containerRef?: RefObject<HTMLElement | null>,
@@ -59,6 +123,8 @@ export function useCursorTracking(
59
123
  metadata?: { [key: string]: any }
60
124
  ) => {
61
125
  if (!channelRef.current) return;
126
+ const cursorUser = sanitizeCursorUser(user);
127
+ if (cursorUser == null) return;
62
128
  // Check error count instead of state to avoid dependency
63
129
  if (errorCountRef.current >= MAX_ERROR_COUNT) return;
64
130
 
@@ -73,7 +139,12 @@ export function useCursorTracking(
73
139
  await channelRef.current?.send({
74
140
  type: 'broadcast',
75
141
  event: 'cursor-move',
76
- payload: { x, y, user, metadata },
142
+ payload: {
143
+ metadata: sanitizeCursorMetadata(metadata),
144
+ user: cursorUser,
145
+ x,
146
+ y,
147
+ },
77
148
  });
78
149
 
79
150
  lastBroadcastTimeRef.current = Date.now();
@@ -90,7 +161,12 @@ export function useCursorTracking(
90
161
  await channelRef.current.send({
91
162
  type: 'broadcast',
92
163
  event: 'cursor-move',
93
- payload: { x, y, user, metadata },
164
+ payload: {
165
+ metadata: sanitizeCursorMetadata(metadata),
166
+ user: cursorUser,
167
+ x,
168
+ y,
169
+ },
94
170
  });
95
171
 
96
172
  lastBroadcastTimeRef.current = now;
@@ -207,38 +283,25 @@ export function useCursorTracking(
207
283
  channelRef.current = null;
208
284
  }
209
285
 
210
- const channel = supabase.channel(channelName, {
211
- config: {
212
- broadcast: {
213
- self: false, // Don't receive own broadcasts
214
- },
215
- },
216
- });
286
+ const channel = supabase.channel(
287
+ channelName,
288
+ PRIVATE_TASK_REALTIME_CHANNEL_CONFIG
289
+ );
217
290
 
218
291
  channelRef.current = channel;
219
292
  // Listen for cursor movements from other users
220
293
  channel
221
294
  .on('broadcast', { event: 'cursor-move' }, (payload) => {
222
295
  try {
223
- const {
224
- x,
225
- y,
226
- user: broadcastUser,
227
- metadata: broadcastMetadata,
228
- } = payload.payload;
296
+ const cursor = parseCursorMovePayload(payload.payload);
297
+ if (cursor == null) return;
229
298
 
230
299
  // Ignore own broadcasts (extra safety)
231
- if (broadcastUser.id === user?.id) return;
300
+ if (cursor.user?.id === user?.id) return;
232
301
 
233
302
  setCursors((prev) => {
234
303
  const updated = new Map(prev);
235
- updated.set(broadcastUser.id || '', {
236
- x,
237
- y,
238
- user: broadcastUser,
239
- metadata: broadcastMetadata,
240
- lastUpdatedAt: Date.now(),
241
- });
304
+ updated.set(cursor.user.id, cursor);
242
305
  return updated;
243
306
  });
244
307
  } catch (err) {
@@ -289,16 +352,17 @@ export function useCursorTracking(
289
352
 
290
353
  // Broadcast cursor removal before unsubscribing (best effort)
291
354
  // This helps other users see the cursor disappear immediately
292
- if (channelRef.current && user?.id) {
355
+ const cursorUser = sanitizeCursorUser(user);
356
+ if (channelRef.current && cursorUser != null) {
293
357
  try {
294
358
  channelRef.current.send({
295
359
  type: 'broadcast',
296
360
  event: 'cursor-move',
297
361
  payload: {
362
+ metadata: sanitizeCursorMetadata(metadata),
363
+ user: cursorUser,
298
364
  x: -1000,
299
365
  y: -1000,
300
- user,
301
- metadata,
302
366
  },
303
367
  });
304
368
  } catch (err) {
@@ -8,6 +8,7 @@ import { useCallback, useEffect, useRef } from 'react';
8
8
  import {
9
9
  type BoardRealtimePayload,
10
10
  createRealtimeClientId,
11
+ PRIVATE_TASK_REALTIME_CHANNEL_CONFIG,
11
12
  type RealtimeChannel,
12
13
  SEEN_REALTIME_EVENT_LIMIT,
13
14
  } from './useBoardRealtime.types';
@@ -255,9 +256,10 @@ export function useTaskUserRealtime(userId: string | null | undefined) {
255
256
  return;
256
257
  }
257
258
 
258
- const channel = supabase.channel(getTaskUserRealtimeChannelName(userId), {
259
- config: { broadcast: { self: false } },
260
- });
259
+ const channel = supabase.channel(
260
+ getTaskUserRealtimeChannelName(userId),
261
+ PRIVATE_TASK_REALTIME_CHANNEL_CONFIG
262
+ );
261
263
  channelRef.current = channel;
262
264
 
263
265
  channel