@tuturuuu/ui 0.38.0 → 0.39.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/package.json +8 -8
  3. package/src/components/ui/custom/public-profile-menu-item.tsx +25 -0
  4. package/src/components/ui/custom/satellite-user-menu-items.test.tsx +61 -0
  5. package/src/components/ui/custom/satellite-user-menu-items.tsx +8 -0
  6. package/src/components/ui/finance/invoices/attendance-calendar.test.tsx +294 -0
  7. package/src/components/ui/finance/invoices/attendance-calendar.tsx +14 -6
  8. package/src/components/ui/finance/invoices/components/invoice-history-actor.test.tsx +341 -0
  9. package/src/components/ui/finance/invoices/components/invoice-history-date.test.tsx +173 -0
  10. package/src/components/ui/finance/invoices/components/invoice-history-error.test.tsx +269 -0
  11. package/src/components/ui/finance/invoices/components/invoice-history-recovery.tsx +47 -0
  12. package/src/components/ui/finance/invoices/components/invoice-user-history-accordion.tsx +43 -10
  13. package/src/components/ui/finance/invoices/components/subscription-create-action.tsx +42 -0
  14. package/src/components/ui/finance/invoices/hooks/customer-search-scope.test.tsx +404 -0
  15. package/src/components/ui/finance/invoices/hooks/invoice-actor-lifetime-key.ts +11 -0
  16. package/src/components/ui/finance/invoices/hooks/use-infinite-user-invoices.ts +48 -0
  17. package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.test.tsx +6 -1
  18. package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.ts +58 -6
  19. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.test.tsx +34 -0
  20. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +8 -2
  21. package/src/components/ui/finance/invoices/hooks/use-subscription-create.ts +219 -0
  22. package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-context.ts +59 -0
  23. package/src/components/ui/finance/invoices/hooks/use-user-groups.ts +38 -0
  24. package/src/components/ui/finance/invoices/hooks.ts +3 -100
  25. package/src/components/ui/finance/invoices/invoiceId/compact-invoice-template.tsx +2 -7
  26. package/src/components/ui/finance/invoices/invoiceId/format-invoice-template-date.ts +13 -0
  27. package/src/components/ui/finance/invoices/invoiceId/full-invoice-template.tsx +2 -7
  28. package/src/components/ui/finance/invoices/invoiceId/invoice-card-date.test.tsx +169 -0
  29. package/src/components/ui/finance/invoices/invoiceId/invoice-card-storage.test.tsx +120 -0
  30. package/src/components/ui/finance/invoices/invoiceId/invoice-card.tsx +25 -62
  31. package/src/components/ui/finance/invoices/invoiceId/invoice-details-page.tsx +1 -0
  32. package/src/components/ui/finance/invoices/invoiceId/invoice-png-lifetime.test.tsx +489 -0
  33. package/src/components/ui/finance/invoices/invoiceId/use-invoice-png-export.ts +184 -0
  34. package/src/components/ui/finance/invoices/subscription-admission.test.ts +77 -0
  35. package/src/components/ui/finance/invoices/subscription-admission.ts +101 -0
  36. package/src/components/ui/finance/invoices/subscription-billing-groups.test.ts +162 -1
  37. package/src/components/ui/finance/invoices/subscription-billing-groups.ts +50 -9
  38. package/src/components/ui/finance/invoices/subscription-context-actor.test.tsx +246 -0
  39. package/src/components/ui/finance/invoices/subscription-create-scope.test.tsx +562 -0
  40. package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +88 -6
  41. package/src/components/ui/finance/invoices/subscription-invoice.tsx +76 -135
  42. package/src/components/ui/finance/invoices/types.ts +3 -3
  43. package/src/components/ui/finance/invoices/user-groups-actor.test.tsx +334 -0
  44. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +47 -10
  45. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +95 -1
  46. package/src/components/ui/legacy/calendar/agenda-view.tsx +4 -1
  47. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +33 -369
  48. package/src/components/ui/legacy/calendar/all-day-event-layout.test.ts +94 -0
  49. package/src/components/ui/legacy/calendar/all-day-event-layout.ts +322 -0
  50. package/src/components/ui/legacy/calendar/calendar-views.test.tsx +58 -27
  51. package/src/components/ui/legacy/calendar/event-card.test.tsx +25 -1
  52. package/src/components/ui/legacy/calendar/event-card.tsx +10 -12
  53. package/src/components/ui/legacy/calendar/event-modal-header.tsx +17 -4
  54. package/src/components/ui/legacy/calendar/event-modal.tsx +26 -1554
  55. package/src/components/ui/legacy/calendar/legacy-event-modal.tsx +1559 -0
  56. package/src/components/ui/legacy/calendar/month-calendar.tsx +5 -2
  57. package/src/components/ui/legacy/calendar/native-recurrence-context-menu.tsx +21 -0
  58. package/src/components/ui/legacy/calendar/native-recurrence-dialog.test.tsx +245 -0
  59. package/src/components/ui/legacy/calendar/native-recurrence-dialog.tsx +95 -0
  60. package/src/components/ui/legacy/calendar/native-recurrence-fields.tsx +137 -0
  61. package/src/components/ui/legacy/calendar/native-recurrence-form.tsx +340 -0
  62. package/src/components/ui/legacy/calendar/native-recurrence-model.test.ts +234 -0
  63. package/src/components/ui/legacy/calendar/native-recurrence-model.ts +245 -0
  64. package/src/components/ui/legacy/calendar/native-recurrence-pattern-fields.test.tsx +142 -0
  65. package/src/components/ui/legacy/calendar/native-recurrence-pattern-fields.tsx +178 -0
  66. package/src/components/ui/legacy/calendar/native-recurrence-pattern-model.test.ts +205 -0
  67. package/src/components/ui/legacy/calendar/native-recurrence-provider-routing.test.tsx +255 -0
  68. package/src/components/ui/legacy/calendar/past-event-treatment.test.ts +137 -0
  69. package/src/components/ui/legacy/calendar/past-event-treatment.ts +25 -0
  70. package/src/components/ui/legacy/calendar/provider-readonly-dialog.test.tsx +52 -0
  71. package/src/components/ui/legacy/calendar/provider-readonly-dialog.tsx +48 -0
  72. package/src/components/ui/legacy/calendar/use-calendar-draft-save.test.tsx +1 -1
  73. package/src/components/ui/legacy/calendar/use-calendar-draft-save.tsx +1 -1
  74. package/src/components/ui/legacy/calendar/use-provider-recurrence-operation.test.tsx +185 -0
  75. package/src/components/ui/legacy/calendar/use-provider-recurrence-operation.ts +186 -0
  76. package/src/components/ui/public-link-button.test.tsx +116 -0
  77. package/src/components/ui/public-link-button.tsx +81 -0
  78. package/src/components/ui/text-editor/editor.tsx +3 -0
  79. package/src/components/ui/text-editor/fixed-toolbar-contents.test.tsx +59 -0
  80. package/src/components/ui/text-editor/fixed-toolbar-contents.tsx +156 -0
  81. package/src/components/ui/text-editor/tool-bar.tsx +18 -74
  82. package/src/hooks/__tests__/calendar-event-write-payload-readonly.test.ts +40 -0
  83. package/src/hooks/__tests__/native-calendar-mutation-guard.test.ts +45 -0
  84. package/src/hooks/__tests__/supabase-provider.test.ts +101 -7
  85. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +27 -0
  86. package/src/hooks/__tests__/use-local-storage.test.tsx +42 -0
  87. package/src/hooks/__tests__/use-native-calendar-occurrences.test.tsx +140 -0
  88. package/src/hooks/__tests__/use-yjs-collaboration.test.tsx +39 -0
  89. package/src/hooks/calendar-connection-types.ts +49 -0
  90. package/src/hooks/calendar-event-update-queue-types.ts +12 -0
  91. package/src/hooks/calendar-event-write-payload.ts +33 -0
  92. package/src/hooks/collaboration-document-hydration.ts +35 -0
  93. package/src/hooks/supabase-provider.ts +39 -41
  94. package/src/hooks/use-calendar-sync.tsx +41 -62
  95. package/src/hooks/use-calendar.tsx +24 -38
  96. package/src/hooks/use-local-storage.ts +9 -11
  97. package/src/hooks/use-native-calendar-occurrences.ts +85 -0
  98. package/src/hooks/use-yjs-collaboration.ts +23 -0
