@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.
- package/CHANGELOG.md +40 -0
- package/package.json +10 -7
- package/src/components/ui/custom/__tests__/workspace-picker-content.test.tsx +267 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +24 -38
- package/src/components/ui/custom/education-todo.test.tsx +131 -0
- package/src/components/ui/custom/education-todo.tsx +191 -0
- 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/satellite-notification-polling.test.tsx +61 -0
- package/src/components/ui/custom/satellite-notification-popover.tsx +7 -3
- 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 +356 -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 +95 -359
- package/src/components/ui/legacy/calendar/calendar-draft-recovery.ts +34 -0
- package/src/components/ui/legacy/calendar/event-modal.tsx +77 -137
- package/src/components/ui/legacy/calendar/event-save-payload.test.ts +40 -0
- package/src/components/ui/legacy/calendar/event-save-payload.ts +79 -0
- package/src/components/ui/legacy/calendar/event-source-selection.test.ts +95 -0
- package/src/components/ui/legacy/calendar/event-source-selection.ts +78 -0
- package/src/components/ui/legacy/calendar/provider-color-picker.test.tsx +147 -0
- package/src/components/ui/legacy/calendar/provider-color-picker.tsx +115 -0
- package/src/components/ui/legacy/calendar/save-calendar-event-drafts.test.ts +35 -1
- package/src/components/ui/legacy/calendar/save-calendar-event-drafts.ts +23 -11
- 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/legacy/calendar/use-calendar-draft-save.test.tsx +119 -0
- package/src/components/ui/legacy/calendar/use-calendar-draft-save.tsx +141 -0
- package/src/components/ui/popover.tsx +3 -1
- package/src/components/ui/resizable.tsx +2 -1
- package/src/components/ui/text-editor/__tests__/collaboration-carets.test.ts +142 -0
- package/src/components/ui/text-editor/__tests__/markdown-document.test.ts +50 -0
- package/src/components/ui/text-editor/clipboard-serialization.ts +10 -0
- package/src/components/ui/text-editor/collaboration-carets.ts +56 -0
- package/src/components/ui/text-editor/editor-readonly-transition.test.tsx +2 -0
- package/src/components/ui/text-editor/editor.tsx +8 -3
- package/src/components/ui/text-editor/extensions.ts +8 -2
- package/src/components/ui/text-editor/markdown-paste-extension.ts +1 -1
- package/src/components/ui/text-editor/markdown.ts +11 -0
- package/src/components/ui/text-editor/tool-bar.tsx +43 -44
- package/src/hooks/__tests__/collaboration-awareness.test.ts +106 -0
- package/src/hooks/__tests__/supabase-provider.test.ts +9 -0
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +12 -2
- package/src/hooks/__tests__/use-notifications-subscription.test.tsx +31 -1
- package/src/hooks/__tests__/use-visible-workspaces.test.tsx +76 -0
- package/src/hooks/__tests__/use-workspace-presence.test.tsx +3 -3
- package/src/hooks/__tests__/use-workspace-visibility.test.tsx +155 -0
- package/src/hooks/__tests__/use-yjs-collaboration.test.tsx +108 -0
- package/src/hooks/__tests__/workspace-actor-lifetime.test.tsx +103 -0
- package/src/hooks/calendar-creation-request.test.ts +62 -0
- package/src/hooks/calendar-creation-request.ts +34 -0
- package/src/hooks/calendar-event-write-payload.ts +24 -0
- package/src/hooks/notification-subscription.ts +5 -2
- package/src/hooks/supabase-provider.ts +38 -72
- package/src/hooks/use-calendar.tsx +21 -45
- package/src/hooks/use-exchange-rates.ts +4 -15
- package/src/hooks/use-visible-workspaces.ts +57 -0
- package/src/hooks/use-workspace-presence.ts +9 -9
- package/src/hooks/use-workspace-user.ts +15 -3
- package/src/hooks/use-workspace-visibility.ts +234 -0
- 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,
|
|
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: ['
|
|
141
|
-
queryFn:
|
|
142
|
-
|
|
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(
|
|
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({
|
|
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
|
|
291
|
-
(ws) => ws
|
|
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
|
-
:
|
|
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({
|
|
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
|
-
<
|
|
534
|
-
<
|
|
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
|
-
</
|
|
602
|
-
<
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
|
|
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>
|
|
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
|
+
}
|