@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.
@@ -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: ['workspaces', ...(cacheScope ? [cacheScope] : [])],
141
- queryFn: fetchWorkspaces,
142
- enabled: !!wsId,
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(resolvedWorkspaceId && !hasListedCurrentWorkspace),
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({ queryKey: ['workspaces'] });
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?.find(
291
- (ws) => ws?.personal === true && ws.access_type !== 'guest'
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
- : (workspaces.find((ws) => ws.id === resolvedWorkspaceId) ??
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({ queryKey: ['workspaces'] });
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
- <Popover modal={popoverModal} open={open} onOpenChange={setOpen}>
534
- <PopoverTrigger asChild disabled={!hasSelectableWorkspaces}>
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
- </PopoverTrigger>
602
- <PopoverContent className="w-full max-w-[16rem] p-0">
603
- <Command>
604
- <CommandInput autoFocus placeholder="Search workspace..." />
605
- <CommandEmpty>No workspace found.</CommandEmpty>
606
- <CommandList className="max-h-64">
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
- {groups.map((group) => (
612
- <CommandGroup key={group.label} heading={group.label}>
613
- {group.teams.map(
614
- (team: {
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
- // Get workspaces from API
27
- const workspacesQuery = useQuery({
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, index) => (
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 };