@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
@@ -0,0 +1,147 @@
1
+ import { QueryClient } from '@tanstack/react-query';
2
+ import type { Task } from '@tuturuuu/types/primitives/Task';
3
+ import { describe, expect, it } from 'vitest';
4
+ import {
5
+ patchTaskInVisibleCaches,
6
+ restoreTasksFromVisibleCacheSnapshot,
7
+ snapshotVisibleTaskCaches,
8
+ } from '../task-cache-patches';
9
+
10
+ function createQueryClient() {
11
+ return new QueryClient({
12
+ defaultOptions: {
13
+ queries: { retry: false },
14
+ mutations: { retry: false },
15
+ },
16
+ });
17
+ }
18
+
19
+ function createTask(id: string, name = id): Task {
20
+ return {
21
+ id,
22
+ name,
23
+ list_id: 'list-1',
24
+ display_number: 1,
25
+ created_at: '2026-01-01T00:00:00.000Z',
26
+ labels: [],
27
+ projects: [],
28
+ assignees: [],
29
+ };
30
+ }
31
+
32
+ describe('task-cache-patches', () => {
33
+ it('patches every visible cache that can render a task card or detail view', () => {
34
+ const queryClient = createQueryClient();
35
+ const task = createTask('task-1');
36
+
37
+ queryClient.setQueryData(['tasks', 'board-1'], [task]);
38
+ queryClient.setQueryData(['tasks-full', 'board-1', 'all'], [task]);
39
+ queryClient.setQueryData(['task', 'task-1'], task);
40
+ queryClient.setQueryData(['workspaceTask', 'ws-1', 'task-1'], { task });
41
+ queryClient.setQueryData(['my-tasks', 'ws-1'], {
42
+ overdue: [],
43
+ today: [task],
44
+ upcoming: [],
45
+ completed: [],
46
+ });
47
+ queryClient.setQueryData(['my-completed-tasks', 'ws-1'], {
48
+ pages: [{ completed: [task] }],
49
+ });
50
+ const patchedLabel = {
51
+ id: 'label-1',
52
+ name: 'Bug',
53
+ color: '#ef4444',
54
+ created_at: '2026-01-01T00:00:00.000Z',
55
+ };
56
+
57
+ patchTaskInVisibleCaches({
58
+ queryClient,
59
+ boardId: 'board-1',
60
+ taskId: 'task-1',
61
+ updater: (cachedTask) => ({
62
+ ...cachedTask,
63
+ labels: [patchedLabel],
64
+ }),
65
+ });
66
+
67
+ expect(
68
+ queryClient.getQueryData<Task[]>(['tasks', 'board-1'])?.[0]?.labels
69
+ ).toEqual([patchedLabel]);
70
+ expect(
71
+ queryClient.getQueryData<Task[]>(['tasks-full', 'board-1', 'all'])?.[0]
72
+ ?.labels
73
+ ).toEqual([patchedLabel]);
74
+ expect(queryClient.getQueryData<Task>(['task', 'task-1'])?.labels).toEqual([
75
+ patchedLabel,
76
+ ]);
77
+ expect(
78
+ queryClient.getQueryData<{ task?: Task }>([
79
+ 'workspaceTask',
80
+ 'ws-1',
81
+ 'task-1',
82
+ ])?.task?.labels
83
+ ).toEqual([patchedLabel]);
84
+ expect(
85
+ queryClient.getQueryData<{ today?: Task[] }>(['my-tasks', 'ws-1'])
86
+ ?.today?.[0]?.labels
87
+ ).toEqual([patchedLabel]);
88
+ expect(
89
+ queryClient.getQueryData<{ pages?: Array<{ completed?: Task[] }> }>([
90
+ 'my-completed-tasks',
91
+ 'ws-1',
92
+ ])?.pages?.[0]?.completed?.[0]?.labels
93
+ ).toEqual([patchedLabel]);
94
+ });
95
+
96
+ it('restores only failed task ids from a snapshot', () => {
97
+ const queryClient = createQueryClient();
98
+ const taskOne = createTask('task-1', 'One');
99
+ const taskTwo = createTask('task-2', 'Two');
100
+
101
+ queryClient.setQueryData(['tasks', 'board-1'], [taskOne, taskTwo]);
102
+ queryClient.setQueryData(
103
+ ['tasks-full', 'board-1', 'all'],
104
+ [taskOne, taskTwo]
105
+ );
106
+ const snapshot = snapshotVisibleTaskCaches(queryClient, 'board-1', [
107
+ 'task-1',
108
+ 'task-2',
109
+ ]);
110
+
111
+ for (const taskId of ['task-1', 'task-2']) {
112
+ patchTaskInVisibleCaches({
113
+ queryClient,
114
+ boardId: 'board-1',
115
+ taskId,
116
+ updater: (cachedTask) => ({
117
+ ...cachedTask,
118
+ projects: [{ id: 'project-1', name: 'Roadmap', status: 'active' }],
119
+ }),
120
+ });
121
+ }
122
+
123
+ restoreTasksFromVisibleCacheSnapshot({
124
+ queryClient,
125
+ snapshot,
126
+ taskIds: ['task-2'],
127
+ });
128
+
129
+ const tasks = queryClient.getQueryData<Task[]>(['tasks', 'board-1']);
130
+ const fullTasks = queryClient.getQueryData<Task[]>([
131
+ 'tasks-full',
132
+ 'board-1',
133
+ 'all',
134
+ ]);
135
+
136
+ expect(tasks?.find((task) => task.id === 'task-1')?.projects).toEqual([
137
+ { id: 'project-1', name: 'Roadmap', status: 'active' },
138
+ ]);
139
+ expect(tasks?.find((task) => task.id === 'task-2')?.projects).toEqual([]);
140
+ expect(fullTasks?.find((task) => task.id === 'task-1')?.projects).toEqual([
141
+ { id: 'project-1', name: 'Roadmap', status: 'active' },
142
+ ]);
143
+ expect(fullTasks?.find((task) => task.id === 'task-2')?.projects).toEqual(
144
+ []
145
+ );
146
+ });
147
+ });
@@ -65,6 +65,22 @@ describe('TaskLegacyRouteRecovery', () => {
65
65
  });
