@tuturuuu/ui 0.26.1 → 0.28.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 +37 -0
- package/biome.json +1 -1
- package/package.json +49 -48
- package/src/components/ui/button.tsx +12 -8
- package/src/components/ui/color-picker.test.tsx +23 -0
- package/src/components/ui/color-picker.tsx +1 -1
- package/src/components/ui/custom/combobox.tsx +4 -0
- package/src/components/ui/custom/notification-popover-client.tsx +6 -19
- package/src/components/ui/custom/notification-popover-trigger.test.tsx +37 -0
- package/src/components/ui/custom/notification-popover-trigger.tsx +44 -0
- package/src/components/ui/custom/workspace-access/adapters.test.ts +89 -1
- package/src/components/ui/custom/workspace-access/adapters.ts +67 -2
- package/src/components/ui/custom/workspace-access/types.ts +13 -0
- package/src/components/ui/custom/workspace-access/workspace-access-invitation-role-menu.test.tsx +79 -0
- package/src/components/ui/custom/workspace-access/workspace-access-invitation-role-menu.tsx +125 -0
- package/src/components/ui/custom/workspace-access/workspace-access-invite-access-picker.tsx +108 -0
- package/src/components/ui/custom/workspace-access/workspace-access-invite-dialog.test.tsx +134 -0
- package/src/components/ui/custom/workspace-access/workspace-access-invite-dialog.tsx +94 -113
- package/src/components/ui/custom/workspace-access/workspace-access-invite-pos-panel.tsx +75 -0
- package/src/components/ui/custom/workspace-access/workspace-access-invite-role-picker.tsx +151 -0
- package/src/components/ui/custom/workspace-access/workspace-access-labels.ts +1 -1
- package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +27 -2
- package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +10 -0
- package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +102 -3
- package/src/components/ui/custom/workspace-access/workspace-access-role-options.test.ts +35 -0
- package/src/components/ui/custom/workspace-access/workspace-access-role-options.ts +21 -0
- package/src/components/ui/custom/workspace-select-invitations.tsx +2 -1
- package/src/components/ui/text-editor/__tests__/editor-classes.test.ts +36 -0
- package/src/components/ui/text-editor/__tests__/inline-task-conversion.test.tsx +30 -0
- package/src/components/ui/text-editor/__tests__/keyboard.test.ts +173 -3
- package/src/components/ui/text-editor/__tests__/toggle-block-extension.test.ts +251 -0
- package/src/components/ui/text-editor/editor-classes.ts +100 -0
- package/src/components/ui/text-editor/editor.tsx +11 -153
- package/src/components/ui/text-editor/extensions.ts +19 -0
- package/src/components/ui/text-editor/keyboard.ts +47 -0
- package/src/components/ui/text-editor/toggle-block-extension.ts +153 -0
- package/src/components/ui/text-editor/tool-bar.tsx +24 -73
- package/src/components/ui/text-editor/toolbar-config.ts +67 -0
- package/src/hooks/__tests__/use-notifications-subscription.test.tsx +34 -1
- package/src/hooks/use-board-actions.test.ts +23 -0
- package/src/hooks/use-board-actions.ts +48 -35
- package/src/hooks/use-calendar-sync.tsx +12 -12
- package/src/hooks/use-notifications.ts +4 -8
|
@@ -2,6 +2,20 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
|
2
2
|
import { toast } from '@tuturuuu/ui/sonner';
|
|
3
3
|
import { getTaskApiUrl } from '../lib/tasks-app-url';
|
|
4
4
|
|
|
5
|
+
export async function getBoardActionError(
|
|
6
|
+
response: Response,
|
|
7
|
+
fallback: string
|
|
8
|
+
) {
|
|
9
|
+
const errorData = (await response.json().catch(() => null)) as {
|
|
10
|
+
error?: unknown;
|
|
11
|
+
message?: unknown;
|
|
12
|
+
} | null;
|
|
13
|
+
|
|
14
|
+
if (typeof errorData?.error === 'string') return errorData.error;
|
|
15
|
+
if (typeof errorData?.message === 'string') return errorData.message;
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
|
|
5
19
|
async function boardAction(
|
|
6
20
|
wsId: string,
|
|
7
21
|
boardId: string,
|
|
@@ -21,8 +35,7 @@ async function boardAction(
|
|
|
21
35
|
);
|
|
22
36
|
|
|
23
37
|
if (!response.ok) {
|
|
24
|
-
|
|
25
|
-
throw new Error(errorData.message || 'Board action failed');
|
|
38
|
+
throw new Error(await getBoardActionError(response, 'Board action failed'));
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
return response.json();
|
|
@@ -45,8 +58,9 @@ async function archiveAction(
|
|
|
45
58
|
);
|
|
46
59
|
|
|
47
60
|
if (!response.ok) {
|
|
48
|
-
|
|
49
|
-
|
|
61
|
+
throw new Error(
|
|
62
|
+
await getBoardActionError(response, 'Archive action failed')
|
|
63
|
+
);
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
return response.json();
|
|
@@ -59,19 +73,28 @@ interface BoardActionOptions {
|
|
|
59
73
|
export function useBoardActions(wsId: string) {
|
|
60
74
|
const queryClient = useQueryClient();
|
|
61
75
|
|
|
76
|
+
const invalidateBoardQueries = (boardId: string) => {
|
|
77
|
+
void queryClient.invalidateQueries({ queryKey: ['boards', wsId] });
|
|
78
|
+
void queryClient.invalidateQueries({
|
|
79
|
+
queryKey: ['accessible-task-boards'],
|
|
80
|
+
});
|
|
81
|
+
void queryClient.invalidateQueries({
|
|
82
|
+
queryKey: ['task-board', wsId, boardId],
|
|
83
|
+
});
|
|
84
|
+
void queryClient.invalidateQueries({
|
|
85
|
+
queryKey: ['task-board-settings', wsId, boardId],
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
|
|
62
89
|
const softDeleteMutation = useMutation<
|
|
63
90
|
any,
|
|
64
91
|
Error,
|
|
65
92
|
{ boardId: string; options?: BoardActionOptions }
|
|
66
93
|
>({
|
|
67
94
|
mutationFn: ({ boardId }) => boardAction(wsId, boardId, 'PUT'),
|
|
68
|
-
onSuccess: (_, { options }) => {
|
|
95
|
+
onSuccess: (_, { boardId, options }) => {
|
|
69
96
|
toast.success('Board moved to trash successfully');
|
|
70
|
-
|
|
71
|
-
// Using exact: false (default) to match all queries with this prefix
|
|
72
|
-
queryClient.invalidateQueries({
|
|
73
|
-
queryKey: ['boards', wsId],
|
|
74
|
-
});
|
|
97
|
+
invalidateBoardQueries(boardId);
|
|
75
98
|
options?.onSuccess?.();
|
|
76
99
|
},
|
|
77
100
|
onError: (error: Error) => {
|
|
@@ -87,12 +110,9 @@ export function useBoardActions(wsId: string) {
|
|
|
87
110
|
{ boardId: string; options?: BoardActionOptions }
|
|
88
111
|
>({
|
|
89
112
|
mutationFn: ({ boardId }) => boardAction(wsId, boardId, 'DELETE'),
|
|
90
|
-
onSuccess: (_, { options }) => {
|
|
113
|
+
onSuccess: (_, { boardId, options }) => {
|
|
91
114
|
toast.success('Board permanently deleted successfully');
|
|
92
|
-
|
|
93
|
-
queryClient.invalidateQueries({
|
|
94
|
-
queryKey: ['boards', wsId],
|
|
95
|
-
});
|
|
115
|
+
invalidateBoardQueries(boardId);
|
|
96
116
|
options?.onSuccess?.();
|
|
97
117
|
},
|
|
98
118
|
onError: (error: Error) => {
|
|
@@ -109,12 +129,9 @@ export function useBoardActions(wsId: string) {
|
|
|
109
129
|
>({
|
|
110
130
|
mutationFn: ({ boardId }) =>
|
|
111
131
|
boardAction(wsId, boardId, 'PATCH', { restore: true }),
|
|
112
|
-
onSuccess: (_, { options }) => {
|
|
132
|
+
onSuccess: (_, { boardId, options }) => {
|
|
113
133
|
toast.success('Board restored successfully');
|
|
114
|
-
|
|
115
|
-
queryClient.invalidateQueries({
|
|
116
|
-
queryKey: ['boards', wsId],
|
|
117
|
-
});
|
|
134
|
+
invalidateBoardQueries(boardId);
|
|
118
135
|
options?.onSuccess?.();
|
|
119
136
|
},
|
|
120
137
|
onError: (error: Error) => {
|
|
@@ -130,12 +147,9 @@ export function useBoardActions(wsId: string) {
|
|
|
130
147
|
{ boardId: string; options?: BoardActionOptions }
|
|
131
148
|
>({
|
|
132
149
|
mutationFn: ({ boardId }) => archiveAction(wsId, boardId, 'POST'),
|
|
133
|
-
onSuccess: (_, { options }) => {
|
|
150
|
+
onSuccess: (_, { boardId, options }) => {
|
|
134
151
|
toast.success('Board archived successfully');
|
|
135
|
-
|
|
136
|
-
queryClient.invalidateQueries({
|
|
137
|
-
queryKey: ['boards', wsId],
|
|
138
|
-
});
|
|
152
|
+
invalidateBoardQueries(boardId);
|
|
139
153
|
options?.onSuccess?.();
|
|
140
154
|
},
|
|
141
155
|
onError: (error: Error) => {
|
|
@@ -151,12 +165,9 @@ export function useBoardActions(wsId: string) {
|
|
|
151
165
|
{ boardId: string; options?: BoardActionOptions }
|
|
152
166
|
>({
|
|
153
167
|
mutationFn: ({ boardId }) => archiveAction(wsId, boardId, 'DELETE'),
|
|
154
|
-
onSuccess: (_, { options }) => {
|
|
168
|
+
onSuccess: (_, { boardId, options }) => {
|
|
155
169
|
toast.success('Board unarchived successfully');
|
|
156
|
-
|
|
157
|
-
queryClient.invalidateQueries({
|
|
158
|
-
queryKey: ['boards', wsId],
|
|
159
|
-
});
|
|
170
|
+
invalidateBoardQueries(boardId);
|
|
160
171
|
options?.onSuccess?.();
|
|
161
172
|
},
|
|
162
173
|
onError: (error: Error) => {
|
|
@@ -191,12 +202,9 @@ export function useBoardActions(wsId: string) {
|
|
|
191
202
|
}
|
|
192
203
|
return res.json();
|
|
193
204
|
}),
|
|
194
|
-
onSuccess: (_, { options }) => {
|
|
205
|
+
onSuccess: (_, { boardId, options }) => {
|
|
195
206
|
toast.success('Board duplicated successfully');
|
|
196
|
-
|
|
197
|
-
queryClient.invalidateQueries({
|
|
198
|
-
queryKey: ['boards', wsId],
|
|
199
|
-
});
|
|
207
|
+
invalidateBoardQueries(boardId);
|
|
200
208
|
options?.onSuccess?.();
|
|
201
209
|
},
|
|
202
210
|
onError: (error: Error) => {
|
|
@@ -219,5 +227,10 @@ export function useBoardActions(wsId: string) {
|
|
|
219
227
|
unarchiveMutation.mutate({ boardId, options }),
|
|
220
228
|
duplicateBoard: (boardId: string, options?: BoardActionOptions) =>
|
|
221
229
|
duplicateMutation.mutate({ boardId, options }),
|
|
230
|
+
isArchiving: archiveMutation.isPending,
|
|
231
|
+
isDeleting: softDeleteMutation.isPending,
|
|
232
|
+
isPermanentlyDeleting: permanentDeleteMutation.isPending,
|
|
233
|
+
isRestoring: restoreMutation.isPending,
|
|
234
|
+
isUnarchiving: unarchiveMutation.isPending,
|
|
222
235
|
};
|
|
223
236
|
}
|
|
@@ -240,7 +240,7 @@ export const CalendarSyncProvider = ({
|
|
|
240
240
|
queryKey: ['workspace-calendars', wsId],
|
|
241
241
|
enabled: !hasExternalEvents && !!wsId,
|
|
242
242
|
queryFn: () => listWorkspaceCalendars(wsId),
|
|
243
|
-
staleTime:
|
|
243
|
+
staleTime: 5 * 60_000,
|
|
244
244
|
});
|
|
245
245
|
const enabledWorkspaceCalendarIds = useMemo(
|
|
246
246
|
() =>
|
|
@@ -438,12 +438,11 @@ export const CalendarSyncProvider = ({
|
|
|
438
438
|
[isVisibleInCurrentRange]
|
|
439
439
|
);
|
|
440
440
|
|
|
441
|
-
// Fetch database events with caching
|
|
442
441
|
const { data: fetchedData, isLoading: isDatabaseLoading } = useQuery({
|
|
443
442
|
queryKey: ['databaseCalendarEvents', wsId, activeCacheKey],
|
|
444
443
|
enabled: !hasExternalEvents && !!wsId && dates.length > 0,
|
|
445
|
-
staleTime:
|
|
446
|
-
gcTime:
|
|
444
|
+
staleTime: 2 * 60_000,
|
|
445
|
+
gcTime: 30 * 60_000,
|
|
447
446
|
queryFn: async () => {
|
|
448
447
|
if (!activeCacheKey) return null;
|
|
449
448
|
|
|
@@ -512,10 +511,11 @@ export const CalendarSyncProvider = ({
|
|
|
512
511
|
lastSyncTime: new Date(),
|
|
513
512
|
});
|
|
514
513
|
|
|
515
|
-
|
|
514
|
+
throw err instanceof Error ? err : new Error(errorMessage);
|
|
516
515
|
}
|
|
517
516
|
},
|
|
518
|
-
refetchInterval:
|
|
517
|
+
refetchInterval: 5 * 60_000,
|
|
518
|
+
refetchIntervalInBackground: false,
|
|
519
519
|
});
|
|
520
520
|
|
|
521
521
|
// Legacy direct Google fetch/reconcile is disabled. Provider inbound sync is
|
|
@@ -523,18 +523,17 @@ export const CalendarSyncProvider = ({
|
|
|
523
523
|
const { isLoading: isGoogleLoading } = useQuery({
|
|
524
524
|
queryKey: ['googleCalendarEvents', wsId, activeCacheKey],
|
|
525
525
|
enabled: false,
|
|
526
|
-
staleTime:
|
|
527
|
-
gcTime:
|
|
526
|
+
staleTime: 2 * 60_000,
|
|
527
|
+
gcTime: 30 * 60_000,
|
|
528
528
|
queryFn: async () => null,
|
|
529
|
-
refetchInterval: 60000, // Reduced from 30s to 60s to lower load
|
|
530
529
|
});
|
|
531
530
|
|
|
532
531
|
// Fetch habit calendar events to identify which events are habits
|
|
533
532
|
const { data: habitEventData } = useQuery({
|
|
534
533
|
queryKey: ['habitCalendarEvents', wsId, activeCacheKey],
|
|
535
534
|
enabled: !hasExternalEvents && !!wsId && dates.length > 0,
|
|
536
|
-
staleTime:
|
|
537
|
-
gcTime:
|
|
535
|
+
staleTime: 2 * 60_000,
|
|
536
|
+
gcTime: 30 * 60_000,
|
|
538
537
|
queryFn: async () => {
|
|
539
538
|
const startDate = dayjs(dates[0]).startOf('day');
|
|
540
539
|
const endDate = dayjs(dates[dates.length - 1])
|
|
@@ -571,7 +570,8 @@ export const CalendarSyncProvider = ({
|
|
|
571
570
|
};
|
|
572
571
|
}
|
|
573
572
|
},
|
|
574
|
-
refetchInterval:
|
|
573
|
+
refetchInterval: 5 * 60_000,
|
|
574
|
+
refetchIntervalInBackground: false,
|
|
575
575
|
});
|
|
576
576
|
|
|
577
577
|
// Helper to check if dates have actually changed
|
|
@@ -91,7 +91,8 @@ interface NotificationSubscriptionEntry {
|
|
|
91
91
|
supabase: SupabaseClient;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
export const
|
|
94
|
+
export const UNREAD_COUNT_STALE_TIME_MS = 5 * 60 * 1000;
|
|
95
|
+
export const UNREAD_COUNT_FALLBACK_INTERVAL_MS = 15 * 60 * 1000;
|
|
95
96
|
|
|
96
97
|
const notificationSubscriptionRegistry = new Map<
|
|
97
98
|
string,
|
|
@@ -164,12 +165,7 @@ function createNotificationSubscriptionEntry(
|
|
|
164
165
|
table: 'notifications',
|
|
165
166
|
filter: `user_id=eq.${userId}`,
|
|
166
167
|
},
|
|
167
|
-
|
|
168
|
-
const newRecord = payload.new as Notification;
|
|
169
|
-
if (newRecord?.data?.action_taken) {
|
|
170
|
-
invalidateQueries();
|
|
171
|
-
}
|
|
172
|
-
}
|
|
168
|
+
invalidateQueries
|
|
173
169
|
)
|
|
174
170
|
.on(
|
|
175
171
|
'postgres_changes',
|
|
@@ -348,7 +344,7 @@ export function useUnreadCount(
|
|
|
348
344
|
return data.count as number;
|
|
349
345
|
},
|
|
350
346
|
enabled: options?.enabled ?? true,
|
|
351
|
-
staleTime:
|
|
347
|
+
staleTime: UNREAD_COUNT_STALE_TIME_MS,
|
|
352
348
|
// Realtime invalidation is the primary update path. Keep a low-frequency
|
|
353
349
|
// refresh as a safety net for disconnected or suspended browser sessions.
|
|
354
350
|
refetchInterval: UNREAD_COUNT_FALLBACK_INTERVAL_MS,
|