@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
@@ -29,7 +29,16 @@ import { toast } from '@tuturuuu/ui/sonner';
29
29
  import { Textarea } from '@tuturuuu/ui/textarea';
30
30
  import { formatCurrency } from '@tuturuuu/utils/format';
31
31
  import { useTranslations } from 'next-intl';
32
- import { useEffect, useMemo, useState } from 'react';
32
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
33
+
34
+ type ReconciliationRequest = {
35
+ wsId: string;
36
+ walletId: string;
37
+ checkpointId: string;
38
+ payload: { category_id?: string; description: string };
39
+ onCreated: () => void;
40
+ onOpenChange: (open: boolean) => void;
41
+ };
33
42
 
34
43
  const NO_CATEGORY = 'none';
35
44
 
@@ -57,6 +66,10 @@ export function WalletCheckpointAdjustmentDialog({
57
66
  wsId: string;
58
67
  }) {
59
68
  const t = useTranslations('wallet-checkpoints');
69
+ const activeTarget = useRef({ wsId, walletId, checkpointId, open });
70
+ useLayoutEffect(() => {
71
+ activeTarget.current = { wsId, walletId, checkpointId, open };
72
+ }, [wsId, walletId, checkpointId, open]);
60
73
  const [categoryId, setCategoryId] = useState(NO_CATEGORY);
61
74
  const [description, setDescription] = useState(
62
75
  t('reconciliation_description', {
@@ -129,17 +142,28 @@ export function WalletCheckpointAdjustmentDialog({
129
142
  [checkedAt]
130
143
  );
131
144
  const mutation = useMutation({
132
- mutationFn: () =>
133
- createWalletCheckpointReconciliation(wsId, walletId, checkpointId, {
134
- category_id: categoryId === NO_CATEGORY ? undefined : categoryId,
135
- description,
136
- }),
137
- onSuccess: (result) => {
145
+ mutationFn: (request: ReconciliationRequest) =>
146
+ createWalletCheckpointReconciliation(
147
+ request.wsId,
148
+ request.walletId,
149
+ request.checkpointId,
150
+ request.payload
151
+ ),
152
+ onSuccess: (result, request) => {
153
+ // Invalidate the submitted target even if the dialog has since moved.
154
+ request.onCreated();
155
+ const current = activeTarget.current;
156
+ if (
157
+ !current.open ||
158
+ current.wsId !== request.wsId ||
159
+ current.walletId !== request.walletId ||
160
+ current.checkpointId !== request.checkpointId
161
+ )
162
+ return;
138
163
  toast.success(
139
164
  result.created ? t('reconciliation_created') : t('reconciliation_clean')
140
165
  );
141
- onCreated();
142
- onOpenChange(false);
166
+ request.onOpenChange(false);
143
167
  },
144
168
  onError: (error) => {
145
169
  toast.error(
@@ -208,7 +232,20 @@ export function WalletCheckpointAdjustmentDialog({
208
232
  </Button>
209
233
  <Button
210
234
  disabled={mutation.isPending || !categoryInitialized}
211
- onClick={() => mutation.mutate()}
235
+ onClick={() =>
236
+ mutation.mutate({
237
+ wsId,
238
+ walletId,
239
+ checkpointId,
240
+ payload: {
241
+ category_id:
242
+ categoryId === NO_CATEGORY ? undefined : categoryId,
243
+ description,
244
+ },
245
+ onCreated,
246
+ onOpenChange,
247
+ })
248
+ }
212
249
  >
213
250
  {mutation.isPending ? t('creating') : t('reconcile')}
214
251
  </Button>
@@ -1,5 +1,11 @@
1
1
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
- import { fireEvent, render, screen, waitFor } from '@testing-library/react';
2
+ import {
3
+ act,
4
+ fireEvent,
5
+ render,
6
+ screen,
7
+ waitFor,
8
+ } from '@testing-library/react';
3
9
  import type { ReactElement, ReactNode } from 'react';
4
10
  import { beforeEach, describe, expect, it, vi } from 'vitest';
5
11
  import { WalletCheckpointAdjustmentDialog } from './wallet-checkpoint-adjustment-dialog';
@@ -385,6 +391,94 @@ describe('wallet checkpoint UI', () => {
385
391
  });
386
392
  });
387
393
 
394
+ it('submits the category visible at click time despite a subsequent edit', async () => {
395
+ mocks.defaultReconciliationCategoryId = 'category-reconcile';
396
+ mocks.listTransactionCategories.mockResolvedValue([
397
+ { id: 'category-reconcile', name: 'Audit' },
398
+ ]);
399
+ renderWithQueryClient(
400
+ <WalletCheckpointAdjustmentDialog
401
+ wsId="ws-1"
402
+ walletId="wallet-1"
403
+ checkpointId="checkpoint-1"
404
+ walletName="Cash"
405
+ checkedAt="2026-06-11T10:00:00.000Z"
406
+ currency="USD"
407
+ variance={-12.34}
408
+ open
409
+ onOpenChange={vi.fn()}
410
+ onCreated={vi.fn()}
411
+ />
412
+ );
413
+ const select = screen.getByRole('combobox');
414
+ await waitFor(() => expect(select).toHaveValue('category-reconcile'));
415
+ const button = screen.getByRole('button', { name: 'reconcile' });
416
+ await act(async () => {
417
+ fireEvent.click(button);
418
+ fireEvent.change(select, { target: { value: 'none' } });
419
+ });
420
+ await waitFor(() =>
421
+ expect(mocks.createWalletCheckpointReconciliation).toHaveBeenCalledWith(
422
+ 'ws-1',
423
+ 'wallet-1',
424
+ 'checkpoint-1',
425
+ expect.objectContaining({ category_id: 'category-reconcile' })
426
+ )
427
+ );
428
+ });
429
+
430
+ it('keeps a queued reconciliation and completion bound to its original target', async () => {
431
+ const oldCreated = vi.fn();
432
+ const newCreated = vi.fn();
433
+ const oldClose = vi.fn();
434
+ const newClose = vi.fn();
435
+ const client = new QueryClient({
436
+ defaultOptions: { queries: { retry: false } },
437
+ });
438
+ const dialog = (
439
+ wallet: string,
440
+ checkpoint: string,
441
+ created: () => void,
442
+ close: (open: boolean) => void
443
+ ) => (
444
+ <QueryClientProvider client={client}>
445
+ <WalletCheckpointAdjustmentDialog
446
+ wsId="ws-1"
447
+ walletId={wallet}
448
+ checkpointId={checkpoint}
449
+ walletName="Cash"
450
+ checkedAt="2026-06-11T10:00:00.000Z"
451
+ currency="USD"
452
+ variance={-12.34}
453
+ open
454
+ onOpenChange={close}
455
+ onCreated={created}
456
+ />
457
+ </QueryClientProvider>
458
+ );
459
+ const view = render(
460
+ dialog('wallet-1', 'checkpoint-1', oldCreated, oldClose)
461
+ );
462
+ const button = screen.getByRole('button', { name: 'reconcile' });
463
+ await waitFor(() => expect(button).not.toBeDisabled());
464
+ await act(async () => {
465
+ fireEvent.click(button);
466
+ view.rerender(dialog('wallet-2', 'checkpoint-2', newCreated, newClose));
467
+ });
468
+ await waitFor(() =>
469
+ expect(mocks.createWalletCheckpointReconciliation).toHaveBeenCalledWith(
470
+ 'ws-1',
471
+ 'wallet-1',
472
+ 'checkpoint-1',
473
+ expect.any(Object)
474
+ )
475
+ );
476
+ await waitFor(() => expect(oldCreated).toHaveBeenCalledOnce());
477
+ expect(newCreated).not.toHaveBeenCalled();
478
+ expect(oldClose).not.toHaveBeenCalled();
479
+ expect(newClose).not.toHaveBeenCalled();
480
+ });
481
+
388
482
  it('submits no reconciliation category after clearing the configured default', async () => {
389
483
  mocks.defaultReconciliationCategoryId = 'category-reconcile';
390
484
  mocks.listTransactionCategories.mockResolvedValue([
@@ -27,6 +27,7 @@ import {
27
27
  import { Button } from '../../button';
28
28
  import { Input } from '../../input';
29
29
  import { calendarDraftDate } from './calendar-period';
30
+ import { pastEventTreatment } from './past-event-treatment';
30
31
  import { useCalendarSettings } from './settings/settings-context';
31
32
 
32
33
  dayjs.extend(utc);
@@ -72,6 +73,7 @@ function EventCard({
72
73
  onOpen: (id: string) => void;
73
74
  }) {
74
75
  const { settings } = useCalendarSettings();
76
+ const { preservePastEventOpacity } = useCalendar();
75
77
  const isAllDay = isAllDayEvent(event);
76
78
 
77
79
  return (
@@ -80,7 +82,8 @@ function EventCard({
80
82
  onClick={() => onOpen(event.id)}
81
83
  style={calendarEventStyle(event)}
82
84
  className={cn(
83
- 'group flex w-full cursor-pointer items-start gap-3 rounded-lg border-l-[3px] px-3 py-2.5 text-left transition-colors hover:ring-1 hover:ring-current'
85
+ 'group relative flex w-full cursor-pointer items-start gap-3 rounded-lg border-l-[3px] px-3 py-2.5 text-left transition-colors hover:ring-1 hover:ring-current',
86
+ pastEventTreatment(event, preservePastEventOpacity)
84
87
  )}
85
88
  >
86
89
  {/* Time column */}