@@ -26,6 +26,7 @@ import {
26
26
  useRef,
27
27
  useState,
28
28
  } from 'react';
29
+ import { NativeRecurrenceContextMenu } from '../components/ui/legacy/calendar/native-recurrence-context-menu';
29
30
  import { getTaskApiUrl } from '../lib/tasks-app-url';
30
31
  import {
31
32
  createCalendarCreationRequests,
@@ -33,7 +34,13 @@ import {
33
34
  } from './calendar-creation-request';
34
35
  import { roundToNearest15Minutes } from './calendar-date-utils';
35
36
  import { createCalendarEventLookup } from './calendar-event-lookup';
36
- import { calendarEventUpdatePayload } from './calendar-event-write-payload';
37
+ import type { PendingEventUpdate } from './calendar-event-update-queue-types';
38
+ import {
39
+ assertOrdinaryCalendarEvent,
40
+ calendarEventByIdentity,
41
+ calendarEventUpdatePayload,
42
+ cleanCalendarEventUpdates,
43
+ } from './calendar-event-write-payload';
37
44
  import { useCalendarSync } from './use-calendar-sync';
38
45
  import { useOpenInitialCalendarEvent } from './use-open-initial-calendar-event';
39
46
 
@@ -229,18 +236,6 @@ const CalendarContext = createContext<{
229
236
  readOnly: false,
230
237
  });
