@tuturuuu/ui 0.37.1 → 0.38.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 (64) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/package.json +10 -7
  3. package/src/components/ui/custom/__tests__/workspace-picker-content.test.tsx +267 -0
  4. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +24 -38
  5. package/src/components/ui/custom/education-todo.test.tsx +131 -0
  6. package/src/components/ui/custom/education-todo.tsx +191 -0
  7. package/src/components/ui/custom/hidden-workspaces-settings.tsx +49 -0
  8. package/src/components/ui/custom/mobile-header-height.ts +16 -0
  9. package/src/components/ui/custom/satellite-notification-polling.test.tsx +61 -0
  10. package/src/components/ui/custom/satellite-notification-popover.tsx +7 -3
  11. package/src/components/ui/custom/structure.tsx +37 -5
  12. package/src/components/ui/custom/user-filters.tsx +2 -1
  13. package/src/components/ui/custom/visible-workspace-filter.tsx +41 -0
  14. package/src/components/ui/custom/workspace-picker-content.tsx +356 -0
  15. package/src/components/ui/custom/workspace-select-helpers.ts +11 -0
  16. package/src/components/ui/custom/workspace-select-invitations.tsx +3 -0
  17. package/src/components/ui/custom/workspace-select.tsx +95 -359
  18. package/src/components/ui/legacy/calendar/calendar-draft-recovery.ts +34 -0
  19. package/src/components/ui/legacy/calendar/event-modal.tsx +77 -137
  20. package/src/components/ui/legacy/calendar/event-save-payload.test.ts +40 -0
  21. package/src/components/ui/legacy/calendar/event-save-payload.ts +79 -0
  22. package/src/components/ui/legacy/calendar/event-source-selection.test.ts +95 -0
  23. package/src/components/ui/legacy/calendar/event-source-selection.ts +78 -0
  24. package/src/components/ui/legacy/calendar/provider-color-picker.test.tsx +147 -0
  25. package/src/components/ui/legacy/calendar/provider-color-picker.tsx +115 -0
  26. package/src/components/ui/legacy/calendar/save-calendar-event-drafts.test.ts +35 -1
  27. package/src/components/ui/legacy/calendar/save-calendar-event-drafts.ts +23 -11
  28. package/src/components/ui/legacy/calendar/settings/calendar-sync-dashboard.tsx +4 -23
  29. package/src/components/ui/legacy/calendar/settings/sync-logs-table.tsx +3 -6
  30. package/src/components/ui/legacy/calendar/use-calendar-draft-save.test.tsx +119 -0
  31. package/src/components/ui/legacy/calendar/use-calendar-draft-save.tsx +141 -0
  32. package/src/components/ui/popover.tsx +3 -1
  33. package/src/components/ui/resizable.tsx +2 -1
  34. package/src/components/ui/text-editor/__tests__/collaboration-carets.test.ts +142 -0
  35. package/src/components/ui/text-editor/__tests__/markdown-document.test.ts +50 -0
  36. package/src/components/ui/text-editor/clipboard-serialization.ts +10 -0
  37. package/src/components/ui/text-editor/collaboration-carets.ts +56 -0
  38. package/src/components/ui/text-editor/editor-readonly-transition.test.tsx +2 -0
  39. package/src/components/ui/text-editor/editor.tsx +8 -3
  40. package/src/components/ui/text-editor/extensions.ts +8 -2
  41. package/src/components/ui/text-editor/markdown-paste-extension.ts +1 -1
  42. package/src/components/ui/text-editor/markdown.ts +11 -0
  43. package/src/components/ui/text-editor/tool-bar.tsx +43 -44
  44. package/src/hooks/__tests__/collaboration-awareness.test.ts +106 -0
  45. package/src/hooks/__tests__/supabase-provider.test.ts +9 -0
  46. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +12 -2
  47. package/src/hooks/__tests__/use-notifications-subscription.test.tsx +31 -1
  48. package/src/hooks/__tests__/use-visible-workspaces.test.tsx +76 -0
  49. package/src/hooks/__tests__/use-workspace-presence.test.tsx +3 -3
  50. package/src/hooks/__tests__/use-workspace-visibility.test.tsx +155 -0
  51. package/src/hooks/__tests__/use-yjs-collaboration.test.tsx +108 -0
  52. package/src/hooks/__tests__/workspace-actor-lifetime.test.tsx +103 -0
  53. package/src/hooks/calendar-creation-request.test.ts +62 -0
  54. package/src/hooks/calendar-creation-request.ts +34 -0
  55. package/src/hooks/calendar-event-write-payload.ts +24 -0
  56. package/src/hooks/notification-subscription.ts +5 -2
  57. package/src/hooks/supabase-provider.ts +38 -72
  58. package/src/hooks/use-calendar.tsx +21 -45
  59. package/src/hooks/use-exchange-rates.ts +4 -15
  60. package/src/hooks/use-visible-workspaces.ts +57 -0
  61. package/src/hooks/use-workspace-presence.ts +9 -9
  62. package/src/hooks/use-workspace-user.ts +15 -3
  63. package/src/hooks/use-workspace-visibility.ts +234 -0
  64. package/src/hooks/use-yjs-collaboration.ts +45 -27
