@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.
- package/CHANGELOG.md +34 -0
- package/package.json +8 -8
- package/src/components/ui/custom/public-profile-menu-item.tsx +25 -0
- package/src/components/ui/custom/satellite-user-menu-items.test.tsx +61 -0
- package/src/components/ui/custom/satellite-user-menu-items.tsx +8 -0
- package/src/components/ui/finance/invoices/attendance-calendar.test.tsx +294 -0
- package/src/components/ui/finance/invoices/attendance-calendar.tsx +14 -6
- package/src/components/ui/finance/invoices/components/invoice-history-actor.test.tsx +341 -0
- package/src/components/ui/finance/invoices/components/invoice-history-date.test.tsx +173 -0
- package/src/components/ui/finance/invoices/components/invoice-history-error.test.tsx +269 -0
- package/src/components/ui/finance/invoices/components/invoice-history-recovery.tsx +47 -0
- package/src/components/ui/finance/invoices/components/invoice-user-history-accordion.tsx +43 -10
- package/src/components/ui/finance/invoices/components/subscription-create-action.tsx +42 -0
- package/src/components/ui/finance/invoices/hooks/customer-search-scope.test.tsx +404 -0
- package/src/components/ui/finance/invoices/hooks/invoice-actor-lifetime-key.ts +11 -0
- package/src/components/ui/finance/invoices/hooks/use-infinite-user-invoices.ts +48 -0
- package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.test.tsx +6 -1
- package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.ts +58 -6
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.test.tsx +34 -0
- package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +8 -2
- package/src/components/ui/finance/invoices/hooks/use-subscription-create.ts +219 -0
- package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-context.ts +59 -0
- package/src/components/ui/finance/invoices/hooks/use-user-groups.ts +38 -0
- package/src/components/ui/finance/invoices/hooks.ts +3 -100
- package/src/components/ui/finance/invoices/invoiceId/compact-invoice-template.tsx +2 -7
- package/src/components/ui/finance/invoices/invoiceId/format-invoice-template-date.ts +13 -0
- package/src/components/ui/finance/invoices/invoiceId/full-invoice-template.tsx +2 -7
- package/src/components/ui/finance/invoices/invoiceId/invoice-card-date.test.tsx +169 -0
- package/src/components/ui/finance/invoices/invoiceId/invoice-card-storage.test.tsx +120 -0
- package/src/components/ui/finance/invoices/invoiceId/invoice-card.tsx +25 -62
- package/src/components/ui/finance/invoices/invoiceId/invoice-details-page.tsx +1 -0
- package/src/components/ui/finance/invoices/invoiceId/invoice-png-lifetime.test.tsx +489 -0
- package/src/components/ui/finance/invoices/invoiceId/use-invoice-png-export.ts +184 -0
- package/src/components/ui/finance/invoices/subscription-admission.test.ts +77 -0
- package/src/components/ui/finance/invoices/subscription-admission.ts +101 -0
- package/src/components/ui/finance/invoices/subscription-billing-groups.test.ts +162 -1
- package/src/components/ui/finance/invoices/subscription-billing-groups.ts +50 -9
- package/src/components/ui/finance/invoices/subscription-context-actor.test.tsx +246 -0
- package/src/components/ui/finance/invoices/subscription-create-scope.test.tsx +562 -0
- package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +88 -6
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +76 -135
- package/src/components/ui/finance/invoices/types.ts +3 -3
- package/src/components/ui/finance/invoices/user-groups-actor.test.tsx +334 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +47 -10
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +95 -1
- package/src/components/ui/legacy/calendar/agenda-view.tsx +4 -1
- package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +33 -369
- package/src/components/ui/legacy/calendar/all-day-event-layout.test.ts +94 -0
- package/src/components/ui/legacy/calendar/all-day-event-layout.ts +322 -0
- package/src/components/ui/legacy/calendar/calendar-views.test.tsx +58 -27
- package/src/components/ui/legacy/calendar/event-card.test.tsx +25 -1
- package/src/components/ui/legacy/calendar/event-card.tsx +10 -12
- package/src/components/ui/legacy/calendar/event-modal-header.tsx +17 -4
- package/src/components/ui/legacy/calendar/event-modal.tsx +26 -1554
- package/src/components/ui/legacy/calendar/legacy-event-modal.tsx +1559 -0
- package/src/components/ui/legacy/calendar/month-calendar.tsx +5 -2
- package/src/components/ui/legacy/calendar/native-recurrence-context-menu.tsx +21 -0
- package/src/components/ui/legacy/calendar/native-recurrence-dialog.test.tsx +245 -0
- package/src/components/ui/legacy/calendar/native-recurrence-dialog.tsx +95 -0
- package/src/components/ui/legacy/calendar/native-recurrence-fields.tsx +137 -0
- package/src/components/ui/legacy/calendar/native-recurrence-form.tsx +340 -0
- package/src/components/ui/legacy/calendar/native-recurrence-model.test.ts +234 -0
- package/src/components/ui/legacy/calendar/native-recurrence-model.ts +245 -0
- package/src/components/ui/legacy/calendar/native-recurrence-pattern-fields.test.tsx +142 -0
- package/src/components/ui/legacy/calendar/native-recurrence-pattern-fields.tsx +178 -0
- package/src/components/ui/legacy/calendar/native-recurrence-pattern-model.test.ts +205 -0
- package/src/components/ui/legacy/calendar/native-recurrence-provider-routing.test.tsx +255 -0
- package/src/components/ui/legacy/calendar/past-event-treatment.test.ts +137 -0
- package/src/components/ui/legacy/calendar/past-event-treatment.ts +25 -0
- package/src/components/ui/legacy/calendar/provider-readonly-dialog.test.tsx +52 -0
- package/src/components/ui/legacy/calendar/provider-readonly-dialog.tsx +48 -0
- package/src/components/ui/legacy/calendar/use-calendar-draft-save.test.tsx +1 -1
- package/src/components/ui/legacy/calendar/use-calendar-draft-save.tsx +1 -1
- package/src/components/ui/legacy/calendar/use-provider-recurrence-operation.test.tsx +185 -0
- package/src/components/ui/legacy/calendar/use-provider-recurrence-operation.ts +186 -0
- package/src/components/ui/public-link-button.test.tsx +116 -0
- package/src/components/ui/public-link-button.tsx +81 -0
- package/src/components/ui/text-editor/editor.tsx +3 -0
- package/src/components/ui/text-editor/fixed-toolbar-contents.test.tsx +59 -0
- package/src/components/ui/text-editor/fixed-toolbar-contents.tsx +156 -0
- package/src/components/ui/text-editor/tool-bar.tsx +18 -74
- package/src/hooks/__tests__/calendar-event-write-payload-readonly.test.ts +40 -0
- package/src/hooks/__tests__/native-calendar-mutation-guard.test.ts +45 -0
- package/src/hooks/__tests__/supabase-provider.test.ts +101 -7
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +27 -0
- package/src/hooks/__tests__/use-local-storage.test.tsx +42 -0
- package/src/hooks/__tests__/use-native-calendar-occurrences.test.tsx +140 -0
- package/src/hooks/__tests__/use-yjs-collaboration.test.tsx +39 -0
- package/src/hooks/calendar-connection-types.ts +49 -0
- package/src/hooks/calendar-event-update-queue-types.ts +12 -0
- package/src/hooks/calendar-event-write-payload.ts +33 -0
- package/src/hooks/collaboration-document-hydration.ts +35 -0
- package/src/hooks/supabase-provider.ts +39 -41
- package/src/hooks/use-calendar-sync.tsx +41 -62
- package/src/hooks/use-calendar.tsx +24 -38
- package/src/hooks/use-local-storage.ts +9 -11
- package/src/hooks/use-native-calendar-occurrences.ts +85 -0
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
921
|
-
const
|
|
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:
|
|
1622
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
33
|
-
console.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,
|