66
66
  });
67
67
 
68
+ it('uses the board skeleton while resolving the canonical task route', () => {
69
+ mockGetWorkspaceTask.mockReturnValue(new Promise(() => {}));
70
+
71
+ renderWithQueryClient(
72
+ <TaskLegacyRouteRecovery
73
+ routePrefix="/tasks"
74
+ taskId="task-1"
75
+ workspaceId="personal"
76
+ />
77
+ );
78
+
79
+ expect(screen.getByTestId('task-board-loading-state')).toBeInTheDocument();
80
+ expect(screen.getByTestId('kanban-skeleton')).toBeInTheDocument();
81
+ expect(screen.queryByText('loading')).not.toBeInTheDocument();
82
+ });
83
+
68
84
  it('shows a recoverable error state instead of a 404 when resolution fails', async () => {
69
85
  mockGetWorkspaceTask.mockRejectedValue(new Error('boom'));
70
86
 
@@ -71,6 +71,7 @@ describe('useProgressiveBoardLoader', () => {
71
71
  limit: 50,
72
72
  offset: 0,
73
73
  includeCount: true,
74
+ includeRelationshipSummary: false,
74
75
  });
75
76
  expect(queryClient.getQueryData<Task[]>(['tasks', 'board-1'])).toEqual([
76
77
  {
@@ -160,6 +161,7 @@ describe('useProgressiveBoardLoader', () => {
160
161
  externalIncludeDocuments: true,
161
162
  externalIncludeDoneClosed: true,
162
163
  externalSortBy: 'due-asc',
164
+ includeRelationshipSummary: false,
163
165
  });
164
166
 
165
167
  vi.mocked(listWorkspaceTasks).mockResolvedValueOnce({
@@ -179,6 +181,7 @@ describe('useProgressiveBoardLoader', () => {
179
181
  externalIncludeDocuments: true,
180
182
  externalIncludeDoneClosed: true,
181
183
  externalSortBy: 'due-asc',
184
+ includeRelationshipSummary: false,
182
185
  });
183
186
  });
184
187
 
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useMutation, useQueryClient } from '@tanstack/react-query';
3
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
4
  import {
5
5
  Search,
6
6
  UserCircle,
@@ -9,7 +9,10 @@ import {
9
9
  UserX,
10
10
  X,
11
11
  } from '@tuturuuu/icons';
12
- import { updateWorkspaceTask } from '@tuturuuu/internal-api/tasks';
12
+ import {
13
+ listWorkspaceTaskBoardViewableMembers,
14
+ updateWorkspaceTask,
15
+ } from '@tuturuuu/internal-api/tasks';
13
16
  import { Avatar, AvatarFallback, AvatarImage } from '@tuturuuu/ui/avatar';
14
17
  import { Button } from '@tuturuuu/ui/button';
15
18
  import { useWorkspaceMembers } from '@tuturuuu/ui/hooks/use-workspace-members';
@@ -26,7 +29,15 @@ import {
26
29
  useMemo,
27
30
  useState,
28
31
  } from 'react';
29
- import { useBoardBroadcast } from './board-broadcast-context';
32
+ import {
33
+ getActiveBoardRefresh,
34
+ useBoardBroadcast,
35
+ } from './board-broadcast-context';
36
+ import {
37
+ patchTaskInVisibleCaches,
38
+ restoreVisibleTaskCaches,
39
+ snapshotVisibleTaskCaches,
40
+ } from './task-cache-patches';
30
41
 
31
42
  interface Member {
32
43
  id: string;
@@ -36,7 +47,7 @@ interface Member {
36
47
  avatar_url?: string;
37
48
  }
38
49
 
39
- interface Task {
50
+ interface TaskWithAssignees {
40
51
  id: string;
41
52
  assignees?: Member[];
42
53
  }
@@ -66,7 +77,8 @@ export const AssigneeSelect = forwardRef<AssigneeSelectHandle, Props>(
66
77
  const params = useParams();
67
78
  const rawWsId = params.wsId;
68
79
  const wsId = Array.isArray(rawWsId) ? rawWsId[0] : rawWsId;
69
- const boardId = params.boardId as string;
80
+ const rawBoardId = params.boardId;
81
+ const boardId = Array.isArray(rawBoardId) ? rawBoardId[0] : rawBoardId;
70
82
  const queryClient = useQueryClient();
71
83
  const broadcast = useBoardBroadcast();
72
84
 
@@ -104,11 +116,41 @@ export const AssigneeSelect = forwardRef<AssigneeSelectHandle, Props>(
104
116
  }, [uniqueAssignees]);
105
117
 
106
118
  // Fetch workspace members with React Query
107
- const {
108
- data: fetchedMembers = [],
109
- isLoading: isFetchingMembers,
110
- error: membersError,
111
- } = useWorkspaceMembers(wsId);
119
+ const shouldFetchBoardMembers = Boolean(wsId && boardId);
120
+ const workspaceMembersQuery = useWorkspaceMembers(wsId, {
121
+ enabled: Boolean(wsId) && !shouldFetchBoardMembers,
122
+ });
123
+ const boardMembersQuery = useQuery({
124
+ queryKey: ['task-board-viewable-members', wsId, boardId],
125
+ queryFn: async (): Promise<Member[]> => {
126
+ if (!wsId || !boardId) return [];
127
+
128
+ const payload = await listWorkspaceTaskBoardViewableMembers(
129
+ wsId,
130
+ boardId
131
+ );
132
+ const members = Array.isArray(payload?.members) ? payload.members : [];
133
+
134
+ return members.map((member) => ({
135
+ id: member.user_id,
136
+ user_id: member.user_id,
137
+ display_name: member.display_name || member.email || member.user_id,
138
+ email: member.email ?? undefined,
139
+ avatar_url: member.avatar_url ?? undefined,
140
+ }));
141
+ },
142
+ enabled: shouldFetchBoardMembers,
143
+ staleTime: 5 * 60 * 1000,
144
+ });
145
+ const fetchedMembers = shouldFetchBoardMembers
146
+ ? (boardMembersQuery.data ?? [])
147
+ : (workspaceMembersQuery.data ?? []);
148
+ const isFetchingMembers = shouldFetchBoardMembers
149
+ ? boardMembersQuery.isLoading
150
+ : workspaceMembersQuery.isLoading;
151
+ const membersError = shouldFetchBoardMembers
152
+ ? boardMembersQuery.error
153
+ : workspaceMembersQuery.error;
112
154
 
113
155
  // Deduplicate members by ID using O(n) Map approach
114
156
  // Also ensure user_id is set for consistency with task creation flow
@@ -148,7 +190,10 @@ export const AssigneeSelect = forwardRef<AssigneeSelectHandle, Props>(
148
190
  throw new Error(t('please_try_again_later'));
149
191
  }
150
192
 
151
- const boardTasks = queryClient.getQueryData<Task[]>(['tasks', boardId]);
193
+ const boardTasks = queryClient.getQueryData<TaskWithAssignees[]>([
194
+ 'tasks',
195
+ boardId,
196
+ ]);
152
197
  const currentTask = boardTasks?.find((task) => task.id === taskId);
153
198
  const existingIds =
154
199
  currentTask?.assignees
@@ -171,19 +216,24 @@ export const AssigneeSelect = forwardRef<AssigneeSelectHandle, Props>(
171
216
  onMutate: async ({ memberId, action }) => {
172
217
  // Cancel any outgoing refetches
173
218
  await queryClient.cancelQueries({ queryKey: ['tasks', boardId] });
219
+ if (boardId) {
220
+ await queryClient.cancelQueries({
221
+ queryKey: ['tasks-full', boardId],
222
+ });
223
+ }
174
224
 
175
225
  // Snapshot the previous value
176
- const previousTasks = queryClient.getQueryData(['tasks', boardId]);
226
+ const cacheSnapshot = boardId
227
+ ? snapshotVisibleTaskCaches(queryClient, boardId, [taskId])
228
+ : undefined;
177
229
 
178
230
  // Optimistically update the cache
179
- queryClient.setQueryData(
180
- ['tasks', boardId],
181
- (old: Task[] | undefined) => {
182
- if (!old) return old;
183
-
184
- return old.map((task: Task) => {
185
- if (task.id !== taskId) return task;
186
-
231
+ if (boardId) {
232
+ patchTaskInVisibleCaches({
233
+ queryClient,
234
+ boardId,
235
+ taskId,
236
+ updater: (task) => {
187
237
  const currentAssignees = task.assignees || [];
188
238
  let newAssignees: Member[];
189
239
 
@@ -204,9 +254,9 @@ export const AssigneeSelect = forwardRef<AssigneeSelectHandle, Props>(
204
254
  }
205
255
 
206
256
  return { ...task, assignees: newAssignees };
207
- });
208
- }
209
- );
257
+ },
258
+ });
259
+ }
210
260
 
211
261
  const previousLocalAssignees = localAssigneesState;
212
262
 
@@ -223,12 +273,12 @@ export const AssigneeSelect = forwardRef<AssigneeSelectHandle, Props>(
223
273
  return old.filter((assignee) => assignee.id !== memberId);
224
274
  });
225
275
 
226
- return { previousTasks, previousLocalAssignees };
276
+ return { cacheSnapshot, previousLocalAssignees };
227
277
  },
228
278
  onError: (err, _, context) => {
229
279
  // Rollback optimistic update on error
230
- if (context?.previousTasks) {
231
- queryClient.setQueryData(['tasks', boardId], context.previousTasks);
280
+ if (context?.cacheSnapshot) {
281
+ restoreVisibleTaskCaches(queryClient, context.cacheSnapshot);
232
282
  }
233
283
 
234
284
  if (context?.previousLocalAssignees) {
@@ -242,6 +292,7 @@ export const AssigneeSelect = forwardRef<AssigneeSelectHandle, Props>(
242
292
  },
243
293
  onSuccess: () => {
244
294
  broadcast?.('task:relations-changed', { taskId });
295
+ getActiveBoardRefresh()?.({ invalidateTasks: false });
245
296
  },
246
297
  // Note: Removed onSettled invalidation to prevent flicker
247
298
  // Optimistic updates handle immediate UI feedback
@@ -21,9 +21,10 @@ import {
21
21
  setActiveBoardRefresh,
22
22
  setActiveBroadcast,
23
23
  } from './board-broadcast-context';
24
- import { BoardViews } from './board-views';
24
+ import { BoardViews, type ViewType } from './board-views';
25
25
  import { ProgressiveLoaderProvider } from './progressive-loader-context';
26
26
  import { dispatchRecentSidebarVisit } from './recent-sidebar-events';
27
+ import { TaskBoardLoadingState } from './task-board-loading-state';
27
28
  import { useProgressiveBoardLoader } from './use-progressive-board-loader';
28
29
 
29
30
  const BOARD_REVALIDATE_COOLDOWN_MS = 30_000;
@@ -34,16 +35,20 @@ interface Props {
34
35
  workspaceTier?: WorkspaceProductTier | null;
35
36
  currentUserId?: string;
36
37
  routePrefix?: string;
38
+ defaultView?: ViewType;
37
39
  idleBottomIsland?: ReactNode;
40
+ rootLoading?: boolean;
38
41
  }
39
42
 
40
43
  export function BoardClient({
41
44
  boardId,
45
+ defaultView,
42
46
  idleBottomIsland,
43
47
  workspace,
44
48
  workspaceTier,
45
49
  currentUserId,
46
50
  routePrefix = '/tasks',
51
+ rootLoading = false,
47
52
  }: Props) {
48
53
  const router = useRouter();
49
54
  const queryClient = useQueryClient();
@@ -80,6 +85,7 @@ export function BoardClient({
80
85
  queryFn: async () => {
81
86
  const result = await listWorkspaceTasks(boardWorkspaceId, {
82
87
  boardId,
88
+ includeRelationshipSummary: false,
83
89
  });
84
90
  return result.tasks;
85
91
  },
@@ -142,8 +148,6 @@ export function BoardClient({
142
148
  // Fetch workspace labels once at the board level
143
149
  const { data: workspaceLabels = [] } = useWorkspaceLabels(boardWorkspaceId);
144
150
 
145
- const { broadcast } = useBoardRealtime(boardId);
146
-
147
151
  const refreshActiveBoard = useCallback(
148
152
  (options?: BoardRefreshOptions) => {
149
153
  const invalidateTasks = options?.invalidateTasks ?? true;
@@ -180,6 +184,12 @@ export function BoardClient({
180
184
  ]
181
185
  );
182
186
 
187
+ const { broadcast } = useBoardRealtime(boardId, {
188
+ onTaskRelationsChange: () => {
189
+ refreshActiveBoard({ invalidateTasks: false });
190
+ },
191
+ });
192
+
183
193
  // Register broadcast at module level so components outside the
184
194
  // BoardBroadcastProvider tree (e.g. task dialog) can access it.
185
195
  useEffect(() => {
@@ -237,13 +247,7 @@ export function BoardClient({
237
247
  ]);
238
248
 
239
249
  if (boardLoading && !board) {
240
- return (
241
- <div className="flex flex-col">
242
- <div className="p-4 text-center text-muted-foreground">
243
- Loading board...
244
- </div>
245
- </div>
246
- );
250
+ return <TaskBoardLoadingState root={rootLoading} />;
247
251
  }
248
252
 
249
253
  if (!board?.id) {
@@ -267,6 +271,7 @@ export function BoardClient({
267
271
  lists={lists}
268
272
  workspaceLabels={workspaceLabels}
269
273
  currentUserId={currentUserId}
274
+ defaultView={defaultView}
270
275
  canManageBoard={canManageBoard}
271
276
  idleBottomIsland={idleBottomIsland}
272
277
  />
@@ -2,7 +2,13 @@
2
2
 
3
3
  import type { TaskFilters } from '../boards/boardId/task-filter';
4
4
 
5
- export type StoredBoardView = 'kanban' | 'list' | 'timeline';
5
+ export type StoredBoardView =
6
+ | 'kanban'
7
+ | 'list'
8
+ | 'my_tasks'
9
+ | 'timeline'
10
+ | 'drafts'
11
+ | 'recycle_bin';
6
12
  export type StoredListStatusFilter = 'all' | 'active' | 'not_started';
7
13
 
8
14
  export interface BoardViewConfig {