@tuturuuu/ui 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +5 -5
  3. package/src/components/ui/custom/settings/task-settings.tsx +76 -0
  4. package/src/components/ui/custom/settings/task-sound-settings.test.tsx +126 -0
  5. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +29 -18
  6. package/src/components/ui/finance/transactions/transaction-card.tsx +75 -43
  7. package/src/components/ui/finance/transactions/transfer-merge.test.ts +90 -0
  8. package/src/components/ui/finance/transactions/transfer-merge.ts +52 -0
  9. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +172 -0
  10. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-amount.tsx +32 -0
  11. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-delete-dialog.tsx +50 -0
  12. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-dialog.tsx +138 -0
  13. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +196 -0
  14. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +201 -0
  15. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +277 -0
  16. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +189 -0
  17. package/src/components/ui/finance/wallets/query-invalidation.ts +2 -0
  18. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +7 -3
  19. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +10 -0
  20. package/src/components/ui/finance/wallets/wallets-page.test.tsx +7 -0
  21. package/src/components/ui/finance/wallets/wallets-page.tsx +21 -5
  22. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +14 -0
  23. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +29 -0
  24. package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +11 -0
  25. package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +10 -0
  26. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +141 -0
  27. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +208 -0
  28. package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +26 -0
  29. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +36 -20
  30. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +24 -0
  31. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +18 -3
  32. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.test.ts +84 -1
  33. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +5 -1
  34. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +300 -172
  35. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +411 -323
  36. package/src/components/ui/tu-do/shared/task-sound-effects.test.ts +189 -0
  37. package/src/components/ui/tu-do/shared/task-sound-effects.tsx +468 -0
  38. package/src/hooks/__tests__/use-task-actions.test.tsx +61 -0
  39. package/src/hooks/use-task-actions.ts +45 -0
