@tuturuuu/ui 0.1.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 +71 -0
- package/package.json +82 -70
- 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/chat/chat-agent-details-external-thread-panel.test.tsx +43 -13
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +138 -74
- package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +70 -0
- package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +60 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.tsx +13 -5
- package/src/components/ui/chat/chat-sidebar-panel.test.tsx +110 -0
- package/src/components/ui/chat/chat-sidebar-panel.tsx +13 -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/legacy/meet/planId/page.tsx +10 -4
- package/src/components/ui/text-editor/__tests__/task-mention-chip.test.tsx +203 -6
- package/src/components/ui/text-editor/task-mention-chip.tsx +29 -7
- 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/__tests__/use-task-realtime-sync.test.tsx +37 -9
- 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-realtime-sync.ts +89 -70
- 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
|
@@ -1081,6 +1081,45 @@ describe('useTaskActions', () => {
|
|
|
1081
1081
|
priority: 'high',
|
|
1082
1082
|
});
|
|
1083
1083
|
});
|
|
1084
|
+
|
|
1085
|
+
it('keeps priority updates single-target when the context task is not selected', async () => {
|
|
1086
|
+
const task2: Task = { ...mockTask, id: 'task-2', name: 'Task 2' };
|
|
1087
|
+
const unselectedContextTask: Task = {
|
|
1088
|
+
...mockTask,
|
|
1089
|
+
id: 'task-3',
|
|
1090
|
+
name: 'Task 3',
|
|
1091
|
+
};
|
|
1092
|
+
queryClient.setQueryData(
|
|
1093
|
+
['tasks', 'board-1'],
|
|
1094
|
+
[mockTask, task2, unselectedContextTask]
|
|
1095
|
+
);
|
|
1096
|
+
|
|
1097
|
+
const { result } = renderHook(
|
|
1098
|
+
() =>
|
|
1099
|
+
useTaskActions({
|
|
1100
|
+
task: unselectedContextTask,
|
|
1101
|
+
boardId: 'board-1',
|
|
1102
|
+
targetCompletionList: mockCompletionList,
|
|
1103
|
+
targetClosedList: mockClosedList,
|
|
1104
|
+
availableLists: mockAvailableLists,
|
|
1105
|
+
onUpdate: vi.fn(),
|
|
1106
|
+
setIsLoading: vi.fn(),
|
|
1107
|
+
setMenuOpen: vi.fn(),
|
|
1108
|
+
selectedTasks: new Set(['task-1', 'task-2']),
|
|
1109
|
+
isMultiSelectMode: true,
|
|
1110
|
+
}),
|
|
1111
|
+
{ wrapper }
|
|
1112
|
+
);
|
|
1113
|
+
|
|
1114
|
+
await act(async () => {
|
|
1115
|
+
await result.current.handlePriorityChange('high');
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
expect(mockUpdateWorkspaceTask).toHaveBeenCalledTimes(1);
|
|
1119
|
+
expect(mockUpdateWorkspaceTask).toHaveBeenCalledWith('ws-1', 'task-3', {
|
|
1120
|
+
priority: 'high',
|
|
1121
|
+
});
|
|
1122
|
+
});
|
|
1084
1123
|
});
|
|
1085
1124
|
|
|
1086
1125
|
describe('updateEstimationPoints', () => {
|
|
@@ -1276,6 +1315,85 @@ describe('useTaskActions', () => {
|
|
|
1276
1315
|
end_date: expect.any(String),
|
|
1277
1316
|
});
|
|
1278
1317
|
});
|
|
1318
|
+
|
|
1319
|
+
it('delegates selected-card custom dates to the bulk updater', async () => {
|
|
1320
|
+
const task2: Task = { ...mockTask, id: 'task-2', name: 'Task 2' };
|
|
1321
|
+
queryClient.setQueryData(['tasks', 'board-1'], [mockTask, task2]);
|
|
1322
|
+
|
|
1323
|
+
const bulkUpdateCustomDueDate = vi.fn().mockResolvedValue(undefined);
|
|
1324
|
+
const testDate = new Date('2024-01-01T00:00:00');
|
|
1325
|
+
|
|
1326
|
+
const { result } = renderHook(
|
|
1327
|
+
() =>
|
|
1328
|
+
useTaskActions({
|
|
1329
|
+
task: mockTask,
|
|
1330
|
+
boardId: 'board-1',
|
|
1331
|
+
targetCompletionList: mockCompletionList,
|
|
1332
|
+
targetClosedList: mockClosedList,
|
|
1333
|
+
availableLists: mockAvailableLists,
|
|
1334
|
+
onUpdate: vi.fn(),
|
|
1335
|
+
setIsLoading: vi.fn(),
|
|
1336
|
+
setMenuOpen: vi.fn(),
|
|
1337
|
+
setCustomDateDialogOpen: vi.fn(),
|
|
1338
|
+
selectedTasks: new Set(['task-1', 'task-2']),
|
|
1339
|
+
isMultiSelectMode: true,
|
|
1340
|
+
bulkUpdateCustomDueDate,
|
|
1341
|
+
}),
|
|
1342
|
+
{ wrapper }
|
|
1343
|
+
);
|
|
1344
|
+
|
|
1345
|
+
await act(async () => {
|
|
1346
|
+
await result.current.handleCustomDateChange(testDate);
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
expect(bulkUpdateCustomDueDate).toHaveBeenCalledWith(testDate);
|
|
1350
|
+
expect(mockUpdateWorkspaceTask).not.toHaveBeenCalled();
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
it('keeps custom date updates single-target when the context task is not selected', async () => {
|
|
1354
|
+
const task2: Task = { ...mockTask, id: 'task-2', name: 'Task 2' };
|
|
1355
|
+
const unselectedContextTask: Task = {
|
|
1356
|
+
...mockTask,
|
|
1357
|
+
id: 'task-3',
|
|
1358
|
+
name: 'Task 3',
|
|
1359
|
+
};
|
|
1360
|
+
queryClient.setQueryData(
|
|
1361
|
+
['tasks', 'board-1'],
|
|
1362
|
+
[mockTask, task2, unselectedContextTask]
|
|
1363
|
+
);
|
|
1364
|
+
|
|
1365
|
+
const bulkUpdateCustomDueDate = vi.fn().mockResolvedValue(undefined);
|
|
1366
|
+
const testDate = new Date('2024-01-01T00:00:00');
|
|
1367
|
+
|
|
1368
|
+
const { result } = renderHook(
|
|
1369
|
+
() =>
|
|
1370
|
+
useTaskActions({
|
|
1371
|
+
task: unselectedContextTask,
|
|
1372
|
+
boardId: 'board-1',
|
|
1373
|
+
targetCompletionList: mockCompletionList,
|
|
1374
|
+
targetClosedList: mockClosedList,
|
|
1375
|
+
availableLists: mockAvailableLists,
|
|
1376
|
+
onUpdate: vi.fn(),
|
|
1377
|
+
setIsLoading: vi.fn(),
|
|
1378
|
+
setMenuOpen: vi.fn(),
|
|
1379
|
+
setCustomDateDialogOpen: vi.fn(),
|
|
1380
|
+
selectedTasks: new Set(['task-1', 'task-2']),
|
|
1381
|
+
isMultiSelectMode: true,
|
|
1382
|
+
bulkUpdateCustomDueDate,
|
|
1383
|
+
}),
|
|
1384
|
+
{ wrapper }
|
|
1385
|
+
);
|
|
1386
|
+
|
|
1387
|
+
await act(async () => {
|
|
1388
|
+
await result.current.handleCustomDateChange(testDate);
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
expect(bulkUpdateCustomDueDate).not.toHaveBeenCalled();
|
|
1392
|
+
expect(mockUpdateWorkspaceTask).toHaveBeenCalledTimes(1);
|
|
1393
|
+
expect(mockUpdateWorkspaceTask).toHaveBeenCalledWith('ws-1', 'task-3', {
|
|
1394
|
+
end_date: expect.any(String),
|
|
1395
|
+
});
|
|
1396
|
+
});
|
|
1279
1397
|
});
|
|
1280
1398
|
|
|
1281
1399
|
describe('handleToggleAssignee', () => {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
2
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { useUserBooleanConfig } from '../use-user-config';
|
|
6
|
+
|
|
7
|
+
const { mockGetUserConfig, mockUpdateUserConfig } = vi.hoisted(() => ({
|
|
8
|
+
mockGetUserConfig: vi.fn(),
|
|
9
|
+
mockUpdateUserConfig: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('@tuturuuu/internal-api/users', () => ({
|
|
13
|
+
getUserConfig: (...args: unknown[]) => mockGetUserConfig(...args),
|
|
14
|
+
updateUserConfig: (...args: unknown[]) => mockUpdateUserConfig(...args),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
function createWrapper() {
|
|
18
|
+
const queryClient = new QueryClient({
|
|
19
|
+
defaultOptions: {
|
|
20
|
+
queries: {
|
|
21
|
+
retry: false,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
27
|
+
return (
|
|
28
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('useUserBooleanConfig', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.clearAllMocks();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('uses the requested boolean default while the config is loading', () => {
|
|
39
|
+
mockGetUserConfig.mockReturnValue(new Promise(() => {}));
|
|
40
|
+
|
|
41
|
+
const { result } = renderHook(
|
|
42
|
+
() => useUserBooleanConfig('EXPAND_SETTINGS_ACCORDIONS', true),
|
|
43
|
+
{ wrapper: createWrapper() }
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(result.current.isLoading).toBe(true);
|
|
47
|
+
expect(result.current.value).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('lets a saved false value override a default true value after loading', async () => {
|
|
51
|
+
mockGetUserConfig.mockResolvedValue({ value: 'false' });
|
|
52
|
+
|
|
53
|
+
const { result } = renderHook(
|
|
54
|
+
() => useUserBooleanConfig('EXPAND_SETTINGS_ACCORDIONS', true),
|
|
55
|
+
{ wrapper: createWrapper() }
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(result.current.value).toBe(true);
|
|
59
|
+
|
|
60
|
+
await waitFor(() => {
|
|
61
|
+
expect(result.current.isLoading).toBe(false);
|
|
62
|
+
expect(result.current.value).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -28,6 +28,10 @@ type CalendarConnection = {
|
|
|
28
28
|
calendar_name: string;
|
|
29
29
|
is_enabled: boolean;
|
|
30
30
|
color: string | null;
|
|
31
|
+
provider?: 'google' | 'microsoft' | string;
|
|
32
|
+
auth_token_id?: string | null;
|
|
33
|
+
workspace_calendar_id?: string | null;
|
|
34
|
+
access_role?: string | null;
|
|
31
35
|
created_at: string;
|
|
32
36
|
updated_at: string;
|
|
33
37
|
};
|
|
@@ -161,7 +165,7 @@ type CacheUpdate = {
|
|
|
161
165
|
export const CalendarSyncProvider = ({
|
|
162
166
|
children,
|
|
163
167
|
wsId,
|
|
164
|
-
experimentalGoogleToken,
|
|
168
|
+
experimentalGoogleToken: _experimentalGoogleToken,
|
|
165
169
|
initialCalendarConnections = [],
|
|
166
170
|
}: {
|
|
167
171
|
children: React.ReactNode;
|
|
@@ -170,9 +174,7 @@ export const CalendarSyncProvider = ({
|
|
|
170
174
|
initialCalendarConnections?: CalendarConnection[];
|
|
171
175
|
}) => {
|
|
172
176
|
const [data, setData] = useState<WorkspaceCalendarEvent[] | null>(null);
|
|
173
|
-
const [googleData
|
|
174
|
-
null
|
|
175
|
-
);
|
|
177
|
+
const [googleData] = useState<WorkspaceCalendarEvent[] | null>(null);
|
|
176
178
|
const [events, setEvents] = useState<CalendarEventWithHabitInfo[]>([]);
|
|
177
179
|
|
|
178
180
|
const [error, setError] = useState<Error | null>(null);
|
|
@@ -184,11 +186,9 @@ export const CalendarSyncProvider = ({
|
|
|
184
186
|
const [calendarCache, setCalendarCache] = useState<CalendarCache>({});
|
|
185
187
|
const [isSyncing, setIsSyncing] = useState(false);
|
|
186
188
|
const [syncStatus, setSyncStatus] = useState<SyncStatus>({ state: 'idle' });
|
|
187
|
-
const prevGoogleDataRef = useRef<string>('');
|
|
188
189
|
const prevDatesRef = useRef<string>('');
|
|
189
190
|
const isForcedRef = useRef<boolean>(false);
|
|
190
191
|
const lastSyncTimeRef = useRef<number>(0);
|
|
191
|
-
const syncDebounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
192
192
|
const queryClient = useQueryClient();
|
|
193
193
|
|
|
194
194
|
// Calendar connections state
|
|
@@ -449,74 +449,14 @@ export const CalendarSyncProvider = ({
|
|
|
449
449
|
refetchInterval: 60000, // Reduced from 30s to 60s to lower load
|
|
450
450
|
});
|
|
451
451
|
|
|
452
|
-
//
|
|
453
|
-
|
|
452
|
+
// Legacy direct Google fetch/reconcile is disabled. Provider inbound sync is
|
|
453
|
+
// owned by the workspace sync route so account/calendar identity stays scoped.
|
|
454
|
+
const { isLoading: isGoogleLoading } = useQuery({
|
|
454
455
|
queryKey: ['googleCalendarEvents', wsId, getCacheKey(dates)],
|
|
455
|
-
enabled:
|
|
456
|
-
!!wsId && experimentalGoogleToken?.ws_id === wsId && dates.length > 0,
|
|
456
|
+
enabled: false,
|
|
457
457
|
staleTime: 30000, // Consider data fresh for 30 seconds
|
|
458
458
|
gcTime: 5 * 60 * 1000, // Keep in cache for 5 minutes
|
|
459
|
-
queryFn: async () =>
|
|
460
|
-
const cacheKey = getCacheKey(dates);
|
|
461
|
-
if (!cacheKey) return null;
|
|
462
|
-
|
|
463
|
-
const cachedData = calendarCache[cacheKey];
|
|
464
|
-
|
|
465
|
-
// If we have cached data and it's not stale, return it immediately
|
|
466
|
-
if (
|
|
467
|
-
cachedData?.googleEvents &&
|
|
468
|
-
cachedData.googleEvents.length > 0 &&
|
|
469
|
-
!isCacheStaleEnhanced(cachedData.googleLastUpdated, dates)
|
|
470
|
-
) {
|
|
471
|
-
setGoogleData(cachedData.googleEvents);
|
|
472
|
-
return cachedData.googleEvents;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Otherwise fetch fresh data
|
|
476
|
-
const startDate = dayjs(dates[0]).startOf('day');
|
|
477
|
-
const endDate = dayjs(dates[dates.length - 1]).endOf('day');
|
|
478
|
-
|
|
479
|
-
const response = await fetch(
|
|
480
|
-
`/api/v1/calendar/auth/fetch?wsId=${wsId}&startDate=${startDate.toISOString()}&endDate=${endDate.toISOString()}`,
|
|
481
|
-
{ cache: 'no-store' }
|
|
482
|
-
);
|
|
483
|
-
const googleResponse = await response.json();
|
|
484
|
-
|
|
485
|
-
if (!response.ok) {
|
|
486
|
-
const errorMessage =
|
|
487
|
-
googleResponse.error +
|
|
488
|
-
'. ' +
|
|
489
|
-
googleResponse.googleError +
|
|
490
|
-
': ' +
|
|
491
|
-
googleResponse.details?.reason;
|
|
492
|
-
setError(new Error(errorMessage));
|
|
493
|
-
|
|
494
|
-
// Notify user of Google Calendar fetch failure
|
|
495
|
-
toast.error('Failed to fetch Google Calendar', {
|
|
496
|
-
description:
|
|
497
|
-
errorMessage || 'Could not retrieve events from Google Calendar',
|
|
498
|
-
duration: 5000,
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
setSyncStatus({
|
|
502
|
-
state: 'error',
|
|
503
|
-
message: 'failed_to_fetch_google', // Translation key
|
|
504
|
-
lastSyncTime: new Date(),
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
return null;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Update cache with new data
|
|
511
|
-
updateCache(cacheKey, {
|
|
512
|
-
googleEvents: googleResponse.events,
|
|
513
|
-
googleLastUpdated: Date.now(),
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
setGoogleData(googleResponse.events);
|
|
517
|
-
setError(null);
|
|
518
|
-
return googleResponse.events;
|
|
519
|
-
},
|
|
459
|
+
queryFn: async () => null,
|
|
520
460
|
refetchInterval: 60000, // Reduced from 30s to 60s to lower load
|
|
521
461
|
});
|
|
522
462
|
|
|
@@ -708,59 +648,6 @@ export const CalendarSyncProvider = ({
|
|
|
708
648
|
[wsId, isActiveSyncOn, refresh]
|
|
709
649
|
);
|
|
710
650
|
|
|
711
|
-
// Sync to Tuturuuu database when google data changes for current view (debounced)
|
|
712
|
-
useEffect(() => {
|
|
713
|
-
// If have not connected to google, don't sync
|
|
714
|
-
if (experimentalGoogleToken?.ws_id !== wsId) {
|
|
715
|
-
return;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// Convert current data to strings for comparison
|
|
719
|
-
const currentGoogleDataStr = JSON.stringify(fetchedGoogleData);
|
|
720
|
-
|
|
721
|
-
// Only sync if the data has actually changed
|
|
722
|
-
const hasDataChanged = currentGoogleDataStr !== prevGoogleDataRef.current;
|
|
723
|
-
|
|
724
|
-
if (hasDataChanged) {
|
|
725
|
-
// Clear any pending sync
|
|
726
|
-
if (syncDebounceTimerRef.current) {
|
|
727
|
-
clearTimeout(syncDebounceTimerRef.current);
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// Debounce sync by 2 seconds to prevent rapid consecutive syncs
|
|
731
|
-
syncDebounceTimerRef.current = setTimeout(() => {
|
|
732
|
-
syncToTuturuuu();
|
|
733
|
-
prevGoogleDataRef.current = currentGoogleDataStr;
|
|
734
|
-
}, 2000);
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// Cleanup on unmount
|
|
738
|
-
return () => {
|
|
739
|
-
if (syncDebounceTimerRef.current) {
|
|
740
|
-
clearTimeout(syncDebounceTimerRef.current);
|
|
741
|
-
}
|
|
742
|
-
};
|
|
743
|
-
}, [fetchedGoogleData, syncToTuturuuu, wsId, experimentalGoogleToken?.ws_id]);
|
|
744
|
-
|
|
745
|
-
// Trigger sync when isActiveSyncOn becomes true
|
|
746
|
-
useEffect(() => {
|
|
747
|
-
// If have not connected to google, don't sync
|
|
748
|
-
if (experimentalGoogleToken?.ws_id !== wsId) {
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// Only sync when isActiveSyncOn becomes true and we have Google data
|
|
753
|
-
if (isActiveSyncOn && fetchedGoogleData && fetchedGoogleData.length > 0) {
|
|
754
|
-
syncToTuturuuu();
|
|
755
|
-
}
|
|
756
|
-
}, [
|
|
757
|
-
isActiveSyncOn,
|
|
758
|
-
fetchedGoogleData,
|
|
759
|
-
syncToTuturuuu,
|
|
760
|
-
wsId,
|
|
761
|
-
experimentalGoogleToken?.ws_id,
|
|
762
|
-
]);
|
|
763
|
-
|
|
764
651
|
// Trigger refetch from DB when changing views (optimized to reduce load)
|
|
765
652
|
useEffect(() => {
|
|
766
653
|
// Skip if dates haven't actually changed
|
|
@@ -899,15 +786,15 @@ export const CalendarSyncProvider = ({
|
|
|
899
786
|
visibleDatabaseEvents as CalendarEvent[]
|
|
900
787
|
);
|
|
901
788
|
|
|
902
|
-
// Filter events by enabled calendars
|
|
903
|
-
//
|
|
904
|
-
//
|
|
789
|
+
// Filter external events by enabled provider calendars. Local Tuturuuu
|
|
790
|
+
// events are always shown here; native calendar visibility is handled
|
|
791
|
+
// by the workspace calendar endpoints.
|
|
905
792
|
const filteredEvents =
|
|
906
793
|
calendarConnections.length > 0
|
|
907
794
|
? result.filter((event) => {
|
|
908
|
-
const eventCalendarId =
|
|
909
|
-
|
|
910
|
-
|
|
795
|
+
const eventCalendarId =
|
|
796
|
+
(event as any).external_calendar_id ||
|
|
797
|
+
(event as any).google_calendar_id;
|
|
911
798
|
return (
|
|
912
799
|
!eventCalendarId || enabledCalendarIds.has(eventCalendarId)
|
|
913
800
|
);
|
|
@@ -978,156 +865,14 @@ export const CalendarSyncProvider = ({
|
|
|
978
865
|
}, []);
|
|
979
866
|
|
|
980
867
|
const syncToGoogle = useCallback(async () => {
|
|
981
|
-
|
|
982
|
-
const logDebug = (
|
|
983
|
-
type: 'info' | 'success' | 'warning' | 'error',
|
|
984
|
-
message: string,
|
|
985
|
-
details?: any
|
|
986
|
-
) => {
|
|
987
|
-
if (typeof window !== 'undefined') {
|
|
988
|
-
window.dispatchEvent(
|
|
989
|
-
new CustomEvent('calendar-debug-log', {
|
|
990
|
-
detail: {
|
|
991
|
-
id: `${Date.now()}-${Math.random()}`,
|
|
992
|
-
timestamp: new Date(),
|
|
993
|
-
type,
|
|
994
|
-
message,
|
|
995
|
-
details,
|
|
996
|
-
},
|
|
997
|
-
})
|
|
998
|
-
);
|
|
999
|
-
}
|
|
1000
|
-
console.log(
|
|
1001
|
-
`[SYNC TO GOOGLE ${type.toUpperCase()}]`,
|
|
1002
|
-
message,
|
|
1003
|
-
details || ''
|
|
1004
|
-
);
|
|
1005
|
-
};
|
|
1006
|
-
|
|
1007
|
-
logDebug('info', '🚀 Starting sync to Google Calendar', {
|
|
1008
|
-
wsId,
|
|
1009
|
-
hasGoogleToken: !!experimentalGoogleToken,
|
|
1010
|
-
dateRange: dates.map((d) => d.toISOString()),
|
|
1011
|
-
});
|
|
1012
|
-
|
|
1013
|
-
if (!experimentalGoogleToken || !wsId) {
|
|
1014
|
-
logDebug('error', 'Google Calendar not connected');
|
|
1015
|
-
toast.error('Google Calendar not connected', {
|
|
1016
|
-
description: 'Please connect your Google Calendar first',
|
|
1017
|
-
});
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
setIsSyncing(true);
|
|
868
|
+
toast.info('Provider events sync when you create or edit them.');
|
|
1022
869
|
setSyncStatus({
|
|
1023
|
-
state: '
|
|
1024
|
-
message: '
|
|
870
|
+
state: 'success',
|
|
871
|
+
message: 'provider_writes_on_save',
|
|
872
|
+
lastSyncTime: new Date(),
|
|
1025
873
|
direction: 'tuturuuu-to-google',
|
|
1026
874
|
});
|
|
1027
|
-
|
|
1028
|
-
try {
|
|
1029
|
-
const startDate = dayjs(dates[0]).startOf('day');
|
|
1030
|
-
const endDate = dayjs(dates[dates.length - 1]).endOf('day');
|
|
1031
|
-
|
|
1032
|
-
logDebug('info', '📅 Date range for sync', {
|
|
1033
|
-
startDate: startDate.toISOString(),
|
|
1034
|
-
endDate: endDate.toISOString(),
|
|
1035
|
-
});
|
|
1036
|
-
|
|
1037
|
-
logDebug('info', '🔍 Current events in memory', {
|
|
1038
|
-
totalEvents: events.length,
|
|
1039
|
-
eventsWithGoogleId: events.filter((e: any) => e.google_event_id).length,
|
|
1040
|
-
eventsWithoutGoogleId: events.filter((e: any) => !e.google_event_id)
|
|
1041
|
-
.length,
|
|
1042
|
-
});
|
|
1043
|
-
|
|
1044
|
-
const requestBody = {
|
|
1045
|
-
wsId,
|
|
1046
|
-
startDate: startDate.toISOString(),
|
|
1047
|
-
endDate: endDate.toISOString(),
|
|
1048
|
-
};
|
|
1049
|
-
|
|
1050
|
-
logDebug('info', '📤 Sending API request', requestBody);
|
|
1051
|
-
|
|
1052
|
-
const response = await fetch('/api/v1/calendar/auth/sync-to-google', {
|
|
1053
|
-
method: 'POST',
|
|
1054
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1055
|
-
body: JSON.stringify(requestBody),
|
|
1056
|
-
});
|
|
1057
|
-
|
|
1058
|
-
logDebug('info', '📥 API response received', {
|
|
1059
|
-
status: response.status,
|
|
1060
|
-
ok: response.ok,
|
|
1061
|
-
});
|
|
1062
|
-
|
|
1063
|
-
const result = await response.json();
|
|
1064
|
-
|
|
1065
|
-
logDebug('info', '📊 API response data', result);
|
|
1066
|
-
|
|
1067
|
-
if (!response.ok) {
|
|
1068
|
-
logDebug('error', 'API request failed', result);
|
|
1069
|
-
throw new Error(result.error || 'Failed to sync to Google Calendar');
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
// Update sync status
|
|
1073
|
-
setSyncStatus({
|
|
1074
|
-
state: 'success',
|
|
1075
|
-
message: `Synced ${result.syncedCount} event(s) to Google Calendar`,
|
|
1076
|
-
lastSyncTime: new Date(),
|
|
1077
|
-
direction: 'tuturuuu-to-google',
|
|
1078
|
-
});
|
|
1079
|
-
|
|
1080
|
-
logDebug(
|
|
1081
|
-
'success',
|
|
1082
|
-
`✅ Sync completed: ${result.syncedCount} events synced`,
|
|
1083
|
-
result
|
|
1084
|
-
);
|
|
1085
|
-
|
|
1086
|
-
// Show success notification
|
|
1087
|
-
toast.success('Synced to Google Calendar', {
|
|
1088
|
-
description: `${result.syncedCount} event(s) synced successfully`,
|
|
1089
|
-
});
|
|
1090
|
-
|
|
1091
|
-
// If there were errors, show them
|
|
1092
|
-
if (result.errorCount > 0 && result.errors) {
|
|
1093
|
-
logDebug(
|
|
1094
|
-
'warning',
|
|
1095
|
-
`⚠️ ${result.errorCount} events failed to sync`,
|
|
1096
|
-
result.errors
|
|
1097
|
-
);
|
|
1098
|
-
toast.warning('Some events failed to sync', {
|
|
1099
|
-
description: `${result.errorCount} event(s) failed. Check debug panel for details.`,
|
|
1100
|
-
duration: 7000,
|
|
1101
|
-
});
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
// Refresh to ensure we have the latest data
|
|
1105
|
-
logDebug('info', '🔄 Refreshing events from database');
|
|
1106
|
-
refresh();
|
|
1107
|
-
} catch (error) {
|
|
1108
|
-
const errorMessage =
|
|
1109
|
-
error instanceof Error
|
|
1110
|
-
? error.message
|
|
1111
|
-
: 'An unexpected error occurred while syncing to Google';
|
|
1112
|
-
|
|
1113
|
-
logDebug('error', '❌ Sync failed', { error, errorMessage });
|
|
1114
|
-
|
|
1115
|
-
setError(error instanceof Error ? error : new Error(errorMessage));
|
|
1116
|
-
setSyncStatus({
|
|
1117
|
-
state: 'error',
|
|
1118
|
-
message: errorMessage,
|
|
1119
|
-
lastSyncTime: new Date(),
|
|
1120
|
-
});
|
|
1121
|
-
|
|
1122
|
-
toast.error('Failed to sync to Google Calendar', {
|
|
1123
|
-
description: errorMessage,
|
|
1124
|
-
duration: 7000,
|
|
1125
|
-
});
|
|
1126
|
-
} finally {
|
|
1127
|
-
setIsSyncing(false);
|
|
1128
|
-
logDebug('info', '🏁 Sync operation completed');
|
|
1129
|
-
}
|
|
1130
|
-
}, [wsId, dates, experimentalGoogleToken, refresh, events]);
|
|
875
|
+
}, []);
|
|
1131
876
|
|
|
1132
877
|
const value = {
|
|
1133
878
|
data,
|