231
238
 
232
- // Add this interface before the updateEvent function
233
- interface PendingEventUpdate extends Partial<CalendarEvent> {
234
- _updateId?: string;
235
- _timestamp: number;
236
- _eventId: string;
237
- _previousEvent?: CalendarEvent;
238
- _resolvers?: Array<{
239
- resolve: (value: CalendarEvent) => void;
240
- reject: (reason: unknown) => void;
241
- }>;
242
- }
243
-
244
239
  export type CalendarEventAdapter = {
245
240
  disableBuiltInEventUi?: boolean;
246
241
  preservePastEventOpacity?: boolean;
@@ -484,11 +479,7 @@ export const CalendarProvider = ({
484
479
  // Event getters
485
480
  const getEvent = useCallback(
486
481
  (eventId: string) => {
487
- // Handle IDs for split multi-day events (they contain a dash and date)
488
- const originalId = eventId.includes('-')
489
- ? eventId.split('-')[0]
490
- : eventId;
491
- return events.find((e: Partial<CalendarEvent>) => e.id === originalId);
482
+ return calendarEventByIdentity(events, eventId);
492
483
  },
493
484
  [events]
494
485
  );
@@ -917,24 +908,8 @@ export const CalendarProvider = ({
917
908
  return undefined;
918
909
  }
919
910
 
920
- // Clean and validate the event updates - only allow known CalendarEvent fields
921
- const allowedFields: (keyof CalendarEvent)[] = [
922
- 'title',
923
- 'description',
924
- 'start_at',
925
- 'end_at',
926
- 'color',
927
- 'location',
928
- 'locked',
929
- 'source',
930
- ];
931
-
932
- const cleanedUpdates: Partial<CalendarEvent> = {};
933
- for (const field of allowedFields) {
934
- if (eventUpdates[field] !== undefined) {
935
- (cleanedUpdates as any)[field] = eventUpdates[field];
936
- }
937
- }
911
+ assertOrdinaryCalendarEvent(events, eventId);
912
+ const cleanedUpdates = cleanCalendarEventUpdates(eventUpdates);
938
913
 
939
914
  // Round start and end times to nearest 15-minute interval if they exist
940
915
  if (cleanedUpdates.start_at) {
@@ -1075,6 +1050,7 @@ export const CalendarProvider = ({
1075
1050
  console.warn('Calendar is in read-only mode');
1076
1051
  return;
1077
1052
  }
1053
+ assertOrdinaryCalendarEvent(events, eventId);
1078
1054
  // If this is a pending new event that hasn't been saved yet
1079
1055
  if (pendingNewEvent && eventId === 'new') {
1080
1056
  // Just clear the pending event
@@ -1618,8 +1594,18 @@ export const CalendarProvider = ({
1618
1594
  defaultNewEventTab,
1619
1595
  disableBuiltInEventUi: eventAdapter?.disableBuiltInEventUi ?? false,
1620
1596
  preservePastEventOpacity: eventAdapter?.preservePastEventOpacity ?? false,
1621
- renderEventContextMenu: eventAdapter?.renderContextMenu,
1622
- isEventReadOnly: eventAdapter?.isEventReadOnly ?? (() => false),
1597
+ renderEventContextMenu: (event: CalendarEvent) =>
1598
+ event.seriesId ? (
1599
+ <NativeRecurrenceContextMenu
1600
+ onEdit={() => openEventEditor(event._originalId ?? event.id)}
1601
+ readOnly={readOnly}
1602
+ />
1603
+ ) : (
1604
+ eventAdapter?.renderContextMenu?.(event)
1605
+ ),
1606
+ isEventReadOnly: (event: CalendarEvent) =>
1607
+ Boolean(event.seriesId) ||
1608
+ (eventAdapter?.isEventReadOnly?.(event) ?? false),
1623
1609
  readOnly,
1624
1610
  };
1625
1611
 
@@ -9,16 +9,14 @@ export const useLocalStorage = <T>(
9
9
  const [initialized, setInitialized] = useState(false);
10
10
 
11
11
  useEffect(() => {
12
- // Retrieve from localStorage
13
- const item = window.localStorage.getItem(key);
14
- if (item) {
15
- try {
16
- setStoredValue(JSON.parse(item));
17
- } catch (error) {
18
- console.error(error);
19
- }
12
+ try {
13
+ const item = window.localStorage.getItem(key);
14
+ if (item) setStoredValue(JSON.parse(item));
15
+ } catch {
16
+ console.error('Unable to read browser preferences.');
17
+ } finally {
18
+ setInitialized(true);
20
19
  }
21
- setInitialized(true);
22
20
  }, [key]);
23
21
 
24
22
  const setValue = useCallback(
@@ -29,8 +27,8 @@ export const useLocalStorage = <T>(
29
27
  // Save to localStorage
30
28
  try {
31
29
  window.localStorage.setItem(key, JSON.stringify(valueToStore));
32
- } catch (error) {
33
- console.error(error);
30
+ } catch {
31
+ console.error('Unable to save browser preferences.');
34
32
  }
35
33
  return valueToStore;
36
34
  });
@@ -0,0 +1,85 @@
1
+ 'use client';
2
+ import { useQuery, useQueryClient } from '@tanstack/react-query';
3
+ import { listNativeCalendarOccurrences } from '@tuturuuu/internal-api/calendar-series';
4
+ import { InternalApiError } from '@tuturuuu/internal-api/client';
5
+ import type { CalendarEvent } from '@tuturuuu/types/primitives/calendar-event';
6
+ import { useEffect } from 'react';
7
+ import { calendarQueryRange } from '../lib/calendar-day';
8
+ import { useWorkspaceActor } from './use-workspace-visibility';
9
+ export const EMPTY_NATIVE_OCCURRENCES: CalendarEvent[] = [];
10
+ export const nativeOccurrencesKey = (wsId: string) =>
11
+ ['native-calendar-occurrences', wsId] as const;
12
+ export function deniedNativeOccurrences(error: unknown) {
13
+ return (
14
+ error instanceof InternalApiError &&
15
+ (error.status === 401 ||
16
+ error.status === 404 ||
17
+ (error.status === 403 && error.code !== 'MFA_REQUIRED'))
18
+ );
19
+ }
20
+ /** A denied snapshot is never rendered as stale data. MFA is a recoverable challenge. */
21
+ export function visibleNativeOccurrences(
22
+ data: CalendarEvent[] | undefined,
23
+ error: unknown
24
+ ) {
25
+ return deniedNativeOccurrences(error)
26
+ ? EMPTY_NATIVE_OCCURRENCES
27
+ : (data ?? EMPTY_NATIVE_OCCURRENCES);
28
+ }
29
+ export function useNativeCalendarOccurrences(
30
+ wsId: string,
31
+ dates: Date[],
32
+ timezone: string | undefined,
33
+ external: boolean
34
+ ) {
35
+ const actor = useWorkspaceActor();
36
+ const actorId = actor?.actorId;
37
+ const client = useQueryClient();
38
+ const range = dates.length ? calendarQueryRange(dates, timezone) : undefined;
39
+ const from = range?.start.toISOString();
40
+ const to = range?.end.toISOString();
41
+ const key = [...nativeOccurrencesKey(wsId), actorId, from, to];
42
+ const enabled = !external && !!actor && !!wsId && !!from && !!to;
43
+ useEffect(
44
+ () => () => {
45
+ client.removeQueries({
46
+ queryKey: [...nativeOccurrencesKey(wsId), actorId],
47
+ });
48
+ },
49
+ [client, wsId, actorId]
50
+ );
51
+ const query = useQuery({
52
+ queryKey: key,
53
+ enabled,
54
+ queryFn: async ({ signal }) => {
55
+ actor!.assertActive();
56
+ try {
57
+ const result = await listNativeCalendarOccurrences(
58
+ wsId,
59
+ { from: from!, to: to! },
60
+ { signal }
61
+ );
62
+ actor!.assertActive();
63
+ return result.data;
64
+ } catch (error) {
65
+ if (deniedNativeOccurrences(error))
66
+ client.setQueryData(key, EMPTY_NATIVE_OCCURRENCES);
67
+ throw error;
68
+ }
69
+ },
70
+ staleTime: 30_000,
71
+ gcTime: 30 * 60_000,
72
+ retry: (count, error) =>
73
+ !(
74
+ error instanceof InternalApiError &&
75
+ error.status >= 400 &&
76
+ error.status < 500
77
+ ) && count < 2,
78
+ });
79
+ return {
80
+ ...query,
81
+ data: enabled
82
+ ? visibleNativeOccurrences(query.data, query.error)
83
+ : EMPTY_NATIVE_OCCURRENCES,
84
+ };
85
+ }
@@ -33,6 +33,8 @@ export interface YjsCollaborationResult {
33
33
  awareness: Awareness | null;
34
34
  provider: SupabaseProvider | null;
35
35
  synced: boolean;
36
+ hydrated: boolean;
37
+ hydrationFailed: boolean;
36
38
  connected: boolean;
37
39
  }
38
40
 
@@ -62,6 +64,10 @@ export function useYjsCollaboration(
62
64
  onSave,
63
65
  } = config;
64
66
 
67
+ const [hydratedProvider, setHydratedProvider] =
68
+ useState<SupabaseProvider | null>(null);
69
+ const [hydrationErrorProvider, setHydrationErrorProvider] =
70
+ useState<SupabaseProvider | null>(null);
65
71
  const [synced, setSynced] = useState(false);
66
72
  const [connected, setConnected] = useState(false);
67
73
  const [providerState, setProviderState] = useState<SupabaseProvider | null>(
@@ -176,6 +182,15 @@ export function useYjsCollaboration(
176
182
 
177
183
  providerRef.current = provider;
178
184
  setProviderState(provider);
185
+ setSynced(false);
186
+ setConnected(false);
187
+
188
+ provider.on('hydrated', () => {
189
+ if (!mountedRef.current || providerRef.current !== provider) return;
190
+ // Publish a new state value so hydration is observable without transport.
191
+ setHydratedProvider(provider);
192
+ setHydrationErrorProvider(null);
193
+ });
179
194
 
180
195
  // Listen to provider events — use mountedRef so listeners stay valid
181
196
  // across StrictMode cleanup/remount without re-registration.
@@ -217,6 +232,8 @@ export function useYjsCollaboration(
217
232
  channel: errorInfo.channel,
218
233
  status: errorInfo.status,
219
234
  });
235
+ if (errorInfo.status === 'DOCUMENT_ERROR')
236
+ setHydrationErrorProvider(provider);
220
237
  onErrorRef.current?.(new Error(errorInfo.message));
221
238
  }
222
239
  );
@@ -308,6 +325,12 @@ export function useYjsCollaboration(
308
325
  providerState && providerState.id === doc?.clientID
309
326
  ? providerState
310
327
  : null,
328
+ hydrated:
329
+ !!hydratedProvider &&
330
+ hydratedProvider.id === doc?.clientID &&
331
+ hydratedProvider.hydrated,
332
+ hydrationFailed:
333
+ !!hydrationErrorProvider && hydrationErrorProvider.id === doc?.clientID,
311
334
  synced: !!providerState && providerState.id === doc?.clientID && synced,
312
335
  connected:
313
336
  !!providerState && providerState.id === doc?.clientID && connected,