@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,25 +1,25 @@
1
- import { useQuery } from '@tanstack/react-query';
1
+ import { useQuery, useQueryClient } from '@tanstack/react-query';
2
+ import { Archive, CheckCircle2, LayoutGrid, Trash2 } from '@tuturuuu/icons';
2
3
  import {
3
- Archive,
4
- CheckCircle2,
5
- ChevronsUpDown,
6
- LayoutGrid,
7
- Loader2,
8
- Trash2,
9
- } from '@tuturuuu/icons';
10
- import { listWorkspaceTaskBoards } from '@tuturuuu/internal-api';
4
+ createWorkspaceTaskBoard,
5
+ listWorkspaceTaskBoards,
6
+ } from '@tuturuuu/internal-api/tasks';
7
+ import {
8
+ isTaskRememberLastBoardEnabled,
9
+ TASK_DEFAULT_BOARD_ID_CONFIG_ID,
10
+ TASK_REMEMBER_LAST_BOARD_CONFIG_ID,
11
+ } from '@tuturuuu/internal-api/users';
11
12
  import type { WorkspaceTaskBoard } from '@tuturuuu/types';
12
13
  import { Badge } from '@tuturuuu/ui/badge';
13
- import {
14
- DropdownMenu,
15
- DropdownMenuContent,
16
- DropdownMenuItem,
17
- DropdownMenuLabel,
18
- DropdownMenuSeparator,
19
- DropdownMenuTrigger,
20
- } from '@tuturuuu/ui/dropdown-menu';
14
+ import { Combobox, type ComboboxOption } from '@tuturuuu/ui/custom/combobox';
15
+ import { toast } from '@tuturuuu/ui/sonner';
21
16
  import { cn } from '@tuturuuu/utils/format';
22
17
  import { useRouter } from 'next/navigation';
18
+ import { useCallback, useMemo } from 'react';
19
+ import {
20
+ useUpdateUserWorkspaceConfig,
21
+ useUserWorkspaceConfig,
22
+ } from '../../../../hooks/use-user-workspace-config';
23
23
  import {
24
24
  getIconComponentByKey,
25
25
  type PlatformIconKey,
@@ -41,8 +41,11 @@ interface BoardSwitcherProps {
41
41
  archived?: string;
42
42
  deleted?: string;
43
43
  daysLeft?: string;
44
- // Board name translations (case-insensitive matching)
44
+ searchBoards?: string;
45
45
  tasks?: string;
46
+ createBoard?: string;
47
+ creatingBoard?: string;
48
+ createBoardError?: string;
46
49
  };
47
50
  }
48
51
 
