@tuturuuu/ui 0.4.1 → 0.5.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 (39) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +5 -5
  3. package/src/components/ui/custom/settings/task-settings.tsx +76 -0
  4. package/src/components/ui/custom/settings/task-sound-settings.test.tsx +126 -0
  5. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +29 -18
  6. package/src/components/ui/finance/transactions/transaction-card.tsx +75 -43
  7. package/src/components/ui/finance/transactions/transfer-merge.test.ts +90 -0
  8. package/src/components/ui/finance/transactions/transfer-merge.ts +52 -0
  9. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +172 -0
  10. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-amount.tsx +32 -0
  11. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-delete-dialog.tsx +50 -0
  12. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-dialog.tsx +138 -0
  13. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +196 -0
  14. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +201 -0
  15. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +277 -0
  16. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +189 -0
  17. package/src/components/ui/finance/wallets/query-invalidation.ts +2 -0
  18. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +7 -3
  19. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +10 -0
  20. package/src/components/ui/finance/wallets/wallets-page.test.tsx +7 -0
  21. package/src/components/ui/finance/wallets/wallets-page.tsx +21 -5
  22. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +14 -0
  23. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +29 -0
  24. package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +11 -0
  25. package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +10 -0
  26. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +141 -0
  27. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +208 -0
  28. package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +26 -0
  29. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +36 -20
  30. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +24 -0
  31. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +18 -3
  32. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.test.ts +84 -1
  33. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +5 -1
  34. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +300 -172
  35. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +411 -323
  36. package/src/components/ui/tu-do/shared/task-sound-effects.test.ts +189 -0
  37. package/src/components/ui/tu-do/shared/task-sound-effects.tsx +468 -0
  38. package/src/hooks/__tests__/use-task-actions.test.tsx +61 -0
  39. package/src/hooks/use-task-actions.ts +45 -0
