@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,394 @@
1
+ import type { QueryClient, QueryKey } from '@tanstack/react-query';
2
+ import type { Task } from '@tuturuuu/types/primitives/Task';
3
+
4
+ type WorkspaceTaskCache = {
5
+ task?: Task | null;
6
+ };
7
+
8
+ type MyTasksCache = {
9
+ overdue?: Task[];
10
+ today?: Task[];
11
+ upcoming?: Task[];
12
+ completed?: Task[];
13
+ };
14
+
15
+ type MyCompletedTasksCache = {
16
+ pages?: Array<{
17
+ completed?: Task[];
18
+ }>;
19
+ };
20
+
21
+ export type VisibleTaskCacheSnapshot = {
22
+ boardTaskEntries: [QueryKey, Task[] | undefined][];
23
+ taskEntries: [QueryKey, Task | undefined][];
24
+ workspaceTaskEntries: [QueryKey, WorkspaceTaskCache | undefined][];
25
+ myTasksEntries: [QueryKey, MyTasksCache | undefined][];
26
+ myCompletedTaskEntries: [QueryKey, MyCompletedTasksCache | undefined][];
27
+ };
28
+
29
+ function isBoardTaskQuery(queryKey: QueryKey, boardId: string) {
30
+ return (
31
+ Array.isArray(queryKey) &&
32
+ (queryKey[0] === 'tasks' || queryKey[0] === 'tasks-full') &&
33
+ queryKey[1] === boardId
34
+ );
35
+ }
36
+
37
+ function isTaskDetailQuery(queryKey: QueryKey, taskIds: Set<string>) {
38
+ return (
39
+ Array.isArray(queryKey) &&
40
+ queryKey[0] === 'task' &&
41
+ typeof queryKey[1] === 'string' &&
42
+ taskIds.has(queryKey[1])
43
+ );
44
+ }
45
+
46
+ function isWorkspaceTaskQuery(queryKey: QueryKey, taskIds: Set<string>) {
47
+ return (
48
+ Array.isArray(queryKey) &&
49
+ queryKey[0] === 'workspaceTask' &&
50
+ typeof queryKey[2] === 'string' &&
51
+ taskIds.has(queryKey[2])
52
+ );
53
+ }
54
+
55
+ function patchTaskArray<T extends { id?: string }>(
56
+ tasks: T[] | undefined,
57
+ taskIds: Set<string>,
58
+ updater: (task: Task) => Task
59
+ ): T[] | undefined {
60
+ if (!tasks) return tasks;
61
+
62
+ let changed = false;
63
+ const nextTasks = tasks.map((task) => {
64
+ if (!task.id || !taskIds.has(task.id)) {
65
+ return task;
66
+ }
67
+
68
+ const updatedTask = updater(task as unknown as Task) as unknown as T;
69
+ if (updatedTask !== task) {
70
+ changed = true;
71
+ }
72
+
73
+ return updatedTask;
74
+ });
75
+
76
+ return changed ? nextTasks : tasks;
77
+ }
78
+
79
+ function restoreTaskArrayFromSnapshot<T extends { id?: string }>(
80
+ currentTasks: T[] | undefined,
81
+ previousTasks: T[] | undefined,
82
+ taskIds: Set<string>
83
+ ): T[] | undefined {
84
+ if (!currentTasks || !previousTasks) return currentTasks;
85
+
86
+ const previousById = new Map(
87
+ previousTasks
88
+ .filter((task) => task.id && taskIds.has(task.id))
89
+ .map((task) => [task.id, task])
90
+ );
91
+ if (previousById.size === 0) return currentTasks;
92
+
93
+ let changed = false;
94
+ const restoredTasks = currentTasks.map((task) => {
95
+ if (!task.id) return task;
96
+ const previousTask = previousById.get(task.id);
97
+ if (!previousTask) return task;
98
+
99
+ changed = true;
100
+ return previousTask;
101
+ });
102
+
103
+ return changed ? restoredTasks : currentTasks;
104
+ }
105
+
106
+ function patchMyTasksCache(
107
+ cache: MyTasksCache | undefined,
108
+ taskIds: Set<string>,
109
+ updater: (task: Task) => Task
110
+ ) {
111
+ if (!cache) return cache;
112
+
113
+ return {
114
+ ...cache,
115
+ overdue: patchTaskArray(cache.overdue, taskIds, updater) ?? cache.overdue,
116
+ today: patchTaskArray(cache.today, taskIds, updater) ?? cache.today,
117
+ upcoming:
118
+ patchTaskArray(cache.upcoming, taskIds, updater) ?? cache.upcoming,
119
+ completed:
120
+ patchTaskArray(cache.completed, taskIds, updater) ?? cache.completed,
121
+ };
122
+ }
123
+
124
+ function patchMyCompletedTasksCache(
125
+ cache: MyCompletedTasksCache | undefined,
126
+ taskIds: Set<string>,
127
+ updater: (task: Task) => Task
128
+ ) {
129
+ if (!cache?.pages) return cache;
130
+
131
+ return {
132
+ ...cache,
133
+ pages: cache.pages.map((page) => ({
134
+ ...page,
135
+ completed:
136
+ patchTaskArray(page.completed, taskIds, updater) ?? page.completed,
137
+ })),
138
+ };
139
+ }
140
+
141
+ function restoreMyTasksCacheFromSnapshot(
142
+ current: MyTasksCache | undefined,
143
+ previous: MyTasksCache | undefined,
144
+ taskIds: Set<string>
145
+ ) {
146
+ if (!current || !previous) return current;
147
+
148
+ return {
149
+ ...current,
150
+ overdue:
151
+ restoreTaskArrayFromSnapshot(
152
+ current.overdue,
153
+ previous.overdue,
154
+ taskIds
155
+ ) ?? current.overdue,
156
+ today:
157
+ restoreTaskArrayFromSnapshot(current.today, previous.today, taskIds) ??
158
+ current.today,
159
+ upcoming:
160
+ restoreTaskArrayFromSnapshot(
161
+ current.upcoming,
162
+ previous.upcoming,
163
+ taskIds
164
+ ) ?? current.upcoming,
165
+ completed:
166
+ restoreTaskArrayFromSnapshot(
167
+ current.completed,
168
+ previous.completed,
169
+ taskIds
170
+ ) ?? current.completed,
171
+ };
172
+ }
173
+
174
+ function restoreMyCompletedTasksCacheFromSnapshot(
175
+ current: MyCompletedTasksCache | undefined,
176
+ previous: MyCompletedTasksCache | undefined,
177
+ taskIds: Set<string>
178
+ ) {
179
+ if (!current?.pages || !previous?.pages) return current;
180
+
181
+ return {
182
+ ...current,
183
+ pages: current.pages.map((page, index) => ({
184
+ ...page,
185
+ completed:
186
+ restoreTaskArrayFromSnapshot(
187
+ page.completed,
188
+ previous.pages?.[index]?.completed,
189
+ taskIds
190
+ ) ?? page.completed,
191
+ })),
192
+ };
193
+ }
194
+
195
+ export function getTaskFromVisibleCaches({
196
+ queryClient,
197
+ boardId,
198
+ taskId,
199
+ fallback,
200
+ }: {
201
+ queryClient: QueryClient;
202
+ boardId: string;
203
+ taskId: string;
204
+ fallback?: Task;
205
+ }): Task | undefined {
206
+ const taskDetail = queryClient.getQueryData<Task>(['task', taskId]);
207
+ if (taskDetail) return taskDetail;
208
+
209
+ const workspaceTaskEntries = queryClient.getQueriesData<WorkspaceTaskCache>({
210
+ predicate: (query) =>
211
+ isWorkspaceTaskQuery(query.queryKey, new Set([taskId])),
212
+ });
213
+ for (const [, entry] of workspaceTaskEntries) {
214
+ if (entry?.task) return entry.task;
215
+ }
216
+
217
+ const boardTaskEntries = queryClient.getQueriesData<Task[]>({
218
+ predicate: (query) => isBoardTaskQuery(query.queryKey, boardId),
219
+ });
220
+ for (const [, tasks] of boardTaskEntries) {
221
+ const task = tasks?.find((entry) => entry.id === taskId);
222
+ if (task) return task;
223
+ }
224
+
225
+ return fallback;
226
+ }
227
+
228
+ export function patchTasksInVisibleCaches({
229
+ queryClient,
230
+ boardId,
231
+ taskIds,
232
+ updater,
233
+ }: {
234
+ queryClient: QueryClient;
235
+ boardId: string;
236
+ taskIds: string[];
237
+ updater: (task: Task) => Task;
238
+ }) {
239
+ const taskIdSet = new Set(taskIds);
240
+ if (taskIdSet.size === 0) return;
241
+
242
+ queryClient.setQueriesData<Task[]>(
243
+ {
244
+ predicate: (query) => isBoardTaskQuery(query.queryKey, boardId),
245
+ },
246
+ (old) => patchTaskArray(old, taskIdSet, updater)
247
+ );
248
+
249
+ queryClient.setQueriesData<Task>(
250
+ {
251
+ predicate: (query) => isTaskDetailQuery(query.queryKey, taskIdSet),
252
+ },
253
+ (old) => (old ? updater(old) : old)
254
+ );
255
+
256
+ queryClient.setQueriesData<WorkspaceTaskCache>(
257
+ {
258
+ predicate: (query) => isWorkspaceTaskQuery(query.queryKey, taskIdSet),
259
+ },
260
+ (old) =>
261
+ old?.task
262
+ ? {
263
+ ...old,
264
+ task: updater(old.task),
265
+ }
266
+ : old
267
+ );
268
+
269
+ queryClient.setQueriesData<MyTasksCache>({ queryKey: ['my-tasks'] }, (old) =>
270
+ patchMyTasksCache(old, taskIdSet, updater)
271
+ );
272
+
273
+ queryClient.setQueriesData<MyCompletedTasksCache>(
274
+ { queryKey: ['my-completed-tasks'] },
275
+ (old) => patchMyCompletedTasksCache(old, taskIdSet, updater)
276
+ );
277
+ }
278
+
279
+ export function patchTaskInVisibleCaches({
280
+ queryClient,
281
+ boardId,
282
+ taskId,
283
+ updater,
284
+ }: {
285
+ queryClient: QueryClient;
286
+ boardId: string;
287
+ taskId: string;
288
+ updater: (task: Task) => Task;
289
+ }) {
290
+ patchTasksInVisibleCaches({
291
+ queryClient,
292
+ boardId,
293
+ taskIds: [taskId],
294
+ updater,
295
+ });
296
+ }
297
+
298
+ export function snapshotVisibleTaskCaches(
299
+ queryClient: QueryClient,
300
+ boardId: string,
301
+ taskIds: string[]
302
+ ): VisibleTaskCacheSnapshot {
303
+ const taskIdSet = new Set(taskIds);
304
+
305
+ return {
306
+ boardTaskEntries: queryClient.getQueriesData<Task[]>({
307
+ predicate: (query) => isBoardTaskQuery(query.queryKey, boardId),
308
+ }),
309
+ taskEntries: queryClient.getQueriesData<Task>({
310
+ predicate: (query) => isTaskDetailQuery(query.queryKey, taskIdSet),
311
+ }),
312
+ workspaceTaskEntries: queryClient.getQueriesData<WorkspaceTaskCache>({
313
+ predicate: (query) => isWorkspaceTaskQuery(query.queryKey, taskIdSet),
314
+ }),
315
+ myTasksEntries: queryClient.getQueriesData<MyTasksCache>({
316
+ queryKey: ['my-tasks'],
317
+ }),
318
+ myCompletedTaskEntries: queryClient.getQueriesData<MyCompletedTasksCache>({
319
+ queryKey: ['my-completed-tasks'],
320
+ }),
321
+ };
322
+ }
323
+
324
+ export function restoreVisibleTaskCaches(
325
+ queryClient: QueryClient,
326
+ snapshot: VisibleTaskCacheSnapshot
327
+ ) {
328
+ for (const [queryKey, data] of snapshot.boardTaskEntries) {
329
+ queryClient.setQueryData(queryKey, data);
330
+ }
331
+
332
+ for (const [queryKey, data] of snapshot.taskEntries) {
333
+ queryClient.setQueryData(queryKey, data);
334
+ }
335
+
336
+ for (const [queryKey, data] of snapshot.workspaceTaskEntries) {
337
+ queryClient.setQueryData(queryKey, data);
338
+ }
339
+
340
+ for (const [queryKey, data] of snapshot.myTasksEntries) {
341
+ queryClient.setQueryData(queryKey, data);
342
+ }
343
+
344
+ for (const [queryKey, data] of snapshot.myCompletedTaskEntries) {
345
+ queryClient.setQueryData(queryKey, data);
346
+ }
347
+ }
348
+
349
+ export function restoreTasksFromVisibleCacheSnapshot({
350
+ queryClient,
351
+ snapshot,
352
+ taskIds,
353
+ }: {
354
+ queryClient: QueryClient;
355
+ snapshot: VisibleTaskCacheSnapshot;
356
+ taskIds: string[];
357
+ }) {
358
+ const taskIdSet = new Set(taskIds);
359
+ if (taskIdSet.size === 0) return;
360
+
361
+ for (const [queryKey, previousTasks] of snapshot.boardTaskEntries) {
362
+ queryClient.setQueryData<Task[]>(queryKey, (currentTasks) =>
363
+ restoreTaskArrayFromSnapshot(currentTasks, previousTasks, taskIdSet)
364
+ );
365
+ }
366
+
367
+ for (const [queryKey, previousTask] of snapshot.taskEntries) {
368
+ if (!Array.isArray(queryKey) || typeof queryKey[1] !== 'string') continue;
369
+ if (!taskIdSet.has(queryKey[1])) continue;
370
+ queryClient.setQueryData(queryKey, previousTask);
371
+ }
372
+
373
+ for (const [queryKey, previousEntry] of snapshot.workspaceTaskEntries) {
374
+ if (!Array.isArray(queryKey) || typeof queryKey[2] !== 'string') continue;
375
+ if (!taskIdSet.has(queryKey[2])) continue;
376
+ queryClient.setQueryData(queryKey, previousEntry);
377
+ }
378
+
379
+ for (const [queryKey, previousEntry] of snapshot.myTasksEntries) {
380
+ queryClient.setQueryData<MyTasksCache>(queryKey, (currentEntry) =>
381
+ restoreMyTasksCacheFromSnapshot(currentEntry, previousEntry, taskIdSet)
382
+ );
383
+ }
384
+
385
+ for (const [queryKey, previousEntry] of snapshot.myCompletedTaskEntries) {
386
+ queryClient.setQueryData<MyCompletedTasksCache>(queryKey, (currentEntry) =>
387
+ restoreMyCompletedTasksCacheFromSnapshot(
388
+ currentEntry,
389
+ previousEntry,
390
+ taskIdSet
391
+ )
392
+ );
393
+ }
394
+ }
@@ -189,6 +189,8 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
189
189
  taskWsId: state.taskWsId,
