@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,144 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
6
+ import { act, renderHook } from '@testing-library/react';
7
+ import type { Task } from '@tuturuuu/types/primitives/Task';
8
+ import type { ReactNode } from 'react';
9
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
10
+ import { useTaskProjectManagement } from '../useTaskProjectManagement';
11
+
12
+ vi.mock('@tuturuuu/ui/sonner', () => ({
13
+ toast: {
14
+ error: vi.fn(),
15
+ success: vi.fn(),
16
+ warning: vi.fn(),
17
+ },
18
+ }));
19
+
20
+ vi.mock('@tuturuuu/internal-api/tasks', () => ({
21
+ createWorkspaceTaskProject: vi.fn(),
22
+ updateWorkspaceTask: vi.fn(),
23
+ }));
24
+
25
+ describe('useTaskProjectManagement', () => {
26
+ let queryClient: QueryClient;
27
+ let mockUpdateWorkspaceTask: any;
28
+
29
+ const project = {
30
+ id: 'project-1',
31
+ name: 'Roadmap',
32
+ status: 'active',
33
+ };
34
+
35
+ const mockTask = {
36
+ id: 'task-1',
37
+ name: 'Test Task',
38
+ list_id: 'list-1',
39
+ display_number: 1,
40
+ created_at: '2026-01-01T00:00:00.000Z',
41
+ projects: [],
42
+ } satisfies Task;
43
+
44
+ const wrapper = ({ children }: { children: ReactNode }) => (
45
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
46
+ );
47
+
48
+ beforeEach(async () => {
49
+ queryClient = new QueryClient({
50
+ defaultOptions: {
51
+ queries: { retry: false },
52
+ mutations: { retry: false },
53
+ },
54
+ });
55
+
56
+ const { updateWorkspaceTask } = await import(
57
+ '@tuturuuu/internal-api/tasks'
58
+ );
59
+ mockUpdateWorkspaceTask = updateWorkspaceTask as any;
60
+
61
+ vi.clearAllMocks();
62
+ mockUpdateWorkspaceTask.mockResolvedValue({ task: { id: 'task-1' } });
63
+ });
64
+
65
+ it('updates full-board and task detail caches without invalidating visible board queries', async () => {
66
+ queryClient.setQueryData(['tasks', 'board-1'], [mockTask]);
67
+ queryClient.setQueryData(['tasks-full', 'board-1', 'all'], [mockTask]);
68
+ queryClient.setQueryData(['task', 'task-1'], mockTask);
69
+ const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
70
+
71
+ const { result } = renderHook(
72
+ () =>
73
+ useTaskProjectManagement({
74
+ task: mockTask,
75
+ boardId: 'board-1',
76
+ workspaceProjects: [project],
77
+ workspaceId: 'ws-1',
78
+ taskId: 'task-1',
79
+ }),
80
+ { wrapper }
81
+ );
82
+
83
+ await act(async () => {
84
+ await result.current.toggleTaskProject('project-1');
85
+ });
86
+
87
+ expect(
88
+ queryClient
89
+ .getQueryData<Task[]>(['tasks-full', 'board-1', 'all'])?.[0]
90
+ ?.projects?.some((entry) => entry.id === 'project-1')
91
+ ).toBe(true);
92
+ expect(
93
+ queryClient
94
+ .getQueryData<Task>(['task', 'task-1'])
95
+ ?.projects?.some((entry) => entry.id === 'project-1')
96
+ ).toBe(true);
97
+ expect(invalidateSpy).not.toHaveBeenCalledWith({
98
+ queryKey: ['tasks', 'board-1'],
99
+ });
100
+ expect(invalidateSpy).not.toHaveBeenCalledWith({
101
+ queryKey: ['tasks-full', 'board-1'],
102
+ });
103
+ });
104
+
105
+ it('rolls back every visible cache when the project update fails', async () => {
106
+ const taskWithProject = {
107
+ ...mockTask,
108
+ projects: [project],
109
+ } as Task;
110
+ mockUpdateWorkspaceTask.mockRejectedValueOnce(new Error('Database error'));
111
+ queryClient.setQueryData(['tasks', 'board-1'], [taskWithProject]);
112
+ queryClient.setQueryData(
113
+ ['tasks-full', 'board-1', 'all'],
114
+ [taskWithProject]
115
+ );
116
+ queryClient.setQueryData(['task', 'task-1'], taskWithProject);
117
+
118
+ const { result } = renderHook(
119
+ () =>
120
+ useTaskProjectManagement({
121
+ task: taskWithProject,
122
+ boardId: 'board-1',
123
+ workspaceProjects: [project],
124
+ workspaceId: 'ws-1',
125
+ taskId: 'task-1',
126
+ }),
127
+ { wrapper }
128
+ );
129
+
130
+ await act(async () => {
131
+ await result.current.toggleTaskProject('project-1');
132
+ });
133
+
134
+ expect(queryClient.getQueryData<Task[]>(['tasks', 'board-1'])).toEqual([
135
+ taskWithProject,
136
+ ]);
137
+ expect(
138
+ queryClient.getQueryData<Task[]>(['tasks-full', 'board-1', 'all'])
139
+ ).toEqual([taskWithProject]);
140
+ expect(queryClient.getQueryData<Task>(['task', 'task-1'])).toEqual(
141
+ taskWithProject
142
+ );
143
+ });
144
+ });
@@ -4,6 +4,7 @@ import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
4
4
  import type { TaskFilters } from '@tuturuuu/ui/tu-do/boards/boardId/task-filter';
