@tuturuuu/ui 0.6.1 → 0.7.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 (51) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +3 -3
  3. package/biome.json +1 -1
  4. package/package.json +8 -8
  5. package/src/components/ui/calendar-app/components/calendar-connections.tsx +17 -13
  6. package/src/components/ui/calendar-app/components/connected-accounts-dialog.tsx +2 -5
  7. package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +2 -5
  8. package/src/components/ui/calendar.test.tsx +24 -0
  9. package/src/components/ui/calendar.tsx +1 -0
  10. package/src/components/ui/date-time-picker.tsx +352 -234
  11. package/src/components/ui/finance/categories-tags-tabs.tsx +23 -1
  12. package/src/components/ui/finance/command/finance-command-actions.test.tsx +48 -0
  13. package/src/components/ui/finance/command/finance-command-actions.tsx +200 -0
  14. package/src/components/ui/finance/command/finance-command-provider.test.tsx +151 -0
  15. package/src/components/ui/finance/command/finance-command-provider.tsx +250 -0
  16. package/src/components/ui/finance/command/finance-command-results.tsx +262 -0
  17. package/src/components/ui/finance/invoices/pending-invoices-table.tsx +22 -9
  18. package/src/components/ui/finance/shared/quick-actions.tsx +39 -90
  19. package/src/components/ui/finance/tags/tag-manager.tsx +24 -5
  20. package/src/components/ui/finance/transactions/form-basic-tab.tsx +33 -49
  21. package/src/components/ui/finance/transactions/form-types.ts +3 -0
  22. package/src/components/ui/finance/transactions/form.test.tsx +105 -22
  23. package/src/components/ui/finance/transactions/form.tsx +116 -20
  24. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +13 -6
  25. package/src/components/ui/finance/transactions/transaction-edit-dialog.test.tsx +25 -1
  26. package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +16 -3
  27. package/src/components/ui/finance/transactions/transactionId/transaction-details-client-page.tsx +3 -0
  28. package/src/components/ui/finance/transactions/transactionId/transaction-details-page.tsx +3 -0
  29. package/src/components/ui/finance/transactions/transactions-create-summary.tsx +6 -0
  30. package/src/components/ui/finance/transactions/transactions-infinite-page.tsx +20 -2
  31. package/src/components/ui/finance/transactions/transactions-page.tsx +4 -0
  32. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +7 -2
  33. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +7 -2
  34. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +38 -1
  35. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +5 -0
  36. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +18 -2
  37. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +3 -0
  38. package/src/components/ui/finance/wallets/wallets-page.tsx +3 -0
  39. package/src/components/ui/legacy/calendar/settings/google-calendar-settings.tsx +2 -9
  40. package/src/components/ui/optional-time-picker.tsx +95 -0
  41. package/src/components/ui/quick-command-center.test.tsx +90 -0
  42. package/src/components/ui/quick-command-center.tsx +190 -0
  43. package/src/components/ui/storefront/cart-summary.tsx +18 -27
  44. package/src/components/ui/storefront/hero-panel.tsx +22 -13
  45. package/src/components/ui/storefront/storefront-surface.test.tsx +8 -4
  46. package/src/components/ui/storefront/storefront-surface.tsx +84 -41
  47. package/src/components/ui/storefront/types.ts +2 -0
  48. package/src/components/ui/storefront/utils.ts +21 -0
  49. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.test.ts +171 -0
  50. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/task-api.ts +200 -36
  51. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +21 -2
@@ -82,6 +82,14 @@ export function TransactionsInfinitePage({
82
82
  shallow: true,
83
83
  })
84
84
  );
85
+ const [tool, setTool] = useQueryState(
86
+ 'tool',
87
+ parseAsString.withDefault('').withOptions({
88
+ shallow: true,
89
+ })
90
+ );
91
+ const importOpen = tool === 'import';
92
+ const exportOpen = tool === 'export';
85
93
 