190
190
  taskWorkspacePersonal: state.taskWorkspacePersonal,
191
191
  taskWorkspaceTier: state.taskWorkspaceTier,
192
+ canUseBoardAssignees: state.canUseBoardAssignees,
193
+ assigneeMemberSource: state.assigneeMemberSource,
192
194
  });
193
195
  return;
194
196
  }
@@ -210,6 +212,8 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
210
212
  taskWsId: currentTaskWsId,
211
213
  taskWorkspacePersonal: state.taskWorkspacePersonal,
212
214
  taskWorkspaceTier: state.taskWorkspaceTier,
215
+ canUseBoardAssignees: state.canUseBoardAssignees,
216
+ assigneeMemberSource: state.assigneeMemberSource,
213
217
  });
214
218
  return;
215
219
  }
@@ -223,8 +227,10 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
223
227
  openTask,
224
228
  openTaskById,
225
229
  queryClient,
230
+ state.assigneeMemberSource,
226
231
  state.availableLists,
227
232
  state.boardId,
233
+ state.canUseBoardAssignees,
228
234
  state.task?.id,
229
235
  state.taskWorkspacePersonal,
230
236
  state.taskWorkspaceTier,
@@ -238,9 +244,17 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
238
244
  return openTaskById(taskId, {
239
245
  taskWsId: wsId,
240
246
  taskWorkspacePersonal: isPersonalWorkspace,
247
+ canUseBoardAssignees: state.canUseBoardAssignees,
248
+ assigneeMemberSource: state.assigneeMemberSource,
241
249
  });