@@ -0,0 +1,208 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Check,
5
+ Copy,
6
+ FileEdit,
7
+ ListTodo,
8
+ Loader2,
9
+ Maximize2,
10
+ X,
11
+ } from '@tuturuuu/icons';
12
+ import { Button } from '@tuturuuu/ui/button';
13
+ import { DialogDescription, DialogTitle } from '@tuturuuu/ui/dialog';
14
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
15
+ import { cn } from '@tuturuuu/utils/format';
16
+ import { useTranslations } from 'next-intl';
17
+ import type { ReactNode } from 'react';
18
+ import { QuickSettingsPopover } from './quick-settings-popover';
19
+
20
+ interface CompactTaskCreatePopoverProps {
21
+ title: string;
22
+ description?: ReactNode;
23
+ icon?: ReactNode;
24
+ iconBgClass?: string;
25
+ iconRingClass?: string;
26
+ titleInput: ReactNode;
27
+ propertyControls: ReactNode;
28
+ saveAsDraft: boolean;
29
+ createMultiple: boolean;
30
+ canSave: boolean;
31
+ isLoading: boolean;
32
+ isPersonalWorkspace?: boolean;
33
+ onSaveAsDraftChange: (value: boolean) => void;
34
+ onCreateMultipleChange: (value: boolean) => void;
35
+ onClose: () => void;
36
+ onFullscreen: () => void;
37
+ onSave: () => void;
38
+ }
39
+
40
+ function CompactIconButton({
41
+ active = false,
42
+ children,
43
+ label,
44
+ onClick,
45
+ }: {
46
+ active?: boolean;
47
+ children: ReactNode;
48
+ label: ReactNode;
49
+ onClick: () => void;
50
+ }) {
51
+ return (
52
+ <Tooltip>
53
+ <TooltipTrigger asChild>
54
+ <Button
55
+ type="button"
56
+ variant={active ? 'secondary' : 'ghost'}
57
+ size="icon"
58
+ aria-label={typeof label === 'string' ? label : undefined}
59
+ aria-pressed={active}
60
+ className={cn(
61
+ 'h-8 w-8 text-muted-foreground hover:text-foreground',
62
+ active && 'text-foreground'
63
+ )}
64
+ onClick={onClick}
65
+ >
66
+ {children}
67
+ </Button>
68
+ </TooltipTrigger>
69
+ <TooltipContent side="bottom">{label}</TooltipContent>
70
+ </Tooltip>
71
+ );
72
+ }
73
+
74
+ export function CompactTaskCreatePopover({
75
+ title,
76
+ description,
77
+ icon,
78
+ iconBgClass = 'bg-dynamic-orange/10',
79
+ iconRingClass = 'ring-dynamic-orange/20',
80
+ titleInput,
81
+ propertyControls,
82
+ saveAsDraft,
83
+ createMultiple,
84
+ canSave,
85
+ isLoading,
86
+ isPersonalWorkspace,
87
+ onSaveAsDraftChange,
88
+ onCreateMultipleChange,
89
+ onClose,
90
+ onFullscreen,
91
+ onSave,
92
+ }: CompactTaskCreatePopoverProps) {
93
+ const t = useTranslations();
94
+ const saveLabel = saveAsDraft
95
+ ? t('task-drafts.save_as_draft')
96
+ : t('ws-task-boards.dialog.create_task');
97
+
98
+ return (
99
+ <div
100
+ data-testid="compact-task-create-popover"
101
+ className="flex max-h-[calc(100vh-2rem)] min-h-0 flex-col overflow-hidden rounded-lg bg-background"
102
+ >
103
+ <div className="flex items-start justify-between gap-3 border-b px-4 py-3">
104
+ <div className="flex min-w-0 items-start gap-2.5">
105
+ <div
106
+ className={cn(
107
+ 'mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ring-1',
108
+ iconBgClass,
109
+ iconRingClass
110
+ )}
111
+ >
112
+ {icon ?? <ListTodo className="h-4 w-4 text-dynamic-orange" />}
113
+ </div>
114
+ <div className="min-w-0 space-y-0.5">
115
+ <DialogTitle className="truncate font-semibold text-base">
116
+ {title}
117
+ </DialogTitle>
118
+ {description && (
119
+ <DialogDescription className="truncate text-muted-foreground text-xs">
120
+ {description}
121
+ </DialogDescription>
122
+ )}
123
+ </div>
124
+ </div>
125
+ <div className="flex shrink-0 items-center gap-1">
126
+ <Tooltip>
127
+ <TooltipTrigger asChild>
128
+ <Button
129
+ type="button"
130
+ variant="ghost"
131
+ size="icon"
132
+ aria-label={t('ws-task-boards.dialog.open_fullscreen')}
133
+ className="h-8 w-8 text-muted-foreground hover:text-foreground"
134
+ onClick={onFullscreen}
135
+ >
136
+ <Maximize2 className="h-4 w-4" />
137
+ </Button>
138
+ </TooltipTrigger>
139
+ <TooltipContent side="bottom">
140
+ {t('ws-task-boards.dialog.open_fullscreen')}
141
+ </TooltipContent>
142
+ </Tooltip>
143
+ <Tooltip>
144
+ <TooltipTrigger asChild>
145
+ <Button
146
+ type="button"
147
+ variant="ghost"
148
+ size="icon"
149
+ aria-label={t('common.close')}
150
+ className="h-8 w-8 text-muted-foreground hover:text-foreground"
151
+ onClick={onClose}
152
+ >
153
+ <X className="h-4 w-4" />
154
+ </Button>
155
+ </TooltipTrigger>
156
+ <TooltipContent side="bottom">{t('common.close')}</TooltipContent>
157
+ </Tooltip>
158
+ </div>
159
+ </div>
160
+
161
+ <div className="min-h-0 space-y-3 overflow-y-auto px-4 py-3">
162
+ {titleInput}
163
+ <div className="flex flex-wrap items-center gap-1.5">
164
+ {propertyControls}
165
+ </div>
166
+ </div>
167
+
168
+ <div className="flex items-center justify-between gap-2 border-t bg-muted/20 px-4 py-3">
169
+ <div className="flex items-center gap-1">
170
+ <CompactIconButton
171
+ active={saveAsDraft}
172
+ label={t('task-drafts.save_as_draft')}
173
+ onClick={() => onSaveAsDraftChange(!saveAsDraft)}
174
+ >
175
+ <FileEdit className="h-4 w-4" />
176
+ </CompactIconButton>
177
+ <CompactIconButton
178
+ active={createMultiple}
179
+ label={t('ws-task-boards.dialog.create_multiple')}
180
+ onClick={() => onCreateMultipleChange(!createMultiple)}
181
+ >
182
+ <Copy className="h-4 w-4" />
183
+ </CompactIconButton>
184
+ <QuickSettingsPopover isPersonalWorkspace={isPersonalWorkspace} />
185
+ </div>
186
+ <Button
187
+ type="button"
188
+ size="sm"
189
+ disabled={!canSave}
190
+ onClick={onSave}
191
+ className="min-w-28"
192
+ >
193
+ {isLoading ? (
194
+ <>
195
+ <Loader2 className="h-4 w-4 animate-spin" />
196
+ {t('ws-task-boards.dialog.saving')}
197
+ </>
198
+ ) : (
199
+ <>
200
+ <Check className="h-4 w-4" />
201
+ {saveLabel}
202
+ </>
203
+ )}
204
+ </Button>
205
+ </div>
206
+ </div>
207
+ );
208
+ }
@@ -3,11 +3,13 @@
3
3
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
4
  import { Settings } from '@tuturuuu/icons';
