@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,301 @@
1
+ import type { UseMutationResult } from '@tanstack/react-query';
2
+ import { Plus, Target } from '@tuturuuu/icons';
3
+ import type { TaskProgressMetric } from '@tuturuuu/internal-api';
4
+ import { Badge } from '@tuturuuu/ui/badge';
5
+ import { Button } from '@tuturuuu/ui/button';
6
+ import { Card, CardContent, CardHeader, CardTitle } from '@tuturuuu/ui/card';
7
+ import { Input } from '@tuturuuu/ui/input';
8
+ import { Textarea } from '@tuturuuu/ui/textarea';
9
+ import type { useTranslations } from 'next-intl';
10
+ import type { ReactNode } from 'react';
11
+
12
+ type Translate = ReturnType<typeof useTranslations>;
13
+ const today = () => new Date().toISOString().slice(0, 10);
14
+
15
+ export function SummaryCard({
16
+ icon,
17
+ label,
18
+ value,
19
+ }: {
20
+ icon: ReactNode;
21
+ label: string;
22
+ value: number;
23
+ }) {
24
+ return (
25
+ <Card>
26
+ <CardHeader className="pb-2">
27
+ <CardTitle className="flex items-center gap-2 text-muted-foreground text-sm">
28
+ {icon}
29
+ {label}
30
+ </CardTitle>
31
+ </CardHeader>
32
+ <CardContent>
33
+ <div className="font-bold text-2xl">
34
+ {Number(value).toLocaleString()}
35
+ </div>
36
+ </CardContent>
37
+ </Card>
38
+ );
39
+ }
40
+
41
+ function MetricSelect({
42
+ metrics,
43
+ name = 'metric_id',
44
+ selectedMetric,
45
+ }: {
46
+ metrics: TaskProgressMetric[];
47
+ name?: string;
48
+ selectedMetric: TaskProgressMetric | null;
49
+ }) {
50
+ return (
51
+ <select
52
+ className="h-10 rounded-md border bg-background px-3 text-sm"
53
+ defaultValue={selectedMetric?.id}
54
+ name={name}
55
+ required
56
+ >
57
+ {metrics.map((metric) => (
58
+ <option key={metric.id} value={metric.id}>
59
+ {metric.name}
60
+ </option>
61
+ ))}
62
+ </select>
63
+ );
64
+ }
65
+
66
+ export function ProgressPanel(props: {
67
+ createEntryMutation: UseMutationResult<any, unknown, FormData>;
68
+ createMetricMutation: UseMutationResult<any, unknown, FormData>;
69
+ entries: any[];
70
+ metrics: TaskProgressMetric[];
71
+ selectedMetric: TaskProgressMetric | null;
72
+ t: Translate;
73
+ }) {
74
+ const {
75
+ createEntryMutation,
76
+ createMetricMutation,
77
+ entries,
78
+ metrics,
79
+ selectedMetric,
80
+ t,
81
+ } = props;
82
+
83
+ return (
84
+ <div className="grid gap-4 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)]">
85
+ <Card>
86
+ <CardHeader>
87
+ <CardTitle>{t('progress.log_entry')}</CardTitle>
88
+ </CardHeader>
89
+ <CardContent>
90
+ <form
91
+ className="grid gap-3"
92
+ onSubmit={(event) => {
93
+ event.preventDefault();
94
+ createEntryMutation.mutate(new FormData(event.currentTarget));
95
+ event.currentTarget.reset();
96
+ }}
97
+ >
98
+ <MetricSelect metrics={metrics} selectedMetric={selectedMetric} />
99
+ <Input defaultValue={today()} name="entry_date" type="date" />
100
+ <Input
101
+ name="value"
102
+ placeholder={t('fields.value')}
103
+ required
104
+ type="number"
105
+ />
106
+ <Input name="tags" placeholder={t('fields.tags')} />
107
+ <Textarea name="note" placeholder={t('fields.note')} />
108
+ <Button disabled={!selectedMetric || createEntryMutation.isPending}>
109
+ <Plus className="mr-2 h-4 w-4" />
110
+ {t('actions.add_entry')}
111
+ </Button>
112
+ </form>
113
+ <form
114
+ className="mt-6 grid gap-3 border-t pt-4"
115
+ onSubmit={(event) => {
116
+ event.preventDefault();
117
+ createMetricMutation.mutate(new FormData(event.currentTarget));
118
+ event.currentTarget.reset();
119
+ }}
120
+ >
121
+ <Input name="name" placeholder={t('fields.metric_name')} required />
122
+ <Input name="unit_label" placeholder={t('fields.unit')} required />
123
+ <Button disabled={createMetricMutation.isPending} variant="outline">
124
+ {t('actions.add_metric')}
125
+ </Button>
126
+ </form>
127
+ </CardContent>
128
+ </Card>
129
+ <Card>
130
+ <CardHeader>
131
+ <CardTitle>{t('progress.recent_entries')}</CardTitle>
132
+ </CardHeader>
133
+ <CardContent className="space-y-3">
134
+ {entries.length === 0 ? (
135
+ <p className="text-muted-foreground text-sm">
136
+ {t('empty.entries')}
137
+ </p>
138
+ ) : (
139
+ entries.map((entry) => (
140
+ <div
141
+ className="flex items-start justify-between gap-3 rounded-md border p-3"
142
+ key={entry.id}
143
+ >
144
+ <div>
145
+ <div className="font-medium">{entry.entry_date}</div>
146
+ <div className="text-muted-foreground text-sm">
147
+ {entry.note || entry.metric?.name}
148
+ </div>
149
+ <div className="mt-2 flex flex-wrap gap-1">
150
+ {(entry.tags ?? []).map((tag: string) => (
151
+ <Badge key={tag} variant="secondary">
152
+ {tag}
153
+ </Badge>
154
+ ))}
155
+ </div>
156
+ </div>
157
+ <div className="text-right font-semibold">
158
+ {Number(entry.value).toLocaleString()}{' '}
159
+ {entry.metric?.unit_label}
160
+ </div>
161
+ </div>
162
+ ))
163
+ )}
164
+ </CardContent>
165
+ </Card>
166
+ </div>
167
+ );
168
+ }
169
+
170
+ export function GoalsPanel(props: {
171
+ createGoalMutation: UseMutationResult<any, unknown, FormData>;
172
+ goals: any[];
173
+ metrics: TaskProgressMetric[];
174
+ selectedMetric: TaskProgressMetric | null;
175
+ t: Translate;
176
+ }) {
177
+ const { createGoalMutation, goals, metrics, selectedMetric, t } = props;
178
+
179
+ return (
180
+ <div className="grid gap-4 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)]">
181
+ <Card>
182
+ <CardHeader>
183
+ <CardTitle>{t('goals.create_goal')}</CardTitle>
184
+ </CardHeader>
185
+ <CardContent>
186
+ <form
187
+ className="grid gap-3"
188
+ onSubmit={(event) => {
189
+ event.preventDefault();
190
+ createGoalMutation.mutate(new FormData(event.currentTarget));
191
+ event.currentTarget.reset();
192
+ }}
193
+ >
194
+ <Input name="name" placeholder={t('fields.goal_name')} required />
195
+ <MetricSelect metrics={metrics} selectedMetric={selectedMetric} />
196
+ <Input
197
+ name="target_value"
198
+ placeholder={t('fields.target')}
199
+ required
200
+ type="number"
201
+ />
202
+ <Input defaultValue={today()} name="period_start" type="date" />
203
+ <Input name="period_end" type="date" />
204
+ <select
205
+ className="h-10 rounded-md border bg-background px-3 text-sm"
206
+ name="goal_type"
207
+ >
208
+ <option value="target">{t('goal_types.target')}</option>
209
+ <option value="habit">{t('goal_types.habit')}</option>
210
+ </select>
211
+ <Button disabled={!selectedMetric || createGoalMutation.isPending}>
212
+ <Target className="mr-2 h-4 w-4" />
213
+ {t('actions.add_goal')}
214
+ </Button>
215
+ </form>
216
+ </CardContent>
217
+ </Card>
218
+ <div className="grid gap-3">
219
+ {goals.length === 0 ? (
220
+ <Card>
221
+ <CardContent className="py-8 text-sm">
222
+ {t('empty.goals')}
223
+ </CardContent>
224
+ </Card>
225
+ ) : (
226
+ goals.map((goal) => (
227
+ <Card key={goal.id}>
228
+ <CardContent className="space-y-3 py-4">
229
+ <div className="flex items-start justify-between gap-3">
230
+ <div>
231
+ <div className="font-semibold">{goal.name}</div>
232
+ <div className="text-muted-foreground text-sm">
233
+ {goal.period_start}
234
+ {goal.period_end ? ` - ${goal.period_end}` : ''}
235
+ </div>
236
+ </div>
237
+ <Badge variant="secondary">{goal.goal_type}</Badge>
238
+ </div>
239
+ <div className="h-2 overflow-hidden rounded-full bg-muted">
240
+ <div
241
+ className="h-full bg-dynamic-green"
242
+ style={{
243
+ width: `${Math.min(Number(goal.percent ?? 0), 100)}%`,
244
+ }}
245
+ />
246
+ </div>
247
+ <div className="text-muted-foreground text-sm">
248
+ {Number(goal.progress ?? 0).toLocaleString()} /{' '}
249
+ {Number(goal.target_value ?? 0).toLocaleString()}{' '}
250
+ {goal.metric?.unit_label}
251
+ </div>
252
+ </CardContent>
253
+ </Card>
254
+ ))
255
+ )}
256
+ </div>
257
+ </div>
258
+ );
259
+ }
260
+
261
+ export function StatsPanel({ stats, t }: { stats: any; t: Translate }) {
262
+ const maxValue = Math.max(
263
+ ...(stats?.daily ?? []).map((day: any) => day.value),
264
+ 1
265
+ );
266
+
267
+ return (
268
+ <Card>
269
+ <CardHeader>
270
+ <CardTitle>{t('stats.daily_progress')}</CardTitle>
271
+ </CardHeader>
272
+ <CardContent className="space-y-4">
273
+ {(stats?.daily ?? []).length === 0 ? (
274
+ <p className="text-muted-foreground text-sm">{t('empty.stats')}</p>
275
+ ) : (
276
+ <div className="space-y-2">
277
+ {stats.daily.slice(-30).map((day: any) => (
278
+ <div
279
+ className="grid grid-cols-[6.5rem_1fr_5rem] items-center gap-3"
280
+ key={day.date}
281
+ >
282
+ <div className="text-muted-foreground text-xs">{day.date}</div>
283
+ <div className="h-2 overflow-hidden rounded-full bg-muted">
284
+ <div
285
+ className="h-full bg-dynamic-blue"
286
+ style={{
287
+ width: `${(Number(day.value) / maxValue) * 100}%`,
288
+ }}
289
+ />
290
+ </div>
291
+ <div className="text-right text-sm">
292
+ {Number(day.value).toLocaleString()}
293
+ </div>
294
+ </div>
295
+ ))}
296
+ </div>
297
+ )}
298
+ </CardContent>
299
+ </Card>
300
+ );
301
+ }
@@ -23,6 +23,11 @@ import { useOptionalWorkspacePresenceContext } from './workspace-presence-provid
23
23
 
