@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
@@ -38,6 +38,8 @@ interface PersonalOverridesSectionProps {
38
38
  onUpdate?: () => void;
39
39
  }
40
40
 
41
+ type PersonalOverridePopoverId = 'priority' | 'estimation';
42
+
41
43
  export function PersonalOverridesSection({
42
44
  taskId,
43
45
  isCreateMode,
@@ -51,10 +53,22 @@ export function PersonalOverridesSection({
51
53
  onUpdate
52
54
  );
53
55
  const [isExpanded, setIsExpanded] = useState(false);
54
- const [isPriorityOpen, setIsPriorityOpen] = useState(false);
55
- const [isEstimationOpen, setIsEstimationOpen] = useState(false);
56
+ const [activePopover, setActivePopover] =
57
+ useState<PersonalOverridePopoverId | null>(null);
56
58
  const [notes, setNotes] = useState('');
57
59
 
60
+ const isPopoverOpen = (popoverId: PersonalOverridePopoverId) =>
61
+ activePopover === popoverId;
62
+ const setPopoverOpen = (
63
+ popoverId: PersonalOverridePopoverId,
64
+ open: boolean
65
+ ) => {
66
+ setActivePopover((currentPopover) => {
67
+ if (open) return popoverId;
68
+ return currentPopover === popoverId ? null : currentPopover;
69
+ });
70
+ };
71
+
58
72
  // Don't render for new tasks
59
73
  if (isCreateMode || !taskId) return null;
60
74
 
@@ -75,7 +89,7 @@ export function PersonalOverridesSection({
75
89
 
76
90
  const handlePriorityChange = (priority: TaskPriority | null) => {
77
91
  upsert({ priority_override: priority });
78
- setIsPriorityOpen(false);
92
+ setPopoverOpen('priority', false);
79
93
  };
80
94
 
81
95
  const handleDueDateChange = (date: Date | undefined) => {
@@ -86,7 +100,7 @@ export function PersonalOverridesSection({
86
100
 
87
101
  const handleEstimationChange = (points: number | null) => {
88
102
  upsert({ estimation_override: points });
89
- setIsEstimationOpen(false);
103
+ setPopoverOpen('estimation', false);
90
104
  };
91
105
 
92
106
  const handleNotesBlur = () => {
@@ -112,7 +126,10 @@ export function PersonalOverridesSection({
112
126
  <div className="border-t bg-muted/20">
113
127
  <button
114
128
  type="button"
115
- onClick={() => setIsExpanded(!isExpanded)}
129
+ onClick={() => {
130
+ setIsExpanded(!isExpanded);
131
+ setActivePopover(null);
132
+ }}
116
133
  className="flex w-full items-center justify-between px-4 py-2 text-left transition-colors hover:bg-muted/40 md:px-8"
117
134
  >
118
135
  <div className="flex items-center gap-2">
@@ -167,7 +184,10 @@ export function PersonalOverridesSection({
167
184
  <Flag className="h-4 w-4 text-dynamic-orange" />
168
185
  {t('ws-tasks.my_priority')}
169
186
  </Label>
170
- <Popover open={isPriorityOpen} onOpenChange={setIsPriorityOpen}>
187
+ <Popover
188
+ open={isPopoverOpen('priority')}
189
+ onOpenChange={(open) => setPopoverOpen('priority', open)}
190
+ >
171
191
  <PopoverTrigger asChild>
172
192
  <Button
173
193
  variant="outline"
@@ -255,8 +275,8 @@ export function PersonalOverridesSection({
255
275
  {t('ws-tasks.my_estimate')}
256
276
  </Label>
257
277
  <Popover
258
- open={isEstimationOpen}
259
- onOpenChange={setIsEstimationOpen}
278
+ open={isPopoverOpen('estimation')}
279
+ onOpenChange={(open) => setPopoverOpen('estimation', open)}
260
280
  >
261
281
  <PopoverTrigger asChild>
262
282
  <Button
@@ -25,10 +25,15 @@ export function DependenciesSection({
25
25
  onNavigateToTask,
26
26
  onAddBlockingTaskDialog,
27
27
  onAddBlockedByTaskDialog,
28
+ searchOpen: controlledSearchOpen,
29
+ onSearchOpenChange,
28
30
  disabled,
29
31
  }: DependenciesSectionProps) {
30
32
  const [subTab, setSubTab] = React.useState<DependencySubTab>(initialSubTab);
31
- const [searchOpen, setSearchOpen] = React.useState(false);
33
+ const [uncontrolledSearchOpen, setUncontrolledSearchOpen] =
34
+ React.useState(false);
35
+ const searchOpen = controlledSearchOpen ?? uncontrolledSearchOpen;
36
+ const setSearchOpen = onSearchOpenChange ?? setUncontrolledSearchOpen;
32
37
 
33
38
  const allExcludeIds = React.useMemo(() => {
34
39
  const ids = new Set<string>();
@@ -56,7 +61,10 @@ export function DependenciesSection({
56
61
  <Button
57
62
  variant={subTab === 'blocks' ? 'default' : 'outline'}
58
63
  size="sm"
59
- onClick={() => setSubTab('blocks')}
64
+ onClick={() => {
65
+ setSubTab('blocks');
66
+ setSearchOpen(false);
67
+ }}
60
68
  className="h-7 text-xs"
61
69
  >
62
70
  Blocks ({blockingTasks.length})
@@ -64,7 +72,10 @@ export function DependenciesSection({
64
72
  <Button
65
73
  variant={subTab === 'blocked-by' ? 'default' : 'outline'}
66
74
  size="sm"
67
- onClick={() => setSubTab('blocked-by')}
75
+ onClick={() => {
76
+ setSubTab('blocked-by');
77
+ setSearchOpen(false);
78
+ }}
68
79
  className="h-7 text-xs"
69
80
  >
70
81
  Blocked By ({blockedByTasks.length})
@@ -16,9 +16,14 @@ export function ParentSection({
16
16
  onRemoveParent,
17
17
  onNavigateToTask,
18
18
  onAddParentTask,
19
+ searchOpen: controlledSearchOpen,
20
+ onSearchOpenChange,
19
21
  disabled,
20
22
  }: ParentSectionProps) {
21
- const [searchOpen, setSearchOpen] = React.useState(false);
23
+ const [uncontrolledSearchOpen, setUncontrolledSearchOpen] =
24
+ React.useState(false);
25
+ const searchOpen = controlledSearchOpen ?? uncontrolledSearchOpen;
26
+ const setSearchOpen = onSearchOpenChange ?? setUncontrolledSearchOpen;
22
27
 
23
28
  const excludeIds = React.useMemo(() => {
24
29
  const ids = taskId ? [taskId, ...childTaskIds] : childTaskIds;
@@ -16,9 +16,14 @@ export function RelatedSection({
16
16
  onRemoveRelated,
17
17
  onNavigateToTask,
18
18
  onAddRelatedTaskDialog,
19
+ searchOpen: controlledSearchOpen,
20
+ onSearchOpenChange,
19
21
  disabled,
20
22
  }: RelatedSectionProps) {
21
- const [searchOpen, setSearchOpen] = React.useState(false);
23
+ const [uncontrolledSearchOpen, setUncontrolledSearchOpen] =
24
+ React.useState(false);
25
+ const searchOpen = controlledSearchOpen ?? uncontrolledSearchOpen;
26
+ const setSearchOpen = onSearchOpenChange ?? setUncontrolledSearchOpen;
22
27
 
23
28
  const excludeIds = React.useMemo(() => {
24
29
  const ids = taskId ? [taskId] : [];
@@ -16,9 +16,14 @@ export function SubtasksSection({
16
16
  onAddSubtask,
17
17
  onAddExistingAsSubtask,
18
18
  isSaving,
19
+ searchOpen: controlledSearchOpen,
20
+ onSearchOpenChange,
19
21
  disabled,
20
22
  }: SubtasksSectionProps) {
21
- const [searchOpen, setSearchOpen] = React.useState(false);
23
+ const [uncontrolledSearchOpen, setUncontrolledSearchOpen] =
24
+ React.useState(false);
25
+ const searchOpen = controlledSearchOpen ?? uncontrolledSearchOpen;
26
+ const setSearchOpen = onSearchOpenChange ?? setUncontrolledSearchOpen;
22
27
 
23
28
  const excludeIds = React.useMemo(() => {
24
29
  const ids = taskId ? [taskId] : [];
@@ -122,6 +122,8 @@ export interface ParentSectionProps {
122
122
  onRemoveParent: () => void;
123
123
  onNavigateToTask: (taskId: string) => void;
124
124
  onAddParentTask?: () => void; // Opens dialog to create new parent task
125
+ searchOpen?: boolean;
126
+ onSearchOpenChange?: (open: boolean) => void;
125
127
  disabled?: boolean;
126
128
  }
127
129
 
@@ -137,6 +139,8 @@ export interface SubtasksSectionProps {
137
139
  onAddSubtask?: () => void;
138
140
  onAddExistingAsSubtask?: (task: RelatedTaskInfo) => Promise<void>;
139
141
  isSaving: boolean;
142
+ searchOpen?: boolean;
143
+ onSearchOpenChange?: (open: boolean) => void;
140
144
  disabled?: boolean;
141
145
  }
142
146
 
@@ -168,6 +172,8 @@ export interface DependenciesSectionProps {
168
172
  onNavigateToTask: (taskId: string) => void;
169
173
  onAddBlockingTaskDialog?: () => void; // Opens dialog to create new blocking task
170
174
  onAddBlockedByTaskDialog?: () => void; // Opens dialog to create new blocked-by task
175
+ searchOpen?: boolean;
176
+ onSearchOpenChange?: (open: boolean) => void;
171
177
  disabled?: boolean;
172
178
  }
173
179
 
@@ -182,5 +188,7 @@ export interface RelatedSectionProps {
182
188
  onRemoveRelated: (taskId: string) => void;
183
189
  onNavigateToTask: (taskId: string) => void;
184
190
  onAddRelatedTaskDialog?: () => void; // Opens dialog to create new related task;
191
+ searchOpen?: boolean;
192
+ onSearchOpenChange?: (open: boolean) => void;
185
193
  disabled?: boolean;
186
194
  }
@@ -0,0 +1,91 @@
1
+ import '@testing-library/jest-dom/vitest';
2
+
3
+ import { fireEvent, render, screen } from '@testing-library/react';
4
+ import { describe, expect, it, vi } from 'vitest';
5
+ import { SelectiveRevertPanel } from './selective-revert-panel';
6
+
7
+ vi.mock('./description-diff-viewer', () => ({
8
+ DescriptionDiffViewer: () => (
9
+ <button type="button">view-description-diff</button>
10
+ ),
11
+ }));
12
+
13
+ const t = (key: string, options?: { defaultValue?: string }) => {
14
+ const messages: Record<string, string> = {
15
+ changed: 'Changed',
16
+ 'field.description': 'Description',
17
+ 'field.name': 'Name',
18
+ 'field.priority': 'Priority',
19
+ unchanged_fields: 'Unchanged fields',
20
+ };
21
+
22
+ return messages[key] ?? options?.defaultValue ?? key;
23
+ };
24
+
25
+ const snapshot = {
26
+ assignees: [],
27
+ completed: false,
28
+ description: {
29
+ content: [
30
+ {
31
+ content: [{ text: 'Previous description', type: 'text' }],
32
+ type: 'paragraph',
33
+ },
34
+ ],
35
+ type: 'doc',
36
+ },
37
+ end_date: null,
38
+ estimation_points: null,
39
+ id: 'task-1',
40
+ labels: [],
41
+ list_id: 'list-1',
42
+ list_name: 'Review',
43
+ name: 'Previous task name',
44
+ priority: 'normal' as const,
45
+ projects: [],
46
+ start_date: null,
47
+ };
48
+
49
+ const currentTask = {
50
+ ...snapshot,
51
+ description: {
52
+ content: [
53
+ {
54
+ content: [{ text: 'Current description', type: 'text' }],
55
+ type: 'paragraph',
56
+ },
57
+ ],
58
+ type: 'doc',
59
+ },
60
+ list_name: 'Review',
61
+ name: 'Current task name',
62
+ };
63
+
64
+ describe('SelectiveRevertPanel', () => {
65
+ it('renders changed fields first and keeps unchanged fields collapsed', () => {
66
+ render(
67
+ <SelectiveRevertPanel
68
+ currentTask={currentTask}
69
+ isReverting={false}
70
+ onRevert={vi.fn()}
71
+ snapshot={snapshot}
72
+ t={t}
73
+ />
74
+ );
75
+
76
+ expect(screen.getByText('Core Fields')).toBeInTheDocument();
77
+ expect(screen.getByText('Name')).toBeInTheDocument();
78
+ expect(screen.getByText('Description')).toBeInTheDocument();
79
+ expect(screen.getByText('view-description-diff')).toBeInTheDocument();
80
+
81
+ const unchangedButton = screen.getByRole('button', {
82
+ name: /Unchanged fields/i,
83
+ });
84
+ expect(unchangedButton).toBeInTheDocument();
85
+ expect(screen.queryByText('Priority')).not.toBeInTheDocument();
86
+
87
+ fireEvent.click(unchangedButton);
88
+
89
+ expect(screen.getByText('Priority')).toBeInTheDocument();
90
+ });
91
+ });
@@ -1,7 +1,14 @@
1
1
  'use client';
2
2
 
3
- import { AlertTriangle, Loader2, RotateCcw } from '@tuturuuu/icons';
3
+ import {
4
+ AlertTriangle,
5
+ ChevronDown,
6
+ ChevronRight,
7
+ Loader2,
8
+ RotateCcw,
9
+ } from '@tuturuuu/icons';
4
10
  import { Alert, AlertDescription } from '@tuturuuu/ui/alert';
11
+ import { Badge } from '@tuturuuu/ui/badge';
5
12
  import { Button } from '@tuturuuu/ui/button';
6
13
  import {
7
14
  ALL_COMPARABLE_FIELDS,
@@ -21,15 +28,49 @@ interface SelectiveRevertPanelProps {
21
28
  onRevert: (fields: RevertibleField[]) => Promise<void>;
22
29
  isReverting: boolean;
23
30
  locale?: string;
24
- t?: (key: string, options?: { defaultValue?: string }) => string;
31
+ t?: (
32
+ key: string,
33
+ options?: { count?: number; defaultValue?: string }
34
+ ) => string;
25
35
  /** Estimation type for displaying points */
26
36
  estimationType?: EstimationType;
27
37
  /** When true, disables the revert functionality (feature not stable) */
28
38
  revertDisabled?: boolean;
29
39
  }
30
40
 
31
- const defaultT = (key: string, opts?: { defaultValue?: string }) =>
32
- opts?.defaultValue || key;
41
+ const defaultT = (
42
+ key: string,
43
+ opts?: { count?: number; defaultValue?: string }
44
+ ) => opts?.defaultValue || key;
45
+
46
+ const FIELD_SECTIONS: {
47
+ fields: ComparableField[];
48
+ titleKey: string;
49
+ titleFallback: string;
50
+ }[] = [
51
+ {
52
+ fields: [
53
+ 'name',
54
+ 'description',
55
+ 'priority',
56
+ 'estimation_points',
57
+ 'list_id',
58
+ 'completed',
59
+ ],
60
+ titleFallback: 'Core Fields',
61
+ titleKey: 'core_fields',
62
+ },
63
+ {
64
+ fields: ['start_date', 'end_date'],
65
+ titleFallback: 'Dates',
66
+ titleKey: 'dates',
67
+ },
68
+ {
69
+ fields: ['assignees', 'labels', 'projects'],
70
+ titleFallback: 'Relationships',
71
+ titleKey: 'relationships',
72
+ },
73
+ ];
33
74
 
34
75
  export function SelectiveRevertPanel({
35
76
  snapshot,
@@ -95,6 +136,31 @@ export function SelectiveRevertPanel({
95
136
 
96
137
  const hasChanges = changedFields.length > 0;
97
138
  const hasSelection = selectedFields.size > 0;
139
+ const changedFieldSet = useMemo(
140
+ () => new Set(changedFields),
141
+ [changedFields]
142
+ );
143
+ const unchangedFields = useMemo(
144
+ () => ALL_COMPARABLE_FIELDS.filter((field) => !changedFieldSet.has(field)),
145
+ [changedFieldSet]
146
+ );
147
+
148
+ const renderField = (field: ComparableField) => (
149
+ <FieldDiffViewer
150
+ key={field}
151
+ fieldName={field}
152
+ snapshotValue={fieldValues[field].snapshot}
153
+ currentValue={fieldValues[field].current}
154
+ selected={selectedFields.has(field)}
155
+ onSelectionChange={(selected) => handleFieldSelect(field, selected)}
156
+ hasChanged={changedFieldSet.has(field)}
157
+ locale={locale}
158
+ t={t}
159
+ snapshotListName={snapshot.list_name}
160
+ currentListName={currentTask.list_name}
161
+ estimationType={estimationType}
162
+ />
163
+ );
98
164
 
99
165
  return (
100
166
  <div className="flex flex-col gap-4">
@@ -171,78 +237,37 @@ export function SelectiveRevertPanel({
171
237
 
172
238
  {/* Field list */}
173
239
  <div className="space-y-2">
174
- {/* Core fields */}
175
- <FieldGroup title={t('core_fields', { defaultValue: 'Core Fields' })}>
176
- {(
177
- [
178
- 'name',
179
- 'description',
180
- 'priority',
181
- 'estimation_points',
182
- 'list_id',
183
- 'completed',
184
- ] as ComparableField[]
185
- ).map((field) => (
186
- <FieldDiffViewer
187
- key={field}
188
- fieldName={field}
189
- snapshotValue={fieldValues[field].snapshot}
190
- currentValue={fieldValues[field].current}
191
- selected={selectedFields.has(field)}
192
- onSelectionChange={(selected) =>
193
- handleFieldSelect(field, selected)
194
- }
195
- hasChanged={changedFields.includes(field)}
196
- locale={locale}
197
- t={t}
198
- snapshotListName={snapshot.list_name}
199
- currentListName={currentTask.list_name}
200
- estimationType={estimationType}
201
- />
202
- ))}
203
- </FieldGroup>
240
+ {FIELD_SECTIONS.map((section) => {
241
+ const sectionChangedFields = section.fields.filter((field) =>
242
+ changedFieldSet.has(field)
243
+ );
204
244
 
205
- {/* Date fields */}
206
- <FieldGroup title={t('dates', { defaultValue: 'Dates' })}>
207
- {(['start_date', 'end_date'] as ComparableField[]).map((field) => (
208
- <FieldDiffViewer
209
- key={field}
210
- fieldName={field}
211
- snapshotValue={fieldValues[field].snapshot}
212
- currentValue={fieldValues[field].current}
213
- selected={selectedFields.has(field)}
214
- onSelectionChange={(selected) =>
215
- handleFieldSelect(field, selected)
216
- }
217
- hasChanged={changedFields.includes(field)}
218
- locale={locale}
219
- t={t}
220
- />
221
- ))}
222
- </FieldGroup>
245
+ if (sectionChangedFields.length === 0) return null;
223
246
 
224
- {/* Relationship fields */}
225
- <FieldGroup
226
- title={t('relationships', { defaultValue: 'Relationships' })}
227
- >
228
- {(['assignees', 'labels', 'projects'] as ComparableField[]).map(
229
- (field) => (
230
- <FieldDiffViewer
231
- key={field}
232
- fieldName={field}
233
- snapshotValue={fieldValues[field].snapshot}
234
- currentValue={fieldValues[field].current}
235
- selected={selectedFields.has(field)}
236
- onSelectionChange={(selected) =>
237
- handleFieldSelect(field, selected)
238
- }
239
- hasChanged={changedFields.includes(field)}
240
- locale={locale}
241
- t={t}
242
- />
243
- )
244
- )}
245
- </FieldGroup>
247
+ return (
248
+ <FieldGroup
249
+ key={section.titleKey}
250
+ count={sectionChangedFields.length}
251
+ title={t(section.titleKey, {
252
+ defaultValue: section.titleFallback,
253
+ })}
254
+ >
255
+ {sectionChangedFields.map(renderField)}
256
+ </FieldGroup>
257
+ );
258
+ })}
259
+
260
+ {unchangedFields.length > 0 && (
261
+ <FieldGroup
262
+ count={unchangedFields.length}
263
+ defaultCollapsed
264
+ title={t('unchanged_fields', {
265
+ defaultValue: 'Unchanged fields',
266
+ })}
267
+ >
268
+ {unchangedFields.map(renderField)}
269
+ </FieldGroup>
270
+ )}
246
271
  </div>
247
272
 
248
273
  {/* Revert button */}
@@ -271,18 +296,38 @@ export function SelectiveRevertPanel({
271
296
  }
272
297
 
273
298
  function FieldGroup({
299
+ count,
300
+ defaultCollapsed = false,
274
301
  title,
275
302
  children,
276
303
  }: {
304
+ count: number;
305
+ defaultCollapsed?: boolean;
277
306
  title: string;
278
307
  children: React.ReactNode;
279
308
  }) {
309
+ const [isOpen, setIsOpen] = useState(!defaultCollapsed);
310
+
280
311
  return (
281
312
  <div className="space-y-2">
282
- <h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
283
- {title}
284
- </h4>
285
- <div className="space-y-2">{children}</div>
313
+ <button
314
+ className="flex w-full items-center gap-2 rounded-md px-1 py-1 text-left transition-colors hover:bg-muted/40"
315
+ onClick={() => setIsOpen((open) => !open)}
316
+ type="button"
317
+ >
318
+ {isOpen ? (
319
+ <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
320
+ ) : (
321
+ <ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
322
+ )}
323
+ <span className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
324
+ {title}
325
+ </span>
326
+ <Badge variant="secondary" className="h-5 px-1.5 text-[10px]">
327
+ {count}
328
+ </Badge>
329
+ </button>
330
+ {isOpen && <div className="space-y-2">{children}</div>}
286
331
  </div>
287
332
  );
288
333
  }
@@ -93,6 +93,7 @@ export function TaskActivitySection({
93
93
  revertDisabled = false,
94
94
  }: TaskActivitySectionProps) {
95
95
  const t = useTranslations('tasks-history');
96
+ const snapshotT = useTranslations('tasks.history');
96
97
  const locale = useLocale();
97
98
  const [isExpanded, setIsExpanded] = useState(false);
98
99
  const [showAll, setShowAll] = useState(false);
@@ -219,7 +220,12 @@ export function TaskActivitySection({
219
220
  onTaskUpdate?.();
220
221
  }}
221
222
  locale={locale}
222
- t={t as (key: string, options?: { defaultValue?: string }) => string}
223
+ t={
224
+ snapshotT as (
225
+ key: string,
226
+ options?: { count?: number; defaultValue?: string }
227
+ ) => string
228
+ }
223
229
  estimationType={estimationType}
224
230
  revertDisabled={revertDisabled}
225
231
  />
@@ -48,6 +48,8 @@ interface TaskDialogActionsProps {
48
48
  onOpenShareDialog?: () => void;
49
49
  disabled?: boolean;
50
50
  controlsDisabled?: boolean;
51
+ moreMenuOpen?: boolean;
52
+ onMoreMenuOpenChange?: (open: boolean) => void;
51
53
  }
52
54
 
53
55
  export function TaskDialogActions({
@@ -66,10 +68,15 @@ export function TaskDialogActions({
66
68
  onOpenShareDialog,
67
69
  disabled = false,
68
70
  controlsDisabled = false,
71
+ moreMenuOpen,
72
+ onMoreMenuOpenChange,
69
73
  }: TaskDialogActionsProps) {
70
74
  const t = useTranslations();
71
75
  const tasksHref = useTasksHref();
72
- const [isMoreMenuOpen, setIsMoreMenuOpen] = useState(false);
76
+ const [uncontrolledMoreMenuOpen, setUncontrolledMoreMenuOpen] =
77
+ useState(false);
78
+ const isMoreMenuOpen = moreMenuOpen ?? uncontrolledMoreMenuOpen;
79
+ const setIsMoreMenuOpen = onMoreMenuOpenChange ?? setUncontrolledMoreMenuOpen;
73
80
 
74
81
  // Determine if we should show the back button (create mode with a pending relationship)
75
82
  const showBackButton = isCreateMode && onNavigateBack && navigateBackTaskName;