@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.
Files changed (107) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/package.json +41 -34
  3. package/src/components/ui/currency-input.tsx +65 -23
  4. package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +64 -0
  5. package/src/components/ui/custom/__tests__/sidebar-remote-behavior-bridge.test.tsx +109 -0
  6. package/src/components/ui/custom/combobox.test.tsx +141 -0
  7. package/src/components/ui/custom/combobox.tsx +105 -36
  8. package/src/components/ui/custom/settings/task-settings.tsx +126 -0
  9. package/src/components/ui/custom/settings/task-sound-settings.test.tsx +146 -0
  10. package/src/components/ui/custom/sidebar-context.tsx +68 -6
  11. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +21 -2
  12. package/src/components/ui/finance/finance-layout.tsx +2 -4
  13. package/src/components/ui/finance/shared/balance-mode-toggle.tsx +35 -0
  14. package/src/components/ui/finance/shared/finance-layout-controls.tsx +43 -0
  15. package/src/components/ui/finance/shared/quick-actions.tsx +14 -6
  16. package/src/components/ui/finance/shared/use-finance-balance-mode.ts +72 -0
  17. package/src/components/ui/finance/shared/wallet-balance-mode.test.ts +66 -0
  18. package/src/components/ui/finance/shared/wallet-balance-mode.ts +42 -0
  19. package/src/components/ui/finance/transactions/form-types.ts +23 -0
  20. package/src/components/ui/finance/transactions/form.tsx +81 -22
  21. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +29 -18
  22. package/src/components/ui/finance/transactions/transaction-card.tsx +75 -43
  23. package/src/components/ui/finance/transactions/transfer-merge.test.ts +90 -0
  24. package/src/components/ui/finance/transactions/transfer-merge.ts +52 -0
  25. package/src/components/ui/finance/transactions/wallet-filter.tsx +21 -2
  26. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +219 -0
  27. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-amount.tsx +32 -0
  28. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-delete-dialog.tsx +50 -0
  29. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-dialog.tsx +138 -0
  30. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +617 -0
  31. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +197 -0
  32. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +201 -0
  33. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +541 -0
  34. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +362 -0
  35. package/src/components/ui/finance/wallets/columns-rendering.test.tsx +125 -0
  36. package/src/components/ui/finance/wallets/columns.test.ts +56 -0
  37. package/src/components/ui/finance/wallets/columns.tsx +196 -43
  38. package/src/components/ui/finance/wallets/form.test.tsx +79 -14
  39. package/src/components/ui/finance/wallets/form.tsx +41 -197
  40. package/src/components/ui/finance/wallets/query-invalidation.ts +3 -0
  41. package/src/components/ui/finance/wallets/wallet-basics-fields.tsx +141 -0
  42. package/src/components/ui/finance/wallets/wallet-credit-fields.tsx +136 -0
  43. package/src/components/ui/finance/wallets/walletId/credit-wallet-summary.tsx +143 -68
  44. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +105 -0
  45. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +120 -16
  46. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.test.tsx +64 -0
  47. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.tsx +226 -6
  48. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +71 -5
  49. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +52 -35
  50. package/src/components/ui/finance/wallets/wallets-data-table.test.tsx +171 -0
  51. package/src/components/ui/finance/wallets/wallets-data-table.tsx +132 -29
  52. package/src/components/ui/finance/wallets/wallets-page.test.tsx +117 -36
  53. package/src/components/ui/finance/wallets/wallets-page.tsx +40 -64
  54. package/src/components/ui/storefront/accent-button.tsx +33 -0
  55. package/src/components/ui/storefront/cart-summary.tsx +140 -0
  56. package/src/components/ui/storefront/empty-listings.tsx +32 -0
  57. package/src/components/ui/storefront/hero-panel.tsx +70 -0
  58. package/src/components/ui/storefront/image-panel.tsx +40 -0
  59. package/src/components/ui/storefront/index.ts +12 -0
  60. package/src/components/ui/storefront/listing-card.tsx +129 -0
  61. package/src/components/ui/storefront/storefront-surface.test.tsx +85 -0
  62. package/src/components/ui/storefront/storefront-surface.tsx +235 -0
  63. package/src/components/ui/storefront/types.ts +99 -0
  64. package/src/components/ui/storefront/utils.ts +90 -0
  65. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +14 -0
  66. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +29 -0
  67. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +134 -0
  68. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +127 -0
  69. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +17 -42
  70. package/src/components/ui/tu-do/boards/boardId/timeline-board-open-task.test.tsx +164 -0
  71. package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +25 -16
  72. package/src/components/ui/tu-do/hooks/useTaskDialog.ts +15 -1
  73. package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +11 -0
  74. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +2 -0
  75. package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +124 -7
  76. package/src/components/ui/tu-do/providers/__tests__/task-dialog-provider.test.tsx +217 -5
  77. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +180 -35
  78. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +222 -26
  79. package/src/components/ui/tu-do/shared/board-client.tsx +1 -3
  80. package/src/components/ui/tu-do/shared/list-view-context-menu.test.tsx +55 -2
  81. package/src/components/ui/tu-do/shared/list-view.tsx +23 -16
  82. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +93 -76
  83. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +11 -0
  84. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +268 -0
  85. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +243 -0
  86. package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +26 -0
  87. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.test.tsx +129 -0
  88. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.tsx +358 -0
  89. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +1 -1
  90. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +6 -2
  91. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +36 -20
  92. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +41 -1
  93. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +157 -102
  94. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-form-reset.ts +18 -2
  95. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +1 -2
  96. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.test.ts +84 -1
  97. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +5 -1
  98. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +5 -3
  99. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +300 -172
  100. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +959 -340
  101. package/src/components/ui/tu-do/shared/task-sound-effects.test.ts +189 -0
  102. package/src/components/ui/tu-do/shared/task-sound-effects.tsx +468 -0
  103. package/src/hooks/__tests__/use-task-actions.test.tsx +61 -0
  104. package/src/hooks/use-task-actions.ts +45 -0
  105. package/src/hooks/useBoardRealtime.ts +54 -1
  106. package/src/hooks/useBoardRealtimeEventHandler.ts +169 -4
  107. 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
- for (const task of merged.values()) {
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
- if (queryClient.getQueryData<Task[]>(['tasks-full', boardId])) {
82
- queryClient.setQueryData(['tasks-full', boardId], updater);
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');