@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,94 +1,45 @@
1
1
  import { useQueryClient } from '@tanstack/react-query';
2
2
  import {
3
- Archive,
4
- ArrowDown,
5
3
  ArrowDownAZ,
6
4
  ArrowLeft,
7
- ArrowUp,
8
5
  ArrowUpAZ,
9
- Bookmark,
6
+ Bolt,
10
7
  CalendarDays,
11
- Check,
12
- ChevronDown,
13
8
  Clock,
14
- Columns3Cog,
15
- Copy,
16
9
  CopyCheck,
17
10
  Flag,
18
11
  Gauge,
19
12
  KanbanSquare,
13
+ Layers,
14
+ LayoutDashboard,
20
15
  LayoutGrid,
21
16
  List,
22
17
  Loader2,
23
- MoreHorizontal,
24
18
  Pencil,
25
19
  Play,
26
- RotateCcw,
27
20
  Search,
28
- Settings,
21
+ Share2,
29
22
  Trash2,
23
+ UserStar,
30
24
  X,
31
25
  Zap,
32
26
  } from '@tuturuuu/icons';
33
- import {
34
- deleteWorkspaceTaskBoard,
35
- updateWorkspaceTaskBoard,
36
- } from '@tuturuuu/internal-api';
27
+ import { getWorkspaceTaskBoard } from '@tuturuuu/internal-api/tasks';
37
28
  import type { WorkspaceTaskBoard } from '@tuturuuu/types';
38
29
  import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
39
- import {
40
- AlertDialog,
41
- AlertDialogAction,
42
- AlertDialogCancel,
43
- AlertDialogContent,
44
- AlertDialogDescription,
45
- AlertDialogFooter,
46
- AlertDialogHeader,
47
- AlertDialogTitle,
48
- AlertDialogTrigger,
49
- } from '@tuturuuu/ui/alert-dialog';
50
30
  import { Button } from '@tuturuuu/ui/button';
51
- import {
52
- Dialog,
53
- DialogContent,
54
- DialogDescription,
55
- DialogFooter,
56
- DialogHeader,
57
- DialogTitle,
58
- } from '@tuturuuu/ui/dialog';
59
- import {
60
- DropdownMenu,
61
- DropdownMenuContent,
62
- DropdownMenuItem,
63
- DropdownMenuSeparator,
64
- DropdownMenuShortcut,
65
- DropdownMenuSub,
66
- DropdownMenuSubContent,
67
- DropdownMenuSubTrigger,
68
- DropdownMenuTrigger,
69
- } from '@tuturuuu/ui/dropdown-menu';
70
- import { useBoardActions } from '@tuturuuu/ui/hooks/use-board-actions';
31
+ import { Combobox } from '@tuturuuu/ui/custom/combobox';
71
32
  import { Input } from '@tuturuuu/ui/input';
72
- import {
73
- Select,
74
- SelectContent,
75
- SelectItem,
76
- SelectTrigger,
77
- SelectValue,
78
- } from '@tuturuuu/ui/select';
33
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
79
34
  import { cn } from '@tuturuuu/utils/format';
80
35
  import Link from 'next/link';
81
- import { useRouter } from 'next/navigation';
36
+ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
82
37
  import { useTranslations } from 'next-intl';
83
- import { useEffect, useMemo, useRef, useState } from 'react';
38
+ import { type ReactNode, useEffect, useMemo, useRef, useState } from 'react';
39
+ import { BoardShareDialog } from '../boards/board-share-dialog';
40
+ import { KanbanPlannerDialog } from '../boards/boardId/kanban/planner/kanban-planner-dialog';
84
41
  import { TaskFilter, type TaskFilters } from '../boards/boardId/task-filter';
85
- import { CopyBoardDialog } from '../boards/copy-board-dialog';
86
- import { TaskBoardForm } from '../boards/form';
87
- import { useTasksHref } from '../tasks-route-context';
88
- import { SaveAsTemplateDialog } from '../templates/save-as-template-dialog';
89
42
  import { saveBoardConfig } from './board-config-storage';
90
- import { BoardLayoutSettings } from './board-layout-settings';
91
- import { syncBoardTicketPrefixCaches } from './board-query-cache';
92
43
  import { BoardSwitcher } from './board-switcher';
93
44
  import { BoardUserPresenceAvatarsComponent } from './board-user-presence-avatars';
94
45
  import type { ViewType } from './board-views';