86
94
  const handleSearch = async (query: string) => {
87
95
  await setQ(query || '');
@@ -143,7 +151,12 @@ export function TransactionsInfinitePage({
143
151
 
144
152
  <div className="flex w-full flex-col gap-2 sm:flex-row md:w-auto">
145
153
  {/* Import button */}
146
- <Dialog>
154
+ <Dialog
155
+ open={importOpen}
156
+ onOpenChange={(nextOpen) =>
157
+ setTool(nextOpen ? 'import' : tool === 'import' ? null : tool)
158
+ }
159
+ >
147
160
  <DialogTrigger asChild>
148
161
  <Button
149
162
  variant="outline"
@@ -161,7 +174,12 @@ export function TransactionsInfinitePage({
161
174
 
162
175
  {/* Export button */}
163
176
  {canExport && exportContent && (
164
- <Dialog>
177
+ <Dialog
178
+ open={exportOpen}
179
+ onOpenChange={(nextOpen) =>
180
+ setTool(nextOpen ? 'export' : tool === 'export' ? null : tool)
181
+ }
182
+ >
165
183
  <DialogTrigger asChild>
166
184
  <Button
167
185
  variant="outline"
@@ -26,6 +26,7 @@ interface Props {
26
26
  };
27
27
  permissionRequestUser?: FinancePermissionRequestUser | null;
28
28
  openCreateDialog?: boolean;
29
+ initialCreateMode?: 'transaction' | 'transfer';
29
30
  showTransactionTypeFilter?: boolean;
30
31
  }
31
32
 
@@ -36,6 +37,7 @@ export default async function TransactionsPage({
36
37
  workspace,
37
38
  permissionRequestUser,
38
39
  openCreateDialog = false,
40
+ initialCreateMode = 'transaction',
39
41
  showTransactionTypeFilter = false,
40
42
  }: Props) {
41
43
  const [t, resolvedWorkspace, resolvedPermissions, resolvedCurrency] =
@@ -102,11 +104,13 @@ export default async function TransactionsPage({
102
104
  createTitle={t('ws-transactions.create')}
103
105
  createDescription={t('ws-transactions.create_description')}
104
106
  defaultOpen={openCreateDialog}
107
+ initialMode={initialCreateMode}
105
108
  wsId={wsId}
106
109
  canCreateTransactions={canCreateTransactions}
107
110
  canChangeFinanceWallets={canChangeFinanceWallets}
108
111
  canSetFinanceWalletsOnCreate={canSetFinanceWalletsOnCreate}
109
112
  canCreateConfidentialTransactions={canCreateConfidentialTransactions}
113
+ timezone={resolvedWorkspace.timezone}
110
114
  permissionRequestUser={permissionRequestUser}
111
115
  />
112
116
  <Separator className="my-4" />
@@ -30,7 +30,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@tuturuuu/ui/tabs';
30
30
  import { cn } from '@tuturuuu/utils/format';
31
31
  import Link from 'next/link';
32
32
  import { useLocale, useTranslations } from 'next-intl';
33
- import { useMemo, useState } from 'react';
33
+ import { useEffect, useMemo, useState } from 'react';
34
34
  import { invalidateWalletMutationQueries } from '../query-invalidation';
35
35
  import { WalletCheckpointAdjustmentDialog } from './wallet-checkpoint-adjustment-dialog';
36
36
  import { WalletCheckpointAmount } from './wallet-checkpoint-amount';
@@ -63,17 +63,19 @@ type WindowRow =
63
63
 
64
64
  export function WalletCheckpointHistoryDialog({
65
65
  canCreateTransactions,
66
+ defaultOpen = false,
66
67
  financePrefix = '/finance',
67
68
  wsId,
68
69
  }: {
69
70
  canCreateTransactions: boolean;
71
+ defaultOpen?: boolean;
70
72
  financePrefix?: string;
71
73
  wsId: string;
72
74
  }) {
73
75
  const t = useTranslations('wallet-checkpoints');
74
76
  const locale = useLocale();
75
77
  const queryClient = useQueryClient();
76
- const [open, setOpen] = useState(false);
78
+ const [open, setOpen] = useState(defaultOpen);
77
79
  const [search, setSearch] = useState('');
78
80
  const [currency, setCurrency] = useState(ALL);
79
81
  const [status, setStatus] = useState<typeof ALL | CheckpointStatus>(ALL);
@@ -84,6 +86,9 @@ export function WalletCheckpointHistoryDialog({
84
86
  queryFn: () => getWalletCheckpointHistory(wsId, { limit: 100 }),
85
87
  enabled: open,
86
88
  });
89
+ useEffect(() => {
90
+ if (defaultOpen) setOpen(true);
91
+ }, [defaultOpen]);
87
92
  const formatDate = (value: string) =>
88
93
  new Intl.DateTimeFormat(locale, {
89
94
  dateStyle: 'medium',
@@ -29,7 +29,7 @@ import { toast } from '@tuturuuu/ui/sonner';
29
29
  import { cn } from '@tuturuuu/utils/format';
30
30
  import { useTranslations } from 'next-intl';
31
31
  import type { ReactNode } from 'react';
32
- import { useMemo, useState } from 'react';
32
+ import { useEffect, useMemo, useState } from 'react';
33
33
  import { useFinanceBalanceMode } from '../../shared/use-finance-balance-mode';
34
34
  import {
35
35
  getWalletBalanceTone,
@@ -51,22 +51,27 @@ type WalletInput = {
51
51
  export function WalletTotalCheckDialog({
52
52
  canUpdateWallets,
53
53
  currency,
54
+ defaultOpen = false,
54
55
  wsId,
55
56
  }: {
56
57
  canUpdateWallets: boolean;
57
58
  currency: string;
59
+ defaultOpen?: boolean;
58
60
  wsId: string;
59
61
  }) {
60
62
  const t = useTranslations('wallet-checkpoints');
61
63
  const queryClient = useQueryClient();
62
64
  const { isAuditedMode } = useFinanceBalanceMode();
63
- const [open, setOpen] = useState(false);
65
+ const [open, setOpen] = useState(defaultOpen);
64
66
  const [values, setValues] = useState<Record<string, string>>({});
65
67
  const walletsQuery = useQuery({
66
68
  enabled: open && canUpdateWallets,
67
69
  queryFn: () => listWallets(wsId),
68
70
  queryKey: ['wallets', wsId, 'all-wallet-check'],
69
71
  });
72
+ useEffect(() => {
73
+ if (defaultOpen) setOpen(true);
74
+ }, [defaultOpen]);
70
75
  const wallets = useMemo<WalletInput[]>(
71
76
  () =>
72
77
  (walletsQuery.data ?? []).flatMap((wallet) => {
@@ -50,7 +50,7 @@ describe('WalletDetailsActions', () => {
50
50
  });
51
51
 
52
52
  it('prefills card payments as transfers into the credit wallet', () => {
53
- render(<WalletDetailsActions {...baseProps} />);
53
+ render(<WalletDetailsActions {...baseProps} timezone="Asia/Ho_Chi_Minh" />);
54
54
 
55
55
  fireEvent.click(screen.getByText('wallet-data-table.credit_payment'));
56
56
 
@@ -63,6 +63,9 @@ describe('WalletDetailsActions', () => {
63
63
  initialTransfer: expect.objectContaining({
64
64
  destination_wallet_id: 'wallet-1',
65
65
  }),
66
+ preferInitialWalletSelection: false,
67
+ refreshPageOnFinish: true,
68
+ timezone: 'Asia/Ho_Chi_Minh',
66
69
  })
67
70
  );
68
71
  });
@@ -81,6 +84,8 @@ describe('WalletDetailsActions', () => {
81
84
  categoryKind: 'expense',
82
85
  origin_wallet_id: 'wallet-1',
83
86
  }),
87
+ preferInitialWalletSelection: true,
88
+ refreshPageOnFinish: true,
84
89
  })
85
90
  );
86
91
  });
@@ -99,6 +104,38 @@ describe('WalletDetailsActions', () => {
99
104
  categoryKind: 'income',
100
105
  origin_wallet_id: 'wallet-1',
101
106
  }),
107
+ preferInitialWalletSelection: true,
108
+ refreshPageOnFinish: true,
109
+ })
110
+ );
111
+ });
112
+
113
+ it('prefills standard wallet transactions as source-wallet transactions', () => {
114
+ render(
115
+ <WalletDetailsActions
116
+ {...baseProps}
117
+ wallet={
118
+ {
119
+ id: 'wallet-1',
120
+ name: 'Cash',
121
+ type: 'STANDARD',
122
+ } as never
123
+ }
124
+ />
125
+ );
126
+
127
+ fireEvent.click(screen.getByText('ws-transactions.singular'));
128
+
129
+ const props = mocks.transactionForm.mock.calls.at(-1)?.[0];
130
+
131
+ expect(props).toEqual(
132
+ expect.objectContaining({
133
+ initialMode: 'transaction',
134
+ initialTransaction: expect.objectContaining({
135
+ origin_wallet_id: 'wallet-1',
136
+ }),
137
+ preferInitialWalletSelection: true,
138
+ refreshPageOnFinish: true,
102
139
  })
103
140
  );
104
141
  });
@@ -35,6 +35,7 @@ interface WalletDetailsActionsProps {
35
35
  canSetFinanceWalletsOnCreate?: boolean;
36
36
  canDeleteWallets: boolean;
37
37
  isPersonalWorkspace: boolean;
38
+ timezone?: string | null;
38
39
  permissionRequestUser?: FinancePermissionRequestUser | null;
39
40
  }
40
41
 
@@ -50,6 +51,7 @@ export function WalletDetailsActions({
50
51
  canSetFinanceWalletsOnCreate,
51
52
  canDeleteWallets,
52
53
  isPersonalWorkspace,
54
+ timezone,
53
55
  permissionRequestUser,
54
56
  }: WalletDetailsActionsProps) {
55
57
  const t = useTranslations();
@@ -208,6 +210,9 @@ export function WalletDetailsActions({
208
210
  canCreateConfidentialTransactions={
209
211
  canCreateConfidentialTransactions
210
212
  }
213
+ timezone={timezone}
214
+ preferInitialWalletSelection={transactionAction !== 'payment'}
215
+ refreshPageOnFinish
211
216
  permissionRequestUser={permissionRequestUser}
212
217
  />
213
218
  }
@@ -30,6 +30,7 @@ const mocks = vi.hoisted(() => {
30
30
  getWorkspace: vi.fn(),
31
31
  getWorkspaceConfig: vi.fn(),
32
32
  headers: vi.fn(),
33
+ infiniteTransactionsList: vi.fn((_props: unknown) => null),
33
34
  notFound: vi.fn(() => {
34
35
  throw new Error('notFound');
35
36
  }),
@@ -81,7 +82,9 @@ vi.mock('@tuturuuu/ui/custom/feature-summary', () => ({
81
82
  }));
82
83
 
83
84
  vi.mock('@tuturuuu/ui/finance/transactions/infinite-transactions-list', () => ({
84
- InfiniteTransactionsList: () => null,
85
+ InfiniteTransactionsList: (
86
+ ...args: Parameters<typeof mocks.infiniteTransactionsList>
87
+ ) => mocks.infiniteTransactionsList(...args),
85
88
  }));
86
89
 
87
90
  vi.mock('@tuturuuu/ui/separator', () => ({
@@ -129,7 +132,10 @@ describe('wallet details page', () => {
129
132
  vi.resetModules();
130
133
  vi.clearAllMocks();
131
134
  mocks.getTranslations.mockResolvedValue((key: string) => key);
132
- mocks.getWorkspace.mockResolvedValue({ personal: false });
135
+ mocks.getWorkspace.mockResolvedValue({
136
+ personal: false,
137
+ timezone: 'Asia/Ho_Chi_Minh',
138
+ });
133
139
  mocks.getWorkspaceConfig.mockResolvedValue('USD');
134
140
  mocks.getPermissions.mockResolvedValue({
135
141
  withoutPermission: vi.fn(() => false),
@@ -208,6 +214,16 @@ describe('wallet details page', () => {
208
214
  expect(actionProps).toEqual(
209
215
  expect.objectContaining({
210
216
  initialAction: 'payment',
217
+ timezone: 'Asia/Ho_Chi_Minh',
218
+ walletId: 'wallet-1',
219
+ })
220
+ );
221
+ expect(mocks.infiniteTransactionsList).toHaveBeenCalled();
222
+ const listProps = mocks.infiniteTransactionsList.mock.calls[0]?.[0];
223
+
224
+ expect(listProps).toEqual(
225
+ expect.objectContaining({
226
+ timezone: 'Asia/Ho_Chi_Minh',
211
227
  walletId: 'wallet-1',
212
228
  })
213
229
  );
@@ -51,6 +51,7 @@ interface Props {
51
51
  permissions?: PermissionsResult;
52
52
  workspace?: {
53
53
  personal?: boolean | null;
54
+ timezone?: string | null;
54
55
  };
55
56
  permissionRequestUser?: FinancePermissionRequestUser | null;
56
57
  }
@@ -152,6 +153,7 @@ export default async function WalletDetailsPage({
152
153
  canSetFinanceWalletsOnCreate={canSetFinanceWalletsOnCreate}
153
154
  canDeleteWallets={canDeleteWallets}
154
155
  isPersonalWorkspace={!!resolvedWorkspace.personal}
156
+ timezone={resolvedWorkspace.timezone}
155
157
  permissionRequestUser={permissionRequestUser}
156
158
  />
157
159
  </div>
@@ -314,6 +316,7 @@ export default async function WalletDetailsPage({
314
316
  wsId={wsId}
315
317
  walletId={walletId}
316
318
  currency={currency}
319
+ timezone={resolvedWorkspace.timezone}
317
320
  canCreateTransactions={canCreateTransactions}
318
321
  canCreateConfidentialTransactions={canCreateConfidentialTransactions}
319
322
  canUpdateTransactions={canUpdateTransactions}
@@ -20,6 +20,7 @@ interface Props {
20
20
  searchParams: {
21
21
  create?: string;
22
22
  q?: string;
23
+ tool?: string;
23
24
  };
24
25
  currency?: string;
25
26
  financePrefix?: string;
@@ -90,11 +91,13 @@ export default async function WalletsPage({
90
91
  wsId={wsId}
91
92
  financePrefix={financePrefix}
92
93
  canCreateTransactions={canCreateTransactions}
94
+ defaultOpen={searchParams.tool === 'checkpoint-history'}
93
95
  />
94
96
  <WalletTotalCheckDialog
95
97
  wsId={wsId}
96
98
  currency={resolvedCurrency ?? 'USD'}
97
99
  canUpdateWallets={canUpdateWallets}
100
+ defaultOpen={searchParams.tool === 'all-wallet-check'}
98
101
  />
99
102
  </div>
100
103
  </div>
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { Check, ExternalLink, Link, Loader2, RefreshCw } from '@tuturuuu/icons';
4
+ import { getGoogleCalendarAuthUrl } from '@tuturuuu/internal-api';
4
5
  import { createClient } from '@tuturuuu/supabase/next/client';
5
6
  import type { WorkspaceCalendarGoogleTokenClient } from '@tuturuuu/types';
6
7
  import { Alert, AlertDescription } from '@tuturuuu/ui/alert';
@@ -119,15 +120,7 @@ export function GoogleCalendarSettings({
119
120
 
120
121
  setIsGoogleAuthenticating(true);
121
122
  try {
122
- const response = await fetch(`/api/v1/calendar/auth?wsId=${wsId}`, {
123
- method: 'GET',
124
- });
125
-
126
- if (!response.ok) {
127
- throw new Error(`HTTP error! Status: ${response.status}`);
128
- }
129
-
130
- const { authUrl } = await response.json();
123
+ const { authUrl } = await getGoogleCalendarAuthUrl(wsId);
131
124
  window.location.href = authUrl;
132
125
  } catch (error) {
133
126
  console.error('Error initiating Google auth:', error);
@@ -0,0 +1,95 @@
1
+ 'use client';
2
+
3
+ import { DateTimePicker } from '@tuturuuu/ui/date-time-picker';
4
+ import { cn } from '@tuturuuu/utils/format';
5
+ import {
6
+ buildDateInTimezone,
7
+ getDatePartsInTimezone,
8
+ } from '@tuturuuu/utils/task-date-timezone';
9
+ import type { ReactNode } from 'react';
10
+
11
+ export interface OptionalTimePickerProps {
12
+ date?: Date;
13
+ setDate: (date: Date | undefined) => void;
14
+ includeTime: boolean;
15
+ setIncludeTime: (includeTime: boolean) => void;
16
+ includeTimeLabel: ReactNode;
17
+ disabled?: boolean;
18
+ allowClear?: boolean;
19
+ showFooterControls?: boolean;
20
+ side?: 'top' | 'right' | 'bottom' | 'left';
21
+ align?: 'start' | 'center' | 'end';
22
+ collisionPadding?: number;
23
+ className?: string;
24
+ preferences?: {
25
+ weekStartsOn?: 0 | 1 | 6;
26
+ timezone?: string;
27
+ timeFormat?: '12h' | '24h';
28
+ };
29
+ }
30
+
31
+ function startOfDay(date: Date, timezone?: string) {
32
+ if (!timezone) {
33
+ const next = new Date(date);
34
+ next.setHours(0, 0, 0, 0);
35
+ return next;
36
+ }
37
+
38
+ const parts = getDatePartsInTimezone(date, timezone);
39
+ return buildDateInTimezone(
40
+ parts.year,
41
+ parts.month,
42
+ parts.day,
43
+ 0,
44
+ 0,
45
+ timezone
46
+ );
47
+ }
48
+
49
+ export function OptionalTimePicker({
50
+ date,
51
+ setDate,
52
+ includeTime,
53
+ setIncludeTime,
54
+ includeTimeLabel,
55
+ disabled = false,
56
+ allowClear = true,
57
+ showFooterControls = true,
58
+ side = 'bottom',
59
+ align = 'start',
60
+ collisionPadding = 16,
61
+ className,
62
+ preferences,
63
+ }: OptionalTimePickerProps) {
64
+ const handleDateChange = (nextDate: Date | undefined) => {
65
+ if (!nextDate || includeTime) {
66
+ setDate(nextDate);
67
+ return;
68
+ }
69
+
70
+ setDate(startOfDay(nextDate, preferences?.timezone));
71
+ };
72
+
73
+ return (
74
+ <div className={cn('w-full', className)}>
75
+ <DateTimePicker
76
+ date={date}
77
+ setDate={handleDateChange}
78
+ showTimeSelect={includeTime}
79
+ allowClear={allowClear}
80
+ showFooterControls={showFooterControls}
81
+ disabled={disabled}
82
+ side={side}
83
+ align={align}
84
+ collisionPadding={collisionPadding}
85
+ preferences={preferences}
86
+ timeToggle={{
87
+ checked: includeTime,
88
+ disabled,
89
+ label: includeTimeLabel,
90
+ onCheckedChange: setIncludeTime,
91
+ }}
92
+ />
93
+ </div>
94
+ );
95
+ }
@@ -0,0 +1,90 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { QuickCommandCenter } from './quick-command-center';
4
+
5
+ describe('QuickCommandCenter', () => {
6
+ beforeEach(() => {
7
+ globalThis.ResizeObserver = class ResizeObserver {
8
+ disconnect() {}
9
+ observe() {}
10
+ unobserve() {}
11
+ };
12
+ Element.prototype.scrollIntoView = vi.fn();
13
+ });
14
+
15
+ it('renders grouped commands and activates digit shortcuts', () => {
16
+ const first = vi.fn();
17
+ const second = vi.fn();
18
+
19
+ render(
20
+ <QuickCommandCenter
21
+ digitShortcuts
22
+ emptyLabel="No commands"
23
+ groups={[
24
+ {
25
+ heading: 'Create',
26
+ id: 'create',
27
+ items: [
28
+ {
29
+ id: 'transaction',
30
+ onSelect: first,
31
+ title: 'New transaction',
32
+ },
33
+ {
34
+ id: 'wallet',
35
+ onSelect: second,
36
+ title: 'New wallet',
37
+ },
38
+ ],
39
+ },
40
+ ]}
41
+ onOpenChange={() => undefined}
42
+ open
43
+ placeholder="Search commands"
44
+ title="Quick command center"
45
+ />
46
+ );
47
+
48
+ expect(screen.getByText('New transaction')).toBeVisible();
49
+ expect(screen.getByText('New wallet')).toBeVisible();
50
+
51
+ fireEvent.keyDown(window, { key: '2' });
52
+
53
+ expect(first).not.toHaveBeenCalled();
54
+ expect(second).toHaveBeenCalledTimes(1);
55
+ });
56
+
57
+ it('filters commands by search text', () => {
58
+ render(
59
+ <QuickCommandCenter
60
+ emptyLabel="No commands"
61
+ groups={[
62
+ {
63
+ heading: 'Create',
64
+ id: 'create',
65
+ items: [
66
+ {
67
+ id: 'transaction',
68
+ onSelect: vi.fn(),
69
+ title: 'New transaction',
70
+ },
71
+ {
72
+ id: 'wallet',
73
+ onSelect: vi.fn(),
74
+ title: 'New wallet',
75
+ },
76
+ ],
77
+ },
78
+ ]}
79
+ onOpenChange={() => undefined}
80
+ open
81
+ placeholder="Search commands"
82
+ searchValue="wallet"
83
+ title="Quick command center"
84
+ />
85
+ );
86
+
87
+ expect(screen.getByText('New wallet')).toBeVisible();
88
+ expect(screen.queryByText('New transaction')).not.toBeInTheDocument();
89
+ });
90
+ });