@tuturuuu/ui 0.8.0 → 0.9.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 (182) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/biome.json +1 -1
  3. package/package.json +73 -71
  4. package/src/components/ui/accordion.tsx +1 -1
  5. package/src/components/ui/breadcrumb.tsx +1 -1
  6. package/src/components/ui/calendar-app/calendar-page-shell.tsx +4 -0
  7. package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +239 -33
  8. package/src/components/ui/calendar-app/components/load-smart-scheduling-tasks.tsx +143 -0
  9. package/src/components/ui/calendar-app/components/priority-view.tsx +10 -3
  10. package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +4 -116
  11. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +67 -2
  12. package/src/components/ui/calendar.tsx +1 -1
  13. package/src/components/ui/carousel.tsx +1 -1
  14. package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +1 -1
  15. package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +1 -1
  16. package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +1 -1
  17. package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +1 -1
  18. package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +1 -1
  19. package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +1 -1
  20. package/src/components/ui/chat/chat-agent-details-sidebar.tsx +2 -2
  21. package/src/components/ui/chat/chat-agent-details-utils.test.ts +1 -1
  22. package/src/components/ui/chat/chat-agent-details-utils.tsx +1 -1
  23. package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +2 -2
  24. package/src/components/ui/checkbox.tsx +1 -1
  25. package/src/components/ui/color-picker.tsx +1 -1
  26. package/src/components/ui/command.tsx +1 -1
  27. package/src/components/ui/context-menu.tsx +5 -1
  28. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
  29. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
  30. package/src/components/ui/custom/combobox.test.tsx +195 -0
  31. package/src/components/ui/custom/combobox.tsx +273 -156
  32. package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
  33. package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
  34. package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
  35. package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
  36. package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
  37. package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
  38. package/src/components/ui/custom/theme-toggle.tsx +1 -1
  39. package/src/components/ui/custom/workspace-select.tsx +8 -3
  40. package/src/components/ui/dialog.test.tsx +52 -0
  41. package/src/components/ui/dialog.tsx +6 -2
  42. package/src/components/ui/dropdown-menu.tsx +5 -1
  43. package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
  44. package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
  45. package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
  46. package/src/components/ui/finance/debts/debts-page.tsx +15 -2
  47. package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
  48. package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
  49. package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
  50. package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
  51. package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
  52. package/src/components/ui/finance/invoices/utils.ts +3 -1
  53. package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
  54. package/src/components/ui/finance/transactions/form-types.ts +1 -0
  55. package/src/components/ui/finance/transactions/form.tsx +2 -0
  56. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
  57. package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
  58. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
  59. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
  60. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
  61. package/src/components/ui/finance/wallets/form.test.tsx +51 -3
  62. package/src/components/ui/finance/wallets/form.tsx +15 -4
  63. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
  64. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
  65. package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
  66. package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
  67. package/src/components/ui/input-otp.tsx +1 -1
  68. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
  69. package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
  70. package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
  71. package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
  72. package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
  73. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
  74. package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
  75. package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
  76. package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
  77. package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
  78. package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
  79. package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
  80. package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
  81. package/src/components/ui/navigation-menu.tsx +1 -1
  82. package/src/components/ui/pagination.tsx +1 -1
  83. package/src/components/ui/radio-group.tsx +1 -1
  84. package/src/components/ui/select.tsx +5 -1
  85. package/src/components/ui/sheet.tsx +1 -1
  86. package/src/components/ui/sidebar.tsx +1 -1
  87. package/src/components/ui/storefront/cart-popover.tsx +61 -0
  88. package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
  89. package/src/components/ui/storefront/cart-summary.tsx +93 -154
  90. package/src/components/ui/storefront/checkout-overlay.tsx +4 -5
  91. package/src/components/ui/storefront/listing-card.tsx +1 -1
  92. package/src/components/ui/storefront/merch-sections.tsx +70 -0
  93. package/src/components/ui/storefront/product-detail.tsx +1 -1
  94. package/src/components/ui/storefront/storefront-surface.test.tsx +106 -11
  95. package/src/components/ui/storefront/storefront-surface.tsx +101 -166
  96. package/src/components/ui/storefront/types.ts +4 -0
  97. package/src/components/ui/storefront/utils.ts +6 -0
  98. package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
  99. package/src/components/ui/text-editor/background-color-extension.ts +62 -0
  100. package/src/components/ui/text-editor/color-controls.tsx +284 -0
  101. package/src/components/ui/text-editor/editor.tsx +69 -14
  102. package/src/components/ui/text-editor/extensions.ts +8 -2
  103. package/src/components/ui/text-editor/highlight-extension.ts +22 -0
  104. package/src/components/ui/text-editor/tool-bar.tsx +9 -16
  105. package/src/components/ui/toast.tsx +1 -1
  106. package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
  107. package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
  108. package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
  109. package/src/components/ui/tu-do/boards/boardId/board-column.tsx +112 -43
  110. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
  111. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
  112. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
  113. package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
  114. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
  115. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
  116. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +38 -9
  117. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
  118. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
  119. package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +81 -30
  120. package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
  121. package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
  122. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
  123. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
  124. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
  125. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
  126. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
  127. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
  128. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
  129. package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
  130. package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
  131. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +397 -2
  132. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +103 -13
  133. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
  134. package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
  135. package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
  136. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +26 -4
  137. package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +5 -2
  138. package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
  139. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
  140. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
  141. package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
  142. package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
  143. package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
  144. package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
  145. package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
  146. package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
  147. package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
  148. package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
  149. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
  150. package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
  151. package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
  152. package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
  153. package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
  154. package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +203 -2
  155. package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
  156. package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
  157. package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
  158. package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
  159. package/src/components/ui/tu-do/shared/board-header.tsx +464 -975
  160. package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
  161. package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
  162. package/src/components/ui/tu-do/shared/board-views.tsx +587 -75
  163. package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
  164. package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
  165. package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
  166. package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
  167. package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
  168. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
  169. package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
  170. package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
  171. package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
  172. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +2 -1
  173. package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
  174. package/src/declarations.d.ts +1 -0
  175. package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
  176. package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
  177. package/src/hooks/use-calendar-sync.tsx +247 -243
  178. package/src/hooks/use-calendar.tsx +323 -138
  179. package/src/hooks/use-task-actions.ts +24 -0
  180. package/src/hooks/use-user-workspace-config.ts +75 -0
  181. package/src/hooks/use-workspace-currency.ts +8 -3
  182. package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
