@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
@@ -1,25 +1,26 @@
1
- import { useQuery } from '@tanstack/react-query';
1
+ import { useQuery, useQueryClient } from '@tanstack/react-query';
2
+ import { Archive, LayoutGrid, Trash2 } from '@tuturuuu/icons';
2
3
  import {
3
- Archive,
4
- CheckCircle2,
5
- ChevronsUpDown,
6
- LayoutGrid,
7
- Loader2,
8
- Trash2,
9
- } from '@tuturuuu/icons';
10
- import { listWorkspaceTaskBoards } from '@tuturuuu/internal-api';
4
+ type AccessibleWorkspaceTaskBoard,
5
+ createWorkspaceTaskBoard,
6
+ listCurrentUserTaskBoards,
7
+ } from '@tuturuuu/internal-api/tasks';
8
+ import {
9
+ isTaskRememberLastBoardEnabled,
10
+ TASK_DEFAULT_BOARD_ID_CONFIG_ID,
11
+ TASK_REMEMBER_LAST_BOARD_CONFIG_ID,
12
+ } from '@tuturuuu/internal-api/users';
11
13
  import type { WorkspaceTaskBoard } from '@tuturuuu/types';
12
14
  import { Badge } from '@tuturuuu/ui/badge';
13
- import {
14
- DropdownMenu,
15
- DropdownMenuContent,
16
- DropdownMenuItem,
17
- DropdownMenuLabel,
18
- DropdownMenuSeparator,
19
- DropdownMenuTrigger,
20
- } from '@tuturuuu/ui/dropdown-menu';
15
+ import { Combobox, type ComboboxOption } from '@tuturuuu/ui/custom/combobox';
16
+ import { toast } from '@tuturuuu/ui/sonner';
21
17
  import { cn } from '@tuturuuu/utils/format';
22
18
  import { useRouter } from 'next/navigation';
19
+ import { useCallback, useMemo } from 'react';
20
+ import {
21
+ useUpdateUserWorkspaceConfig,
22
+ useUserWorkspaceConfig,
23
+ } from '../../../../hooks/use-user-workspace-config';
23
24
  import {
24
25
  getIconComponentByKey,
25
26
  type PlatformIconKey,
@@ -41,18 +42,24 @@ interface BoardSwitcherProps {
41
42
  archived?: string;
42
43
  deleted?: string;
43
44
  daysLeft?: string;
44
- // Board name translations (case-insensitive matching)
45
+ searchBoards?: string;
45
46
  tasks?: string;
47
+ createBoard?: string;
48
+ creatingBoard?: string;
49
+ createBoardError?: string;
46
50
  };
47
51
  }
48
52
 
49
53
  type BoardWithStatus = {
54
+ access_type?: 'member' | 'guest';
50
55
  id: string;
51
56
  name: string | null;
52
57
  icon: string | null;
53
58
  archived_at: string | null;
54
59
  deleted_at: string | null;
55
60
  created_at: string | null;
61
+ workspace?: AccessibleWorkspaceTaskBoard['workspace'];
62
+ ws_id: string;
56
63
  };
57
64
 
