@tuturuuu/ui 0.37.1 → 0.37.2
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 +12 -0
- package/package.json +3 -3
- package/src/components/ui/custom/__tests__/workspace-picker-content.test.tsx +181 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +24 -38
- package/src/components/ui/custom/hidden-workspaces-settings.tsx +49 -0
- package/src/components/ui/custom/mobile-header-height.ts +16 -0
- package/src/components/ui/custom/structure.tsx +37 -5
- package/src/components/ui/custom/user-filters.tsx +2 -1
- package/src/components/ui/custom/visible-workspace-filter.tsx +41 -0
- package/src/components/ui/custom/workspace-picker-content.tsx +293 -0
- package/src/components/ui/custom/workspace-select-helpers.ts +11 -0
- package/src/components/ui/custom/workspace-select-invitations.tsx +3 -0
- package/src/components/ui/custom/workspace-select.tsx +87 -359
- package/src/components/ui/legacy/calendar/settings/calendar-sync-dashboard.tsx +4 -23
- package/src/components/ui/legacy/calendar/settings/sync-logs-table.tsx +3 -6
- package/src/components/ui/resizable.tsx +2 -1
- package/src/hooks/__tests__/use-visible-workspaces.test.tsx +76 -0
- package/src/hooks/__tests__/use-workspace-visibility.test.tsx +155 -0
- package/src/hooks/__tests__/workspace-actor-lifetime.test.tsx +103 -0
- package/src/hooks/use-visible-workspaces.ts +57 -0
- package/src/hooks/use-workspace-user.ts +15 -3
- package/src/hooks/use-workspace-visibility.ts +234 -0
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
4
|
-
import {
|
|
5
|
-
CheckIcon,
|
|
6
|
-
ChevronDown,
|
|
7
|
-
Link,
|
|
8
|
-
Loader2,
|
|
9
|
-
PlusCircle,
|
|
10
|
-
Star,
|
|
11
|
-
} from '@tuturuuu/icons';
|
|
4
|
+
import { ChevronDown } from '@tuturuuu/icons';
|
|
12
5
|
import { InternalApiError } from '@tuturuuu/internal-api/client';
|
|
13
6
|
import { updateCurrentUserDefaultWorkspace } from '@tuturuuu/internal-api/users';
|
|
14
7
|
import {
|
|
@@ -35,18 +28,14 @@ import { toast } from 'sonner';
|
|
|
35
28
|
import { z } from 'zod';
|
|
36
29
|
import { useForm } from '../../../hooks/use-form';
|
|
37
30
|
import { useWorkspaceUser } from '../../../hooks/use-workspace-user';
|
|
31
|
+
import {
|
|
32
|
+
useWorkspaceActor,
|
|
33
|
+
useWorkspaceVisibility,
|
|
34
|
+
} from '../../../hooks/use-workspace-visibility';
|
|
38
35
|
import { zodResolver } from '../../../resolvers';
|
|
39
36
|
import { Badge } from '../badge';
|
|
40
37
|
import { Button } from '../button';
|
|
41
|
-
import {
|
|
42
|
-
Command,
|
|
43
|
-
CommandEmpty,
|
|
44
|
-
CommandGroup,
|
|
45
|
-
CommandInput,
|
|
46
|
-
CommandItem,
|
|
47
|
-
CommandList,
|
|
48
|
-
CommandSeparator,
|
|
49
|
-
} from '../command';
|
|
38
|
+
import { Command } from '../command';
|
|
50
39
|
import {
|
|
51
40
|
Dialog,
|
|
52
41
|
DialogContent,
|
|
@@ -54,6 +43,7 @@ import {
|
|
|
54
43
|
DialogFooter,
|
|
55
44
|
DialogHeader,
|
|
56
45
|
DialogTitle,
|
|
46
|
+
DialogTrigger,
|
|
57
47
|
} from '../dialog';
|
|
58
48
|
import {
|
|
59
49
|
Form,
|
|
@@ -64,12 +54,13 @@ import {
|
|
|
64
54
|
FormMessage,
|
|
65
55
|
} from '../form';
|
|
66
56
|
import { Input } from '../input';
|
|
67
|
-
import { Popover, PopoverContent, PopoverTrigger } from '../popover';
|
|
68
57
|
import { TUTURUUU_LOGO_URL } from './tuturuuu-logo';
|
|
58
|
+
import { WorkspacePickerContent } from './workspace-picker-content';
|
|
69
59
|
import {
|
|
70
60
|
buildWorkspaceSetupHandoffUrl,
|
|
71
61
|
mergeWorkspaceSelectWorkspaces,
|
|
72
62
|
normalizeWorkspaceSwitchPath,
|
|
63
|
+
resolveGuestWorkspaceLanding,
|
|
73
64
|
resolveWorkspaceAvatarUrl,
|
|
74
65
|
} from './workspace-select-helpers';
|
|
75
66
|
import { WorkspaceIcon } from './workspace-select-icon';
|
|
@@ -100,7 +91,6 @@ export function WorkspaceSelect({
|
|
|
100
91
|
fallbackLogoUrl = TUTURUUU_LOGO_URL,
|
|
101
92
|
resolveNextPathname,
|
|
102
93
|
triggerClassName,
|
|
103
|
-
popoverModal = false,
|
|
104
94
|
platformWorkspaceSetupUrl,
|
|
105
95
|
cacheScope,
|
|
106
96
|
}: {
|
|
@@ -119,8 +109,6 @@ export function WorkspaceSelect({
|
|
|
119
109
|
nextSlug: string;
|
|
120
110
|
}) => string;
|
|
121
111
|
triggerClassName?: string;
|
|
122
|
-
/** Keep the picker interactive and scrollable when rendered inside a modal. */
|
|
123
|
-
popoverModal?: boolean;
|
|
124
112
|
/** Platform origin used to prepare a newly created satellite workspace. */
|
|
125
113
|
platformWorkspaceSetupUrl?: string;
|
|
126
114
|
/** Authenticated identity used to isolate user-specific picker caches. */
|
|
@@ -132,14 +120,22 @@ export function WorkspaceSelect({
|
|
|
132
120
|
const pathname = usePathname();
|
|
133
121
|
const queryClient = useQueryClient();
|
|
134
122
|
|
|
123
|
+
const actorScope = useWorkspaceActor();
|
|
124
|
+
const visibility = useWorkspaceVisibility();
|
|
125
|
+
const actorId = actorScope?.actorId ?? cacheScope;
|
|
135
126
|
const resolvedWorkspaceId =
|
|
136
127
|
wsId && wsId !== PERSONAL_WORKSPACE_SLUG
|
|
137
128
|
? resolveWorkspaceId(wsId)
|
|
138
129
|
: undefined;
|
|
139
130
|
const { data: listedWorkspaces } = useQuery({
|
|
140
|
-
queryKey: ['
|
|
141
|
-
queryFn:
|
|
142
|
-
|
|
131
|
+
queryKey: ['workspace-ui-list', actorId],
|
|
132
|
+
queryFn: async () => {
|
|
133
|
+
actorScope?.assertActive();
|
|
134
|
+
const result = await fetchWorkspaces();
|
|
135
|
+
actorScope?.assertActive();
|
|
136
|
+
return result;
|
|
137
|
+
},
|
|
138
|
+
enabled: Boolean(wsId && actorId),
|
|
143
139
|
});
|
|
144
140
|
const hasListedCurrentWorkspace = Boolean(
|
|
145
141
|
resolvedWorkspaceId &&
|
|
@@ -150,19 +146,21 @@ export function WorkspaceSelect({
|
|
|
150
146
|
const { data: currentWorkspaceFallback } = useQuery({
|
|
151
147
|
queryKey: [
|
|
152
148
|
'workspace-select-current-workspace',
|
|
149
|
+
actorId,
|
|
153
150
|
resolvedWorkspaceId,
|
|
154
|
-
...(cacheScope ? [cacheScope] : []),
|
|
155
151
|
],
|
|
156
152
|
queryFn: async () =>
|
|
157
153
|
(await getWorkspace(resolvedWorkspaceId!)) as InternalApiWorkspaceSummary,
|
|
158
|
-
enabled: Boolean(
|
|
154
|
+
enabled: Boolean(
|
|
155
|
+
actorId && resolvedWorkspaceId && !hasListedCurrentWorkspace
|
|
156
|
+
),
|
|
159
157
|
retry: 1,
|
|
160
158
|
});
|
|
161
159
|
const workspaces = mergeWorkspaceSelectWorkspaces(
|
|
162
160
|
listedWorkspaces,
|
|
163
161
|
currentWorkspaceFallback
|
|
164
162
|
);
|
|
165
|
-
const { data: currentUser } = useWorkspaceUser();
|
|
163
|
+
const { data: currentUser } = useWorkspaceUser(actorId);
|
|
166
164
|
const defaultWorkspaceId = currentUser?.default_workspace_id || null;
|
|
167
165
|
|
|
168
166
|
const form = useForm({
|
|
@@ -185,8 +183,8 @@ export function WorkspaceSelect({
|
|
|
185
183
|
const [loading, setLoading] = useState(false);
|
|
186
184
|
const [joiningByHandle, setJoiningByHandle] = useState(false);
|
|
187
185
|
const invitationController = useWorkspaceInvitations({
|
|
188
|
-
cacheScope,
|
|
189
|
-
enabled: Boolean(wsId),
|
|
186
|
+
cacheScope: actorId,
|
|
187
|
+
enabled: Boolean(wsId && actorId),
|
|
190
188
|
onAccepted: (invitation) => {
|
|
191
189
|
setOpen(false);
|
|
192
190
|
const slug = invitation.workspace.handle || invitation.workspace.id;
|
|
@@ -202,7 +200,7 @@ export function WorkspaceSelect({
|
|
|
202
200
|
updateCurrentUserDefaultWorkspace(workspaceId),
|
|
203
201
|
onSuccess: (_, workspaceId) => {
|
|
204
202
|
queryClient.setQueryData(
|
|
205
|
-
['workspace-user'],
|
|
203
|
+
['workspace-user', ...(actorId ? [actorId] : [])],
|
|
206
204
|
(previous: WorkspaceUser | undefined) =>
|
|
207
205
|
previous
|
|
208
206
|
? {
|
|
@@ -216,7 +214,9 @@ export function WorkspaceSelect({
|
|
|
216
214
|
void queryClient.invalidateQueries({ queryKey: ['default-workspace'] });
|
|
217
215
|
void queryClient.invalidateQueries({ queryKey: ['user'] });
|
|
218
216
|
void queryClient.invalidateQueries({ queryKey: ['user-workspaces'] });
|
|
219
|
-
void queryClient.invalidateQueries({
|
|
217
|
+
void queryClient.invalidateQueries({
|
|
218
|
+
queryKey: ['workspace-ui-list', actorId],
|
|
219
|
+
});
|
|
220
220
|
router.refresh();
|
|
221
221
|
},
|
|
222
222
|
onError: (error) => {
|
|
@@ -287,146 +287,31 @@ export function WorkspaceSelect({
|
|
|
287
287
|
}
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
const personalWorkspace = workspaces
|
|
291
|
-
(ws) => ws
|
|
290
|
+
const personalWorkspace = workspaces.find(
|
|
291
|
+
(ws) => ws.personal && ws.access_type !== 'guest'
|
|
292
292
|
);
|
|
293
|
-
const rootWorkspace = workspaces?.find(
|
|
294
|
-
(ws) => ws?.id === ROOT_WORKSPACE_ID && ws.access_type !== 'guest'
|
|
295
|
-
);
|
|
296
|
-
const nonPersonalWorkspaces =
|
|
297
|
-
workspaces?.filter(
|
|
298
|
-
(ws) =>
|
|
299
|
-
!ws?.personal &&
|
|
300
|
-
ws?.id !== ROOT_WORKSPACE_ID &&
|
|
301
|
-
ws.access_type !== 'guest'
|
|
302
|
-
) || [];
|
|
303
|
-
const guestWorkspaces =
|
|
304
|
-
workspaces?.filter((ws) => ws.access_type === 'guest') || [];
|
|
305
|
-
|
|
306
|
-
const groups = [
|
|
307
|
-
rootWorkspace && {
|
|
308
|
-
id: 'root',
|
|
309
|
-
label: t('common.system'),
|
|
310
|
-
teams: [
|
|
311
|
-
{
|
|
312
|
-
id: rootWorkspace.id,
|
|
313
|
-
label: rootWorkspace.name || t('common.root'),
|
|
314
|
-
value: ROOT_WORKSPACE_ID,
|
|
315
|
-
avatarUrl: resolveWorkspaceAvatarUrl(rootWorkspace.avatar_url, {
|
|
316
|
-
rootWorkspaceLogoUrl: TUTURUUU_LOGO_URL,
|
|
317
|
-
}),
|
|
318
|
-
tier: rootWorkspace.tier as
|
|
319
|
-
| 'FREE'
|
|
320
|
-
| 'PLUS'
|
|
321
|
-
| 'PRO'
|
|
322
|
-
| 'ENTERPRISE'
|
|
323
|
-
| null,
|
|
324
|
-
},
|
|
325
|
-
],
|
|
326
|
-
},
|
|
327
|
-
personalWorkspace && {
|
|
328
|
-
id: 'personal',
|
|
329
|
-
label: t('common.personal_account'),
|
|
330
|
-
teams: [
|
|
331
|
-
{
|
|
332
|
-
id: personalWorkspace.id,
|
|
333
|
-
label: personalWorkspace.name || 'Personal',
|
|
334
|
-
value: PERSONAL_WORKSPACE_SLUG,
|
|
335
|
-
avatarUrl: resolveWorkspaceAvatarUrl(personalWorkspace.avatar_url),
|
|
336
|
-
tier: personalWorkspace.tier as
|
|
337
|
-
| 'FREE'
|
|
338
|
-
| 'PLUS'
|
|
339
|
-
| 'PRO'
|
|
340
|
-
| 'ENTERPRISE'
|
|
341
|
-
| null,
|
|
342
|
-
},
|
|
343
|
-
],
|
|
344
|
-
},
|
|
345
|
-
nonPersonalWorkspaces.length > 0 && {
|
|
346
|
-
id: 'workspaces',
|
|
347
|
-
label: t('common.workspaces'),
|
|
348
|
-
teams: nonPersonalWorkspaces.map(
|
|
349
|
-
(workspace: InternalApiWorkspaceSummary) => ({
|
|
350
|
-
id: workspace.id,
|
|
351
|
-
label: workspace.name || 'Untitled',
|
|
352
|
-
value: toWorkspaceSlug(workspace.id, {
|
|
353
|
-
personal: workspace?.personal,
|
|
354
|
-
}),
|
|
355
|
-
// Signal creator-owned workspaces for UI
|
|
356
|
-
isCreator: workspace?.created_by_me === true,
|
|
357
|
-
avatarUrl: resolveWorkspaceAvatarUrl(workspace.avatar_url),
|
|
358
|
-
tier: workspace.tier || null,
|
|
359
|
-
})
|
|
360
|
-
),
|
|
361
|
-
},
|
|
362
|
-
guestWorkspaces.length > 0 && {
|
|
363
|
-
id: 'guest-workspaces',
|
|
364
|
-
label: t('common.guest_workspaces'),
|
|
365
|
-
teams: guestWorkspaces.map((workspace: InternalApiWorkspaceSummary) => ({
|
|
366
|
-
id: workspace.id,
|
|
367
|
-
label: workspace.name || 'Untitled',
|
|
368
|
-
value: toWorkspaceSlug(workspace.id, {
|
|
369
|
-
personal: workspace?.personal,
|
|
370
|
-
}),
|
|
371
|
-
accessType: 'guest' as const,
|
|
372
|
-
avatarUrl: resolveWorkspaceAvatarUrl(workspace.avatar_url),
|
|
373
|
-
guestBoardCount: workspace.guest_board_count ?? 0,
|
|
374
|
-
guestLandingPath: workspace.guest_landing_path ?? '/tasks/boards',
|
|
375
|
-
guestProducts: workspace.guest_products ?? ['tasks'],
|
|
376
|
-
guestPermission: workspace.guest_highest_permission ?? 'view',
|
|
377
|
-
tier: workspace.tier || null,
|
|
378
|
-
})),
|
|
379
|
-
},
|
|
380
|
-
].filter(Boolean) as {
|
|
381
|
-
id: string;
|
|
382
|
-
label: string;
|
|
383
|
-
teams: {
|
|
384
|
-
id: string;
|
|
385
|
-
label: string;
|
|
386
|
-
value: string | undefined;
|
|
387
|
-
isCreator?: boolean;
|
|
388
|
-
avatarUrl?: string | null;
|
|
389
|
-
accessType?: 'member' | 'guest';
|
|
390
|
-
guestBoardCount?: number;
|
|
391
|
-
guestLandingPath?: string | null;
|
|
392
|
-
guestPermission?: 'view' | 'edit' | null;
|
|
393
|
-
guestProducts?: Array<'tasks'>;
|
|
394
|
-
tier?: 'FREE' | 'PLUS' | 'PRO' | 'ENTERPRISE' | null;
|
|
395
|
-
isRoot?: boolean;
|
|
396
|
-
}[];
|
|
397
|
-
}[];
|
|
398
|
-
|
|
399
|
-
const onValueChange = (nextSlug: string) => {
|
|
400
|
-
const selectedTeam = groups
|
|
401
|
-
.flatMap((group) => group.teams)
|
|
402
|
-
.find((team) => team.value === nextSlug);
|
|
403
|
-
const usesGuestLandingPath =
|
|
404
|
-
selectedTeam?.accessType === 'guest' && selectedTeam.guestLandingPath;
|
|
405
|
-
let newPathname = usesGuestLandingPath
|
|
406
|
-
? `/${nextSlug}${selectedTeam.guestLandingPath}`
|
|
407
|
-
: pathname
|
|
408
|
-
? (resolveNextPathname?.({
|
|
409
|
-
currentPathname: pathname,
|
|
410
|
-
nextSlug,
|
|
411
|
-
}) ?? pathname.replace(/^\/[^/]+/, `/${nextSlug}`))
|
|
412
|
-
: undefined;
|
|
413
|
-
if (newPathname && !usesGuestLandingPath) {
|
|
414
|
-
newPathname = normalizeWorkspaceSwitchPath(newPathname, nextSlug);
|
|
415
|
-
}
|
|
416
|
-
if (newPathname) {
|
|
417
|
-
router.push(newPathname);
|
|
418
|
-
}
|
|
419
|
-
};
|
|
420
|
-
|
|
421
293
|
const hasSelectableWorkspaces =
|
|
422
294
|
workspaces.length > 0 || invitations.length > 0;
|
|
423
295
|
useOpenWorkspaceSelectWhenRevealed(hasSelectableWorkspaces, setOpen);
|
|
424
|
-
|
|
296
|
+
const onValueChange = (selected: InternalApiWorkspaceSummary) => {
|
|
297
|
+
const nextSlug = toWorkspaceSlug(selected.id, {
|
|
298
|
+
personal: selected.personal,
|
|
299
|
+
});
|
|
300
|
+
const guestLanding = resolveGuestWorkspaceLanding(selected);
|
|
301
|
+
let nextPath = guestLanding
|
|
302
|
+
? `/${nextSlug}${guestLanding}`
|
|
303
|
+
: pathname
|
|
304
|
+
? (resolveNextPathname?.({ currentPathname: pathname, nextSlug }) ??
|
|
305
|
+
pathname.replace(/^\/[^/]+/, `/${nextSlug}`))
|
|
306
|
+
: getWorkspaceLandingPath(nextSlug);
|
|
307
|
+
if (!guestLanding)
|
|
308
|
+
nextPath = normalizeWorkspaceSwitchPath(nextPath, nextSlug);
|
|
309
|
+
router.push(nextPath);
|
|
310
|
+
};
|
|
425
311
|
const workspace =
|
|
426
312
|
wsId === PERSONAL_WORKSPACE_SLUG
|
|
427
313
|
? personalWorkspace
|
|
428
|
-
:
|
|
429
|
-
guestWorkspaces.find((ws) => ws.id === resolvedWorkspaceId));
|
|
314
|
+
: workspaces.find((ws) => ws.id === resolvedWorkspaceId);
|
|
430
315
|
if (!wsId) return <div />;
|
|
431
316
|
|
|
432
317
|
async function onJoinByHandleSubmit(
|
|
@@ -442,7 +327,9 @@ export function WorkspaceSelect({
|
|
|
442
327
|
setShowJoinWorkspaceDialog(false);
|
|
443
328
|
setOpen(false);
|
|
444
329
|
|
|
445
|
-
void queryClient.invalidateQueries({
|
|
330
|
+
void queryClient.invalidateQueries({
|
|
331
|
+
queryKey: ['workspace-ui-list', actorId],
|
|
332
|
+
});
|
|
446
333
|
void queryClient.invalidateQueries({ queryKey: ['user-workspaces'] });
|
|
447
334
|
void queryClient.invalidateQueries({ queryKey: ['workspace-user'] });
|
|
448
335
|
|
|
@@ -530,8 +417,8 @@ export function WorkspaceSelect({
|
|
|
530
417
|
setShowNewWorkspaceDialog(open);
|
|
531
418
|
}}
|
|
532
419
|
>
|
|
533
|
-
<
|
|
534
|
-
<
|
|
420
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
421
|
+
<DialogTrigger asChild disabled={!hasSelectableWorkspaces}>
|
|
535
422
|
<Button
|
|
536
423
|
size="xs"
|
|
537
424
|
variant="outline"
|
|
@@ -598,202 +485,39 @@ export function WorkspaceSelect({
|
|
|
598
485
|
<ChevronDown className="ml-1 h-4 w-4 shrink-0 opacity-50" />
|
|
599
486
|
)}
|
|
600
487
|
</Button>
|
|
601
|
-
</
|
|
602
|
-
<
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
488
|
+
</DialogTrigger>
|
|
489
|
+
<WorkspacePickerContent
|
|
490
|
+
workspaces={workspaces ?? []}
|
|
491
|
+
currentId={workspace?.id}
|
|
492
|
+
defaultId={defaultWorkspaceId}
|
|
493
|
+
visibility={visibility}
|
|
494
|
+
onSelect={(selected) => {
|
|
495
|
+
onValueChange(selected);
|
|
496
|
+
setOpen(false);
|
|
497
|
+
}}
|
|
498
|
+
onDefault={(id) => updateDefaultWorkspaceMutation.mutate(id)}
|
|
499
|
+
onCreate={
|
|
500
|
+
disableCreateNewWorkspace
|
|
501
|
+
? undefined
|
|
502
|
+
: () => {
|
|
503
|
+
setOpen(false);
|
|
504
|
+
setShowNewWorkspaceDialog(true);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
onJoin={() => {
|
|
508
|
+
setOpen(false);
|
|
509
|
+
setShowJoinWorkspaceDialog(true);
|
|
510
|
+
}}
|
|
511
|
+
invitations={
|
|
512
|
+
<Command>
|
|
607
513
|
<WorkspaceInvitationItems
|
|
608
514
|
controller={invitationController}
|
|
609
515
|
fallbackLogoUrl={fallbackLogoUrl}
|
|
610
516
|
/>
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
id: string;
|
|
616
|
-
label: string;
|
|
617
|
-
value: string | undefined;
|
|
618
|
-
isCreator?: boolean;
|
|
619
|
-
avatarUrl?: string | null;
|
|
620
|
-
accessType?: 'member' | 'guest';
|
|
621
|
-
guestBoardCount?: number;
|
|
622
|
-
guestLandingPath?: string | null;
|
|
623
|
-
guestPermission?: 'view' | 'edit' | null;
|
|
624
|
-
guestProducts?: Array<'tasks'>;
|
|
625
|
-
tier?: 'FREE' | 'PLUS' | 'PRO' | 'ENTERPRISE' | null;
|
|
626
|
-
isRoot?: boolean;
|
|
627
|
-
}) => {
|
|
628
|
-
const isCurrentWorkspace = wsId === team.value;
|
|
629
|
-
const isDefaultWorkspace =
|
|
630
|
-
defaultWorkspaceId === team.id;
|
|
631
|
-
const isUpdatingDefaultWorkspace =
|
|
632
|
-
updateDefaultWorkspaceMutation.isPending &&
|
|
633
|
-
updateDefaultWorkspaceMutation.variables === team.id;
|
|
634
|
-
|
|
635
|
-
return (
|
|
636
|
-
<CommandItem
|
|
637
|
-
key={team.value}
|
|
638
|
-
value={`${team.label} ${team.value || ''}`}
|
|
639
|
-
onSelect={() => {
|
|
640
|
-
if (!team?.value || team?.value === wsId) return;
|
|
641
|
-
onValueChange(team.value);
|
|
642
|
-
setOpen(false);
|
|
643
|
-
}}
|
|
644
|
-
className={cn(
|
|
645
|
-
'gap-1.5 text-sm',
|
|
646
|
-
isCurrentWorkspace && 'bg-accent'
|
|
647
|
-
)}
|
|
648
|
-
disabled={!team}
|
|
649
|
-
>
|
|
650
|
-
<WorkspaceIcon
|
|
651
|
-
fallbackLogoUrl={fallbackLogoUrl}
|
|
652
|
-
name={team.label}
|
|
653
|
-
avatarUrl={team.avatarUrl}
|
|
654
|
-
/>
|
|
655
|
-
<div className="flex min-w-0 flex-1 items-center gap-1.5">
|
|
656
|
-
<span className="line-clamp-1 min-w-0 flex-1 text-xs">
|
|
657
|
-
{team.label}
|
|
658
|
-
</span>
|
|
659
|
-
{showTierBadges && (
|
|
660
|
-
<Badge
|
|
661
|
-
variant="outline"
|
|
662
|
-
className={cn(
|
|
663
|
-
'h-4 shrink-0 px-1 py-0 font-medium text-[10px]',
|
|
664
|
-
(!team.tier || team.tier === 'FREE') &&
|
|
665
|
-
'border-muted-foreground/30 bg-muted/50 text-muted-foreground',
|
|
666
|
-
team.tier === 'PLUS' &&
|
|
667
|
-
'border-dynamic-blue/50 bg-dynamic-blue/10 text-dynamic-blue',
|
|
668
|
-
team.tier === 'PRO' &&
|
|
669
|
-
'border-dynamic-purple/50 bg-dynamic-purple/10 text-dynamic-purple',
|
|
670
|
-
team.tier === 'ENTERPRISE' &&
|
|
671
|
-
'border-dynamic-amber/50 bg-dynamic-amber/10 text-dynamic-amber'
|
|
672
|
-
)}
|
|
673
|
-
>
|
|
674
|
-
{team.tier || 'FREE'}
|
|
675
|
-
</Badge>
|
|
676
|
-
)}
|
|
677
|
-
{team.accessType === 'guest' && (
|
|
678
|
-
<>
|
|
679
|
-
<Badge
|
|
680
|
-
variant="outline"
|
|
681
|
-
className="h-4 shrink-0 border-dynamic-green/50 bg-dynamic-green/10 px-1 py-0 font-medium text-[10px] text-dynamic-green"
|
|
682
|
-
>
|
|
683
|
-
{t('common.guest_access')}
|
|
684
|
-
</Badge>
|
|
685
|
-
<Badge
|
|
686
|
-
variant="outline"
|
|
687
|
-
className="h-4 shrink-0 border-dynamic-blue/50 bg-dynamic-blue/10 px-1 py-0 font-medium text-[10px] text-dynamic-blue"
|
|
688
|
-
>
|
|
689
|
-
{t('common.tasks_only')}
|
|
690
|
-
</Badge>
|
|
691
|
-
</>
|
|
692
|
-
)}
|
|
693
|
-
</div>
|
|
694
|
-
<div className="flex shrink-0 items-center gap-0.5">
|
|
695
|
-
{team.accessType !== 'guest' && (
|
|
696
|
-
<Button
|
|
697
|
-
type="button"
|
|
698
|
-
variant={
|
|
699
|
-
isDefaultWorkspace ? 'secondary' : 'ghost'
|
|
700
|
-
}
|
|
701
|
-
size="xs"
|
|
702
|
-
className={cn(
|
|
703
|
-
'h-6 w-6 shrink-0 rounded-sm p-0',
|
|
704
|
-
isDefaultWorkspace &&
|
|
705
|
-
'bg-dynamic-amber/12 text-dynamic-amber hover:bg-dynamic-amber/18 hover:text-dynamic-amber'
|
|
706
|
-
)}
|
|
707
|
-
aria-label="Default workspace"
|
|
708
|
-
title="Default workspace"
|
|
709
|
-
disabled={
|
|
710
|
-
isDefaultWorkspace ||
|
|
711
|
-
isUpdatingDefaultWorkspace ||
|
|
712
|
-
updateDefaultWorkspaceMutation.isPending
|
|
713
|
-
}
|
|
714
|
-
onMouseDown={(event) => {
|
|
715
|
-
event.preventDefault();
|
|
716
|
-
}}
|
|
717
|
-
onClick={(event) => {
|
|
718
|
-
event.preventDefault();
|
|
719
|
-
event.stopPropagation();
|
|
720
|
-
|
|
721
|
-
if (
|
|
722
|
-
isDefaultWorkspace ||
|
|
723
|
-
isUpdatingDefaultWorkspace
|
|
724
|
-
) {
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
updateDefaultWorkspaceMutation.mutate(
|
|
729
|
-
team.id
|
|
730
|
-
);
|
|
731
|
-
}}
|
|
732
|
-
>
|
|
733
|
-
{isUpdatingDefaultWorkspace ? (
|
|
734
|
-
<Loader2 className="h-3 w-3 animate-spin" />
|
|
735
|
-
) : (
|
|
736
|
-
<Star
|
|
737
|
-
className={cn(
|
|
738
|
-
'h-3 w-3',
|
|
739
|
-
isDefaultWorkspace && 'fill-current'
|
|
740
|
-
)}
|
|
741
|
-
/>
|
|
742
|
-
)}
|
|
743
|
-
</Button>
|
|
744
|
-
)}
|
|
745
|
-
<CheckIcon
|
|
746
|
-
className={cn(
|
|
747
|
-
'h-3.5 w-3.5 shrink-0',
|
|
748
|
-
isCurrentWorkspace
|
|
749
|
-
? 'opacity-100'
|
|
750
|
-
: 'opacity-0'
|
|
751
|
-
)}
|
|
752
|
-
/>
|
|
753
|
-
</div>
|
|
754
|
-
</CommandItem>
|
|
755
|
-
);
|
|
756
|
-
}
|
|
757
|
-
)}
|
|
758
|
-
</CommandGroup>
|
|
759
|
-
))}
|
|
760
|
-
</CommandList>
|
|
761
|
-
<CommandSeparator />
|
|
762
|
-
<CommandGroup
|
|
763
|
-
className={cn(
|
|
764
|
-
'[&_[cmdk-group-items]]:grid [&_[cmdk-group-items]]:gap-1',
|
|
765
|
-
disableCreateNewWorkspace
|
|
766
|
-
? '[&_[cmdk-group-items]]:grid-cols-1'
|
|
767
|
-
: '[&_[cmdk-group-items]]:grid-cols-2'
|
|
768
|
-
)}
|
|
769
|
-
>
|
|
770
|
-
{!disableCreateNewWorkspace && (
|
|
771
|
-
<CommandItem
|
|
772
|
-
className="h-9 justify-center gap-1.5 px-2"
|
|
773
|
-
disabled={disableCreateNewWorkspace}
|
|
774
|
-
onSelect={() => {
|
|
775
|
-
setOpen(false);
|
|
776
|
-
setShowNewWorkspaceDialog(true);
|
|
777
|
-
}}
|
|
778
|
-
>
|
|
779
|
-
<PlusCircle className="size-4" />
|
|
780
|
-
<span>{t('common.create_workspace_action')}</span>
|
|
781
|
-
</CommandItem>
|
|
782
|
-
)}
|
|
783
|
-
<CommandItem
|
|
784
|
-
className="h-9 justify-center gap-1.5 px-2"
|
|
785
|
-
onSelect={() => {
|
|
786
|
-
setOpen(false);
|
|
787
|
-
setShowJoinWorkspaceDialog(true);
|
|
788
|
-
}}
|
|
789
|
-
>
|
|
790
|
-
<Link className="size-4" />
|
|
791
|
-
<span>{t('common.join_workspace_action')}</span>
|
|
792
|
-
</CommandItem>
|
|
793
|
-
</CommandGroup>
|
|
794
|
-
</Command>
|
|
795
|
-
</PopoverContent>
|
|
796
|
-
</Popover>
|
|
517
|
+
</Command>
|
|
518
|
+
}
|
|
519
|
+
/>
|
|
520
|
+
</Dialog>
|
|
797
521
|
<DialogContent>
|
|
798
522
|
<DialogHeader>
|
|
799
523
|
<DialogTitle>{t('common.create_workspace')}</DialogTitle>
|
|
@@ -848,3 +572,7 @@ export function WorkspaceSelect({
|
|
|
848
572
|
</>
|
|
849
573
|
);
|
|
850
574
|
}
|
|
575
|
+
|
|
576
|
+
export { HiddenWorkspacesSettings } from './hidden-workspaces-settings';
|
|
577
|
+
|
|
578
|
+
export { VisibleWorkspaceFilter } from './visible-workspace-filter';
|
|
@@ -1,42 +1,23 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useQuery } from '@tanstack/react-query';
|
|
4
3
|
import { Calendar, Download, RefreshCw } from '@tuturuuu/icons';
|
|
5
|
-
import type { Workspace } from '@tuturuuu/types';
|
|
6
4
|
import { Button } from '@tuturuuu/ui/button';
|
|
7
5
|
import { AnalyticsCharts } from '@tuturuuu/ui/legacy/calendar/settings/analytics-charts';
|
|
8
6
|
import { SummaryCards } from '@tuturuuu/ui/legacy/calendar/settings/summary-cards';
|
|
9
7
|
import { SyncLogsTable } from '@tuturuuu/ui/legacy/calendar/settings/sync-logs-table';
|
|
10
8
|
import { useMemo, useState } from 'react';
|
|
9
|
+
import { useVisibleWorkspaces } from '../../../../../hooks/use-visible-workspaces';
|
|
11
10
|
import type { SyncLog } from './types';
|
|
12
11
|
|
|
13
|
-
const getWorkspaces = async () => {
|
|
14
|
-
const workspaces = await fetch('/api/workspaces');
|
|
15
|
-
if (!workspaces.ok) {
|
|
16
|
-
throw new Error(`Failed to fetch workspaces: ${workspaces.status}`);
|
|
17
|
-
}
|
|
18
|
-
return workspaces.json();
|
|
19
|
-
};
|
|
20
|
-
|
|
21
12
|
export function CalendarSyncDashboard({ syncLogs }: { syncLogs: SyncLog[] }) {
|
|
22
13
|
const [filterType, setFilterType] = useState('all');
|
|
23
14
|
const [filterWorkspace, setFilterWorkspace] = useState('all');
|
|
24
15
|
const [searchTerm, setSearchTerm] = useState('');
|
|
25
16
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
queryKey: ['workspaces'],
|
|
29
|
-
queryFn: () => getWorkspaces(),
|
|
30
|
-
refetchInterval: 1000 * 60 * 5, // 5 minutes
|
|
17
|
+
const workspacesQuery = useVisibleWorkspaces(true, undefined, {
|
|
18
|
+
refetchInterval: 300_000,
|
|
31
19
|
});
|
|
32
|
-
|
|
33
|
-
// Fallback to mock workspaces if API fails
|
|
34
|
-
const workspaces: Workspace[] = workspacesQuery.data || [
|
|
35
|
-
{ id: 'ws_1', name: 'Marketing Team' },
|
|
36
|
-
{ id: 'ws_2', name: 'Engineering' },
|
|
37
|
-
{ id: 'ws_3', name: 'Sales Department' },
|
|
38
|
-
{ id: 'ws_4', name: 'Executive Team' },
|
|
39
|
-
];
|
|
20
|
+
const workspaces = workspacesQuery.data ?? [];
|
|
40
21
|
|
|
41
22
|
const filteredLogs = useMemo(() => {
|
|
42
23
|
return syncLogs.filter((log) => {
|
|
@@ -44,7 +44,7 @@ import type { SyncLog } from './types';
|
|
|
44
44
|
|
|
45
45
|
interface SyncLogsTableProps {
|
|
46
46
|
syncLogs: SyncLog[];
|
|
47
|
-
workspaces: Workspace[];
|
|
47
|
+
workspaces: Pick<Workspace, 'id' | 'name'>[];
|
|
48
48
|
filterType: string;
|
|
49
49
|
filterWorkspace: string;
|
|
50
50
|
searchTerm: string;
|
|
@@ -221,11 +221,8 @@ export function SyncLogsTable({
|
|
|
221
221
|
</SelectTrigger>
|
|
222
222
|
<SelectContent>
|
|
223
223
|
<SelectItem value="all">All Workspaces</SelectItem>
|
|
224
|
-
{workspaces.map((workspace
|
|
225
|
-
<SelectItem
|
|
226
|
-
key={`${workspace.id}-${index}`}
|
|
227
|
-
value={workspace.id}
|
|
228
|
-
>
|
|
224
|
+
{workspaces.map((workspace) => (
|
|
225
|
+
<SelectItem key={workspace.id} value={workspace.id}>
|
|
229
226
|
<div className="flex items-center gap-2">
|
|
230
227
|
<div className="h-2 w-2 rounded-full bg-blue-500" />
|
|
231
228
|
{workspace.name}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type GroupProps as GroupPropsOriginal,
|
|
9
9
|
Panel,
|
|
10
10
|
Separator,
|
|
11
|
+
usePanelRef,
|
|
11
12
|
} from 'react-resizable-panels';
|
|
12
13
|
|
|
13
14
|
interface ResizablePanelGroupProps
|
|
@@ -96,4 +97,4 @@ function ResizableHandle({
|
|
|
96
97
|
);
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
export { ResizableHandle, ResizablePanel, ResizablePanelGroup };
|
|
100
|
+
export { ResizableHandle, ResizablePanel, ResizablePanelGroup, usePanelRef };
|