@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.
- package/CHANGELOG.md +29 -0
- package/package.json +6 -5
- package/src/components/ui/custom/__tests__/settings-dialog-search.test.ts +78 -0
- package/src/components/ui/custom/__tests__/settings-dialog-shell-compile-graph.test.ts +76 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +27 -1
- package/src/components/ui/custom/nav-link.test.tsx +165 -0
- package/src/components/ui/custom/nav-link.tsx +69 -11
- package/src/components/ui/custom/navigation.tsx +1 -0
- package/src/components/ui/custom/settings/task-settings.tsx +104 -0
- package/src/components/ui/custom/settings-dialog-search-loader.d.ts +5 -0
- package/src/components/ui/custom/settings-dialog-search-loader.js +3 -0
- package/src/components/ui/custom/settings-dialog-search.ts +75 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +63 -27
- package/src/components/ui/custom/workspace-select-helpers.ts +23 -0
- package/src/components/ui/custom/workspace-select.tsx +17 -16
- package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +16 -0
- package/src/components/ui/tu-do/boards/__tests__/task-board-form.test.tsx +12 -0
- package/src/components/ui/tu-do/boards/board-share-dialog.tsx +4 -328
- package/src/components/ui/tu-do/boards/board-share-settings-panel.tsx +351 -0
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +50 -37
- package/src/components/ui/tu-do/boards/boardId/enhanced-task-list.tsx +7 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-types.ts +3 -3
- package/src/components/ui/tu-do/boards/boardId/kanban/data/use-bulk-resources.ts +59 -5
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/drag-preview.tsx +20 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +263 -21
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +133 -14
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +112 -54
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +8 -2
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +29 -14
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +24 -1
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +7 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +7 -1
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +20 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +10 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +80 -8
- package/src/components/ui/tu-do/boards/boardId/task-list.tsx +15 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-toolbar.tsx +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +35 -3
- package/src/components/ui/tu-do/boards/form.tsx +1 -1
- package/src/components/ui/tu-do/hooks/__tests__/useTaskLabelManagement.test.tsx +48 -0
- package/src/components/ui/tu-do/hooks/__tests__/useTaskProjectManagement.test.tsx +144 -0
- package/src/components/ui/tu-do/hooks/useTaskDialog.ts +7 -0
- package/src/components/ui/tu-do/hooks/useTaskLabelManagement.ts +115 -106
- package/src/components/ui/tu-do/hooks/useTaskProjectManagement.ts +115 -122
- package/src/components/ui/tu-do/progress/task-progress-import-panel.tsx +60 -0
- package/src/components/ui/tu-do/progress/task-progress-leaderboards-panel.tsx +156 -0
- package/src/components/ui/tu-do/progress/task-progress-page.tsx +348 -0
- package/src/components/ui/tu-do/progress/task-progress-panels.tsx +301 -0
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +26 -0
- package/src/components/ui/tu-do/shared/__tests__/assignee-select.test.tsx +81 -10
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +116 -1
- package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +38 -0
- package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +128 -7
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +222 -9
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +21 -0
- package/src/components/ui/tu-do/shared/__tests__/task-cache-patches.test.ts +147 -0
- package/src/components/ui/tu-do/shared/__tests__/use-progressive-board-loader.test.tsx +3 -0
- package/src/components/ui/tu-do/shared/assignee-select.tsx +77 -26
- package/src/components/ui/tu-do/shared/board-client.tsx +14 -4
- package/src/components/ui/tu-do/shared/board-header.tsx +8 -1
- package/src/components/ui/tu-do/shared/board-switcher.tsx +70 -38
- package/src/components/ui/tu-do/shared/board-user-presence-avatars.tsx +18 -12
- package/src/components/ui/tu-do/shared/board-views.tsx +49 -69
- package/src/components/ui/tu-do/shared/list-view.tsx +21 -3
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +4 -4
- package/src/components/ui/tu-do/shared/task-cache-patches.ts +394 -0
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +21 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +5 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +25 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +7 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-data.ts +79 -10
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +76 -77
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.test.tsx +63 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-relationships.ts +78 -69
- package/src/components/ui/tu-do/shared/task-edit-dialog/personal-overrides-section.tsx +28 -8
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/dependencies-section.tsx +14 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/parent-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/related-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/subtasks-section.tsx +6 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/relationships/types/task-relationships.types.ts +8 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +8 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.test.tsx +150 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +61 -35
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-relationships-properties.tsx +44 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +9 -0
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +11 -0
- package/src/components/ui/tu-do/shared/use-progressive-board-loader.ts +2 -0
- package/src/hooks/__tests__/useBoardPresence.test.tsx +191 -0
- package/src/hooks/__tests__/useBoardRealtime.test.tsx +24 -144
- package/src/hooks/useBoardPresence.ts +364 -0
- package/src/hooks/useBoardRealtimeEventHandler.ts +34 -90
- 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,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
|
|
148
|
-
.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
166
|
-
|
|
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) =>
|
|
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
|
|
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
|
-
{
|
|
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 {
|
|
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
|
-
|
|
442
|
-
selectedTeam?.accessType === 'guest' && selectedTeam.guestLandingPath
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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',
|