@@ -7,6 +7,7 @@ import ExportDialogContent from '@tuturuuu/ui/finance/transactions/export-dialog
7
7
  import { TransactionsCreateSummary } from '@tuturuuu/ui/finance/transactions/transactions-create-summary';
8
8
  import { TransactionsInfinitePage } from '@tuturuuu/ui/finance/transactions/transactions-infinite-page';
9
9
  import { Separator } from '@tuturuuu/ui/separator';
10
+ import { resolveSupportedCurrency } from '@tuturuuu/utils/currencies';
10
11
  import {
11
12
  getPermissions,
12
13
  getWorkspace,
@@ -51,6 +52,7 @@ export default async function TransactionsPage({
51
52
  ]);
52
53
  if (!resolvedWorkspace || !resolvedPermissions) return notFound();
53
54
  const { containsPermission } = resolvedPermissions;
55
+ const workspaceCurrency = resolveSupportedCurrency(resolvedCurrency);
54
56
 
55
57
  const canViewTransactions = containsPermission('view_transactions');
56
58
  const canExportFinanceData = containsPermission('export_finance_data');
@@ -110,13 +112,14 @@ export default async function TransactionsPage({
110
112
  canChangeFinanceWallets={canChangeFinanceWallets}
111
113
  canSetFinanceWalletsOnCreate={canSetFinanceWalletsOnCreate}
112
114
  canCreateConfidentialTransactions={canCreateConfidentialTransactions}
115
+ defaultCurrency={workspaceCurrency}
113
116
  timezone={resolvedWorkspace.timezone}
114
117
  permissionRequestUser={permissionRequestUser}
115
118
  />
116
119
  <Separator className="my-4" />
117
120
  <TransactionsInfinitePage
118
121
  wsId={wsId}
119
- currency={resolvedCurrency ?? 'USD'}
122
+ currency={workspaceCurrency}
120
123
  timezone={resolvedWorkspace.timezone}
121
124
  canExport={canExportFinanceData}
122
125
  exportContent={
@@ -5,10 +5,27 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
5
5
  import type { WalletFormValues } from './form';
6
6
  import { WalletForm } from './form';
7
7
 
8
+ const mocks = vi.hoisted(() => ({
9
+ createWallet: vi.fn(),
10
+ updateWallet: vi.fn(),
11
+ }));
12
+
8
13
  vi.mock('@tuturuuu/ui/hooks/use-workspace-currency', () => ({
9
- useWorkspaceCurrency: () => ({ currency: 'USD' }),
14
+ useWorkspaceCurrency: (_wsId: string, fallbackCurrency = 'USD') => ({
15
+ currency: fallbackCurrency,
16
+ }),
10
17
  }));
11
18
 
19
+ vi.mock('@tuturuuu/internal-api/finance', async (importOriginal) => {
20
+ const actual =
21
+ await importOriginal<typeof import('@tuturuuu/internal-api/finance')>();
22
+ return {
23
+ ...actual,
24
+ createWallet: mocks.createWallet,
25
+ updateWallet: mocks.updateWallet,
26
+ };
27
+ });
28
+
12
29
  vi.mock('next/navigation', () => ({
13
30
  useRouter: () => ({ refresh: vi.fn() }),
14
31
  }));
@@ -29,6 +46,7 @@ function renderWalletForm(
29
46
  options: {
30
47
  data?: ComponentProps<typeof WalletForm>['data'];
31
48
  defaultType?: WalletFormValues['type'];
49
+ defaultCurrency?: string;
32
50
  } = {}
33
51
  ) {
34
52
  const data =
@@ -41,13 +59,18 @@ function renderWalletForm(
41
59
  name: 'Primary',
42
60
  type: 'STANDARD',
43
61
  } as never);
44
- const { defaultType } = options;
62
+ const { defaultCurrency, defaultType } = options;
45
63
 
46
64
  const queryClient = new QueryClient();
47
65
 
48
66
  return render(
49
67
  <QueryClientProvider client={queryClient}>
50
- <WalletForm wsId="ws-1" data={data} defaultType={defaultType} />
68
+ <WalletForm
69
+ wsId="ws-1"
70
+ data={data}
71
+ defaultType={defaultType}
72
+ defaultCurrency={defaultCurrency}
73
+ />
51
74
  </QueryClientProvider>
52
75
  );
53
76
  }
@@ -69,6 +92,8 @@ function typeIntoCurrencyInput(input: HTMLInputElement, value: string) {
69
92
  describe('WalletForm', () => {
70
93
  beforeEach(() => {
71
94
  vi.clearAllMocks();
95
+ mocks.createWallet.mockResolvedValue({ message: 'success' });
96
+ mocks.updateWallet.mockResolvedValue({ message: 'success' });
72
97
  // biome-ignore lint/suspicious/noDocumentCookie: test resets the finance visibility cookie.
73
98
  document.cookie =
74
99
  'finance-confidential-mode=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/';
@@ -118,6 +143,29 @@ describe('WalletForm', () => {
118
143
  );
119
144
  });
120
145
 
146
+ it('initializes new wallet currency from the supplied workspace default', async () => {
147
+ renderWalletForm({
148
+ data: undefined,
149
+ defaultCurrency: 'SGD',
150
+ });
151
+
152
+ fireEvent.change(screen.getByLabelText('wallet-data-table.wallet_name'), {
153
+ target: { value: 'Singapore Cash' },
154
+ });
155
+ fireEvent.click(screen.getByRole('button', { name: 'ws-wallets.create' }));
156
+
157
+ await waitFor(() =>
158
+ expect(mocks.createWallet).toHaveBeenCalledWith(
159
+ 'ws-1',
160
+ expect.objectContaining({
161
+ currency: 'SGD',
162
+ name: 'Singapore Cash',
163
+ type: 'STANDARD',
164
+ })
165
+ )
166
+ );
167
+ });
168
+
121
169
  it('keeps accepting large credit limits after locale grouping is inserted', () => {
122
170
  renderWalletForm({
123
171
  data: {
@@ -20,6 +20,7 @@ import { Input } from '@tuturuuu/ui/input';
20
20
  import { zodResolver } from '@tuturuuu/ui/resolvers';
21
21
  import {
22
22
  getCurrencyLocale,
23
+ resolveSupportedCurrency,
23
24
  SUPPORTED_CURRENCIES,
24
25
  } from '@tuturuuu/utils/currencies';
25
26
  import { useRouter } from 'next/navigation';
@@ -97,6 +98,7 @@ interface Props {
97
98
  wsId: string;
98
99
  data?: Wallet;
99
100
  defaultType?: WalletFormValues['type'];
101
+ defaultCurrency?: string;
100
102
  onFinish?: (data: WalletFormValues) => void;
101
103
  isPersonalWorkspace?: boolean;
102
104
  }
@@ -105,11 +107,20 @@ export function WalletForm({
105
107
  wsId,
106
108
  data,
107
109
  defaultType = 'STANDARD',
110
+ defaultCurrency,
108
111
  onFinish,
109
112
  isPersonalWorkspace,
110
113
  }: Props) {
111
114
  const t = useTranslations();
112
- const { currency: workspaceCurrency } = useWorkspaceCurrency(wsId);
115
+ const fallbackCurrency = resolveSupportedCurrency(defaultCurrency);
116
+ const { currency: workspaceCurrency } = useWorkspaceCurrency(
117
+ wsId,
118
+ fallbackCurrency
119
+ );
120
+ const resolvedWorkspaceCurrency = resolveSupportedCurrency(
121
+ workspaceCurrency,
122
+ fallbackCurrency
123
+ );
113
124
  const { isConfidential: areNumbersHidden } =
114
125
  useFinanceConfidentialVisibility();
115
126
  const queryClient = useQueryClient();
@@ -127,7 +138,7 @@ export function WalletForm({
127
138
  description: data?.description || '',
128
139
  balance: data?.balance || 0,
129
140
  type: data?.type || defaultType,
130
- currency: data?.currency || workspaceCurrency || 'USD',
141
+ currency: data?.currency || resolvedWorkspaceCurrency,
131
142
  icon: data?.icon || null,
132
143
  image_src: data?.image_src || null,
133
144
  limit: data?.limit ?? undefined,
@@ -193,7 +204,7 @@ export function WalletForm({
193
204
  ? ''
194
205
  : new Intl.NumberFormat(
195
206
  getCurrencyLocale(
196
- walletCurrency || workspaceCurrency || 'USD'
207
+ walletCurrency || resolvedWorkspaceCurrency
197
208
  ),
198
209
  {
199
210
  maximumFractionDigits: 2,
@@ -296,7 +307,7 @@ export function WalletForm({
296
307
  <WalletCreditFields
297
308
  form={form}
298
309
  loading={loading}
299
- currency={walletCurrency || workspaceCurrency || 'USD'}
310
+ currency={walletCurrency || resolvedWorkspaceCurrency}
300
311
  />
301
312
  )}
302
313
 
@@ -35,6 +35,7 @@ interface WalletDetailsActionsProps {
35
35
  canSetFinanceWalletsOnCreate?: boolean;
36
36
  canDeleteWallets: boolean;
37
37
  isPersonalWorkspace: boolean;
38
+ defaultCurrency?: string;
38
39
  timezone?: string | null;
39
40
  permissionRequestUser?: FinancePermissionRequestUser | null;
40
41
  }
@@ -51,6 +52,7 @@ export function WalletDetailsActions({
51
52
  canSetFinanceWalletsOnCreate,
52
53
  canDeleteWallets,
53
54
  isPersonalWorkspace,
55
+ defaultCurrency,
54
56
  timezone,
55
57
  permissionRequestUser,
56
58
  }: WalletDetailsActionsProps) {
@@ -110,6 +112,7 @@ export function WalletDetailsActions({
110
112
  <WalletForm
111
113
  wsId={wsId}
112
114
  data={wallet}
115
+ defaultCurrency={defaultCurrency}
113
116
  isPersonalWorkspace={isPersonalWorkspace}
114
117
  />
115
118
  }
@@ -210,6 +213,7 @@ export function WalletDetailsActions({
210
213
  canCreateConfidentialTransactions={
211
214
  canCreateConfidentialTransactions
212
215
  }
216
+ defaultCurrency={defaultCurrency}
213
217
  timezone={timezone}
214
218
  preferInitialWalletSelection={transactionAction !== 'payment'}
215
219
  refreshPageOnFinish
@@ -11,6 +11,7 @@ import type { FinancePermissionRequestUser } from '@tuturuuu/ui/finance/shared/f
11
11
  import { InfiniteTransactionsList } from '@tuturuuu/ui/finance/transactions/infinite-transactions-list';
12
12
  import { Separator } from '@tuturuuu/ui/separator';
13
13
  import { Skeleton } from '@tuturuuu/ui/skeleton';
14
+ import { resolveSupportedCurrency } from '@tuturuuu/utils/currencies';
14
15
  import type { ExchangeRate } from '@tuturuuu/utils/exchange-rates';
15
16
  import { getCurrencyLocale } from '@tuturuuu/utils/format';
16
17
  import {
@@ -117,8 +118,8 @@ export default async function WalletDetailsPage({
117
118
  notFound();
118
119
  }
119
120
 
120
- const currency = wallet.currency || resolvedDefaultCurrency || 'USD';
121
- const workspaceCurrency = resolvedDefaultCurrency || 'USD';
121
+ const workspaceCurrency = resolveSupportedCurrency(resolvedDefaultCurrency);
122
+ const currency = wallet.currency || workspaceCurrency;
122
123
  const initialAction = getInitialWalletAction(searchParams.action);
123
124
 
124
125
  // Fetch exchange rates for conversion display
@@ -153,6 +154,7 @@ export default async function WalletDetailsPage({
153
154
  canSetFinanceWalletsOnCreate={canSetFinanceWalletsOnCreate}
154
155
  canDeleteWallets={canDeleteWallets}
155
156
  isPersonalWorkspace={!!resolvedWorkspace.personal}
157
+ defaultCurrency={workspaceCurrency}
156
158
  timezone={resolvedWorkspace.timezone}
157
159
  permissionRequestUser={permissionRequestUser}
158
160
  />
@@ -228,6 +228,7 @@ export function WalletsDataTable({
228
228
  <WalletForm
229
229
  wsId={wsId}
230
230
  data={selectedWallet}
231
+ defaultCurrency={currency}
231
232
  onFinish={handleEditComplete}
232
233
  isPersonalWorkspace={isPersonalWorkspace}
233
234
  />
@@ -6,6 +6,7 @@ import { WalletTotalCheckDialog } from '@tuturuuu/ui/finance/wallets/checkpoints
6
6
  import { WalletForm } from '@tuturuuu/ui/finance/wallets/form';
7
7
  import { WalletsDataTable } from '@tuturuuu/ui/finance/wallets/wallets-data-table';
8
8
  import { Separator } from '@tuturuuu/ui/separator';
9
+ import { resolveSupportedCurrency } from '@tuturuuu/utils/currencies';
9
10
  import {
10
11
  getPermissions,
11
12
  getWorkspace,
@@ -51,6 +52,7 @@ export default async function WalletsPage({
51
52
  ]);
52
53
  if (!resolvedPermissions || !resolvedWorkspace) notFound();
53
54
  const { containsPermission } = resolvedPermissions;
55
+ const workspaceCurrency = resolveSupportedCurrency(resolvedCurrency);
54
56
 
55
57
  const canCreateWallets = containsPermission('create_wallets');
56
58
  const canUpdateWallets = containsPermission('update_wallets');
@@ -76,6 +78,7 @@ export default async function WalletsPage({
76
78
  <WalletForm
77
79
  wsId={wsId}
78
80
  defaultType={isCreditCardCreate ? 'CREDIT' : 'STANDARD'}
81
+ defaultCurrency={workspaceCurrency}
79
82
  />
80
83
  ) : undefined
81
84
  }
@@ -95,7 +98,7 @@ export default async function WalletsPage({
95
98
  />
96
99
  <WalletTotalCheckDialog
97
100
  wsId={wsId}
98
- currency={resolvedCurrency ?? 'USD'}
101
+ currency={workspaceCurrency}
99
102
  canUpdateWallets={canUpdateWallets}
100
103
  defaultOpen={searchParams.tool === 'all-wallet-check'}
101
104
  />
@@ -105,7 +108,7 @@ export default async function WalletsPage({
105
108
  wsId={wsId}
106
109
  canUpdateWallets={canUpdateWallets}
107
110
  canDeleteWallets={canDeleteWallets}
108
- currency={resolvedCurrency ?? 'USD'}
111
+ currency={workspaceCurrency}
109
112
  financePrefix={financePrefix}
110
113
  isPersonalWorkspace={!!resolvedWorkspace?.personal}
111
114
  query={searchParams.q}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { MinusIcon } from '@tuturuuu/icons';
3
+ import { MinusIcon } from '@tuturuuu/icons/lucide-static';
4
4
  import { cn } from '@tuturuuu/utils/format';
5
5
  import { OTPInput, OTPInputContext, type SlotProps } from 'input-otp';
6
6
  import * as React from 'react';
@@ -9,9 +9,9 @@ import isBetween from 'dayjs/plugin/isBetween';
9
9
  import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
10
10
  import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
11
11
  import timezone from 'dayjs/plugin/timezone';
12
- import Image from 'next/image';
13
12
  import React, { useCallback, useMemo, useRef, useState } from 'react';
14
13
  import { MIN_COLUMN_WIDTH } from './config';
14
+ import { CalendarEventProviderIcon } from './event-provider-display';
15
15
  import { getLocationType, LocationTimeline } from './location-timeline';
16
16
  import { useCalendarSettings } from './settings/settings-context';
17
17
 
@@ -68,18 +68,10 @@ interface DragState {
68
68
  // 1. Extract EventContent component for shared rendering
69
69
  const EventContent = ({ event }: { event: CalendarEvent }) => (
70
70
  <>
71
- {typeof event.google_event_id === 'string' &&
72
- event.google_event_id.trim() !== '' && (
73
- <Image
74
- src="/media/google-calendar-icon.png"
75
- alt="Google Calendar"
76
- className="mr-1 inline-block h-[1.25em] w-[1.25em] align-middle opacity-80 dark:opacity-90"
77
- title="Synced from Google Calendar"
78
- data-testid="google-calendar-logo"
79
- width={18}
80
- height={18}
81
- />
82
- )}
71
+ <CalendarEventProviderIcon
72
+ event={event}
73
+ className="mr-1 h-[1.25em] w-[1.25em] opacity-80 dark:opacity-90"
74
+ />
83
75
  <span className="truncate">{event.title}</span>
84
76
  </>
85
77
  );
@@ -342,27 +334,6 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
342
334
  eventStart.isBefore(lastVisibleDate.add(1, 'day'), 'day') &&
343
335
  eventEnd.isAfter(firstVisibleDate, 'day');
344
336
 
345
- // Debug logging for multi-day events
346
- const eventDurationDays = eventEnd.diff(eventStart, 'day');
347
- if (eventDurationDays > 0) {
348
- console.log('Multi-day event processing:', {
349
- title: event.title,
350
- eventStart: eventStart.format('YYYY-MM-DD'),
351
- eventEnd: eventEnd.format('YYYY-MM-DD'),
352
- durationDays: eventDurationDays,
353
- isActuallyMultiDay: eventDurationDays > 1,
354
- firstVisibleDate: firstVisibleDate.format('YYYY-MM-DD'),
355
- lastVisibleDate: lastVisibleDate.format('YYYY-MM-DD'),
356
- eventOverlaps,
357
- wouldShowCutOffStart:
358
- eventDurationDays > 1 &&
359
- eventStart.isBefore(firstVisibleDate, 'day'),
360
- wouldShowCutOffEnd:
361
- eventDurationDays > 1 &&
362
- eventEnd.isAfter(lastVisibleDate.add(1, 'day'), 'day'),
363
- });
364
- }
365
-
366
337
  if (!eventOverlaps) {
367
338
  return; // Skip this event if it doesn't overlap with visible dates
368
339
  }
@@ -680,7 +651,13 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
680
651
  };
681
652
 
682
653
  // Calculate dynamic height based on visible events
683
- const barHeight = Math.max(1.9, eventLayout.maxVisibleEventsPerDay * 1.75);
654
+ const locationTopOffset =
655
+ locationSpans.length > 0 ? LOCATION_TIMELINE_HEIGHT_REM : 0;
656
+ const regularRowsHeight =
657
+ eventLayout.maxVisibleEventsPerDay > 0
658
+ ? eventLayout.maxVisibleEventsPerDay * 1.75
659
+ : 0;
660
+ const barHeight = Math.max(1.9, locationTopOffset + regularRowsHeight);
684
661
 
685
662
  // Enhanced mouse and touch handlers
686
663
  const handleEventMouseDown = (e: React.MouseEvent, eventSpan: EventSpan) => {
@@ -839,7 +816,7 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
839
816
  onClick={() => toggleDateExpansion(dateKey)}
840
817
  style={{
841
818
  position: 'absolute',
842
- top: `${MAX_EVENTS_DISPLAY * 1.7}rem`,
819
+ top: `${locationTopOffset + MAX_EVENTS_DISPLAY * 1.7}rem`,
843
820
  left: `${(dateIndex * 100) / visibleDates.length}%`,
844
821
  width: `${100 / visibleDates.length}%`,
845
822
  zIndex: 10,
@@ -858,7 +835,7 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
858
835
  onClick={() => toggleDateExpansion(dateKey)}
859
836
  style={{
860
837
  position: 'absolute',
861
- top: `${dateEvents.length * 1.7}rem`,
838
+ top: `${locationTopOffset + dateEvents.length * 1.7}rem`,
862
839
  left: `${(dateIndex * 100) / visibleDates.length}%`,
863
840
  width: `${100 / visibleDates.length}%`,
864
841
  zIndex: 10,
@@ -941,8 +918,18 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
941
918
  dragState.isDragging && dragState.draggedEvent?.id === event.id;
942
919
 
943
920
  // Calculate top offset based on location strip presence
944
- const topOffset =
945
- locationSpans.length > 0 ? LOCATION_TIMELINE_HEIGHT_REM : 0;
921
+ const topOffset = locationTopOffset;
922
+ const optimisticStatus = (
923
+ event as CalendarEvent & {
924
+ _optimisticStatus?:
925
+ | 'creating'
926
+ | 'updating'
927
+ | 'deleting'
928
+ | 'error';
929
+ }
930
+ )._optimisticStatus;
931
+ const isPendingMutation =
932
+ optimisticStatus === 'updating' || optimisticStatus === 'deleting';
946
933
 
947
934
  return (
948
935
  <div
@@ -959,6 +946,8 @@ export const AllDayEventBar = ({ dates }: { dates: Date[] }) => {
959
946
  bg,
960
947
  border,
961
948
  text,
949
+ isPendingMutation &&
950
+ 'opacity-60 outline outline-dashed outline-1 outline-primary/50',
962
951
  // Special styling for cut-off events
963
952
  (isCutOffStart || isCutOffEnd) && 'border-dashed'
964
953
  )}
@@ -880,6 +880,8 @@ export const CalendarCell = ({ date, hour }: CalendarCellProps) => {
880
880
  height: `${HOUR_HEIGHT}px`,
881
881
  }}
882
882
  onContextMenu={(e) => {
883
+ const target = e.target as HTMLElement | null;
884
+ if (target?.closest('[data-slot="context-menu-trigger"]')) return;
883
885
  e.preventDefault();
884
886
  }}
885
887
  onMouseEnter={() => setIsHovering(true)}
@@ -9,6 +9,7 @@ import { cn } from '@tuturuuu/utils/format';
9
9
  import { useCallback, useEffect, useRef, useState } from 'react';
10
10
  import { AgendaView } from './agenda-view';
11
11
  import { CalendarHeader } from './calendar-header';
12
+ import { CalendarLoadingSkeleton } from './calendar-loading-skeleton';
12
13
  import { CalendarViewWithTrail } from './calendar-view-with-trail';
13
14
  import { EventModal } from './event-modal';
14
15
  import { EventPreviewPopover } from './event-preview-popover';
@@ -102,6 +103,7 @@ export const CalendarContent = ({
102
103
  externalState,
103
104
  extras,
104
105
  overlay,
106
+ disableBuiltInEventUi,
105
107
  }: {
106
108
  t: any;
107
109
  locale: string;
@@ -118,10 +120,11 @@ export const CalendarContent = ({
118
120
  };
119
121
  extras?: React.ReactNode;
120
122
  overlay?: React.ReactNode;
123
+ disableBuiltInEventUi?: boolean;
121
124
  }) => {
122
125
  const { transition } = useViewTransition();
123
126
  const { settings } = useCalendarSettings();
124
- const { dates, setDates } = useCalendarSync();
127
+ const { dates, isLoading, setDates } = useCalendarSync();
125
128
 
126
129
  // Use ref to always have the latest settings without causing dependency cascades
127
130
  const settingsRef = useRef(settings);
@@ -191,7 +194,6 @@ export const CalendarContent = ({
191
194
  handleSetView('4-days');
192
195
  setDates(dates);
193
196
  });
194
- console.log('enable4DayView', dates);
195
197
  }, [date, transition, handleSetView, setDates]);
196
198
 
197
199
  const enableWeekView = useCallback(() => {
@@ -578,7 +580,7 @@ export const CalendarContent = ({
578
580
  return (
579
581
  <div
580
582
  className={cn(
581
- 'grid h-full w-full',
583
+ 'grid h-full min-h-0 w-full',
582
584
  view === 'month' || view === 'year'
583
585
  ? 'grid-rows-[auto_1fr]'
584
586
  : 'grid-rows-[auto_auto_1fr]'
@@ -621,7 +623,7 @@ export const CalendarContent = ({
621
623
 
622
624
  <div
623
625
  className={cn(
624
- 'scrollbar-none relative flex-1 overflow-auto rounded-lg focus:outline-none',
626
+ 'scrollbar-none relative min-h-0 flex-1 overflow-auto rounded-lg focus:outline-none',
625
627
  view === 'agenda' ||
626
628
  view === 'month' ||
627
629
  view === 'year' ||
@@ -632,7 +634,9 @@ export const CalendarContent = ({
632
634
  e.currentTarget.focus();
633
635
  }}
634
636
  >
635
- {view === 'month' && dates?.[0] ? (
637
+ {isLoading ? (
638
+ <CalendarLoadingSkeleton dates={dates} view={view} />
639
+ ) : view === 'month' && dates?.[0] ? (
636
640
  <MonthCalendar
637
641
  date={dates[0]}
638
642
  workspace={workspace}
@@ -665,7 +669,7 @@ export const CalendarContent = ({
665
669
  )}
666
670
  </div>
667
671
 
668
- {disabled
672
+ {disabled || disableBuiltInEventUi
669
673
  ? null
670
674
  : workspace && (
671
675
  <>
@@ -1,5 +1,7 @@
1
1
  import {
2
+ AlertTriangle,
2
3
  CalendarIcon,
4
+ Check,
3
5
  ChevronLeft,
4
6
  ChevronRight,
5
7
  Moon,
@@ -81,7 +83,7 @@ export function CalendarHeader({
81
83
  return newDate;
82
84
  });
83
85
 
84
- const { isLoading, isSyncing } = useCalendarSync();
86
+ const { syncStatus } = useCalendarSync();
85
87
  const selectToday = () => setDate(new Date());
86
88
  const isTodaySelected = () => dayjs(date).isSame(dayjs(), 'day');
87
89
  const isCurrentMonth = () =>
@@ -112,6 +114,14 @@ export function CalendarHeader({
112
114
  };
113
115
 
114
116
  const LunarIcon = showLunar ? MoonStar : Moon;
117
+ const statusLabel =
118
+ syncStatus.state === 'error'
119
+ ? t('failed_to_load_events')
120
+ : syncStatus.lastSyncTime
121
+ ? `${t('sync_completed')} ${dayjs(syncStatus.lastSyncTime)
122
+ .locale(locale)
123
+ .format('HH:mm')}`
124
+ : null;
115
125
 
116
126
  return (
117
127
  <div className="mb-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
@@ -121,8 +131,18 @@ export function CalendarHeader({
121
131
  </div>
122
132
  <div className="flex flex-col gap-2 md:flex-row md:items-center">
123
133
  <div className="flex items-center gap-2">
124
- {(isLoading || isSyncing) && (
125
- <div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
134
+ {statusLabel && (
135
+ <div
136
+ aria-live="polite"
137
+ className="hidden min-w-0 items-center gap-1.5 rounded-full border bg-background/80 px-2 py-1 text-muted-foreground text-xs shadow-xs sm:inline-flex"
138
+ >
139
+ {syncStatus.state === 'error' ? (
140
+ <AlertTriangle className="h-3.5 w-3.5 text-dynamic-red" />
141
+ ) : (
142
+ <Check className="h-3.5 w-3.5 text-dynamic-green" />
143
+ )}
144
+ <span className="truncate">{statusLabel}</span>
145
+ </div>
126
146
  )}
127
147
  <div className="flex flex-none items-center justify-center gap-2 md:justify-start">
128
148
  <Button