@tuturuuu/ui 0.4.1 → 0.6.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 +43 -0
- package/package.json +41 -34
- package/src/components/ui/currency-input.tsx +65 -23
- package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +64 -0
- package/src/components/ui/custom/__tests__/sidebar-remote-behavior-bridge.test.tsx +109 -0
- package/src/components/ui/custom/combobox.test.tsx +141 -0
- package/src/components/ui/custom/combobox.tsx +105 -36
- package/src/components/ui/custom/settings/task-settings.tsx +126 -0
- package/src/components/ui/custom/settings/task-sound-settings.test.tsx +146 -0
- package/src/components/ui/custom/sidebar-context.tsx +68 -6
- package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +21 -2
- package/src/components/ui/finance/finance-layout.tsx +2 -4
- package/src/components/ui/finance/shared/balance-mode-toggle.tsx +35 -0
- package/src/components/ui/finance/shared/finance-layout-controls.tsx +43 -0
- package/src/components/ui/finance/shared/quick-actions.tsx +14 -6
- package/src/components/ui/finance/shared/use-finance-balance-mode.ts +72 -0
- package/src/components/ui/finance/shared/wallet-balance-mode.test.ts +66 -0
- package/src/components/ui/finance/shared/wallet-balance-mode.ts +42 -0
- package/src/components/ui/finance/transactions/form-types.ts +23 -0
- package/src/components/ui/finance/transactions/form.tsx +81 -22
- 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/transactions/wallet-filter.tsx +21 -2
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +219 -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-history-dialog.tsx +617 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +197 -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 +541 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +362 -0
- package/src/components/ui/finance/wallets/columns-rendering.test.tsx +125 -0
- package/src/components/ui/finance/wallets/columns.test.ts +56 -0
- package/src/components/ui/finance/wallets/columns.tsx +196 -43
- package/src/components/ui/finance/wallets/form.test.tsx +79 -14
- package/src/components/ui/finance/wallets/form.tsx +41 -197
- package/src/components/ui/finance/wallets/query-invalidation.ts +3 -0
- package/src/components/ui/finance/wallets/wallet-basics-fields.tsx +141 -0
- package/src/components/ui/finance/wallets/wallet-credit-fields.tsx +136 -0
- package/src/components/ui/finance/wallets/walletId/credit-wallet-summary.tsx +143 -68
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +105 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +120 -16
- package/src/components/ui/finance/wallets/walletId/wallet-details-amount.test.tsx +64 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-amount.tsx +226 -6
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +71 -5
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +52 -35
- package/src/components/ui/finance/wallets/wallets-data-table.test.tsx +171 -0
- package/src/components/ui/finance/wallets/wallets-data-table.tsx +132 -29
- package/src/components/ui/finance/wallets/wallets-page.test.tsx +117 -36
- package/src/components/ui/finance/wallets/wallets-page.tsx +40 -64
- package/src/components/ui/storefront/accent-button.tsx +33 -0
- package/src/components/ui/storefront/cart-summary.tsx +140 -0
- package/src/components/ui/storefront/empty-listings.tsx +32 -0
- package/src/components/ui/storefront/hero-panel.tsx +70 -0
- package/src/components/ui/storefront/image-panel.tsx +40 -0
- package/src/components/ui/storefront/index.ts +12 -0
- package/src/components/ui/storefront/listing-card.tsx +129 -0
- package/src/components/ui/storefront/storefront-surface.test.tsx +85 -0
- package/src/components/ui/storefront/storefront-surface.tsx +235 -0
- package/src/components/ui/storefront/types.ts +99 -0
- package/src/components/ui/storefront/utils.ts +90 -0
- 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/boards/boardId/task-card/task-card-open-options.test.ts +134 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +127 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +17 -42
- package/src/components/ui/tu-do/boards/boardId/timeline-board-open-task.test.tsx +164 -0
- package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +25 -16
- package/src/components/ui/tu-do/hooks/useTaskDialog.ts +15 -1
- 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-my-tasks-state.ts +2 -0
- package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +124 -7
- package/src/components/ui/tu-do/providers/__tests__/task-dialog-provider.test.tsx +217 -5
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +180 -35
- package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +222 -26
- package/src/components/ui/tu-do/shared/board-client.tsx +1 -3
- package/src/components/ui/tu-do/shared/list-view-context-menu.test.tsx +55 -2
- package/src/components/ui/tu-do/shared/list-view.tsx +23 -16
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +93 -76
- package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +11 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +268 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +243 -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/smart-task-suggestions-panel.test.tsx +129 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.tsx +358 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +1 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +6 -2
- 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 +41 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +157 -102
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-form-reset.ts +18 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +1 -2
- 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-dialog-actions.tsx +5 -3
- 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 +959 -340
- 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
- package/src/hooks/useBoardRealtime.ts +54 -1
- package/src/hooks/useBoardRealtimeEventHandler.ts +169 -4
- package/src/hooks/useTaskUserRealtime.ts +338 -0
|
@@ -10,11 +10,19 @@ import type { ReactNode } from 'react';
|
|
|
10
10
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
11
11
|
import { useTaskActions } from '../use-task-actions';
|
|
12
12
|
|
|
13
|
+
const { mockDispatchTaskSoundCue } = vi.hoisted(() => ({
|
|
14
|
+
mockDispatchTaskSoundCue: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
13
17
|
// Mock dependencies
|
|
14
18
|
vi.mock('@tuturuuu/supabase/next/client', () => ({
|
|
15
19
|
createClient: vi.fn(),
|
|
16
20
|
}));
|
|
17
21
|
|
|
22
|
+
vi.mock('../../components/ui/tu-do/shared/task-sound-effects', () => ({
|
|
23
|
+
dispatchTaskSoundCue: mockDispatchTaskSoundCue,
|
|
24
|
+
}));
|
|
25
|
+
|
|
18
26
|
vi.mock('@tuturuuu/ui/sonner', () => ({
|
|
19
27
|
toast: {
|
|
20
28
|
success: vi.fn(),
|
|
@@ -268,6 +276,12 @@ describe('useTaskActions', () => {
|
|
|
268
276
|
expect(mockToast.success).toHaveBeenCalledWith('Task completed', {
|
|
269
277
|
description: 'Task marked as done and moved to Done',
|
|
270
278
|
});
|
|
279
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledTimes(1);
|
|
280
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith({
|
|
281
|
+
count: 1,
|
|
282
|
+
cue: 'complete',
|
|
283
|
+
intensity: 1,
|
|
284
|
+
});
|
|
271
285
|
});
|
|
272
286
|
|
|
273
287
|
it('moves an external task to the personal and source done lists when checked', async () => {
|
|
@@ -421,6 +435,11 @@ describe('useTaskActions', () => {
|
|
|
421
435
|
expect(mockUpdateWorkspaceTask).toHaveBeenCalledWith('ws-1', 'task-1', {
|
|
422
436
|
closed_at: expect.any(String),
|
|
423
437
|
});
|
|
438
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith({
|
|
439
|
+
count: 1,
|
|
440
|
+
cue: 'complete',
|
|
441
|
+
intensity: 1,
|
|
442
|
+
});
|
|
424
443
|
});
|
|
425
444
|
|
|
426
445
|
it('prefers task.ws_id over a fallback workspace prop when archiving', async () => {
|
|
@@ -667,6 +686,12 @@ describe('useTaskActions', () => {
|
|
|
667
686
|
expect(mockToast.success).toHaveBeenCalledWith('2 tasks completed', {
|
|
668
687
|
description: 'Tasks marked as done',
|
|
669
688
|
});
|
|
689
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledTimes(1);
|
|
690
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith({
|
|
691
|
+
count: 2,
|
|
692
|
+
cue: 'complete',
|
|
693
|
+
intensity: 1.15,
|
|
694
|
+
});
|
|
670
695
|
});
|
|
671
696
|
});
|
|
672
697
|
|
|
@@ -849,6 +874,12 @@ describe('useTaskActions', () => {
|
|
|
849
874
|
expect(mockToast.success).toHaveBeenCalledWith('Success', {
|
|
850
875
|
description: 'Task deleted successfully',
|
|
851
876
|
});
|
|
877
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledTimes(1);
|
|
878
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith({
|
|
879
|
+
count: 1,
|
|
880
|
+
cue: 'delete',
|
|
881
|
+
intensity: 1,
|
|
882
|
+
});
|
|
852
883
|
expect(setDeleteDialogOpen).toHaveBeenCalledWith(false);
|
|
853
884
|
|
|
854
885
|
const deletedTasks = queryClient.getQueryData<Task[]>([
|
|
@@ -939,6 +970,12 @@ describe('useTaskActions', () => {
|
|
|
939
970
|
expect(mockToast.success).toHaveBeenCalledWith('Due date updated', {
|
|
940
971
|
description: 'Due date set successfully',
|
|
941
972
|
});
|
|
973
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledTimes(1);
|
|
974
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith({
|
|
975
|
+
count: 1,
|
|
976
|
+
cue: 'update',
|
|
977
|
+
intensity: 1,
|
|
978
|
+
});
|
|
942
979
|
});
|
|
943
980
|
|
|
944
981
|
it('should handle bulk due date update', async () => {
|
|
@@ -1041,6 +1078,12 @@ describe('useTaskActions', () => {
|
|
|
1041
1078
|
expect(mockToast.success).toHaveBeenCalledWith('Priority updated', {
|
|
1042
1079
|
description: 'Priority changed',
|
|
1043
1080
|
});
|
|
1081
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledTimes(1);
|
|
1082
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith({
|
|
1083
|
+
count: 1,
|
|
1084
|
+
cue: 'update',
|
|
1085
|
+
intensity: 1,
|
|
1086
|
+
});
|
|
1044
1087
|
});
|
|
1045
1088
|
|
|
1046
1089
|
it('should handle bulk priority update', async () => {
|
|
@@ -1154,6 +1197,12 @@ describe('useTaskActions', () => {
|
|
|
1154
1197
|
expect(mockToast.success).toHaveBeenCalledWith('Estimation updated', {
|
|
1155
1198
|
description: 'Estimation points updated successfully',
|
|
1156
1199
|
});
|
|
1200
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledTimes(1);
|
|
1201
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith({
|
|
1202
|
+
count: 1,
|
|
1203
|
+
cue: 'update',
|
|
1204
|
+
intensity: 1,
|
|
1205
|
+
});
|
|
1157
1206
|
});
|
|
1158
1207
|
|
|
1159
1208
|
it('should skip update if estimation points already match', async () => {
|
|
@@ -1314,6 +1363,12 @@ describe('useTaskActions', () => {
|
|
|
1314
1363
|
expect(mockUpdateWorkspaceTask).toHaveBeenCalledWith('ws-1', 'task-1', {
|
|
1315
1364
|
end_date: expect.any(String),
|
|
1316
1365
|
});
|
|
1366
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledTimes(1);
|
|
1367
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith({
|
|
1368
|
+
count: 1,
|
|
1369
|
+
cue: 'update',
|
|
1370
|
+
intensity: 1,
|
|
1371
|
+
});
|
|
1317
1372
|
});
|
|
1318
1373
|
|
|
1319
1374
|
it('delegates selected-card custom dates to the bulk updater', async () => {
|
|
@@ -1625,6 +1680,12 @@ describe('useTaskActions', () => {
|
|
|
1625
1680
|
expect(mockUpdateWorkspaceTask).toHaveBeenCalledWith('ws-1', 'task-1', {
|
|
1626
1681
|
list_id: 'list-2',
|
|
1627
1682
|
});
|
|
1683
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledTimes(1);
|
|
1684
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith({
|
|
1685
|
+
count: 1,
|
|
1686
|
+
cue: 'move',
|
|
1687
|
+
intensity: 1,
|
|
1688
|
+
});
|
|
1628
1689
|
expect(setMenuOpen).toHaveBeenCalledWith(false);
|
|
1629
1690
|
});
|
|
1630
1691
|
|
|
@@ -16,6 +16,10 @@ import {
|
|
|
16
16
|
import { addDays } from 'date-fns';
|
|
17
17
|
import { useCallback } from 'react';
|
|
18
18
|
import { useBoardBroadcast } from '../components/ui/tu-do/shared/board-broadcast-context';
|
|
19
|
+
import {
|
|
20
|
+
dispatchTaskSoundCue,
|
|
21
|
+
type TaskSoundCue,
|
|
22
|
+
} from '../components/ui/tu-do/shared/task-sound-effects';
|
|
19
23
|
import {
|
|
20
24
|
isPersonalExternalTask,
|
|
21
25
|
moveExternalTaskToPersonalList,
|
|
@@ -42,6 +46,21 @@ interface UseTaskActionsProps {
|
|
|
42
46
|
bulkUpdateCustomDueDate?: (date: Date | null) => Promise<void>;
|
|
43
47
|
}
|
|
44
48
|
|
|
49
|
+
function dispatchTaskActionSound(cue: TaskSoundCue, count = 1) {
|
|
50
|
+
dispatchTaskSoundCue({
|
|
51
|
+
cue,
|
|
52
|
+
count,
|
|
53
|
+
intensity: count > 1 ? 1.15 : 1,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getMoveSoundCue(targetList?: TaskList | null): TaskSoundCue {
|
|
58
|
+
return isTaskBoardCompletedStatus(targetList?.status) ||
|
|
59
|
+
isTaskBoardTerminalStatus(targetList?.status)
|
|
60
|
+
? 'complete'
|
|
61
|
+
: 'move';
|
|
62
|
+
}
|
|
63
|
+
|
|
45
64
|
export function useTaskActions({
|
|
46
65
|
task,
|
|
47
66
|
boardId,
|
|
@@ -252,6 +271,7 @@ export function useTaskActions({
|
|
|
252
271
|
toast.success('Task completed', {
|
|
253
272
|
description: `Task marked as ${targetCompletionList.status === 'done' ? 'done' : 'closed'} and moved to ${targetCompletionList.name}`,
|
|
254
273
|
});
|
|
274
|
+
dispatchTaskActionSound('complete');
|
|
255
275
|
} catch (error) {
|
|
256
276
|
console.error('Failed to complete external task:', error);
|
|
257
277
|
toast.error('Error', {
|
|
@@ -321,6 +341,7 @@ export function useTaskActions({
|
|
|
321
341
|
toast.success('Task completed', {
|
|
322
342
|
description: `Task marked as done and moved to ${targetCompletionList.name}`,
|
|
323
343
|
});
|
|
344
|
+
dispatchTaskActionSound('complete');
|
|
324
345
|
} catch (error) {
|
|
325
346
|
// Rollback on error
|
|
326
347
|
if (previousTasks) {
|
|
@@ -373,6 +394,7 @@ export function useTaskActions({
|
|
|
373
394
|
completed_at: updatedTask.completed_at,
|
|
374
395
|
},
|
|
375
396
|
});
|
|
397
|
+
dispatchTaskActionSound(newClosedState ? 'complete' : 'update');
|
|
376
398
|
} catch (error) {
|
|
377
399
|
// Rollback on error
|
|
378
400
|
if (previousTasks) {
|
|
@@ -429,6 +451,7 @@ export function useTaskActions({
|
|
|
429
451
|
toast.success('Task completed', {
|
|
430
452
|
description: `Task marked as ${targetCompletionList.status === 'done' ? 'done' : 'closed'} and moved to ${targetCompletionList.name}`,
|
|
431
453
|
});
|
|
454
|
+
dispatchTaskActionSound('complete');
|
|
432
455
|
return;
|
|
433
456
|
}
|
|
434
457
|
|
|
@@ -496,6 +519,7 @@ export function useTaskActions({
|
|
|
496
519
|
toast.warning('Partial completion update', {
|
|
497
520
|
description: `${successCount}/${tasksToMove.length} tasks updated`,
|
|
498
521
|
});
|
|
522
|
+
dispatchTaskActionSound('complete', successCount);
|
|
499
523
|
return;
|
|
500
524
|
}
|
|
501
525
|
|
|
@@ -509,6 +533,7 @@ export function useTaskActions({
|
|
|
509
533
|
: `Task marked as ${targetCompletionList.status === 'done' ? 'done' : 'closed'} and moved to ${targetCompletionList.name}`,
|
|
510
534
|
}
|
|
511
535
|
);
|
|
536
|
+
dispatchTaskActionSound('complete', taskCount);
|
|
512
537
|
} catch (error) {
|
|
513
538
|
// Rollback on error
|
|
514
539
|
if (previousTasks) {
|
|
@@ -564,6 +589,7 @@ export function useTaskActions({
|
|
|
564
589
|
toast.success('Success', {
|
|
565
590
|
description: 'Task marked as closed',
|
|
566
591
|
});
|
|
592
|
+
dispatchTaskActionSound('complete');
|
|
567
593
|
return;
|
|
568
594
|
}
|
|
569
595
|
|
|
@@ -627,6 +653,7 @@ export function useTaskActions({
|
|
|
627
653
|
toast.warning('Partial close update', {
|
|
628
654
|
description: `${successCount}/${tasksToMove.length} tasks updated`,
|
|
629
655
|
});
|
|
656
|
+
dispatchTaskActionSound('complete', successCount);
|
|
630
657
|
return;
|
|
631
658
|
}
|
|
632
659
|
|
|
@@ -637,6 +664,7 @@ export function useTaskActions({
|
|
|
637
664
|
? `${taskCount} tasks marked as closed`
|
|
638
665
|
: 'Task marked as closed',
|
|
639
666
|
});
|
|
667
|
+
dispatchTaskActionSound('complete', taskCount);
|
|
640
668
|
} catch (error) {
|
|
641
669
|
// Rollback on error
|
|
642
670
|
if (previousTasks) {
|
|
@@ -741,6 +769,7 @@ export function useTaskActions({
|
|
|
741
769
|
toast.warning('Partial delete update', {
|
|
742
770
|
description: `${successCount}/${tasksToDelete.length} tasks deleted`,
|
|
743
771
|
});
|
|
772
|
+
dispatchTaskActionSound('delete', successCount);
|
|
744
773
|
return;
|
|
745
774
|
}
|
|
746
775
|
|
|
@@ -751,6 +780,7 @@ export function useTaskActions({
|
|
|
751
780
|
? `${taskCount} tasks deleted`
|
|
752
781
|
: 'Task deleted successfully',
|
|
753
782
|
});
|
|
783
|
+
dispatchTaskActionSound('delete', taskCount);
|
|
754
784
|
|
|
755
785
|
setDeleteDialogOpen?.(false);
|
|
756
786
|
} catch (error) {
|
|
@@ -817,6 +847,7 @@ export function useTaskActions({
|
|
|
817
847
|
toast.success('Success', {
|
|
818
848
|
description: 'All assignees removed from task',
|
|
819
849
|
});
|
|
850
|
+
dispatchTaskActionSound('update');
|
|
820
851
|
} catch (error) {
|
|
821
852
|
queryClient.setQueryData(['tasks', boardId], previousTasks);
|
|
822
853
|
console.error('Failed to remove all assignees:', error);
|
|
@@ -888,6 +919,7 @@ export function useTaskActions({
|
|
|
888
919
|
description: `${assignee?.display_name || assignee?.email || 'Assignee'} removed from task`,
|
|
889
920
|
});
|
|
890
921
|
broadcast?.('task:relations-changed', { taskId: task.id });
|
|
922
|
+
dispatchTaskActionSound('update');
|
|
891
923
|
} catch (error) {
|
|
892
924
|
queryClient.setQueryData(['tasks', boardId], previousTasks);
|
|
893
925
|
console.error('Failed to remove assignee:', error);
|
|
@@ -977,6 +1009,7 @@ export function useTaskActions({
|
|
|
977
1009
|
toast.success('Success', {
|
|
978
1010
|
description: `Task moved to ${targetList.name || 'selected list'}`,
|
|
979
1011
|
});
|
|
1012
|
+
dispatchTaskActionSound(getMoveSoundCue(targetList));
|
|
980
1013
|
} catch (error) {
|
|
981
1014
|
console.error('Failed to move external task:', error);
|
|
982
1015
|
toast.error('Error', {
|
|
@@ -1087,6 +1120,7 @@ export function useTaskActions({
|
|
|
1087
1120
|
toast.warning('Partial move update', {
|
|
1088
1121
|
description: `${successCount}/${tasksToMove.length} tasks updated`,
|
|
1089
1122
|
});
|
|
1123
|
+
dispatchTaskActionSound(getMoveSoundCue(targetList), successCount);
|
|
1090
1124
|
return;
|
|
1091
1125
|
}
|
|
1092
1126
|
|
|
@@ -1097,6 +1131,7 @@ export function useTaskActions({
|
|
|
1097
1131
|
? `${taskCount} tasks moved to ${targetList?.name || 'selected list'}`
|
|
1098
1132
|
: `Task moved to ${targetList?.name || 'selected list'}`,
|
|
1099
1133
|
});
|
|
1134
|
+
dispatchTaskActionSound(getMoveSoundCue(targetList), taskCount);
|
|
1100
1135
|
} catch (error) {
|
|
1101
1136
|
// Rollback on error
|
|
1102
1137
|
if (previousTasks) {
|
|
@@ -1213,6 +1248,7 @@ export function useTaskActions({
|
|
|
1213
1248
|
toast.warning('Partial due date update', {
|
|
1214
1249
|
description: `${succeededTaskIds.length}/${tasksToUpdate.length} tasks updated`,
|
|
1215
1250
|
});
|
|
1251
|
+
dispatchTaskActionSound('update', succeededTaskIds.length);
|
|
1216
1252
|
} else {
|
|
1217
1253
|
const taskCount = tasksToUpdate.length;
|
|
1218
1254
|
toast.success('Due date updated', {
|
|
@@ -1223,6 +1259,7 @@ export function useTaskActions({
|
|
|
1223
1259
|
? 'Due date set successfully'
|
|
1224
1260
|
: 'Due date removed',
|
|
1225
1261
|
});
|
|
1262
|
+
dispatchTaskActionSound('update', taskCount);
|
|
1226
1263
|
}
|
|
1227
1264
|
} catch (error) {
|
|
1228
1265
|
console.error('Failed to update due date:', error);
|
|
@@ -1328,6 +1365,7 @@ export function useTaskActions({
|
|
|
1328
1365
|
toast.warning('Partial priority update', {
|
|
1329
1366
|
description: `${succeededTaskIds.length}/${tasksToUpdate.length} tasks updated`,
|
|
1330
1367
|
});
|
|
1368
|
+
dispatchTaskActionSound('update', succeededTaskIds.length);
|
|
1331
1369
|
} else {
|
|
1332
1370
|
const taskCount = tasksToUpdate.length;
|
|
1333
1371
|
toast.success('Priority updated', {
|
|
@@ -1338,6 +1376,7 @@ export function useTaskActions({
|
|
|
1338
1376
|
? 'Priority changed'
|
|
1339
1377
|
: 'Priority cleared',
|
|
1340
1378
|
});
|
|
1379
|
+
dispatchTaskActionSound('update', taskCount);
|
|
1341
1380
|
}
|
|
1342
1381
|
|
|
1343
1382
|
// Don't auto-clear selection - let user manually clear with "Clear" button
|
|
@@ -1459,6 +1498,7 @@ export function useTaskActions({
|
|
|
1459
1498
|
toast.warning('Partial estimation update', {
|
|
1460
1499
|
description: `${succeededIds.length}/${tasksToUpdate.length} tasks updated`,
|
|
1461
1500
|
});
|
|
1501
|
+
dispatchTaskActionSound('update', succeededIds.length);
|
|
1462
1502
|
|
|
1463
1503
|
return;
|
|
1464
1504
|
}
|
|
@@ -1476,6 +1516,7 @@ export function useTaskActions({
|
|
|
1476
1516
|
? `${taskCount} tasks updated`
|
|
1477
1517
|
: 'Estimation points updated successfully',
|
|
1478
1518
|
});
|
|
1519
|
+
dispatchTaskActionSound('update', taskCount);
|
|
1479
1520
|
|
|
1480
1521
|
return;
|
|
1481
1522
|
} catch (e: any) {
|
|
@@ -1575,6 +1616,7 @@ export function useTaskActions({
|
|
|
1575
1616
|
? 'Custom due date set successfully'
|
|
1576
1617
|
: 'Due date removed',
|
|
1577
1618
|
});
|
|
1619
|
+
dispatchTaskActionSound('update');
|
|
1578
1620
|
} catch (error) {
|
|
1579
1621
|
if (previousTasks) {
|
|
1580
1622
|
queryClient.setQueryData(['tasks', boardId], previousTasks);
|
|
@@ -1797,6 +1839,9 @@ export function useTaskActions({
|
|
|
1797
1839
|
? `${succeededTaskIds.length} tasks updated`
|
|
1798
1840
|
: `${assigneeName} ${active ? 'removed' : 'added'} on task`,
|
|
1799
1841
|
});
|
|
1842
|
+
if (succeededTaskIds.length > 0) {
|
|
1843
|
+
dispatchTaskActionSound('update', succeededTaskIds.length);
|
|
1844
|
+
}
|
|
1800
1845
|
|
|
1801
1846
|
// Don't auto-clear selection - let user manually clear with "Clear" button
|
|
1802
1847
|
} catch (e: any) {
|
|
@@ -223,6 +223,18 @@ export function useBoardRealtime(
|
|
|
223
223
|
payload as BoardRealtimePayload
|
|
224
224
|
);
|
|
225
225
|
})
|
|
226
|
+
.on('broadcast', { event: 'task:upsert:batch' }, ({ payload }) => {
|
|
227
|
+
handleBoardRealtimeEvent(
|
|
228
|
+
'task:upsert:batch',
|
|
229
|
+
payload as BoardRealtimePayload
|
|
230
|
+
);
|
|
231
|
+
})
|
|
232
|
+
.on('broadcast', { event: 'task:delete:batch' }, ({ payload }) => {
|
|
233
|
+
handleBoardRealtimeEvent(
|
|
234
|
+
'task:delete:batch',
|
|
235
|
+
payload as BoardRealtimePayload
|
|
236
|
+
);
|
|
237
|
+
})
|
|
226
238
|
.on('broadcast', { event: 'list:upsert' }, ({ payload }) => {
|
|
227
239
|
handleBoardRealtimeEvent(
|
|
228
240
|
'list:upsert',
|
|
@@ -235,6 +247,18 @@ export function useBoardRealtime(
|
|
|
235
247
|
payload as BoardRealtimePayload
|
|
236
248
|
);
|
|
237
249
|
})
|
|
250
|
+
.on('broadcast', { event: 'list:upsert:batch' }, ({ payload }) => {
|
|
251
|
+
handleBoardRealtimeEvent(
|
|
252
|
+
'list:upsert:batch',
|
|
253
|
+
payload as BoardRealtimePayload
|
|
254
|
+
);
|
|
255
|
+
})
|
|
256
|
+
.on('broadcast', { event: 'list:delete:batch' }, ({ payload }) => {
|
|
257
|
+
handleBoardRealtimeEvent(
|
|
258
|
+
'list:delete:batch',
|
|
259
|
+
payload as BoardRealtimePayload
|
|
260
|
+
);
|
|
261
|
+
})
|
|
238
262
|
.on('broadcast', { event: 'task:relations-changed' }, ({ payload }) => {
|
|
239
263
|
handleBoardRealtimeEvent(
|
|
240
264
|
'task:relations-changed',
|
|
@@ -246,6 +270,22 @@ export function useBoardRealtime(
|
|
|
246
270
|
'task:deps-changed',
|
|
247
271
|
payload as BoardRealtimePayload
|
|
248
272
|
);
|
|
273
|
+
})
|
|
274
|
+
.on(
|
|
275
|
+
'broadcast',
|
|
276
|
+
{ event: 'task:relations-changed:batch' },
|
|
277
|
+
({ payload }) => {
|
|
278
|
+
handleBoardRealtimeEvent(
|
|
279
|
+
'task:relations-changed:batch',
|
|
280
|
+
payload as BoardRealtimePayload
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
.on('broadcast', { event: 'task:deps-changed:batch' }, ({ payload }) => {
|
|
285
|
+
handleBoardRealtimeEvent(
|
|
286
|
+
'task:deps-changed:batch',
|
|
287
|
+
payload as BoardRealtimePayload
|
|
288
|
+
);
|
|
249
289
|
});
|
|
250
290
|
|
|
251
291
|
channel.subscribe((status, err) => {
|
|
@@ -328,11 +368,24 @@ export function useBoardRealtime(
|
|
|
328
368
|
const existing = merged.get(task.id);
|
|
329
369
|
merged.set(task.id, existing ? { ...existing, ...task } : task);
|
|
330
370
|
}
|
|
331
|
-
|
|
371
|
+
const tasks = [...merged.values()];
|
|
372
|
+
if (tasks.length > 1) {
|
|
373
|
+
sendBoardRealtimeEvent(channel, 'task:upsert:batch', {
|
|
374
|
+
payloads: tasks.map((task) => ({ task })),
|
|
375
|
+
});
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
for (const task of tasks) {
|
|
332
379
|
sendBoardRealtimeEvent(channel, event, { task });
|
|
333
380
|
}
|
|
334
381
|
} else {
|
|
335
382
|
// Other events (task:delete, list:upsert, list:delete) — send each
|
|
383
|
+
if (payloads.length > 1) {
|
|
384
|
+
sendBoardRealtimeEvent(channel, `${event}:batch`, {
|
|
385
|
+
payloads,
|
|
386
|
+
});
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
336
389
|
for (const p of payloads) {
|
|
337
390
|
sendBoardRealtimeEvent(channel, event, p);
|
|
338
391
|
}
|
|
@@ -76,11 +76,120 @@ function updateBoardTaskCaches(
|
|
|
76
76
|
boardId: string,
|
|
77
77
|
updater: (old: Task[] | undefined) => Task[] | undefined
|
|
78
78
|
) {
|
|
79
|
-
queryClient.setQueryData(['tasks', boardId], updater);
|
|
79
|
+
queryClient.setQueryData<Task[]>(['tasks', boardId], updater);
|
|
80
|
+
queryClient.setQueryData<Task[]>(['tasks-full', boardId], updater);
|
|
81
|
+
queryClient.setQueriesData<Task[]>({ queryKey: ['tasks', boardId] }, updater);
|
|
82
|
+
queryClient.setQueriesData<Task[]>(
|
|
83
|
+
{ queryKey: ['tasks-full', boardId] },
|
|
84
|
+
updater
|
|
85
|
+
);
|
|
86
|
+
}
|
|
80
87
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
88
|
+
function patchWorkspaceTaskCaches(
|
|
89
|
+
queryClient: QueryClient,
|
|
90
|
+
taskData: Partial<Task> & { id: string }
|
|
91
|
+
) {
|
|
92
|
+
queryClient.setQueriesData<{ task?: Task | null }>(
|
|
93
|
+
{
|
|
94
|
+
predicate: (query) =>
|
|
95
|
+
Array.isArray(query.queryKey) &&
|
|
96
|
+
query.queryKey[0] === 'workspaceTask' &&
|
|
97
|
+
query.queryKey[2] === taskData.id,
|
|
98
|
+
},
|
|
99
|
+
(old) =>
|
|
100
|
+
old?.task
|
|
101
|
+
? {
|
|
102
|
+
...old,
|
|
103
|
+
task: { ...old.task, ...taskData },
|
|
104
|
+
}
|
|
105
|
+
: old
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function patchMyTasksCaches(
|
|
110
|
+
queryClient: QueryClient,
|
|
111
|
+
taskData: Partial<Task> & { id: string }
|
|
112
|
+
) {
|
|
113
|
+
const patchTask = <T extends { id?: string }>(task: T): T =>
|
|
114
|
+
task.id === taskData.id ? ({ ...task, ...taskData } as T) : task;
|
|
115
|
+
|
|
116
|
+
queryClient.setQueriesData<{
|
|
117
|
+
overdue?: Array<{ id?: string }>;
|
|
118
|
+
today?: Array<{ id?: string }>;
|
|
119
|
+
upcoming?: Array<{ id?: string }>;
|
|
120
|
+
completed?: Array<{ id?: string }>;
|
|
121
|
+
}>({ queryKey: ['my-tasks'] }, (old) =>
|
|
122
|
+
old
|
|
123
|
+
? {
|
|
124
|
+
...old,
|
|
125
|
+
overdue: old.overdue?.map(patchTask) ?? old.overdue,
|
|
126
|
+
today: old.today?.map(patchTask) ?? old.today,
|
|
127
|
+
upcoming: old.upcoming?.map(patchTask) ?? old.upcoming,
|
|
128
|
+
completed: old.completed?.map(patchTask) ?? old.completed,
|
|
129
|
+
}
|
|
130
|
+
: old
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
queryClient.setQueriesData<{
|
|
134
|
+
pages?: Array<{ completed?: Array<{ id?: string }> }>;
|
|
135
|
+
}>({ queryKey: ['my-completed-tasks'] }, (old) =>
|
|
136
|
+
old?.pages
|
|
137
|
+
? {
|
|
138
|
+
...old,
|
|
139
|
+
pages: old.pages.map((page) => ({
|
|
140
|
+
...page,
|
|
141
|
+
completed: page.completed?.map(patchTask) ?? page.completed,
|
|
142
|
+
})),
|
|
143
|
+
}
|
|
144
|
+
: old
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function deleteFromMyTasksCaches(queryClient: QueryClient, taskId: string) {
|
|
149
|
+
const removeTask = <T extends { id?: string }>(tasks: T[] | undefined) =>
|
|
150
|
+
tasks?.filter((task) => task.id !== taskId);
|
|
151
|
+
|
|
152
|
+
queryClient.setQueriesData<{
|
|
153
|
+
overdue?: Array<{ id?: string }>;
|
|
154
|
+
today?: Array<{ id?: string }>;
|
|
155
|
+
upcoming?: Array<{ id?: string }>;
|
|
156
|
+
completed?: Array<{ id?: string }>;
|
|
157
|
+
}>({ queryKey: ['my-tasks'] }, (old) =>
|
|
158
|
+
old
|
|
159
|
+
? {
|
|
160
|
+
...old,
|
|
161
|
+
overdue: removeTask(old.overdue) ?? old.overdue,
|
|
162
|
+
today: removeTask(old.today) ?? old.today,
|
|
163
|
+
upcoming: removeTask(old.upcoming) ?? old.upcoming,
|
|
164
|
+
completed: removeTask(old.completed) ?? old.completed,
|
|
165
|
+
}
|
|
166
|
+
: old
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
queryClient.setQueriesData<{
|
|
170
|
+
pages?: Array<{ completed?: Array<{ id?: string }> }>;
|
|
171
|
+
}>({ queryKey: ['my-completed-tasks'] }, (old) =>
|
|
172
|
+
old?.pages
|
|
173
|
+
? {
|
|
174
|
+
...old,
|
|
175
|
+
pages: old.pages.map((page) => ({
|
|
176
|
+
...page,
|
|
177
|
+
completed: removeTask(page.completed) ?? page.completed,
|
|
178
|
+
})),
|
|
179
|
+
}
|
|
180
|
+
: old
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function invalidateTaskMembershipQueries(
|
|
185
|
+
queryClient: QueryClient,
|
|
186
|
+
boardId: string
|
|
187
|
+
) {
|
|
188
|
+
void queryClient.invalidateQueries({
|
|
189
|
+
queryKey: ['task-list-counts', boardId],
|
|
190
|
+
});
|
|
191
|
+
void queryClient.invalidateQueries({ queryKey: ['my-tasks'] });
|
|
192
|
+
void queryClient.invalidateQueries({ queryKey: ['my-completed-tasks'] });
|
|
84
193
|
}
|
|
85
194
|
|
|
86
195
|
export function useBoardRealtimeEventHandler({
|
|
@@ -164,6 +273,10 @@ export function useBoardRealtimeEventHandler({
|
|
|
164
273
|
return relations ? { ...task, ...relations } : task;
|
|
165
274
|
});
|
|
166
275
|
});
|
|
276
|
+
for (const [taskId, relations] of relationsMap.entries()) {
|
|
277
|
+
patchWorkspaceTaskCaches(queryClient, { id: taskId, ...relations });
|
|
278
|
+
patchMyTasksCaches(queryClient, { id: taskId, ...relations });
|
|
279
|
+
}
|
|
167
280
|
} catch (err) {
|
|
168
281
|
if (DEV_MODE) {
|
|
169
282
|
console.error(
|
|
@@ -178,8 +291,40 @@ export function useBoardRealtimeEventHandler({
|
|
|
178
291
|
(event: string, payload: BoardRealtimePayload) => {
|
|
179
292
|
if (rememberEventId(payload)) return;
|
|
180
293
|
|
|
294
|
+
if (event.endsWith(':batch')) {
|
|
295
|
+
const baseEvent = event.slice(0, -':batch'.length);
|
|
296
|
+
const payloads = Array.isArray(payload.payloads)
|
|
297
|
+
? payload.payloads
|
|
298
|
+
: Array.isArray(payload.events)
|
|
299
|
+
? payload.events
|
|
300
|
+
.filter((entry) => {
|
|
301
|
+
return (
|
|
302
|
+
typeof entry === 'object' &&
|
|
303
|
+
entry !== null &&
|
|
304
|
+
'payload' in entry
|
|
305
|
+
);
|
|
306
|
+
})
|
|
307
|
+
.map((entry) => (entry as { payload: unknown }).payload)
|
|
308
|
+
: [];
|
|
309
|
+
|
|
310
|
+
for (const childPayload of payloads) {
|
|
311
|
+
if (
|
|
312
|
+
typeof childPayload === 'object' &&
|
|
313
|
+
childPayload !== null &&
|
|
314
|
+
!Array.isArray(childPayload)
|
|
315
|
+
) {
|
|
316
|
+
handleBoardRealtimeEvent(
|
|
317
|
+
baseEvent,
|
|
318
|
+
childPayload as BoardRealtimePayload
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
181
325
|
if (event === 'task:upsert') {
|
|
182
326
|
const taskData = payload.task as Partial<Task> & { id: string };
|
|
327
|
+
if (!taskData?.id) return;
|
|
183
328
|
if (DEV_MODE) {
|
|
184
329
|
console.log('[useBoardRealtime] task:upsert received', taskData.id);
|
|
185
330
|
}
|
|
@@ -193,6 +338,17 @@ export function useBoardRealtimeEventHandler({
|
|
|
193
338
|
updateBoardTaskCaches(queryClient, boardId, (old) =>
|
|
194
339
|
mergeRealtimeTask(old, taskData)
|
|
195
340
|
);
|
|
341
|
+
patchWorkspaceTaskCaches(queryClient, taskData);
|
|
342
|
+
patchMyTasksCaches(queryClient, taskData);
|
|
343
|
+
if (
|
|
344
|
+
'list_id' in taskData ||
|
|
345
|
+
'completed' in taskData ||
|
|
346
|
+
'completed_at' in taskData ||
|
|
347
|
+
'closed_at' in taskData ||
|
|
348
|
+
'deleted_at' in taskData
|
|
349
|
+
) {
|
|
350
|
+
invalidateTaskMembershipQueries(queryClient, boardId);
|
|
351
|
+
}
|
|
196
352
|
return;
|
|
197
353
|
}
|
|
198
354
|
|
|
@@ -208,6 +364,14 @@ export function useBoardRealtimeEventHandler({
|
|
|
208
364
|
updateBoardTaskCaches(queryClient, boardId, (old) =>
|
|
209
365
|
deleteRealtimeTask(old, taskId)
|
|
210
366
|
);
|
|
367
|
+
deleteFromMyTasksCaches(queryClient, taskId);
|
|
368
|
+
queryClient.removeQueries({
|
|
369
|
+
predicate: (query) =>
|
|
370
|
+
Array.isArray(query.queryKey) &&
|
|
371
|
+
query.queryKey[0] === 'workspaceTask' &&
|
|
372
|
+
query.queryKey[2] === taskId,
|
|
373
|
+
});
|
|
374
|
+
invalidateTaskMembershipQueries(queryClient, boardId);
|
|
211
375
|
|
|
212
376
|
if (deleted) {
|
|
213
377
|
onTaskChangeRef.current?.(deleted, 'DELETE');
|
|
@@ -262,6 +426,7 @@ export function useBoardRealtimeEventHandler({
|
|
|
262
426
|
updateBoardTaskCaches(queryClient, boardId, (old) =>
|
|
263
427
|
old?.filter((task) => task.list_id !== listId)
|
|
264
428
|
);
|
|
429
|
+
invalidateTaskMembershipQueries(queryClient, boardId);
|
|
265
430
|
|
|
266
431
|
if (deleted) {
|
|
267
432
|
onListChangeRef.current?.(deleted, 'DELETE');
|