242
250
  },
243
- [isPersonalWorkspace, openTaskById, wsId]
251
+ [
252
+ isPersonalWorkspace,
253
+ openTaskById,
254
+ state.assigneeMemberSource,
255
+ state.canUseBoardAssignees,
256
+ wsId,
257
+ ]
244
258
  );
245
259
 
246
260
  useEffect(() => {
@@ -388,10 +402,14 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
388
402
  taskWsId: state.taskWsId,
389
403
  taskWorkspacePersonal: state.taskWorkspacePersonal,
390
404
  taskWorkspaceTier: state.taskWorkspaceTier,
405
+ canUseBoardAssignees: state.canUseBoardAssignees,
406
+ assigneeMemberSource: state.assigneeMemberSource,
391
407
  initialSharedContext: state.initialSharedContext,
392
408
  });
393
409
  }, [
394
410
  openTaskById,
411
+ state.assigneeMemberSource,
412
+ state.canUseBoardAssignees,
395
413
  state.initialSharedContext,
396
414
  state.availableLists,
397
415
  state.boardId,
@@ -539,6 +557,8 @@ export function TaskDialogManager({ wsId }: { wsId: string }) {
539
557
  taskLoadError={state.taskLoadError}
540
558
  taskHydrationVersion={state.taskHydrationVersion}
541
559
  isPersonalWorkspace={isPersonalWorkspace}
560
+ canUseBoardAssignees={state.canUseBoardAssignees}
561
+ assigneeMemberSource={state.assigneeMemberSource}
542
562
  parentTaskId={state.parentTaskId}
543
563
  parentTaskName={state.parentTaskName}
544
564
  pendingRelationship={state.pendingRelationship}
@@ -28,6 +28,8 @@ function updateBodyFadeAttribute(enabled: boolean) {
28
28
  interface QuickSettingsPopoverProps {
29
29
  /** Whether the workspace is personal (forces auto-assign to true) */
30
30
  isPersonalWorkspace?: boolean;
31
+ open?: boolean;
32
+ onOpenChange?: (open: boolean) => void;
31
33
  }
32
34
 
33
35
  /**
@@ -36,6 +38,8 @@ interface QuickSettingsPopoverProps {
36
38
  */
37
39
  export function QuickSettingsPopover({
38
40
  isPersonalWorkspace = false,
41
+ open,
42
+ onOpenChange,
39
43
  }: QuickSettingsPopoverProps) {
40
44
  const t = useTranslations('settings.tasks');
41
45
  const tCommon = useTranslations('common');
@@ -96,7 +100,7 @@ export function QuickSettingsPopover({
96
100
  : (settings?.task_auto_assign_to_self ?? false);
97
101
 
98
102
  return (
99
- <Popover>
103
+ <Popover open={open} onOpenChange={onOpenChange}>
100
104
  <Tooltip>
101
105
  <TooltipTrigger asChild>
102
106
  <PopoverTrigger asChild>
@@ -16,7 +16,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
16
16
  import { cn } from '@tuturuuu/utils/format';
17
17
  import { getTicketIdentifier } from '@tuturuuu/utils/task-helper';
18
18
  import { useTranslations } from 'next-intl';
19
- import type { ReactNode } from 'react';
19
+ import { type ReactNode, useState } from 'react';
20
20
  import { TaskViewerAvatarsComponent } from '../../user-presence-avatars';
21
21
  import { TaskDialogActions } from '../task-dialog-actions';
22
22
  import type {
@@ -268,6 +268,19 @@ export function TaskDialogHeader({
268
268
  onScrollToUserCursor,
269
269
  }: TaskDialogHeaderProps) {
270
270
  const t = useTranslations();
271
+ const [activeHeaderOverlay, setActiveHeaderOverlay] = useState<
272
+ 'quick-settings' | 'more-menu' | null
273
+ >(null);
274
+
275
+ const setHeaderOverlayOpen = (
276
+ overlay: 'quick-settings' | 'more-menu',
277
+ open: boolean
278
+ ) => {
279
+ setActiveHeaderOverlay((currentOverlay) => {
280
+ if (open) return overlay;
281
+ return currentOverlay === overlay ? null : currentOverlay;
282
+ });
283
+ };
271
284
 
272
285
  // Use custom headerInfo if provided, otherwise generate from task context
273
286
  const resolvedHeaderInfo =
@@ -452,7 +465,13 @@ export function TaskDialogHeader({
452
465
 
453
466
  {/* Quick Settings */}
454
467
  {!controlsDisabled && (
455
- <QuickSettingsPopover isPersonalWorkspace={isPersonalWorkspace} />
468
+ <QuickSettingsPopover
469
+ isPersonalWorkspace={isPersonalWorkspace}
470
+ open={activeHeaderOverlay === 'quick-settings'}
471
+ onOpenChange={(open) =>
472
+ setHeaderOverlayOpen('quick-settings', open)
473
+ }
474
+ />
456
475
  )}
457
476
 
458
477
  <TaskDialogActions
@@ -471,6 +490,10 @@ export function TaskDialogHeader({
471
490
  onOpenShareDialog={onOpenShareDialog}
472
491
  disabled={disabled}
473
492
  controlsDisabled={controlsDisabled}
493
+ moreMenuOpen={activeHeaderOverlay === 'more-menu'}
494
+ onMoreMenuOpenChange={(open) =>
495
+ setHeaderOverlayOpen('more-menu', open)
496
+ }
474
497
  />
475
498
 
476
499
  {/* Hide save button in edit mode when realtime is enabled (either cursors or Yjs sync) */}
@@ -21,6 +21,8 @@ interface TaskListSelectorProps {
21
21
  availableLists: TaskList[];
22
22
  disabled?: boolean;
23
23
  compact?: boolean;
24
+ open?: boolean;
25
+ onOpenChange?: (open: boolean) => void;
24
26
  onListChange: (listId: string) => void;
25
27
  }
26
28
 
@@ -31,11 +33,15 @@ export function TaskListSelector({
31
33
  availableLists,
32
34
  disabled = false,
33
35
  compact = false,
36
+ open,
37
+ onOpenChange,
34
38
  onListChange,
35
39
  }: TaskListSelectorProps) {
36
40
  const t = useTranslations();
37
- const [isPopoverOpen, setIsPopoverOpen] = useState(false);
41
+ const [uncontrolledPopoverOpen, setUncontrolledPopoverOpen] = useState(false);
38
42
  const [isCreateListDialogOpen, setIsCreateListDialogOpen] = useState(false);
43
+ const isPopoverOpen = open ?? uncontrolledPopoverOpen;
44
+ const setIsPopoverOpen = onOpenChange ?? setUncontrolledPopoverOpen;
39
45
 
40
46
  const statusLabels = useMemo(
41
47
  () => ({
@@ -373,8 +373,9 @@ function DescriptionDiff({
373
373
  </>
374
374
  );
375
375
 
376
- // If both exist and there are text changes, show the full diff viewer
377
- if (hasSnapshot && hasCurrent && hasTextChanges) {
376
+ // Show the full viewer whenever either side has content so users can inspect
377
+ // content-only, empty/content, and structural editor changes.
378
+ if (hasSnapshot || hasCurrent || hasTextChanges) {
378
379
  return (
379
380
  <div className="text-sm">
380
381
  <div className="flex items-center gap-2">