@tuturuuu/ui 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/biome.json +1 -1
  3. package/package.json +74 -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-search.test.ts +78 -0
  29. package/src/components/ui/custom/__tests__/settings-dialog-shell-compile-graph.test.ts +76 -0
  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 +46 -1
  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/nav-link.test.tsx +165 -0
  40. package/src/components/ui/custom/nav-link.tsx +69 -11
  41. package/src/components/ui/custom/navigation.tsx +1 -0
  42. package/src/components/ui/custom/settings/task-settings.tsx +104 -0
  43. package/src/components/ui/custom/settings-dialog-search-loader.d.ts +5 -0
  44. package/src/components/ui/custom/settings-dialog-search-loader.js +3 -0
  45. package/src/components/ui/custom/settings-dialog-search.ts +75 -0
  46. package/src/components/ui/custom/settings-dialog-shell.tsx +65 -28
  47. package/src/components/ui/custom/theme-toggle.tsx +1 -1
  48. package/src/components/ui/custom/workspace-select-helpers.ts +23 -0
  49. package/src/components/ui/custom/workspace-select.tsx +25 -19
  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 +1 -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-edit-dialog.tsx +1 -4
  69. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
  70. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
  71. package/src/components/ui/finance/wallets/form.test.tsx +51 -3
  72. package/src/components/ui/finance/wallets/form.tsx +15 -4
  73. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  74. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
  75. package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
  76. package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
  77. package/src/components/ui/input-otp.tsx +1 -1
  78. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
  79. package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
  80. package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
  81. package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
  82. package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
  83. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
  84. package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
  85. package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
  86. package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
  87. package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
  88. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
  89. package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
  90. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
  91. package/src/components/ui/navigation-menu.tsx +1 -1
  92. package/src/components/ui/pagination.tsx +1 -1
  93. package/src/components/ui/radio-group.tsx +1 -1
  94. package/src/components/ui/select.tsx +5 -1
  95. package/src/components/ui/sheet.tsx +1 -1
  96. package/src/components/ui/sidebar.tsx +1 -1
  97. package/src/components/ui/storefront/cart-popover.tsx +61 -0
  98. package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
  99. package/src/components/ui/storefront/cart-summary.tsx +93 -154
  100. package/src/components/ui/storefront/checkout-overlay.tsx +4 -5
  101. package/src/components/ui/storefront/listing-card.tsx +1 -1
  102. package/src/components/ui/storefront/merch-sections.tsx +70 -0
  103. package/src/components/ui/storefront/product-detail.tsx +1 -1
  104. package/src/components/ui/storefront/storefront-surface.test.tsx +106 -11
  105. package/src/components/ui/storefront/storefront-surface.tsx +101 -166
  106. package/src/components/ui/storefront/types.ts +4 -0
  107. package/src/components/ui/storefront/utils.ts +6 -0
  108. package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
  109. package/src/components/ui/text-editor/background-color-extension.ts +62 -0
  110. package/src/components/ui/text-editor/color-controls.tsx +284 -0
  111. package/src/components/ui/text-editor/editor.tsx +69 -14
  112. package/src/components/ui/text-editor/extensions.ts +8 -2
  113. package/src/components/ui/text-editor/highlight-extension.ts +22 -0
  114. package/src/components/ui/text-editor/tool-bar.tsx +9 -16
  115. package/src/components/ui/toast.tsx +1 -1
  116. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +286 -0
  117. package/src/components/ui/tu-do/boards/__tests__/task-board-form.test.tsx +12 -0
  118. package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
  119. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +15 -226
  120. package/src/components/ui/tu-do/boards/board-share-settings-panel.tsx +351 -0
  121. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +121 -39
  122. package/src/components/ui/tu-do/boards/boardId/enhanced-task-list.tsx +7 -0
  123. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
  124. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
  125. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
  126. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-types.ts +3 -3
  127. package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
  128. package/src/components/ui/tu-do/boards/boardId/kanban/data/use-bulk-resources.ts +59 -5
  129. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
  130. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
  131. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/drag-preview.tsx +20 -1
  132. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +38 -9
  133. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
  134. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
  135. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +81 -30
  136. package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
  137. package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
  138. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
  139. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
  140. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
  141. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
  142. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
  143. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
  144. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
  145. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
  146. package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
  147. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +642 -5
  148. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +224 -15
  149. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +535 -53
  150. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +101 -33
  151. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +235 -113
  152. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +50 -5
  153. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +12 -2
  154. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +10 -1
  155. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
  156. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +20 -0
  157. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +10 -0
  158. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +271 -36
  159. package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
  160. package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
  161. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +22 -0
  162. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +9 -0
  163. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +9 -0
  164. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-toolbar.tsx +9 -0
  165. package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +35 -3
  166. package/src/components/ui/tu-do/boards/form.tsx +1 -1
  167. package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
  168. package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
  169. package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
  170. package/src/components/ui/tu-do/hooks/__tests__/useTaskLabelManagement.test.tsx +48 -0
  171. package/src/components/ui/tu-do/hooks/__tests__/useTaskProjectManagement.test.tsx +144 -0
  172. package/src/components/ui/tu-do/hooks/useTaskDialog.ts +7 -0
  173. package/src/components/ui/tu-do/hooks/useTaskLabelManagement.ts +115 -106
  174. package/src/components/ui/tu-do/hooks/useTaskProjectManagement.ts +115 -122
  175. package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
  176. package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
  177. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
  178. package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
  179. package/src/components/ui/tu-do/progress/task-progress-import-panel.tsx +60 -0
  180. package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +156 -0
  181. package/src/components/ui/tu-do/progress/task-progress-page.tsx +348 -0
  182. package/src/components/ui/tu-do/progress/task-progress-panels.tsx +301 -0
  183. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +26 -0
  184. package/src/components/ui/tu-do/shared/__tests__/assignee-select.test.tsx +81 -10
  185. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +141 -1
  186. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +377 -36
  187. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +374 -0
  188. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +419 -5
  189. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +38 -0
  190. package/src/components/ui/tu-do/shared/__tests__/task-cache-patches.test.ts +147 -0
  191. package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
  192. package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +3 -0
  193. package/src/components/ui/tu-do/shared/assignee-select.tsx +77 -26
  194. package/src/components/ui/tu-do/shared/board-client.tsx +15 -10
  195. package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
  196. package/src/components/ui/tu-do/shared/board-header.tsx +471 -975
  197. package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
  198. package/src/components/ui/tu-do/shared/board-switcher.tsx +244 -220
  199. package/src/components/ui/tu-do/shared/board-user-presence-avatars.tsx +18 -12
  200. package/src/components/ui/tu-do/shared/board-views.tsx +577 -85
  201. package/src/components/ui/tu-do/shared/list-view.tsx +246 -2
  202. package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
  203. package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
  204. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
  205. package/src/components/ui/tu-do/shared/task-cache-patches.ts +394 -0
  206. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +21 -1
  207. package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +5 -1
  208. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +25 -2
  209. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +7 -1
  210. package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
  211. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-data.ts +79 -10
  212. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +76 -77
  213. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.test.tsx +63 -0
  214. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.ts +78 -69
  215. package/src/components/ui/tu-do/shared/task-edit-dialog/personal-overrides-section.tsx +28 -8
  216. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/dependencies-section.tsx +14 -3
  217. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/parent-section.tsx +6 -1
  218. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/related-section.tsx +6 -1
  219. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/subtasks-section.tsx +6 -1
  220. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/types/task-relationships.types.ts +8 -0
  221. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
  222. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
  223. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
  224. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +8 -1
  225. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.test.tsx +150 -0
  226. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +61 -35
  227. package/src/components/ui/tu-do/shared/task-edit-dialog/task-relationships-properties.tsx +44 -2
  228. package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
  229. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +11 -1
  230. package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
  231. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +11 -0
  232. package/src/components/ui/tu-do/shared/use-progressive-board-loader.ts +2 -0
  233. package/src/declarations.d.ts +1 -0
  234. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
  235. package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
  236. package/src/hooks/__tests__/useBoardPresence.test.tsx +191 -0
  237. package/src/hooks/__tests__/useBoardRealtime.test.tsx +24 -144
  238. package/src/hooks/use-calendar-sync.tsx +247 -243
  239. package/src/hooks/use-calendar.tsx +323 -138
  240. package/src/hooks/use-task-actions.ts +24 -0
  241. package/src/hooks/use-user-workspace-config.ts +75 -0
  242. package/src/hooks/use-workspace-currency.ts +8 -3
  243. package/src/hooks/useBoardPresence.ts +364 -0
  244. package/src/hooks/useBoardRealtimeEventHandler.ts +45 -90
  245. package/src/lib/workspace-actions.ts +2 -6