5
5
  import {
6
6
  type PendingRelationshipType,
7
+ type TaskAssigneeMemberSource,
7
8
  useTaskDialogContext,
8
9
  } from '../providers/task-dialog-provider';
9
10
  import type { SharedTaskContext } from '../shared/task-edit-dialog/hooks/use-task-data';
@@ -50,6 +51,10 @@ export function useTaskDialog(): {
50
51
  taskWsId?: string;
51
52
  /** Whether the task's workspace is personal (affects realtime features) */
52
53
  taskWorkspacePersonal?: boolean;
54
+ /** Whether the board context should expose assignee controls */
55
+ canUseBoardAssignees?: boolean;
56
+ /** Where assignee candidates should be loaded from */
57
+ assigneeMemberSource?: TaskAssigneeMemberSource;
53
58
  }
54
59
  ) => void;
55
60
  openTaskById: (
@@ -62,6 +67,8 @@ export function useTaskDialog(): {
62
67
  taskWsId?: string;
63
68
  taskWorkspacePersonal?: boolean;
64
69
  taskWorkspaceTier?: WorkspaceProductTier;
70
+ canUseBoardAssignees?: boolean;
71
+ assigneeMemberSource?: TaskAssigneeMemberSource;
65
72
  initialSharedContext?: SharedTaskContext;
66
73
  }
67
74
  ) => Promise<boolean>;
@@ -9,7 +9,17 @@ import type { TaskLabel as DbTaskLabel } from '@tuturuuu/types/db';
9
9
  import type { Task } from '@tuturuuu/types/primitives/Task';
10
10
  import { toast } from '@tuturuuu/ui/sonner';
11
11
  import { useState } from 'react';
12
- import { useBoardBroadcast } from '../shared/board-broadcast-context';
12
+ import {
13
+ getActiveBoardRefresh,
14
+ useBoardBroadcast,
15
+ } from '../shared/board-broadcast-context';
16
+ import {
17
+ getTaskFromVisibleCaches,
18
+ patchTaskInVisibleCaches,
19
+ restoreTasksFromVisibleCacheSnapshot,
20
+ restoreVisibleTaskCaches,
21
+ snapshotVisibleTaskCaches,
22
+ } from '../shared/task-cache-patches';
13
23
  import { getRandomNewLabelColor } from '../utils/taskConstants';
14
24
 
15
25
  type WorkspaceTaskLabel = Pick<
