@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.
- package/CHANGELOG.md +14 -0
- package/package.json +5 -5
- package/src/components/ui/custom/settings/task-settings.tsx +76 -0
- package/src/components/ui/custom/settings/task-sound-settings.test.tsx +126 -0
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +29 -18
- package/src/components/ui/finance/transactions/transaction-card.tsx +75 -43
- package/src/components/ui/finance/transactions/transfer-merge.test.ts +90 -0
- package/src/components/ui/finance/transactions/transfer-merge.ts +52 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +172 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-amount.tsx +32 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-delete-dialog.tsx +50 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-dialog.tsx +138 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +196 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +201 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +277 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +189 -0
- package/src/components/ui/finance/wallets/query-invalidation.ts +2 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +7 -3
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +10 -0
- package/src/components/ui/finance/wallets/wallets-page.test.tsx +7 -0
- package/src/components/ui/finance/wallets/wallets-page.tsx +21 -5
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +14 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +29 -0
- package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +11 -0
- package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +10 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +141 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +208 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +26 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +36 -20
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +24 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +18 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.test.ts +84 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +5 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +300 -172
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +411 -323
- package/src/components/ui/tu-do/shared/task-sound-effects.test.ts +189 -0
- package/src/components/ui/tu-do/shared/task-sound-effects.tsx +468 -0
- package/src/hooks/__tests__/use-task-actions.test.tsx +61 -0
- 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
|
-
|
|
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
|
-
|
|
48
|
-
mocks.
|
|
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 {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
}
|
package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx
CHANGED
|
@@ -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
|
+
});
|