@@ -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,14 @@ import {
64
54
  FormMessage,
65
55
  } from '../form';
66
56
  import { Input } from '../input';
67
- import { Popover, PopoverContent, PopoverTrigger } from '../popover';
57
+ import { Popover, PopoverTrigger } from '../popover';
68
58
  import { TUTURUUU_LOGO_URL } from './tuturuuu-logo';
59
+ import { WorkspacePickerContent } from './workspace-picker-content';
69
60
  import {
70
61
  buildWorkspaceSetupHandoffUrl,
71
62
  mergeWorkspaceSelectWorkspaces,
72
63
  normalizeWorkspaceSwitchPath,
64
+ resolveGuestWorkspaceLanding,
73
65
  resolveWorkspaceAvatarUrl,
74
66
  } from './workspace-select-helpers';
75
67
  import { WorkspaceIcon } from './workspace-select-icon';
@@ -100,9 +92,9 @@ export function WorkspaceSelect({
100
92
  fallbackLogoUrl = TUTURUUU_LOGO_URL,
101
93
  resolveNextPathname,
102
94
  triggerClassName,
103
- popoverModal = false,
104
95
  platformWorkspaceSetupUrl,
105
96
  cacheScope,
97
+ presentation = 'dropdown',
106
98
  }: {
107
99
  wsId: string;
108
100
  hideLeading?: boolean;
@@ -119,27 +111,37 @@ export function WorkspaceSelect({
119
111
  nextSlug: string;
120
112
  }) => string;
121
113
  triggerClassName?: string;
122
- /** Keep the picker interactive and scrollable when rendered inside a modal. */
123
- popoverModal?: boolean;
124
114
  /** Platform origin used to prepare a newly created satellite workspace. */
125
115
  platformWorkspaceSetupUrl?: string;
126
116
  /** Authenticated identity used to isolate user-specific picker caches. */
127
117
  cacheScope?: string;
118
+ presentation?: 'fullscreen' | 'dropdown';
128
119
  }) {
120
+ const Picker = presentation === 'dropdown' ? Popover : Dialog;
121
+ const PickerTrigger =
122
+ presentation === 'dropdown' ? PopoverTrigger : DialogTrigger;
129
123
  const t = useTranslations();
130
124
  const locale = useLocale();
131
125
  const router = useRouter();
132
126
  const pathname = usePathname();
133
127
  const queryClient = useQueryClient();
134
128
 
129
+ const actorScope = useWorkspaceActor();
130
+ const visibility = useWorkspaceVisibility();
131
+ const actorId = actorScope?.actorId ?? cacheScope;
135
132
  const resolvedWorkspaceId =
136
133
  wsId && wsId !== PERSONAL_WORKSPACE_SLUG
137
134
  ? resolveWorkspaceId(wsId)
138
135
  : undefined;
139
136
  const { data: listedWorkspaces } = useQuery({
140
- queryKey: ['workspaces', ...(cacheScope ? [cacheScope] : [])],
141
- queryFn: fetchWorkspaces,
142
- enabled: !!wsId,
137
+ queryKey: ['workspace-ui-list', actorId],
138
+ queryFn: async () => {
139
+ actorScope?.assertActive();
140
+ const result = await fetchWorkspaces();
141
+ actorScope?.assertActive();
142
+ return result;
143
+ },
144
+ enabled: Boolean(wsId && actorId),
143
145
  });
144
146
  const hasListedCurrentWorkspace = Boolean(
145
147
  resolvedWorkspaceId &&
@@ -150,19 +152,21 @@ export function WorkspaceSelect({
150
152
  const { data: currentWorkspaceFallback } = useQuery({
151
153
  queryKey: [
152
154
  'workspace-select-current-workspace',
155
+ actorId,
153
156
  resolvedWorkspaceId,
154
- ...(cacheScope ? [cacheScope] : []),
155
157
  ],
156
158
  queryFn: async () =>
157
159
  (await getWorkspace(resolvedWorkspaceId!)) as InternalApiWorkspaceSummary,
158
- enabled: Boolean(resolvedWorkspaceId && !hasListedCurrentWorkspace),
160
+ enabled: Boolean(
161
+ actorId && resolvedWorkspaceId && !hasListedCurrentWorkspace
162
+ ),
159
163
  retry: 1,
160
164
  });
161
165
  const workspaces = mergeWorkspaceSelectWorkspaces(
162
166
  listedWorkspaces,
163
167
  currentWorkspaceFallback
164
168
  );
165
- const { data: currentUser } = useWorkspaceUser();
169
+ const { data: currentUser } = useWorkspaceUser(actorId);
166
170
  const defaultWorkspaceId = currentUser?.default_workspace_id || null;
167
171
 
168
172
  const form = useForm({
@@ -185,8 +189,8 @@ export function WorkspaceSelect({
185
189
  const [loading, setLoading] = useState(false);
186
190
  const [joiningByHandle, setJoiningByHandle] = useState(false);
187
191
  const invitationController = useWorkspaceInvitations({
188
- cacheScope,
189
- enabled: Boolean(wsId),
192
+ cacheScope: actorId,
193
+ enabled: Boolean(wsId && actorId),
190
194
  onAccepted: (invitation) => {
191
195
  setOpen(false);
192
196
  const slug = invitation.workspace.handle || invitation.workspace.id;
@@ -202,7 +206,7 @@ export function WorkspaceSelect({
202
206
  updateCurrentUserDefaultWorkspace(workspaceId),
203
207
  onSuccess: (_, workspaceId) => {
204
208
  queryClient.setQueryData(
205
- ['workspace-user'],
209
+ ['workspace-user', ...(actorId ? [actorId] : [])],
206
210
  (previous: WorkspaceUser | undefined) =>
207
211
  previous
208
212
  ? {
@@ -216,7 +220,9 @@ export function WorkspaceSelect({
216
220
  void queryClient.invalidateQueries({ queryKey: ['default-workspace'] });
217
221
  void queryClient.invalidateQueries({ queryKey: ['user'] });
218
222
  void queryClient.invalidateQueries({ queryKey: ['user-workspaces'] });
219
- void queryClient.invalidateQueries({ queryKey: ['workspaces'] });
223
+ void queryClient.invalidateQueries({
224
+ queryKey: ['workspace-ui-list', actorId],
225
+ });
220
226
  router.refresh();
221
227
  },
222
228
  onError: (error) => {
@@ -287,146 +293,31 @@ export function WorkspaceSelect({
287
293
  }
288
294
  }
289
295
 
290
- const personalWorkspace = workspaces?.find(
291
- (ws) => ws?.personal === true && ws.access_type !== 'guest'
296
+ const personalWorkspace = workspaces.find(
297
+ (ws) => ws.personal && ws.access_type !== 'guest'
292
298
  );
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
299
  const hasSelectableWorkspaces =
422
300
  workspaces.length > 0 || invitations.length > 0;
423
301
  useOpenWorkspaceSelectWhenRevealed(hasSelectableWorkspaces, setOpen);
424
-
302
+ const onValueChange = (selected: InternalApiWorkspaceSummary) => {
303
+ const nextSlug = toWorkspaceSlug(selected.id, {
304
+ personal: selected.personal,
305
+ });
306
+ const guestLanding = resolveGuestWorkspaceLanding(selected);
307
+ let nextPath = guestLanding
308
+ ? `/${nextSlug}${guestLanding}`
309
+ : pathname
310
+ ? (resolveNextPathname?.({ currentPathname: pathname, nextSlug }) ??
311
+ pathname.replace(/^\/[^/]+/, `/${nextSlug}`))
312
+ : getWorkspaceLandingPath(nextSlug);
313
+ if (!guestLanding)
314
+ nextPath = normalizeWorkspaceSwitchPath(nextPath, nextSlug);
315
+ router.push(nextPath);
316
+ };
425
317
  const workspace =
426
318
  wsId === PERSONAL_WORKSPACE_SLUG
427
319
  ? personalWorkspace
428
- : (workspaces.find((ws) => ws.id === resolvedWorkspaceId) ??
429
- guestWorkspaces.find((ws) => ws.id === resolvedWorkspaceId));
320
+ : workspaces.find((ws) => ws.id === resolvedWorkspaceId);
430
321
  if (!wsId) return <div />;
431
322
 
432
323
  async function onJoinByHandleSubmit(
@@ -442,7 +333,9 @@ export function WorkspaceSelect({
442
333
  setShowJoinWorkspaceDialog(false);
443
334
  setOpen(false);
444
335
 
445
- void queryClient.invalidateQueries({ queryKey: ['workspaces'] });
336
+ void queryClient.invalidateQueries({
337
+ queryKey: ['workspace-ui-list', actorId],
338
+ });
446
339
  void queryClient.invalidateQueries({ queryKey: ['user-workspaces'] });
447
340
  void queryClient.invalidateQueries({ queryKey: ['workspace-user'] });
448
341
 
@@ -530,8 +423,8 @@ export function WorkspaceSelect({
530
423
  setShowNewWorkspaceDialog(open);
531
424
  }}
532
425
  >
533
- <Popover modal={popoverModal} open={open} onOpenChange={setOpen}>
534
- <PopoverTrigger asChild disabled={!hasSelectableWorkspaces}>
426
+ <Picker open={open} onOpenChange={setOpen}>
427
+ <PickerTrigger asChild disabled={!hasSelectableWorkspaces}>
535
428
  <Button
536
429
  size="xs"
537
430
  variant="outline"
@@ -598,202 +491,41 @@ export function WorkspaceSelect({
598
491
  <ChevronDown className="ml-1 h-4 w-4 shrink-0 opacity-50" />
599
492
  )}
600
493
  </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">
494
+ </PickerTrigger>
495
+ <WorkspacePickerContent
496
+ presentation={presentation}
497
+ onClose={() => setOpen(false)}
498
+ workspaces={workspaces ?? []}
499
+ currentId={workspace?.id}
500
+ defaultId={defaultWorkspaceId}
501
+ visibility={visibility}
502
+ onSelect={(selected) => {
503
+ onValueChange(selected);
504
+ setOpen(false);
505
+ }}
506
+ onDefault={(id) => updateDefaultWorkspaceMutation.mutate(id)}
507
+ onCreate={
508
+ disableCreateNewWorkspace
509
+ ? undefined
510
+ : () => {
511
+ setOpen(false);
512
+ setShowNewWorkspaceDialog(true);
513
+ }
514
+ }
515
+ onJoin={() => {
516
+ setOpen(false);
517
+ setShowJoinWorkspaceDialog(true);
518
+ }}
519
+ invitations={
520
+ <Command>
607
521
  <WorkspaceInvitationItems
608
522
  controller={invitationController}
609
523
  fallbackLogoUrl={fallbackLogoUrl}
610
524
  />
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>
525
+ </Command>
526
+ }
527
+ />
528
+ </Picker>
797
529
  <DialogContent>
798
530
  <DialogHeader>
799
531
  <DialogTitle>{t('common.create_workspace')}</DialogTitle>
@@ -848,3 +580,7 @@ export function WorkspaceSelect({
848
580
  </>
849
581
  );
850
582
  }
583
+
584
+ export { HiddenWorkspacesSettings } from './hidden-workspaces-settings';
585
+
586
+ export { VisibleWorkspaceFilter } from './visible-workspace-filter';
@@ -0,0 +1,34 @@
1
+ import type { CalendarEvent } from '@tuturuuu/types/primitives/calendar-event';
2
+
3
+ export function createCalendarDraftRecovery() {
4
+ let submitted:
5
+ | {
6
+ payload: Partial<CalendarEvent>;
7
+ requestId: string;
8
+ recoverable: boolean;
9
+ }
10
+ | undefined;
11
+ let recovered: CalendarEvent | undefined;
12
+ return {
13
+ original: () => recovered,
14
+ submitted: () => submitted,
15
+ capture(
16
+ payload: Partial<CalendarEvent>,
17
+ requestId: string,
18
+ recoverable: boolean
19
+ ) {
20
+ // Snapshot once. An uncertain request never changes its ID or contents.
21
+ submitted ??= {
22
+ payload: structuredClone(payload),
23
+ requestId,
24
+ recoverable,
25
+ };
26
+ return submitted;
27
+ },
28
+ complete(event: CalendarEvent) {
29
+ if (!event.id || event.id === 'new')
30
+ throw new Error('Unconfirmed creation');
31
+ recovered = event;
32
+ },
33
+ };
34
+ }