@tuturuuu/ui 0.8.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 (182) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/biome.json +1 -1
  3. package/package.json +73 -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-shell.test.tsx +3 -0
  29. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
  30. package/src/components/ui/custom/combobox.test.tsx +195 -0
  31. package/src/components/ui/custom/combobox.tsx +273 -156
  32. package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
  33. package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
  34. package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
  35. package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
  36. package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
  37. package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
  38. package/src/components/ui/custom/theme-toggle.tsx +1 -1
  39. package/src/components/ui/custom/workspace-select.tsx +8 -3
  40. package/src/components/ui/dialog.test.tsx +52 -0
  41. package/src/components/ui/dialog.tsx +6 -2
  42. package/src/components/ui/dropdown-menu.tsx +5 -1
  43. package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
  44. package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
  45. package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
  46. package/src/components/ui/finance/debts/debts-page.tsx +15 -2
  47. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
  48. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
  49. package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
  50. package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
  51. package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
  52. package/src/components/ui/finance/invoices/utils.ts +3 -1
  53. package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
  54. package/src/components/ui/finance/transactions/form-types.ts +1 -0
  55. package/src/components/ui/finance/transactions/form.tsx +2 -0
  56. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
  57. package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
  58. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
  59. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
  60. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
  61. package/src/components/ui/finance/wallets/form.test.tsx +51 -3
  62. package/src/components/ui/finance/wallets/form.tsx +15 -4
  63. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  64. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
  65. package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
  66. package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
  67. package/src/components/ui/input-otp.tsx +1 -1
  68. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
  69. package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
  70. package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
  71. package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
  72. package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
  73. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
  74. package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
  75. package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
  76. package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
  77. package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
  78. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
  79. package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
  80. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
  81. package/src/components/ui/navigation-menu.tsx +1 -1
  82. package/src/components/ui/pagination.tsx +1 -1
  83. package/src/components/ui/radio-group.tsx +1 -1
  84. package/src/components/ui/select.tsx +5 -1
  85. package/src/components/ui/sheet.tsx +1 -1
  86. package/src/components/ui/sidebar.tsx +1 -1
  87. package/src/components/ui/storefront/cart-popover.tsx +61 -0
  88. package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
  89. package/src/components/ui/storefront/cart-summary.tsx +93 -154
  90. package/src/components/ui/storefront/checkout-overlay.tsx +4 -5
  91. package/src/components/ui/storefront/listing-card.tsx +1 -1
  92. package/src/components/ui/storefront/merch-sections.tsx +70 -0
  93. package/src/components/ui/storefront/product-detail.tsx +1 -1
  94. package/src/components/ui/storefront/storefront-surface.test.tsx +106 -11
  95. package/src/components/ui/storefront/storefront-surface.tsx +101 -166
  96. package/src/components/ui/storefront/types.ts +4 -0
  97. package/src/components/ui/storefront/utils.ts +6 -0
  98. package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
  99. package/src/components/ui/text-editor/background-color-extension.ts +62 -0
  100. package/src/components/ui/text-editor/color-controls.tsx +284 -0
  101. package/src/components/ui/text-editor/editor.tsx +69 -14
  102. package/src/components/ui/text-editor/extensions.ts +8 -2
  103. package/src/components/ui/text-editor/highlight-extension.ts +22 -0
  104. package/src/components/ui/text-editor/tool-bar.tsx +9 -16
  105. package/src/components/ui/toast.tsx +1 -1
  106. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
  107. package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
  108. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
  109. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +112 -43
  110. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
  111. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
  112. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
  113. package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
  114. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
  115. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
  116. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +38 -9
  117. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
  118. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
  119. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +81 -30
  120. package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
  121. package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
  122. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
  123. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
  124. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
  125. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
  126. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
  127. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
  128. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
  129. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
  130. package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
  131. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +397 -2
  132. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +103 -13
  133. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
  134. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
  135. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
  136. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +26 -4
  137. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +5 -2
  138. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
  139. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
  140. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
  141. package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
  142. package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
  143. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
  144. package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
  145. package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
  146. package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
  147. package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
  148. package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
  149. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
  150. package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
  151. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
  152. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
  153. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
  154. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +203 -2
  155. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
  156. package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
  157. package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
  158. package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
  159. package/src/components/ui/tu-do/shared/board-header.tsx +464 -975
  160. package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
  161. package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
  162. package/src/components/ui/tu-do/shared/board-views.tsx +587 -75
  163. package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
  164. package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
  165. package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
  166. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
  167. package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
  168. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
  169. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
  170. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
  171. package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
  172. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +2 -1
  173. package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
  174. package/src/declarations.d.ts +1 -0
  175. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
  176. package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
  177. package/src/hooks/use-calendar-sync.tsx +247 -243
  178. package/src/hooks/use-calendar.tsx +323 -138
  179. package/src/hooks/use-task-actions.ts +24 -0
  180. package/src/hooks/use-user-workspace-config.ts +75 -0
  181. package/src/hooks/use-workspace-currency.ts +8 -3
  182. package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