@@ -68,10 +78,14 @@ export function useTaskLabelManagement({
68
78
 
69
79
  // CRITICAL: Get current task state from cache instead of stale prop
70
80
  // This ensures we read the most up-to-date state after optimistic updates
71
- const currentTask = taskId
72
- ? ((queryClient.getQueryData(['task', taskId]) as Task | undefined) ??
73
- task)
74
- : task;
81
+ const canonicalTaskId = taskId ?? task.id;
82
+ const currentTask =
83
+ getTaskFromVisibleCaches({
84
+ queryClient,
85
+ boardId,
86
+ taskId: canonicalTaskId,
87
+ fallback: task,
88
+ }) ?? task;
75
89
 
76
90
  // Check if we're in multi-select mode with multiple tasks selected
77
91
  const shouldBulkUpdate =
@@ -86,11 +100,17 @@ export function useTaskLabelManagement({
86
100
 
87
101
  // Cancel any outgoing refetches
88
102
  await queryClient.cancelQueries({ queryKey: ['tasks', boardId] });
103
+ await queryClient.cancelQueries({ queryKey: ['tasks-full', boardId] });
89
104
 
90
105
  // Snapshot the previous value BEFORE optimistic update
91
106
  const previousTasks = queryClient.getQueryData(['tasks', boardId]) as
92
107
  | Task[]
93
108
  | undefined;
109
+ const cacheSnapshot = snapshotVisibleTaskCaches(
110
+ queryClient,
111
+ boardId,
112
+ tasksToUpdate
113
+ );
94
114
 
95
115
  // Determine action: remove if ALL selected tasks have the label, add otherwise
96
116
  // Use currentTask from cache, not stale task prop
@@ -112,6 +132,13 @@ export function useTaskLabelManagement({
112
132
  const fromBoardCache = previousTasks?.find((ct) => ct.id === taskId);
113
133
  if (fromBoardCache) return fromBoardCache;
114
134
 
135
+ const fromVisibleCaches = getTaskFromVisibleCaches({
136
+ queryClient,
137
+ boardId,
138
+ taskId,
139
+ });
140
+ if (fromVisibleCaches) return fromVisibleCaches;
141
+
115
142
  // Fallback to individual task cache (for tasks not in board view)
116
143
  if (taskId === currentTask.id) return currentTask;
117
144
 
@@ -135,63 +162,36 @@ export function useTaskLabelManagement({
135
162
 
136
163
  // Get label details from workspace labels for optimistic update
137
164
  const label = workspaceLabels.find((l) => l.id === labelId);
165
+ const fallbackLabel = label || {
166
+ id: labelId,
167
+ name: 'Unknown',
168
+ color: '#3b82f6',
169
+ created_at: new Date().toISOString(),
170
+ };
138
171
 
139
172
  // Optimistically update the cache - only update tasks that actually change
140
- queryClient.setQueryData(['tasks', boardId], (old: Task[] | undefined) => {
141
- if (!old) return old;
142
- return old.map((t) => {
143
- if (active && tasksToRemoveFrom.includes(t.id)) {
144
- // Remove the label
145
- return {
146
- ...t,
147
- labels: t.labels?.filter((l) => l.id !== labelId) || [],
148
- };
149
- } else if (!active && tasksNeedingLabel.includes(t.id)) {
150
- // Add the label
151
- return {
152
- ...t,
153
- labels: [
154
- ...(t.labels || []),
155
- label || {
156
- id: labelId,
157
- name: 'Unknown',
158
- color: '#3b82f6',
159
- created_at: new Date().toISOString(),
160
- },
161
- ],
162
- };
163
- }
164
- return t;
165
- });
166
- });
167
-
168
- // CRITICAL: Also update the individual task cache if taskId is provided
169
- // This ensures the chip menu's task cache stays in sync with the board cache
170
- if (taskId) {
171
- queryClient.setQueryData(['task', taskId], (old: Task | undefined) => {
172
- if (!old) return old;
173
- if (active && tasksToRemoveFrom.includes(taskId)) {
174
- // Remove the label
175
- return {
176
- ...old,
177
- labels: old.labels?.filter((l) => l.id !== labelId) || [],
178
- };
179
- } else if (!active && tasksNeedingLabel.includes(taskId)) {
180
- // Add the label
173
+ for (const tid of active ? tasksToRemoveFrom : tasksNeedingLabel) {
174
+ patchTaskInVisibleCaches({
175
+ queryClient,
176
+ boardId,
177
+ taskId: tid,
178
+ updater: (cachedTask) => {
179
+ if (active) {
180
+ return {
181
+ ...cachedTask,
182
+ labels: cachedTask.labels?.filter((l) => l.id !== labelId) || [],
183
+ };
184
+ }
185
+
186
+ if (cachedTask.labels?.some((l) => l.id === labelId)) {
187
+ return cachedTask;
188
+ }
189
+
181
190
  return {
182
- ...old,
183
- labels: [
184
- ...(old.labels || []),
185
- label || {
186
- id: labelId,
187
- name: 'Unknown',
188
- color: '#3b82f6',
189
- created_at: new Date().toISOString(),
190
- },
191
- ],
191
+ ...cachedTask,
192
+ labels: [...(cachedTask.labels || []), fallbackLabel],
192
193
  };
193
- }
194
- return old;
194
+ },
195
195
  });
196
196
  }
197
197
 
@@ -201,33 +201,36 @@ export function useTaskLabelManagement({
201
201
  ? { baseUrl: window.location.origin }
202
202
  : undefined;
203
203
  let successCount = 0;
204
+ const succeededTaskIds: string[] = [];
204
205
 
205
206
  if (active) {
206
- for (const taskId of tasksToRemoveFrom) {
207
+ for (const tid of tasksToRemoveFrom) {
207
208
  try {
208
209
  await removeWorkspaceTaskLabel(
209
210
  workspaceId,
210
- taskId,
211
+ tid,
211
212
  labelId,
212
213
  internalApiOptions
213
214
  );
214
215
  successCount++;
216
+ succeededTaskIds.push(tid);
215
217
  } catch (error) {
216
- console.error(`Failed to remove label from task ${taskId}:`, error);
218
+ console.error(`Failed to remove label from task ${tid}:`, error);
217
219
  }
218
220
  }
219
221
  } else {
220
- for (const taskId of tasksNeedingLabel) {
222
+ for (const tid of tasksNeedingLabel) {
221
223
  try {
222
224
  await addWorkspaceTaskLabel(
223
225
  workspaceId,
224
- taskId,
226
+ tid,
225
227
  labelId,
226
228
  internalApiOptions
227
229
  );
228
230
  successCount++;
231
+ succeededTaskIds.push(tid);
229
232
  } catch (error) {
230
- console.error(`Failed to add label to task ${taskId}:`, error);
233
+ console.error(`Failed to add label to task ${tid}:`, error);
231
234
  }
232
235
  }
233
236
  }
@@ -240,10 +243,22 @@ export function useTaskLabelManagement({
240
243
  throw new Error('Failed to update any tasks');
241
244
  }
242
245
 
246
+ const failedTaskIds = (
247
+ active ? tasksToRemoveFrom : tasksNeedingLabel
248
+ ).filter((tid) => !succeededTaskIds.includes(tid));
249
+ restoreTasksFromVisibleCacheSnapshot({
250
+ queryClient,
251
+ snapshot: cacheSnapshot,
252
+ taskIds: failedTaskIds,
253
+ });
254
+
243
255
  // Broadcast relation changes for all affected tasks
244
- for (const tid of active ? tasksToRemoveFrom : tasksNeedingLabel) {
256
+ for (const tid of succeededTaskIds) {
245
257
  broadcast?.('task:relations-changed', { taskId: tid });
246
258
  }
259
+ if (succeededTaskIds.length > 0) {
260
+ getActiveBoardRefresh()?.({ invalidateTasks: false });
261
+ }
247
262
 
248
263
  toast.success(active ? 'Label removed' : 'Label added', {
249
264
  description:
@@ -253,9 +268,7 @@ export function useTaskLabelManagement({
253
268
  // Don't auto-clear selection - let user manually clear with "Clear" button
254
269
  } catch (e: any) {
255
270
  // Rollback on error
256
- if (previousTasks) {
257
- queryClient.setQueryData(['tasks', boardId], previousTasks);
258
- }
271
+ restoreVisibleTaskCaches(queryClient, cacheSnapshot);
259
272
  console.error('Failed to toggle label:', e);
260
273
  toast.error('Error', {
261
274
  description: 'Failed to update label. Please try again.',
@@ -306,48 +319,44 @@ export function useTaskLabelManagement({
306
319
 
307
320
  // Auto-apply the newly created label to this task
308
321
  let linkSucceeded = false;
309
- let previousTasks: unknown;
322
+ const canonicalTaskId = taskId ?? task.id;
323
+ let cacheSnapshot:
324
+ | ReturnType<typeof snapshotVisibleTaskCaches>
325
+ | undefined;
310
326
  try {
311
327
  // Cancel any outgoing refetches
312
328
  await queryClient.cancelQueries({ queryKey: ['tasks', boardId] });
329
+ await queryClient.cancelQueries({ queryKey: ['tasks-full', boardId] });
313
330
 
314
331
  // Snapshot the previous value
315
- previousTasks = queryClient.getQueryData(['tasks', boardId]);
332
+ cacheSnapshot = snapshotVisibleTaskCaches(queryClient, boardId, [
333
+ canonicalTaskId,
334
+ ]);
316
335
 
317
336
  // Optimistically update the cache
318
- queryClient.setQueryData(
319
- ['tasks', boardId],
320
- (old: any[] | undefined) => {
321
- if (!old) return old;
322
- return old.map((t) => {
323
- if (t.id === task.id) {
324
- return {
325
- ...t,
326
- labels: [...(t.labels || []), newLabel],
327
- };
328
- }
329
- return t;
330
- });
331
- }
332
- );
333
-
334
- // CRITICAL: Also update individual task cache if taskId is provided
335
- if (taskId) {
336
- queryClient.setQueryData(
337
- ['task', taskId],
338
- (old: Task | undefined) => {
339
- if (!old) return old;
340
- return {
341
- ...old,
342
- labels: [...(old.labels || []), newLabel],
343
- };
337
+ patchTaskInVisibleCaches({
338
+ queryClient,
339
+ boardId,
340
+ taskId: canonicalTaskId,
341
+ updater: (cachedTask) => {
342
+ if (cachedTask.labels?.some((label) => label.id === newLabel.id)) {
343
+ return cachedTask;
344
344
  }
345
- );
346
- }
345
+
346
+ return {
347
+ ...cachedTask,
348
+ labels: [...(cachedTask.labels || []), newLabel],
349
+ };
350
+ },
351
+ });
347
352
 
348
353
  const taskState =
349
- (queryClient.getQueryData(['task', task.id]) as Task | undefined) ??
350
- task;
354
+ getTaskFromVisibleCaches({
355
+ queryClient,
356
+ boardId,
357
+ taskId: canonicalTaskId,
358
+ fallback: task,
359
+ }) ?? task;
351
360
  const nextLabelIds = [
352
361
  ...new Set([
353
362
  ...(taskState.labels ?? []).map((entry) => entry.id),
@@ -357,7 +366,7 @@ export function useTaskLabelManagement({
357
366
 
358
367
  await updateWorkspaceTask(
359
368
  workspaceId,
360
- task.id,
369
+ canonicalTaskId,
361
370
  {
362
371
  label_ids: nextLabelIds,
363
372
  },
@@ -368,19 +377,19 @@ export function useTaskLabelManagement({
368
377
  linkSucceeded = true;
369
378
  } catch (linkErr: any) {
370
379
  // Rollback on error
371
- queryClient.setQueryData(['tasks', boardId], previousTasks);
380
+ if (cacheSnapshot) {
381
+ restoreVisibleTaskCaches(queryClient, cacheSnapshot);
382
+ }
372
383
  toast.error(
373
384
  'The label was created but could not be attached to the task. Refresh and try manually.'
374
385
  );
375
- if (taskId) {
376
- queryClient.invalidateQueries({ queryKey: ['task', taskId] });
377
- }
378
386
  console.error('Failed to auto-apply new label', linkErr);
379
387
  }
380
388
 
381
389
  // Only show success toast and reset form if link succeeded
382
390
  if (linkSucceeded) {
383
- broadcast?.('task:relations-changed', { taskId: task.id });
391
+ broadcast?.('task:relations-changed', { taskId: canonicalTaskId });
392
+ getActiveBoardRefresh()?.({ invalidateTasks: false });
384
393
 
385
394
  // Reset form and close dialog
386
395
  setNewLabelName('');