@tuturuuu/ui 0.37.2 → 0.39.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.
Files changed (126) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/package.json +10 -7
  3. package/src/components/ui/custom/__tests__/workspace-picker-content.test.tsx +86 -0
  4. package/src/components/ui/custom/education-todo.test.tsx +131 -0
  5. package/src/components/ui/custom/education-todo.tsx +191 -0
  6. package/src/components/ui/custom/satellite-notification-polling.test.tsx +61 -0
  7. package/src/components/ui/custom/satellite-notification-popover.tsx +7 -3
  8. package/src/components/ui/custom/workspace-picker-content.tsx +93 -30
  9. package/src/components/ui/custom/workspace-select.tsx +12 -4
  10. package/src/components/ui/finance/invoices/attendance-calendar.test.tsx +294 -0
  11. package/src/components/ui/finance/invoices/attendance-calendar.tsx +14 -6
  12. package/src/components/ui/finance/invoices/components/invoice-history-actor.test.tsx +341 -0
  13. package/src/components/ui/finance/invoices/components/invoice-history-date.test.tsx +173 -0
  14. package/src/components/ui/finance/invoices/components/invoice-history-error.test.tsx +269 -0
  15. package/src/components/ui/finance/invoices/components/invoice-history-recovery.tsx +47 -0
  16. package/src/components/ui/finance/invoices/components/invoice-user-history-accordion.tsx +43 -10
  17. package/src/components/ui/finance/invoices/components/subscription-create-action.tsx +42 -0
  18. package/src/components/ui/finance/invoices/hooks/customer-search-scope.test.tsx +404 -0
  19. package/src/components/ui/finance/invoices/hooks/invoice-actor-lifetime-key.ts +11 -0
  20. package/src/components/ui/finance/invoices/hooks/use-infinite-user-invoices.ts +48 -0
  21. package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.test.tsx +6 -1
  22. package/src/components/ui/finance/invoices/hooks/use-invoice-customer-search.ts +58 -6
  23. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.test.tsx +34 -0
  24. package/src/components/ui/finance/invoices/hooks/use-subscription-auto-selection.ts +8 -2
  25. package/src/components/ui/finance/invoices/hooks/use-subscription-create.ts +219 -0
  26. package/src/components/ui/finance/invoices/hooks/use-subscription-invoice-context.ts +59 -0
  27. package/src/components/ui/finance/invoices/hooks/use-user-groups.ts +38 -0
  28. package/src/components/ui/finance/invoices/hooks.ts +3 -100
  29. package/src/components/ui/finance/invoices/invoiceId/compact-invoice-template.tsx +2 -7
  30. package/src/components/ui/finance/invoices/invoiceId/format-invoice-template-date.ts +13 -0
  31. package/src/components/ui/finance/invoices/invoiceId/full-invoice-template.tsx +2 -7
  32. package/src/components/ui/finance/invoices/invoiceId/invoice-card-date.test.tsx +169 -0
  33. package/src/components/ui/finance/invoices/invoiceId/invoice-card-storage.test.tsx +120 -0
  34. package/src/components/ui/finance/invoices/invoiceId/invoice-card.tsx +25 -62
  35. package/src/components/ui/finance/invoices/invoiceId/invoice-details-page.tsx +1 -0
  36. package/src/components/ui/finance/invoices/invoiceId/invoice-png-lifetime.test.tsx +438 -0
  37. package/src/components/ui/finance/invoices/invoiceId/use-invoice-png-export.ts +184 -0
  38. package/src/components/ui/finance/invoices/subscription-admission.test.ts +77 -0
  39. package/src/components/ui/finance/invoices/subscription-admission.ts +101 -0
  40. package/src/components/ui/finance/invoices/subscription-billing-groups.test.ts +162 -1
  41. package/src/components/ui/finance/invoices/subscription-billing-groups.ts +50 -9
  42. package/src/components/ui/finance/invoices/subscription-context-actor.test.tsx +246 -0
  43. package/src/components/ui/finance/invoices/subscription-create-scope.test.tsx +562 -0
  44. package/src/components/ui/finance/invoices/subscription-invoice.test.tsx +88 -6
  45. package/src/components/ui/finance/invoices/subscription-invoice.tsx +76 -135
  46. package/src/components/ui/finance/invoices/types.ts +3 -3
  47. package/src/components/ui/finance/invoices/user-groups-actor.test.tsx +334 -0
  48. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +47 -10
  49. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +95 -1
  50. package/src/components/ui/legacy/calendar/agenda-view.tsx +4 -1
  51. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +33 -369
  52. package/src/components/ui/legacy/calendar/all-day-event-layout.test.ts +94 -0
  53. package/src/components/ui/legacy/calendar/all-day-event-layout.ts +322 -0
  54. package/src/components/ui/legacy/calendar/calendar-draft-recovery.ts +34 -0
  55. package/src/components/ui/legacy/calendar/calendar-views.test.tsx +58 -27
  56. package/src/components/ui/legacy/calendar/event-card.test.tsx +25 -1
  57. package/src/components/ui/legacy/calendar/event-card.tsx +10 -12
  58. package/src/components/ui/legacy/calendar/event-modal-header.tsx +17 -4
  59. package/src/components/ui/legacy/calendar/event-modal.tsx +26 -1614
  60. package/src/components/ui/legacy/calendar/event-save-payload.test.ts +40 -0
  61. package/src/components/ui/legacy/calendar/event-save-payload.ts +79 -0
  62. package/src/components/ui/legacy/calendar/event-source-selection.test.ts +95 -0
  63. package/src/components/ui/legacy/calendar/event-source-selection.ts +78 -0
  64. package/src/components/ui/legacy/calendar/legacy-event-modal.tsx +1559 -0
  65. package/src/components/ui/legacy/calendar/month-calendar.tsx +5 -2
  66. package/src/components/ui/legacy/calendar/native-recurrence-context-menu.tsx +21 -0
  67. package/src/components/ui/legacy/calendar/native-recurrence-dialog.test.tsx +245 -0
  68. package/src/components/ui/legacy/calendar/native-recurrence-dialog.tsx +95 -0
  69. package/src/components/ui/legacy/calendar/native-recurrence-fields.tsx +137 -0
  70. package/src/components/ui/legacy/calendar/native-recurrence-form.tsx +340 -0
  71. package/src/components/ui/legacy/calendar/native-recurrence-model.test.ts +234 -0
  72. package/src/components/ui/legacy/calendar/native-recurrence-model.ts +245 -0
  73. package/src/components/ui/legacy/calendar/native-recurrence-pattern-fields.test.tsx +142 -0
  74. package/src/components/ui/legacy/calendar/native-recurrence-pattern-fields.tsx +178 -0
  75. package/src/components/ui/legacy/calendar/native-recurrence-pattern-model.test.ts +205 -0
  76. package/src/components/ui/legacy/calendar/native-recurrence-provider-routing.test.tsx +255 -0
  77. package/src/components/ui/legacy/calendar/past-event-treatment.test.ts +137 -0
  78. package/src/components/ui/legacy/calendar/past-event-treatment.ts +25 -0
  79. package/src/components/ui/legacy/calendar/provider-color-picker.test.tsx +147 -0
  80. package/src/components/ui/legacy/calendar/provider-color-picker.tsx +115 -0
  81. package/src/components/ui/legacy/calendar/provider-readonly-dialog.test.tsx +52 -0
  82. package/src/components/ui/legacy/calendar/provider-readonly-dialog.tsx +48 -0
  83. package/src/components/ui/legacy/calendar/save-calendar-event-drafts.test.ts +35 -1
  84. package/src/components/ui/legacy/calendar/save-calendar-event-drafts.ts +23 -11
  85. package/src/components/ui/legacy/calendar/use-calendar-draft-save.test.tsx +119 -0
  86. package/src/components/ui/legacy/calendar/use-calendar-draft-save.tsx +141 -0
  87. package/src/components/ui/legacy/calendar/use-provider-recurrence-operation.test.tsx +185 -0
  88. package/src/components/ui/legacy/calendar/use-provider-recurrence-operation.ts +186 -0
  89. package/src/components/ui/popover.tsx +3 -1
  90. package/src/components/ui/text-editor/__tests__/collaboration-carets.test.ts +142 -0
  91. package/src/components/ui/text-editor/__tests__/markdown-document.test.ts +50 -0
  92. package/src/components/ui/text-editor/clipboard-serialization.ts +10 -0
  93. package/src/components/ui/text-editor/collaboration-carets.ts +56 -0
  94. package/src/components/ui/text-editor/editor-readonly-transition.test.tsx +2 -0
  95. package/src/components/ui/text-editor/editor.tsx +11 -3
  96. package/src/components/ui/text-editor/extensions.ts +8 -2
  97. package/src/components/ui/text-editor/fixed-toolbar-contents.test.tsx +59 -0
  98. package/src/components/ui/text-editor/fixed-toolbar-contents.tsx +156 -0
  99. package/src/components/ui/text-editor/markdown-paste-extension.ts +1 -1
  100. package/src/components/ui/text-editor/markdown.ts +11 -0
  101. package/src/components/ui/text-editor/tool-bar.tsx +45 -102
  102. package/src/hooks/__tests__/calendar-event-write-payload-readonly.test.ts +40 -0
  103. package/src/hooks/__tests__/collaboration-awareness.test.ts +106 -0
  104. package/src/hooks/__tests__/native-calendar-mutation-guard.test.ts +45 -0
  105. package/src/hooks/__tests__/supabase-provider.test.ts +110 -7
  106. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +39 -2
  107. package/src/hooks/__tests__/use-local-storage.test.tsx +42 -0
  108. package/src/hooks/__tests__/use-native-calendar-occurrences.test.tsx +140 -0
  109. package/src/hooks/__tests__/use-notifications-subscription.test.tsx +31 -1
  110. package/src/hooks/__tests__/use-workspace-presence.test.tsx +3 -3
  111. package/src/hooks/__tests__/use-yjs-collaboration.test.tsx +147 -0
  112. package/src/hooks/calendar-connection-types.ts +49 -0
  113. package/src/hooks/calendar-creation-request.test.ts +62 -0
  114. package/src/hooks/calendar-creation-request.ts +34 -0
  115. package/src/hooks/calendar-event-update-queue-types.ts +12 -0
  116. package/src/hooks/calendar-event-write-payload.ts +57 -0
  117. package/src/hooks/collaboration-document-hydration.ts +35 -0
  118. package/src/hooks/notification-subscription.ts +5 -2
  119. package/src/hooks/supabase-provider.ts +76 -112
  120. package/src/hooks/use-calendar-sync.tsx +41 -62
  121. package/src/hooks/use-calendar.tsx +44 -82
  122. package/src/hooks/use-exchange-rates.ts +4 -15
  123. package/src/hooks/use-local-storage.ts +9 -11
  124. package/src/hooks/use-native-calendar-occurrences.ts +85 -0
  125. package/src/hooks/use-workspace-presence.ts +9 -9
  126. package/src/hooks/use-yjs-collaboration.ts +68 -27