@@ -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';
@@ -123,6 +74,40 @@ interface Props {
123
74
  onRecycleBinOpen?: () => void;
124
75
  isMultiSelectMode: boolean;
125
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;
126
111
  }
127
112
 
128
113
  export function BoardHeader({
@@ -140,35 +125,36 @@ export function BoardHeader({
140
125
  backUrl,
141
126
  hideActions = false,
142
127
  isSearching = false,
143
- lists = [],
144
- onUpdate,
145
- onRecycleBinOpen,
146
128
  isMultiSelectMode,
147
129
  setIsMultiSelectMode,
130
+ availableViews,
131
+ publicView = false,
132
+ readOnly = false,
133
+ titlePrefix,
134
+ onBoardSettingsIntent,
148
135
  }: Props) {
149
136
  const t = useTranslations();
137
+ const queryClient = useQueryClient();
150
138
  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
- );
139
+ const [shareBoardOpen, setShareBoardOpen] = useState(false);
140
+ const [plannerOpen, setPlannerOpen] = useState(false);
165
141
  const [localSearchQuery, setLocalSearchQuery] = useState(
166
142
  filters.searchQuery || ''
167
143
  );
168
- const { archiveBoard, unarchiveBoard } = useBoardActions(workspaceId);
169
- const queryClient = useQueryClient();
170
144
  const router = useRouter();
171
- 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';
172
158
 
173
159
  // Stable refs for callbacks and values to avoid effect re-runs
174
160
  const onFiltersChangeRef = useRef(onFiltersChange);
@@ -229,62 +215,58 @@ export function BoardHeader({
229
215
  return () => clearTimeout(timeoutId);
230
216
  }, [board.id, currentView, filters, listStatusFilter]);
231
217
 
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
- }
218
+ function handleSortChange(sortBy: TaskFilters['sortBy']) {
219
+ onFiltersChange({ ...filters, sortBy });
242
220
  }
243
221
 
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;
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
+ }
251
231
 
252
- await updateWorkspaceTaskBoard(workspaceId, board.id, {
253
- ticket_prefix: nextTicketPrefix,
254
- default_list_id: defaultListId,
255
- });
232
+ function prefetchBoardSettings() {
233
+ if (!managerControlsVisible) return;
256
234
 
257
- syncBoardTicketPrefixCaches({
258
- queryClient,
259
- workspaceId,
260
- board,
261
- ticketPrefix: nextTicketPrefix,
262
- });
235
+ onBoardSettingsIntent?.();
236
+ if (typeof window !== 'undefined') {
237
+ window.dispatchEvent(new Event(BOARD_SETTINGS_PRELOAD_EVENT));
238
+ }
263
239
 
264
- 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
+ }
265
253
 
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
- });
254
+ function announceSettingsOpenIntent() {
255
+ if (typeof window === 'undefined') return;
273
256
 
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
- }
257
+ window.dispatchEvent(
258
+ new CustomEvent(SETTINGS_DIALOG_OPEN_INTENT_EVENT, {
259
+ detail: {
260
+ settingsBoardId: board.id,
261
+ settingsTab: 'task_board',
262
+ },
263
+ })
264
+ );
283
265
  }
284
266
 
