@tuturuuu/ui 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/package.json +6 -5
  3. package/src/components/ui/custom/__tests__/settings-dialog-search.test.ts +78 -0
  4. package/src/components/ui/custom/__tests__/settings-dialog-shell-compile-graph.test.ts +76 -0
  5. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +27 -1
  6. package/src/components/ui/custom/nav-link.test.tsx +165 -0
  7. package/src/components/ui/custom/nav-link.tsx +69 -11
  8. package/src/components/ui/custom/navigation.tsx +1 -0
  9. package/src/components/ui/custom/settings/task-settings.tsx +104 -0
  10. package/src/components/ui/custom/settings-dialog-search-loader.d.ts +5 -0
  11. package/src/components/ui/custom/settings-dialog-search-loader.js +3 -0
  12. package/src/components/ui/custom/settings-dialog-search.ts +75 -0
  13. package/src/components/ui/custom/settings-dialog-shell.tsx +63 -27
  14. package/src/components/ui/custom/workspace-select-helpers.ts +23 -0
  15. package/src/components/ui/custom/workspace-select.tsx +17 -16
  16. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +16 -0
  17. package/src/components/ui/tu-do/boards/__tests__/task-board-form.test.tsx +12 -0
  18. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +4 -328
  19. package/src/components/ui/tu-do/boards/board-share-settings-panel.tsx +351 -0
  20. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +50 -37
  21. package/src/components/ui/tu-do/boards/boardId/enhanced-task-list.tsx +7 -0
  22. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-types.ts +3 -3
  23. package/src/components/ui/tu-do/boards/boardId/kanban/data/use-bulk-resources.ts +59 -5
  24. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/drag-preview.tsx +20 -1
  25. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +263 -21
  26. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +133 -14
  27. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +112 -54
  28. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +8 -2
  29. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +29 -14
  30. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +24 -1
  31. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +7 -0
  32. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +7 -1
  33. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +20 -0
  34. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +10 -0
  35. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +80 -8
  36. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +15 -0
  37. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +9 -0
  38. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +9 -0
  39. package/src/components/ui/tu-do/boards/boardId/timeline/timeline-toolbar.tsx +9 -0
  40. package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +35 -3
  41. package/src/components/ui/tu-do/boards/form.tsx +1 -1
  42. package/src/components/ui/tu-do/hooks/__tests__/useTaskLabelManagement.test.tsx +48 -0
  43. package/src/components/ui/tu-do/hooks/__tests__/useTaskProjectManagement.test.tsx +144 -0
  44. package/src/components/ui/tu-do/hooks/useTaskDialog.ts +7 -0
  45. package/src/components/ui/tu-do/hooks/useTaskLabelManagement.ts +115 -106
  46. package/src/components/ui/tu-do/hooks/useTaskProjectManagement.ts +115 -122
  47. package/src/components/ui/tu-do/progress/task-progress-import-panel.tsx +60 -0
  48. package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +156 -0
  49. package/src/components/ui/tu-do/progress/task-progress-page.tsx +348 -0
  50. package/src/components/ui/tu-do/progress/task-progress-panels.tsx +301 -0
  51. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +26 -0
  52. package/src/components/ui/tu-do/shared/__tests__/assignee-select.test.tsx +81 -10
  53. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +116 -1
  54. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +38 -0
  55. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +128 -7
  56. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +222 -9
  57. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +21 -0
  58. package/src/components/ui/tu-do/shared/__tests__/task-cache-patches.test.ts +147 -0
  59. package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +3 -0
  60. package/src/components/ui/tu-do/shared/assignee-select.tsx +77 -26
  61. package/src/components/ui/tu-do/shared/board-client.tsx +14 -4
  62. package/src/components/ui/tu-do/shared/board-header.tsx +8 -1
  63. package/src/components/ui/tu-do/shared/board-switcher.tsx +70 -38
  64. package/src/components/ui/tu-do/shared/board-user-presence-avatars.tsx +18 -12
  65. package/src/components/ui/tu-do/shared/board-views.tsx +49 -69
  66. package/src/components/ui/tu-do/shared/list-view.tsx +21 -3
  67. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +4 -4
  68. package/src/components/ui/tu-do/shared/task-cache-patches.ts +394 -0
  69. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +21 -1
  70. package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +5 -1
  71. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +25 -2
  72. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +7 -1
  73. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-data.ts +79 -10
  74. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +76 -77
  75. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.test.tsx +63 -0
  76. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.ts +78 -69
  77. package/src/components/ui/tu-do/shared/task-edit-dialog/personal-overrides-section.tsx +28 -8
  78. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/dependencies-section.tsx +14 -3
  79. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/parent-section.tsx +6 -1
  80. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/related-section.tsx +6 -1
  81. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/subtasks-section.tsx +6 -1
  82. package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/types/task-relationships.types.ts +8 -0
  83. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +8 -1
  84. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.test.tsx +150 -0
  85. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +61 -35
  86. package/src/components/ui/tu-do/shared/task-edit-dialog/task-relationships-properties.tsx +44 -2
  87. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +9 -0
  88. package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +11 -0
  89. package/src/components/ui/tu-do/shared/use-progressive-board-loader.ts +2 -0
  90. package/src/hooks/__tests__/useBoardPresence.test.tsx +191 -0
  91. package/src/hooks/__tests__/useBoardRealtime.test.tsx +24 -144
  92. package/src/hooks/useBoardPresence.ts +364 -0
  93. package/src/hooks/useBoardRealtimeEventHandler.ts +34 -90
  94. package/src/lib/workspace-actions.ts +2 -6