58
65
  function getDaysRemaining(deletedAt: string) {
@@ -66,233 +73,250 @@ function getDaysRemaining(deletedAt: string) {
66
73
 
67
74
  export function BoardSwitcher({ board, translations }: BoardSwitcherProps) {
68
75
  const router = useRouter();
76
+ const queryClient = useQueryClient();
69
77
  const tasksHref = useTasksHref();
78
+ const { data: rememberLastBoardRaw } = useUserWorkspaceConfig(
79
+ board.ws_id,
80
+ TASK_REMEMBER_LAST_BOARD_CONFIG_ID,
81
+ 'true'
82
+ );
83
+ const updateUserWorkspaceConfig = useUpdateUserWorkspaceConfig();
70
84
 
71
- // Use provided translations or fall back to English defaults
72
85
  const t = {
73
86
  loadingBoards: translations?.loadingBoards ?? 'Loading boards...',
74
87
  noOtherBoards: translations?.noOtherBoards ?? 'No other boards',
75
- activeBoards: translations?.activeBoards ?? 'Active Boards',
76
- archivedBoards: translations?.archivedBoards ?? 'Archived Boards',
77
- deletedBoards: translations?.deletedBoards ?? 'Deleted Boards',
88
+ activeBoards: translations?.activeBoards ?? 'Active boards',
89
+ archivedBoards: translations?.archivedBoards ?? 'Archived boards',
90
+ deletedBoards: translations?.deletedBoards ?? 'Deleted boards',
78
91
  untitled: translations?.untitled ?? 'Untitled',
79
92
  active: translations?.active ?? 'Active',
80
93
  archived: translations?.archived ?? 'Archived',
81
94
  deleted: translations?.deleted ?? 'Deleted',
82
95
  daysLeft: translations?.daysLeft ?? '{count} days left',
96
+ searchBoards: translations?.searchBoards ?? 'Search boards',
83
97
  tasks: translations?.tasks ?? 'Tasks',
98
+ createBoard: translations?.createBoard ?? 'Create board',
99
+ creatingBoard: translations?.creatingBoard ?? 'Creating',
100
+ createBoardError:
101
+ translations?.createBoardError ?? 'Could not create board',
84
102
  };
85
103
 
86
- // Helper to translate board names (case-insensitive matching)
87
- const translateBoardName = (name: string | null): string => {
88
- if (!name) return t.untitled;
89
- // Check if board name matches known translatable names
90
- if (name.toLowerCase() === 'tasks') return t.tasks;
91
- return name;
92
- };
104
+ const translateBoardName = useCallback(
105
+ (name: string | null): string => {
106
+ if (!name) return t.untitled;
107
+ if (name.toLowerCase() === 'tasks') return t.tasks;
108
+ return name;
109
+ },
110
+ [t.tasks, t.untitled]
111
+ );
93
112
 
94
113
  const { data: boards = [], isLoading: isFetchingBoards } = useQuery({
95
- queryKey: ['other-boards', board.ws_id, board.id],
114
+ queryKey: ['accessible-task-boards'],
96
115
  queryFn: async () => {
97
- const payload = await listWorkspaceTaskBoards(board.ws_id);
116
+ const payload = await listCurrentUserTaskBoards();
98
117
  return (payload.boards || []) as BoardWithStatus[];
99
118
  },
100
- enabled: !!board.ws_id,
101
119
  });
120
+ const rememberLastBoard =
121
+ isTaskRememberLastBoardEnabled(rememberLastBoardRaw);
122
+ const boardsById = useMemo(() => {
123
+ return new Map(boards.map((item) => [item.id, item] as const));
124
+ }, [boards]);
102
125
 
103
- // Separate boards by status
104
- const activeBoards = boards.filter((b) => !b.archived_at && !b.deleted_at);
105
- const archivedBoards = boards.filter((b) => b.archived_at && !b.deleted_at);
106
- const deletedBoards = boards.filter((b) => b.deleted_at);
126
+ const selectBoard = useCallback(
127
+ (value: string | string[]) => {
128
+ const boardId = Array.isArray(value) ? value[0] : value;
129
+ if (!boardId || boardId === board.id) return;
107
130
 
108
- // Get current board's icon
109
- const CurrentBoardIcon =
110
- getIconComponentByKey(board.icon as PlatformIconKey | null) ?? LayoutGrid;
131
+ const selectedBoard = boardsById.get(boardId);
132
+ const targetWorkspaceId = selectedBoard?.ws_id ?? board.ws_id;
111
133
 
112
- return (
113
- <DropdownMenu>
114
- <DropdownMenuTrigger asChild>
115
- <div className="group flex cursor-pointer items-center gap-2 transition-colors hover:text-foreground">
116
- <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary transition-colors group-hover:bg-primary/20">
117
- <CurrentBoardIcon className="h-4 w-4" />
118
- </div>
119
- <h1 className="truncate font-bold text-base text-foreground sm:text-xl md:text-2xl">
120
- {translateBoardName(board.name)}
121
- </h1>
122
- <ChevronsUpDown className="h-4 w-4 text-muted-foreground transition-transform group-hover:scale-110" />
123
- </div>
124
- </DropdownMenuTrigger>
125
- <DropdownMenuContent align="start" className="w-[320px]">
126
- {isFetchingBoards ? (
127
- <DropdownMenuItem disabled>
128
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
129
- {t.loadingBoards}
130
- </DropdownMenuItem>
131
- ) : boards.length === 0 ? (
132
- <DropdownMenuItem disabled className="justify-center">
133
- {t.noOtherBoards}
134
- </DropdownMenuItem>
135
- ) : (
136
- <>
137
- {/* Active Boards */}
138
- {activeBoards.length > 0 && (
139
- <>
140
- <DropdownMenuLabel className="text-muted-foreground text-xs">
141
- {t.activeBoards}
142
- </DropdownMenuLabel>
143
- {activeBoards.map((otherBoard) => {
144
- const isCurrentBoard = otherBoard.id === board.id;
145
- const BoardIcon =
146
- getIconComponentByKey(
147
- otherBoard.icon as PlatformIconKey | null
148
- ) ?? LayoutGrid;
149
- return (
150
- <DropdownMenuItem
151
- key={otherBoard.id}
152
- onClick={() =>
153
- router.push(
154
- `/${board.ws_id}${tasksHref(`/boards/${otherBoard.id}`)}`
155
- )
156
- }
157
- className={cn(
158
- 'group/item cursor-pointer gap-3 py-2.5',
159
- isCurrentBoard && 'bg-dynamic-blue/10 text-dynamic-blue'
160
- )}
161
- >
162
- <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary transition-colors group-hover/item:bg-primary/20">
163
- <BoardIcon className="h-4 w-4" />
164
- </div>
165
- <div className="flex min-w-0 flex-1 flex-col gap-1">
166
- <span className="truncate font-medium text-sm leading-none">
167
- {translateBoardName(otherBoard.name)}
168
- </span>
169
- </div>
170
- <Badge
171
- className={cn(
172
- 'shrink-0 gap-1 px-2 py-0.5 text-[10px]',
173
- 'bg-dynamic-green/10 text-dynamic-green'
174
- )}
175
- >
176
- <CheckCircle2 className="h-3 w-3 text-dynamic-green/50" />
177
- {t.active}
178
- </Badge>
179
- </DropdownMenuItem>
180
- );
181
- })}
182
- </>
183
- )}
134
+ if (rememberLastBoard && targetWorkspaceId === board.ws_id) {
135
+ updateUserWorkspaceConfig.mutate({
136
+ configId: TASK_DEFAULT_BOARD_ID_CONFIG_ID,
137
+ value: boardId,
138
+ workspaceId: board.ws_id,
139
+ });
140
+ }
184
141
 
185
- {/* Archived Boards */}
186
- {archivedBoards.length > 0 && (
187
- <>
188
- {activeBoards.length > 0 && <DropdownMenuSeparator />}
189
- <DropdownMenuLabel className="text-muted-foreground text-xs">
190
- {t.archivedBoards}
191
- </DropdownMenuLabel>
192
- {archivedBoards.map((otherBoard) => {
193
- const isCurrentBoard = otherBoard.id === board.id;
194
- const BoardIcon =
195
- getIconComponentByKey(
196
- otherBoard.icon as PlatformIconKey | null
197
- ) ?? LayoutGrid;
198
- return (
199
- <DropdownMenuItem
200
- key={otherBoard.id}
201
- onClick={() =>
202
- router.push(
203
- `/${board.ws_id}${tasksHref(`/boards/${otherBoard.id}`)}`
204
- )
205
- }
206
- className={cn(
207
- 'group/item cursor-pointer gap-3 py-2.5 opacity-75 hover:opacity-100',
208
- isCurrentBoard &&
209
- 'bg-dynamic-blue/10 text-dynamic-blue opacity-100'
210
- )}
211
- >
212
- <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
213
- <BoardIcon className="h-4 w-4" />
214
- </div>
215
- <div className="flex min-w-0 flex-1 flex-col gap-1">
216
- <span className="truncate font-medium text-sm leading-none">
217
- {otherBoard.name || t.untitled}
218
- </span>
219
- </div>
220
- <Badge
221
- className={cn(
222
- 'shrink-0 gap-1 px-2 py-0.5 text-[10px]',
223
- 'bg-muted text-foreground'
224
- )}
225
- >
226
- <Archive className="h-3 w-3 text-foreground/50" />
227
- {t.archived}
228
- </Badge>
229
- </DropdownMenuItem>
230
- );
231
- })}
232
- </>
233
- )}
142
+ router.push(`/${targetWorkspaceId}${tasksHref(`/boards/${boardId}`)}`);
143
+ },
144
+ [
145
+ board.id,
146
+ board.ws_id,
147
+ boardsById,
148
+ rememberLastBoard,
149
+ router,
150
+ tasksHref,
151
+ updateUserWorkspaceConfig,
152
+ ]
153
+ );
154
+
155
+ const createBoard = useCallback(
156
+ async (value: string) => {
157
+ const name = value.trim();
158
+ if (!name) return;
234
159
 
235
- {/* Deleted Boards */}
236
- {deletedBoards.length > 0 && (
237
- <>
238
- {(activeBoards.length > 0 || archivedBoards.length > 0) && (
239
- <DropdownMenuSeparator />
240
- )}
241
- <DropdownMenuLabel className="text-muted-foreground text-xs">
242
- {t.deletedBoards}
243
- </DropdownMenuLabel>
244
- {deletedBoards.map((otherBoard) => {
245
- const daysRemaining = getDaysRemaining(
246
- otherBoard.deleted_at ?? ''
247
- );
248
- const isCurrentBoard = otherBoard.id === board.id;
249
- const BoardIcon =
250
- getIconComponentByKey(
251
- otherBoard.icon as PlatformIconKey | null
252
- ) ?? LayoutGrid;
160
+ try {
161
+ const payload = await createWorkspaceTaskBoard(board.ws_id, { name });
162
+ await queryClient.invalidateQueries({
163
+ queryKey: ['accessible-task-boards'],
164
+ });
253
165
 
254
- return (
255
- <DropdownMenuItem
256
- key={otherBoard.id}
257
- onClick={() =>
258
- router.push(
259
- `/${board.ws_id}${tasksHref(`/boards/${otherBoard.id}`)}`
260
- )
261
- }
262
- className={cn(
263
- 'group/item cursor-pointer gap-3 py-2.5 opacity-60 hover:opacity-100',
264
- isCurrentBoard &&
265
- 'bg-dynamic-blue/10 text-dynamic-blue opacity-100'
266
- )}
267
- >
268
- <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-destructive/10 text-destructive">
269
- <BoardIcon className="h-4 w-4" />
270
- </div>
271
- <div className="flex min-w-0 flex-1 flex-col gap-1">
272
- <span className="truncate font-medium text-sm leading-none">
273
- {otherBoard.name || t.untitled}
274
- </span>
275
- <span className="text-[10px] text-muted-foreground leading-none">
276
- {t.daysLeft.replace('{count}', String(daysRemaining))}
277
- </span>
278
- </div>
279
- <Badge
280
- className={cn(
281
- 'shrink-0 gap-1 px-2 py-0.5 text-[10px]',
282
- 'bg-dynamic-red/10 text-dynamic-red'
283
- )}
284
- >
285
- <Trash2 className="h-3 w-3 text-dynamic-red/50" />
286
- {t.deleted}
287
- </Badge>
288
- </DropdownMenuItem>
289
- );
290
- })}
291
- </>
166
+ return {
167
+ label: translateBoardName(payload.board.name ?? name),
168
+ value: payload.board.id,
169
+ };
170
+ } catch (error) {
171
+ toast.error(t.createBoardError);
172
+ throw error;
173
+ }
174
+ },
175
+ [board.ws_id, queryClient, t.createBoardError, translateBoardName]
176
+ );
177
+
178
+ const currentBoardFromAccessible = boardsById.get(board.id);
179
+ const canCreateBoard = currentBoardFromAccessible?.access_type !== 'guest';
180
+
181
+ const boardOptions = useMemo(() => {
182
+ const byId = new Map<string, BoardWithStatus>();
183
+ for (const item of boards) byId.set(item.id, item);
184
+ if (!byId.has(board.id)) {
185
+ byId.set(board.id, {
186
+ id: board.id,
187
+ name: board.name ?? null,
188
+ icon: board.icon ?? null,
189
+ archived_at: null,
190
+ deleted_at: null,
191
+ created_at: null,
192
+ ws_id: board.ws_id,
193
+ });
194
+ }
195
+
196
+ const orderedBoards = [...byId.values()].sort((a, b) => {
197
+ const workspaceDelta =
198
+ (a.ws_id === board.ws_id ? 0 : 1) - (b.ws_id === board.ws_id ? 0 : 1);
199
+ if (workspaceDelta !== 0) return workspaceDelta;
200
+
201
+ const currentBoardDelta =
202
+ (a.id === board.id ? 0 : 1) - (b.id === board.id ? 0 : 1);
203
+ if (currentBoardDelta !== 0) return currentBoardDelta;
204
+
205
+ const workspaceNameDelta = (a.workspace?.name ?? a.ws_id).localeCompare(
206
+ b.workspace?.name ?? b.ws_id
207
+ );
208
+ if (workspaceNameDelta !== 0) return workspaceNameDelta;
209
+
210
+ const statusWeight = (item: BoardWithStatus) =>
211
+ item.deleted_at ? 2 : item.archived_at ? 1 : 0;
212
+ const statusDelta = statusWeight(a) - statusWeight(b);
213
+ if (statusDelta !== 0) return statusDelta;
214
+ return translateBoardName(a.name).localeCompare(
215
+ translateBoardName(b.name)
216
+ );
217
+ });
218
+
219
+ return orderedBoards.map((item): ComboboxOption => {
220
+ const BoardIcon =
221
+ getIconComponentByKey(item.icon as PlatformIconKey | null) ??
222
+ LayoutGrid;
223
+ const isDeleted = Boolean(item.deleted_at);
224
+ const isArchived = Boolean(item.archived_at && !item.deleted_at);
225
+ const statusLabel = isDeleted
226
+ ? t.deleted
227
+ : isArchived
228
+ ? t.archived
229
+ : t.active;
230
+ const workspaceLabel = item.workspace?.name ?? item.ws_id;
231
+ const groupLabel = isDeleted
232
+ ? t.deletedBoards
233
+ : isArchived
234
+ ? t.archivedBoards
235
+ : t.activeBoards;
236
+ const daysRemaining =
237
+ item.deleted_at && getDaysRemaining(item.deleted_at);
238
+ const description = daysRemaining
239
+ ? `${groupLabel} · ${t.daysLeft.replace(
240
+ '{count}',
241
+ String(daysRemaining)
242
+ )}`
243
+ : isArchived || isDeleted
244
+ ? groupLabel
245
+ : undefined;
246
+ const badge =
247
+ isArchived || isDeleted ? (
248
+ <Badge
249
+ key={`${item.id}-status`}
250
+ className={cn(
251
+ 'shrink-0 gap-1 px-2 py-0.5 text-[10px]',
252
+ isDeleted && 'bg-dynamic-red/10 text-dynamic-red',
253
+ isArchived && 'bg-muted text-foreground'
254
+ )}
255
+ >
256
+ {isDeleted ? (
257
+ <Trash2 className="h-3 w-3 text-dynamic-red/50" />
258
+ ) : (
259
+ <Archive className="h-3 w-3 text-foreground/50" />
292
260
  )}
293
- </>
294
- )}
295
- </DropdownMenuContent>
296
- </DropdownMenu>
261
+ {statusLabel}
262
+ </Badge>
263
+ ) : undefined;
264
+
265
+ return {
266
+ value: item.id,
267
+ label: translateBoardName(item.name),
268
+ searchValue: [
269
+ translateBoardName(item.name),
270
+ workspaceLabel,
271
+ statusLabel,
272
+ groupLabel,
273
+ daysRemaining
274
+ ? t.daysLeft.replace('{count}', String(daysRemaining))
275
+ : null,
276
+ ]
277
+ .filter(Boolean)
278
+ .join(' '),
279
+ description,
280
+ group: workspaceLabel,
281
+ icon: <BoardIcon className="h-4 w-4" />,
282
+ muted: isArchived || isDeleted,
283
+ badge,
284
+ };
285
+ });
286
+ }, [
287
+ board.icon,
288
+ board.id,
289
+ board.name,
290
+ board.ws_id,
291
+ boards,
292
+ t.active,
293
+ t.activeBoards,
294
+ t.archived,
295
+ t.archivedBoards,
296
+ t.daysLeft,
297
+ t.deleted,
298
+ t.deletedBoards,
299
+ translateBoardName,
300
+ ]);
301
+
302
+ return (
303
+ <Combobox
304
+ className="w-[min(22rem,70vw)] [&_button]:h-7 [&_button]:min-h-7 [&_button]:px-2 sm:[&_button]:h-8 sm:[&_button]:min-h-8"
305
+ createText={canCreateBoard ? t.createBoard : undefined}
306
+ creatingText={canCreateBoard ? t.creatingBoard : undefined}
307
+ disabled={isFetchingBoards}
308
+ emptyText={isFetchingBoards ? t.loadingBoards : t.noOtherBoards}
309
+ label={
310
+ <span className="truncate font-semibold text-foreground text-sm">
311
+ {translateBoardName(board.name)}
312
+ </span>
313
+ }
314
+ onChange={selectBoard}
315
+ onCreate={canCreateBoard ? createBoard : undefined}
316
+ options={boardOptions}
317
+ placeholder={translateBoardName(board.name)}
318
+ searchPlaceholder={t.searchBoards}
319
+ selected={board.id}
320
+ />
297
321
  );
298
322
  }
@@ -5,6 +5,7 @@ import type { RealtimePresenceState } from '@tuturuuu/supabase/next/realtime';
5
5
  import type { User } from '@tuturuuu/types/primitives/User';
6
6
  import { Avatar, AvatarFallback, AvatarImage } from '@tuturuuu/ui/avatar';
7
7
  import { Button } from '@tuturuuu/ui/button';
8
+ import { useBoardPresence } from '@tuturuuu/ui/hooks/useBoardPresence';
8
9
  import type { UserPresenceState } from '@tuturuuu/ui/hooks/usePresence';
9
10
  import { Popover, PopoverContent, PopoverTrigger } from '@tuturuuu/ui/popover';
10
11
  import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
@@ -105,12 +106,17 @@ export function BoardUserPresenceAvatarsComponent({
105
106
  onListStatusFilterChange: (filter: ListStatusFilter) => void;
106
107
  }) {
107
108
  const wsPresence = useOptionalWorkspacePresenceContext();
109
+ const useWorkspacePresence = wsPresence?.realtimeEnabled === true;
110
+ const boardPresence = useBoardPresence(boardId, {
111
+ enabled: !useWorkspacePresence,
112
+ });
113
+ const activePresence = useWorkspacePresence ? wsPresence : boardPresence;
108
114
  const { openTaskById } = useTaskDialog();
109
115
  const { state: dialogState } = useTaskDialogContext();
110
116
 
111
117
  // Extract stable function refs to avoid re-running effects on every context change
112
- const wsUpdateLocation = wsPresence?.updateLocation;
113
- const wsUpdateMetadata = wsPresence?.updateMetadata;
118
+ const updateLocation = activePresence?.updateLocation;
119
+ const updateMetadata = activePresence?.updateMetadata;
114
120
 
115
121
  // Derive dialog props — only track task presence when editing a real task
116
122
  const dialogOpen = dialogState.isOpen;
@@ -124,26 +130,26 @@ export function BoardUserPresenceAvatarsComponent({
124
130
  // When the dialog is open with a real task → track { board, boardId, taskId }
125
131
  // Otherwise → track { board, boardId } (board-level only)
126
132
  useEffect(() => {
127
- if (!wsUpdateLocation || !boardId) return;
133
+ if (!updateLocation || !boardId) return;
128
134
  if (dialogOpen && dialogTaskId) {
129
- wsUpdateLocation({ type: 'board', boardId, taskId: dialogTaskId });
135
+ updateLocation({ type: 'board', boardId, taskId: dialogTaskId });
130
136
  } else {
131
- wsUpdateLocation({ type: 'board', boardId });
137
+ updateLocation({ type: 'board', boardId });
132
138
  }
133
139
 
134
140
  return () => {
135
- wsUpdateLocation({ type: 'other' });
141
+ updateLocation({ type: 'other' });
136
142
  };
137
- }, [wsUpdateLocation, boardId, dialogOpen, dialogTaskId]);
143
+ }, [updateLocation, boardId, dialogOpen, dialogTaskId]);
138
144
 
139
145
  // Update metadata (filters) separately so it doesn't overwrite task-level location
140
146
  useEffect(() => {
141
- if (!wsUpdateMetadata || !currentMetadata) return;
142
- wsUpdateMetadata(currentMetadata as Record<string, any>);
143
- }, [wsUpdateMetadata, currentMetadata]);
147
+ if (!updateMetadata || !currentMetadata) return;
148
+ updateMetadata(currentMetadata as Record<string, any>);
149
+ }, [updateMetadata, currentMetadata]);
144
150
 
145
- const boardViewers = wsPresence?.getBoardViewers(boardId) ?? [];
146
- const currentUserId = wsPresence?.currentUserId;
151
+ const boardViewers = activePresence?.getBoardViewers(boardId) ?? [];
152
+ const currentUserId = activePresence?.currentUserId;
147
153
 
148
154
  const presenceState: RealtimePresenceState<UserPresenceState> =
149
155
  useMemo(() => {