@@ -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,
@@ -0,0 +1,364 @@
1
+ 'use client';
2
+
3
+ import { getCurrentUserProfile } from '@tuturuuu/internal-api/users';
4
+ import { createClient } from '@tuturuuu/supabase/next/client';
5
+ import type { RealtimePresenceState } from '@tuturuuu/supabase/next/realtime';
6
+ import type { User } from '@tuturuuu/types/primitives/User';
7
+ import { DEV_MODE } from '@tuturuuu/utils/constants';
8
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
9
+ import type {
10
+ PresenceLocation,
11
+ WorkspacePresenceState,
12
+ } from './use-workspace-presence';
13
+ import { getBoardRealtimeChannelName } from './useBoardRealtime.types';
14
+
15
+ type BoardPresenceChannel = ReturnType<
16
+ ReturnType<typeof createClient>['channel']
17
+ >;
18
+
19
+ export type BoardPresenceState = WorkspacePresenceState;
20
+
21
+ export interface UseBoardPresenceConfig {
22
+ enabled?: boolean;
23
+ }
24
+
25
+ export interface UseBoardPresenceResult {
26
+ presenceState: RealtimePresenceState<BoardPresenceState>;
27
+ currentUserId?: string;
28
+ updateLocation: (
29
+ location: PresenceLocation,
30
+ metadata?: Record<string, any>
31
+ ) => void;
32
+ updateMetadata: (metadata: Record<string, any>) => void;
33
+ getBoardViewers: (boardId: string) => BoardPresenceState[];
34
+ getTaskViewers: (taskId: string) => BoardPresenceState[];
35
+ }
36
+
37
+ const SESSION_STORAGE_SESSION_KEY = 'tuturuuu:board-presence:session-id';
38
+ let fallbackBoardPresenceCounter = 0;
39
+
40
+ function createPresenceSessionId(): string {
41
+ if (
42
+ typeof crypto !== 'undefined' &&
43
+ typeof crypto.randomUUID === 'function'
44
+ ) {
45
+ return crypto.randomUUID();
46
+ }
47
+
48
+ fallbackBoardPresenceCounter += 1;
49
+ return `fallback-${Date.now().toString(36)}-${fallbackBoardPresenceCounter}`;
50
+ }
51
+
52
+ function getOrCreatePresenceSessionId(): string {
53
+ if (typeof window === 'undefined' || typeof sessionStorage === 'undefined') {
54
+ return createPresenceSessionId();
55
+ }
56
+
57
+ try {
58
+ const existing = sessionStorage.getItem(SESSION_STORAGE_SESSION_KEY);
59
+ if (existing) return existing;
60
+
61
+ const next = createPresenceSessionId();
62
+ sessionStorage.setItem(SESSION_STORAGE_SESSION_KEY, next);
63
+ return next;
64
+ } catch {
65
+ return createPresenceSessionId();
66
+ }
67
+ }
68
+
69
+ function buildTrackSignature(
70
+ payload: Omit<BoardPresenceState, 'online_at'>
71
+ ): string {
72
+ return JSON.stringify(payload);
73
+ }
74
+
75
+ export function useBoardPresence(
76
+ boardId: string,
77
+ { enabled = true }: UseBoardPresenceConfig = {}
78
+ ): UseBoardPresenceResult {
79
+ const [presenceState, setPresenceState] = useState<
80
+ RealtimePresenceState<BoardPresenceState>
81
+ >({});
82
+ const [currentUserId, setCurrentUserId] = useState<string>();
83
+
84
+ const channelRef = useRef<BoardPresenceChannel | null>(null);
85
+ const isCleanedUpRef = useRef(false);
86
+ const setupPromiseRef = useRef<Promise<boolean> | null>(null);
87
+ const locationRef = useRef<PresenceLocation>({ type: 'other' });
88
+ const metadataRef = useRef<Record<string, any> | undefined>(undefined);
89
+ const awayRef = useRef(false);
90
+ const userDataRef = useRef<User | null>(null);
91
+ const presenceSessionIdRef = useRef<string>(getOrCreatePresenceSessionId());
92
+ const lastTrackSignatureRef = useRef<string | null>(null);
93
+
94
+ const channelName =
95
+ enabled && boardId ? getBoardRealtimeChannelName(boardId) : '';
96
+
97
+ const ensureChannel = useCallback(async (): Promise<boolean> => {
98
+ if (channelRef.current) return true;
99
+ if (!channelName || isCleanedUpRef.current) return false;
100
+ if (setupPromiseRef.current) return setupPromiseRef.current;
101
+
102
+ const promise = (async () => {
103
+ const supabase = createClient();
104
+
105
+ try {
106
+ const {
107
+ data: { user },
108
+ } = await supabase.auth.getUser();
109
+
110
+ if (!user?.id || isCleanedUpRef.current) return false;
111
+
112
+ if (!userDataRef.current) {
113
+ const profile = await getCurrentUserProfile().catch((error) => {
114
+ if (DEV_MODE) {
115
+ console.error('Error fetching board presence user data:', error);
116
+ }
117
+ return null;
118
+ });
119
+
120
+ if (!profile) return false;
121
+
122
+ setCurrentUserId(user.id);
123
+ userDataRef.current = {
124
+ id: user.id,
125
+ display_name: profile.display_name,
126
+ email: profile.email,
127
+ avatar_url: profile.avatar_url,
128
+ };
129
+ }
130
+
131
+ if (isCleanedUpRef.current) return false;
132
+
133
+ const channel = supabase.channel(channelName, {
134
+ config: {
135
+ presence: {
136
+ enabled: true,
137
+ key: user.id,
138
+ },
139
+ private: true,
140
+ },
141
+ });
142
+ channelRef.current = channel;
143
+
144
+ return new Promise<boolean>((resolve) => {
145
+ channel
146
+ .on('presence', { event: 'sync' }, () => {
147
+ if (isCleanedUpRef.current) return;
148
+ const nextState =
149
+ channel.presenceState() as RealtimePresenceState<BoardPresenceState>;
150
+ setPresenceState({ ...nextState });
151
+ })
152
+ .on('presence', { event: 'join' }, ({ key }) => {
153
+ if (DEV_MODE) {
154
+ console.log('Board presence join:', key);
155
+ }
156
+ })
157
+ .on('presence', { event: 'leave' }, ({ key }) => {
158
+ if (DEV_MODE) {
159
+ console.log('Board presence leave:', key);
160
+ }
161
+ })
162
+ .subscribe((status) => {
163
+ if (DEV_MODE) {
164
+ console.log('Board presence status:', status);
165
+ }
166
+
167
+ if (status === 'SUBSCRIBED') {
168
+ resolve(true);
169
+ return;
170
+ }
171
+
172
+ if (
173
+ status === 'CHANNEL_ERROR' ||
174
+ status === 'TIMED_OUT' ||
175
+ status === 'CLOSED'
176
+ ) {
177
+ const deadChannel = channelRef.current;
178
+ channelRef.current = null;
179
+ setupPromiseRef.current = null;
180
+ lastTrackSignatureRef.current = null;
181
+ if (deadChannel) {
182
+ supabase.removeChannel(deadChannel).catch(() => {});
183
+ }
184
+ resolve(false);
185
+ }
186
+ });
187
+ });
188
+ } catch (error) {
189
+ if (DEV_MODE) {
190
+ console.error('Error setting up board presence:', error);
191
+ }
192
+ channelRef.current = null;
193
+ setupPromiseRef.current = null;
194
+ lastTrackSignatureRef.current = null;
195
+ return false;
196
+ }
197
+ })();
198
+
199
+ setupPromiseRef.current = promise;
200
+ return promise;
201
+ }, [channelName]);
202
+
203
+ useEffect(() => {
204
+ if (!channelName) return;
205
+ isCleanedUpRef.current = false;
206
+
207
+ return () => {
208
+ isCleanedUpRef.current = true;
209
+ setupPromiseRef.current = null;
210
+ if (channelRef.current) {
211
+ channelRef.current.untrack?.().catch(() => {});
212
+ createClient().removeChannel(channelRef.current);
213
+ channelRef.current = null;
214
+ lastTrackSignatureRef.current = null;
215
+ }
216
+ };
217
+ }, [channelName]);
218
+
219
+ const trackPresence = useCallback(async () => {
220
+ if (isCleanedUpRef.current) return;
221
+
222
+ const ready = await ensureChannel();
223
+ if (
224
+ !ready ||
225
+ !channelRef.current ||
226
+ !userDataRef.current ||
227
+ isCleanedUpRef.current
228
+ ) {
229
+ return;
230
+ }
231
+
232
+ try {
233
+ const payload: Omit<BoardPresenceState, 'online_at'> = {
234
+ user: userDataRef.current,
235
+ session_id: presenceSessionIdRef.current,
236
+ location: locationRef.current,
237
+ away: awayRef.current,
238
+ metadata: metadataRef.current,
239
+ };
240
+ const nextSignature = buildTrackSignature(payload);
241
+ if (nextSignature === lastTrackSignatureRef.current) return;
242
+
243
+ await channelRef.current.track({
244
+ ...payload,
245
+ online_at: new Date().toISOString(),
246
+ });
247
+ lastTrackSignatureRef.current = nextSignature;
248
+ } catch (error) {
249
+ if (DEV_MODE) {
250
+ console.error('Error tracking board presence:', error);
251
+ }
252
+ }
253
+ }, [ensureChannel]);
254
+
255
+ const updateLocation = useCallback(
256
+ async (location: PresenceLocation, metadata?: Record<string, any>) => {
257
+ locationRef.current = location;
258
+ if (metadata !== undefined) metadataRef.current = metadata;
259
+ await trackPresence();
260
+ },
261
+ [trackPresence]
262
+ );
263
+
264
+ const updateMetadata = useCallback(
265
+ async (metadata: Record<string, any>) => {
266
+ metadataRef.current = metadata;
267
+ await trackPresence();
268
+ },
269
+ [trackPresence]
270
+ );
271
+
272
+ useEffect(() => {
273
+ if (!channelName) return;
274
+
275
+ const handleVisibilityChange = () => {
276
+ const isAway =
277
+ typeof document !== 'undefined' &&
278
+ document.visibilityState === 'hidden';
279
+ if (awayRef.current === isAway) return;
280
+
281
+ awayRef.current = isAway;
282
+ if (channelRef.current) {
283
+ void trackPresence();
284
+ }
285
+ };
286
+
287
+ document.addEventListener('visibilitychange', handleVisibilityChange);
288
+ return () =>
289
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
290
+ }, [channelName, trackPresence]);
291
+
292
+ const allPresences = useMemo(() => {
293
+ const latestBySession = new Map<string, BoardPresenceState>();
294
+
295
+ for (const [presenceKey, presences] of Object.entries(presenceState)) {
296
+ for (const presence of presences) {
297
+ if (!presence) continue;
298
+
299
+ const userId = presence.user?.id || presenceKey;
300
+ if (!userId) continue;
301
+
302
+ const sessionId =
303
+ presence.session_id ||
304
+ (presence as { presence_ref?: string }).presence_ref ||
305
+ `${userId}:${presenceKey}`;
306
+ const dedupeKey = `${userId}:${sessionId}`;
307
+
308
+ const existing = latestBySession.get(dedupeKey);
309
+ if (!existing) {
310
+ latestBySession.set(dedupeKey, presence);
311
+ continue;
312
+ }
313
+
314
+ const existingTimestamp = Date.parse(existing.online_at);
315
+ const nextTimestamp = Date.parse(presence.online_at);
316
+ const shouldReplace = Number.isFinite(nextTimestamp)
317
+ ? !Number.isFinite(existingTimestamp) ||
318
+ nextTimestamp >= existingTimestamp
319
+ : !Number.isFinite(existingTimestamp);
320
+
321
+ if (shouldReplace) {
322
+ latestBySession.set(dedupeKey, presence);
323
+ }
324
+ }
325
+ }
326
+
327
+ return Array.from(latestBySession.values());
328
+ }, [presenceState]);
329
+
330
+ const getBoardViewers = useCallback(
331
+ (viewerBoardId: string) =>
332
+ allPresences.filter(
333
+ (presence) =>
334
+ presence.location?.type === 'board' &&
335
+ presence.location?.boardId === viewerBoardId
336
+ ),
337
+ [allPresences]
338
+ );
339
+
340
+ const getTaskViewers = useCallback(
341
+ (taskId: string) =>
342
+ allPresences.filter((presence) => presence.location?.taskId === taskId),
343
+ [allPresences]
344
+ );
345
+
346
+ return useMemo(
347
+ () => ({
348
+ presenceState,
349
+ currentUserId,
350
+ updateLocation,
351
+ updateMetadata,
352
+ getBoardViewers,
353
+ getTaskViewers,
354
+ }),
355
+ [
356
+ presenceState,
357
+ currentUserId,
358
+ updateLocation,
359
+ updateMetadata,
360
+ getBoardViewers,
361
+ getTaskViewers,
362
+ ]
363
+ );
364
+ }