285
- function handleSortChange(sortBy: TaskFilters['sortBy']) {
286
- onFiltersChange({ ...filters, sortBy });
287
- setSortMenuOpen(false);
267
+ function handleBoardSettingsPointerDown() {
268
+ prefetchBoardSettings();
269
+ announceSettingsOpenIntent();
288
270
  }
289
271
 
290
272
  function handleSmartFocus() {
@@ -348,8 +330,139 @@ export function BoardHeader({
348
330
  label: t('ws-task-boards.views.timeline'),
349
331
  description: t('ws-task-boards.views.timeline_description'),
350
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
+ },
351
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];
352
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];
353
466
  // Create metadata for presence tracking (excludes search query for stability)
354
467
  const presenceMetadata: BoardFiltersMetadata = useMemo(() => {
355
468
  const { searchQuery: _, ...filtersWithoutSearch } = filters;
@@ -372,26 +485,46 @@ export function BoardHeader({
372
485
  <ArrowLeft className="h-5 w-5" />
373
486
  </Link>
374
487
  )}
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
- />
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
+ )}
391
524
  </div>
392
525
 
393
526
  {/* Search Bar */}
394
- <div className="relative max-w-md flex-1">
527
+ <div className="relative min-w-0 flex-1 basis-72">
395
528
  {isSearching ? (
396
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" />
397
530
  ) : (
@@ -417,9 +550,9 @@ export function BoardHeader({
417
550
  </div>
418
551
 
419
552
  {/* Controls - Compact Row */}
420
- <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">
421
554
  {/* Online Users */}
422
- {!isPersonalWorkspace && (
555
+ {interactiveControlsVisible && !isPersonalWorkspace && (
423
556
  <BoardUserPresenceAvatarsComponent
424
557
  boardId={board.id}
425
558
  currentMetadata={presenceMetadata}
@@ -429,859 +562,215 @@ export function BoardHeader({
429
562
  )}
430
563
 
431
564
  {/* 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>
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
+ )}
456
599
 
457
600
  {/* 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>
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
+ )}
473
618
 
474
619
  {/* List Status Filter */}
475
- <Select
476
- value={listStatusFilter}
477
- onValueChange={(value) =>
620
+ <Combobox
621
+ mode="single"
622
+ options={listStatusOptions}
623
+ selected={listStatusFilter}
624
+ onChange={(value) =>
478
625
  onListStatusFilterChange(value as ListStatusFilter)
479
626
  }
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>
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
+ />
510
643
 
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>
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
+ />
558
666
 
559
667
  {/* Task Filter */}
560
- <TaskFilter
561
- wsId={workspaceId}
562
- currentUserId={currentUserId}
563
- filters={filters}
564
- 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
+ )}
565
707
  />
566
708
 
567
- {/* Sort Dropdown */}
568
- <DropdownMenu open={sortMenuOpen} onOpenChange={setSortMenuOpen}>
569
- <DropdownMenuTrigger asChild>
709
+ {plannerVisible && (
710
+ <ToolbarTooltip label={t('ws-task-plans.planner')}>
570
711
  <Button
712
+ type="button"
571
713
  size="xs"
572
714
  variant="outline"
573
- className={cn(
574
- 'text-[10px] sm:text-xs',
575
- filters.sortBy && 'border-primary/50 bg-primary/5'
576
- )}
715
+ className={toolbarButtonClass}
716
+ onClick={() => setPlannerOpen(true)}
717
+ aria-label={t('ws-task-plans.planner')}
577
718
  >
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>
719
+ <CalendarDays className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
584
720
  </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>
721
+ </ToolbarTooltip>
722
+ )}
842
723
 
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>
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
+ )}
859
738
 
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>
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>
1006
756
  )}
1007
757
  </div>
1008
758
  </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}
759
+ <BoardShareDialog
760
+ board={{ id: board.id, name: board.name }}
761
+ open={shareBoardOpen}
762
+ onOpenChange={setShareBoardOpen}
763
+ wsId={workspaceId}
1050
764
  />
1051
- {/* Board Layout Settings */}
1052
- {onUpdate && (
1053
- <BoardLayoutSettings
1054
- open={layoutSettingsOpen}
1055
- onOpenChange={setLayoutSettingsOpen}
765
+ {plannerVisible && (
766
+ <KanbanPlannerDialog
1056
767
  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
- }}
768
+ isPersonalWorkspace={isPersonalWorkspace}
769
+ onOpenChange={setPlannerOpen}
770
+ open={plannerOpen}
771
+ workspaceId={workspaceId}
1134
772
  />
1135
773
  )}
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
774
  </div>
1286
775
  );
1287
776
  }