@tuturuuu/ui 0.2.0 → 0.3.1
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 +53 -0
- package/package.json +79 -67
- package/src/components/ui/__tests__/avatar.test.tsx +8 -5
- package/src/components/ui/calendar-app/components/calendar-connections-compact.tsx +414 -0
- package/src/components/ui/calendar-app/components/calendar-connections-manager.tsx +5 -1
- package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +529 -0
- package/src/components/ui/calendar-app/components/calendar-connections-unified.tsx +26 -1429
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +711 -0
- package/src/components/ui/chart.test.tsx +29 -0
- package/src/components/ui/chart.tsx +12 -3
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +24 -1
- package/src/components/ui/custom/__tests__/tuturuuu-logo.test.ts +12 -3
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +39 -0
- package/src/components/ui/custom/common-footer.tsx +16 -1
- package/src/components/ui/custom/production-indicator.tsx +1 -1
- package/src/components/ui/custom/settings/sidebar-settings.tsx +1 -1
- package/src/components/ui/custom/settings/task-settings.tsx +18 -0
- package/src/components/ui/custom/settings-dialog-shell.tsx +38 -23
- package/src/components/ui/custom/sidebar-context-compile-graph.test.ts +60 -0
- package/src/components/ui/custom/sidebar-context.tsx +61 -61
- package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +123 -0
- package/src/components/ui/custom/tuturuuu-logo-urls.ts +6 -0
- package/src/components/ui/custom/tuturuuu-logo.tsx +25 -7
- package/src/components/ui/custom/workspace-select-helpers.ts +20 -0
- package/src/components/ui/custom/workspace-select.tsx +33 -12
- package/src/components/ui/finance/invoices/components/invoice-checkout-summary.tsx +7 -1
- package/src/components/ui/finance/invoices/components/invoice-payment-settings.tsx +3 -0
- package/src/components/ui/finance/invoices/components/invoice-products-permission-warning.tsx +58 -0
- package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +12 -20
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +10 -9
- package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-content.ts +10 -5
- package/src/components/ui/finance/invoices/hooks.ts +75 -20
- package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +137 -0
- package/src/components/ui/finance/invoices/new-invoice-page.tsx +86 -37
- package/src/components/ui/finance/invoices/product-selection.test.tsx +8 -26
- package/src/components/ui/finance/invoices/product-selection.tsx +2 -10
- package/src/components/ui/finance/invoices/standard-invoice.tsx +88 -26
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +154 -46
- package/src/components/ui/finance/invoices/utils.test.ts +50 -0
- package/src/components/ui/finance/invoices/utils.ts +75 -17
- package/src/components/ui/finance/shared/finance-display-amount.tsx +3 -1
- package/src/components/ui/finance/shared/finance-permission-warning-dialog.test.tsx +34 -0
- package/src/components/ui/finance/shared/finance-permission-warning-dialog.tsx +157 -0
- package/src/components/ui/finance/transactions/form-basic-tab.tsx +8 -0
- package/src/components/ui/finance/transactions/form-more-tab.tsx +8 -0
- package/src/components/ui/finance/transactions/form-types.ts +2 -0
- package/src/components/ui/finance/transactions/form.test.tsx +43 -0
- package/src/components/ui/finance/transactions/form.tsx +60 -0
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +27 -0
- package/src/components/ui/finance/transactions/transactions-create-summary.tsx +13 -1
- package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +4 -0
- package/src/components/ui/finance/transactions/transactions-page.tsx +23 -1
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +5 -0
- package/src/components/ui/legacy/calendar/calendar-content.tsx +9 -1
- package/src/components/ui/legacy/calendar/event-modal.tsx +146 -2
- package/src/components/ui/legacy/calendar/event-preview-popover.tsx +200 -0
- package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +76 -0
- package/src/components/ui/legacy/calendar/smart-calendar.tsx +13 -1
- package/src/components/ui/legacy/meet/page.test.ts +180 -0
- package/src/components/ui/legacy/meet/page.tsx +87 -39
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +79 -25
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-external-workspaces.test.tsx +392 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.test.tsx +57 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-island.tsx +106 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +106 -161
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-assignees.ts +96 -150
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-labels.ts +63 -79
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-relations-projects.ts +64 -83
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +115 -155
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operation-utils.ts +319 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +8 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +63 -37
- package/src/components/ui/tu-do/boards/boardId/kanban/kanban-column-collapse.ts +16 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +46 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +5 -3
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +19 -7
- package/src/components/ui/tu-do/boards/boardId/menus/__tests__/task-menus.test.tsx +181 -2
- package/src/components/ui/tu-do/boards/boardId/menus/index.ts +1 -0
- package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-menu.tsx +463 -0
- package/src/components/ui/tu-do/boards/boardId/menus/task-scheduling-utils.ts +109 -0
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +4 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardCheckbox.tsx +6 -3
- package/src/components/ui/tu-do/boards/boardId/task-card/TaskCardDates.tsx +26 -9
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-checkbox-style.ts +39 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.test.ts +43 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +33 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.test.ts +31 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-completion-checkbox-visibility.ts +9 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.test.tsx +124 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-identifier-row.tsx +88 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +151 -76
- package/src/components/ui/tu-do/boards/boardId/task-card/task-scheduling-badge.tsx +174 -0
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +34 -13
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +54 -1
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +158 -0
- package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +5 -2
- package/src/components/ui/tu-do/shared/board-client.tsx +12 -2
- package/src/components/ui/tu-do/shared/board-views.tsx +195 -328
- package/src/components/ui/tu-do/shared/list-view.tsx +18 -8
- package/src/components/ui/tu-do/shared/task-due-date-visibility.test.ts +72 -0
- package/src/components/ui/tu-do/shared/task-due-date-visibility.ts +38 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-mutations.ts +6 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +2 -2
- package/src/components/ui/tu-do/shared/task-row-actions-menu.tsx +33 -0
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +74 -3
- package/src/hooks/__tests__/use-task-actions.test.tsx +118 -0
- package/src/hooks/__tests__/use-user-config.test.tsx +65 -0
- package/src/hooks/__tests__/use-workspace-presence.test.tsx +1 -1
- package/src/hooks/use-calendar-sync.tsx +22 -277
- package/src/hooks/use-calendar.tsx +95 -525
- package/src/hooks/use-task-actions.ts +43 -117
- package/src/hooks/use-user-config.ts +1 -1
- package/src/hooks/use-workspace-config.ts +6 -2
- package/src/hooks/use-workspace-presence.ts +1 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-actions-bar.tsx +0 -94
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
useRef,
|
|
24
24
|
useState,
|
|
25
25
|
} from 'react';
|
|
26
|
-
import { toast } from '../components/ui/sonner';
|
|
27
26
|
import { useCalendarSync } from './use-calendar-sync';
|
|
28
27
|
|
|
29
28
|
// Utility function to round time to nearest 15-minute interval
|
|
@@ -107,11 +106,18 @@ const CalendarContext = createContext<{
|
|
|
107
106
|
deleteEvent: (eventId: string) => Promise<void>;
|
|
108
107
|
isModalOpen: boolean;
|
|
109
108
|
activeEvent: CalendarEvent | undefined;
|
|
109
|
+
isPreviewOpen: boolean;
|
|
110
|
+
previewEvent: CalendarEvent | undefined;
|
|
110
111
|
openModal: (
|
|
111
112
|
eventId?: string,
|
|
112
113
|
modalType?: 'all-day' | 'event',
|
|
113
114
|
options?: { defaultNewEventTab?: 'manual' | 'ai' }
|
|
114
115
|
) => void;
|
|
116
|
+
openEventEditor: (
|
|
117
|
+
eventId?: string,
|
|
118
|
+
options?: { defaultNewEventTab?: 'manual' | 'ai' }
|
|
119
|
+
) => void;
|
|
120
|
+
closePreview: () => void;
|
|
115
121
|
closeModal: () => void;
|
|
116
122
|
isEditing: () => boolean;
|
|
117
123
|
hideModal: () => void;
|
|
@@ -174,7 +180,11 @@ const CalendarContext = createContext<{
|
|
|
174
180
|
deleteEvent: () => Promise.resolve(),
|
|
175
181
|
isModalOpen: false,
|
|
176
182
|
activeEvent: undefined,
|
|
183
|
+
isPreviewOpen: false,
|
|
184
|
+
previewEvent: undefined,
|
|
177
185
|
openModal: () => undefined,
|
|
186
|
+
openEventEditor: () => undefined,
|
|
187
|
+
closePreview: () => undefined,
|
|
178
188
|
closeModal: () => undefined,
|
|
179
189
|
isEditing: () => false,
|
|
180
190
|
hideModal: () => undefined,
|
|
@@ -362,10 +372,10 @@ async function syncTaskDurationAfterEventChange(
|
|
|
362
372
|
|
|
363
373
|
export const CalendarProvider = ({
|
|
364
374
|
ws,
|
|
365
|
-
useQuery,
|
|
375
|
+
useQuery: _useQuery,
|
|
366
376
|
useQueryClient,
|
|
367
377
|
children,
|
|
368
|
-
experimentalGoogleToken,
|
|
378
|
+
experimentalGoogleToken: _experimentalGoogleToken,
|
|
369
379
|
readOnly = false,
|
|
370
380
|
}: {
|
|
371
381
|
ws?: Workspace;
|
|
@@ -391,6 +401,7 @@ export const CalendarProvider = ({
|
|
|
391
401
|
|
|
392
402
|
// Modal state
|
|
393
403
|
const [activeEventId, setActiveEventId] = useState<string | null>(null);
|
|
404
|
+
const [previewEventId, setPreviewEventId] = useState<string | null>(null);
|
|
394
405
|
const [isModalHidden, setModalHidden] = useState(false);
|
|
395
406
|
const [pendingNewEvent, setPendingNewEvent] =
|
|
396
407
|
useState<Partial<CalendarEvent> | null>(null);
|
|
@@ -615,6 +626,7 @@ export const CalendarProvider = ({
|
|
|
615
626
|
color: eventColor as SupportedColor,
|
|
616
627
|
location: event.location || '',
|
|
617
628
|
locked: true,
|
|
629
|
+
source: event.source,
|
|
618
630
|
}),
|
|
619
631
|
}
|
|
620
632
|
);
|
|
@@ -804,6 +816,7 @@ export const CalendarProvider = ({
|
|
|
804
816
|
location: updateData.location,
|
|
805
817
|
}),
|
|
806
818
|
...(updateData.locked !== undefined && { locked: updateData.locked }),
|
|
819
|
+
...(updateData.source !== undefined && { source: updateData.source }),
|
|
807
820
|
};
|
|
808
821
|
|
|
809
822
|
// ws is guaranteed to be defined here (validated above at line 732)
|
|
@@ -904,6 +917,7 @@ export const CalendarProvider = ({
|
|
|
904
917
|
'color',
|
|
905
918
|
'location',
|
|
906
919
|
'locked',
|
|
920
|
+
'source',
|
|
907
921
|
];
|
|
908
922
|
|
|
909
923
|
const cleanedUpdates: Partial<CalendarEvent> = {};
|
|
@@ -1021,68 +1035,15 @@ export const CalendarProvider = ({
|
|
|
1021
1035
|
// Just clear the pending event
|
|
1022
1036
|
setPendingNewEvent(null);
|
|
1023
1037
|
setActiveEventId(null);
|
|
1038
|
+
setPreviewEventId(null);
|
|
1024
1039
|
return;
|
|
1025
1040
|
}
|
|
1026
1041
|
|
|
1027
1042
|
if (!ws) throw new Error('No workspace selected');
|
|
1028
1043
|
|
|
1029
|
-
// Find the event first to get the Google Calendar ID
|
|
1030
1044
|
const eventToDelete = events.find(
|
|
1031
1045
|
(e: CalendarEvent) => e.id === eventId
|
|
1032
1046
|
) as (CalendarEvent & { task_id?: string | null }) | undefined;
|
|
1033
|
-
const googleCalendarEventId = eventToDelete?.google_event_id;
|
|
1034
|
-
|
|
1035
|
-
// --- Google Calendar Sync (Delete) ---
|
|
1036
|
-
if (googleCalendarEventId && experimentalGoogleToken) {
|
|
1037
|
-
// Check if ID exists and feature enabled
|
|
1038
|
-
try {
|
|
1039
|
-
const syncResponse = await fetch('/api/v1/calendar/auth/sync', {
|
|
1040
|
-
method: 'DELETE',
|
|
1041
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1042
|
-
body: JSON.stringify({ googleCalendarEventId }),
|
|
1043
|
-
});
|
|
1044
|
-
|
|
1045
|
-
if (!syncResponse.ok) {
|
|
1046
|
-
const errorData = await syncResponse.json();
|
|
1047
|
-
if (errorData.eventNotFound) {
|
|
1048
|
-
console.warn(
|
|
1049
|
-
`Google event ${googleCalendarEventId} not found during delete sync. Proceeding with local delete.`
|
|
1050
|
-
);
|
|
1051
|
-
// Don't throw, just log. The event is gone from Google anyway.
|
|
1052
|
-
} else if (errorData.needsReAuth) {
|
|
1053
|
-
// Notify user to re-authenticate with Google Calendar
|
|
1054
|
-
toast.error('Google Calendar authentication expired', {
|
|
1055
|
-
description:
|
|
1056
|
-
'Please re-authenticate your Google Calendar connection to continue syncing events.',
|
|
1057
|
-
action: {
|
|
1058
|
-
label: 'Re-authenticate',
|
|
1059
|
-
onClick: () => {
|
|
1060
|
-
// Redirect to Google Calendar auth page or open auth modal
|
|
1061
|
-
// This could be enhanced to open a specific auth flow
|
|
1062
|
-
window.open(
|
|
1063
|
-
`/api/v1/calendar/auth?wsId=${ws?.id}`,
|
|
1064
|
-
'_blank'
|
|
1065
|
-
);
|
|
1066
|
-
},
|
|
1067
|
-
},
|
|
1068
|
-
});
|
|
1069
|
-
// Continue with local delete - don't block user action
|
|
1070
|
-
console.warn(
|
|
1071
|
-
'Google Calendar re-authentication required, proceeding with local delete'
|
|
1072
|
-
);
|
|
1073
|
-
} else {
|
|
1074
|
-
// Throw an error to potentially stop the local delete or notify user
|
|
1075
|
-
throw new Error(
|
|
1076
|
-
`Google Calendar sync (DELETE) failed: ${syncResponse.statusText} - ${JSON.stringify(errorData)}`
|
|
1077
|
-
);
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
} catch (_) {
|
|
1081
|
-
// Failed to sync delete with Google Calendar
|
|
1082
|
-
}
|
|
1083
|
-
} else if (experimentalGoogleToken && !googleCalendarEventId) {
|
|
1084
|
-
// Event has no Google Calendar ID, skipping delete sync
|
|
1085
|
-
}
|
|
1086
1047
|
|
|
1087
1048
|
if (!ws?.id) {
|
|
1088
1049
|
throw new Error('No workspace selected');
|
|
@@ -1116,6 +1077,7 @@ export const CalendarProvider = ({
|
|
|
1116
1077
|
// Refresh the query cache after deleting an event
|
|
1117
1078
|
refresh();
|
|
1118
1079
|
setActiveEventId(null);
|
|
1080
|
+
setPreviewEventId(null);
|
|
1119
1081
|
|
|
1120
1082
|
// If this was a task-linked event, refresh task queries
|
|
1121
1083
|
if (hasLinkedTask || hasLinkedHabit) {
|
|
@@ -1128,357 +1090,49 @@ export const CalendarProvider = ({
|
|
|
1128
1090
|
refresh,
|
|
1129
1091
|
pendingNewEvent,
|
|
1130
1092
|
events,
|
|
1131
|
-
experimentalGoogleToken,
|
|
1132
1093
|
queryClient,
|
|
1133
1094
|
onTaskScheduled,
|
|
1134
1095
|
readOnly,
|
|
1135
1096
|
]
|
|
1136
1097
|
);
|
|
1137
1098
|
|
|
1138
|
-
|
|
1139
|
-
const fetchGoogleCalendarEvents = async () => {
|
|
1140
|
-
if (!ws?.id) {
|
|
1141
|
-
throw new Error('No workspace selected');
|
|
1142
|
-
}
|
|
1143
|
-
const response = await fetch(`/api/v1/calendar/auth/fetch?wsId=${ws.id}`);
|
|
1144
|
-
if (!response.ok) {
|
|
1145
|
-
throw new Error('Failed to fetch Google Calendar events');
|
|
1146
|
-
}
|
|
1147
|
-
return await response.json();
|
|
1148
|
-
};
|
|
1149
|
-
|
|
1150
|
-
// Query to fetch Google Calendar events every 1 hour
|
|
1151
|
-
const { data: googleData } = useQuery({
|
|
1152
|
-
queryKey: ['googleCalendarEvents', ws?.id],
|
|
1153
|
-
queryFn: fetchGoogleCalendarEvents,
|
|
1154
|
-
enabled: !!ws?.id && !!experimentalGoogleToken?.id,
|
|
1155
|
-
refetchInterval: 1000 * 60 * 60, // Fetch every 1 hour
|
|
1156
|
-
staleTime: 1000 * 60 * 60, // Data is considered fresh for 1 hour
|
|
1157
|
-
});
|
|
1158
|
-
|
|
1159
|
-
const googleEvents = useMemo(() => googleData?.events || [], [googleData]);
|
|
1160
|
-
|
|
1099
|
+
const googleEvents = useMemo(() => [], []);
|
|
1161
1100
|
const getGoogleEvents = useCallback(() => googleEvents, [googleEvents]);
|
|
1162
1101
|
|
|
1163
|
-
//
|
|
1164
|
-
const
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
phase: 'delete' | 'update' | 'insert' | 'complete';
|
|
1168
|
-
current: number;
|
|
1169
|
-
total: number;
|
|
1170
|
-
changesMade: boolean;
|
|
1171
|
-
}) => void
|
|
1172
|
-
) => {
|
|
1173
|
-
const workspaceId = ws?.id;
|
|
1174
|
-
if (!workspaceId || !googleEvents.length || !experimentalGoogleToken)
|
|
1175
|
-
return;
|
|
1176
|
-
|
|
1177
|
-
// Get local events that are synced with Google Calendar
|
|
1178
|
-
const localGoogleEvents: CalendarEvent[] = events.filter(
|
|
1179
|
-
(e: CalendarEvent) => e.google_event_id
|
|
1180
|
-
);
|
|
1181
|
-
|
|
1182
|
-
// Create a map for faster lookups of local events
|
|
1183
|
-
const localEventMap = new Map<string, CalendarEvent>();
|
|
1184
|
-
localGoogleEvents.forEach((event) => {
|
|
1185
|
-
if (event.google_event_id) {
|
|
1186
|
-
localEventMap.set(event.google_event_id, event);
|
|
1187
|
-
}
|
|
1188
|
-
});
|
|
1189
|
-
|
|
1190
|
-
// Create a set of google_event_id from Google Calendar events for quick lookup
|
|
1191
|
-
const googleEventIds: Set<string | undefined> = new Set(
|
|
1192
|
-
googleEvents.map((e: { google_event_id?: string }) => e.google_event_id)
|
|
1193
|
-
);
|
|
1194
|
-
|
|
1195
|
-
// Identify events to delete: local events not present in Google Calendar
|
|
1196
|
-
const eventsToDelete = localGoogleEvents.filter(
|
|
1197
|
-
(e) => e.google_event_id && !googleEventIds.has(e.google_event_id)
|
|
1198
|
-
);
|
|
1199
|
-
|
|
1200
|
-
// Initialize batch operations - we'll perform these in a more optimized way
|
|
1201
|
-
const eventsToUpdate: Array<{ id: string; data: any }> = [];
|
|
1202
|
-
const eventsToInsert: Array<any> = [];
|
|
1203
|
-
let changesMade = false;
|
|
1204
|
-
|
|
1205
|
-
// Report initial progress
|
|
1206
|
-
if (progressCallback) {
|
|
1207
|
-
progressCallback({
|
|
1208
|
-
phase: 'delete',
|
|
1209
|
-
current: 0,
|
|
1210
|
-
total: eventsToDelete.length,
|
|
1211
|
-
changesMade: false,
|
|
1212
|
-
});
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
// Handle events to delete
|
|
1216
|
-
if (eventsToDelete.length > 0) {
|
|
1217
|
-
changesMade = true;
|
|
1218
|
-
// Delete events in batches for better performance
|
|
1219
|
-
const batchSize = 10;
|
|
1220
|
-
for (let i = 0; i < eventsToDelete.length; i += batchSize) {
|
|
1221
|
-
const batch = eventsToDelete.slice(i, i + batchSize);
|
|
1222
|
-
const eventIds = batch.map((e) => e.id);
|
|
1223
|
-
|
|
1224
|
-
// Report progress
|
|
1225
|
-
if (progressCallback) {
|
|
1226
|
-
progressCallback({
|
|
1227
|
-
phase: 'delete',
|
|
1228
|
-
current: i + batch.length,
|
|
1229
|
-
total: eventsToDelete.length,
|
|
1230
|
-
changesMade: true,
|
|
1231
|
-
});
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
try {
|
|
1235
|
-
await Promise.all(
|
|
1236
|
-
eventIds.map(async (eventId) => {
|
|
1237
|
-
const response = await fetch(
|
|
1238
|
-
`/api/v1/workspaces/${workspaceId}/calendar/events/${eventId}`,
|
|
1239
|
-
{
|
|
1240
|
-
method: 'DELETE',
|
|
1241
|
-
}
|
|
1242
|
-
);
|
|
1243
|
-
|
|
1244
|
-
if (!response.ok) {
|
|
1245
|
-
const errorData = await response.json().catch(() => null);
|
|
1246
|
-
throw new Error(
|
|
1247
|
-
errorData?.error || 'Failed to delete calendar event'
|
|
1248
|
-
);
|
|
1249
|
-
}
|
|
1250
|
-
})
|
|
1251
|
-
);
|
|
1252
|
-
} catch (_) {
|
|
1253
|
-
// Failed to delete events batch
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
// Gather events to update or insert
|
|
1259
|
-
for (const gEvent of googleEvents) {
|
|
1260
|
-
// Skip events without google_event_id
|
|
1261
|
-
if (!gEvent.google_event_id) continue;
|
|
1262
|
-
|
|
1263
|
-
const localEvent = localEventMap.get(gEvent.google_event_id);
|
|
1264
|
-
|
|
1265
|
-
if (localEvent) {
|
|
1266
|
-
// Check if there are any significant changes in the event details that require an update
|
|
1267
|
-
// For encrypted events, we only check non-encrypted fields (dates, color)
|
|
1268
|
-
// since we can't compare encrypted content with plaintext Google data
|
|
1269
|
-
const isEncrypted = localEvent.is_encrypted === true;
|
|
1270
|
-
|
|
1271
|
-
const hasNonEncryptedChanges =
|
|
1272
|
-
localEvent.start_at !== gEvent.start_at ||
|
|
1273
|
-
localEvent.end_at !== gEvent.end_at ||
|
|
1274
|
-
localEvent.color !== gEvent.color;
|
|
1275
|
-
|
|
1276
|
-
const hasContentChanges =
|
|
1277
|
-
!isEncrypted &&
|
|
1278
|
-
(localEvent.title !== gEvent.title ||
|
|
1279
|
-
localEvent.description !== (gEvent.description || '') ||
|
|
1280
|
-
localEvent.location !== (gEvent.location || ''));
|
|
1281
|
-
|
|
1282
|
-
const hasChanges = hasNonEncryptedChanges || hasContentChanges;
|
|
1283
|
-
|
|
1284
|
-
// Only update if there are actual changes
|
|
1285
|
-
if (hasChanges) {
|
|
1286
|
-
changesMade = true;
|
|
1287
|
-
|
|
1288
|
-
// For encrypted events, only update non-encrypted fields (dates, color)
|
|
1289
|
-
// to preserve the encrypted title/description/location
|
|
1290
|
-
if (isEncrypted) {
|
|
1291
|
-
eventsToUpdate.push({
|
|
1292
|
-
id: localEvent.id,
|
|
1293
|
-
data: {
|
|
1294
|
-
start_at: gEvent.start_at,
|
|
1295
|
-
end_at: gEvent.end_at,
|
|
1296
|
-
color: gEvent.color || 'BLUE',
|
|
1297
|
-
// Don't update title, description, location - they are encrypted
|
|
1298
|
-
},
|
|
1299
|
-
});
|
|
1300
|
-
} else {
|
|
1301
|
-
eventsToUpdate.push({
|
|
1302
|
-
id: localEvent.id,
|
|
1303
|
-
data: {
|
|
1304
|
-
title: gEvent.title,
|
|
1305
|
-
description: gEvent.description || '',
|
|
1306
|
-
start_at: gEvent.start_at,
|
|
1307
|
-
end_at: gEvent.end_at,
|
|
1308
|
-
color: gEvent.color || 'BLUE',
|
|
1309
|
-
location: gEvent.location || '',
|
|
1310
|
-
},
|
|
1311
|
-
});
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
} else {
|
|
1315
|
-
// Check for content-based duplicates before adding
|
|
1316
|
-
const potentialDuplicates = events.filter(
|
|
1317
|
-
(localEvent: CalendarEvent) => {
|
|
1318
|
-
return (
|
|
1319
|
-
localEvent.title === gEvent.title &&
|
|
1320
|
-
localEvent.description === (gEvent.description || '') &&
|
|
1321
|
-
localEvent.start_at === gEvent.start_at &&
|
|
1322
|
-
localEvent.end_at === gEvent.end_at
|
|
1323
|
-
);
|
|
1324
|
-
}
|
|
1325
|
-
);
|
|
1326
|
-
|
|
1327
|
-
if (potentialDuplicates.length > 0) {
|
|
1328
|
-
if (potentialDuplicates[0]) {
|
|
1329
|
-
changesMade = true;
|
|
1330
|
-
// Update the existing event with the Google Event ID rather than creating a new one
|
|
1331
|
-
eventsToUpdate.push({
|
|
1332
|
-
id: potentialDuplicates[0].id,
|
|
1333
|
-
data: {
|
|
1334
|
-
google_event_id: gEvent.google_event_id,
|
|
1335
|
-
},
|
|
1336
|
-
});
|
|
1337
|
-
continue;
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
// No duplicates found, add to insert batch
|
|
1342
|
-
changesMade = true;
|
|
1343
|
-
eventsToInsert.push({
|
|
1344
|
-
title: gEvent.title,
|
|
1345
|
-
description: gEvent.description || '',
|
|
1346
|
-
start_at: gEvent.start_at,
|
|
1347
|
-
end_at: gEvent.end_at,
|
|
1348
|
-
color: gEvent.color || 'BLUE',
|
|
1349
|
-
location: gEvent.location || '',
|
|
1350
|
-
ws_id: ws?.id ?? '',
|
|
1351
|
-
google_event_id: gEvent.google_event_id,
|
|
1352
|
-
locked: gEvent.locked || false,
|
|
1353
|
-
created_at: new Date().toISOString(),
|
|
1354
|
-
});
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
// Process batch updates
|
|
1359
|
-
if (eventsToUpdate.length > 0) {
|
|
1360
|
-
// Report progress for update phase
|
|
1361
|
-
if (progressCallback) {
|
|
1362
|
-
progressCallback({
|
|
1363
|
-
phase: 'update',
|
|
1364
|
-
current: 0,
|
|
1365
|
-
total: eventsToUpdate.length,
|
|
1366
|
-
changesMade: changesMade,
|
|
1367
|
-
});
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
const batchSize = 5; // Smaller batch size for updates to be safer
|
|
1371
|
-
|
|
1372
|
-
for (let i = 0; i < eventsToUpdate.length; i += batchSize) {
|
|
1373
|
-
const batch = eventsToUpdate.slice(i, i + batchSize);
|
|
1374
|
-
|
|
1375
|
-
// Report progress update
|
|
1376
|
-
if (progressCallback) {
|
|
1377
|
-
progressCallback({
|
|
1378
|
-
phase: 'update',
|
|
1379
|
-
current: i + batch.length,
|
|
1380
|
-
total: eventsToUpdate.length,
|
|
1381
|
-
changesMade: changesMade,
|
|
1382
|
-
});
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
// Process each update one by one to ensure reliability
|
|
1386
|
-
for (const item of batch) {
|
|
1387
|
-
try {
|
|
1388
|
-
const response = await fetch(
|
|
1389
|
-
`/api/v1/workspaces/${workspaceId}/calendar/events/${item.id}`,
|
|
1390
|
-
{
|
|
1391
|
-
method: 'PUT',
|
|
1392
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1393
|
-
body: JSON.stringify(item.data),
|
|
1394
|
-
}
|
|
1395
|
-
);
|
|
1102
|
+
// Modal management
|
|
1103
|
+
const openEventEditor = useCallback(
|
|
1104
|
+
(eventId?: string, options?: { defaultNewEventTab?: 'manual' | 'ai' }) => {
|
|
1105
|
+
setPreviewEventId(null);
|
|
1396
1106
|
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
}
|
|
1403
|
-
} catch (_) {
|
|
1404
|
-
// Failed to update event
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1107
|
+
if (eventId) {
|
|
1108
|
+
setActiveEventId(eventId);
|
|
1109
|
+
setPendingNewEvent(null);
|
|
1110
|
+
setModalHidden(false);
|
|
1111
|
+
return;
|
|
1408
1112
|
}
|
|
1409
1113
|
|
|
1410
|
-
|
|
1411
|
-
if (eventsToInsert.length > 0) {
|
|
1412
|
-
// Report progress for insert phase
|
|
1413
|
-
if (progressCallback) {
|
|
1414
|
-
progressCallback({
|
|
1415
|
-
phase: 'insert',
|
|
1416
|
-
current: 0,
|
|
1417
|
-
total: eventsToInsert.length,
|
|
1418
|
-
changesMade: changesMade,
|
|
1419
|
-
});
|
|
1420
|
-
}
|
|
1114
|
+
setDefaultNewEventTab(options?.defaultNewEventTab ?? 'manual');
|
|
1421
1115
|
|
|
1422
|
-
|
|
1116
|
+
const now = roundToNearest15Minutes(new Date());
|
|
1117
|
+
const oneHourLater = new Date(now);
|
|
1118
|
+
oneHourLater.setHours(oneHourLater.getHours() + 1);
|
|
1423
1119
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
total: eventsToInsert.length,
|
|
1433
|
-
changesMade: changesMade,
|
|
1434
|
-
});
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
try {
|
|
1438
|
-
await Promise.all(
|
|
1439
|
-
batch.map(async (event) => {
|
|
1440
|
-
const response = await fetch(
|
|
1441
|
-
`/api/v1/workspaces/${workspaceId}/calendar/events`,
|
|
1442
|
-
{
|
|
1443
|
-
method: 'POST',
|
|
1444
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1445
|
-
body: JSON.stringify(event),
|
|
1446
|
-
}
|
|
1447
|
-
);
|
|
1448
|
-
|
|
1449
|
-
if (!response.ok) {
|
|
1450
|
-
const errorData = await response.json().catch(() => null);
|
|
1451
|
-
throw new Error(
|
|
1452
|
-
errorData?.error || 'Failed to insert calendar event'
|
|
1453
|
-
);
|
|
1454
|
-
}
|
|
1455
|
-
})
|
|
1456
|
-
);
|
|
1457
|
-
} catch (_) {
|
|
1458
|
-
// Failed to insert events batch
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
// Report completion
|
|
1464
|
-
if (progressCallback) {
|
|
1465
|
-
progressCallback({
|
|
1466
|
-
phase: 'complete',
|
|
1467
|
-
current: 1,
|
|
1468
|
-
total: 1,
|
|
1469
|
-
changesMade: changesMade,
|
|
1470
|
-
});
|
|
1471
|
-
}
|
|
1120
|
+
const newEvent: Omit<CalendarEvent, 'id'> = {
|
|
1121
|
+
title: '',
|
|
1122
|
+
description: '',
|
|
1123
|
+
start_at: now.toISOString(),
|
|
1124
|
+
end_at: oneHourLater.toISOString(),
|
|
1125
|
+
color: 'BLUE',
|
|
1126
|
+
ws_id: ws?.id || '',
|
|
1127
|
+
};
|
|
1472
1128
|
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
}
|
|
1129
|
+
setPendingNewEvent(newEvent);
|
|
1130
|
+
setActiveEventId('new');
|
|
1131
|
+
setModalHidden(false);
|
|
1477
1132
|
},
|
|
1478
|
-
[
|
|
1133
|
+
[ws?.id]
|
|
1479
1134
|
);
|
|
1480
1135
|
|
|
1481
|
-
// Modal management
|
|
1482
1136
|
const openModal = useCallback(
|
|
1483
1137
|
(
|
|
1484
1138
|
eventId?: string,
|
|
@@ -1486,32 +1140,15 @@ export const CalendarProvider = ({
|
|
|
1486
1140
|
options?: { defaultNewEventTab?: 'manual' | 'ai' }
|
|
1487
1141
|
) => {
|
|
1488
1142
|
if (eventId) {
|
|
1489
|
-
// Opening an existing event
|
|
1490
|
-
setActiveEventId(eventId);
|
|
1491
1143
|
setPendingNewEvent(null);
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
const now = roundToNearest15Minutes(new Date());
|
|
1497
|
-
const oneHourLater = new Date(now);
|
|
1498
|
-
oneHourLater.setHours(oneHourLater.getHours() + 1);
|
|
1499
|
-
|
|
1500
|
-
// Create a pending new event
|
|
1501
|
-
const newEvent: Omit<CalendarEvent, 'id'> = {
|
|
1502
|
-
title: '',
|
|
1503
|
-
description: '',
|
|
1504
|
-
start_at: now.toISOString(),
|
|
1505
|
-
end_at: oneHourLater.toISOString(),
|
|
1506
|
-
color: 'BLUE',
|
|
1507
|
-
};
|
|
1508
|
-
|
|
1509
|
-
setPendingNewEvent(newEvent);
|
|
1510
|
-
setActiveEventId('new');
|
|
1144
|
+
setActiveEventId(null);
|
|
1145
|
+
setPreviewEventId(eventId);
|
|
1146
|
+
return;
|
|
1511
1147
|
}
|
|
1512
|
-
|
|
1148
|
+
|
|
1149
|
+
openEventEditor(undefined, options);
|
|
1513
1150
|
},
|
|
1514
|
-
[]
|
|
1151
|
+
[openEventEditor]
|
|
1515
1152
|
);
|
|
1516
1153
|
|
|
1517
1154
|
const closeModal = useCallback(() => {
|
|
@@ -1519,6 +1156,10 @@ export const CalendarProvider = ({
|
|
|
1519
1156
|
setPendingNewEvent(null);
|
|
1520
1157
|
}, []);
|
|
1521
1158
|
|
|
1159
|
+
const closePreview = useCallback(() => {
|
|
1160
|
+
setPreviewEventId(null);
|
|
1161
|
+
}, []);
|
|
1162
|
+
|
|
1522
1163
|
const activeEvent = useMemo(() => {
|
|
1523
1164
|
// If it's a pending new event
|
|
1524
1165
|
if (pendingNewEvent && activeEventId === 'new') {
|
|
@@ -1534,6 +1175,12 @@ export const CalendarProvider = ({
|
|
|
1534
1175
|
: undefined;
|
|
1535
1176
|
}, [activeEventId, events, pendingNewEvent]);
|
|
1536
1177
|
|
|
1178
|
+
const previewEvent = useMemo(() => {
|
|
1179
|
+
return previewEventId
|
|
1180
|
+
? events.find((e: Partial<CalendarEvent>) => e.id === previewEventId)
|
|
1181
|
+
: undefined;
|
|
1182
|
+
}, [events, previewEventId]);
|
|
1183
|
+
|
|
1537
1184
|
const isEditing = useCallback(() => !!activeEventId, [activeEventId]);
|
|
1538
1185
|
const hideModal = useCallback(() => setModalHidden(true), []);
|
|
1539
1186
|
const showModal = useCallback(() => setModalHidden(false), []);
|
|
@@ -1565,137 +1212,64 @@ export const CalendarProvider = ({
|
|
|
1565
1212
|
statusMessage?: string;
|
|
1566
1213
|
}) => void
|
|
1567
1214
|
) => {
|
|
1568
|
-
if (!
|
|
1215
|
+
if (!ws?.id) {
|
|
1569
1216
|
return false;
|
|
1570
1217
|
}
|
|
1571
1218
|
|
|
1572
1219
|
try {
|
|
1573
|
-
// First, capture current events count for comparison
|
|
1574
|
-
const beforeCount = events.length;
|
|
1575
|
-
const beforeGoogleCount = googleEvents.length;
|
|
1576
|
-
|
|
1577
|
-
// Report fetch starting
|
|
1578
1220
|
if (progressCallback) {
|
|
1579
1221
|
progressCallback({
|
|
1580
1222
|
phase: 'fetch',
|
|
1581
1223
|
current: 0,
|
|
1582
1224
|
total: 1,
|
|
1583
1225
|
changesMade: false,
|
|
1584
|
-
statusMessage: '
|
|
1226
|
+
statusMessage: 'Syncing connected calendars...',
|
|
1585
1227
|
});
|
|
1586
1228
|
}
|
|
1587
1229
|
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
if (!response.ok) {
|
|
1596
|
-
// Include the full error details from Google API
|
|
1597
|
-
const errorDetails = data.error || response.statusText;
|
|
1598
|
-
const statusCode = response.status;
|
|
1599
|
-
const googleError = data.googleError || data.details || '';
|
|
1600
|
-
throw new Error(
|
|
1601
|
-
`Failed to fetch Google Calendar events: ${errorDetails} (${statusCode})${googleError ? ` - Google API: ${googleError}` : ''}`
|
|
1602
|
-
);
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
// Update googleEvents directly through queryClient for faster UI response
|
|
1606
|
-
queryClient.setQueryData(['googleCalendarEvents', ws?.id], data);
|
|
1607
|
-
|
|
1608
|
-
// Report fetch complete
|
|
1609
|
-
if (progressCallback) {
|
|
1610
|
-
progressCallback({
|
|
1611
|
-
phase: 'fetch',
|
|
1612
|
-
current: 1,
|
|
1613
|
-
total: 1,
|
|
1614
|
-
changesMade: data.events?.length !== beforeGoogleCount,
|
|
1615
|
-
statusMessage: `Fetched ${data.events?.length || 0} events from Google Calendar`,
|
|
1616
|
-
});
|
|
1617
|
-
}
|
|
1618
|
-
} catch (fetchError) {
|
|
1619
|
-
if (progressCallback) {
|
|
1620
|
-
progressCallback({
|
|
1621
|
-
phase: 'fetch',
|
|
1622
|
-
current: 0,
|
|
1623
|
-
total: 1,
|
|
1624
|
-
changesMade: false,
|
|
1625
|
-
statusMessage:
|
|
1626
|
-
fetchError instanceof Error
|
|
1627
|
-
? fetchError.message
|
|
1628
|
-
: 'Failed to fetch events from Google Calendar',
|
|
1629
|
-
});
|
|
1230
|
+
const response = await fetch(
|
|
1231
|
+
`/api/v1/workspaces/${ws.id}/calendar/sync`,
|
|
1232
|
+
{
|
|
1233
|
+
method: 'POST',
|
|
1234
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1235
|
+
body: JSON.stringify({ direction: 'inbound', source: 'manual' }),
|
|
1630
1236
|
}
|
|
1237
|
+
);
|
|
1631
1238
|
|
|
1632
|
-
|
|
1633
|
-
|
|
1239
|
+
const result = await response.json().catch(() => null);
|
|
1240
|
+
if (!response.ok) {
|
|
1241
|
+
throw new Error(result?.error || 'Calendar sync failed');
|
|
1634
1242
|
}
|
|
1635
1243
|
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
: progress.phase === 'complete'
|
|
1651
|
-
? 'Sync completed'
|
|
1652
|
-
: undefined,
|
|
1653
|
-
});
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
// Track if any changes were made
|
|
1657
|
-
if (progress.changesMade) {
|
|
1658
|
-
changesMade = true;
|
|
1659
|
-
}
|
|
1244
|
+
const inserted =
|
|
1245
|
+
(result?.summary?.google?.inserted ?? 0) +
|
|
1246
|
+
(result?.summary?.microsoft?.inserted ?? 0);
|
|
1247
|
+
const updated =
|
|
1248
|
+
(result?.summary?.google?.updated ?? 0) +
|
|
1249
|
+
(result?.summary?.microsoft?.updated ?? 0);
|
|
1250
|
+
const deleted =
|
|
1251
|
+
(result?.summary?.google?.deleted ?? 0) +
|
|
1252
|
+
(result?.summary?.microsoft?.deleted ?? 0);
|
|
1253
|
+
const changesMade = inserted + updated + deleted > 0;
|
|
1254
|
+
|
|
1255
|
+
await queryClient.invalidateQueries({
|
|
1256
|
+
queryKey: ['databaseCalendarEvents', ws.id],
|
|
1257
|
+
exact: false,
|
|
1660
1258
|
});
|
|
1661
1259
|
|
|
1662
|
-
// Force refresh of local events
|
|
1663
|
-
if (changesMade) {
|
|
1664
|
-
if (progressCallback) {
|
|
1665
|
-
progressCallback({
|
|
1666
|
-
phase: 'complete',
|
|
1667
|
-
current: 1,
|
|
1668
|
-
total: 1,
|
|
1669
|
-
changesMade: true,
|
|
1670
|
-
statusMessage: 'Refreshing your calendar...',
|
|
1671
|
-
});
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
await queryClient.invalidateQueries({
|
|
1675
|
-
queryKey: ['calendarEvents', ws?.id],
|
|
1676
|
-
refetchType: 'all',
|
|
1677
|
-
});
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
// Calculate changes
|
|
1681
|
-
const afterCount = getEvents().length;
|
|
1682
|
-
const countDifference = afterCount - beforeCount;
|
|
1683
|
-
|
|
1684
|
-
// Final callback with summary
|
|
1685
1260
|
if (progressCallback) {
|
|
1686
1261
|
progressCallback({
|
|
1687
1262
|
phase: 'complete',
|
|
1688
1263
|
current: 1,
|
|
1689
1264
|
total: 1,
|
|
1690
|
-
changesMade
|
|
1265
|
+
changesMade,
|
|
1691
1266
|
statusMessage: changesMade
|
|
1692
|
-
? `Sync complete. ${
|
|
1267
|
+
? `Sync complete. ${inserted} added, ${updated} updated, ${deleted} removed.`
|
|
1693
1268
|
: 'Sync complete. No changes needed.',
|
|
1694
1269
|
});
|
|
1695
1270
|
}
|
|
1696
1271
|
|
|
1697
|
-
|
|
1698
|
-
return changesMade || beforeGoogleCount !== googleEvents.length;
|
|
1272
|
+
return changesMade;
|
|
1699
1273
|
} catch (_) {
|
|
1700
1274
|
if (progressCallback) {
|
|
1701
1275
|
progressCallback({
|
|
@@ -1710,15 +1284,7 @@ export const CalendarProvider = ({
|
|
|
1710
1284
|
return false;
|
|
1711
1285
|
}
|
|
1712
1286
|
},
|
|
1713
|
-
[
|
|
1714
|
-
experimentalGoogleToken,
|
|
1715
|
-
ws?.id,
|
|
1716
|
-
events.length,
|
|
1717
|
-
googleEvents.length,
|
|
1718
|
-
queryClient,
|
|
1719
|
-
syncEvents,
|
|
1720
|
-
getEvents,
|
|
1721
|
-
]
|
|
1287
|
+
[queryClient, ws?.id]
|
|
1722
1288
|
);
|
|
1723
1289
|
|
|
1724
1290
|
const [isDragging, setIsDragging] = useState(false);
|
|
@@ -1905,7 +1471,11 @@ export const CalendarProvider = ({
|
|
|
1905
1471
|
// New API
|
|
1906
1472
|
isModalOpen: !isModalHidden && activeEventId !== null,
|
|
1907
1473
|
activeEvent,
|
|
1474
|
+
isPreviewOpen: !!previewEvent,
|
|
1475
|
+
previewEvent,
|
|
1908
1476
|
openModal,
|
|
1477
|
+
openEventEditor,
|
|
1478
|
+
closePreview,
|
|
1909
1479
|
closeModal,
|
|
1910
1480
|
|
|
1911
1481
|
// Legacy API
|