@@ -66,30 +69,42 @@ function getDaysRemaining(deletedAt: string) {
66
69
 
67
70
  export function BoardSwitcher({ board, translations }: BoardSwitcherProps) {
68
71
  const router = useRouter();
72
+ const queryClient = useQueryClient();
69
73
  const tasksHref = useTasksHref();
74
+ const { data: rememberLastBoardRaw } = useUserWorkspaceConfig(
75
+ board.ws_id,
76
+ TASK_REMEMBER_LAST_BOARD_CONFIG_ID,
77
+ 'true'
78
+ );
79
+ const updateUserWorkspaceConfig = useUpdateUserWorkspaceConfig();
70
80
 
71
- // Use provided translations or fall back to English defaults
72
81
  const t = {
73
82
  loadingBoards: translations?.loadingBoards ?? 'Loading boards...',
74
83
  noOtherBoards: translations?.noOtherBoards ?? 'No other boards',
75
- activeBoards: translations?.activeBoards ?? 'Active Boards',
76
- archivedBoards: translations?.archivedBoards ?? 'Archived Boards',
77
- deletedBoards: translations?.deletedBoards ?? 'Deleted Boards',
84
+ activeBoards: translations?.activeBoards ?? 'Active boards',
85
+ archivedBoards: translations?.archivedBoards ?? 'Archived boards',
86
+ deletedBoards: translations?.deletedBoards ?? 'Deleted boards',
78
87
  untitled: translations?.untitled ?? 'Untitled',
79
88
  active: translations?.active ?? 'Active',
80
89
  archived: translations?.archived ?? 'Archived',
81
90
  deleted: translations?.deleted ?? 'Deleted',
82
91
  daysLeft: translations?.daysLeft ?? '{count} days left',
92
+ searchBoards: translations?.searchBoards ?? 'Search boards',
83
93
  tasks: translations?.tasks ?? 'Tasks',
94
+ createBoard: translations?.createBoard ?? 'Create board',
95
+ creatingBoard: translations?.creatingBoard ?? 'Creating',
96
+ createBoardError:
97
+ translations?.createBoardError ?? 'Could not create board',
84
98
  };
85
99
 
86
- // Helper to translate board names (case-insensitive matching)
87
- const translateBoardName = (name: string | null): string => {
88
- if (!name) return t.untitled;
89
- // Check if board name matches known translatable names
90
- if (name.toLowerCase() === 'tasks') return t.tasks;
91
- return name;
92
- };
100
+ const translateBoardName = useCallback(
101
+ (name: string | null): string => {
102
+ if (!name) return t.untitled;
103
+ if (name.toLowerCase() === 'tasks') return t.tasks;
104
+ return name;
105
+ },
106
+ [t.tasks, t.untitled]
107
+ );
93
108
 
94
109
  const { data: boards = [], isLoading: isFetchingBoards } = useQuery({
95
110
  queryKey: ['other-boards', board.ws_id, board.id],
@@ -99,200 +114,177 @@ export function BoardSwitcher({ board, translations }: BoardSwitcherProps) {
99
114
  },
100
115
  enabled: !!board.ws_id,
101
116
  });
117
+ const rememberLastBoard =
118
+ isTaskRememberLastBoardEnabled(rememberLastBoardRaw);
102
119
 
103
- // Separate boards by status
104
- const activeBoards = boards.filter((b) => !b.archived_at && !b.deleted_at);
105
- const archivedBoards = boards.filter((b) => b.archived_at && !b.deleted_at);
106
- const deletedBoards = boards.filter((b) => b.deleted_at);
120
+ const selectBoard = useCallback(
121
+ (value: string | string[]) => {
122
+ const boardId = Array.isArray(value) ? value[0] : value;
123
+ if (!boardId || boardId === board.id) return;
107
124
 
108
- // Get current board's icon
109
- const CurrentBoardIcon =
110
- getIconComponentByKey(board.icon as PlatformIconKey | null) ?? LayoutGrid;
125
+ if (rememberLastBoard) {
126
+ updateUserWorkspaceConfig.mutate({
127
+ configId: TASK_DEFAULT_BOARD_ID_CONFIG_ID,
128
+ value: boardId,
129
+ workspaceId: board.ws_id,
130
+ });
131
+ }
111
132
 
112
- return (
113
- <DropdownMenu>
114
- <DropdownMenuTrigger asChild>
115
- <div className="group flex cursor-pointer items-center gap-2 transition-colors hover:text-foreground">
116
- <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary transition-colors group-hover:bg-primary/20">
117
- <CurrentBoardIcon className="h-4 w-4" />
118
- </div>
119
- <h1 className="truncate font-bold text-base text-foreground sm:text-xl md:text-2xl">
120
- {translateBoardName(board.name)}
121
- </h1>
122
- <ChevronsUpDown className="h-4 w-4 text-muted-foreground transition-transform group-hover:scale-110" />
123
- </div>
124
- </DropdownMenuTrigger>
125
- <DropdownMenuContent align="start" className="w-[320px]">
126
- {isFetchingBoards ? (
127
- <DropdownMenuItem disabled>
128
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
129
- {t.loadingBoards}
130
- </DropdownMenuItem>
131
- ) : boards.length === 0 ? (
132
- <DropdownMenuItem disabled className="justify-center">
133
- {t.noOtherBoards}
134
- </DropdownMenuItem>
135
- ) : (
136
- <>
137
- {/* Active Boards */}
138
- {activeBoards.length > 0 && (
139
- <>
140
- <DropdownMenuLabel className="text-muted-foreground text-xs">
141
- {t.activeBoards}
142
- </DropdownMenuLabel>
143
- {activeBoards.map((otherBoard) => {
144
- const isCurrentBoard = otherBoard.id === board.id;
145
- const BoardIcon =
146
- getIconComponentByKey(
147
- otherBoard.icon as PlatformIconKey | null
148
- ) ?? LayoutGrid;
149
- return (
150
- <DropdownMenuItem
151
- key={otherBoard.id}
152
- onClick={() =>
153
- router.push(
154
- `/${board.ws_id}${tasksHref(`/boards/${otherBoard.id}`)}`
155
- )
156
- }
157
- className={cn(
158
- 'group/item cursor-pointer gap-3 py-2.5',
159
- isCurrentBoard && 'bg-dynamic-blue/10 text-dynamic-blue'
160
- )}
161
- >
162
- <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary transition-colors group-hover/item:bg-primary/20">
163
- <BoardIcon className="h-4 w-4" />
164
- </div>
165
- <div className="flex min-w-0 flex-1 flex-col gap-1">
166
- <span className="truncate font-medium text-sm leading-none">
167
- {translateBoardName(otherBoard.name)}
168
- </span>
169
- </div>
170
- <Badge
171
- className={cn(
172
- 'shrink-0 gap-1 px-2 py-0.5 text-[10px]',
173
- 'bg-dynamic-green/10 text-dynamic-green'
174
- )}
175
- >
176
- <CheckCircle2 className="h-3 w-3 text-dynamic-green/50" />
177
- {t.active}
178
- </Badge>
179
- </DropdownMenuItem>
180
- );
181
- })}
182
- </>
183
- )}
133
+ router.push(`/${board.ws_id}${tasksHref(`/boards/${boardId}`)}`);
134
+ },
135
+ [
136
+ board.id,
137
+ board.ws_id,
138
+ rememberLastBoard,
139
+ router,
140
+ tasksHref,
141
+ updateUserWorkspaceConfig,
142
+ ]
143
+ );
184
144
 
185
- {/* Archived Boards */}
186
- {archivedBoards.length > 0 && (
187
- <>
188
- {activeBoards.length > 0 && <DropdownMenuSeparator />}
189
- <DropdownMenuLabel className="text-muted-foreground text-xs">
190
- {t.archivedBoards}
191
- </DropdownMenuLabel>
192
- {archivedBoards.map((otherBoard) => {
193
- const isCurrentBoard = otherBoard.id === board.id;
194
- const BoardIcon =
195
- getIconComponentByKey(
196
- otherBoard.icon as PlatformIconKey | null
197
- ) ?? LayoutGrid;
198
- return (
199
- <DropdownMenuItem
200
- key={otherBoard.id}
201
- onClick={() =>
202
- router.push(
203
- `/${board.ws_id}${tasksHref(`/boards/${otherBoard.id}`)}`
204
- )
205
- }
206
- className={cn(
207
- 'group/item cursor-pointer gap-3 py-2.5 opacity-75 hover:opacity-100',
208
- isCurrentBoard &&
209
- 'bg-dynamic-blue/10 text-dynamic-blue opacity-100'
210
- )}
211
- >
212
- <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
213
- <BoardIcon className="h-4 w-4" />
214
- </div>
215
- <div className="flex min-w-0 flex-1 flex-col gap-1">
216
- <span className="truncate font-medium text-sm leading-none">
217
- {otherBoard.name || t.untitled}
218
- </span>
219
- </div>
220
- <Badge
221
- className={cn(
222
- 'shrink-0 gap-1 px-2 py-0.5 text-[10px]',
223
- 'bg-muted text-foreground'
224
- )}
225
- >
226
- <Archive className="h-3 w-3 text-foreground/50" />
227
- {t.archived}
228
- </Badge>
229
- </DropdownMenuItem>
230
- );
231
- })}
232
- </>
233
- )}
145
+ const createBoard = useCallback(
146
+ async (value: string) => {
147
+ const name = value.trim();
148
+ if (!name) return;
149
+
150
+ try {
151
+ const payload = await createWorkspaceTaskBoard(board.ws_id, { name });
152
+ await queryClient.invalidateQueries({
153
+ queryKey: ['other-boards', board.ws_id],
154
+ });
155
+
156
+ return {
157
+ label: translateBoardName(payload.board.name ?? name),
158
+ value: payload.board.id,
159
+ };
160
+ } catch (error) {
161
+ toast.error(t.createBoardError);
162
+ throw error;
163
+ }
164
+ },
165
+ [board.ws_id, queryClient, t.createBoardError, translateBoardName]
166
+ );
234
167
 
235
- {/* Deleted Boards */}
236
- {deletedBoards.length > 0 && (
237
- <>
238
- {(activeBoards.length > 0 || archivedBoards.length > 0) && (
239
- <DropdownMenuSeparator />
240
- )}
241
- <DropdownMenuLabel className="text-muted-foreground text-xs">
242
- {t.deletedBoards}
243
- </DropdownMenuLabel>
244
- {deletedBoards.map((otherBoard) => {
245
- const daysRemaining = getDaysRemaining(
246
- otherBoard.deleted_at ?? ''
247
- );
248
- const isCurrentBoard = otherBoard.id === board.id;
249
- const BoardIcon =
250
- getIconComponentByKey(
251
- otherBoard.icon as PlatformIconKey | null
252
- ) ?? LayoutGrid;
168
+ const boardOptions = useMemo(() => {
169
+ const byId = new Map<string, BoardWithStatus>();
170
+ for (const item of boards) byId.set(item.id, item);
171
+ if (!byId.has(board.id)) {
172
+ byId.set(board.id, {
173
+ id: board.id,
174
+ name: board.name ?? null,
175
+ icon: board.icon ?? null,
176
+ archived_at: null,
177
+ deleted_at: null,
178
+ created_at: null,
179
+ });
180
+ }
253
181
 
254
- return (
255
- <DropdownMenuItem
256
- key={otherBoard.id}
257
- onClick={() =>
258
- router.push(
259
- `/${board.ws_id}${tasksHref(`/boards/${otherBoard.id}`)}`
260
- )
261
- }
262
- className={cn(
263
- 'group/item cursor-pointer gap-3 py-2.5 opacity-60 hover:opacity-100',
264
- isCurrentBoard &&
265
- 'bg-dynamic-blue/10 text-dynamic-blue opacity-100'
266
- )}
267
- >
268
- <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-destructive/10 text-destructive">
269
- <BoardIcon className="h-4 w-4" />
270
- </div>
271
- <div className="flex min-w-0 flex-1 flex-col gap-1">
272
- <span className="truncate font-medium text-sm leading-none">
273
- {otherBoard.name || t.untitled}
274
- </span>
275
- <span className="text-[10px] text-muted-foreground leading-none">
276
- {t.daysLeft.replace('{count}', String(daysRemaining))}
277
- </span>
278
- </div>
279
- <Badge
280
- className={cn(
281
- 'shrink-0 gap-1 px-2 py-0.5 text-[10px]',
282
- 'bg-dynamic-red/10 text-dynamic-red'
283
- )}
284
- >
285
- <Trash2 className="h-3 w-3 text-dynamic-red/50" />
286
- {t.deleted}
287
- </Badge>
288
- </DropdownMenuItem>
289
- );
290
- })}
291
- </>
182
+ const orderedBoards = [...byId.values()].sort((a, b) => {
183
+ const statusWeight = (item: BoardWithStatus) =>
184
+ item.deleted_at ? 2 : item.archived_at ? 1 : 0;
185
+ const statusDelta = statusWeight(a) - statusWeight(b);
186
+ if (statusDelta !== 0) return statusDelta;
187
+ return translateBoardName(a.name).localeCompare(
188
+ translateBoardName(b.name)
189
+ );
190
+ });
191
+
192
+ return orderedBoards.map((item): ComboboxOption => {
193
+ const BoardIcon =
194
+ getIconComponentByKey(item.icon as PlatformIconKey | null) ??
195
+ LayoutGrid;
196
+ const isDeleted = Boolean(item.deleted_at);
197
+ const isArchived = Boolean(item.archived_at && !item.deleted_at);
198
+ const statusLabel = isDeleted
199
+ ? t.deleted
200
+ : isArchived
201
+ ? t.archived
202
+ : t.active;
203
+ const groupLabel = isDeleted
204
+ ? t.deletedBoards
205
+ : isArchived
206
+ ? t.archivedBoards
207
+ : t.activeBoards;
208
+ const daysRemaining =
209
+ item.deleted_at && getDaysRemaining(item.deleted_at);
210
+
211
+ return {
212
+ value: item.id,
213
+ label: translateBoardName(item.name),
214
+ searchValue: [
215
+ translateBoardName(item.name),
216
+ statusLabel,
217
+ groupLabel,
218
+ daysRemaining
219
+ ? t.daysLeft.replace('{count}', String(daysRemaining))
220
+ : null,
221
+ ]
222
+ .filter(Boolean)
223
+ .join(' '),
224
+ description: daysRemaining
225
+ ? `${groupLabel} · ${t.daysLeft.replace(
226
+ '{count}',
227
+ String(daysRemaining)
228
+ )}`
229
+ : groupLabel,
230
+ icon: <BoardIcon className="h-4 w-4" />,
231
+ muted: isArchived || isDeleted,
232
+ badge: (
233
+ <Badge
234
+ className={cn(
235
+ 'shrink-0 gap-1 px-2 py-0.5 text-[10px]',
236
+ isDeleted && 'bg-dynamic-red/10 text-dynamic-red',
237
+ isArchived && 'bg-muted text-foreground',
238
+ !isDeleted &&
239
+ !isArchived &&
240
+ 'bg-dynamic-green/10 text-dynamic-green'
241
+ )}
242
+ >
243
+ {isDeleted ? (
244
+ <Trash2 className="h-3 w-3 text-dynamic-red/50" />
245
+ ) : isArchived ? (
246
+ <Archive className="h-3 w-3 text-foreground/50" />
247
+ ) : (
248
+ <CheckCircle2 className="h-3 w-3 text-dynamic-green/50" />
292
249
  )}
293
- </>
294
- )}
295
- </DropdownMenuContent>
296
- </DropdownMenu>
250
+ {statusLabel}
251
+ </Badge>
252
+ ),
253
+ };
254
+ });
255
+ }, [
256
+ board.icon,
257
+ board.id,
258
+ board.name,
259
+ boards,
260
+ t.active,
261
+ t.activeBoards,
262
+ t.archived,
263
+ t.archivedBoards,
264
+ t.daysLeft,
265
+ t.deleted,
266
+ t.deletedBoards,
267
+ translateBoardName,
268
+ ]);
269
+
270
+ return (
271
+ <Combobox
272
+ className="w-[min(22rem,70vw)] [&_button]:h-7 [&_button]:min-h-7 [&_button]:px-2 sm:[&_button]:h-8 sm:[&_button]:min-h-8"
273
+ createText={t.createBoard}
274
+ creatingText={t.creatingBoard}
275
+ disabled={isFetchingBoards}
276
+ emptyText={isFetchingBoards ? t.loadingBoards : t.noOtherBoards}
277
+ label={
278
+ <span className="truncate font-semibold text-foreground text-sm">
279
+ {translateBoardName(board.name)}
280
+ </span>
281
+ }
282
+ onChange={selectBoard}
283
+ onCreate={createBoard}
284
+ options={boardOptions}
285
+ placeholder={translateBoardName(board.name)}
286
+ searchPlaceholder={t.searchBoards}
287
+ selected={board.id}
288
+ />
297
289
  );
298
290
  }