@@ -1,6 +1,13 @@
1
1
  'use client';
2
2
 
3
3
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
+ import {
5
+ TASK_NAVIGATION_GOALS_CONFIG_ID,
6
+ TASK_NAVIGATION_IMPORT_CONFIG_ID,
7
+ TASK_NAVIGATION_LEADERBOARDS_CONFIG_ID,
8
+ TASK_NAVIGATION_PROGRESS_CONFIG_ID,
9
+ TASK_NAVIGATION_STATS_CONFIG_ID,
10
+ } from '@tuturuuu/internal-api/users';
4
11
  import type { Workspace } from '@tuturuuu/types';
5
12
  import { SettingItemTab } from '@tuturuuu/ui/custom/settings-item-tab';
6
13
  import {
@@ -85,6 +92,36 @@ export function TaskSettings({ workspace }: TaskSettingsProps) {
85
92
  isLoading: showReviewDueDatesLoading,
86
93
  isPending: showReviewDueDatesPending,
87
94
  } = useUserBooleanConfig(TASKS_SHOW_REVIEW_DUE_DATES_CONFIG_ID, false);
95
+ const {
96
+ value: showTaskProgressNavigation,
97
+ setValue: setShowTaskProgressNavigation,
98
+ isLoading: showTaskProgressNavigationLoading,
99
+ isPending: showTaskProgressNavigationPending,
100
+ } = useUserBooleanConfig(TASK_NAVIGATION_PROGRESS_CONFIG_ID, false);
101
+ const {
102
+ value: showTaskGoalsNavigation,
103
+ setValue: setShowTaskGoalsNavigation,
104
+ isLoading: showTaskGoalsNavigationLoading,
105
+ isPending: showTaskGoalsNavigationPending,
106
+ } = useUserBooleanConfig(TASK_NAVIGATION_GOALS_CONFIG_ID, false);
107
+ const {
108
+ value: showTaskStatsNavigation,
109
+ setValue: setShowTaskStatsNavigation,
110
+ isLoading: showTaskStatsNavigationLoading,
111
+ isPending: showTaskStatsNavigationPending,
112
+ } = useUserBooleanConfig(TASK_NAVIGATION_STATS_CONFIG_ID, false);
113
+ const {
114
+ value: showTaskLeaderboardsNavigation,
115
+ setValue: setShowTaskLeaderboardsNavigation,
116
+ isLoading: showTaskLeaderboardsNavigationLoading,
117
+ isPending: showTaskLeaderboardsNavigationPending,
118
+ } = useUserBooleanConfig(TASK_NAVIGATION_LEADERBOARDS_CONFIG_ID, false);
119
+ const {
120
+ value: showTaskImportNavigation,
121
+ setValue: setShowTaskImportNavigation,
122
+ isLoading: showTaskImportNavigationLoading,
123
+ isPending: showTaskImportNavigationPending,
124
+ } = useUserBooleanConfig(TASK_NAVIGATION_IMPORT_CONFIG_ID, false);
88
125
  const {
89
126
  value: soundEffectsEnabled,
90
127
  setValue: setSoundEffectsEnabled,
@@ -346,6 +383,73 @@ export function TaskSettings({ workspace }: TaskSettingsProps) {
346
383
  />
347
384
  </SettingItemTab>
348
385
  <Separator />
386
+ <SettingItemTab
387
+ title={t('navigation_progress')}
388
+ description={t('navigation_progress_description')}
389
+ >
390
+ <Switch
391
+ checked={showTaskProgressNavigation}
392
+ onCheckedChange={setShowTaskProgressNavigation}
393
+ disabled={
394
+ showTaskProgressNavigationLoading ||
395
+ showTaskProgressNavigationPending
396
+ }
397
+ />
398
+ </SettingItemTab>
399
+ <Separator />
400
+ <SettingItemTab
401
+ title={t('navigation_goals')}
402
+ description={t('navigation_goals_description')}
403
+ >
404
+ <Switch
405
+ checked={showTaskGoalsNavigation}
406
+ onCheckedChange={setShowTaskGoalsNavigation}
407
+ disabled={
408
+ showTaskGoalsNavigationLoading || showTaskGoalsNavigationPending
409
+ }
410
+ />
411
+ </SettingItemTab>
412
+ <Separator />
413
+ <SettingItemTab
414
+ title={t('navigation_stats')}
415
+ description={t('navigation_stats_description')}
416
+ >
417
+ <Switch
418
+ checked={showTaskStatsNavigation}
419
+ onCheckedChange={setShowTaskStatsNavigation}
420
+ disabled={
421
+ showTaskStatsNavigationLoading || showTaskStatsNavigationPending
422
+ }
423
+ />
424
+ </SettingItemTab>
425
+ <Separator />
426
+ <SettingItemTab
427
+ title={t('navigation_leaderboards')}
428
+ description={t('navigation_leaderboards_description')}
429
+ >
430
+ <Switch
431
+ checked={showTaskLeaderboardsNavigation}
432
+ onCheckedChange={setShowTaskLeaderboardsNavigation}
433
+ disabled={
434
+ showTaskLeaderboardsNavigationLoading ||
435
+ showTaskLeaderboardsNavigationPending
436
+ }
437
+ />
438
+ </SettingItemTab>
439
+ <Separator />
440
+ <SettingItemTab
441
+ title={t('navigation_import')}
442
+ description={t('navigation_import_description')}
443
+ >
444
+ <Switch
445
+ checked={showTaskImportNavigation}
446
+ onCheckedChange={setShowTaskImportNavigation}
447
+ disabled={
448
+ showTaskImportNavigationLoading || showTaskImportNavigationPending
449
+ }
450
+ />
451
+ </SettingItemTab>
452
+ <Separator />
349
453
  <SettingItemTab
350
454
  title={t('submit_shortcut')}
351
455
  description={t('submit_shortcut_description')}
@@ -0,0 +1,5 @@
1
+ import type { createSettingsSearchEngine } from './settings-dialog-search';
2
+
3
+ export function loadSettingsSearchEngine(): Promise<{
4
+ createSettingsSearchEngine: typeof createSettingsSearchEngine;
5
+ }>;
@@ -0,0 +1,3 @@
1
+ export function loadSettingsSearchEngine() {
2
+ return import('./settings-dialog-search');
3
+ }
@@ -0,0 +1,75 @@
1
+ import { removeAccents } from '@tuturuuu/utils/text-helper';
2
+ import type {
3
+ SettingsNavGroup,
4
+ SettingsNavItem,
5
+ } from './settings-dialog-shell';
6
+
7
+ type IndexedSettingsNavItem = {
8
+ groupLabel: string;
9
+ item: SettingsNavItem;
10
+ normalizedSearchText: string;
11
+ };
12
+
13
+ function normalizeSearchText(value: string) {
14
+ return removeAccents(value.toLowerCase());
15
+ }
16
+
17
+ function getItemSearchText(groupLabel: string, item: SettingsNavItem) {
18
+ return [
19
+ groupLabel,
20
+ item.name,
21
+ item.label,
22
+ item.description,
23
+ ...(item.keywords ?? []),
24
+ ...(item.aliases ?? []),
25
+ ...(item.searchLabels ?? []),
26
+ ]
27
+ .filter(Boolean)
28
+ .join(' ');
29
+ }
30
+
31
+ export function createSettingsSearchEngine(navItems: SettingsNavGroup[]) {
32
+ const indexedItems: IndexedSettingsNavItem[] = navItems.flatMap((group) =>
33
+ group.items.map((item) => ({
34
+ groupLabel: group.label,
35
+ item,
36
+ normalizedSearchText: normalizeSearchText(
37
+ getItemSearchText(group.label, item)
38
+ ),
39
+ }))
40
+ );
41
+
42
+ const allItems = indexedItems.map(({ item }) => item);
43
+
44
+ function search(query: string) {
45
+ const normalizedQuery = normalizeSearchText(query.trim());
46
+ if (!normalizedQuery) return navItems;
47
+
48
+ const matchesByName = new Set(
49
+ indexedItems
50
+ .filter(({ normalizedSearchText }) =>
51
+ normalizedSearchText.includes(normalizedQuery)
52
+ )
53
+ .map(({ item }) => item.name)
54
+ );
55
+
56
+ return navItems
57
+ .map((group) => ({
58
+ ...group,
59
+ items: group.items.filter((item) => matchesByName.has(item.name)),
60
+ }))
61
+ .filter((group) => group.items.length > 0);
62
+ }
63
+
64
+ function getEnabledItems(query = '') {
65
+ return search(query)
66
+ .flatMap((group) => group.items)
67
+ .filter((item) => !item.disabled);
68
+ }
69
+
70
+ return {
71
+ allItems,
72
+ getEnabledItems,
73
+ search,
74
+ };
75
+ }
@@ -53,10 +53,11 @@ import {
53
53
  SidebarProvider,
54
54
  } from '@tuturuuu/ui/sidebar';
55
55
  import { cn } from '@tuturuuu/utils/format';
56
- import { removeAccents } from '@tuturuuu/utils/text-helper';
57
56
  import { useTranslations } from 'next-intl';
58
57
  import type { ComponentType, KeyboardEvent, ReactNode } from 'react';
59
58
  import { useCallback, useMemo, useRef, useState } from 'react';
59
+ import type { createSettingsSearchEngine } from './settings-dialog-search';
60
+ import { loadSettingsSearchEngine } from './settings-dialog-search-loader';
60
61
 
61
62
  export interface SettingsNavItem {
62
63
  name: string;
@@ -65,6 +66,8 @@ export interface SettingsNavItem {
65
66
  description?: string;
66
67
  disabled?: boolean;
67
68
  keywords?: string[];
69
+ aliases?: string[];
70
+ searchLabels?: string[];
68
71
  }
69
72
 
70
73
  export interface SettingsNavGroup {
@@ -93,6 +96,9 @@ export interface SettingsDialogShellProps {
93
96
  children: ReactNode;
94
97
  }
95
98
 
99
+ type SettingsSearchEngine = ReturnType<typeof createSettingsSearchEngine>;
100
+ type SettingsSearchEngineFactory = typeof createSettingsSearchEngine;
101
+
96
102
  function isEditableShortcutTarget(target: EventTarget | null) {
97
103
  if (!(target instanceof HTMLElement)) return false;
98
104
 
@@ -127,12 +133,20 @@ export function SettingsDialogShell({
127
133
  const isMobile = useIsMobile();
128
134
  const desktopSearchInputRef = useRef<HTMLInputElement>(null);
129
135
  const mobileSearchInputRef = useRef<HTMLInputElement>(null);
136
+ const searchEngineLoadRef = useRef<Promise<void> | null>(null);
130
137
  const [searchQuery, setSearchQuery] = useState('');
138
+ const [searchEngineFactory, setSearchEngineFactory] =
139
+ useState<SettingsSearchEngineFactory | null>(null);
131
140
  const [mobileNavOpen, setMobileNavOpen] = useState(false);
132
141
 
142
+ const searchEngine = useMemo<SettingsSearchEngine | null>(
143
+ () => searchEngineFactory?.(navItems) ?? null,
144
+ [navItems, searchEngineFactory]
145
+ );
146
+
133
147
  const allNavItems = useMemo(
134
- () => navItems.flatMap((group) => group.items),
135
- [navItems]
148
+ () => searchEngine?.allItems ?? navItems.flatMap((group) => group.items),
149
+ [navItems, searchEngine]
136
150
  );
137
151
 
138
152
  const activeGroup = navItems.find((group) =>
@@ -144,26 +158,39 @@ export function SettingsDialogShell({
144
158
  allNavItems.find((item) => !item.disabled) ||
145
159
  allNavItems[0];
146
160
 
147
- const filteredNavItems = navItems
148
- .map((group) => {
149
- const normalizedQuery = removeAccents(searchQuery.toLowerCase());
150
- const filteredItems = group.items.filter(
151
- (item) =>
152
- removeAccents(item.label.toLowerCase()).includes(normalizedQuery) ||
153
- (item.description &&
154
- removeAccents(item.description.toLowerCase()).includes(
155
- normalizedQuery
156
- )) ||
157
- item.keywords?.some((keyword) =>
158
- removeAccents(keyword.toLowerCase()).includes(normalizedQuery)
159
- )
160
- );
161
- return { ...group, items: filteredItems };
162
- })
163
- .filter((group) => group.items.length > 0);
161
+ const ensureSearchEngine = useCallback(() => {
162
+ if (searchEngineFactory || searchEngineLoadRef.current) return;
163
+
164
+ searchEngineLoadRef.current = loadSettingsSearchEngine()
165
+ .then((module) => {
166
+ setSearchEngineFactory(() => module.createSettingsSearchEngine);
167
+ })
168
+ .finally(() => {
169
+ searchEngineLoadRef.current = null;
170
+ });
171
+ }, [searchEngineFactory]);
164
172
 
165
- const filteredEnabledItems = filteredNavItems.flatMap((group) =>
166
- group.items.filter((item) => !item.disabled)
173
+ const filteredNavItems = useMemo(
174
+ () =>
175
+ searchQuery ? (searchEngine?.search(searchQuery) ?? navItems) : navItems,
176
+ [navItems, searchEngine, searchQuery]
177
+ );
178
+
179
+ const filteredEnabledItems = useMemo(
180
+ () =>
181
+ searchEngine?.getEnabledItems(searchQuery) ??
182
+ filteredNavItems.flatMap((group) =>
183
+ group.items.filter((item) => !item.disabled)
184
+ ),
185
+ [filteredNavItems, searchEngine, searchQuery]
186
+ );
187
+
188
+ const updateSearchQuery = useCallback(
189
+ (value: string) => {
190
+ if (value) ensureSearchEngine();
191
+ setSearchQuery(value);
192
+ },
193
+ [ensureSearchEngine]
167
194
  );
168
195
 
169
196
  const isGroupExpandedByDefault = (groupLabel: string, index: number) => {
@@ -175,14 +202,16 @@ export function SettingsDialogShell({
175
202
  const focusSearch = useCallback(() => {
176
203
  if (isMobile) {
177
204
  setMobileNavOpen(true);
205
+ ensureSearchEngine();
178
206
  requestAnimationFrame(() => {
179
207
  mobileSearchInputRef.current?.focus();
180
208
  });
181
209
  return;
182
210
  }
183
211
 
212
+ ensureSearchEngine();
184
213
  desktopSearchInputRef.current?.focus();
185
- }, [isMobile]);
214
+ }, [ensureSearchEngine, isMobile]);
186
215
 
187
216
  const changeActiveItem = useCallback(
188
217
  (targetIndex: number) => {
@@ -305,7 +334,8 @@ export function SettingsDialogShell({
305
334
  placeholder={t('settings.search_settings_placeholder')}
306
335
  className="bg-background pl-8"
307
336
  value={searchQuery}
308
- onChange={(e) => setSearchQuery(e.target.value)}
337
+ onChange={(e) => updateSearchQuery(e.target.value)}
338
+ onFocus={ensureSearchEngine}
309
339
  />
310
340
  </div>
311
341
  </SidebarHeader>
@@ -403,22 +433,28 @@ export function SettingsDialogShell({
403
433
  {t('search.search')}
404
434
  </DrawerDescription>
405
435
  </DrawerHeader>
406
- <Command className="rounded-none border-0">
436
+ <Command
437
+ className="rounded-none border-0"
438
+ shouldFilter={false}
439
+ >
407
440
  <CommandInput
408
441
  ref={mobileSearchInputRef}
442
+ value={searchQuery}
443
+ onFocus={ensureSearchEngine}
444
+ onValueChange={updateSearchQuery}
409
445
  placeholder={t('settings.search_settings_placeholder')}
410
446
  />
411
447
  <CommandList className="max-h-[50vh]">
412
448
  <CommandEmpty>
413
449
  {t('common.no_results_found')}
414
450
  </CommandEmpty>
415
- {navItems.map((group) => (
451
+ {filteredNavItems.map((group) => (
416
452
  <CommandGroup key={group.label} heading={group.label}>
417
453
  {group.items.map((item) => (
418
454
  <CommandItem
419
455
  disabled={item.disabled}
420
456
  key={item.name}
421
- value={`${group.label} ${item.label} ${item.keywords?.join(' ') || ''}`}
457
+ value={`${group.label} ${item.label} ${item.description || ''} ${item.keywords?.join(' ') || ''} ${item.aliases?.join(' ') || ''} ${item.searchLabels?.join(' ') || ''}`}
422
458
  onSelect={() => {
423
459
  if (item.disabled) return;
424
460
  onActiveTabChange(item.name);
@@ -18,3 +18,26 @@ export function mergeWorkspaceSelectWorkspaces(
18
18
 
19
19
  return [...workspaceList, currentWorkspaceFallback];
20
20
  }
21
+
22
+ export function normalizeWorkspaceSwitchPath(
23
+ pathname: string,
24
+ nextSlug: string
25
+ ) {
26
+ const taskBoardsPath = `/${nextSlug}/tasks/boards`;
27
+
28
+ if (
29
+ pathname === taskBoardsPath ||
30
+ pathname.startsWith(`${taskBoardsPath}/`)
31
+ ) {
32
+ return `/${nextSlug}/tasks`;
33
+ }
34
+
35
+ const uuidRegex =
36
+ /\/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[0-9a-fA-F]{32})$/;
37
+
38
+ if (uuidRegex.test(pathname) && pathname !== `/${nextSlug}`) {
39
+ return pathname.replace(uuidRegex, '');
40
+ }
41
+
42
+ return pathname;
43
+ }
@@ -66,7 +66,10 @@ import {
66
66
  import { Input } from '../input';
67
67
  import { Popover, PopoverContent, PopoverTrigger } from '../popover';
68
68
  import { TUTURUUU_LOGO_URL } from './tuturuuu-logo';
69
- import { mergeWorkspaceSelectWorkspaces } from './workspace-select-helpers';
69
+ import {
70
+ mergeWorkspaceSelectWorkspaces,
71
+ normalizeWorkspaceSwitchPath,
72
+ } from './workspace-select-helpers';
70
73
 
71
74
  const FormSchema = z.object({
72
75
  name: z.string().min(1).max(100),
@@ -438,22 +441,20 @@ export function WorkspaceSelect({
438
441
  const selectedTeam = groups
439
442
  .flatMap((group) => group.teams)
440
443
  .find((team) => team.value === nextSlug);
441
- let newPathname =
442
- selectedTeam?.accessType === 'guest' && selectedTeam.guestLandingPath
443
- ? `/${nextSlug}${selectedTeam.guestLandingPath}`
444
- : pathname
445
- ? (resolveNextPathname?.({
446
- currentPathname: pathname,
447
- nextSlug,
448
- }) ?? pathname.replace(/^\/[^/]+/, `/${nextSlug}`))
449
- : undefined;
444
+ const usesGuestLandingPath =
445
+ selectedTeam?.accessType === 'guest' && selectedTeam.guestLandingPath;
446
+ let newPathname = usesGuestLandingPath
447
+ ? `/${nextSlug}${selectedTeam.guestLandingPath}`
448
+ : pathname
449
+ ? (resolveNextPathname?.({
450
+ currentPathname: pathname,
451
+ nextSlug,
452
+ }) ?? pathname.replace(/^\/[^/]+/, `/${nextSlug}`))
453
+ : undefined;
454
+ if (newPathname && !usesGuestLandingPath) {
455
+ newPathname = normalizeWorkspaceSwitchPath(newPathname, nextSlug);
456
+ }
450
457
  if (newPathname) {
451
- // Regex to match a UUID at the end of the string, with or without dashes
452
- const uuidRegex =
453
- /\/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[0-9a-fA-F]{32})$/;
454
- // Remove the UUID if present, and the current path is not /:wsId
455
- if (uuidRegex.test(newPathname) && newPathname !== `/${nextSlug}`)
456
- newPathname = newPathname.replace(uuidRegex, '');
457
458
  router.push(newPathname);
458
459
  }
459
460
  };
@@ -195,6 +195,22 @@ describe('BoardShareDialog', () => {
195
195
  expect(screen.getByText('pm@example.com')).toBeInTheDocument();
196
196
  });
197
197
 
198
+ it('does not crash when viewable members payload is missing members', async () => {
199
+ listWorkspaceTaskBoardViewableMembersMock.mockResolvedValue({});
200
+
201
+ renderBoardShareDialog();
202
+
203
+ fireEvent.click(
204
+ screen.getByRole('button', {
205
+ name: /ws-task-boards.share.workspace_members.title/,
206
+ })
207
+ );
208
+
209
+ expect(
210
+ await screen.findByText('ws-task-boards.share.workspace_members.empty')
211
+ ).toBeInTheDocument();
212
+ });
213
+
198
214
  it('keeps direct board guests first-class for invite, update, and remove', async () => {
199
215
  listWorkspaceTaskBoardSharesMock.mockResolvedValue({
200
216
  shares: [
@@ -104,6 +104,18 @@ describe('TaskBoardForm', () => {
104
104
  });
105
105
  });
106
106
 
107
+ it('uses the surface background for the board name input', () => {
108
+ render(
109
+ <QueryClientProvider client={queryClient}>
110
+ <TaskBoardForm wsId="ws-1" onFinish={vi.fn()} />
111
+ </QueryClientProvider>
112
+ );
113
+
114
+ expect(screen.getByLabelText('ws-task-boards.name')).toHaveClass(
115
+ 'bg-background'
116
+ );
117
+ });
118
+
107
119
  it('shows the localized duplicate-name message when the API rejects a duplicate board name', async () => {
108
120
  mutateAsync.mockRejectedValueOnce({
109
121
  code: 'TASK_BOARD_NAME_EXISTS',