@@ -4,7 +4,7 @@ const mocks = vi.hoisted(() => {
4
4
  const currencyRateLimit = vi.fn();
5
5
 
6
6
  return {
7
- createClient: vi.fn(() => ({
7
+ createAdminClient: vi.fn(() => ({
8
8
  from: vi.fn((table: string) => {
9
9
  if (table === 'currency_exchange_rates') {
10
10
  return {
@@ -44,8 +44,8 @@ vi.mock('@tuturuuu/internal-api', () => ({
44
44
  }));
45
45
 
46
46
  vi.mock('@tuturuuu/supabase/next/server', () => ({
47
- createClient: (...args: Parameters<typeof mocks.createClient>) =>
48
- mocks.createClient(...args),
47
+ createAdminClient: (...args: Parameters<typeof mocks.createAdminClient>) =>
48
+ mocks.createAdminClient(...args),
49
49
  }));
50
50
 
51
51
  vi.mock('@tuturuuu/utils/workspace-helper', () => ({
@@ -96,6 +96,10 @@ vi.mock('../wallet-icon-display', () => ({
96
96
  WalletIconDisplay: () => null,
97
97
  }));
98
98
 
99
+ vi.mock('../checkpoints/wallet-checkpoint-panel', () => ({
100
+ WalletCheckpointPanel: () => null,
101
+ }));
102
+
99
103
  vi.mock('./credit-wallet-summary', () => ({
100
104
  CreditWalletSummary: () => null,
101
105
  }));
@@ -27,6 +27,7 @@ import { notFound } from 'next/navigation';
27
27
  import { getTranslations } from 'next-intl/server';
28
28
  import { Suspense } from 'react';
29
29
  import { Card } from '../../../card';
30
+ import { WalletCheckpointPanel } from '../checkpoints/wallet-checkpoint-panel';
30
31
  import { WalletIconDisplay } from '../wallet-icon-display';
31
32
  import { CreditWalletSummary } from './credit-wallet-summary';
32
33
  import { WalletInterestSection } from './interest';
@@ -299,6 +300,15 @@ export default async function WalletDetailsPage({
299
300
  <Separator className="my-4" />
300
301
  </>
301
302
  )}
303
+ <WalletCheckpointPanel
304
+ wsId={wsId}
305
+ walletId={walletId}
306
+ walletName={wallet.name ?? t('ws-wallets.singular')}
307
+ currency={currency}
308
+ canUpdateWallets={canUpdateWallets}
309
+ canCreateTransactions={canCreateTransactions}
310
+ />
311
+ <Separator className="my-4" />
302
312
  {/* Interest Tracking Section - for Momo/ZaloPay wallets */}
303
313
  <WalletInterestSection wsId={wsId} wallet={wallet as Wallet} />
304
314
  <Suspense
@@ -53,6 +53,13 @@ vi.mock('@tuturuuu/ui/finance/shared/create-dialog-feature-summary', () => ({
53
53
  CreateDialogFeatureSummary: () => null,
54
54
  }));
55
55
 
56
+ vi.mock(
57
+ '@tuturuuu/ui/finance/wallets/checkpoints/wallet-total-check-dialog',
58
+ () => ({
59
+ WalletTotalCheckDialog: () => null,
60
+ })
61
+ );
62
+
56
63
  vi.mock('@tuturuuu/ui/finance/wallets/form', () => ({
57
64
  WalletForm: () => null,
58
65
  }));
@@ -5,6 +5,7 @@ import {
5
5
  } from '@tuturuuu/internal-api';
6
6
  import type { Wallet } from '@tuturuuu/types/primitives/Wallet';
7
7
  import { CreateDialogFeatureSummary } from '@tuturuuu/ui/finance/shared/create-dialog-feature-summary';
8
+ import { WalletTotalCheckDialog } from '@tuturuuu/ui/finance/wallets/checkpoints/wallet-total-check-dialog';
8
9
  import { WalletForm } from '@tuturuuu/ui/finance/wallets/form';
9
10
  import { WalletsDataTable } from '@tuturuuu/ui/finance/wallets/wallets-data-table';
10
11
  import { Separator } from '@tuturuuu/ui/separator';
@@ -67,11 +68,11 @@ export default async function WalletsPage({
67
68
  const canDeleteWallets = containsPermission('delete_wallets');
68
69
  const resolvedInternalApiOptions =
69
70
  internalApiOptions ?? withForwardedInternalApiAuth(await headers());
70
- const { data: rawData, count } = await getData(
71
- wsId,
72
- searchParams,
73
- resolvedInternalApiOptions
74
- );
71
+ const {
72
+ data: rawData,
73
+ count,
74
+ allWallets,
75
+ } = await getData(wsId, searchParams, resolvedInternalApiOptions);
75
76
 
76
77
  const data = rawData.map((d) => ({
77
78
  ...d,
@@ -91,6 +92,20 @@ export default async function WalletsPage({
91
92
  form={canCreateWallets ? <WalletForm wsId={wsId} /> : undefined}
92
93
  />
93
94
  <Separator className="my-4" />
95
+ <div className="mb-4 flex justify-end">
96
+ <WalletTotalCheckDialog
97
+ wsId={wsId}
98
+ wallets={allWallets
99
+ .filter((wallet) => !!wallet.id)
100
+ .map((wallet) => ({
101
+ balance: wallet.balance,
102
+ currency: wallet.currency || resolvedCurrency || 'USD',
103
+ id: wallet.id as string,
104
+ name: wallet.name,
105
+ }))}
106
+ canUpdateWallets={canUpdateWallets}
107
+ />
108
+ </div>
94
109
  <WalletsDataTable
95
110
  wsId={wsId}
96
111
  data={data}
@@ -131,6 +146,7 @@ async function getData(
131
146
 
132
147
  return {
133
148
  data: filteredWallets.slice(start, start + parsedPageSize) as Wallet[],
149
+ allWallets: wallets as Wallet[],
134
150
  count: filteredWallets.length,
135
151
  };
136
152
  }
@@ -10,6 +10,10 @@ import type { ReactElement, ReactNode } from 'react';
10
10
  import { beforeEach, describe, expect, it, vi } from 'vitest';
11
11
  import { useBulkOperations } from '../bulk-operations';
12
12
 
13
+ const { mockDispatchTaskSoundCue } = vi.hoisted(() => ({
14
+ mockDispatchTaskSoundCue: vi.fn(),
15
+ }));
16
+
13
17
  vi.mock('next-intl', () => ({
14
18
  useTranslations: () => {
15
19
  const translate = (key: string, values?: Record<string, unknown>) =>
@@ -49,6 +53,10 @@ vi.mock('@tuturuuu/ui/sonner', () => ({
49
53
  },
50
54
  }));
51
55
 
56
+ vi.mock('../../../../../shared/task-sound-effects', () => ({
57
+ dispatchTaskSoundCue: mockDispatchTaskSoundCue,
58
+ }));
59
+
52
60
  describe('bulk move mutations', () => {
53
61
  let queryClient: QueryClient;
54
62
  let wrapper: ({ children }: { children: ReactNode }) => ReactElement;
@@ -172,5 +180,11 @@ describe('bulk move mutations', () => {
172
180
  personal_list_id: 'list-2',
173
181
  })
174
182
  );
183
+ expect(mockDispatchTaskSoundCue).toHaveBeenCalledTimes(1);
184
+ expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith({
185
+ count: 2,
186
+ cue: 'move',
187
+ intensity: 1.2,
188
+ });
175
189
  });
176
190
  });
@@ -2,6 +2,10 @@
2
2
 
3
3
  import type { Task } from '@tuturuuu/types/primitives/Task';
4
4
  import { useEffect } from 'react';
5
+ import {
6
+ dispatchTaskSoundCue,
7
+ type TaskSoundCue,
8
+ } from '../../../../shared/task-sound-effects';
5
9
  import {
6
10
  useBulkClearAssignees,
7
11
  useBulkClearLabels,
@@ -31,6 +35,14 @@ import { useBulkOperationI18n } from './bulk-operation-i18n';
31
35
  import type { BulkOperationsConfig } from './bulk-operation-types';
32
36
  import { getBulkTaskWorkspaceGroups } from './bulk-operation-utils';
33
37
 
38
+ function dispatchBulkTaskSoundCue(cue: TaskSoundCue, count: number) {
39
+ dispatchTaskSoundCue({
40
+ cue,
41
+ count,
42
+ intensity: count > 1 ? 1.2 : 1,
43
+ });
44
+ }
45
+
34
46
  export function useBulkOperations(config: BulkOperationsConfig) {
35
47
  const i18n = useBulkOperationI18n();
36
48
 
@@ -213,11 +225,13 @@ export function useBulkOperations(config: BulkOperationsConfig) {
213
225
  const taskIds = Array.from(selectedTasks);
214
226
  if (!taskIds.length) return;
215
227
  await priorityMutation.mutateAsync({ priority, taskIds });
228
+ dispatchBulkTaskSoundCue('update', taskIds.length);
216
229
  },
217
230
  bulkUpdateEstimation: async (points: number | null) => {
218
231
  const taskIds = Array.from(selectedTasks);
219
232
  if (!taskIds.length) return;
220
233
  await estimationMutation.mutateAsync({ points, taskIds });
234
+ dispatchBulkTaskSoundCue('update', taskIds.length);
221
235
  },
222
236
  bulkUpdateDueDate: async (
223
237
  preset: 'today' | 'tomorrow' | 'this_week' | 'next_week' | 'clear'
@@ -225,16 +239,19 @@ export function useBulkOperations(config: BulkOperationsConfig) {
225
239
  const taskIds = Array.from(selectedTasks);
226
240
  if (!taskIds.length) return;
227
241
  await dueDateMutation.mutateAsync({ preset, taskIds });
242
+ dispatchBulkTaskSoundCue('update', taskIds.length);
228
243
  },
229
244
  bulkUpdateCustomDueDate: async (date: Date | null) => {
230
245
  const taskIds = Array.from(selectedTasks);
231
246
  if (!taskIds.length) return;
232
247
  await customDueDateMutation.mutateAsync({ date, taskIds });
248
+ dispatchBulkTaskSoundCue('update', taskIds.length);
233
249
  },
234
250
  bulkMoveToList: async (listId: string, listName: string) => {
235
251
  const taskIds = Array.from(selectedTasks);
236
252
  if (!taskIds.length) return;
237
253
  await moveToListMutation.mutateAsync({ listId, listName, taskIds });
254
+ dispatchBulkTaskSoundCue('move', taskIds.length);
238
255
  },
239
256
  bulkMoveToStatus: async (status: 'done' | 'closed') => {
240
257
  const taskIds = Array.from(selectedTasks);
@@ -242,51 +259,61 @@ export function useBulkOperations(config: BulkOperationsConfig) {
242
259
  const listId = getListIdByStatus(status);
243
260
  if (!listId) return;
244
261
  await statusMutation.mutateAsync({ status, taskIds });
262
+ dispatchBulkTaskSoundCue('complete', taskIds.length);
245
263
  },
246
264
  bulkAddLabel: async (labelId: string) => {
247
265
  const taskIds = Array.from(selectedTasks);
248
266
  if (!taskIds.length) return;
249
267
  await addLabelMutation.mutateAsync({ labelId, taskIds });
268
+ dispatchBulkTaskSoundCue('update', taskIds.length);
250
269
  },
251
270
  bulkRemoveLabel: async (labelId: string) => {
252
271
  const taskIds = Array.from(selectedTasks);
253
272
  if (!taskIds.length) return;
254
273
  await removeLabelMutation.mutateAsync({ labelId, taskIds });
274
+ dispatchBulkTaskSoundCue('update', taskIds.length);
255
275
  },
256
276
  bulkAddProject: async (projectId: string) => {
257
277
  const taskIds = Array.from(selectedTasks);
258
278
  if (!taskIds.length) return;
259
279
  await addProjectMutation.mutateAsync({ projectId, taskIds });
280
+ dispatchBulkTaskSoundCue('update', taskIds.length);
260
281
  },
261
282
  bulkRemoveProject: async (projectId: string) => {
262
283
  const taskIds = Array.from(selectedTasks);
263
284
  if (!taskIds.length) return;
264
285
  await removeProjectMutation.mutateAsync({ projectId, taskIds });
286
+ dispatchBulkTaskSoundCue('update', taskIds.length);
265
287
  },
266
288
  bulkAddAssignee: async (assigneeId: string) => {
267
289
  const taskIds = Array.from(selectedTasks);
268
290
  if (!taskIds.length) return;
269
291
  await addAssigneeMutation.mutateAsync({ assigneeId, taskIds });
292
+ dispatchBulkTaskSoundCue('update', taskIds.length);
270
293
  },
271
294
  bulkRemoveAssignee: async (assigneeId: string) => {
272
295
  const taskIds = Array.from(selectedTasks);
273
296
  if (!taskIds.length) return;
274
297
  await removeAssigneeMutation.mutateAsync({ assigneeId, taskIds });
298
+ dispatchBulkTaskSoundCue('update', taskIds.length);
275
299
  },
276
300
  bulkClearLabels: async () => {
277
301
  const taskIds = Array.from(selectedTasks);
278
302
  if (!taskIds.length) return;
279
303
  await clearLabelsMutation.mutateAsync({ taskIds });
304
+ dispatchBulkTaskSoundCue('update', taskIds.length);
280
305
  },
281
306
  bulkClearProjects: async () => {
282
307
  const taskIds = Array.from(selectedTasks);
283
308
  if (!taskIds.length) return;
284
309
  await clearProjectsMutation.mutateAsync({ taskIds });
310
+ dispatchBulkTaskSoundCue('update', taskIds.length);
285
311
  },
286
312
  bulkClearAssignees: async () => {
287
313
  const taskIds = Array.from(selectedTasks);
288
314
  if (!taskIds.length) return;
289
315
  await clearAssigneesMutation.mutateAsync({ taskIds });
316
+ dispatchBulkTaskSoundCue('update', taskIds.length);
290
317
  },
291
318
  bulkDeleteTasks: async () => {
292
319
  const taskIds = Array.from(selectedTasks);
@@ -298,6 +325,7 @@ export function useBulkOperations(config: BulkOperationsConfig) {
298
325
  taskIds,
299
326
  });
300
327
  await deleteMutation.mutateAsync({ taskIds, workspaceGroups });
328
+ dispatchBulkTaskSoundCue('delete', taskIds.length);
301
329
  },
302
330
  bulkMoveToBoard: async (targetBoardId: string, targetListId: string) => {
303
331
  const taskIds = Array.from(selectedTasks);
@@ -307,6 +335,7 @@ export function useBulkOperations(config: BulkOperationsConfig) {
307
335
  targetListId,
308
336
  taskIds,
309
337
  });
338
+ dispatchBulkTaskSoundCue('move', taskIds.length);
310
339
  },
311
340
  getListIdByStatus,
312
341
  };
@@ -7,6 +7,7 @@ import { useTaskContextActions } from '../use-task-context-actions';
7
7
  const {
8
8
  mockAddWorkspaceTaskLabel,
9
9
  mockDeleteWorkspaceTask,
10
+ mockDispatchTaskSoundCue,
10
11
  mockInvalidateQueries,
11
12
  mockListWorkspaceTaskLists,
12
13
  mockRemoveWorkspaceTaskLabel,
@@ -17,6 +18,7 @@ const {
17
18
  } = vi.hoisted(() => ({
18
19
  mockAddWorkspaceTaskLabel: vi.fn(),
19
20
  mockDeleteWorkspaceTask: vi.fn(),
21
+ mockDispatchTaskSoundCue: vi.fn(),
20
22
  mockInvalidateQueries: vi.fn(),
21
23
  mockListWorkspaceTaskLists: vi.fn(),
22
24
  mockRemoveWorkspaceTaskLabel: vi.fn(),
@@ -55,6 +57,10 @@ vi.mock('@tuturuuu/ui/sonner', () => ({
55
57
  },
56
58
  }));
57
59
 
60
+ vi.mock('../../shared/task-sound-effects', () => ({
61
+ dispatchTaskSoundCue: mockDispatchTaskSoundCue,
62
+ }));
63
+
58
64
  // Mock fetch
59
65
  global.fetch = vi.fn();
60
66
 
@@ -131,6 +137,7 @@ describe('useTaskContextActions', () => {
131
137
  );
132
138
  expect(onTaskUpdate).toHaveBeenCalled();
133
139
  expect(onClose).toHaveBeenCalled();
140
+ expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith('complete');
134
141
  });
135
142
 
136
143
  it('handleUndoDoneWithMyPart sends PUT with both flags cleared', async () => {
@@ -165,6 +172,7 @@ describe('useTaskContextActions', () => {
165
172
  );
166
173
  expect(onTaskUpdate).toHaveBeenCalled();
167
174
  expect(onClose).toHaveBeenCalled();
175
+ expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith('update');
168
176
  });
169
177
 
170
178
  it('handleComplete clears overrides when task has completed_at override', async () => {
@@ -279,6 +287,7 @@ describe('useTaskContextActions', () => {
279
287
  expect(mockDeleteWorkspaceTask).toHaveBeenCalledWith('ws-1', mockTask.id);
280
288
  expect(onTaskUpdate).toHaveBeenCalled();
281
289
  expect(onClose).toHaveBeenCalled();
290
+ expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith('delete');
282
291
  });
283
292
 
284
293
  it('handlePriorityChange updates task priority via internal API', async () => {
@@ -299,6 +308,7 @@ describe('useTaskContextActions', () => {
299
308
  priority: 'high',
300
309
  });
301
310
  expect(mockInvalidateQueries).toHaveBeenCalled();
311
+ expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith('update');
302
312
  });
303
313
 
304
314
  it('handleUnassignMe updates assignee_ids via internal API', async () => {
@@ -512,5 +522,6 @@ describe('useTaskContextActions', () => {
512
522
  });
513
523
  expect(onTaskUpdate).toHaveBeenCalled();
514
524
  expect(onClose).toHaveBeenCalled();
525
+ expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith('move');
515
526
  });
516
527
  });
@@ -13,6 +13,7 @@ import type { TaskPriority } from '@tuturuuu/types/primitives/Priority';
13
13
  import { toast } from '@tuturuuu/ui/sonner';
14
14
  import { useTranslations } from 'next-intl';
15
15
  import { useCallback, useState } from 'react';
16
+ import { dispatchTaskSoundCue } from '../shared/task-sound-effects';
16
17
  import {
17
18
  MY_COMPLETED_TASKS_QUERY_KEY,
18
19
  MY_TASKS_QUERY_KEY,
@@ -73,6 +74,7 @@ export function useTaskContextActions({
73
74
  if (!taskWorkspaceId) throw new Error('Task workspace not found');
74
75
  await updateWorkspaceTask(taskWorkspaceId, task.id, { priority });
75
76
  invalidateQueries();
77
+ dispatchTaskSoundCue('update');
76
78
  } catch {
77
79
  toast.error(t('failed_to_update'));
78
80
  invalidateQueries();
@@ -97,6 +99,7 @@ export function useTaskContextActions({
97
99
  end_date: newDate,
98
100
  });
99
101
  invalidateQueries();
102
+ dispatchTaskSoundCue('update');
100
103
  } catch {
101
104
  toast.error(t('failed_to_update'));
102
105
  invalidateQueries();
@@ -119,6 +122,7 @@ export function useTaskContextActions({
119
122
  await addWorkspaceTaskLabel(taskWorkspaceId, task.id, labelId);
120
123
  }
121
124
  invalidateQueries();
125
+ dispatchTaskSoundCue('update');
122
126
  } catch {
123
127
  toast.error(t('failed_to_update'));
124
128
  invalidateQueries();
@@ -167,6 +171,7 @@ export function useTaskContextActions({
167
171
 
168
172
  onTaskUpdate();
169
173
  onClose();
174
+ dispatchTaskSoundCue('complete');
170
175
  } catch {
171
176
  toast.error(t('failed_to_update'));
172
177
  } finally {
@@ -196,6 +201,7 @@ export function useTaskContextActions({
196
201
  if (!response.ok) throw new Error('Failed');
197
202
  onTaskUpdate();
198
203
  onClose();
204
+ dispatchTaskSoundCue('complete');
199
205
  } catch {
200
206
  toast.error(t('failed_to_update'));
201
207
  } finally {
@@ -220,6 +226,7 @@ export function useTaskContextActions({
220
226
  if (!response.ok) throw new Error('Failed');
221
227
  onTaskUpdate();
222
228
  onClose();
229
+ dispatchTaskSoundCue('update');
223
230
  } catch {
224
231
  toast.error(t('failed_to_update'));
225
232
  } finally {
@@ -250,6 +257,7 @@ export function useTaskContextActions({
250
257
 
251
258
  onTaskUpdate();
252
259
  onClose();
260
+ dispatchTaskSoundCue('move');
253
261
  } catch {
254
262
  toast.error(t('failed_to_update'));
255
263
  } finally {
@@ -285,6 +293,7 @@ export function useTaskContextActions({
285
293
  });
286
294
  onTaskUpdate();
287
295
  onClose();
296
+ dispatchTaskSoundCue('update');
288
297
  } catch {
289
298
  toast.error(t('failed_to_update'));
290
299
  invalidateQueries();
@@ -310,6 +319,7 @@ export function useTaskContextActions({
310
319
  await deleteWorkspaceTask(taskWorkspaceId, task.id);
311
320
  onTaskUpdate();
312
321
  onClose();
322
+ dispatchTaskSoundCue('delete');
313
323
  } catch {
314
324
  toast.error(t('failed_to_update'));
315
325
  } finally {
@@ -0,0 +1,141 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import { fireEvent, render, screen } from '@testing-library/react';
6
+ import { Dialog } from '@tuturuuu/ui/dialog';
7
+ import { describe, expect, it, vi } from 'vitest';
8
+ import { CompactTaskCreatePopover } from './compact-task-create-popover';
9
+
10
+ vi.mock('next-intl', () => ({
11
+ useTranslations: () => (key: string) => key,
12
+ }));
13
+
14
+ vi.mock('./quick-settings-popover', () => ({
15
+ QuickSettingsPopover: () => (
16
+ <button type="button" aria-label="Quick Settings">
17
+ Quick Settings
18
+ </button>
19
+ ),
20
+ }));
21
+
22
+ function renderCompactTaskCreatePopover({
23
+ canSave = true,
24
+ createMultiple = false,
25
+ saveAsDraft = false,
26
+ }: {
27
+ canSave?: boolean;
28
+ createMultiple?: boolean;
29
+ saveAsDraft?: boolean;
30
+ } = {}) {
31
+ const props = {
32
+ title: 'Create task',
33
+ description: 'New task',
34
+ titleInput: (
35
+ <input aria-label="Task title" defaultValue="Draft compact title" />
36
+ ),
37
+ propertyControls: (
38
+ <button type="button" aria-label="Priority: High">
39
+ Priority
40
+ </button>
41
+ ),
42
+ saveAsDraft,
43
+ createMultiple,
44
+ canSave,
45
+ isLoading: false,
46
+ isPersonalWorkspace: false,
47
+ onSaveAsDraftChange: vi.fn(),
48
+ onCreateMultipleChange: vi.fn(),
49
+ onClose: vi.fn(),
50
+ onFullscreen: vi.fn(),
51
+ onSave: vi.fn(),
52
+ };
53
+
54
+ render(
55
+ <Dialog open={true}>
56
+ <CompactTaskCreatePopover {...props} />
57
+ </Dialog>
58
+ );
59
+
60
+ return props;
61
+ }
62
+
63
+ describe('CompactTaskCreatePopover', () => {
64
+ it('renders compact create content with accessible icon actions', () => {
65
+ renderCompactTaskCreatePopover();
66
+
67
+ expect(screen.getByTestId('compact-task-create-popover')).toBeTruthy();
68
+ expect(screen.getByText('Create task')).toBeTruthy();
69
+ expect(screen.getByLabelText('Task title')).toHaveProperty(
70
+ 'value',
71
+ 'Draft compact title'
72
+ );
73
+ expect(screen.getByLabelText('Priority: High')).toBeTruthy();
74
+ expect(
75
+ screen.getByLabelText('ws-task-boards.dialog.open_fullscreen')
76
+ ).toBeTruthy();
77
+ expect(screen.getByLabelText('common.close')).toBeTruthy();
78
+ expect(screen.getByLabelText('task-drafts.save_as_draft')).toBeTruthy();
79
+ expect(
80
+ screen.getByLabelText('ws-task-boards.dialog.create_multiple')
81
+ ).toBeTruthy();
82
+ expect(screen.getByLabelText('Quick Settings')).toBeTruthy();
83
+ });
84
+
85
+ it('routes compact actions to the caller while keeping current form nodes mounted', () => {
86
+ const props = renderCompactTaskCreatePopover();
87
+
88
+ fireEvent.click(
89
+ screen.getByLabelText('ws-task-boards.dialog.open_fullscreen')
90
+ );
91
+ fireEvent.click(screen.getByLabelText('task-drafts.save_as_draft'));
92
+ fireEvent.click(
93
+ screen.getByLabelText('ws-task-boards.dialog.create_multiple')
94
+ );
95
+ fireEvent.click(
96
+ screen.getByRole('button', {
97
+ name: 'ws-task-boards.dialog.create_task',
98
+ })
99
+ );
100
+
101
+ expect(props.onFullscreen).toHaveBeenCalledTimes(1);
102
+ expect(props.onSaveAsDraftChange).toHaveBeenCalledWith(true);
103
+ expect(props.onCreateMultipleChange).toHaveBeenCalledWith(true);
104
+ expect(props.onSave).toHaveBeenCalledTimes(1);
105
+ expect(screen.getByLabelText('Task title')).toHaveProperty(
106
+ 'value',
107
+ 'Draft compact title'
108
+ );
109
+ expect(screen.getByLabelText('Priority: High')).toBeTruthy();
110
+ });
111
+
112
+ it('disables create when the dialog cannot save', () => {
113
+ const props = renderCompactTaskCreatePopover({ canSave: false });
114
+ const saveButton = screen.getByRole('button', {
115
+ name: 'ws-task-boards.dialog.create_task',
116
+ });
117
+
118
+ expect(saveButton).toHaveProperty('disabled', true);
119
+ fireEvent.click(saveButton);
120
+
121
+ expect(props.onSave).not.toHaveBeenCalled();
122
+ });
123
+
124
+ it('reflects draft and create-multiple toggled states', () => {
125
+ renderCompactTaskCreatePopover({
126
+ createMultiple: true,
127
+ saveAsDraft: true,
128
+ });
129
+
130
+ expect(screen.getByLabelText('task-drafts.save_as_draft')).toHaveAttribute(
131
+ 'aria-pressed',
132
+ 'true'
133
+ );
134
+ expect(
135
+ screen.getByLabelText('ws-task-boards.dialog.create_multiple')
136
+ ).toHaveAttribute('aria-pressed', 'true');
137
+ expect(
138
+ screen.getAllByRole('button', { name: 'task-drafts.save_as_draft' })
139
+ ).toHaveLength(2);
140
+ });
141
+ });