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