@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
|
@@ -53,10 +53,20 @@ function renderCalendarSync({
|
|
|
53
53
|
return renderHook(() => useCalendarSync(), { wrapper });
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function mockCalendarFetch(
|
|
56
|
+
function mockCalendarFetch(
|
|
57
|
+
events: CalendarEvent[],
|
|
58
|
+
calendars: Array<{ id: string; is_enabled: boolean }> = []
|
|
59
|
+
) {
|
|
57
60
|
return vi.fn(async (input: RequestInfo | URL) => {
|
|
58
61
|
const url = String(input);
|
|
59
62
|
|
|
63
|
+
if (url.includes('/calendars')) {
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
json: async () => ({ calendars, grouped: {}, total: calendars.length }),
|
|
67
|
+
} as Response;
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
if (url.includes('/calendar/habit-events')) {
|
|
61
71
|
return {
|
|
62
72
|
ok: true,
|
|
@@ -95,6 +105,13 @@ function mockCalendarFetchByWeek(
|
|
|
95
105
|
return vi.fn(async (input: RequestInfo | URL) => {
|
|
96
106
|
const url = new URL(String(input), 'http://localhost');
|
|
97
107
|
|
|
108
|
+
if (url.pathname.endsWith('/calendars')) {
|
|
109
|
+
return {
|
|
110
|
+
ok: true,
|
|
111
|
+
json: async () => ({ calendars: [], grouped: {}, total: 0 }),
|
|
112
|
+
} as Response;
|
|
113
|
+
}
|
|
114
|
+
|
|
98
115
|
if (url.pathname.includes('/calendar/habit-events')) {
|
|
99
116
|
return {
|
|
100
117
|
ok: true,
|
|
@@ -252,6 +269,60 @@ describe('CalendarSyncProvider optimistic visible events', () => {
|
|
|
252
269
|
expect(result.current.events.map((event) => event.id)).toEqual(['event-1']);
|
|
253
270
|
});
|
|
254
271
|
|
|
272
|
+
it('clears a transient event loading warning after the next successful fetch', async () => {
|
|
273
|
+
const initialEvent = createEvent('event-1');
|
|
274
|
+
const fetchMock = mockCalendarFetch([initialEvent]);
|
|
275
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
276
|
+
|
|
277
|
+
const { result } = renderCalendarSync();
|
|
278
|
+
|
|
279
|
+
act(() => {
|
|
280
|
+
result.current.setDates([new Date('2026-06-22T00:00:00.000Z')]);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
await waitFor(() => {
|
|
284
|
+
expect(result.current.events).toHaveLength(1);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
fetchMock.mockImplementation(async (input: RequestInfo | URL) => {
|
|
288
|
+
if (String(input).includes('/calendar/habit-events')) {
|
|
289
|
+
return {
|
|
290
|
+
ok: true,
|
|
291
|
+
json: async () => ({
|
|
292
|
+
completedHabitEventIds: [],
|
|
293
|
+
habitEventIds: [],
|
|
294
|
+
}),
|
|
295
|
+
} as Response;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
ok: false,
|
|
300
|
+
json: async () => ({ error: 'Temporary outage' }),
|
|
301
|
+
} as Response;
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
act(() => {
|
|
305
|
+
result.current.refresh();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
await waitFor(() => {
|
|
309
|
+
expect(result.current.syncStatus).toMatchObject({
|
|
310
|
+
message: 'failed_to_load_events',
|
|
311
|
+
state: 'error',
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
fetchMock.mockImplementation(mockCalendarFetch([initialEvent]));
|
|
316
|
+
act(() => {
|
|
317
|
+
result.current.refresh();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
await waitFor(() => {
|
|
321
|
+
expect(result.current.syncStatus.state).toBe('idle');
|
|
322
|
+
});
|
|
323
|
+
expect(result.current.error).toBeNull();
|
|
324
|
+
});
|
|
325
|
+
|
|
255
326
|
it('shows the active week events when navigating away and back', async () => {
|
|
256
327
|
const currentWeekEvent = createEvent('current-week-event', {
|
|
257
328
|
start_at: '2026-06-22T09:00:00.000Z',
|
|
@@ -443,4 +514,35 @@ describe('CalendarSyncProvider optimistic visible events', () => {
|
|
|
443
514
|
expect.objectContaining({ method: 'DELETE' })
|
|
444
515
|
);
|
|
445
516
|
});
|
|
517
|
+
|
|
518
|
+
it('hides events whose workspace calendar is disabled', async () => {
|
|
519
|
+
const visibleEvent = createEvent('visible-event', {
|
|
520
|
+
source_calendar_id: 'calendar-visible',
|
|
521
|
+
});
|
|
522
|
+
const hiddenEvent = createEvent('hidden-event', {
|
|
523
|
+
source_calendar_id: 'calendar-hidden',
|
|
524
|
+
});
|
|
525
|
+
vi.stubGlobal(
|
|
526
|
+
'fetch',
|
|
527
|
+
mockCalendarFetch(
|
|
528
|
+
[visibleEvent, hiddenEvent],
|
|
529
|
+
[
|
|
530
|
+
{ id: 'calendar-visible', is_enabled: true },
|
|
531
|
+
{ id: 'calendar-hidden', is_enabled: false },
|
|
532
|
+
]
|
|
533
|
+
)
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
const { result } = renderCalendarSync();
|
|
537
|
+
|
|
538
|
+
act(() => {
|
|
539
|
+
result.current.setDates([new Date('2026-06-22T00:00:00.000Z')]);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
await waitFor(() => {
|
|
543
|
+
expect(result.current.events.map((event) => event.id)).toEqual([
|
|
544
|
+
'visible-event',
|
|
545
|
+
]);
|
|
546
|
+
});
|
|
547
|
+
});
|
|
446
548
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
4
|
+
import { listWorkspaceCalendars } from '@tuturuuu/internal-api/calendar';
|
|
4
5
|
import type {
|
|
5
6
|
Workspace,
|
|
6
7
|
WorkspaceCalendarEvent,
|
|
@@ -235,6 +236,22 @@ export const CalendarSyncProvider = ({
|
|
|
235
236
|
);
|
|
236
237
|
}, [calendarConnections]);
|
|
237
238
|
|
|
239
|
+
const { data: workspaceCalendarsData } = useQuery({
|
|
240
|
+
queryKey: ['workspace-calendars', wsId],
|
|
241
|
+
enabled: !hasExternalEvents && !!wsId,
|
|
242
|
+
queryFn: () => listWorkspaceCalendars(wsId),
|
|
243
|
+
staleTime: 30_000,
|
|
244
|
+
});
|
|
245
|
+
const enabledWorkspaceCalendarIds = useMemo(
|
|
246
|
+
() =>
|
|
247
|
+
new Set(
|
|
248
|
+
(workspaceCalendarsData?.calendars ?? [])
|
|
249
|
+
.filter((calendar) => calendar.is_enabled)
|
|
250
|
+
.map((calendar) => calendar.id)
|
|
251
|
+
),
|
|
252
|
+
[workspaceCalendarsData?.calendars]
|
|
253
|
+
);
|
|
254
|
+
|
|
238
255
|
// Update calendar connection state
|
|
239
256
|
const updateCalendarConnection = useCallback(
|
|
240
257
|
(connectionId: string, isEnabled: boolean) => {
|
|
@@ -454,8 +471,8 @@ export const CalendarSyncProvider = ({
|
|
|
454
471
|
);
|
|
455
472
|
|
|
456
473
|
if (!response.ok) {
|
|
457
|
-
const errorData = await response.json();
|
|
458
|
-
throw new Error(errorData
|
|
474
|
+
const errorData = await response.json().catch(() => null);
|
|
475
|
+
throw new Error(errorData?.error || 'Failed to fetch events');
|
|
459
476
|
}
|
|
460
477
|
|
|
461
478
|
const result = await response.json();
|
|
@@ -471,6 +488,12 @@ export const CalendarSyncProvider = ({
|
|
|
471
488
|
isForcedRef.current = false;
|
|
472
489
|
|
|
473
490
|
setError(null);
|
|
491
|
+
setSyncStatus((currentStatus) =>
|
|
492
|
+
currentStatus.state === 'error' &&
|
|
493
|
+
currentStatus.message === 'failed_to_load_events'
|
|
494
|
+
? { state: 'idle' }
|
|
495
|
+
: currentStatus
|
|
496
|
+
);
|
|
474
497
|
return fetchedData;
|
|
475
498
|
} catch (err) {
|
|
476
499
|
const errorMessage =
|
|
@@ -745,18 +768,30 @@ export const CalendarSyncProvider = ({
|
|
|
745
768
|
const visibleEventsWithOptimisticState = useMemo(() => {
|
|
746
769
|
const removedIds = new Set(optimisticState.removedIds);
|
|
747
770
|
|
|
748
|
-
// Filter external events
|
|
749
|
-
//
|
|
750
|
-
//
|
|
751
|
-
const filteredEvents =
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
771
|
+
// Filter native and external events using the same visibility controls as
|
|
772
|
+
// the sidebar. Events without a source remain visible for backwards
|
|
773
|
+
// compatibility with older calendar rows.
|
|
774
|
+
const filteredEvents = !hasExternalEvents
|
|
775
|
+
? (visibleDatabaseEvents as CalendarEvent[]).filter((event) => {
|
|
776
|
+
const sourceCalendarId = event.source_calendar_id;
|
|
777
|
+
const eventCalendarId =
|
|
778
|
+
event.external_calendar_id || event.google_calendar_id;
|
|
779
|
+
|
|
780
|
+
if (
|
|
781
|
+
sourceCalendarId &&
|
|
782
|
+
(workspaceCalendarsData?.calendars.length ?? 0) > 0 &&
|
|
783
|
+
!enabledWorkspaceCalendarIds.has(sourceCalendarId)
|
|
784
|
+
) {
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return (
|
|
789
|
+
!eventCalendarId ||
|
|
790
|
+
calendarConnections.length === 0 ||
|
|
791
|
+
enabledCalendarIds.has(eventCalendarId)
|
|
792
|
+
);
|
|
793
|
+
})
|
|
794
|
+
: (visibleDatabaseEvents as CalendarEvent[]);
|
|
760
795
|
|
|
761
796
|
const byId = new Map<string, CalendarEvent>();
|
|
762
797
|
|
|
@@ -782,11 +817,13 @@ export const CalendarSyncProvider = ({
|
|
|
782
817
|
}, [
|
|
783
818
|
calendarConnections.length,
|
|
784
819
|
enabledCalendarIds,
|
|
820
|
+
enabledWorkspaceCalendarIds,
|
|
785
821
|
hasExternalEvents,
|
|
786
822
|
isVisibleInCurrentRange,
|
|
787
823
|
optimisticState.events,
|
|
788
824
|
optimisticState.removedIds,
|
|
789
825
|
visibleDatabaseEvents,
|
|
826
|
+
workspaceCalendarsData?.calendars.length,
|
|
790
827
|
]);
|
|
791
828
|
|
|
792
829
|
useEffect(() => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
2
|
-
import { isPersonalExternalStagingListId } from '@tuturuuu/utils/task-helper';
|
|
2
|
+
import { isPersonalExternalStagingListId } from '@tuturuuu/utils/task-helper/personal-external-staging';
|
|
3
3
|
|
|
4
4
|
function hasValue(value: string | null | undefined): value is string {
|
|
5
5
|
return typeof value === 'string' && value.length > 0;
|