@tuturuuu/ui 0.18.0 → 0.19.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 +32 -0
- package/package.json +70 -69
- package/src/components/ui/calendar-app/calendar-client-page.tsx +19 -1
- package/src/components/ui/calendar-app/calendar-page-shell.tsx +37 -16
- package/src/components/ui/calendar-app/components/calendar-connections-manager-helpers.ts +70 -0
- package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +516 -578
- package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +1 -0
- package/src/components/ui/calendar-app/components/calendar-sync-settings-panel.tsx +155 -0
- package/src/components/ui/calendar-app/components/merge-provider-calendars.test.ts +46 -0
- package/src/components/ui/calendar-app/components/merge-provider-calendars.ts +81 -0
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +22 -129
- package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +17 -3
- package/src/components/ui/chat/chat-agent-details-utils.tsx +2 -0
- package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +17 -6
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +35 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +17 -0
- package/src/components/ui/custom/nav-link.test.tsx +1 -0
- package/src/components/ui/custom/nav-link.tsx +4 -1
- package/src/components/ui/custom/settings-dialog-shell.tsx +23 -14
- package/src/components/ui/custom/statistic-card.tsx +3 -5
- package/src/components/ui/custom/structure.test.tsx +4 -0
- package/src/components/ui/custom/user-filters.tsx +4 -7
- package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +1 -1
- package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +1 -1
- package/src/components/ui/custom/workspace-select.tsx +38 -53
- package/src/components/ui/finance/analytics/category-spending-chart.test.tsx +3 -3
- package/src/components/ui/finance/budgets/budget-card.test.tsx +3 -3
- package/src/components/ui/finance/debts/debt-loan-card.test.tsx +3 -3
- package/src/components/ui/finance/debts/debt-loan-detail-cards.test.tsx +3 -3
- package/src/components/ui/finance/invoices/charts/invoice-totals-chart.test.tsx +3 -3
- package/src/components/ui/finance/invoices/columns.test.tsx +3 -3
- package/src/components/ui/finance/invoices/components/invoice-customer-select-card.tsx +1 -1
- package/src/components/ui/finance/shared/numbers-visibility-toggle.test.tsx +1 -1
- package/src/components/ui/finance/tags/tag-manager.test.tsx +2 -2
- package/src/components/ui/finance/transactions/categories/columns.test.tsx +5 -5
- package/src/components/ui/finance/transactions/columns.test.tsx +2 -2
- package/src/components/ui/finance/transactions/confidential-field.test.tsx +2 -2
- package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +2 -2
- package/src/components/ui/finance/transactions/transaction-statistics.test.tsx +3 -3
- package/src/components/ui/finance/wallets/form.test.tsx +3 -3
- package/src/components/ui/finance/wallets/wallets-data-table.test.tsx +10 -0
- package/src/components/ui/legacy/calendar/calendar-cell.tsx +1 -1
- package/src/components/ui/storefront/accent-button.tsx +4 -1
- package/src/components/ui/storefront/browse-panel.tsx +6 -1
- package/src/components/ui/storefront/cart-summary-parts.tsx +18 -4
- package/src/components/ui/storefront/cart-summary.tsx +22 -6
- package/src/components/ui/storefront/hero-panel.tsx +13 -17
- package/src/components/ui/storefront/index.ts +2 -0
- package/src/components/ui/storefront/listing-card.tsx +6 -4
- package/src/components/ui/storefront/product-detail.tsx +51 -21
- package/src/components/ui/storefront/product-dialog.tsx +8 -1
- package/src/components/ui/storefront/storefront-surface.test.tsx +79 -3
- package/src/components/ui/storefront/storefront-surface.tsx +151 -100
- package/src/components/ui/storefront/types.ts +3 -0
- package/src/components/ui/storefront/utils.ts +5 -1
- package/src/components/ui/text-editor/__tests__/task-mention-checkbox.test.ts +23 -6
- package/src/components/ui/text-editor/__tests__/upload-placeholder.test.ts +1 -10
- package/src/components/ui/text-editor/color-controls.tsx +1 -1
- package/src/globals.css +20 -0
- package/src/globals.test.ts +75 -0
- package/src/hooks/__tests__/use-calendar-sync.test.tsx +103 -1
- package/src/hooks/use-calendar-sync.tsx +51 -14
- package/src/lib/task-personal-external.ts +1 -1
|
@@ -46,6 +46,8 @@ import {
|
|
|
46
46
|
} from '../../select';
|
|
47
47
|
import { Separator } from '../../separator';
|
|
48
48
|
import { Switch } from '../../switch';
|
|
49
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../tabs';
|
|
50
|
+
import { CalendarSyncSettingsPanel } from './calendar-sync-settings-panel';
|
|
49
51
|
import type { CalendarConnectionsManagerState } from './use-calendar-connections-manager';
|
|
50
52
|
|
|
51
53
|
function isWritableProviderAccess(accessRole?: string | null) {
|
|
@@ -78,6 +80,7 @@ export function CalendarConnectionsSettingsContent({
|
|
|
78
80
|
handleToggle,
|
|
79
81
|
microsoftAuthMutation,
|
|
80
82
|
newCalendarName,
|
|
83
|
+
providerAccountStatuses,
|
|
81
84
|
resetCalendarDataMutation,
|
|
82
85
|
setNewCalendarName,
|
|
83
86
|
setShowCreateCalendarDialog,
|
|
@@ -92,10 +95,6 @@ export function CalendarConnectionsSettingsContent({
|
|
|
92
95
|
toggleWorkspaceCalendarMutation,
|
|
93
96
|
updateConnectionSyncSettingsMutation,
|
|
94
97
|
} = state;
|
|
95
|
-
const externalSyncOptions =
|
|
96
|
-
syncPreferencesData?.options.filter(
|
|
97
|
-
(option) => option.provider !== 'tuturuuu'
|
|
98
|
-
) ?? [];
|
|
99
98
|
const syncSettingsUnavailable =
|
|
100
99
|
syncPreferencesData?.settingsAvailable === false;
|
|
101
100
|
const syncSettingsDisabled =
|
|
@@ -105,343 +104,190 @@ export function CalendarConnectionsSettingsContent({
|
|
|
105
104
|
|
|
106
105
|
return (
|
|
107
106
|
<div className={className ? `space-y-4 ${className}` : 'space-y-4'}>
|
|
108
|
-
<
|
|
109
|
-
<
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
</
|
|
119
|
-
|
|
120
|
-
value={defaultSourceData?.defaultSource?.id}
|
|
121
|
-
onValueChange={(value) => defaultSourceMutation.mutate(value)}
|
|
122
|
-
disabled={
|
|
123
|
-
!defaultSourceData?.options.length ||
|
|
124
|
-
defaultSourceMutation.isPending
|
|
125
|
-
}
|
|
126
|
-
>
|
|
127
|
-
<SelectTrigger className="w-full">
|
|
128
|
-
<SelectValue placeholder="Choose default calendar" />
|
|
129
|
-
</SelectTrigger>
|
|
130
|
-
<SelectContent>
|
|
131
|
-
{defaultSourceData?.options.map((option) => (
|
|
132
|
-
<SelectItem key={option.id} value={option.id}>
|
|
133
|
-
<div className="flex min-w-0 items-center gap-2">
|
|
134
|
-
<span
|
|
135
|
-
className="h-2.5 w-2.5 shrink-0 rounded-full"
|
|
136
|
-
style={{
|
|
137
|
-
backgroundColor: option.color ?? undefined,
|
|
138
|
-
}}
|
|
139
|
-
/>
|
|
140
|
-
<span className="truncate">{option.label}</span>
|
|
141
|
-
<Badge variant="secondary" className="capitalize">
|
|
142
|
-
{option.provider}
|
|
143
|
-
</Badge>
|
|
144
|
-
</div>
|
|
145
|
-
</SelectItem>
|
|
146
|
-
))}
|
|
147
|
-
</SelectContent>
|
|
148
|
-
</Select>
|
|
149
|
-
</div>
|
|
150
|
-
|
|
151
|
-
<div className="space-y-3 rounded-md border p-3">
|
|
152
|
-
<div>
|
|
153
|
-
<h4 className="font-medium text-sm">
|
|
154
|
-
{t('two_way_sync') || 'Two-way sync'}
|
|
155
|
-
</h4>
|
|
156
|
-
<p className="text-muted-foreground text-xs">
|
|
157
|
-
{t('two_way_sync_desc') ||
|
|
158
|
-
'Control how Tuturuuu imports provider changes and mirrors native events outward.'}
|
|
159
|
-
</p>
|
|
160
|
-
</div>
|
|
161
|
-
|
|
162
|
-
{syncSettingsUnavailable && (
|
|
163
|
-
<p className="rounded-md bg-muted px-3 py-2 text-muted-foreground text-xs">
|
|
164
|
-
{t('calendar_sync_settings_unavailable') ||
|
|
165
|
-
'Sync controls will be available after the latest calendar migration is applied.'}
|
|
166
|
-
</p>
|
|
167
|
-
)}
|
|
168
|
-
|
|
169
|
-
<div className="space-y-2">
|
|
170
|
-
<div className="flex items-center justify-between gap-3">
|
|
171
|
-
<div className="min-w-0">
|
|
172
|
-
<p className="font-medium text-sm">
|
|
173
|
-
{t('import_external_changes') || 'Import external changes'}
|
|
174
|
-
</p>
|
|
175
|
-
<p className="text-muted-foreground text-xs">
|
|
176
|
-
{t('import_external_changes_desc') ||
|
|
177
|
-
'Bring Google and Outlook event changes into Tuturuuu.'}
|
|
178
|
-
</p>
|
|
179
|
-
</div>
|
|
180
|
-
<Switch
|
|
181
|
-
checked={syncPreferencesData?.inboundSyncEnabled ?? true}
|
|
182
|
-
onCheckedChange={(checked) =>
|
|
183
|
-
syncPreferencesMutation.mutate({
|
|
184
|
-
inboundSyncEnabled: checked,
|
|
185
|
-
})
|
|
186
|
-
}
|
|
187
|
-
disabled={syncSettingsDisabled}
|
|
188
|
-
/>
|
|
189
|
-
</div>
|
|
190
|
-
|
|
191
|
-
<div className="flex items-center justify-between gap-3">
|
|
192
|
-
<div className="min-w-0">
|
|
193
|
-
<p className="font-medium text-sm">
|
|
194
|
-
{t('sync_tuturuuu_events_outward') ||
|
|
195
|
-
'Sync Tuturuuu events outward'}
|
|
196
|
-
</p>
|
|
197
|
-
<p className="text-muted-foreground text-xs">
|
|
198
|
-
{t('sync_tuturuuu_events_outward_desc') ||
|
|
199
|
-
'Mirror native Tuturuuu event creates and edits to a connected provider calendar.'}
|
|
200
|
-
</p>
|
|
201
|
-
</div>
|
|
202
|
-
<Switch
|
|
203
|
-
checked={syncPreferencesData?.outboundSyncEnabled ?? false}
|
|
204
|
-
onCheckedChange={(checked) =>
|
|
205
|
-
syncPreferencesMutation.mutate({
|
|
206
|
-
outboundSyncEnabled: checked,
|
|
207
|
-
})
|
|
208
|
-
}
|
|
209
|
-
disabled={
|
|
210
|
-
syncSettingsDisabled || externalSyncOptions.length === 0
|
|
211
|
-
}
|
|
212
|
-
/>
|
|
213
|
-
</div>
|
|
214
|
-
</div>
|
|
215
|
-
|
|
216
|
-
<Select
|
|
217
|
-
value={syncPreferencesData?.defaultOutboundConnectionId ?? 'none'}
|
|
218
|
-
onValueChange={(value) =>
|
|
219
|
-
syncPreferencesMutation.mutate({
|
|
220
|
-
defaultOutboundConnectionId: value === 'none' ? null : value,
|
|
221
|
-
})
|
|
222
|
-
}
|
|
223
|
-
disabled={syncSettingsDisabled || externalSyncOptions.length === 0}
|
|
224
|
-
>
|
|
225
|
-
<SelectTrigger className="w-full">
|
|
226
|
-
<SelectValue
|
|
227
|
-
placeholder={
|
|
228
|
-
t('choose_outbound_calendar') || 'Choose outbound calendar'
|
|
229
|
-
}
|
|
230
|
-
/>
|
|
231
|
-
</SelectTrigger>
|
|
232
|
-
<SelectContent>
|
|
233
|
-
<SelectItem value="none">
|
|
234
|
-
{t('no_outbound_calendar') || 'No outbound calendar'}
|
|
235
|
-
</SelectItem>
|
|
236
|
-
{externalSyncOptions.map((option) => (
|
|
237
|
-
<SelectItem key={option.id} value={option.connectionId}>
|
|
238
|
-
<div className="flex min-w-0 items-center gap-2">
|
|
239
|
-
<span
|
|
240
|
-
className="h-2.5 w-2.5 shrink-0 rounded-full"
|
|
241
|
-
style={{
|
|
242
|
-
backgroundColor: option.color ?? undefined,
|
|
243
|
-
}}
|
|
244
|
-
/>
|
|
245
|
-
<span className="truncate">{option.label}</span>
|
|
246
|
-
<Badge variant="secondary" className="capitalize">
|
|
247
|
-
{option.provider}
|
|
248
|
-
</Badge>
|
|
249
|
-
</div>
|
|
250
|
-
</SelectItem>
|
|
251
|
-
))}
|
|
252
|
-
</SelectContent>
|
|
253
|
-
</Select>
|
|
107
|
+
<Tabs defaultValue="calendars" className="py-4">
|
|
108
|
+
<TabsList className="grid w-full grid-cols-3">
|
|
109
|
+
<TabsTrigger value="calendars">
|
|
110
|
+
{t('settings_tab_calendars') || 'Calendars'}
|
|
111
|
+
</TabsTrigger>
|
|
112
|
+
<TabsTrigger value="accounts">
|
|
113
|
+
{t('settings_tab_accounts') || 'Accounts'}
|
|
114
|
+
</TabsTrigger>
|
|
115
|
+
<TabsTrigger value="sync">
|
|
116
|
+
{t('settings_tab_sync') || 'Sync'}
|
|
117
|
+
</TabsTrigger>
|
|
118
|
+
</TabsList>
|
|
254
119
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
120
|
+
<TabsContent value="calendars" className="space-y-4">
|
|
121
|
+
<div className="space-y-2 rounded-md border p-3">
|
|
122
|
+
<div>
|
|
123
|
+
<h4 className="font-medium text-sm">
|
|
124
|
+
{t('default_calendar_source') || 'Default calendar source'}
|
|
125
|
+
</h4>
|
|
260
126
|
<p className="text-muted-foreground text-xs">
|
|
261
|
-
{t('
|
|
262
|
-
'
|
|
127
|
+
{t('default_calendar_source_desc') ||
|
|
128
|
+
'New events use this calendar unless you choose another source.'}
|
|
263
129
|
</p>
|
|
264
130
|
</div>
|
|
265
|
-
<
|
|
266
|
-
{
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
<div className="space-y-3">
|
|
273
|
-
<div className="flex items-center justify-between">
|
|
274
|
-
<div className="flex items-center gap-2">
|
|
275
|
-
<Image
|
|
276
|
-
src="/icon-512x512.png"
|
|
277
|
-
alt="Tuturuuu"
|
|
278
|
-
width={24}
|
|
279
|
-
height={24}
|
|
280
|
-
/>
|
|
281
|
-
<span className="font-medium text-sm">
|
|
282
|
-
{t('tuturuuu_calendars') || 'Tuturuuu Calendars'}
|
|
283
|
-
</span>
|
|
284
|
-
</div>
|
|
285
|
-
<Button
|
|
286
|
-
variant="ghost"
|
|
287
|
-
size="sm"
|
|
288
|
-
className="h-7 gap-1 text-xs"
|
|
289
|
-
onClick={() => setShowCreateCalendarDialog(true)}
|
|
290
|
-
disabled={createCalendarMutation.isPending}
|
|
131
|
+
<Select
|
|
132
|
+
value={defaultSourceData?.defaultSource?.id}
|
|
133
|
+
onValueChange={(value) => defaultSourceMutation.mutate(value)}
|
|
134
|
+
disabled={
|
|
135
|
+
!defaultSourceData?.options.length ||
|
|
136
|
+
defaultSourceMutation.isPending
|
|
137
|
+
}
|
|
291
138
|
>
|
|
292
|
-
|
|
293
|
-
<
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
>
|
|
306
|
-
<DialogContent className="sm:max-w-md">
|
|
307
|
-
<DialogHeader>
|
|
308
|
-
<DialogTitle>{t('create_calendar')}</DialogTitle>
|
|
309
|
-
</DialogHeader>
|
|
310
|
-
<div className="py-4">
|
|
311
|
-
<Input
|
|
312
|
-
value={newCalendarName}
|
|
313
|
-
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
314
|
-
setNewCalendarName(e.target.value)
|
|
315
|
-
}
|
|
316
|
-
placeholder={
|
|
317
|
-
t('enter_calendar_name') || 'Enter calendar name'
|
|
318
|
-
}
|
|
319
|
-
autoFocus
|
|
320
|
-
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
321
|
-
if (e.key === 'Enter' && newCalendarName.trim()) {
|
|
322
|
-
createCalendarMutation.mutate({
|
|
323
|
-
name: newCalendarName.trim(),
|
|
324
|
-
});
|
|
325
|
-
setShowCreateCalendarDialog(false);
|
|
326
|
-
setNewCalendarName('');
|
|
327
|
-
}
|
|
328
|
-
}}
|
|
329
|
-
/>
|
|
330
|
-
</div>
|
|
331
|
-
<DialogFooter>
|
|
332
|
-
<Button
|
|
333
|
-
onClick={() => {
|
|
334
|
-
if (newCalendarName.trim()) {
|
|
335
|
-
createCalendarMutation.mutate({
|
|
336
|
-
name: newCalendarName.trim(),
|
|
337
|
-
});
|
|
338
|
-
setShowCreateCalendarDialog(false);
|
|
339
|
-
setNewCalendarName('');
|
|
340
|
-
}
|
|
341
|
-
}}
|
|
342
|
-
disabled={
|
|
343
|
-
!newCalendarName.trim() ||
|
|
344
|
-
createCalendarMutation.isPending
|
|
345
|
-
}
|
|
346
|
-
>
|
|
347
|
-
{createCalendarMutation.isPending && (
|
|
348
|
-
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
349
|
-
)}
|
|
350
|
-
{t('create')}
|
|
351
|
-
</Button>
|
|
352
|
-
</DialogFooter>
|
|
353
|
-
</DialogContent>
|
|
354
|
-
</Dialog>
|
|
355
|
-
</div>
|
|
356
|
-
|
|
357
|
-
{/* System Calendars */}
|
|
358
|
-
<div className="space-y-1">
|
|
359
|
-
{systemCalendars.map((cal) => {
|
|
360
|
-
const Icon =
|
|
361
|
-
cal.calendar_type === 'primary'
|
|
362
|
-
? Calendar
|
|
363
|
-
: cal.calendar_type === 'tasks'
|
|
364
|
-
? Target
|
|
365
|
-
: cal.calendar_type === 'habits'
|
|
366
|
-
? Sparkles
|
|
367
|
-
: Calendar;
|
|
368
|
-
return (
|
|
369
|
-
<div
|
|
370
|
-
key={cal.id}
|
|
371
|
-
className="flex items-center justify-between gap-2 rounded-md px-2 py-1.5 hover:bg-muted/50"
|
|
372
|
-
>
|
|
373
|
-
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
374
|
-
<div
|
|
375
|
-
className="flex h-5 w-5 shrink-0 items-center justify-center rounded"
|
|
376
|
-
style={{
|
|
377
|
-
backgroundColor: `${getCalendarColor(cal.color || 'BLUE')}20`,
|
|
378
|
-
}}
|
|
379
|
-
>
|
|
380
|
-
<Icon
|
|
381
|
-
className="h-3 w-3"
|
|
139
|
+
<SelectTrigger className="w-full">
|
|
140
|
+
<SelectValue
|
|
141
|
+
placeholder={
|
|
142
|
+
t('choose_default_calendar') || 'Choose default calendar'
|
|
143
|
+
}
|
|
144
|
+
/>
|
|
145
|
+
</SelectTrigger>
|
|
146
|
+
<SelectContent>
|
|
147
|
+
{defaultSourceData?.options.map((option) => (
|
|
148
|
+
<SelectItem key={option.id} value={option.id}>
|
|
149
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
150
|
+
<span
|
|
151
|
+
className="h-2.5 w-2.5 shrink-0 rounded-full"
|
|
382
152
|
style={{
|
|
383
|
-
|
|
153
|
+
backgroundColor: option.color ?? undefined,
|
|
384
154
|
}}
|
|
385
155
|
/>
|
|
156
|
+
<span className="truncate">{option.label}</span>
|
|
157
|
+
<Badge variant="secondary" className="capitalize">
|
|
158
|
+
{option.provider}
|
|
159
|
+
</Badge>
|
|
386
160
|
</div>
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
{cal.name}
|
|
392
|
-
</span>
|
|
393
|
-
<Lock className="h-3 w-3 shrink-0 text-muted-foreground" />
|
|
394
|
-
</div>
|
|
395
|
-
<Switch
|
|
396
|
-
checked={cal.is_enabled}
|
|
397
|
-
onCheckedChange={() =>
|
|
398
|
-
toggleWorkspaceCalendarMutation.mutate({
|
|
399
|
-
id: cal.id,
|
|
400
|
-
is_enabled: !cal.is_enabled,
|
|
401
|
-
})
|
|
402
|
-
}
|
|
403
|
-
disabled={togglingTuturuuuIds.has(cal.id)}
|
|
404
|
-
/>
|
|
405
|
-
</div>
|
|
406
|
-
);
|
|
407
|
-
})}
|
|
161
|
+
</SelectItem>
|
|
162
|
+
))}
|
|
163
|
+
</SelectContent>
|
|
164
|
+
</Select>
|
|
408
165
|
</div>
|
|
409
166
|
|
|
410
|
-
{/*
|
|
411
|
-
|
|
412
|
-
<div className="
|
|
413
|
-
<
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
>
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
167
|
+
{/* Tuturuuu Calendars Section */}
|
|
168
|
+
<div className="space-y-3">
|
|
169
|
+
<div className="flex items-center justify-between">
|
|
170
|
+
<div className="flex items-center gap-2">
|
|
171
|
+
<Image
|
|
172
|
+
src="/icon-512x512.png"
|
|
173
|
+
alt="Tuturuuu"
|
|
174
|
+
width={24}
|
|
175
|
+
height={24}
|
|
176
|
+
/>
|
|
177
|
+
<span className="font-medium text-sm">
|
|
178
|
+
{t('tuturuuu_calendars') || 'Tuturuuu Calendars'}
|
|
179
|
+
</span>
|
|
180
|
+
</div>
|
|
181
|
+
<Button
|
|
182
|
+
variant="ghost"
|
|
183
|
+
size="sm"
|
|
184
|
+
className="h-7 gap-1 text-xs"
|
|
185
|
+
onClick={() => setShowCreateCalendarDialog(true)}
|
|
186
|
+
disabled={createCalendarMutation.isPending}
|
|
187
|
+
>
|
|
188
|
+
{createCalendarMutation.isPending ? (
|
|
189
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
190
|
+
) : (
|
|
191
|
+
<Plus className="h-3 w-3" />
|
|
192
|
+
)}
|
|
193
|
+
{t('new') || 'New'}
|
|
194
|
+
</Button>
|
|
195
|
+
<Dialog
|
|
196
|
+
open={showCreateCalendarDialog}
|
|
197
|
+
onOpenChange={(open) => {
|
|
198
|
+
setShowCreateCalendarDialog(open);
|
|
199
|
+
if (!open) setNewCalendarName('');
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
<DialogContent className="sm:max-w-md">
|
|
203
|
+
<DialogHeader>
|
|
204
|
+
<DialogTitle>{t('create_calendar')}</DialogTitle>
|
|
205
|
+
</DialogHeader>
|
|
206
|
+
<div className="py-4">
|
|
207
|
+
<Input
|
|
208
|
+
value={newCalendarName}
|
|
209
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
210
|
+
setNewCalendarName(e.target.value)
|
|
211
|
+
}
|
|
212
|
+
placeholder={
|
|
213
|
+
t('enter_calendar_name') || 'Enter calendar name'
|
|
214
|
+
}
|
|
215
|
+
autoFocus
|
|
216
|
+
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
217
|
+
if (e.key === 'Enter' && newCalendarName.trim()) {
|
|
218
|
+
createCalendarMutation.mutate({
|
|
219
|
+
name: newCalendarName.trim(),
|
|
220
|
+
});
|
|
221
|
+
setShowCreateCalendarDialog(false);
|
|
222
|
+
setNewCalendarName('');
|
|
223
|
+
}
|
|
426
224
|
}}
|
|
427
225
|
/>
|
|
428
|
-
<span
|
|
429
|
-
className="line-clamp-1 break-all text-sm"
|
|
430
|
-
title={cal.name}
|
|
431
|
-
>
|
|
432
|
-
{cal.name}
|
|
433
|
-
</span>
|
|
434
226
|
</div>
|
|
435
|
-
<
|
|
227
|
+
<DialogFooter>
|
|
436
228
|
<Button
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
229
|
+
onClick={() => {
|
|
230
|
+
if (newCalendarName.trim()) {
|
|
231
|
+
createCalendarMutation.mutate({
|
|
232
|
+
name: newCalendarName.trim(),
|
|
233
|
+
});
|
|
234
|
+
setShowCreateCalendarDialog(false);
|
|
235
|
+
setNewCalendarName('');
|
|
236
|
+
}
|
|
237
|
+
}}
|
|
238
|
+
disabled={
|
|
239
|
+
!newCalendarName.trim() ||
|
|
240
|
+
createCalendarMutation.isPending
|
|
241
|
+
}
|
|
442
242
|
>
|
|
443
|
-
|
|
243
|
+
{createCalendarMutation.isPending && (
|
|
244
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
245
|
+
)}
|
|
246
|
+
{t('create')}
|
|
444
247
|
</Button>
|
|
248
|
+
</DialogFooter>
|
|
249
|
+
</DialogContent>
|
|
250
|
+
</Dialog>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
{/* System Calendars */}
|
|
254
|
+
<div className="space-y-1">
|
|
255
|
+
{systemCalendars.map((cal) => {
|
|
256
|
+
const Icon =
|
|
257
|
+
cal.calendar_type === 'primary'
|
|
258
|
+
? Calendar
|
|
259
|
+
: cal.calendar_type === 'tasks'
|
|
260
|
+
? Target
|
|
261
|
+
: cal.calendar_type === 'habits'
|
|
262
|
+
? Sparkles
|
|
263
|
+
: Calendar;
|
|
264
|
+
return (
|
|
265
|
+
<div
|
|
266
|
+
key={cal.id}
|
|
267
|
+
className="flex items-center justify-between gap-2 rounded-md px-2 py-1.5 hover:bg-muted/50"
|
|
268
|
+
>
|
|
269
|
+
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
270
|
+
<div
|
|
271
|
+
className="flex h-5 w-5 shrink-0 items-center justify-center rounded"
|
|
272
|
+
style={{
|
|
273
|
+
backgroundColor: `${getCalendarColor(cal.color || 'BLUE')}20`,
|
|
274
|
+
}}
|
|
275
|
+
>
|
|
276
|
+
<Icon
|
|
277
|
+
className="h-3 w-3"
|
|
278
|
+
style={{
|
|
279
|
+
color: getCalendarColor(cal.color || 'BLUE'),
|
|
280
|
+
}}
|
|
281
|
+
/>
|
|
282
|
+
</div>
|
|
283
|
+
<span
|
|
284
|
+
className="line-clamp-1 break-all text-sm"
|
|
285
|
+
title={cal.name}
|
|
286
|
+
>
|
|
287
|
+
{cal.name}
|
|
288
|
+
</span>
|
|
289
|
+
<Lock className="h-3 w-3 shrink-0 text-muted-foreground" />
|
|
290
|
+
</div>
|
|
445
291
|
<Switch
|
|
446
292
|
checked={cal.is_enabled}
|
|
447
293
|
onCheckedChange={() =>
|
|
@@ -453,283 +299,375 @@ export function CalendarConnectionsSettingsContent({
|
|
|
453
299
|
disabled={togglingTuturuuuIds.has(cal.id)}
|
|
454
300
|
/>
|
|
455
301
|
</div>
|
|
456
|
-
|
|
457
|
-
)
|
|
302
|
+
);
|
|
303
|
+
})}
|
|
458
304
|
</div>
|
|
459
|
-
)}
|
|
460
|
-
</div>
|
|
461
|
-
|
|
462
|
-
<Separator />
|
|
463
305
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
<
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
:
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
306
|
+
{/* Custom Calendars */}
|
|
307
|
+
{customCalendars.length > 0 && (
|
|
308
|
+
<div className="space-y-1">
|
|
309
|
+
<p className="px-2 text-muted-foreground text-xs">
|
|
310
|
+
{t('custom_calendars') || 'Custom'}
|
|
311
|
+
</p>
|
|
312
|
+
{customCalendars.map((cal) => (
|
|
313
|
+
<div
|
|
314
|
+
key={cal.id}
|
|
315
|
+
className="flex items-center justify-between gap-2 rounded-md px-2 py-1.5 hover:bg-muted/50"
|
|
316
|
+
>
|
|
317
|
+
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
318
|
+
<div
|
|
319
|
+
className="h-3 w-3 shrink-0 rounded-full"
|
|
320
|
+
style={{
|
|
321
|
+
backgroundColor: getCalendarColor(
|
|
322
|
+
cal.color || 'BLUE'
|
|
323
|
+
),
|
|
324
|
+
}}
|
|
325
|
+
/>
|
|
326
|
+
<span
|
|
327
|
+
className="line-clamp-1 break-all text-sm"
|
|
328
|
+
title={cal.name}
|
|
329
|
+
>
|
|
330
|
+
{cal.name}
|
|
331
|
+
</span>
|
|
332
|
+
</div>
|
|
333
|
+
<div className="flex items-center gap-1">
|
|
334
|
+
<Button
|
|
335
|
+
variant="ghost"
|
|
336
|
+
size="icon"
|
|
337
|
+
className="h-6 w-6 text-destructive hover:bg-destructive/10"
|
|
338
|
+
onClick={() => deleteCalendarMutation.mutate(cal.id)}
|
|
339
|
+
disabled={deleteCalendarMutation.isPending}
|
|
340
|
+
>
|
|
341
|
+
<Trash2 className="h-3 w-3" />
|
|
342
|
+
</Button>
|
|
343
|
+
<Switch
|
|
344
|
+
checked={cal.is_enabled}
|
|
345
|
+
onCheckedChange={() =>
|
|
346
|
+
toggleWorkspaceCalendarMutation.mutate({
|
|
347
|
+
id: cal.id,
|
|
348
|
+
is_enabled: !cal.is_enabled,
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
disabled={togglingTuturuuuIds.has(cal.id)}
|
|
352
|
+
/>
|
|
353
|
+
</div>
|
|
485
354
|
</div>
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
355
|
+
))}
|
|
356
|
+
</div>
|
|
357
|
+
)}
|
|
358
|
+
</div>
|
|
359
|
+
</TabsContent>
|
|
360
|
+
|
|
361
|
+
<TabsContent value="accounts" className="space-y-3">
|
|
362
|
+
{/* Connected accounts with calendars */}
|
|
363
|
+
{accounts.map((account) => (
|
|
364
|
+
<Collapsible
|
|
365
|
+
key={account.id}
|
|
366
|
+
open={expandedAccounts.has(account.id)}
|
|
367
|
+
onOpenChange={() => toggleAccountExpanded(account.id)}
|
|
368
|
+
>
|
|
369
|
+
<div className="rounded-lg border">
|
|
370
|
+
<CollapsibleTrigger className="flex w-full items-center justify-between p-3 transition-colors hover:bg-muted/50">
|
|
371
|
+
<div className="flex items-center gap-3">
|
|
372
|
+
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted">
|
|
373
|
+
<Image
|
|
374
|
+
src={
|
|
375
|
+
account.provider === 'google'
|
|
376
|
+
? '/media/logos/google.svg'
|
|
377
|
+
: '/media/logos/microsoft.svg'
|
|
378
|
+
}
|
|
379
|
+
alt={account.provider}
|
|
380
|
+
width={18}
|
|
381
|
+
height={18}
|
|
382
|
+
/>
|
|
383
|
+
</div>
|
|
384
|
+
<div className="text-left">
|
|
385
|
+
<p className="font-medium text-sm">
|
|
386
|
+
{account.account_name ||
|
|
387
|
+
account.account_email ||
|
|
388
|
+
t('unknown_account')}
|
|
495
389
|
</p>
|
|
390
|
+
{account.account_email && account.account_name && (
|
|
391
|
+
<p className="text-muted-foreground text-xs">
|
|
392
|
+
{account.account_email}
|
|
393
|
+
</p>
|
|
394
|
+
)}
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
<div className="flex items-center gap-2">
|
|
398
|
+
{providerAccountStatuses[account.id]?.state ===
|
|
399
|
+
'reconnect_required' && (
|
|
400
|
+
<Badge variant="destructive" className="text-xs">
|
|
401
|
+
{t('reconnect_required') || 'Reconnect'}
|
|
402
|
+
</Badge>
|
|
496
403
|
)}
|
|
404
|
+
<Badge variant="outline" className="text-xs capitalize">
|
|
405
|
+
{account.provider}
|
|
406
|
+
</Badge>
|
|
407
|
+
<ChevronDown
|
|
408
|
+
className={`h-4 w-4 transition-transform ${expandedAccounts.has(account.id) ? 'rotate-180' : ''}`}
|
|
409
|
+
/>
|
|
497
410
|
</div>
|
|
498
|
-
</
|
|
499
|
-
<div className="flex items-center gap-2">
|
|
500
|
-
<Badge variant="outline" className="text-xs capitalize">
|
|
501
|
-
{account.provider}
|
|
502
|
-
</Badge>
|
|
503
|
-
<ChevronDown
|
|
504
|
-
className={`h-4 w-4 transition-transform ${expandedAccounts.has(account.id) ? 'rotate-180' : ''}`}
|
|
505
|
-
/>
|
|
506
|
-
</div>
|
|
507
|
-
</CollapsibleTrigger>
|
|
508
|
-
|
|
509
|
-
<CollapsibleContent>
|
|
510
|
-
<Separator />
|
|
511
|
-
<div className="space-y-2 p-3">
|
|
512
|
-
{(calendarsByAccount[account.id] || []).length > 0 ? (
|
|
513
|
-
(calendarsByAccount[account.id] || []).map((cal) => {
|
|
514
|
-
const syncControlsDisabled =
|
|
515
|
-
!cal.connectionExists ||
|
|
516
|
-
!cal.is_enabled ||
|
|
517
|
-
syncSettingsDisabled ||
|
|
518
|
-
updateConnectionSyncSettingsMutation.isPending;
|
|
519
|
-
const canWriteToProvider = isWritableProviderAccess(
|
|
520
|
-
cal.accessRole
|
|
521
|
-
);
|
|
411
|
+
</CollapsibleTrigger>
|
|
522
412
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
413
|
+
<CollapsibleContent>
|
|
414
|
+
<Separator />
|
|
415
|
+
<div className="space-y-2 p-3">
|
|
416
|
+
{providerAccountStatuses[account.id]?.state ===
|
|
417
|
+
'reconnect_required' && (
|
|
418
|
+
<div className="flex items-center justify-between gap-3 rounded-md border border-destructive/30 bg-destructive/5 p-2">
|
|
419
|
+
<p className="text-muted-foreground text-xs">
|
|
420
|
+
{t('provider_reconnect_description') ||
|
|
421
|
+
'The provider session expired. Stored calendars remain available; reconnect to refresh them.'}
|
|
422
|
+
</p>
|
|
423
|
+
<Button
|
|
424
|
+
variant="outline"
|
|
425
|
+
size="sm"
|
|
426
|
+
className="shrink-0"
|
|
427
|
+
onClick={() =>
|
|
428
|
+
account.provider === 'google'
|
|
429
|
+
? googleAuthMutation.mutate()
|
|
430
|
+
: microsoftAuthMutation.mutate()
|
|
431
|
+
}
|
|
527
432
|
>
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
</div>
|
|
543
|
-
<Switch
|
|
544
|
-
checked={cal.is_enabled}
|
|
545
|
-
onCheckedChange={() =>
|
|
546
|
-
handleToggle(cal.id, cal.is_enabled, {
|
|
547
|
-
calendar_id: cal.calendar_id,
|
|
548
|
-
calendar_name: cal.calendar_name,
|
|
549
|
-
color: cal.color,
|
|
550
|
-
connectionExists: cal.connectionExists,
|
|
551
|
-
accountId: cal.accountId,
|
|
552
|
-
accessRole: cal.accessRole,
|
|
553
|
-
})
|
|
554
|
-
}
|
|
555
|
-
disabled={togglingIds.has(cal.id)}
|
|
556
|
-
/>
|
|
557
|
-
</div>
|
|
433
|
+
{t('reconnect') || 'Reconnect'}
|
|
434
|
+
</Button>
|
|
435
|
+
</div>
|
|
436
|
+
)}
|
|
437
|
+
{(calendarsByAccount[account.id] || []).length > 0 ? (
|
|
438
|
+
(calendarsByAccount[account.id] || []).map((cal) => {
|
|
439
|
+
const syncControlsDisabled =
|
|
440
|
+
!cal.connectionExists ||
|
|
441
|
+
!cal.is_enabled ||
|
|
442
|
+
syncSettingsDisabled ||
|
|
443
|
+
updateConnectionSyncSettingsMutation.isPending;
|
|
444
|
+
const canWriteToProvider = isWritableProviderAccess(
|
|
445
|
+
cal.accessRole
|
|
446
|
+
);
|
|
558
447
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
)
|
|
572
|
-
}
|
|
573
|
-
disabled={syncControlsDisabled}
|
|
574
|
-
/>
|
|
575
|
-
</label>
|
|
576
|
-
<label className="flex items-center justify-between gap-2 rounded-md bg-muted/40 px-2 py-1 text-muted-foreground text-xs">
|
|
577
|
-
<span>{t('send') || 'Send'}</span>
|
|
578
|
-
<Switch
|
|
579
|
-
checked={cal.syncOutboundEnabled}
|
|
580
|
-
onCheckedChange={(checked) =>
|
|
581
|
-
updateConnectionSyncSettingsMutation.mutate(
|
|
582
|
-
{
|
|
583
|
-
id: cal.id,
|
|
584
|
-
syncOutboundEnabled: checked,
|
|
585
|
-
}
|
|
586
|
-
)
|
|
587
|
-
}
|
|
588
|
-
disabled={
|
|
589
|
-
syncControlsDisabled || !canWriteToProvider
|
|
590
|
-
}
|
|
591
|
-
/>
|
|
592
|
-
</label>
|
|
593
|
-
<label className="flex items-center justify-between gap-2 rounded-md bg-muted/40 px-2 py-1 text-muted-foreground text-xs">
|
|
594
|
-
<span>{t('deletes') || 'Deletes'}</span>
|
|
595
|
-
<Switch
|
|
596
|
-
checked={cal.syncDeleteEnabled}
|
|
597
|
-
onCheckedChange={(checked) =>
|
|
598
|
-
updateConnectionSyncSettingsMutation.mutate(
|
|
599
|
-
{
|
|
600
|
-
id: cal.id,
|
|
601
|
-
syncDeleteEnabled: checked,
|
|
602
|
-
}
|
|
603
|
-
)
|
|
604
|
-
}
|
|
605
|
-
disabled={syncControlsDisabled}
|
|
448
|
+
return (
|
|
449
|
+
<div
|
|
450
|
+
key={cal.id}
|
|
451
|
+
className="rounded-md px-2 py-1.5 hover:bg-muted/50"
|
|
452
|
+
>
|
|
453
|
+
<div className="flex items-center justify-between gap-2">
|
|
454
|
+
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
455
|
+
<div
|
|
456
|
+
className="h-3 w-3 shrink-0 rounded-full"
|
|
457
|
+
style={{
|
|
458
|
+
backgroundColor: cal.color || '#4285f4',
|
|
459
|
+
}}
|
|
606
460
|
/>
|
|
607
|
-
|
|
461
|
+
<span
|
|
462
|
+
className="line-clamp-1 break-all text-sm"
|
|
463
|
+
title={cal.calendar_name}
|
|
464
|
+
>
|
|
465
|
+
{cal.calendar_name}
|
|
466
|
+
</span>
|
|
467
|
+
</div>
|
|
468
|
+
<Switch
|
|
469
|
+
checked={cal.is_enabled}
|
|
470
|
+
onCheckedChange={() =>
|
|
471
|
+
handleToggle(cal.id, cal.is_enabled, {
|
|
472
|
+
calendar_id: cal.calendar_id,
|
|
473
|
+
calendar_name: cal.calendar_name,
|
|
474
|
+
color: cal.color,
|
|
475
|
+
connectionExists: cal.connectionExists,
|
|
476
|
+
accountId: cal.accountId,
|
|
477
|
+
accessRole: cal.accessRole,
|
|
478
|
+
})
|
|
479
|
+
}
|
|
480
|
+
disabled={togglingIds.has(cal.id)}
|
|
481
|
+
/>
|
|
608
482
|
</div>
|
|
609
|
-
)}
|
|
610
|
-
</div>
|
|
611
|
-
);
|
|
612
|
-
})
|
|
613
|
-
) : (
|
|
614
|
-
<p className="py-2 text-center text-muted-foreground text-xs">
|
|
615
|
-
{t('no_calendars_found')}
|
|
616
|
-
</p>
|
|
617
|
-
)}
|
|
618
483
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
484
|
+
{cal.connectionExists && (
|
|
485
|
+
<div className="mt-2 grid grid-cols-3 gap-2 pl-5">
|
|
486
|
+
<label className="flex items-center justify-between gap-2 rounded-md bg-muted/40 px-2 py-1 text-muted-foreground text-xs">
|
|
487
|
+
<span>{t('import') || 'Import'}</span>
|
|
488
|
+
<Switch
|
|
489
|
+
checked={cal.syncInboundEnabled}
|
|
490
|
+
onCheckedChange={(checked) =>
|
|
491
|
+
updateConnectionSyncSettingsMutation.mutate(
|
|
492
|
+
{
|
|
493
|
+
id: cal.id,
|
|
494
|
+
syncInboundEnabled: checked,
|
|
495
|
+
}
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
disabled={syncControlsDisabled}
|
|
499
|
+
/>
|
|
500
|
+
</label>
|
|
501
|
+
<label className="flex items-center justify-between gap-2 rounded-md bg-muted/40 px-2 py-1 text-muted-foreground text-xs">
|
|
502
|
+
<span>{t('send') || 'Send'}</span>
|
|
503
|
+
<Switch
|
|
504
|
+
checked={cal.syncOutboundEnabled}
|
|
505
|
+
onCheckedChange={(checked) =>
|
|
506
|
+
updateConnectionSyncSettingsMutation.mutate(
|
|
507
|
+
{
|
|
508
|
+
id: cal.id,
|
|
509
|
+
syncOutboundEnabled: checked,
|
|
510
|
+
}
|
|
511
|
+
)
|
|
512
|
+
}
|
|
513
|
+
disabled={
|
|
514
|
+
syncControlsDisabled ||
|
|
515
|
+
!canWriteToProvider
|
|
516
|
+
}
|
|
517
|
+
/>
|
|
518
|
+
</label>
|
|
519
|
+
<label className="flex items-center justify-between gap-2 rounded-md bg-muted/40 px-2 py-1 text-muted-foreground text-xs">
|
|
520
|
+
<span>{t('deletes') || 'Deletes'}</span>
|
|
521
|
+
<Switch
|
|
522
|
+
checked={cal.syncDeleteEnabled}
|
|
523
|
+
onCheckedChange={(checked) =>
|
|
524
|
+
updateConnectionSyncSettingsMutation.mutate(
|
|
525
|
+
{
|
|
526
|
+
id: cal.id,
|
|
527
|
+
syncDeleteEnabled: checked,
|
|
528
|
+
}
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
disabled={syncControlsDisabled}
|
|
532
|
+
/>
|
|
533
|
+
</label>
|
|
534
|
+
</div>
|
|
535
|
+
)}
|
|
536
|
+
</div>
|
|
537
|
+
);
|
|
538
|
+
})
|
|
629
539
|
) : (
|
|
630
|
-
<
|
|
540
|
+
<p className="py-2 text-center text-muted-foreground text-xs">
|
|
541
|
+
{t('no_calendars_found')}
|
|
542
|
+
</p>
|
|
631
543
|
)}
|
|
632
|
-
{t('disconnect')}
|
|
633
|
-
</Button>
|
|
634
|
-
</div>
|
|
635
|
-
</CollapsibleContent>
|
|
636
|
-
</div>
|
|
637
|
-
</Collapsible>
|
|
638
|
-
))}
|
|
639
544
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
height={16}
|
|
661
|
-
/>
|
|
662
|
-
)}
|
|
663
|
-
{t('google')}
|
|
664
|
-
</Button>
|
|
665
|
-
<Button
|
|
666
|
-
variant="outline"
|
|
667
|
-
size="sm"
|
|
668
|
-
className="gap-2 text-center"
|
|
669
|
-
onClick={() => microsoftAuthMutation.mutate()}
|
|
670
|
-
disabled={microsoftAuthMutation.isPending}
|
|
671
|
-
>
|
|
672
|
-
{microsoftAuthMutation.isPending ? (
|
|
673
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
674
|
-
) : (
|
|
675
|
-
<Image
|
|
676
|
-
src="/media/logos/microsoft.svg"
|
|
677
|
-
alt="Microsoft"
|
|
678
|
-
width={16}
|
|
679
|
-
height={16}
|
|
680
|
-
/>
|
|
681
|
-
)}
|
|
682
|
-
{t('outlook')}
|
|
683
|
-
</Button>
|
|
684
|
-
</div>
|
|
685
|
-
</div>
|
|
545
|
+
<Separator className="my-2" />
|
|
546
|
+
<Button
|
|
547
|
+
variant="ghost"
|
|
548
|
+
size="sm"
|
|
549
|
+
className="w-full text-destructive hover:bg-destructive/10 hover:text-destructive"
|
|
550
|
+
onClick={() => disconnectMutation.mutate(account.id)}
|
|
551
|
+
disabled={disconnectingId === account.id}
|
|
552
|
+
>
|
|
553
|
+
{disconnectingId === account.id ? (
|
|
554
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
555
|
+
) : (
|
|
556
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
557
|
+
)}
|
|
558
|
+
{t('disconnect')}
|
|
559
|
+
</Button>
|
|
560
|
+
</div>
|
|
561
|
+
</CollapsibleContent>
|
|
562
|
+
</div>
|
|
563
|
+
</Collapsible>
|
|
564
|
+
))}
|
|
686
565
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
<AlertDialog>
|
|
694
|
-
<AlertDialogTrigger asChild>
|
|
566
|
+
{/* Add new account buttons */}
|
|
567
|
+
<div className="space-y-2 pt-2">
|
|
568
|
+
<p className="font-medium text-muted-foreground text-xs uppercase tracking-wider">
|
|
569
|
+
{t('add_account')}
|
|
570
|
+
</p>
|
|
571
|
+
<div className="grid grid-cols-2 gap-2">
|
|
695
572
|
<Button
|
|
696
|
-
variant="
|
|
573
|
+
variant="outline"
|
|
697
574
|
size="sm"
|
|
698
|
-
className="
|
|
699
|
-
|
|
575
|
+
className="gap-2 text-center"
|
|
576
|
+
onClick={() => googleAuthMutation.mutate()}
|
|
577
|
+
disabled={googleAuthMutation.isPending}
|
|
700
578
|
>
|
|
701
|
-
{
|
|
579
|
+
{googleAuthMutation.isPending ? (
|
|
702
580
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
703
581
|
) : (
|
|
704
|
-
<
|
|
582
|
+
<Image
|
|
583
|
+
src="/media/logos/google.svg"
|
|
584
|
+
alt="Google"
|
|
585
|
+
width={16}
|
|
586
|
+
height={16}
|
|
587
|
+
/>
|
|
705
588
|
)}
|
|
706
|
-
{t('
|
|
589
|
+
{t('google')}
|
|
707
590
|
</Button>
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
591
|
+
<Button
|
|
592
|
+
variant="outline"
|
|
593
|
+
size="sm"
|
|
594
|
+
className="gap-2 text-center"
|
|
595
|
+
onClick={() => microsoftAuthMutation.mutate()}
|
|
596
|
+
disabled={microsoftAuthMutation.isPending}
|
|
597
|
+
>
|
|
598
|
+
{microsoftAuthMutation.isPending ? (
|
|
599
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
600
|
+
) : (
|
|
601
|
+
<Image
|
|
602
|
+
src="/media/logos/microsoft.svg"
|
|
603
|
+
alt="Microsoft"
|
|
604
|
+
width={16}
|
|
605
|
+
height={16}
|
|
606
|
+
/>
|
|
607
|
+
)}
|
|
608
|
+
{t('outlook')}
|
|
609
|
+
</Button>
|
|
610
|
+
</div>
|
|
611
|
+
</div>
|
|
612
|
+
</TabsContent>
|
|
613
|
+
|
|
614
|
+
<TabsContent value="sync" className="space-y-4">
|
|
615
|
+
<CalendarSyncSettingsPanel state={state} />
|
|
616
|
+
|
|
617
|
+
{/* Danger Zone */}
|
|
618
|
+
<Separator />
|
|
619
|
+
<Collapsible className="rounded-lg border border-destructive/20">
|
|
620
|
+
<CollapsibleTrigger className="flex w-full items-center justify-between px-3 py-2 text-destructive transition-colors hover:bg-destructive/5">
|
|
621
|
+
<span className="font-medium text-xs uppercase tracking-wider">
|
|
622
|
+
{t('danger_zone') || 'Danger Zone'}
|
|
623
|
+
</span>
|
|
624
|
+
<ChevronDown className="h-4 w-4" />
|
|
625
|
+
</CollapsibleTrigger>
|
|
626
|
+
<CollapsibleContent className="border-destructive/20 border-t p-3">
|
|
627
|
+
<AlertDialog>
|
|
628
|
+
<AlertDialogTrigger asChild>
|
|
629
|
+
<Button
|
|
630
|
+
variant="destructive"
|
|
631
|
+
size="sm"
|
|
632
|
+
className="w-full gap-2"
|
|
633
|
+
disabled={resetCalendarDataMutation.isPending}
|
|
634
|
+
>
|
|
635
|
+
{resetCalendarDataMutation.isPending ? (
|
|
636
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
637
|
+
) : (
|
|
638
|
+
<RotateCcw className="h-4 w-4" />
|
|
639
|
+
)}
|
|
640
|
+
{t('reset_calendar_data') || 'Reset Calendar Data'}
|
|
641
|
+
</Button>
|
|
642
|
+
</AlertDialogTrigger>
|
|
643
|
+
<AlertDialogContent>
|
|
644
|
+
<AlertDialogHeader>
|
|
645
|
+
<AlertDialogTitle>
|
|
646
|
+
{t('reset_calendar_confirm_title') ||
|
|
647
|
+
'Reset all calendar data?'}
|
|
648
|
+
</AlertDialogTitle>
|
|
649
|
+
<AlertDialogDescription>
|
|
650
|
+
{t('reset_calendar_confirm_desc') ||
|
|
651
|
+
'This will permanently delete all calendar events, disconnect all linked accounts (Google, Microsoft), and remove custom calendars. System calendars will be preserved but emptied. This action cannot be undone.'}
|
|
652
|
+
</AlertDialogDescription>
|
|
653
|
+
</AlertDialogHeader>
|
|
654
|
+
<AlertDialogFooter>
|
|
655
|
+
<AlertDialogCancel>
|
|
656
|
+
{t('cancel') || 'Cancel'}
|
|
657
|
+
</AlertDialogCancel>
|
|
658
|
+
<AlertDialogAction
|
|
659
|
+
onClick={() => resetCalendarDataMutation.mutate()}
|
|
660
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
661
|
+
>
|
|
662
|
+
{t('reset_all_data') || 'Reset All Data'}
|
|
663
|
+
</AlertDialogAction>
|
|
664
|
+
</AlertDialogFooter>
|
|
665
|
+
</AlertDialogContent>
|
|
666
|
+
</AlertDialog>
|
|
667
|
+
</CollapsibleContent>
|
|
668
|
+
</Collapsible>
|
|
669
|
+
</TabsContent>
|
|
670
|
+
</Tabs>
|
|
733
671
|
</div>
|
|
734
672
|
);
|
|
735
673
|
}
|