@@ -0,0 +1,40 @@
1
+ import type { CalendarEvent } from '@tuturuuu/types/primitives/calendar-event';
2
+ import { expect, it } from 'vitest';
3
+ import { calendarEventUpdatePayload } from '../../../../hooks/calendar-event-write-payload';
4
+ import { eventSavePayload } from './event-save-payload';
5
+
6
+ const original = {
7
+ id: 'saved',
8
+ title: 'Synthetic',
9
+ description: '',
10
+ location: '',
11
+ start_at: '2026-10-02T10:00:00Z',
12
+ end_at: '2026-10-02T11:00:00Z',
13
+ color: 'BLUE',
14
+ locked: false,
15
+ } as CalendarEvent;
16
+ it('sends only a provider color command for an unchanged existing event', () => {
17
+ const providerColor = { connectionId: 'owned', kind: 'inherit' as const };
18
+ const payload = eventSavePayload(
19
+ {
20
+ ...original,
21
+ providerColor,
22
+ source: { provider: 'google', connectionId: 'owned' },
23
+ },
24
+ original,
25
+ false
26
+ );
27
+ expect(payload).toEqual({ providerColor });
28
+ expect(calendarEventUpdatePayload(payload)).toEqual({ providerColor });
29
+ });
30
+ it('does not include unchanged native color or source with an ordinary title edit', () => {
31
+ expect(eventSavePayload({ ...original, title: 'Changed' }, original)).toEqual(
32
+ { title: 'Changed' }
33
+ );
34
+ });
35
+ it('includes a calendar transfer only when its source actually changed', () => {
36
+ const source = { provider: 'google' as const, connectionId: 'new' };
37
+ expect(eventSavePayload({ ...original, source }, original, true)).toEqual({
38
+ source,
39
+ });
40
+ });
@@ -0,0 +1,79 @@
1
+ import type { CalendarEvent } from '@tuturuuu/types/primitives/calendar-event';
2
+ import { newCalendarCreationRequestId } from '../../../../hooks/calendar-creation-request';
3
+
4
+ /** Existing edits send only changed fields so a color-only command neither
5
+ * broadcasts invitations nor accidentally becomes a calendar transfer. */
6
+ export function eventSavePayload(
7
+ draft: Partial<CalendarEvent>,
8
+ original?: Partial<CalendarEvent>,
9
+ sourceChanged = false
10
+ ) {
11
+ if (!original || original.id === 'new') return draft;
12
+ const updates: Partial<CalendarEvent> = {};
13
+ for (const field of [
14
+ 'title',
15
+ 'description',
16
+ 'location',
17
+ 'start_at',
18
+ 'end_at',
19
+ 'locked',
20
+ ] as const) {
21
+ if (
22
+ (draft[field] ?? (field === 'locked' ? false : '')) !==
23
+ (original[field] ?? (field === 'locked' ? false : ''))
24
+ )
25
+ Object.assign(updates, { [field]: draft[field] });
26
+ }
27
+ if (draft.providerColor) updates.providerColor = draft.providerColor;
28
+ else if (draft.color !== original.color) updates.color = draft.color;
29
+ if (sourceChanged) updates.source = draft.source;
30
+ return updates;
31
+ }
32
+
33
+ export function eventModalSavePayload(
34
+ draft: Partial<CalendarEvent>,
35
+ original: Partial<CalendarEvent> | undefined,
36
+ source: CalendarEvent['source'],
37
+ sourceChanged: boolean
38
+ ) {
39
+ return eventSavePayload(
40
+ {
41
+ title: draft.title || '',
42
+ description: draft.description || '',
43
+ start_at: draft.start_at,
44
+ end_at: draft.end_at,
45
+ color: draft.color || 'BLUE',
46
+ location: draft.location || '',
47
+ locked: draft.locked || false,
48
+ source,
49
+ providerColor: draft.providerColor,
50
+ },
51
+ original,
52
+ sourceChanged
53
+ );
54
+ }
55
+
56
+ export function eventModalDraft(
57
+ activeEvent: CalendarEvent
58
+ ): Partial<CalendarEvent> {
59
+ return {
60
+ id: activeEvent.id,
61
+ requestId:
62
+ activeEvent.id === 'new' ? newCalendarCreationRequestId() : undefined,
63
+ title: activeEvent.title || '',
64
+ description: activeEvent.description || '',
65
+ start_at: activeEvent.start_at,
66
+ end_at: activeEvent.end_at,
67
+ color: activeEvent.color || 'BLUE',
68
+ location: activeEvent.location || '',
69
+ locked: activeEvent.locked || false,
70
+ ws_id: activeEvent.ws_id,
71
+ provider: activeEvent.provider,
72
+ source_calendar_id: activeEvent.source_calendar_id,
73
+ external_calendar_id: activeEvent.external_calendar_id,
74
+ external_event_id: activeEvent.external_event_id,
75
+ google_event_id: activeEvent.google_event_id,
76
+ google_calendar_id: activeEvent.google_calendar_id,
77
+ scheduling_metadata: activeEvent.scheduling_metadata,
78
+ };
79
+ }
@@ -0,0 +1,95 @@
1
+ import type { CalendarSourceOption } from '@tuturuuu/internal-api';
2
+ import { expect, it } from 'vitest';
3
+ import { eventModalSavePayload } from './event-save-payload';
4
+ import {
5
+ eventSourceChanged,
6
+ findEventSourceOption,
7
+ selectedEventSource,
8
+ } from './event-source-selection';
9
+
10
+ const options = ['one', 'two'].map((id) => ({
11
+ id,
12
+ provider: 'google',
13
+ connectionId: id,
14
+ workspaceCalendarId: id,
15
+ externalCalendarId: 'shared-provider-calendar',
16
+ })) as CalendarSourceOption[];
17
+ it('matches the canonical workspace source despite identical provider calendar IDs', () => {
18
+ expect(
19
+ findEventSourceOption(options, {
20
+ provider: 'google',
21
+ source_calendar_id: 'two',
22
+ external_calendar_id: 'shared-provider-calendar',
23
+ })?.id
24
+ ).toBe('two');
25
+ expect(
26
+ findEventSourceOption(options, {
27
+ provider: 'google',
28
+ source_calendar_id: 'missing',
29
+ external_calendar_id: 'shared-provider-calendar',
30
+ })
31
+ ).toBeUndefined();
32
+ });
33
+ it('fails closed when a legacy external calendar ID is ambiguous across accounts', () => {
34
+ expect(
35
+ findEventSourceOption(options, {
36
+ provider: 'google',
37
+ external_calendar_id: 'shared-provider-calendar',
38
+ })
39
+ ).toBeUndefined();
40
+ expect(
41
+ findEventSourceOption(options.slice(0, 1), {
42
+ provider: 'google',
43
+ external_calendar_id: 'shared-provider-calendar',
44
+ })?.id
45
+ ).toBe('one');
46
+ });
47
+
48
+ it('leaves an unresolved existing event on its original source and preserves new defaults', () => {
49
+ const original = {
50
+ id: 'saved',
51
+ provider: 'google' as const,
52
+ source_calendar_id: 'missing',
53
+ external_calendar_id: 'shared-provider-calendar',
54
+ };
55
+ expect(
56
+ selectedEventSource(options, original, null, options[0])
57
+ ).toBeUndefined();
58
+ expect(eventSourceChanged(options, original, null)).toBe(false);
59
+ expect(
60
+ selectedEventSource(options, { id: 'new' }, null, options[0])?.id
61
+ ).toBe('one');
62
+ expect(selectedEventSource(options, original, 'two', options[0])?.id).toBe(
63
+ 'two'
64
+ );
65
+ expect(eventSourceChanged(options, original, 'two')).toBe(true);
66
+ });
67
+
68
+ it('sends only a title edit when an existing source cannot be resolved', () => {
69
+ const original = {
70
+ id: 'saved',
71
+ title: 'Original',
72
+ color: 'BLUE' as const,
73
+ provider: 'google' as const,
74
+ source_calendar_id: 'missing',
75
+ start_at: '2026-10-02T10:00:00Z',
76
+ end_at: '2026-10-02T11:00:00Z',
77
+ };
78
+ const selected = selectedEventSource(options, original, null, options[0]);
79
+ expect(
80
+ eventModalSavePayload(
81
+ { ...original, title: 'Changed' },
82
+ original,
83
+ selected?.provider === 'google'
84
+ ? { provider: 'google', connectionId: selected.connectionId }
85
+ : undefined,
86
+ eventSourceChanged(options, original, null)
87
+ )
88
+ ).toEqual({ title: 'Changed' });
89
+ });
90
+
91
+ it('recognizes a legacy Google event before considering native sources', () => {
92
+ const legacy = { google_event_id: 'external', source_calendar_id: 'two' };
93
+ expect(findEventSourceOption(options, legacy)?.id).toBe('two');
94
+ expect(eventSourceChanged(options, legacy, 'two')).toBe(false);
95
+ });
@@ -0,0 +1,78 @@
1
+ import type {
2
+ CalendarSourceInput,
3
+ CalendarSourceOption,
4
+ } from '@tuturuuu/internal-api';
5
+ import type { CalendarEvent } from '@tuturuuu/types/primitives/calendar-event';
6
+
7
+ export function sourceInputFromOption(
8
+ option?: CalendarSourceOption | null
9
+ ): CalendarSourceInput | undefined {
10
+ if (!option) return undefined;
11
+
12
+ if (option.provider === 'tuturuuu') {
13
+ return {
14
+ provider: 'tuturuuu',
15
+ workspaceCalendarId: option.workspaceCalendarId,
16
+ };
17
+ }
18
+
19
+ return {
20
+ provider: option.provider,
21
+ connectionId: option.connectionId,
22
+ };
23
+ }
24
+
25
+ export function findEventSourceOption(
26
+ options: CalendarSourceOption[],
27
+ event: Partial<CalendarEvent>
28
+ ) {
29
+ const provider =
30
+ event.provider ?? (event.google_event_id ? 'google' : undefined);
31
+ if (provider === 'google' || provider === 'microsoft') {
32
+ const matches = options.filter((option) => {
33
+ if (option.provider === 'tuturuuu' || option.provider !== provider)
34
+ return false;
35
+ if (event.source_calendar_id)
36
+ return option.workspaceCalendarId === event.source_calendar_id;
37
+ return (
38
+ option.externalCalendarId ===
39
+ (event.external_calendar_id ?? event.google_calendar_id)
40
+ );
41
+ });
42
+ // Legacy rows without a canonical source must not choose another account
43
+ // merely because it contains the same provider calendar ID.
44
+ return matches.length === 1 ? matches[0] : undefined;
45
+ }
46
+
47
+ return options.find(
48
+ (option) =>
49
+ option.provider === 'tuturuuu' &&
50
+ option.workspaceCalendarId === event.source_calendar_id
51
+ );
52
+ }
53
+
54
+ export function selectedEventSource(
55
+ options: CalendarSourceOption[],
56
+ event: Partial<CalendarEvent>,
57
+ selectedId: string | null,
58
+ defaultSource?: CalendarSourceOption
59
+ ) {
60
+ const selected = options.find((option) => option.id === selectedId);
61
+ const existing = !!event.id && event.id !== 'new';
62
+ return (
63
+ selected ??
64
+ findEventSourceOption(options, event) ??
65
+ (existing ? undefined : (defaultSource ?? options[0]))
66
+ );
67
+ }
68
+ export function eventSourceChanged(
69
+ options: CalendarSourceOption[],
70
+ event: Partial<CalendarEvent>,
71
+ selectedId: string | null
72
+ ) {
73
+ return (
74
+ selectedId !== null &&
75
+ options.some((option) => option.id === selectedId) &&
76
+ selectedId !== findEventSourceOption(options, event)?.id
77
+ );
78
+ }