24
24
  export type { PendingRelationship, PendingRelationshipType };
25
25
 
26
+ export type TaskAssigneeMemberSource =
27
+ | 'workspace'
28
+ | 'board'
29
+ | 'workspace-and-board';
30
+
26
31
  type WorkspaceLabelSummary = {
27
32
  id: string;
28
33
  name: string | null;
@@ -57,6 +62,10 @@ interface TaskDialogState {
57
62
  taskWsId?: string;
58
63
  /** Whether the task's workspace is personal (affects realtime/presence decisions) */
59
64
  taskWorkspacePersonal?: boolean;
65
+ /** Whether the board context should expose assignee controls. */
66
+ canUseBoardAssignees?: boolean;
67
+ /** Where assignee candidates should be loaded from. */
68
+ assigneeMemberSource?: TaskAssigneeMemberSource;
60
69
  /** The task workspace tier used to gate cursor tracking for edit mode */
61
70
  taskWorkspaceTier?: WorkspaceProductTier;
62
71
  /** Initial board/list context used for immediate partial-task rendering. */
@@ -78,6 +87,8 @@ interface OpenTaskByIdOptions {
78
87
  taskWsId?: string;
79
88
  taskWorkspacePersonal?: boolean;
80
89
  taskWorkspaceTier?: WorkspaceProductTier;
90
+ canUseBoardAssignees?: boolean;
91
+ assigneeMemberSource?: TaskAssigneeMemberSource;
81
92
  initialSharedContext?: SharedTaskContext;
82
93
  }
83
94
 
@@ -100,6 +111,10 @@ interface TaskDialogContextValue {
100
111
  taskWsId?: string;
101
112
  /** Whether the task's workspace is personal (affects realtime features) */
102
113
  taskWorkspacePersonal?: boolean;
114
+ /** Whether the board context should expose assignee controls */
115
+ canUseBoardAssignees?: boolean;
116
+ /** Where assignee candidates should be loaded from */
117
+ assigneeMemberSource?: TaskAssigneeMemberSource;
103
118
  /** The task's workspace tier (affects cursor tracking) */
104
119
  taskWorkspaceTier?: WorkspaceProductTier;
105
120
  }
@@ -383,6 +398,8 @@ export function TaskDialogProvider({
383
398
  preserveUrl?: boolean;
384
399
  taskWsId?: string;
385
400
  taskWorkspacePersonal?: boolean;
401
+ canUseBoardAssignees?: boolean;
402
+ assigneeMemberSource?: TaskAssigneeMemberSource;
386
403
  taskWorkspaceTier?: WorkspaceProductTier;
387
404
  }
388
405
  ) => {
@@ -408,6 +425,9 @@ export function TaskDialogProvider({
408
425
  fakeTaskUrl,
409
426
  taskWsId: options?.taskWsId,
410
427
  taskWorkspacePersonal: isTaskWorkspacePersonal,
428
+ canUseBoardAssignees:
429
+ options?.canUseBoardAssignees ?? !isTaskWorkspacePersonal,
430
+ assigneeMemberSource: options?.assigneeMemberSource,
411
431
  taskWorkspaceTier: options?.taskWorkspaceTier,
412
432
  });
413
433
  },
@@ -461,6 +481,9 @@ export function TaskDialogProvider({
461
481
  fakeTaskUrl: options?.fakeTaskUrl,
462
482
  taskWsId: options?.taskWsId,
463
483
  taskWorkspacePersonal: initialTaskWorkspacePersonal,
484
+ canUseBoardAssignees:
485
+ options?.canUseBoardAssignees ?? !initialTaskWorkspacePersonal,
486
+ assigneeMemberSource: options?.assigneeMemberSource,
464
487
  taskWorkspaceTier: options?.taskWorkspaceTier,
465
488
  initialSharedContext: options?.initialSharedContext,
466
489
  isHydratingTask: true,
@@ -540,6 +563,9 @@ export function TaskDialogProvider({
540
563
  fakeTaskUrl: options?.fakeTaskUrl,
541
564
  taskWsId,
542
565
  taskWorkspacePersonal: isTaskWorkspacePersonal,
566
+ canUseBoardAssignees:
567
+ options?.canUseBoardAssignees ?? !isTaskWorkspacePersonal,
568
+ assigneeMemberSource: options?.assigneeMemberSource,
543
569
  taskWorkspaceTier,
544
570
  isHydratingTask: false,
545
571
  taskLoadError: false,
@@ -1,9 +1,19 @@
1
1
  import '@testing-library/jest-dom';
2
2
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3
- import { render } from '@testing-library/react';
3
+ import {
4
+ act,
5
+ fireEvent,
6
+ render,
7
+ screen,
8
+ waitFor,
9
+ } from '@testing-library/react';
4
10
  import type React from 'react';
11
+ import { createRef } from 'react';
5
12
  import { beforeEach, describe, expect, it, vi } from 'vitest';
6
- import { AssigneeSelect } from '../assignee-select';
13
+ import { AssigneeSelect, type AssigneeSelectHandle } from '../assignee-select';
14
+
15
+ const listWorkspaceTaskBoardViewableMembersMock = vi.fn();
16
+ const updateWorkspaceTaskMock = vi.fn();
7
17
 
8
18
  vi.mock('next-intl', () => ({
9
19
  useTranslations: () => (key: string) => key,
@@ -25,7 +35,11 @@ vi.mock('@tuturuuu/ui/hooks/use-workspace-members', () => ({
25
35
  }));
26
36
 
27
37
  vi.mock('@tuturuuu/internal-api/tasks', () => ({
28
- updateWorkspaceTask: vi.fn(),
38
+ listWorkspaceTaskBoardViewableMembers: (
39
+ ...args: Parameters<typeof listWorkspaceTaskBoardViewableMembersMock>
40
+ ) => listWorkspaceTaskBoardViewableMembersMock(...args),
41
+ updateWorkspaceTask: (...args: Parameters<typeof updateWorkspaceTaskMock>) =>
42
+ updateWorkspaceTaskMock(...args),
29
43
  }));
30
44
 
31
45
  vi.mock('../board-broadcast-context', () => ({
@@ -35,9 +49,18 @@ vi.mock('../board-broadcast-context', () => ({
35
49
  describe('AssigneeSelect', () => {
36
50
  beforeEach(() => {
37
51
  vi.clearAllMocks();
52
+ listWorkspaceTaskBoardViewableMembersMock.mockResolvedValue({
53
+ members: [],
54
+ });
55
+ updateWorkspaceTaskMock.mockResolvedValue({});
38
56
  });
39
57
 
40
- it('does not enter a render loop when rerendered with equivalent assignees', () => {
58
+ function renderWithQueryClient(
59
+ props: React.ComponentProps<typeof AssigneeSelect> = {
60
+ taskId: 'task-1',
61
+ },
62
+ ref?: React.Ref<AssigneeSelectHandle>
63
+ ) {
41
64
  const queryClient = new QueryClient({
42
65
  defaultOptions: {
43
66
  queries: {
@@ -46,6 +69,14 @@ describe('AssigneeSelect', () => {
46
69
  },
47
70
  });
48
71
 
72
+ return render(
73
+ <QueryClientProvider client={queryClient}>
74
+ <AssigneeSelect {...props} ref={ref} />
75
+ </QueryClientProvider>
76
+ );
77
+ }
78
+
79
+ it('does not enter a render loop when rerendered with equivalent assignees', () => {
49
80
  const assignees: React.ComponentProps<typeof AssigneeSelect>['assignees'] =
50
81
  [
51
82
  {
@@ -55,18 +86,58 @@ describe('AssigneeSelect', () => {
55
86
  },
56
87
  ];
57
88
 
58
- const { rerender } = render(
59
- <QueryClientProvider client={queryClient}>
60
- <AssigneeSelect taskId="task-1" assignees={assignees} />
61
- </QueryClientProvider>
62
- );
89
+ const { rerender } = renderWithQueryClient({
90
+ assignees,
91
+ taskId: 'task-1',
92
+ });
63
93
 
64
94
  expect(() =>
65
95
  rerender(
66
- <QueryClientProvider client={queryClient}>
96
+ <QueryClientProvider client={new QueryClient()}>
67
97
  <AssigneeSelect taskId="task-1" assignees={[{ ...assignees[0]! }]} />
68
98
  </QueryClientProvider>
69
99
  )
70
100
  ).not.toThrow();
71
101
  });
102
+
103
+ it('uses board viewable members so direct guests can be assigned', async () => {
104
+ listWorkspaceTaskBoardViewableMembersMock.mockResolvedValue({
105
+ members: [
106
+ {
107
+ avatar_url: null,
108
+ display_name: 'Board Guest',
109
+ email: 'guest@example.com',
110
+ handle: null,
111
+ id: 'guest-user-1',
112
+ is_creator: false,
113
+ roles: [],
114
+ user_id: 'guest-user-1',
115
+ workspace_member_type: 'GUEST',
116
+ },
117
+ ],
118
+ });
119
+
120
+ const assigneeSelectRef = createRef<AssigneeSelectHandle>();
121
+ renderWithQueryClient({ taskId: 'task-1' }, assigneeSelectRef);
122
+
123
+ await waitFor(() => {
124
+ expect(listWorkspaceTaskBoardViewableMembersMock).toHaveBeenCalledWith(
125
+ 'ws-1',
126
+ 'board-1'
127
+ );
128
+ });
129
+ act(() => {
130
+ assigneeSelectRef.current?.open();
131
+ });
132
+
133
+ expect(await screen.findByText('Board Guest')).toBeInTheDocument();
134
+
135
+ fireEvent.click(screen.getByText('Board Guest'));
136
+
137
+ await waitFor(() => {
138
+ expect(updateWorkspaceTaskMock).toHaveBeenCalledWith('ws-1', 'task-1', {
139
+ assignee_ids: ['guest-user-1'],
140
+ });
141
+ });
142
+ });
72
143
  });
@@ -12,6 +12,7 @@ const useWorkspaceLabelsMock = vi.fn();
12
12
  const getWorkspaceTaskBoardMock = vi.fn();
13
13
  const listWorkspaceTasksMock = vi.fn();
14
14
  const useProgressiveBoardLoaderMock = vi.fn();
15
+ const useBoardRealtimeMock = vi.fn();
15
16
  const revalidateLoadedListsMock = vi.fn();
16
17
 
17
18
  vi.mock('@tuturuuu/internal-api/tasks', () => ({
@@ -25,7 +26,10 @@ vi.mock('@tuturuuu/utils/task-helper', () => ({
25
26
  }));
26
27
 
27
28
  vi.mock('@tuturuuu/ui/hooks/useBoardRealtime', () => ({
28
- useBoardRealtime: () => ({ broadcast: null }),
29
+ useBoardRealtime: (...args: unknown[]) => {
30
+ useBoardRealtimeMock(...args);
31
+ return { broadcast: null };
32
+ },
29
33
  }));
30
34
 
31
35
  vi.mock('next/navigation', () => ({
@@ -78,6 +82,7 @@ describe('BoardClient', () => {
78
82
  ],
79
83
  },
80
84
  });
85
+ useBoardRealtimeMock.mockReset();
81
86
  useProgressiveBoardLoaderMock.mockReset();
82
87
  revalidateLoadedListsMock.mockReset();
83
88
  revalidateLoadedListsMock.mockResolvedValue(undefined);
@@ -115,6 +120,98 @@ describe('BoardClient', () => {
115
120
  );
116
121
  });
117
122
 
123
+ it('refreshes board task cache without relationship summaries', async () => {
124
+ const queryClient = new QueryClient({
125
+ defaultOptions: {
126
+ queries: {
127
+ retry: false,
128
+ },
129
+ },
130
+ });
131
+
132
+ render(
133
+ <QueryClientProvider client={queryClient}>
134
+ <BoardClient
135
+ boardId="board-1"
136
+ workspace={{ id: 'workspace-uuid', personal: false } as any}
137
+ currentUserId="user-1"
138
+ />
139
+ </QueryClientProvider>
140
+ );
141
+
142
+ expect(await screen.findByTestId('board-views')).toBeInTheDocument();
143
+ await waitFor(() => {
144
+ expect(getActiveBoardRefresh()).toBeInstanceOf(Function);
145
+ });
146
+
147
+ await act(async () => {
148
+ getActiveBoardRefresh()?.();
149
+ });
150
+
151
+ await waitFor(() => {
152
+ expect(listWorkspaceTasksMock).toHaveBeenCalledWith('board-ws-uuid', {
153
+ boardId: 'board-1',
154
+ includeRelationshipSummary: false,
155
+ });
156
+ });
157
+ });
158
+
159
+ it('uses the shared task board loading state while the board query resolves', () => {
160
+ getWorkspaceTaskBoardMock.mockReturnValue(new Promise(() => {}));
161
+ const queryClient = new QueryClient({
162
+ defaultOptions: {
163
+ queries: {
164
+ retry: false,
165
+ },
166
+ },
167
+ });
168
+
169
+ render(
170
+ <QueryClientProvider client={queryClient}>
171
+ <BoardClient
172
+ boardId="board-1"
173
+ workspace={{ id: 'workspace-uuid', personal: false } as any}
174
+ currentUserId="user-1"
175
+ />
176
+ </QueryClientProvider>
177
+ );
178
+
179
+ expect(screen.getByTestId('task-board-loading-state')).toBeInTheDocument();
180
+ expect(screen.getByTestId('task-board-loading-state')).not.toHaveClass(
181
+ '-m-4'
182
+ );
183
+ expect(screen.getByTestId('kanban-skeleton')).toBeInTheDocument();
184
+ expect(screen.queryByText('Loading board...')).not.toBeInTheDocument();
185
+ });
186
+
187
+ it('can render the shared task board loading state as a full-bleed route root', () => {
188
+ getWorkspaceTaskBoardMock.mockReturnValue(new Promise(() => {}));
189
+ const queryClient = new QueryClient({
190
+ defaultOptions: {
191
+ queries: {
192
+ retry: false,
193
+ },
194
+ },
195
+ });
196
+
197
+ render(
198
+ <QueryClientProvider client={queryClient}>
199
+ <BoardClient
200
+ boardId="board-1"
201
+ workspace={{ id: 'workspace-uuid', personal: false } as any}
202
+ currentUserId="user-1"
203
+ rootLoading
204
+ />
205
+ </QueryClientProvider>
206
+ );
207
+
208
+ expect(screen.getByTestId('task-board-loading-state')).toHaveClass(
209
+ '-m-4',
210
+ 'h-[calc(100dvh+2rem)]',
211
+ 'w-[calc(100%+2rem)]'
212
+ );
213
+ });
214
+
118
215
  it('can revalidate loaded board lists without invalidating visible task caches', async () => {
119
216
  const queryClient = new QueryClient({
120
217
  defaultOptions: {
@@ -154,6 +251,49 @@ describe('BoardClient', () => {
154
251
  expect(revalidateLoadedListsMock).toHaveBeenCalledTimes(1);
155
252
  });
156
253
 
254
+ it('revalidates loaded lists for relation broadcasts without invalidating visible task caches', async () => {
255
+ const queryClient = new QueryClient({
256
+ defaultOptions: {
257
+ queries: {
258
+ retry: false,
259
+ },
260
+ },
261
+ });
262
+ const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
263
+
264
+ render(
265
+ <QueryClientProvider client={queryClient}>
266
+ <BoardClient
267
+ boardId="board-1"
268
+ workspace={{ id: 'workspace-uuid', personal: false } as any}
269
+ currentUserId="user-1"
270
+ />
271
+ </QueryClientProvider>
272
+ );
273
+
274
+ expect(await screen.findByTestId('board-views')).toBeInTheDocument();
275
+
276
+ const realtimeOptions = useBoardRealtimeMock.mock.calls.find(
277
+ ([boardId]) => boardId === 'board-1'
278
+ )?.[1] as
279
+ | {
280
+ onTaskRelationsChange?: (taskIds: string[]) => void;
281
+ }
282
+ | undefined;
283
+
284
+ await act(async () => {
285
+ realtimeOptions?.onTaskRelationsChange?.(['task-1']);
286
+ });
287
+
288
+ expect(invalidateSpy).not.toHaveBeenCalledWith({
289
+ queryKey: ['tasks', 'board-1'],
290
+ });
291
+ expect(invalidateSpy).not.toHaveBeenCalledWith({
292
+ queryKey: ['tasks-full', 'board-1'],
293
+ });
294
+ expect(revalidateLoadedListsMock).toHaveBeenCalledTimes(1);
295
+ });
296
+
157
297
  it('throttles focus-driven list revalidation for thirty seconds', async () => {
158
298
  const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(100_000);
159
299
  const queryClient = new QueryClient({