@@ -102,9 +53,11 @@ interface Props {
102
53
  WorkspaceTaskBoard,
103
54
  'id' | 'name' | 'ticket_prefix' | 'archived_at'
104
55
  > & {
56
+ access_type?: 'member' | 'guest';
105
57
  ws_id?: WorkspaceTaskBoard['ws_id'] | null;
106
58
  icon?: WorkspaceTaskBoard['icon'];
107
59
  default_list_id?: WorkspaceTaskBoard['default_list_id'] | null;
60
+ has_guest_access?: boolean;
108
61
  };
109
62
  currentUserId?: string;
110
63
  currentView: ViewType;
@@ -123,6 +76,40 @@ interface Props {
123
76
  onRecycleBinOpen?: () => void;
124
77
  isMultiSelectMode: boolean;
125
78
  setIsMultiSelectMode: (enabled: boolean) => void;
79
+ availableViews?: ViewType[];
80
+ publicView?: boolean;
81
+ readOnly?: boolean;
82
+ titlePrefix?: ReactNode;
83
+ onBoardSettingsIntent?: () => void;
84
+ }
85
+
86
+ function ToolbarTooltip({
87
+ children,
88
+ label,
89
+ }: {
90
+ children: ReactNode;
91
+ label: string;
92
+ }) {
93
+ return (
94
+ <Tooltip delayDuration={0}>
95
+ <TooltipTrigger asChild>{children}</TooltipTrigger>
96
+ <TooltipContent>{label}</TooltipContent>
97
+ </Tooltip>
98
+ );
99
+ }
100
+
101
+ const toolbarButtonClass =
102
+ 'h-7 w-7 px-0 text-muted-foreground transition-colors hover:text-foreground sm:h-8 sm:w-8';
103
+ const toolbarComboboxClass =
104
+ 'w-auto [&_button]:h-7 [&_button]:w-7 [&_button]:min-w-7 [&_button]:text-muted-foreground [&_button]:transition-colors hover:[&_button]:text-foreground [&_button_svg]:text-current sm:[&_button]:h-8 sm:[&_button]:w-8 sm:[&_button]:min-w-8';
105
+ const BOARD_SETTINGS_PRELOAD_EVENT = 'tuturuuu:board-settings-intent';
106
+ const SETTINGS_DIALOG_OPEN_INTENT_EVENT =
107
+ 'tuturuuu:settings-dialog-open-intent';
108
+
109
+ function getBrowserInternalApiOptions() {
110
+ return typeof window !== 'undefined'
111
+ ? { baseUrl: window.location.origin }
112
+ : undefined;
126
113
  }
127
114
 
128
115
  export function BoardHeader({
@@ -140,35 +127,41 @@ export function BoardHeader({
140
127
  backUrl,
141
128
  hideActions = false,
142
129
  isSearching = false,
143
- lists = [],
144
- onUpdate,
145
- onRecycleBinOpen,
146
130
  isMultiSelectMode,
147
131
  setIsMultiSelectMode,
132
+ availableViews,
133
+ publicView = false,
134
+ readOnly = false,
135
+ titlePrefix,
136
+ onBoardSettingsIntent,
148
137
  }: Props) {
149
138
  const t = useTranslations();
139
+ const queryClient = useQueryClient();
150
140
  const [isLoading, setIsLoading] = useState(false);
151
- const [editBoardOpen, setEditBoardOpen] = useState(false);
152
- const [duplicateBoardOpen, setDuplicateBoardOpen] = useState(false);
153
- const [saveAsTemplateOpen, setSaveAsTemplateOpen] = useState(false);
154
- const [boardMenuOpen, setBoardMenuOpen] = useState(false);
155
- const [viewMenuOpen, setViewMenuOpen] = useState(false);
156
- const [sortMenuOpen, setSortMenuOpen] = useState(false);
157
- const [layoutSettingsOpen, setLayoutSettingsOpen] = useState(false);
158
- const [boardSettingsOpen, setBoardSettingsOpen] = useState(false);
159
- const [showArchiveDialog, setShowArchiveDialog] = useState(false);
160
- const [showUnarchiveDialog, setShowUnarchiveDialog] = useState(false);
161
- const [ticketPrefix, setTicketPrefix] = useState(board.ticket_prefix || '');
162
- const [defaultListId, setDefaultListId] = useState<string | null>(
163
- board.default_list_id ?? null
164
- );
141
+ const [shareBoardOpen, setShareBoardOpen] = useState(false);
142
+ const [plannerOpen, setPlannerOpen] = useState(false);
165
143
  const [localSearchQuery, setLocalSearchQuery] = useState(
166
144
  filters.searchQuery || ''
167
145
  );
168
- const { archiveBoard, unarchiveBoard } = useBoardActions(workspaceId);
169
- const queryClient = useQueryClient();
170
146
  const router = useRouter();
171
- const tasksHref = useTasksHref();
147
+ const pathname = usePathname();
148
+ const searchParams = useSearchParams();
149
+ const enabledViews = availableViews ?? ['kanban', 'list', 'timeline'];
150
+ const activeView = enabledViews.includes(currentView)
151
+ ? currentView
152
+ : (enabledViews[0] ?? 'kanban');
153
+ const interactiveControlsVisible = !readOnly;
154
+ const managerControlsVisible = !hideActions && !readOnly;
155
+ const plannerVisible =
156
+ interactiveControlsVisible &&
157
+ !publicView &&
158
+ isPersonalWorkspace &&
159
+ currentView === 'kanban';
160
+ const hasSharedBoardGuests =
161
+ board.access_type === 'guest' || board.has_guest_access === true;
162
+ const presenceVisible =
163
+ interactiveControlsVisible &&
164
+ (!isPersonalWorkspace || hasSharedBoardGuests);
172
165
 
173
166
  // Stable refs for callbacks and values to avoid effect re-runs
174
167
  const onFiltersChangeRef = useRef(onFiltersChange);
@@ -229,62 +222,58 @@ export function BoardHeader({
229
222
  return () => clearTimeout(timeoutId);
230
223
  }, [board.id, currentView, filters, listStatusFilter]);
231
224
 
232
- async function handleDelete() {
233
- try {
234
- setIsLoading(true);
235
- await deleteWorkspaceTaskBoard(workspaceId, board.id);
236
- router.push(`/${workspaceId}${tasksHref('/boards')}`);
237
- } catch (error) {
238
- console.error('Failed to delete board:', error);
239
- } finally {
240
- setIsLoading(false);
241
- }
225
+ function handleSortChange(sortBy: TaskFilters['sortBy']) {
226
+ onFiltersChange({ ...filters, sortBy });
242
227
  }
243
228
 
244
- async function handleSaveTicketPrefix() {
245
- try {
246
- setIsLoading(true);
247
-
248
- // Validate and clean the prefix
249
- const cleanedPrefix = ticketPrefix.trim().toUpperCase();
250
- const nextTicketPrefix = cleanedPrefix || null;
229
+ function openBoardSettings() {
230
+ prefetchBoardSettings();
231
+ announceSettingsOpenIntent();
232
+ const params = new URLSearchParams(searchParams.toString());
233
+ params.set('settingsDialog', 'open');
234
+ params.set('settingsTab', 'task_board');
235
+ params.set('settingsBoardId', board.id);
236
+ router.replace(`${pathname}?${params.toString()}`, { scroll: false });
237
+ }
251
238
 
252
- await updateWorkspaceTaskBoard(workspaceId, board.id, {
253
- ticket_prefix: nextTicketPrefix,
254
- default_list_id: defaultListId,
255
- });
239
+ function prefetchBoardSettings() {
240
+ if (!managerControlsVisible) return;
256
241
 
257
- syncBoardTicketPrefixCaches({
258
- queryClient,
259
- workspaceId,
260
- board,
261
- ticketPrefix: nextTicketPrefix,
262
- });
242
+ onBoardSettingsIntent?.();
243
+ if (typeof window !== 'undefined') {
244
+ window.dispatchEvent(new Event(BOARD_SETTINGS_PRELOAD_EVENT));
245
+ }
263
246
 
264
- setBoardSettingsOpen(false);
247
+ void queryClient.prefetchQuery({
248
+ queryKey: ['task-board-settings', workspaceId, board.id],
249
+ queryFn: async () => {
250
+ const payload = await getWorkspaceTaskBoard(
251
+ workspaceId,
252
+ board.id,
253
+ getBrowserInternalApiOptions()
254
+ );
255
+ return payload.board;
256
+ },
257
+ staleTime: 30_000,
258
+ });
259
+ }
265
260
 
266
- // Invalidate relevant caches
267
- queryClient.invalidateQueries({
268
- queryKey: ['task-board', workspaceId, board.id],
269
- });
270
- queryClient.invalidateQueries({
271
- queryKey: ['board-config', workspaceId, board.id],
272
- });
261
+ function announceSettingsOpenIntent() {
262
+ if (typeof window === 'undefined') return;
273
263
 
274
- // Trigger parent update
275
- if (onUpdate) {
276
- onUpdate();
277
- }
278
- } catch (error) {
279
- console.error('Failed to update ticket prefix:', error);
280
- } finally {
281
- setIsLoading(false);
282
- }
264
+ window.dispatchEvent(
265
+ new CustomEvent(SETTINGS_DIALOG_OPEN_INTENT_EVENT, {
266
+ detail: {
267
+ settingsBoardId: board.id,
268
+ settingsTab: 'task_board',
269
+ },
270
+ })
271
+ );
283
272
  }
284
273
 
285
- function handleSortChange(sortBy: TaskFilters['sortBy']) {
286
- onFiltersChange({ ...filters, sortBy });
287
- setSortMenuOpen(false);
274
+ function handleBoardSettingsPointerDown() {
275
+ prefetchBoardSettings();
276
+ announceSettingsOpenIntent();
288
277
  }
289
278
 
290
279
  function handleSmartFocus() {
@@ -348,8 +337,139 @@ export function BoardHeader({
348
337
  label: t('ws-task-boards.views.timeline'),
349
338
  description: t('ws-task-boards.views.timeline_description'),
350
339
  },
340
+ my_tasks: {
341
+ icon: UserStar,
342
+ label: t('ws-task-boards.views.my_tasks'),
343
+ description: t('ws-task-boards.views.my_tasks_description'),
344
+ },
345
+ drafts: {
346
+ icon: Pencil,
347
+ label: t('task-drafts.title'),
348
+ description: t('task-drafts.board_view_description'),
349
+ },
350
+ recycle_bin: {
351
+ icon: Trash2,
352
+ label: t('common.recycle_bin'),
353
+ description: t('common.recycle_bin_board_description'),
354
+ },
351
355
  };
356
+ const viewOptions = Object.entries(viewConfig).filter(([view]) =>
357
+ enabledViews.includes(view as ViewType)
358
+ );
359
+ const listStatusOptions = [
360
+ {
361
+ value: 'all',
362
+ label: t('common.all'),
363
+ icon: <LayoutGrid className="h-3.5 w-3.5" />,
364
+ },
365
+ {
366
+ value: 'active',
367
+ label: t('common.active'),
368
+ icon: <Play className="h-3.5 w-3.5" />,
369
+ },
370
+ {
371
+ value: 'not_started',
372
+ label: t('common.backlog'),
373
+ icon: <Clock className="h-3.5 w-3.5" />,
374
+ },
375
+ ];
376
+ const viewComboboxOptions = viewOptions.map(([view, config]) => {
377
+ const Icon = config.icon;
378
+ const hotkeyLabel = viewHotkeyLabels?.[view as ViewType];
352
379
 
380
+ return {
381
+ value: view,
382
+ label: config.label,
383
+ description: config.description,
384
+ icon: <Icon className="h-3.5 w-3.5" />,
385
+ badge: hotkeyLabel ? (
386
+ <span className="text-muted-foreground text-xs">{hotkeyLabel}</span>
387
+ ) : undefined,
388
+ };
389
+ });
390
+ const sortOptions = [
391
+ {
392
+ value: '__none__',
393
+ label: t('common.sort'),
394
+ description: filters.sortBy
395
+ ? t('ws-task-boards.filters.sort_options.clear_sorting')
396
+ : undefined,
397
+ icon: <ArrowUpAZ className="h-3.5 w-3.5" />,
398
+ muted: !filters.sortBy,
399
+ },
400
+ {
401
+ value: 'name-asc',
402
+ label: `${t('ws-task-boards.filters.sort.name')} · ${t(
403
+ 'ws-task-boards.filters.sort_order.asc'
404
+ )}`,
405
+ icon: <ArrowUpAZ className="h-3.5 w-3.5" />,
406
+ },
407
+ {
408
+ value: 'name-desc',
409
+ label: `${t('ws-task-boards.filters.sort.name')} · ${t(
410
+ 'ws-task-boards.filters.sort_order.desc'
411
+ )}`,
412
+ icon: <ArrowDownAZ className="h-3.5 w-3.5" />,
413
+ },
414
+ {
415
+ value: 'priority-high',
416
+ label: t('ws-task-boards.filters.sort_options.high_to_low'),
417
+ description: t('ws-task-boards.filters.sort_options.priority'),
418
+ icon: <Flag className="h-3.5 w-3.5" />,
419
+ },
420
+ {
421
+ value: 'priority-low',
422
+ label: t('ws-task-boards.filters.sort_options.low_to_high'),
423
+ description: t('ws-task-boards.filters.sort_options.priority'),
424
+ icon: <Flag className="h-3.5 w-3.5" />,
425
+ },
426
+ {
427
+ value: 'due-date-asc',
428
+ label: t('ws-task-boards.filters.sort_options.soonest_first'),
429
+ description: t('ws-task-boards.filters.sort_options.due_date'),
430
+ icon: <CalendarDays className="h-3.5 w-3.5" />,
431
+ },
432
+ {
433
+ value: 'due-date-desc',
434
+ label: t('ws-task-boards.filters.sort_options.latest_first'),
435
+ description: t('ws-task-boards.filters.sort_options.due_date'),
436
+ icon: <CalendarDays className="h-3.5 w-3.5" />,
437
+ },
438
+ {
439
+ value: 'created-date-desc',
440
+ label: t('ws-task-boards.filters.sort_options.newest_first'),
441
+ description: t('ws-task-boards.filters.sort.created_at'),
442
+ icon: <Clock className="h-3.5 w-3.5" />,
443
+ },
444
+ {
445
+ value: 'created-date-asc',
446
+ label: t('ws-task-boards.filters.sort_options.oldest_first'),
447
+ description: t('ws-task-boards.filters.sort.created_at'),
448
+ icon: <Clock className="h-3.5 w-3.5" />,
449
+ },
450
+ {
451
+ value: 'estimation-high',
452
+ label: t('ws-task-boards.filters.sort_options.highest_first'),
453
+ description: t('ws-task-boards.filters.sort_options.estimate'),
454
+ icon: <Gauge className="h-3.5 w-3.5" />,
455
+ },
456
+ {
457
+ value: 'estimation-low',
458
+ label: t('ws-task-boards.filters.sort_options.lowest_first'),
459
+ description: t('ws-task-boards.filters.sort_options.estimate'),
460
+ icon: <Gauge className="h-3.5 w-3.5" />,
461
+ },
462
+ ];
463
+ const selectedListStatusOption =
464
+ listStatusOptions.find((option) => option.value === listStatusFilter) ??
465
+ listStatusOptions[0];
466
+ const selectedViewOption =
467
+ viewComboboxOptions.find((option) => option.value === activeView) ??
468
+ viewComboboxOptions[0];
469
+ const selectedSortOption =
470
+ sortOptions.find(
471
+ (option) => option.value === (filters.sortBy ?? '__none__')
472
+ ) ?? sortOptions[0];
353
473
  // Create metadata for presence tracking (excludes search query for stability)
354
474
  const presenceMetadata: BoardFiltersMetadata = useMemo(() => {
355
475
  const { searchQuery: _, ...filtersWithoutSearch } = filters;
@@ -372,26 +492,46 @@ export function BoardHeader({
372
492
  <ArrowLeft className="h-5 w-5" />
373
493
  </Link>
374
494
  )}
375
- <BoardSwitcher
376
- board={{ ...board, ws_id: workspaceId }}
377
- translations={{
378
- loadingBoards: t('common.loading'),
379
- noOtherBoards: t('common.no_other_boards'),
380
- activeBoards: t('common.active_boards'),
381
- archivedBoards: t('common.archived_boards'),
382
- deletedBoards: t('common.deleted_boards'),
383
- untitled: t('common.untitled'),
384
- active: t('common.active'),
385
- archived: t('common.archived'),
386
- deleted: t('common.deleted'),
387
- daysLeft: t('common.days_left', { count: '{count}' }),
388
- tasks: t('common.tasks'),
389
- }}
390
- />
495
+ {publicView ? (
496
+ <div className="flex min-w-0 items-center gap-2">
497
+ {titlePrefix}
498
+ <h1 className="truncate font-semibold text-foreground text-sm">
499
+ {board.name || t('common.untitled')}
500
+ </h1>
501
+ </div>
502
+ ) : currentView === 'my_tasks' ? (
503
+ <div className="flex h-7 min-w-0 items-center gap-2 rounded-md border bg-background px-2 text-foreground sm:h-8">
504
+ <UserStar className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
505
+ <span className="truncate font-semibold text-sm">
506
+ {t('ws-task-boards.views.my_tasks')}
507
+ </span>
508
+ </div>
509
+ ) : (
510
+ <BoardSwitcher
511
+ board={{ ...board, ws_id: workspaceId }}
512
+ translations={{
513
+ loadingBoards: t('common.loading'),
514
+ noOtherBoards: t('common.no_other_boards'),
515
+ activeBoards: t('common.active_boards'),
516
+ archivedBoards: t('common.archived_boards'),
517
+ deletedBoards: t('common.deleted_boards'),
518
+ untitled: t('common.untitled'),
519
+ active: t('common.active'),
520
+ archived: t('common.archived'),
521
+ deleted: t('common.deleted'),
522
+ daysLeft: t('common.days_left', { count: '{count}' }),
523
+ searchBoards: t('common.search_boards'),
524
+ tasks: t('common.tasks'),
525
+ createBoard: t('ws-task-boards.create'),
526
+ creatingBoard: t('common.creating'),
527
+ createBoardError: t('ws-task-boards.errors.unexpected'),
528
+ }}
529
+ />
530
+ )}
391
531
  </div>
392
532
 
393
533
  {/* Search Bar */}
394
- <div className="relative max-w-md flex-1">
534
+ <div className="relative min-w-0 flex-1 basis-72">
395
535
  {isSearching ? (
396
536
  <Loader2 className="pointer-events-none absolute top-1/2 left-2 h-4 w-4 -translate-y-1/2 animate-spin text-muted-foreground" />
397
537
  ) : (
@@ -417,9 +557,9 @@ export function BoardHeader({
417
557
  </div>
418
558
 
419
559
  {/* Controls - Compact Row */}
420
- <div className="flex items-center gap-1.5 sm:gap-2">
560
+ <div className="flex shrink-0 items-center gap-1.5 sm:gap-2">
421
561
  {/* Online Users */}
422
- {!isPersonalWorkspace && (
562
+ {presenceVisible && (
423
563
  <BoardUserPresenceAvatarsComponent
424
564
  boardId={board.id}
425
565
  currentMetadata={presenceMetadata}
@@ -429,859 +569,215 @@ export function BoardHeader({
429
569
  )}
430
570
 
431
571
  {/* Smart Focus Button */}
432
- <Button
433
- variant={isSmartFocusActive ? 'secondary' : 'outline'}
434
- size="xs"
435
- onClick={handleSmartFocus}
436
- disabled={isLoading}
437
- className={cn(
438
- 'h-7 px-1.5 transition-colors sm:h-8 sm:px-2',
439
- isSmartFocusActive
440
- ? 'border-dynamic-yellow/20 bg-dynamic-yellow/10 text-dynamic-yellow hover:bg-dynamic-yellow/20'
441
- : 'text-muted-foreground hover:text-dynamic-yellow'
442
- )}
443
- title={
444
- isSmartFocusActive
445
- ? t('common.clear_smart_focus')
446
- : t('common.smart_focus')
447
- }
448
- >
449
- <Zap
450
- className={cn(
451
- 'h-3.5 w-3.5',
452
- isSmartFocusActive && 'fill-current'
453
- )}
454
- />
455
- </Button>
572
+ {interactiveControlsVisible && (
573
+ <ToolbarTooltip
574
+ label={
575
+ isSmartFocusActive
576
+ ? t('common.clear_smart_focus')
577
+ : t('common.smart_focus')
578
+ }
579
+ >
580
+ <Button
581
+ variant={isSmartFocusActive ? 'secondary' : 'outline'}
582
+ size="xs"
583
+ onClick={handleSmartFocus}
584
+ disabled={isLoading}
585
+ className={cn(
586
+ toolbarButtonClass,
587
+ isSmartFocusActive
588
+ ? 'border-primary/50 bg-primary/5 text-foreground hover:bg-primary/10'
589
+ : 'hover:bg-accent'
590
+ )}
591
+ aria-label={
592
+ isSmartFocusActive
593
+ ? t('common.clear_smart_focus')
594
+ : t('common.smart_focus')
595
+ }
596
+ >
597
+ <Zap
598
+ className={cn(
599
+ 'h-3.5 w-3.5',
600
+ isSmartFocusActive && 'fill-current text-foreground'
601
+ )}
602
+ />
603
+ </Button>
604
+ </ToolbarTooltip>
605
+ )}
456
606
 
457
607
  {/* Multi-select Toggle */}
458
- <Button
459
- variant={isMultiSelectMode ? 'secondary' : 'outline'}
460
- size="xs"
461
- onClick={() => setIsMultiSelectMode(!isMultiSelectMode)}
462
- className={cn(
463
- 'h-7 px-1.5 sm:h-8 sm:px-2',
464
- isMultiSelectMode &&
465
- 'bg-primary/10 text-primary hover:bg-primary/20'
466
- )}
467
- title={t('common.choose_tasks')}
468
- >
469
- <CopyCheck
470
- className={cn('h-3.5 w-3.5', isMultiSelectMode && 'text-primary')}
471
- />
472
- </Button>
608
+ {interactiveControlsVisible && (
609
+ <ToolbarTooltip label={t('common.choose_tasks')}>
610
+ <Button
611
+ variant={isMultiSelectMode ? 'secondary' : 'outline'}
612
+ size="xs"
613
+ onClick={() => setIsMultiSelectMode(!isMultiSelectMode)}
614
+ className={cn(
615
+ toolbarButtonClass,
616
+ isMultiSelectMode &&
617
+ 'border-primary/50 bg-primary/5 text-foreground hover:bg-primary/10'
618
+ )}
619
+ aria-label={t('common.choose_tasks')}
620
+ >
621
+ <CopyCheck className="h-3.5 w-3.5" />
622
+ </Button>
623
+ </ToolbarTooltip>
624
+ )}
473
625
 
474
626
  {/* List Status Filter */}
475
- <Select
476
- value={listStatusFilter}
477
- onValueChange={(value) =>
627
+ <Combobox
628
+ mode="single"
629
+ options={listStatusOptions}
630
+ selected={listStatusFilter}
631
+ onChange={(value) =>
478
632
  onListStatusFilterChange(value as ListStatusFilter)
479
633
  }
480
- >
481
- <SelectTrigger
482
- className={cn(
483
- 'h-7 w-auto gap-1 bg-background px-2 text-[10px] sm:h-8 sm:px-2.5 sm:text-xs',
484
- listStatusFilter !== 'all' && 'border-primary/50 bg-primary/5'
485
- )}
486
- >
487
- <SelectValue />
488
- </SelectTrigger>
489
- <SelectContent>
490
- <SelectItem value="all">
491
- <div className="flex items-center gap-2">
492
- <LayoutGrid className="h-3.5 w-3.5 text-foreground" />
493
- <span>{t('common.all')}</span>
494
- </div>
495
- </SelectItem>
496
- <SelectItem value="active">
497
- <div className="flex items-center gap-2">
498
- <Play className="h-3.5 w-3.5 text-dynamic-green" />
499
- <span>{t('common.active')}</span>
500
- </div>
501
- </SelectItem>
502
- <SelectItem value="not_started">
503
- <div className="flex items-center gap-2">
504
- <Clock className="h-3.5 w-3.5 text-dynamic-orange" />
505
- <span>{t('common.backlog')}</span>
506
- </div>
507
- </SelectItem>
508
- </SelectContent>
509
- </Select>
634
+ ariaLabel={selectedListStatusOption?.label ?? t('common.all')}
635
+ contentWidth="sm"
636
+ hideTriggerLabel
637
+ placeholder={t('common.all')}
638
+ searchPlaceholder={t('common.search_tasks')}
639
+ showChevron={false}
640
+ triggerMode="compact"
641
+ triggerTooltip={`${t('common.status')}: ${selectedListStatusOption?.label ?? t('common.all')}`}
642
+ triggerIcon={<Layers className="h-3.5 w-3.5" />}
643
+ colorizeTriggerIcon={false}
644
+ className={cn(
645
+ toolbarComboboxClass,
646
+ listStatusFilter !== 'all' &&
647
+ '[&_button]:border-primary/50 [&_button]:bg-primary/5'
648
+ )}
649
+ />
510
650
 
511
- {/* View Switcher Dropdown */}
512
- <DropdownMenu open={viewMenuOpen} onOpenChange={setViewMenuOpen}>
513
- <DropdownMenuTrigger asChild>
514
- <Button size="xs" variant="outline">
515
- {(() => {
516
- const Icon = viewConfig[currentView].icon;
517
- return (
518
- <>
519
- <Icon className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
520
- <span className="hidden text-[10px] sm:text-xs md:inline">
521
- {viewConfig[currentView].label}
522
- </span>
523
- <ChevronDown className="h-3 w-3 opacity-50 sm:h-3.5 sm:w-3.5" />
524
- </>
525
- );
526
- })()}
527
- </Button>
528
- </DropdownMenuTrigger>
529
- <DropdownMenuContent align="end">
530
- {Object.entries(viewConfig).map(([view, config]) => {
531
- const Icon = config.icon;
532
- return (
533
- <DropdownMenuItem
534
- key={view}
535
- onClick={() => {
536
- onViewChange(view as ViewType);
537
- setViewMenuOpen(false);
538
- }}
539
- className="gap-3"
540
- >
541
- <Icon className="h-4 w-4" />
542
- <div className="flex flex-1 flex-col">
543
- <span className="font-medium">{config.label}</span>
544
- <span className="text-muted-foreground text-xs">
545
- {config.description}
546
- </span>
547
- </div>
548
- {viewHotkeyLabels?.[view as ViewType] && (
549
- <DropdownMenuShortcut className="self-start pt-0.5">
550
- {viewHotkeyLabels[view as ViewType]}
551
- </DropdownMenuShortcut>
552
- )}
553
- </DropdownMenuItem>
554
- );
555
- })}
556
- </DropdownMenuContent>
557
- </DropdownMenu>
651
+ {/* View Switcher */}
652
+ <Combobox
653
+ mode="single"
654
+ options={viewComboboxOptions}
655
+ selected={activeView}
656
+ onChange={(value) => onViewChange(value as ViewType)}
657
+ ariaLabel={
658
+ selectedViewOption?.label ?? viewConfig[activeView].label
659
+ }
660
+ contentWidth="md"
661
+ hideTriggerLabel
662
+ placeholder={viewConfig[activeView].label}
663
+ searchPlaceholder={t('common.search_tasks')}
664
+ showChevron={false}
665
+ triggerMode="compact"
666
+ triggerTooltip={`${t('common.view')}: ${
667
+ selectedViewOption?.label ?? viewConfig[activeView].label
668
+ }`}
669
+ triggerIcon={<LayoutDashboard className="h-3.5 w-3.5" />}
670
+ colorizeTriggerIcon={false}
671
+ className={toolbarComboboxClass}
672
+ />
558
673
 
559
674
  {/* Task Filter */}
560
- <TaskFilter
561
- wsId={workspaceId}
562
- currentUserId={currentUserId}
563
- filters={filters}
564
- onFiltersChange={onFiltersChange}
675
+ {interactiveControlsVisible && (
676
+ <TaskFilter
677
+ wsId={workspaceId}
678
+ currentUserId={currentUserId}
679
+ filters={filters}
680
+ onFiltersChange={onFiltersChange}
681
+ />
682
+ )}
683
+
684
+ {/* Sort */}
685
+ <Combobox
686
+ mode="single"
687
+ options={sortOptions}
688
+ selected={filters.sortBy ?? '__none__'}
689
+ onChange={(value) =>
690
+ handleSortChange(
691
+ value === '__none__'
692
+ ? undefined
693
+ : (value as TaskFilters['sortBy'])
694
+ )
695
+ }
696
+ ariaLabel={selectedSortOption?.label ?? t('common.sort')}
697
+ contentWidth="md"
698
+ hideTriggerLabel
699
+ placeholder={t('common.sort')}
700
+ searchPlaceholder={t('common.search_tasks')}
701
+ showChevron={false}
702
+ triggerMode="compact"
703
+ triggerTooltip={`${t('common.sort')}: ${
704
+ selectedSortOption?.value === '__none__'
705
+ ? t('common.sort')
706
+ : (selectedSortOption?.label ?? t('common.sort'))
707
+ }`}
708
+ colorizeTriggerIcon={false}
709
+ className={cn(
710
+ toolbarComboboxClass,
711
+ filters.sortBy &&
712
+ '[&_button]:border-primary/50 [&_button]:bg-primary/5'
713
+ )}
565
714
  />
566
715
 
567
- {/* Sort Dropdown */}
568
- <DropdownMenu open={sortMenuOpen} onOpenChange={setSortMenuOpen}>
569
- <DropdownMenuTrigger asChild>
716
+ {plannerVisible && (
717
+ <ToolbarTooltip label={t('ws-task-plans.planner')}>
570
718
  <Button
719
+ type="button"
571
720
  size="xs"
572
721
  variant="outline"
573
- className={cn(
574
- 'text-[10px] sm:text-xs',
575
- filters.sortBy && 'border-primary/50 bg-primary/5'
576
- )}
722
+ className={toolbarButtonClass}
723
+ onClick={() => setPlannerOpen(true)}
724
+ aria-label={t('ws-task-plans.planner')}
577
725
  >
578
- {filters.sortBy ? (
579
- <ArrowDownAZ className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
580
- ) : (
581
- <ArrowUpAZ className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
582
- )}
583
- <span className="hidden sm:inline">{t('common.sort')}</span>
726
+ <CalendarDays className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
584
727
  </Button>
585
- </DropdownMenuTrigger>
586
- <DropdownMenuContent align="end" className="w-50">
587
- {/* Name */}
588
- <DropdownMenuSub>
589
- <DropdownMenuSubTrigger className="gap-2">
590
- <ArrowUpAZ className="h-4 w-4 text-muted-foreground" />
591
- <span className="flex-1">
592
- {t('ws-task-boards.filters.sort.name')}
593
- </span>
594
- {(filters.sortBy === 'name-asc' ||
595
- filters.sortBy === 'name-desc') && (
596
- <Check className="h-3.5 w-3.5 text-primary" />
597
- )}
598
- </DropdownMenuSubTrigger>
599
- <DropdownMenuSubContent>
600
- <DropdownMenuItem
601
- onClick={() =>
602
- handleSortChange(
603
- filters.sortBy === 'name-asc' ? undefined : 'name-asc'
604
- )
605
- }
606
- className="gap-2"
607
- >
608
- <ArrowUp className="h-3.5 w-3.5 text-dynamic-blue" />
609
- <span className="flex-1">
610
- {t('ws-task-boards.filters.sort_order.asc')}
611
- </span>
612
- {filters.sortBy === 'name-asc' && (
613
- <Check className="h-4 w-4 text-primary" />
614
- )}
615
- </DropdownMenuItem>
616
- <DropdownMenuItem
617
- onClick={() =>
618
- handleSortChange(
619
- filters.sortBy === 'name-desc' ? undefined : 'name-desc'
620
- )
621
- }
622
- className="gap-2"
623
- >
624
- <ArrowDown className="h-3.5 w-3.5 text-dynamic-purple" />
625
- <span className="flex-1">
626
- {t('ws-task-boards.filters.sort_order.desc')}
627
- </span>
628
- {filters.sortBy === 'name-desc' && (
629
- <Check className="h-4 w-4 text-primary" />
630
- )}
631
- </DropdownMenuItem>
632
- </DropdownMenuSubContent>
633
- </DropdownMenuSub>
634
-
635
- {/* Priority */}
636
- <DropdownMenuSub>
637
- <DropdownMenuSubTrigger className="gap-2">
638
- <Flag className="h-4 w-4 text-dynamic-red" />
639
- <span className="flex-1">
640
- {t('ws-task-boards.filters.sort_options.priority')}
641
- </span>
642
- {(filters.sortBy === 'priority-high' ||
643
- filters.sortBy === 'priority-low') && (
644
- <Check className="h-3.5 w-3.5 text-primary" />
645
- )}
646
- </DropdownMenuSubTrigger>
647
- <DropdownMenuSubContent>
648
- <DropdownMenuItem
649
- onClick={() =>
650
- handleSortChange(
651
- filters.sortBy === 'priority-high'
652
- ? undefined
653
- : 'priority-high'
654
- )
655
- }
656
- className="gap-2"
657
- >
658
- <ArrowUp className="h-3.5 w-3.5 text-dynamic-red" />
659
- <span className="flex-1">
660
- {t('ws-task-boards.filters.sort_options.high_to_low')}
661
- </span>
662
- {filters.sortBy === 'priority-high' && (
663
- <Check className="h-4 w-4 text-primary" />
664
- )}
665
- </DropdownMenuItem>
666
- <DropdownMenuItem
667
- onClick={() =>
668
- handleSortChange(
669
- filters.sortBy === 'priority-low'
670
- ? undefined
671
- : 'priority-low'
672
- )
673
- }
674
- className="gap-2"
675
- >
676
- <ArrowDown className="h-3.5 w-3.5 text-dynamic-gray" />
677
- <span className="flex-1">
678
- {t('ws-task-boards.filters.sort_options.low_to_high')}
679
- </span>
680
- {filters.sortBy === 'priority-low' && (
681
- <Check className="h-4 w-4 text-primary" />
682
- )}
683
- </DropdownMenuItem>
684
- </DropdownMenuSubContent>
685
- </DropdownMenuSub>
686
-
687
- {/* Due Date */}
688
- <DropdownMenuSub>
689
- <DropdownMenuSubTrigger className="gap-2">
690
- <CalendarDays className="h-4 w-4 text-dynamic-orange" />
691
- <span className="flex-1">
692
- {t('ws-task-boards.filters.sort_options.due_date')}
693
- </span>
694
- {(filters.sortBy === 'due-date-asc' ||
695
- filters.sortBy === 'due-date-desc') && (
696
- <Check className="h-3.5 w-3.5 text-primary" />
697
- )}
698
- </DropdownMenuSubTrigger>
699
- <DropdownMenuSubContent>
700
- <DropdownMenuItem
701
- onClick={() =>
702
- handleSortChange(
703
- filters.sortBy === 'due-date-asc'
704
- ? undefined
705
- : 'due-date-asc'
706
- )
707
- }
708
- className="gap-2"
709
- >
710
- <ArrowUp className="h-3.5 w-3.5 text-dynamic-orange" />
711
- <span className="flex-1">
712
- {t('ws-task-boards.filters.sort_options.soonest_first')}
713
- </span>
714
- {filters.sortBy === 'due-date-asc' && (
715
- <Check className="h-4 w-4 text-primary" />
716
- )}
717
- </DropdownMenuItem>
718
- <DropdownMenuItem
719
- onClick={() =>
720
- handleSortChange(
721
- filters.sortBy === 'due-date-desc'
722
- ? undefined
723
- : 'due-date-desc'
724
- )
725
- }
726
- className="gap-2"
727
- >
728
- <ArrowDown className="h-3.5 w-3.5 text-dynamic-blue" />
729
- <span className="flex-1">
730
- {t('ws-task-boards.filters.sort_options.latest_first')}
731
- </span>
732
- {filters.sortBy === 'due-date-desc' && (
733
- <Check className="h-4 w-4 text-primary" />
734
- )}
735
- </DropdownMenuItem>
736
- </DropdownMenuSubContent>
737
- </DropdownMenuSub>
738
-
739
- {/* Created Date */}
740
- <DropdownMenuSub>
741
- <DropdownMenuSubTrigger className="gap-2">
742
- <Clock className="h-4 w-4 text-dynamic-green" />
743
- <span className="flex-1">
744
- {t('ws-task-boards.filters.sort.created_at')}
745
- </span>
746
- {(filters.sortBy === 'created-date-desc' ||
747
- filters.sortBy === 'created-date-asc') && (
748
- <Check className="h-3.5 w-3.5 text-primary" />
749
- )}
750
- </DropdownMenuSubTrigger>
751
- <DropdownMenuSubContent>
752
- <DropdownMenuItem
753
- onClick={() =>
754
- handleSortChange(
755
- filters.sortBy === 'created-date-desc'
756
- ? undefined
757
- : 'created-date-desc'
758
- )
759
- }
760
- className="gap-2"
761
- >
762
- <ArrowDown className="h-3.5 w-3.5 text-dynamic-green" />
763
- <span className="flex-1">
764
- {t('ws-task-boards.filters.sort_options.newest_first')}
765
- </span>
766
- {filters.sortBy === 'created-date-desc' && (
767
- <Check className="h-4 w-4 text-primary" />
768
- )}
769
- </DropdownMenuItem>
770
- <DropdownMenuItem
771
- onClick={() =>
772
- handleSortChange(
773
- filters.sortBy === 'created-date-asc'
774
- ? undefined
775
- : 'created-date-asc'
776
- )
777
- }
778
- className="gap-2"
779
- >
780
- <ArrowUp className="h-3.5 w-3.5 text-muted-foreground" />
781
- <span className="flex-1">
782
- {t('ws-task-boards.filters.sort_options.oldest_first')}
783
- </span>
784
- {filters.sortBy === 'created-date-asc' && (
785
- <Check className="h-4 w-4 text-primary" />
786
- )}
787
- </DropdownMenuItem>
788
- </DropdownMenuSubContent>
789
- </DropdownMenuSub>
790
-
791
- {/* Estimation Points */}
792
- <DropdownMenuSub>
793
- <DropdownMenuSubTrigger className="gap-2">
794
- <Gauge className="h-4 w-4 text-dynamic-purple" />
795
- <span className="flex-1">
796
- {t('ws-task-boards.filters.sort_options.estimate')}
797
- </span>
798
- {(filters.sortBy === 'estimation-high' ||
799
- filters.sortBy === 'estimation-low') && (
800
- <Check className="h-3.5 w-3.5 text-primary" />
801
- )}
802
- </DropdownMenuSubTrigger>
803
- <DropdownMenuSubContent>
804
- <DropdownMenuItem
805
- onClick={() =>
806
- handleSortChange(
807
- filters.sortBy === 'estimation-high'
808
- ? undefined
809
- : 'estimation-high'
810
- )
811
- }
812
- className="gap-2"
813
- >
814
- <ArrowUp className="h-3.5 w-3.5 text-dynamic-purple" />
815
- <span className="flex-1">
816
- {t('ws-task-boards.filters.sort_options.highest_first')}
817
- </span>
818
- {filters.sortBy === 'estimation-high' && (
819
- <Check className="h-4 w-4 text-primary" />
820
- )}
821
- </DropdownMenuItem>
822
- <DropdownMenuItem
823
- onClick={() =>
824
- handleSortChange(
825
- filters.sortBy === 'estimation-low'
826
- ? undefined
827
- : 'estimation-low'
828
- )
829
- }
830
- className="gap-2"
831
- >
832
- <ArrowDown className="h-3.5 w-3.5 text-dynamic-cyan" />
833
- <span className="flex-1">
834
- {t('ws-task-boards.filters.sort_options.lowest_first')}
835
- </span>
836
- {filters.sortBy === 'estimation-low' && (
837
- <Check className="h-4 w-4 text-primary" />
838
- )}
839
- </DropdownMenuItem>
840
- </DropdownMenuSubContent>
841
- </DropdownMenuSub>
728
+ </ToolbarTooltip>
729
+ )}
842
730
 
843
- {filters.sortBy && (
844
- <>
845
- <DropdownMenuSeparator />
846
- <DropdownMenuItem
847
- onClick={() => handleSortChange(undefined)}
848
- className="gap-2 text-dynamic-red/80 focus:text-dynamic-red"
849
- >
850
- <X className="h-4 w-4" />
851
- <span>
852
- {t('ws-task-boards.filters.sort_options.clear_sorting')}
853
- </span>
854
- </DropdownMenuItem>
855
- </>
856
- )}
857
- </DropdownMenuContent>
858
- </DropdownMenu>
731
+ {managerControlsVisible && (
732
+ <ToolbarTooltip label={t('ws-task-boards.share.action')}>
733
+ <Button
734
+ type="button"
735
+ size="xs"
736
+ variant="outline"
737
+ className={toolbarButtonClass}
738
+ onClick={() => setShareBoardOpen(true)}
739
+ aria-label={t('ws-task-boards.share.action')}
740
+ >
741
+ <Share2 className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
742
+ </Button>
743
+ </ToolbarTooltip>
744
+ )}
859
745
 
860
- {/* Board Actions Menu */}
861
- {!hideActions && (
862
- <DropdownMenu open={boardMenuOpen} onOpenChange={setBoardMenuOpen}>
863
- <DropdownMenuTrigger asChild>
864
- <Button
865
- variant="ghost"
866
- size="icon"
867
- className="h-6 w-6 transition-all hover:bg-muted sm:h-7 sm:w-7"
868
- >
869
- <MoreHorizontal className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
870
- <span className="sr-only">Open board menu</span>
871
- </Button>
872
- </DropdownMenuTrigger>
873
- <DropdownMenuContent align="end" className="w-50">
874
- <DropdownMenuItem
875
- onClick={() => {
876
- setEditBoardOpen(true);
877
- setBoardMenuOpen(false);
878
- }}
879
- className="gap-2"
880
- >
881
- <Pencil className="h-4 w-4" />
882
- {t('common.edit')}
883
- </DropdownMenuItem>
884
- <DropdownMenuSeparator />
885
- <DropdownMenuItem
886
- onClick={() => {
887
- setDuplicateBoardOpen(true);
888
- setBoardMenuOpen(false);
889
- }}
890
- className="gap-2"
891
- >
892
- <Copy className="h-4 w-4" />
893
- {t('ws-task-boards.actions.duplicate')}
894
- </DropdownMenuItem>
895
- <DropdownMenuItem
896
- onClick={() => {
897
- setSaveAsTemplateOpen(true);
898
- setBoardMenuOpen(false);
899
- }}
900
- className="gap-2"
901
- >
902
- <Bookmark className="h-4 w-4" />
903
- {t('ws-task-boards.actions.save_as_template')}
904
- </DropdownMenuItem>
905
- <DropdownMenuSeparator />
906
- <DropdownMenuItem
907
- onClick={() => {
908
- setLayoutSettingsOpen(true);
909
- setBoardMenuOpen(false);
910
- }}
911
- className="gap-2"
912
- >
913
- <Columns3Cog className="h-4 w-4" />
914
- {t('ws-task-boards.actions.board_layout')}
915
- </DropdownMenuItem>
916
- <DropdownMenuItem
917
- onClick={() => {
918
- setTicketPrefix(board.ticket_prefix || '');
919
- setDefaultListId(board.default_list_id ?? null);
920
- setBoardSettingsOpen(true);
921
- setBoardMenuOpen(false);
922
- }}
923
- className="gap-2"
924
- >
925
- <Settings className="h-4 w-4" />
926
- {t('ws-task-boards.actions.board_settings')}
927
- </DropdownMenuItem>
928
- {(onRecycleBinOpen || board.archived_at) && (
929
- <DropdownMenuSeparator />
930
- )}
931
- {onRecycleBinOpen && (
932
- <DropdownMenuItem
933
- onClick={() => {
934
- onRecycleBinOpen();
935
- setBoardMenuOpen(false);
936
- }}
937
- className="gap-2"
938
- >
939
- <Trash2 className="h-4 w-4" />
940
- {t('ws-task-boards.actions.recycle_bin')}
941
- </DropdownMenuItem>
942
- )}
943
- {board.archived_at ? (
944
- <DropdownMenuItem
945
- onClick={() => {
946
- setShowUnarchiveDialog(true);
947
- setBoardMenuOpen(false);
948
- }}
949
- className="gap-2"
950
- >
951
- <RotateCcw className="h-4 w-4" />
952
- {t('ws-task-boards.row_actions.unarchive')}
953
- </DropdownMenuItem>
954
- ) : (
955
- <DropdownMenuItem
956
- onClick={() => {
957
- setShowArchiveDialog(true);
958
- setBoardMenuOpen(false);
959
- }}
960
- className="gap-2"
961
- >
962
- <Archive className="h-4 w-4" />
963
- {t('ws-task-boards.row_actions.archive')}
964
- </DropdownMenuItem>
965
- )}
966
- <DropdownMenuSeparator />
967
- <AlertDialog>
968
- <AlertDialogTrigger asChild>
969
- <DropdownMenuItem
970
- onSelect={(e) => e.preventDefault()}
971
- className="gap-2 text-dynamic-red/80 focus:text-dynamic-red"
972
- >
973
- <Trash2 className="h-4 w-4" />
974
- {t('ws-task-boards.actions.delete_board')}
975
- </DropdownMenuItem>
976
- </AlertDialogTrigger>
977
- <AlertDialogContent>
978
- <AlertDialogHeader>
979
- <AlertDialogTitle>
980
- {t('common.are_you_sure')}
981
- </AlertDialogTitle>
982
- <AlertDialogDescription>
983
- {t('ws-task-boards.dialog.delete_board_confirmation', {
984
- name: board.name || '',
985
- })}
986
- </AlertDialogDescription>
987
- </AlertDialogHeader>
988
- <AlertDialogFooter>
989
- <AlertDialogCancel disabled={isLoading}>
990
- {t('common.cancel')}
991
- </AlertDialogCancel>
992
- <AlertDialogAction
993
- onClick={handleDelete}
994
- disabled={isLoading}
995
- className="bg-dynamic-red/90 text-white hover:bg-dynamic-red"
996
- >
997
- {isLoading
998
- ? t('common.deleting')
999
- : t('ws-task-boards.actions.delete_board')}
1000
- </AlertDialogAction>
1001
- </AlertDialogFooter>
1002
- </AlertDialogContent>
1003
- </AlertDialog>
1004
- </DropdownMenuContent>
1005
- </DropdownMenu>
746
+ {/* Board Settings */}
747
+ {managerControlsVisible && (
748
+ <ToolbarTooltip label={t('ws-task-boards.actions.board_settings')}>
749
+ <Button
750
+ type="button"
751
+ variant="outline"
752
+ size="xs"
753
+ className={toolbarButtonClass}
754
+ onFocus={prefetchBoardSettings}
755
+ onMouseEnter={prefetchBoardSettings}
756
+ onClick={openBoardSettings}
757
+ onPointerDown={handleBoardSettingsPointerDown}
758
+ aria-label={t('ws-task-boards.actions.board_settings')}
759
+ >
760
+ <Bolt className="h-3.5 w-3.5" />
761
+ </Button>
762
+ </ToolbarTooltip>
1006
763
  )}
1007
764
  </div>
1008
765
  </div>
1009
- {/* Edit Board (name + icon) Dialog */}
1010
- <Dialog open={editBoardOpen} onOpenChange={setEditBoardOpen}>
1011
- <DialogContent className="p-0 sm:max-w-lg">
1012
- <DialogHeader className="sr-only">
1013
- <DialogTitle>{t('ws-task-boards.edit_dialog.title')}</DialogTitle>
1014
- <DialogDescription>
1015
- {t('ws-task-boards.edit_dialog.description')}
1016
- </DialogDescription>
1017
- </DialogHeader>
1018
- <TaskBoardForm
1019
- wsId={workspaceId}
1020
- data={{
1021
- id: board.id,
1022
- name: board.name ?? '',
1023
- icon: board.icon ?? null,
1024
- }}
1025
- showCancel
1026
- onCancel={() => setEditBoardOpen(false)}
1027
- onFinish={() => {
1028
- setEditBoardOpen(false);
1029
- queryClient.invalidateQueries({
1030
- queryKey: ['task-board', workspaceId, board.id],
1031
- });
1032
- queryClient.invalidateQueries({
1033
- queryKey: ['other-boards', workspaceId, board.id],
1034
- });
1035
- }}
1036
- />
1037
- </DialogContent>
1038
- </Dialog>
1039
- {/* Duplicate Board Dialog */}
1040
- <CopyBoardDialog
1041
- board={{ id: board.id, ws_id: workspaceId, name: board.name }}
1042
- open={duplicateBoardOpen}
1043
- onOpenChange={setDuplicateBoardOpen}
1044
- />
1045
- {/* Save as Template Dialog */}
1046
- <SaveAsTemplateDialog
1047
- board={{ id: board.id, ws_id: workspaceId, name: board.name }}
1048
- open={saveAsTemplateOpen}
1049
- onOpenChange={setSaveAsTemplateOpen}
766
+ <BoardShareDialog
767
+ board={{ id: board.id, name: board.name }}
768
+ open={shareBoardOpen}
769
+ onOpenChange={setShareBoardOpen}
770
+ wsId={workspaceId}
1050
771
  />
1051
- {/* Board Layout Settings */}
1052
- {onUpdate && (
1053
- <BoardLayoutSettings
1054
- open={layoutSettingsOpen}
1055
- onOpenChange={setLayoutSettingsOpen}
772
+ {plannerVisible && (
773
+ <KanbanPlannerDialog
1056
774
  boardId={board.id}
1057
- wsId={workspaceId}
1058
- lists={lists}
1059
- onUpdate={onUpdate}
1060
- translations={{
1061
- boardLayoutSettings: t('ws-task-boards.layout_settings.title'),
1062
- boardLayoutSettingsDescription: t(
1063
- 'ws-task-boards.layout_settings.description'
1064
- ),
1065
- addNewList: t('ws-task-boards.layout_settings.add_new_list'),
1066
- noListsInStatus: t('ws-task-boards.layout_settings.no_lists'),
1067
- done: t('common.done'),
1068
- editList: t('ws-task-boards.layout_settings.edit_list'),
1069
- updateListDescription: t(
1070
- 'ws-task-boards.layout_settings.edit_list_description'
1071
- ),
1072
- listName: t('ws-task-boards.layout_settings.list_name'),
1073
- statusCategory: t('ws-task-boards.layout_settings.status_category'),
1074
- color: t('common.color'),
1075
- cancel: t('common.cancel'),
1076
- saving: t('common.saving'),
1077
- saveChanges: t('common.save_changes'),
1078
- deleteListTitle: t('ws-task-boards.layout_settings.delete_list'),
1079
- deleteListDescription: t(
1080
- 'ws-task-boards.layout_settings.delete_list_description',
1081
- { name: '{name}' }
1082
- ),
1083
- deleteListConfirm: t('ws-task-boards.layout_settings.delete_list'),
1084
- listUpdatedSuccessfully: t(
1085
- 'ws-task-boards.layout_settings.list_updated'
1086
- ),
1087
- failedToUpdateList: t(
1088
- 'ws-task-boards.layout_settings.failed_to_update'
1089
- ),
1090
- listNameAlreadyExists: t(
1091
- 'ws-task-boards.layout_settings.list_name_exists'
1092
- ),
1093
- colorUpdated: t('ws-task-boards.layout_settings.color_updated'),
1094
- failedToUpdateColor: t(
1095
- 'ws-task-boards.layout_settings.failed_to_update_color'
1096
- ),
1097
- listDeletedSuccessfully: t(
1098
- 'ws-task-boards.layout_settings.list_deleted'
1099
- ),
1100
- failedToDeleteList: t(
1101
- 'ws-task-boards.layout_settings.failed_to_delete'
1102
- ),
1103
- cannotMoveToClosedStatus: t(
1104
- 'ws-task-boards.layout_settings.cannot_move_to_closed'
1105
- ),
1106
- listsReordered: t('ws-task-boards.layout_settings.lists_reordered'),
1107
- failedToReorderLists: t(
1108
- 'ws-task-boards.layout_settings.failed_to_reorder'
1109
- ),
1110
- task: t('common.task'),
1111
- tasks: t('common.tasks_plural'),
1112
- changeColor: t('ws-task-boards.layout_settings.change_color'),
1113
- backlog: t('ws-task-boards.layout_settings.backlog'),
1114
- active: t('ws-task-boards.layout_settings.active'),
1115
- review: t('ws-task-boards.layout_settings.review'),
1116
- doneStatus: t('ws-task-boards.layout_settings.done_status'),
1117
- closed: t('ws-task-boards.layout_settings.closed'),
1118
- documents: t('ws-task-boards.layout_settings.documents'),
1119
- gray: t('ws-task-boards.layout_settings.gray'),
1120
- red: t('ws-task-boards.layout_settings.red'),
1121
- blue: t('ws-task-boards.layout_settings.blue'),
1122
- green: t('ws-task-boards.layout_settings.green'),
1123
- yellow: t('ws-task-boards.layout_settings.yellow'),
1124
- orange: t('ws-task-boards.layout_settings.orange'),
1125
- purple: t('ws-task-boards.layout_settings.purple'),
1126
- pink: t('ws-task-boards.layout_settings.pink'),
1127
- indigo: t('ws-task-boards.layout_settings.indigo'),
1128
- cyan: t('ws-task-boards.layout_settings.cyan'),
1129
- movedToStatus: t('ws-task-boards.layout_settings.moved_to_status', {
1130
- status: '{status}',
1131
- }),
1132
- deleteList: t('ws-task-boards.layout_settings.delete_list'),
1133
- }}
775
+ isPersonalWorkspace={isPersonalWorkspace}
776
+ onOpenChange={setPlannerOpen}
777
+ open={plannerOpen}
778
+ workspaceId={workspaceId}
1134
779
  />
1135
780
  )}
1136
- {/* Board Settings Dialog */}
1137
- <Dialog open={boardSettingsOpen} onOpenChange={setBoardSettingsOpen}>
1138
- <DialogContent className="sm:max-w-106.25">
1139
- <DialogHeader>
1140
- <DialogTitle>
1141
- {t('ws-task-boards.actions.board_settings')}
1142
- </DialogTitle>
1143
- <DialogDescription>
1144
- {t('ws-task-boards.settings.configure_description')}
1145
- </DialogDescription>
1146
- </DialogHeader>
1147
- <div className="grid gap-4 py-4">
1148
- <div className="grid gap-2">
1149
- <label htmlFor="ticketPrefix" className="font-medium text-sm">
1150
- {t('ws-task-boards.settings.ticket_prefix')}
1151
- </label>
1152
- <Input
1153
- id="ticketPrefix"
1154
- value={ticketPrefix}
1155
- onChange={(e) => setTicketPrefix(e.target.value.toUpperCase())}
1156
- placeholder={t(
1157
- 'ws-task-boards.settings.ticket_prefix_placeholder'
1158
- )}
1159
- onKeyDown={(e) => {
1160
- if (e.key === 'Enter') {
1161
- e.preventDefault();
1162
- handleSaveTicketPrefix();
1163
- }
1164
- }}
1165
- maxLength={10}
1166
- autoFocus
1167
- />
1168
- <p className="text-muted-foreground text-xs">
1169
- {t('ws-task-boards.settings.ticket_prefix_description')}
1170
- </p>
1171
- </div>
1172
- <div className="grid gap-2">
1173
- <label htmlFor="defaultList" className="font-medium text-sm">
1174
- {t('ws-task-boards.settings.default_list')}
1175
- </label>
1176
- <Select
1177
- value={defaultListId ?? '__none__'}
1178
- onValueChange={(value) =>
1179
- setDefaultListId(value === '__none__' ? null : value)
1180
- }
1181
- >
1182
- <SelectTrigger id="defaultList">
1183
- <SelectValue />
1184
- </SelectTrigger>
1185
- <SelectContent>
1186
- <SelectItem value="__none__">
1187
- {t('ws-task-boards.settings.default_list_none')}
1188
- </SelectItem>
1189
- {lists
1190
- .filter(
1191
- (list) => !list.deleted && !list.is_external_staging
1192
- )
1193
- .map((list) => (
1194
- <SelectItem key={list.id} value={list.id}>
1195
- {list.name ||
1196
- t('ws-task-boards.settings.untitled_list')}
1197
- </SelectItem>
1198
- ))}
1199
- </SelectContent>
1200
- </Select>
1201
- <p className="text-muted-foreground text-xs">
1202
- {t('ws-task-boards.settings.default_list_description')}
1203
- </p>
1204
- </div>
1205
- </div>
1206
- <DialogFooter>
1207
- <Button
1208
- variant="outline"
1209
- onClick={() => setBoardSettingsOpen(false)}
1210
- disabled={isLoading}
1211
- >
1212
- {t('common.cancel')}
1213
- </Button>
1214
- <Button onClick={handleSaveTicketPrefix} disabled={isLoading}>
1215
- {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
1216
- {t('common.save_changes')}
1217
- </Button>
1218
- </DialogFooter>
1219
- </DialogContent>
1220
- </Dialog>{' '}
1221
- {/* End Board Settings Dialog */}
1222
- {/* Archive Dialog */}
1223
- <AlertDialog open={showArchiveDialog} onOpenChange={setShowArchiveDialog}>
1224
- <AlertDialogContent>
1225
- <AlertDialogHeader>
1226
- <AlertDialogTitle>
1227
- {t('ws-task-boards.row_actions.dialog.archive_title')}
1228
- </AlertDialogTitle>
1229
- <AlertDialogDescription>
1230
- {(() => {
1231
- const name = board.name ?? '';
1232
- const truncated = name.length > 20;
1233
- const display = truncated ? `${name.slice(0, 20)}…` : name;
1234
- return t(
1235
- 'ws-task-boards.row_actions.dialog.archive_description',
1236
- { name: display }
1237
- );
1238
- })()}
1239
- </AlertDialogDescription>
1240
- </AlertDialogHeader>
1241
- <AlertDialogFooter>
1242
- <AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
1243
- <AlertDialogAction
1244
- onClick={() => archiveBoard(board.id)}
1245
- className="bg-primary text-primary-foreground hover:bg-primary/90"
1246
- >
1247
- {t('ws-task-boards.row_actions.dialog.archive_button')}
1248
- </AlertDialogAction>
1249
- </AlertDialogFooter>
1250
- </AlertDialogContent>
1251
- </AlertDialog>
1252
- {/* Unarchive Dialog */}
1253
- <AlertDialog
1254
- open={showUnarchiveDialog}
1255
- onOpenChange={setShowUnarchiveDialog}
1256
- >
1257
- <AlertDialogContent>
1258
- <AlertDialogHeader>
1259
- <AlertDialogTitle>
1260
- {t('ws-task-boards.row_actions.dialog.unarchive_title')}
1261
- </AlertDialogTitle>
1262
- <AlertDialogDescription>
1263
- {(() => {
1264
- const name = board.name ?? '';
1265
- const truncated = name.length > 20;
1266
- const display = truncated ? `${name.slice(0, 20)}…` : name;
1267
- return t(
1268
- 'ws-task-boards.row_actions.dialog.unarchive_description',
1269
- { name: display }
1270
- );
1271
- })()}
1272
- </AlertDialogDescription>
1273
- </AlertDialogHeader>
1274
- <AlertDialogFooter>
1275
- <AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
1276
- <AlertDialogAction
1277
- onClick={() => unarchiveBoard(board.id)}
1278
- className="bg-primary text-primary-foreground hover:bg-primary/90"
1279
- >
1280
- {t('ws-task-boards.row_actions.dialog.unarchive_button')}
1281
- </AlertDialogAction>
1282
- </AlertDialogFooter>
1283
- </AlertDialogContent>
1284
- </AlertDialog>
1285
781
  </div>
1286
782
  );
1287
783
  }