5
5
  import { Button } from '@tuturuuu/ui/button';
6
+ import { useUserBooleanConfig } from '@tuturuuu/ui/hooks/use-user-config';
6
7
  import { Label } from '@tuturuuu/ui/label';
7
8
  import { Popover, PopoverContent, PopoverTrigger } from '@tuturuuu/ui/popover';
8
9
  import { Switch } from '@tuturuuu/ui/switch';
9
10
  import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
10
11
  import { useTranslations } from 'next-intl';
12
+ import { TASK_SOUND_EFFECTS_ENABLED_CONFIG_ID } from '../../task-sound-effects';
11
13
 
12
14
  interface TaskSettingsData {
13
15
  task_auto_assign_to_self: boolean;
@@ -38,6 +40,12 @@ export function QuickSettingsPopover({
38
40
  const t = useTranslations('settings.tasks');
39
41
  const tCommon = useTranslations('common');
40
42
  const queryClient = useQueryClient();
43
+ const {
44
+ value: soundEffectsEnabled,
45
+ setValue: setSoundEffectsEnabled,
46
+ isLoading: soundEffectsEnabledLoading,
47
+ isPending: soundEffectsEnabledPending,
48
+ } = useUserBooleanConfig(TASK_SOUND_EFFECTS_ENABLED_CONFIG_ID, true);
41
49
 
42
50
  const { data: settings, isLoading } = useQuery({
43
51
  queryKey: ['user-task-settings'],
@@ -156,6 +164,24 @@ export function QuickSettingsPopover({
156
164
  disabled={isLoading || updateSettings.isPending}
157
165
  />
158
166
  </div>
167
+ <div className="flex items-center justify-between gap-2">
168
+ <div className="space-y-0.5">
169
+ <Label htmlFor="task-sound-effects" className="text-sm">
170
+ {t('sound_effects')}
171
+ </Label>
172
+ <p className="text-muted-foreground text-xs">
173
+ {t('sound_effects_description')}
174
+ </p>
175
+ </div>
176
+ <Switch
177
+ id="task-sound-effects"
178
+ checked={soundEffectsEnabled}
179
+ onCheckedChange={setSoundEffectsEnabled}
180
+ disabled={
181
+ soundEffectsEnabledLoading || soundEffectsEnabledPending
182
+ }
183
+ />
184
+ </div>
159
185
  </div>
160
186
  </div>
161
187
  </PopoverContent>
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type { TaskList } from '@tuturuuu/types/primitives/TaskList';
4
4
  import { Popover, PopoverContent, PopoverTrigger } from '@tuturuuu/ui/popover';
5
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
5
6
  import { cn } from '@tuturuuu/utils/format';
6
7
  import { useTranslations } from 'next-intl';
7
8
  import { useMemo, useState } from 'react';
@@ -19,6 +20,7 @@ interface TaskListSelectorProps {
19
20
  selectedListId: string;
20
21
  availableLists: TaskList[];
21
22
  disabled?: boolean;
23
+ compact?: boolean;
22
24
  onListChange: (listId: string) => void;
23
25
  }
24
26
 
@@ -28,6 +30,7 @@ export function TaskListSelector({
28
30
  selectedListId,
29
31
  availableLists,
30
32
  disabled = false,
33
+ compact = false,
31
34
  onListChange,
32
35
  }: TaskListSelectorProps) {
33
36
  const t = useTranslations();
@@ -65,30 +68,43 @@ export function TaskListSelector({
65
68
 
66
69
  const TriggerIcon = getTaskListTriggerIcon(selectedList);
67
70
  const triggerSurfaceClass = getTaskListTriggerSurfaceClass(selectedList);
71
+ const triggerLabel = selectedList
72
+ ? translateTaskListNameForDisplay(selectedList.name, nameLabels)
73
+ : t('ws-task-boards.dialog.field.list');
74
+ const triggerButton = (
75
+ <button
76
+ type="button"
77
+ disabled={disabled}
78
+ aria-label={compact ? triggerLabel : undefined}
79
+ className={cn(
80
+ 'inline-flex shrink-0 items-center border font-medium text-xs transition-colors',
81
+ compact
82
+ ? 'h-9 w-9 justify-center rounded-md p-0'
83
+ : 'h-8 gap-1.5 rounded-lg px-3',
84
+ selectedList && triggerSurfaceClass
85
+ ? triggerSurfaceClass
86
+ : 'border-border bg-background text-muted-foreground hover:border-primary/30 hover:bg-muted hover:text-foreground',
87
+ disabled && 'cursor-not-allowed opacity-50'
88
+ )}
89
+ >
90
+ <TriggerIcon className="h-3.5 w-3.5 shrink-0" />
91
+ <span className={compact ? 'sr-only' : undefined}>{triggerLabel}</span>
92
+ </button>
93
+ );
68
94
 
69
95
  return (
70
96
  <>
71
97
  <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
72
- <PopoverTrigger asChild>
73
- <button
74
- type="button"
75
- disabled={disabled}
76
- className={cn(
77
- 'inline-flex h-8 shrink-0 items-center gap-1.5 rounded-lg border px-3 font-medium text-xs transition-colors',
78
- selectedList && triggerSurfaceClass
79
- ? triggerSurfaceClass
80
- : 'border-border bg-background text-muted-foreground hover:border-primary/30 hover:bg-muted hover:text-foreground',
81
- disabled && 'cursor-not-allowed opacity-50'
82
- )}
83
- >
84
- <TriggerIcon className="h-3.5 w-3.5 shrink-0" />
85
- <span>
86
- {selectedList
87
- ? translateTaskListNameForDisplay(selectedList.name, nameLabels)
88
- : t('ws-task-boards.dialog.field.list')}
89
- </span>
90
- </button>
91
- </PopoverTrigger>
98
+ {compact ? (
99
+ <Tooltip>
100
+ <TooltipTrigger asChild>
101
+ <PopoverTrigger asChild>{triggerButton}</PopoverTrigger>
102
+ </TooltipTrigger>
103
+ <TooltipContent side="bottom">{triggerLabel}</TooltipContent>
104
+ </Tooltip>
105
+ ) : (
106
+ <PopoverTrigger asChild>{triggerButton}</PopoverTrigger>
107
+ )}
92
108
  <PopoverContent align="start" className="w-80 p-0">
93
109
  <TaskListPickerPanel
94
110
  selectedListId={selectedListId}
@@ -13,9 +13,13 @@ vi.mock('next-intl', () => ({
13
13
  function renderTaskNameInput({
14
14
  isCreateMode = true,
15
15
  targetCursor = 12,
16
+ variant = 'fullscreen',
17
+ onSubmit,
16
18
  }: {
17
19
  isCreateMode?: boolean;
18
20
  targetCursor?: number | null;
21
+ variant?: 'fullscreen' | 'compact';
22
+ onSubmit?: () => void;
19
23
  } = {}) {
20
24
  const editorWrapper = document.createElement('div');
21
25
  const editorElement = document.createElement('div');
@@ -37,6 +41,8 @@ function renderTaskNameInput({
37
41
  setName: vi.fn(),
38
42
  updateName: vi.fn(),
39
43
  flushNameUpdate: vi.fn(),
44
+ variant,
45
+ onSubmit,
40
46
  };
41
47
 
42
48
  render(<TaskNameInput {...props} />);
@@ -137,4 +143,22 @@ describe('TaskNameInput', () => {
137
143
  window.removeEventListener('keydown', windowKeyDown);
138
144
  }
139
145
  });
146
+
147
+ it('submits from compact mode without focusing the description editor', () => {
148
+ const onSubmit = vi.fn();
149
+ const { focusEditor, targetEditorCursorRef, input } = renderTaskNameInput({
150
+ onSubmit,
151
+ variant: 'compact',
152
+ });
153
+
154
+ const eventAllowed = fireEvent.keyDown(input, { key: 'Enter' });
155
+
156
+ expect(eventAllowed).toBe(false);
157
+ expect(onSubmit).toHaveBeenCalledTimes(1);
158
+ expect(targetEditorCursorRef.current).toBe(12);
159
+
160
+ vi.runOnlyPendingTimers();
161
+
162
+ expect(focusEditor).not.toHaveBeenCalled();
163
+ });
140
164
  });
@@ -18,6 +18,8 @@ interface TaskNameInputProps {
18
18
  updateName: (value: string) => void;
19
19
  flushNameUpdate: () => void;
20
20
  disabled?: boolean;
21
+ variant?: 'fullscreen' | 'compact';
22
+ onSubmit?: () => void;
21
23
  }
22
24
 
23
25
  export function TaskNameInput({
@@ -31,10 +33,15 @@ export function TaskNameInput({
31
33
  updateName,
32
34
  flushNameUpdate,
33
35
  disabled,
36
+ variant = 'fullscreen',
37
+ onSubmit,
34
38
  }: TaskNameInputProps) {
35
39
  const t = useTranslations('ws-task-boards.dialog');
40
+ const isCompact = variant === 'compact';
36
41
 
37
42
  const focusDescriptionEditor = () => {
43
+ if (isCompact) return;
44
+
38
45
  targetEditorCursorRef.current = null;
39
46
 
40
47
  setTimeout(() => {
@@ -115,10 +122,14 @@ export function TaskNameInput({
115
122
  if (!isCreateMode && e.currentTarget.value.trim()) {
116
123
  flushNameUpdate();
117
124
  }
125
+ if (isCompact) {
126
+ onSubmit?.();
127
+ return;
128
+ }
118
129
  focusDescriptionEditor();
119
130
  }
120
131
 
121
- if (e.key === 'ArrowDown') {
132
+ if (!isCompact && e.key === 'ArrowDown') {
122
133
  e.preventDefault();
123
134
  const input = e.currentTarget;
124
135
  const cursorPosition = input.selectionStart ?? 0;
@@ -137,7 +148,7 @@ export function TaskNameInput({
137
148
  }
138
149
 
139
150
  // Right arrow at end of title moves to description
140
- if (e.key === 'ArrowRight') {
151
+ if (!isCompact && e.key === 'ArrowRight') {
141
152
  const input = e.currentTarget;
142
153
  const cursorPosition = input.selectionStart ?? 0;
143
154
  const textLength = input.value.length;
@@ -155,7 +166,11 @@ export function TaskNameInput({
155
166
  }
156
167
  }}
157
168
  placeholder={t('task_name_placeholder')}
158
- className="h-auto border-0 bg-transparent px-4 pt-4 pb-2 font-bold text-2xl text-foreground leading-tight tracking-tight shadow-none transition-colors placeholder:text-muted-foreground/30 focus-visible:outline-0 focus-visible:ring-0 disabled:opacity-100 md:px-8 md:pt-4 md:pb-2 md:text-2xl"
169
+ className={
170
+ isCompact
171
+ ? 'h-11 border-0 bg-transparent px-0 font-semibold text-base text-foreground leading-tight shadow-none transition-colors placeholder:text-muted-foreground/40 focus-visible:outline-0 focus-visible:ring-0 disabled:opacity-100 md:text-lg'
172
+ : 'h-auto border-0 bg-transparent px-4 pt-4 pb-2 font-bold text-2xl text-foreground leading-tight tracking-tight shadow-none transition-colors placeholder:text-muted-foreground/30 focus-visible:outline-0 focus-visible:ring-0 disabled:opacity-100 md:px-8 md:pt-4 md:pb-2 md:text-2xl'
173
+ }
159
174
  autoFocus
160
175
  />
161
176
  </div>
@@ -1,8 +1,14 @@
1
1
  import { QueryClient } from '@tanstack/react-query';
2
2
  import { describe, expect, it, vi } from 'vitest';
3
3
 
4
- const { mockCreateWorkspaceTaskRelationship } = vi.hoisted(() => ({
4
+ const {
5
+ mockCreateTask,
6
+ mockCreateWorkspaceTaskRelationship,
7
+ mockDispatchTaskSoundCue,
8
+ } = vi.hoisted(() => ({
9
+ mockCreateTask: vi.fn(),
5
10
  mockCreateWorkspaceTaskRelationship: vi.fn(),
11
+ mockDispatchTaskSoundCue: vi.fn(),
6
12
  }));
7
13
 
8
14
  vi.mock('@tuturuuu/internal-api/tasks', async () => {
@@ -26,8 +32,17 @@ vi.mock('@tuturuuu/supabase/next/client', () => ({
26
32
  })),
27
33
  }));
28
34
 
35
+ vi.mock('@tuturuuu/utils/task-helper', () => ({
36
+ createTask: mockCreateTask,
37
+ }));
38
+
39
+ vi.mock('../../task-sound-effects', () => ({
40
+ dispatchTaskSoundCue: mockDispatchTaskSoundCue,
41
+ }));
42
+
29
43
  import {
30
44
  applyPendingRelationshipSummary,
45
+ handleCreateTask,
31
46
  persistPendingTaskRelationships,
32
47
  } from './use-task-save';
33
48
 
@@ -204,3 +219,71 @@ describe('applyPendingRelationshipSummary', () => {
204
219
  ]);
205
220
  });
206
221
  });
222
+
223
+ describe('handleCreateTask', () => {
224
+ it('dispatches the create sound cue once after successful task creation', async () => {
225
+ const queryClient = new QueryClient({
226
+ defaultOptions: { queries: { retry: false } },
227
+ });
228
+ const toast = vi.fn();
229
+
230
+ mockCreateTask.mockResolvedValueOnce({
231
+ id: 'task-new',
232
+ name: 'New task',
233
+ list_id: 'list-1',
234
+ created_at: '2026-01-01T00:00:00Z',
235
+ });
236
+
237
+ await handleCreateTask({
238
+ autoSchedule: false,
239
+ boardId: 'board-1',
240
+ broadcast: null,
241
+ calendarHours: null,
242
+ createMultiple: false,
243
+ descriptionString: null,
244
+ descriptionYjsState: null,
245
+ endDate: undefined,
246
+ estimationPoints: null,
247
+ isPersonalWorkspace: false,
248
+ isSplittable: false,
249
+ maxSplitDurationMinutes: null,
250
+ minSplitDurationMinutes: null,
251
+ name: 'New task',
252
+ onClose: vi.fn(),
253
+ onUpdate: vi.fn(),
254
+ priority: null,
255
+ queryClient,
256
+ selectedAssignees: [],
257
+ selectedLabels: [],
258
+ selectedListId: 'list-1',
259
+ selectedProjects: [],
260
+ setDescription: vi.fn(),
261
+ setEndDate: vi.fn(),
262
+ setEstimationPoints: vi.fn(),
263
+ setIsLoading: vi.fn(),
264
+ setIsSaving: vi.fn(),
265
+ setName: vi.fn(),
266
+ setPriority: vi.fn(),
267
+ setSelectedAssignees: vi.fn(),
268
+ setSelectedLabels: vi.fn(),
269
+ setSelectedProjects: vi.fn(),
270
+ setStartDate: vi.fn(),
271
+ startDate: undefined,
272
+ toast,
273
+ totalDuration: null,
274
+ user: { id: 'user-1' },
275
+ userTaskSettings: { task_auto_assign_to_self: false },
276
+ wsId: 'ws-1',
277
+ });
278
+
279
+ expect(mockCreateTask).toHaveBeenCalledWith(
280
+ 'ws-1',
281
+ 'list-1',
282
+ expect.objectContaining({
283
+ name: 'New task',
284
+ })
285
+ );
286
+ expect(mockDispatchTaskSoundCue).toHaveBeenCalledTimes(1);
287
+ expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith('create');
288
+ });
289
+ });
@@ -23,6 +23,7 @@ import {
23
23
  getActiveBroadcast,
24
24
  useBoardBroadcast,
25
25
  } from '../../board-broadcast-context';
26
+ import { dispatchTaskSoundCue } from '../../task-sound-effects';
26
27
  import type {
27
28
  PendingRelationship,
28
29
  PendingTaskRelationships,
@@ -800,7 +801,7 @@ async function handleSaveAsDraft({
800
801
  }
801
802
 
802
803
  // Helper function for creating tasks
803
- async function handleCreateTask({
804
+ export async function handleCreateTask({
804
805
  wsId,
805
806
  name,
806
807
  descriptionString,
@@ -1104,6 +1105,7 @@ async function handleCreateTask({
1104
1105
  ? 'New sub-task added.'
1105
1106
  : 'New task added.',
1106
1107
  });
1108
+ dispatchTaskSoundCue('create');
1107
1109
  onUpdate();
1108
1110
 
1109
1111
  if (createMultiple) {
@@ -1394,6 +1396,7 @@ async function handleUpdateTask({
1394
1396
  title: 'Task updated',
1395
1397
  description: 'The task has been successfully updated.',
1396
1398
  });
1399
+ dispatchTaskSoundCue('update');
1397
1400
  onUpdate();
1398
1401
  onClose();
1399
1402
  },
@@ -1431,6 +1434,7 @@ async function handleUpdateTask({
1431
1434
  title: 'Task updated',
1432
1435
  description: 'The task has been successfully updated.',
1433
1436
  });
1437
+ dispatchTaskSoundCue('update');
1434
1438
  onUpdate();
1435
1439
  onClose();
1436
1440
  } catch (error) {